Skip to content

feat(a2a): add public_url param to agent card for production deployments#621

Merged
bokelley merged 3 commits intomainfrom
claude/issue-616-agent-card-public-url
May 10, 2026
Merged

feat(a2a): add public_url param to agent card for production deployments#621
bokelley merged 3 commits intomainfrom
claude/issue-616-agent-card-public-url

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Closes #616

Summary

_build_agent_card hardcodes http://localhost:{port}/ in the A2A agent card at server-init time. In production behind a load balancer the public URL comes from outside the container — the static localhost URL leaks into /.well-known/agent-card.json, causing every A2A request from SDK clients that use the card for discovery to fail with fetch failed.

This PR adds a public_url parameter to replace that default across the full serving stack.

Changes:

  • public_url: str | None = None added to _build_agent_card, create_a2a_server, serve, ServeConfig, _serve_a2a, _build_mcp_and_a2a_app, _serve_mcp_and_a2a
  • When set, public_url replaces http://localhost:{port}/ in all supportedInterfaces URL entries; trailing slash is normalised so both "https://x.com" and "https://x.com/" work
  • PUBLIC_URL env var fallback in create_a2a_server — zero-code-change for Cloud Run / Fly.io / Railway; kwarg takes precedence when both are set
  • public_url added to _a2a_only in ServeConfig.__post_init__ so the cross-transport warning fires correctly
  • Non-breaking: default None preserves existing http://localhost:{port}/ behaviour

Out of scope: dynamic trust_forwarded_headers per-request URL derivation (option B/C from the issue). Both the DX and code-review experts flagged thread-safety and header-trust-boundary concerns; deferred to a follow-up issue.

What was tested

  • ruff check — clean
  • mypy — no new errors in changed files (pre-existing errors in unrelated generated types)
  • pytest tests/test_a2a_server.py — 45/45 pass (5 new tests added)
  • pytest tests/ (unit tier, excluding integration/conformance) — 3759 passed, 18 skipped, 1 xfailed

New tests:

  • test_build_agent_card_public_url_overrides_localhost — unit, verifies all supported_interfaces URLs use the override
  • test_build_agent_card_public_url_none_uses_localhost — unit, verifies default unchanged
  • test_create_a2a_server_public_url_in_card — integration via TestClient hitting /.well-known/agent-card.json
  • test_create_a2a_server_public_url_env_var — env var fallback
  • test_create_a2a_server_public_url_kwarg_takes_precedence_over_env — kwarg wins over env var

Pre-PR review

  • code-reviewer: approved after fixup round — blocker (missing serve() docstring) fixed, nits (trailing-slash normalisation, redundant or None, dead test code) fixed
  • dx-expert: approved — public_url naming correct (distinct from existing base_url for MCP discovery manifest), env-var fallback is the right zero-config pattern for PaaS deployments, ServeConfig coverage is complete

Nits surfaced (not fixed — noted here per convention):

  • PUBLIC_URL env var not surfaced in ServeConfig field-level comment; only documented in create_a2a_server docstring. Low impact — caller using ServeConfig can pass public_url= explicitly.

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01NXgGie4ARyxg3hWBYo5tG6


Generated by Claude Code

claude added 3 commits May 10, 2026 08:52
Fixes #616

Adds `public_url` to `_build_agent_card`, `create_a2a_server`, `serve`,
and `ServeConfig` so adopters running behind a load balancer or reverse
proxy can advertise the correct public URL in `/.well-known/agent-card.json`
instead of leaking `http://localhost:{port}/`.

Also checks the `PUBLIC_URL` environment variable as a zero-code-change
fallback for Cloud Run / Fly.io / Railway deployments.

https://claude.ai/code/session_01NXgGie4ARyxg3hWBYo5tG6
- Add public_url docstring to serve() (was missing despite being present in create_a2a_server)
- Normalise trailing slash so callers can pass "https://x.com" or "https://x.com/"
- Remove redundant `or None` from resolved_public_url assignment
- Remove dead handler_ref loop from test

https://claude.ai/code/session_01NXgGie4ARyxg3hWBYo5tG6
- Assert trailing-slash normalisation (URL without trailing slash)
- Assert create_a2a_server defaults to localhost when PUBLIC_URL unset

https://claude.ai/code/session_01NXgGie4ARyxg3hWBYo5tG6
@bokelley bokelley force-pushed the claude/issue-616-agent-card-public-url branch from 1fcdf68 to c41e6f7 Compare May 10, 2026 12:52
@bokelley
Copy link
Copy Markdown
Contributor Author

Pre-merge expert pass (fresh independent review, post-rebase)

  • code-reviewer: APPROVE — no blockers. public_url plumbing complete across all entry points; both well-known routes (canonical + 0.3 alias from fix(server): register /.well-known/agent.json alias route in create_a2a_server #613) inherit the override from the shared agent_card instance; env-var precedence (kwarg > PUBLIC_URL > localhost default) matches docs; trailing-slash normalisation handles sub-path deployments correctly. Non-blocking: whitespace-only public_url=" " propagates as " /" (would benefit from .strip()); minor docstring camelCase/snake_case inconsistency between serve() and create_a2a_server.
  • security-reviewer: SAFE — no blockers. PUBLIC_URL is operator-scope (read once at boot, baked into the single agent_card), no per-tenant resolution path. No new info leakage. Defense-in-depth suggestion (not a block): validate scheme {http,https} and reject userinfo / query / fragment in _build_agent_card — currently https://user:pass@host would round-trip embedded creds into the public card. ~5 lines of urllib.parse.urlsplit.
  • ad-tech-protocol-expert: APPROVE — supported_interfaces[].url is the only URL-bearing field the framework currently emits; coverage is complete for current emit. If provider, documentation_url, icon_url, or signatures[] are populated later, they'd need the same plumbing. Doc nit: serve() docstring says public_url is "Ignored for MCP transports" but ServeConfig.__post_init__ actually rejects it via the _a2a_only warning — tighten wording.

Suggested follow-ups (separate issue, not blocking this PR):

  1. URL validation in _build_agent_card (scheme allowlist, no userinfo, no query/fragment)
  2. .strip() on public_url and PUBLIC_URL env value
  3. logger.warning when neither kwarg nor env set and bind host is non-loopback
  4. Doc consistency: "Rejected for MCP" + camelCase/snake_case alignment

Admin-merging once CI re-completes post-rebase.

@bokelley
Copy link
Copy Markdown
Contributor Author

Acknowledged. Three independent expert passes (code-reviewer, security-reviewer, ad-tech-protocol-expert) all approved with no blockers — good shape to merge.

The four follow-up items are worth filing as separate issues so they don't get lost:

  1. URL validation in _build_agent_card — scheme allowlist, reject userinfo/query/fragment
  2. .strip() on public_url kwarg and PUBLIC_URL env value
  3. logger.warning when bind host is non-loopback and no public_url is set
  4. Doc consistency — "Rejected for MCP" wording + camelCase/snake_case alignment across serve() / create_a2a_server docstrings

Triaged by Claude Code.


Generated by Claude Code

@bokelley bokelley merged commit 14d294c into main May 10, 2026
16 checks passed
@bokelley bokelley deleted the claude/issue-616-agent-card-public-url branch May 10, 2026 12:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(a2a): agent-card public URL injection (X-Forwarded-Host aware)

2 participants