From 1c41c30310dff725bf5a0c7531dce9776fe3e602 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Wed, 15 Apr 2026 12:02:01 -0700 Subject: [PATCH] =?UTF-8?q?fix(workspace-template):=20#220=20=E2=80=94=20s?= =?UTF-8?q?end=20auth=5Fheaders()=20on=20initial=5Fprompt=20+=20idle=20loo?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #220. #215 added auth_headers() to /registry/register but missed two other self-post paths from the same workspace container: 1. initial_prompt (_do_send_sync) — fires once on first boot after the A2A server is ready. Posts to /workspaces/:id/a2a via the platform proxy. Missing headers meant the initial prompt got silently dropped as 401 on any token-enrolled workspace. 2. idle loop (_post_sync) — fires every idle_interval_seconds while the workspace has no active task (#205 pattern). Same proxy path, same missing headers, same silent 401 in multi-tenant mode. Both now build headers as {"Content-Type": "application/json", **auth_headers()} auth_headers() returns {"Authorization": "Bearer "} when /auth-token.txt exists, empty dict otherwise (first boot before register issues the token). The existing lazy-bootstrap fail-open on the platform side covers the empty-dict case. Co-Authored-By: Claude Opus 4.6 (1M context) --- workspace-template/main.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/workspace-template/main.py b/workspace-template/main.py index 4d23e8aa..f9b7e459 100644 --- a/workspace-template/main.py +++ b/workspace-template/main.py @@ -347,6 +347,12 @@ async def main(): # pragma: no cover }, }).encode() + # #220: include platform bearer token so the request isn't + # silently rejected once any workspace has a live token on + # file. Without this, initial_prompt 401s in multi-tenant + # mode exactly like /registry/register did in #215. + headers = {"Content-Type": "application/json", **auth_headers()} + # Retry with backoff — the platform proxy may not be able to # reach us yet (container networking takes a moment to settle). max_retries = 5 @@ -355,7 +361,7 @@ async def main(): # pragma: no cover req = urllib.request.Request( f"{platform_url}/workspaces/{workspace_id}/a2a", data=payload, - headers={"Content-Type": "application/json"}, + headers=headers, ) with urllib.request.urlopen(req, timeout=600) as resp: resp.read() @@ -435,11 +441,14 @@ async def main(): # pragma: no cover def _post_sync(): # Returns (status_code, error_type) so the caller logs the # actual outcome instead of a bare "post failed" line. + # #220: include auth_headers() on every idle fire. Without + # this, the idle loop 401s in multi-tenant mode. + headers = {"Content-Type": "application/json", **auth_headers()} try: req = _urlreq.Request( f"{platform_url}/workspaces/{workspace_id}/a2a", data=payload, - headers={"Content-Type": "application/json"}, + headers=headers, ) with _urlreq.urlopen(req, timeout=IDLE_FIRE_TIMEOUT_SECONDS) as resp: resp.read()