Skip to content

sdk: rewrite the card of an agent behind a node for the mesh - #556

Merged
aojea merged 2 commits into
google:mainfrom
kaisoz:kaisoz/fix-a2a-sdk
Sep 30, 2026
Merged

aojea merged 2 commits into
google:mainfrom
kaisoz:kaisoz/fix-a2a-sdk

Conversation

@kaisoz

@kaisoz kaisoz commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Why

A stock A2A client used over an SDK session (session.fetch() in JS, MeshTransport in Python) fails against an agent hosted behind a sam-node. The agent's card names its own address, the client sends every request there, and the SDK refuses a URL that is not a mesh URL. sam-node's egress proxy regenerates the card for its callers (a2aServeAgentCard); the SDKs never did.

Both SDKs now do at their /libp2p-http client what a2aServeAgentCard does: a GET of /a2a/<name>/.well-known/agent-card.json or of the bare service root is held, the SDK fetches the card itself at the well-known path with identity encoding (httpx sends accept-encoding: gzip, deflate by default, and the node deletes it for the same reason), and answers with the card regenerated as regenerateAgentCardForMesh does. The rewrite points every HTTP interface at the mesh URL, drops gRPC interfaces and signatures, and answers 502 for a card that is not JSON or has no interface left. The agent's own non-200 is relayed as it is. session.request() and the transports share that path, so a raw GET reads the same card.

Scope

  • JS: rewriteAgentCard, AGENT_CARD_PATH and serveAgentCard behind fetchOverStream (sdk/js/src/libp2p-http.ts), the first two exported from the package.
  • Python: rewrite_agent_card, AGENT_CARD_PATH, mesh_url and _serve_agent_card behind open_http_request (sdk/python/src/agent_mesh/libp2p_http.py); MESH_PATH_PREFIX moves there from httpx_transport.py, which re-exports it; MeshSession.mesh_url delegates.
  • Tests: unit in each SDK against the in-process provider; TestNativeSDKsMesh hosts a stock a2a card behind the fixture node and asserts each SDK member reads it rewritten.
  • Docs: sdk/README.md, site/content/docs/guides/native-sdks.md.

Tradeoffs

  • Streaming stays as the agent declares it. The node forces capabilities.streaming off because its egress proxy is unverified for SSE; this transport streams bodies (unit-tested), and SDK-hosted agents advertise streaming, so forcing it off would regress SDK-to-SDK calls.
  • A GET of the bare service root is a card request, as on the node. A test that used /a2a/<name> as a synthetic path for stream-reset checks moved to /inference/<name>.

Blast Radius

Every card read through an SDK session, including cards of SDK-hosted agents, passes through the rewrite. For those the interface URL is already the mesh URL, so the rewrite is a no-op apart from dropping signatures. The stock A2A example clients in both languages still pass against each other's agents.

Verification

  • TestNativeSDKsMesh failed before the fix with both members returning http://127.0.0.1:7777/ and the gRPC interface, and passes after; the whole TestNativeSDK family passes with both toolchains present.
  • npm test in sdk/js: 90 passed. pytest sdk/python/tests: 101 passed. The unit tests cover the root path, the relayed 404, the 502 for a gRPC-only card, and that the client's accept-encoding never reaches the agent.
  • make lint and gen-sdk-docs -check pass.

A stock A2A client on session.fetch() or MeshTransport follows the URL
in the agent card. An agent behind a sam-node serves a card naming its
own address, so the client left the mesh and the SDK refused the URL.
sam-node's egress proxy impersonates that card endpoint for its own
callers. The SDKs now do the same at their /libp2p-http client: a GET
of /a2a/<name>/.well-known/agent-card.json or of the bare service root
is held, the SDK fetches the card itself at the well-known path with
identity encoding, and answers with it regenerated. The rewrite points
every HTTP interface at the mesh URL, drops gRPC interfaces and
signatures, and answers 502 for a card that is not JSON or has no
interface left; the agent's own non-200 is relayed as it is. Streaming
stays as the agent declares it, unlike the node's rewrite, since this
transport streams and SDK-hosted agents advertise it.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements agent card rewriting in both the JavaScript and Python SDKs, aligning their behavior with sam-node's egress proxy. When a client requests an agent's card (at .well-known/agent-card.json or the service root), the SDK fetches the card, rewrites its supported interfaces to point to the mesh URL, filters out unsupported protocols like gRPC, and strips signatures. This allows stock A2A clients to bootstrap seamlessly. The changes include comprehensive unit and integration tests in both JS and Python, as well as updates to the documentation. I have no feedback to provide as there are no review comments.

@aojea
aojea merged commit 5ad9e97 into google:main Sep 30, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants