From d8c670a687410b23b47da201a892c983d42b40f6 Mon Sep 17 00:00:00 2001 From: Backend Engineer Date: Tue, 14 Apr 2026 08:37:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(security):=20N1=20=E2=80=94=20add=20auth=20?= =?UTF-8?q?headers=20to=20all=20platform=20calls=20in=20Python=20callers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IMPACT WITHOUT THIS FIX: deploying PR #31 (WorkspaceAuth middleware on /workspaces/*) without this patch causes EVERY delegation cycle to silently break — the heartbeat poll returns 401, the self-message A2A POST returns 401, agents never wake up after task completion, and memory consolidation stops. The entire multi-agent coordination system degrades to single-shot interactions with no result delivery. Changes (all using the existing platform_auth.auth_headers() pattern already used for POST /registry/heartbeat): heartbeat.py — 5 calls fixed: - GET /workspaces/:id/delegations (delegation poll) - GET /workspaces/:id (self workspace info for parent lookup) - GET /workspaces/{parent_id} (parent workspace name lookup) - POST /workspaces/:id/a2a (self-message to wake agent on results) - POST /workspaces/:id/notify (canvas delegation result notification) Also moved `from platform_auth import auth_headers` from inline (per-call) to module-level import so _check_delegations() can use it without re-importing. consolidation.py — 4 calls fixed: - GET /workspaces/:id/memories (fetch memories for consolidation) - POST /workspaces/:id/memories (write consolidated summary — agent path) - DELETE /workspaces/:id/memories/:id (delete original memories post-consolidation) - POST /workspaces/:id/memories (write consolidated summary — fallback path) a2a_client.py — 1 call fixed: - GET /workspaces/:id (get_workspace_info()) ⚠️ DEPLOYMENT NOTE: This PR MUST be merged and deployed at the same time as PR #31 (WorkspaceAuth middleware). Deploying #31 without this fix will immediately break all delegation result delivery. Co-Authored-By: Claude Sonnet 4.6 --- workspace-template/a2a_client.py | 7 ++++++- workspace-template/consolidation.py | 8 +++++++- workspace-template/heartbeat.py | 14 ++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/workspace-template/a2a_client.py b/workspace-template/a2a_client.py index ded66fc2..a5282991 100644 --- a/workspace-template/a2a_client.py +++ b/workspace-template/a2a_client.py @@ -10,6 +10,8 @@ import uuid import httpx +from platform_auth import auth_headers + logger = logging.getLogger(__name__) WORKSPACE_ID = os.environ.get("WORKSPACE_ID", "") @@ -94,7 +96,10 @@ async def get_workspace_info() -> dict: """Get this workspace's info from the platform.""" async with httpx.AsyncClient(timeout=10.0) as client: try: - resp = await client.get(f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}") + resp = await client.get( + f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}", + headers=auth_headers(), + ) if resp.status_code == 200: return resp.json() return {"error": "not found"} diff --git a/workspace-template/consolidation.py b/workspace-template/consolidation.py index 63f384b5..38e4b58f 100644 --- a/workspace-template/consolidation.py +++ b/workspace-template/consolidation.py @@ -14,6 +14,8 @@ import os import httpx +from platform_auth import auth_headers + logger = logging.getLogger(__name__) PLATFORM_URL = os.environ.get("PLATFORM_URL", "http://platform:8080") @@ -53,6 +55,7 @@ class ConsolidationLoop: resp = await client.get( f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}/memories", params={"scope": "LOCAL"}, + headers=auth_headers(), ) if resp.status_code != 200: return @@ -95,12 +98,14 @@ class ConsolidationLoop: resp = await client.post( f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}/memories", json={"content": f"[Consolidated] {summary}", "scope": "TEAM"}, + headers=auth_headers(), ) if resp.status_code in (200, 201): # Safe to delete originals — consolidated version is saved for m in memories: await client.delete( - f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}/memories/{m['id']}" + f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}/memories/{m['id']}", + headers=auth_headers(), ) logger.info("Consolidated %d memories into team knowledge", len(memories)) else: @@ -118,6 +123,7 @@ class ConsolidationLoop: await client.post( f"{PLATFORM_URL}/workspaces/{WORKSPACE_ID}/memories", json={"content": f"[Consolidated] {combined}", "scope": "TEAM"}, + headers=auth_headers(), ) logger.info("Consolidated %d memories via concatenation fallback", len(memories)) diff --git a/workspace-template/heartbeat.py b/workspace-template/heartbeat.py index 64f5b827..a67bec7b 100644 --- a/workspace-template/heartbeat.py +++ b/workspace-template/heartbeat.py @@ -17,6 +17,8 @@ from pathlib import Path import httpx +from platform_auth import auth_headers + logger = logging.getLogger(__name__) HEARTBEAT_INTERVAL = 30 # seconds @@ -83,7 +85,6 @@ class HeartbeatLoop: while True: # 1. Send heartbeat (Phase 30.1: include auth header if token known) try: - from platform_auth import auth_headers await client.post( f"{self.platform_url}/registry/heartbeat", json={ @@ -135,7 +136,8 @@ class HeartbeatLoop: """Check for completed delegations and store results for the agent.""" try: resp = await client.get( - f"{self.platform_url}/workspaces/{self.workspace_id}/delegations" + f"{self.platform_url}/workspaces/{self.workspace_id}/delegations", + headers=auth_headers(), ) if resp.status_code != 200: return @@ -208,13 +210,15 @@ class HeartbeatLoop: if self._parent_name is None: try: parent_resp = await client.get( - f"{self.platform_url}/workspaces/{self.workspace_id}" + f"{self.platform_url}/workspaces/{self.workspace_id}", + headers=auth_headers(), ) if parent_resp.status_code == 200: parent_id = parent_resp.json().get("parent_id", "") if parent_id: parent_info = await client.get( - f"{self.platform_url}/workspaces/{parent_id}" + f"{self.platform_url}/workspaces/{parent_id}", + headers=auth_headers(), ) if parent_info.status_code == 200: self._parent_name = parent_info.json().get("name", "") @@ -262,6 +266,7 @@ class HeartbeatLoop: }, }, }, + headers=auth_headers(), timeout=120.0, ) logger.info("Heartbeat: self-message sent to process delegation results") @@ -277,6 +282,7 @@ class HeartbeatLoop: await client.post( f"{self.platform_url}/workspaces/{self.workspace_id}/notify", json={"message": msg, "type": "delegation_result"}, + headers=auth_headers(), ) except Exception: pass