From b99497cd3fc359941fdf0598a15528eb5bd7b8c3 Mon Sep 17 00:00:00 2001 From: Dev Lead Agent Date: Tue, 14 Apr 2026 13:23:44 +0000 Subject: [PATCH] fix(security): complete Phase 30.6 auth headers in a2a_client get_peers and discover_peer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_peers() was sending no auth headers to /registry/:id/peers — this would return 401 for every workspace agent after PR #31 (WorkspaceAuth middleware) deploys, breaking peer discovery entirely. discover_peer() had X-Workspace-ID but was missing the bearer token, also required by Phase 30.6 for /registry/discover/:id. Both functions now send {"X-Workspace-ID": WORKSPACE_ID, **auth_headers()}. get_workspace_info() was already correct (auth_headers() present since PR #39). Adds test_request_sends_workspace_id_header to TestGetPeers; hardens the discover_peer header assertion to use presence-check rather than exact equality. Co-Authored-By: Claude Sonnet 4.6 --- workspace-template/a2a_client.py | 7 +++++-- workspace-template/tests/test_a2a_client.py | 22 +++++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/workspace-template/a2a_client.py b/workspace-template/a2a_client.py index a5282991..15038bce 100644 --- a/workspace-template/a2a_client.py +++ b/workspace-template/a2a_client.py @@ -31,7 +31,7 @@ async def discover_peer(target_id: str) -> dict | None: try: resp = await client.get( f"{PLATFORM_URL}/registry/discover/{target_id}", - headers={"X-Workspace-ID": WORKSPACE_ID}, + headers={"X-Workspace-ID": WORKSPACE_ID, **auth_headers()}, ) if resp.status_code == 200: return resp.json() @@ -84,7 +84,10 @@ async def get_peers() -> list[dict]: """Get this workspace's peers from the platform registry.""" async with httpx.AsyncClient(timeout=10.0) as client: try: - resp = await client.get(f"{PLATFORM_URL}/registry/{WORKSPACE_ID}/peers") + resp = await client.get( + f"{PLATFORM_URL}/registry/{WORKSPACE_ID}/peers", + headers={"X-Workspace-ID": WORKSPACE_ID, **auth_headers()}, + ) if resp.status_code == 200: return resp.json() return [] diff --git a/workspace-template/tests/test_a2a_client.py b/workspace-template/tests/test_a2a_client.py index 7bc824bf..fbae0ea0 100644 --- a/workspace-template/tests/test_a2a_client.py +++ b/workspace-template/tests/test_a2a_client.py @@ -119,14 +119,11 @@ class TestDiscoverPeer: await a2a_client.discover_peer("ws-xyz") mock_client.get.assert_called_once() - call_args = mock_client.get.call_args - url = call_args[0][0] if call_args[0] else call_args[1].get("url") or call_args[0][0] - # The first positional arg is the URL positional_url = mock_client.get.call_args.args[0] assert "ws-xyz" in positional_url - assert mock_client.get.call_args.kwargs.get("headers") == { - "X-Workspace-ID": a2a_client.WORKSPACE_ID - } + # X-Workspace-ID must be present; bearer token also merged in when available + headers_sent = mock_client.get.call_args.kwargs.get("headers", {}) + assert headers_sent.get("X-Workspace-ID") == a2a_client.WORKSPACE_ID # --------------------------------------------------------------------------- @@ -312,6 +309,19 @@ class TestGetPeers: url = mock_client.get.call_args.args[0] assert "peers" in url + async def test_request_sends_workspace_id_header(self): + """GET /registry/:id/peers must send X-Workspace-ID header (Phase 30.6).""" + import a2a_client + + resp = _make_response(200, []) + mock_client = _make_mock_client(get_resp=resp) + + with patch("a2a_client.httpx.AsyncClient", return_value=mock_client): + await a2a_client.get_peers() + + headers_sent = mock_client.get.call_args.kwargs.get("headers", {}) + assert headers_sent.get("X-Workspace-ID") == a2a_client.WORKSPACE_ID + # --------------------------------------------------------------------------- # get_workspace_info