From ad726e51febb5518a1ee66ccd87dc73336ae038a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 11:57:19 +0000 Subject: [PATCH 1/2] fix(server): register /.well-known/agent.json alias route in create_a2a_server The 0.3 discovery alias was exempted from auth (auth.py _A2A_DISCOVERY_PATHS) but never registered as a Starlette route, causing buyer SDKs that probe the alias (including @adcp/sdk CLI auto-detection) to receive 404/503. Adds a second create_agent_card_routes call with card_url="/.well-known/agent.json" so both paths serve identical agent-card JSON without a redirect round-trip. Also tightens three tests that masked the bug by accepting 404 as valid, fixes the misleading auth comment, adds an inverse coverage check asserting all _A2A_DISCOVERY_PATHS entries are registered routes, and corrects the docs path. Closes #612 https://claude.ai/code/session_01JHq6DLPhFf2kQQpUkPtfZe --- docs/handler-authoring.md | 5 +++-- src/adcp/server/a2a_server.py | 12 ++++++++++-- src/adcp/server/auth.py | 3 ++- tests/test_a2a_server.py | 11 +++++++---- tests/test_serve_auth_both.py | 21 +++++++++++++++++++++ tests/test_unified_mcp_a2a.py | 17 +++++++---------- 6 files changed, 50 insertions(+), 19 deletions(-) diff --git a/docs/handler-authoring.md b/docs/handler-authoring.md index c77c9e4d0..1c7ec8d78 100644 --- a/docs/handler-authoring.md +++ b/docs/handler-authoring.md @@ -929,8 +929,9 @@ mock-B). ## A2A transport `serve(MyAgent(), transport="a2a")` wires the same handler through the -A2A protocol with auto-generated agent card (`/.well-known/agent.json`) -derived from the `ADCPHandler` methods your class overrides. +A2A protocol with auto-generated agent card (`/.well-known/agent-card.json`, +with the 0.3 alias `/.well-known/agent.json` also served for backwards +compatibility) derived from the `ADCPHandler` methods your class overrides. ### Durable task storage diff --git a/src/adcp/server/a2a_server.py b/src/adcp/server/a2a_server.py index e20dd0f07..2111731ea 100644 --- a/src/adcp/server/a2a_server.py +++ b/src/adcp/server/a2a_server.py @@ -917,8 +917,16 @@ def create_a2a_server( } if context_builder is not None: jsonrpc_kwargs["context_builder"] = context_builder - routes = list(create_agent_card_routes(agent_card=agent_card)) + list( - create_jsonrpc_routes(**jsonrpc_kwargs) + routes = ( + list(create_agent_card_routes(agent_card=agent_card)) + # 0.3 alias: buyer SDKs (e.g. @adcp/sdk) probe /.well-known/agent.json + # as a positive A2A signal. Same handler, no redirect round-trip. + + list( + create_agent_card_routes( + agent_card=agent_card, card_url="/.well-known/agent.json" + ) + ) + + list(create_jsonrpc_routes(**jsonrpc_kwargs)) ) app = Starlette(routes=routes) diff --git a/src/adcp/server/auth.py b/src/adcp/server/auth.py index 48e8ccb88..fd501f356 100644 --- a/src/adcp/server/auth.py +++ b/src/adcp/server/auth.py @@ -750,7 +750,8 @@ def resolved_a2a_bearer_prefix_required(self) -> bool: _A2A_DISCOVERY_PATHS: frozenset[str] = frozenset( { _A2A_AGENT_CARD_PATH, # 1.0 canonical: ``/.well-known/agent-card.json``. - "/.well-known/agent.json", # Legacy 0.3 alias retained by enable_v0_3_compat=True. + # Legacy 0.3 alias — route registered explicitly in create_a2a_server. + "/.well-known/agent.json", } ) diff --git a/tests/test_a2a_server.py b/tests/test_a2a_server.py index 7d19bce8b..bc3205a0c 100644 --- a/tests/test_a2a_server.py +++ b/tests/test_a2a_server.py @@ -345,10 +345,13 @@ def test_create_a2a_server_creates_starlette_app(): # Starlette app has .routes assert hasattr(app, "routes") route_paths = [r.path for r in app.routes] - # A2A well-known agent card endpoint - # 1.0 serves ``/.well-known/agent-card.json`` in addition to the - # legacy ``/.well-known/agent.json`` aliased path (compat shim). - assert any(p.startswith("/.well-known/agent-card") for p in route_paths) + # Both the 1.0 canonical path and the 0.3 alias must be registered. + assert any(p.startswith("/.well-known/agent-card") for p in route_paths), ( + "canonical /.well-known/agent-card.json route missing" + ) + assert "/.well-known/agent.json" in route_paths, ( + "0.3 alias /.well-known/agent.json route missing from create_a2a_server" + ) # --------------------------------------------------------------------------- diff --git a/tests/test_serve_auth_both.py b/tests/test_serve_auth_both.py index e99c0f0d3..947f627ca 100644 --- a/tests/test_serve_auth_both.py +++ b/tests/test_serve_auth_both.py @@ -618,6 +618,27 @@ def test_discovery_paths_match_a2a_sdk_routes() -> None: ) +def test_all_discovery_paths_are_registered_routes() -> None: + """Inverse of test_discovery_paths_match_a2a_sdk_routes: every path in + _A2A_DISCOVERY_PATHS must actually be registered in the Starlette app + produced by create_a2a_server. A path in the frozenset but missing from + the routing table is auth-exempted but unserviceable — a 404 dressed up + as a bypass. This test would have caught the missing /.well-known/agent.json + route (issue #612).""" + from adcp.server.a2a_server import create_a2a_server + from adcp.server.auth import _A2A_DISCOVERY_PATHS + + app = create_a2a_server(_OkHandler(), name="inverse-drift-guard", validation=None) + app_paths = {r.path for r in app.routes} + + not_routed = [p for p in _A2A_DISCOVERY_PATHS if p not in app_paths] + assert not not_routed, ( + f"_A2A_DISCOVERY_PATHS contains path(s) {not_routed!r} that are NOT " + f"registered in the Starlette app. Auth middleware exempts them but " + f"they 404 — add matching routes in create_a2a_server." + ) + + def test_a2a_agent_card_constant_referenced_directly() -> None: """The 1.0 path uses ``a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH`` rather than a string literal. If a2a-sdk changes the constant, diff --git a/tests/test_unified_mcp_a2a.py b/tests/test_unified_mcp_a2a.py index 97683fbd4..ed58a9524 100644 --- a/tests/test_unified_mcp_a2a.py +++ b/tests/test_unified_mcp_a2a.py @@ -60,17 +60,14 @@ def unified_client(): def test_a2a_agent_card_served_on_root_path(unified_client) -> None: - """``/.well-known/agent.json`` resolves through the dispatcher - to the A2A app. Even if a2a-sdk's wrapper returns 404 (variation - in card-path between versions), the request must NOT 405 / 502 - / 500 — those would indicate the dispatcher routed wrong.""" + """``/.well-known/agent.json`` (0.3 alias) resolves to a 200 agent-card + response. The route must be registered — a 404 means the alias was + stripped from create_a2a_server's route list.""" resp = unified_client.get("/.well-known/agent.json") - assert resp.status_code in (200, 404) - assert resp.status_code not in ( - 405, - 502, - 500, - ), f"A2A agent-card path resolved to wrong inner app: status={resp.status_code}" + assert resp.status_code == 200, ( + f"/.well-known/agent.json returned {resp.status_code}; " + "expected 200 — the 0.3 alias route is missing from create_a2a_server" + ) def test_a2a_root_path_routed_to_a2a_app(unified_client) -> None: From fd9fadc28afedaeab523ae23c18dedf7a5a5d0a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 12:04:44 +0000 Subject: [PATCH 2/2] fix(server): address pre-PR review findings on alias route fix - Remove brand name from inline comment (use generic "A2A 0.3 buyer SDKs") - Expand A2ABearerAuthMiddleware docstring to mention 0.3 alias path - Add comment to inverse-drift-guard test noting it's structural-only https://claude.ai/code/session_01JHq6DLPhFf2kQQpUkPtfZe --- src/adcp/server/a2a_server.py | 2 +- src/adcp/server/auth.py | 3 ++- tests/test_serve_auth_both.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/adcp/server/a2a_server.py b/src/adcp/server/a2a_server.py index 2111731ea..4f3a4a532 100644 --- a/src/adcp/server/a2a_server.py +++ b/src/adcp/server/a2a_server.py @@ -919,7 +919,7 @@ def create_a2a_server( jsonrpc_kwargs["context_builder"] = context_builder routes = ( list(create_agent_card_routes(agent_card=agent_card)) - # 0.3 alias: buyer SDKs (e.g. @adcp/sdk) probe /.well-known/agent.json + # 0.3 alias: A2A 0.3 buyer SDKs probe /.well-known/agent.json # as a positive A2A signal. Same handler, no redirect round-trip. + list( create_agent_card_routes( diff --git a/src/adcp/server/auth.py b/src/adcp/server/auth.py index fd501f356..4b27c66c4 100644 --- a/src/adcp/server/auth.py +++ b/src/adcp/server/auth.py @@ -763,7 +763,8 @@ class A2ABearerAuthMiddleware: :func:`adcp.server.a2a_server.create_a2a_server` with this middleware to require a valid bearer header on every JSON-RPC request, while leaving the spec-mandated public discovery - surface (``/.well-known/agent-card.json``) accessible. + surface (``/.well-known/agent-card.json`` and the 0.3 alias + ``/.well-known/agent.json``) accessible. Designed to compose with a2a-sdk's :class:`DefaultServerCallContextBuilder`: on auth success the diff --git a/tests/test_serve_auth_both.py b/tests/test_serve_auth_both.py index 947f627ca..824154590 100644 --- a/tests/test_serve_auth_both.py +++ b/tests/test_serve_auth_both.py @@ -629,6 +629,8 @@ def test_all_discovery_paths_are_registered_routes() -> None: from adcp.server.auth import _A2A_DISCOVERY_PATHS app = create_a2a_server(_OkHandler(), name="inverse-drift-guard", validation=None) + # Structural check only (r.path membership). Live dispatch is validated by + # test_a2a_agent_card_served_on_root_path in test_unified_mcp_a2a.py. app_paths = {r.path for r in app.routes} not_routed = [p for p in _A2A_DISCOVERY_PATHS if p not in app_paths]