ADR: Outbound A2A client (agents calling external A2A agents)¶
Status: first slice implemented behind app.a2a.client.enabled (default OFF). Date: 2026-10-01.
Audience: developers, freelancers, scientists, researchers, general users (no enterprise/B2B scope).
Context¶
The platform already serves A2A inbound (core/agents/a2a: signed Agent Cards, JSON-RPC 0.3 and 1.0,
task store). Agents could not call out to third-party A2A agents. The only agent-calls-agent paths were
MCP or http_request, neither of which verifies identity or understands the task lifecycle.
Decision¶
Expose one tool, a2a_call_agent(agent_card_url, message), backed by A2aClientService.
Per call: allow-list + SSRF check -> fetch card (no redirects, size cap) -> verify detached JWS with the
existing A2aCardVerifier.verifyCard over the exact fetched bytes -> take endpoint from the SIGNED card
(never from the model) -> re-guard endpoint -> message/send | SendMessage per protocolVersion ->
poll tasks/get | GetTask to terminal or max-wait -> best-effort cancel on timeout, or when the
remote asks for input/auth (a one-shot tool cannot answer) -> text-only result.
Options considered¶
- Trust any URL the model supplies, rely on an SSRF denylist only. Rejected: the model (prompt-injectable) chooses the URL; a denylist is the wrong default.
- Operator allow-list + SSRF resolution check + signed card (chosen). Empty allow-list reaches nothing.
- Full A2A client with auth schemes, streaming, push notifications, multi-turn input-required. Deferred: needs a credential model and a place to park an interrupted task (see staged plan).
- MCP-over-A2A bridges. Rejected: adds a hop and loses card verification.
Trade-offs¶
- Card verification is origin-bound integrity (JWKS from the same origin or a guarded
jwks_uri), not independent attestation. The allow-list is what makes an origin trusted; the signature stops tampering and card/endpoint swaps between fetch and use. - DNS rebinding between check and connect is an accepted limitation (same as
fetch_web_page); redirects are never followed, which removes the cheap variant. - Result text is third-party data: classified
UNTRUSTED_EXTERNAL(and egress) inBuiltInToolClassifications, framed as untrusted in the tool output, text parts only. - No credentials are sent. Remotes needing auth are out of scope for this slice.
Task lifecycle and namespace¶
Each call is mirrored as an A2aTask in the shared A2aTaskStore (tenant/subject scoped) for retention
and Redis machinery. Outbound ids carry A2aTask.OUTBOUND_ID_PREFIX (a2a-out-) and the INBOUND
A2aTaskService.get/cancel refuses that prefix, so a remote caller can never read or cancel outbound
bookkeeping (it holds the prompt we sent and the third-party reply). Store failures are swallowed;
bookkeeping never breaks a call. The remote's state remains authoritative.
Config (app.a2a.client in application.yml)¶
enabled (false), allowed-hosts (empty), require-signed-card (true), allow-insecure-loopback
(false, dev only), connect-timeout 5s, request-timeout 30s, max-wait 120s, poll-interval 2s,
max-response-bytes 1 MiB, max-result-chars 20000.
Staged plan¶
- (done) Unauthenticated blocking call, both dialects, polling, cancel, guard, classification, tests with a loopback fake A2A server.
- Per-tenant allow-list and per-agent opt-in (today operator-global), plus an audit-log row per call.
- Outbound auth (bearer/API key from the tenant secret store, scheme taken from the signed card).
- Resumable
input-required: park the remote task id on the run and let the user answer instead of canceling. Needs the step journal (V376) from the durability item. - Streaming (
message/stream) and push notifications.
Risks¶
- Operator misconfiguration: a wildcard allow-list (
*.example.com) trusts every subdomain. - A malicious but allow-listed remote can return prompt-injection text; mitigated by classification and framing, not eliminated.
- A remote task that never terminates holds a poll loop up to
max-waiton the calling thread. - Per-call card + JWKS fetch adds latency; no caching yet (deliberate: avoids stale-key trust).