Skip to main content

A2A Gateway

Yabanin's A2A gateway is the agent-to-agent counterpart of the MCP gateway: an organization registers its external A2A (Agent2Agent protocol) agents in an org-scoped registry, and the org's agent principals (yag_ keys) send them messages through the gateway — with agent-only access control, SSRF guarding, pre-egress parameter guards and a per-send audit trail. It is off by default: the route group is mounted only when YABANIN_A2A_GATEWAY=1.

v1 scope — stated honestly​

The proxy carries the A2A JSON-RPC message/send method (request/response). Streaming (message/stream) and push notifications are not proxied in v1 — they are a later increment, not a hidden gap. The gateway wraps the client's params (the A2A message object) into the JSON-RPC envelope itself, so callers cannot smuggle arbitrary methods.

How it works​

agent (yag_ key) ──► POST /api/a2a/send {"agent":"helper","params":{...}}
│
├─ auth: agent-principal only (org keys → 403 agent_only)
├─ registry: (org, name) must exist and be enabled
├─ SSRF: per-send re-validation (rebind window)
├─ guard: pre-egress scan of the message params
└─ upstream: JSON-RPC message/send → the agent's URL
result (raw JSON-RPC result) ──► back to the caller

Every send is audited to the same request_logs trail as model traffic: model_used carries the a2a:<agent> label, the (guard-redacted) params ride the payload, and X-Yabanin-Task tags attribute the send to a task.

Registry​

a2a_agents (migration 0015) — org-scoped, https://-only URLs, UNIQUE (org_id, name), inert until enabled. Admin CRUD (root-only, org-scoped):

MethodPath
POST/api/organizations/{id}/a2a/agents
GET/api/organizations/{id}/a2a/agents
GET/api/organizations/{id}/a2a/agents/{name}
PATCH/api/organizations/{id}/a2a/agents/{name}
DELETE/api/organizations/{id}/a2a/agents/{name}

Registration (and every URL update) runs the SSRF guard: https-only, no credentials in the URL, the deployment's domain allow-list (YABANIN_A2A_ALLOWED_UPSTREAMS, comma-separated; empty = no domain allowed — fail-closed), and DNS resolution that rejects reserved-IP answers (metadata endpoints, RFC1918, loopback, link-local, CGNAT). The guard re-runs on every send — a DNS-rebind window between registration and use is not exploitable.

Gateway API​

curl -X POST "$BASE_URL/api/a2a/send" \
-H "Authorization: Bearer yag_..." \
-H "Content-Type: application/json" \
-d '{
"agent": "helper",
"params": {"message": {"messageId": "m-1", "role": "user",
"parts": [{"kind": "text", "text": "status?"}]}}
}'

Error contract: agent_only (403), not_found (404 — a missing, disabled or other-org agent is indistinguishable), ssrf_blocked (400), denied (409, guard), upstream (502), timeout (504).

Configuration​

EnvDefaultMeaning
YABANIN_A2A_GATEWAYfalseMount the /api/a2a/send route group
YABANIN_A2A_ALLOWED_UPSTREAMS(empty)SSRF domain allow-list for agent URLs (empty = fail-closed)

Upstream calls are bounded: 10s connect, 30s read, 8 MiB response cap, redirects refused. Identity headers (X-Yabanin-Agent-Id/-Name/-Team-Id) ride upstream from the verified principal — client copies are stripped on ingress.

Compared to the MCP gateway​

Same discipline (SSRF, guards, audit, agent-only), different object: the MCP registry points at tools, the A2A registry at agents. Per-agent grant tables (the MCP pattern) are a planned increment for A2A — v1 authorizes sends at the org level.