feat(a2a): per-request agent-card URL resolution via callable public_url#650
Merged
Conversation
…ent-card URL resolution Adds support for a `Callable[[Request], str]` in addition to the existing static `str` for `serve(public_url=...)` and `create_a2a_server(public_url=...)`. When a callable is supplied, an ASGI-layer intercept replaces the static `create_agent_card_routes` for the well-known endpoints, building the card per GET request. The `DefaultRequestHandler` retains a localhost fallback card for the `GetAgentCard` RPC path. Option A (trust_forwarded_headers) was ruled out by security review: the unauthenticated agent-card endpoint + unvalidated X-Forwarded-Host = agent-card hijack with no clean mitigation short of an allow-list. The callable puts the trust-policy decision in adopter code where it belongs. Closes #647 https://claude.ai/code/session_01CTpSC8E52DSKTDFJx7Y4Gf
- Await async resolvers (inspect.iscoroutine) so async def resolvers work - Add test_async_resolver_works integration test covering async path - Use logger.error(..., exc_info=True) to preserve resolver tracebacks - Use JSONResponse body for 500 errors instead of empty Response - Consolidate serve.py type annotations to use PublicUrlResolver alias - Replace real brand names in tests with RFC 2606 example.com hostnames https://claude.ai/code/session_01CTpSC8E52DSKTDFJx7Y4Gf
- Type alias is now Callable[[Any], str | Awaitable[str]] so adopters annotating ``async def`` resolvers against the public alias type-check. - Propagate the alias through create_a2a_server signature and the internal resolved_public_url type. - Tighten _wrap_with_per_request_card resolver parameter to the alias. - Use inspect.isawaitable (not iscoroutine) for resolver result, with a runtime assert to narrow for mypy. - Replace real brand host async.scope3.com with async.example.com in tests per repo convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #647
PublicUrlResolver = Callable[[Any], str]type alias exported fromadcp.servercreate_a2a_server(public_url=…)andserve(public_url=…)/ServeConfig.public_urlto accept a callable in addition tostr | None— fully backwards-compatibleGET /.well-known/agent-card.jsonandGET /.well-known/agent.jsonper request, calls the resolver with the StarletteRequest, validates the returned URL, and rebuilds the agent card on the fly — bypassing the a2a-sdk's build-time bakeinspect.iscoroutinedetection + await){"error": "agent-card temporarily unavailable"}with HTTP 500 and a full traceback in the server logMotivation
Multi-tenant subdomain deployments (
acme.platform.io,beta.platform.io, …) share one server process. A staticpublic_urlcan only name one host, so every tenant's agent card points at the same URL. The callable pattern puts host-resolution logic in adopter code (e.g. read theHostheader) while keepingX-Forwarded-Hosttrust decisions out of the framework's unauthenticated agent-card endpoint.Typical usage
Test plan
tests/test_a2a_public_url_resolver.py— 15 tests: 6 unit (_validate_card_url) + 9 integration (per-request card, multi-host, 0.3 alias, error→500, invalid URL→500, async resolver, static/None unchanged, export check)ruff check src/— all checks passedmypy src/adcp/— no issues in 2 changed source filesexc_info=True+ brand names replaced)https://claude.ai/code/session_01CTpSC8E52DSKTDFJx7Y4Gf
Generated by Claude Code