Skip to content

Commit 2989101

Browse files
bokelleyclaude
andauthored
fix(server): register /.well-known/agent.json alias route in create_a2a_server (#613)
* 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 * 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fca954e commit 2989101

6 files changed

Lines changed: 54 additions & 20 deletions

File tree

docs/handler-authoring.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -929,8 +929,9 @@ mock-B).
929929
## A2A transport
930930

931931
`serve(MyAgent(), transport="a2a")` wires the same handler through the
932-
A2A protocol with auto-generated agent card (`/.well-known/agent.json`)
933-
derived from the `ADCPHandler` methods your class overrides.
932+
A2A protocol with auto-generated agent card (`/.well-known/agent-card.json`,
933+
with the 0.3 alias `/.well-known/agent.json` also served for backwards
934+
compatibility) derived from the `ADCPHandler` methods your class overrides.
934935

935936
### Durable task storage
936937

src/adcp/server/a2a_server.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -917,8 +917,16 @@ def create_a2a_server(
917917
}
918918
if context_builder is not None:
919919
jsonrpc_kwargs["context_builder"] = context_builder
920-
routes = list(create_agent_card_routes(agent_card=agent_card)) + list(
921-
create_jsonrpc_routes(**jsonrpc_kwargs)
920+
routes = (
921+
list(create_agent_card_routes(agent_card=agent_card))
922+
# 0.3 alias: A2A 0.3 buyer SDKs probe /.well-known/agent.json
923+
# as a positive A2A signal. Same handler, no redirect round-trip.
924+
+ list(
925+
create_agent_card_routes(
926+
agent_card=agent_card, card_url="/.well-known/agent.json"
927+
)
928+
)
929+
+ list(create_jsonrpc_routes(**jsonrpc_kwargs))
922930
)
923931
app = Starlette(routes=routes)
924932

src/adcp/server/auth.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,8 @@ def resolved_a2a_bearer_prefix_required(self) -> bool:
750750
_A2A_DISCOVERY_PATHS: frozenset[str] = frozenset(
751751
{
752752
_A2A_AGENT_CARD_PATH, # 1.0 canonical: ``/.well-known/agent-card.json``.
753-
"/.well-known/agent.json", # Legacy 0.3 alias retained by enable_v0_3_compat=True.
753+
# Legacy 0.3 alias — route registered explicitly in create_a2a_server.
754+
"/.well-known/agent.json",
754755
}
755756
)
756757

@@ -762,7 +763,8 @@ class A2ABearerAuthMiddleware:
762763
:func:`adcp.server.a2a_server.create_a2a_server` with this
763764
middleware to require a valid bearer header on every JSON-RPC
764765
request, while leaving the spec-mandated public discovery
765-
surface (``/.well-known/agent-card.json``) accessible.
766+
surface (``/.well-known/agent-card.json`` and the 0.3 alias
767+
``/.well-known/agent.json``) accessible.
766768
767769
Designed to compose with a2a-sdk's
768770
:class:`DefaultServerCallContextBuilder`: on auth success the

tests/test_a2a_server.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,13 @@ def test_create_a2a_server_creates_starlette_app():
345345
# Starlette app has .routes
346346
assert hasattr(app, "routes")
347347
route_paths = [r.path for r in app.routes]
348-
# A2A well-known agent card endpoint
349-
# 1.0 serves ``/.well-known/agent-card.json`` in addition to the
350-
# legacy ``/.well-known/agent.json`` aliased path (compat shim).
351-
assert any(p.startswith("/.well-known/agent-card") for p in route_paths)
348+
# Both the 1.0 canonical path and the 0.3 alias must be registered.
349+
assert any(p.startswith("/.well-known/agent-card") for p in route_paths), (
350+
"canonical /.well-known/agent-card.json route missing"
351+
)
352+
assert "/.well-known/agent.json" in route_paths, (
353+
"0.3 alias /.well-known/agent.json route missing from create_a2a_server"
354+
)
352355

353356

354357
# ---------------------------------------------------------------------------

tests/test_serve_auth_both.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,29 @@ def test_discovery_paths_match_a2a_sdk_routes() -> None:
618618
)
619619

620620

621+
def test_all_discovery_paths_are_registered_routes() -> None:
622+
"""Inverse of test_discovery_paths_match_a2a_sdk_routes: every path in
623+
_A2A_DISCOVERY_PATHS must actually be registered in the Starlette app
624+
produced by create_a2a_server. A path in the frozenset but missing from
625+
the routing table is auth-exempted but unserviceable — a 404 dressed up
626+
as a bypass. This test would have caught the missing /.well-known/agent.json
627+
route (issue #612)."""
628+
from adcp.server.a2a_server import create_a2a_server
629+
from adcp.server.auth import _A2A_DISCOVERY_PATHS
630+
631+
app = create_a2a_server(_OkHandler(), name="inverse-drift-guard", validation=None)
632+
# Structural check only (r.path membership). Live dispatch is validated by
633+
# test_a2a_agent_card_served_on_root_path in test_unified_mcp_a2a.py.
634+
app_paths = {r.path for r in app.routes}
635+
636+
not_routed = [p for p in _A2A_DISCOVERY_PATHS if p not in app_paths]
637+
assert not not_routed, (
638+
f"_A2A_DISCOVERY_PATHS contains path(s) {not_routed!r} that are NOT "
639+
f"registered in the Starlette app. Auth middleware exempts them but "
640+
f"they 404 — add matching routes in create_a2a_server."
641+
)
642+
643+
621644
def test_a2a_agent_card_constant_referenced_directly() -> None:
622645
"""The 1.0 path uses ``a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH``
623646
rather than a string literal. If a2a-sdk changes the constant,

tests/test_unified_mcp_a2a.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,14 @@ def unified_client():
6060

6161

6262
def test_a2a_agent_card_served_on_root_path(unified_client) -> None:
63-
"""``/.well-known/agent.json`` resolves through the dispatcher
64-
to the A2A app. Even if a2a-sdk's wrapper returns 404 (variation
65-
in card-path between versions), the request must NOT 405 / 502
66-
/ 500 — those would indicate the dispatcher routed wrong."""
63+
"""``/.well-known/agent.json`` (0.3 alias) resolves to a 200 agent-card
64+
response. The route must be registered — a 404 means the alias was
65+
stripped from create_a2a_server's route list."""
6766
resp = unified_client.get("/.well-known/agent.json")
68-
assert resp.status_code in (200, 404)
69-
assert resp.status_code not in (
70-
405,
71-
502,
72-
500,
73-
), f"A2A agent-card path resolved to wrong inner app: status={resp.status_code}"
67+
assert resp.status_code == 200, (
68+
f"/.well-known/agent.json returned {resp.status_code}; "
69+
"expected 200 — the 0.3 alias route is missing from create_a2a_server"
70+
)
7471

7572

7673
def test_a2a_root_path_routed_to_a2a_app(unified_client) -> None:

0 commit comments

Comments
 (0)