A2A Adapter
The A2A adapter is a thin bridge between Google's Agent-to-Agent protocol and the chio kernel. It discovers an A2A server's Agent Card, maps the advertised skills to chio tools, and routes every SendMessage or streaming call through the normal guard and receipt pipeline. The adapter is intentionally honest about the protocol boundary: it does not invent skill routing that A2A v1.0.0 does not define, and it fails closed against any auth scheme it does not yet implement.
Why A2A Through chio
Google's A2A protocol standardises how agents expose capabilities to other agents over HTTP. Agents publish an Agent Card describing their skills, bindings, and security schemes; callers select an interface and send structured messages. Out of the box, A2A gives you transport and discovery but leaves enforcement, attribution, and audit to the operator. That is exactly the gap chio fills.
When an A2A call is proxied through chio, the caller's capability token is validated, guards fire at the adapter boundary, and a signed ChioReceipt is emitted for every invocation. Cross-organisation delegation becomes auditable: each hop in the call chain is bound into the receipt DAG, not lost in freeform operator notes.
| A2A alone | A2A + chio |
|---|---|
| Transport and Agent Card discovery | Capability validation and guard evaluation at the adapter boundary |
| Skill advertisement via Agent Card | One chio tool per skill, with stable tool names and scoping |
| Auth negotiation is per-integration glue | Declared schemes matched to configured credentials, fail closed otherwise |
| Cross-agent trust is implicit | Delegated provenance is carried in call_chain and signed into receipts |
Current Scope
The adapter supports the A2A v1.0.0 surface needed to run production partner integrations:
- Agent Card discovery via
/.well-known/agent-card.jsonon a base URL, or a full Agent Card URL consumed directly - Both
JSONRPCandHTTP+JSONbindings - Blocking
SendMessage, streamingSendStreamingMessage, and follow-upGetTask,SubscribeToTask, andCancelTask - Push-notification config create, get, list, and delete over both bindings
- OAuth2 client-credentials, OpenID Connect discovery, HTTP Basic, API keys in header, query, or cookie position, and mutual TLS
- Explicit adapter-level request headers, query params, and cookies for partner integrations that need extra glue without per-call plumbing
- Fail-closed partner admission policy for expected tenant, required skills, required security schemes, and allowed interface origins
- Optional durable task registry so follow-up correlation survives adapter recreation and process restarts
HTTPS by default
https for remote A2A targets. Plain http is allowed only for localhost, which keeps development ergonomic without leaking into production configurations.The Honest Boundary
A2A v1.0.0 does not define a native skillId selector inside SendMessage. Rather than pretend otherwise, the adapter injects an adapter-local convention into top-level request metadata:
{
"chio": {
"targetSkillId": "research",
"targetSkillName": "Research"
}
}This keeps the protocol boundary explicit while still giving chio stable per-skill tool names and per-skill capability scoping.
Auth negotiation is equally explicit. The adapter only sends credentials that satisfy a declared A2A requirement set. If the Agent Card demands a scheme the adapter does not implement yet, the invocation is denied locally before any call goes upstream. The following Agent Card declarations are recognised:
| Agent Card declaration | chio behaviour |
|---|---|
| bearer, OAuth2-bearer, OpenID-bearer | Bearer-style Authorization header |
httpAuthSecurityScheme basic | HTTP Basic authentication |
oauth2SecurityScheme | Client-credentials token acquisition against the declared endpoint |
openIdConnectSecurityScheme | OIDC discovery followed by client-credentials |
mtlsSecurityScheme | Mutual TLS using the configured client identity |
apiKeySecurityScheme | Named API key placed in header, query, or cookie |
Lifecycle payload validation is fail-closed
SendMessage task responses, GetTask results, and streamed task, statusUpdate, and artifactUpdate events must contain the required fields (id, status.state, taskId, and artifact where applicable). Malformed payloads are rejected before reaching the kernel.Serving an A2A Bridge
chio exposes the A2A adapter through the tool server surface. Depending on the release, you can either invoke the dedicated subcommand or use the generic MCP serve command with a protocol flag:
# Dedicated subcommand, where available
chio a2a serve \
--target https://agent.example.com \
--manifest-key ./manifest.key \
--partner design-partner-a \
--required-tenant tenant-alpha \
--require-skill research
# Generic tool-server form
chio mcp serve \
--protocol a2a \
--target https://agent.example.com \
--manifest-key ./manifest.keyBoth forms register the adapter with the local kernel so every advertised A2A skill becomes a chio tool. Listing the registered tools afterwards confirms what was discovered:
chio tools list --server a2a:agent.example.comConfiguring the Adapter from Rust
Most operators run the adapter as a long-lived process backed by a chio kernel. The builder-style configuration mirrors the CLI flags:
use chio_a2a_adapter::{A2aAdapter, A2aAdapterConfig, A2aPartnerPolicy};
use chio_core::crypto::Keypair;
use chio_kernel::ChioKernel;
let manifest_key = Keypair::generate();
let adapter = A2aAdapter::discover(
A2aAdapterConfig::new(
"https://agent.example.com",
manifest_key.public_key().to_hex(),
)
.with_tls_root_ca_pem(include_str!("agent-root-ca.pem"))
.with_mtls_client_auth_pem(
include_str!("agent-client-cert-chain.pem"),
include_str!("agent-client-key.pem"),
)
.with_request_header("X-Partner", "design-partner-a")
.with_oauth_client_credentials("client-id", "client-secret")
.with_oauth_scope("a2a.invoke")
.with_partner_policy(
A2aPartnerPolicy::new("design-partner-a")
.with_required_tenant("tenant-alpha")
.require_skill("research")
.require_security_scheme("oauthAuth")
.allow_interface_origin("https://agent.example.com"),
)
.with_task_registry_file(".chio/a2a-task-registry.json")
)?;
let mut kernel = ChioKernel::new(/* ... */);
kernel.register_tool_server(Box::new(adapter));Tool Contract
Each generated chio tool accepts a superset of SendMessage fields plus adapter-local follow-up modes. The blocking fields are:
message: plain text sent as an A2A text partdata: structured JSON sent as an A2A data partcontext_id,task_id,reference_task_idsmetadataforSendMessageRequest.metadatamessage_metadataforMessage.metadatahistory_length, requires the Agent Card to advertisecapabilities.stateTransitionHistoryreturn_immediatelystream: adapter-local opt-in forSendStreamingMessage
Follow-up and task-management modes are mutually exclusive with the SendMessage fields above. They include get_task, subscribe_task, cancel_task, and the push-notification config family (create, get, list, delete).
Caller Examples
A caller agent does not talk to A2A directly. It invokes the chio tool that the adapter published, presenting its capability token. The chio kernel validates the token, runs guards, calls the A2A server, and returns the result along with a receipt id.
TypeScript Caller
import { ChioClient } from "@chio-protocol/sdk";
const chio = new ChioClient({
sidecarUrl: "http://127.0.0.1:9090",
capabilityToken: process.env.CHIO_CAP_TOKEN!,
});
const result = await chio.invoke({
server: "a2a:agent.example.com",
tool: "research",
arguments: {
message: "Summarise the latest filings for tenant-alpha.",
metadata: { source: "partner-console" },
},
});
console.log("receipt:", result.receiptId);
console.log("payload:", result.payload);Python Caller
from chio import ChioClient
chio = ChioClient(
sidecar_url="http://127.0.0.1:9090",
capability_token=os.environ["CHIO_CAP_TOKEN"],
)
result = chio.invoke(
server="a2a:agent.example.com",
tool="research",
arguments={
"message": "Summarise the latest filings for tenant-alpha.",
"return_immediately": True,
},
)
# A long-running task was returned. Poll for completion.
while result.payload.get("task", {}).get("status", {}).get("state") != "TASK_STATE_COMPLETED":
result = chio.invoke(
server="a2a:agent.example.com",
tool="research",
arguments={"get_task": {"id": result.payload["task"]["id"]}},
)Streaming and Follow-Up
When stream: true is set, the adapter issues SendStreamingMessage and the kernel surfaces each upstream chunk as one stream event. The chunk payload is the raw A2A object, for example a status update:
{
"statusUpdate": {
"taskId": "task-1",
"status": {
"state": "TASK_STATE_COMPLETED"
}
}
}If a call returns a task rather than a terminal message, follow-up modes let you poll, subscribe, or cancel it through the same tool:
{ "get_task": { "id": "task-1", "history_length": 2 } }
{ "subscribe_task": { "id": "task-1" } }
{ "cancel_task": { "id": "task-1", "metadata": { "reason": "user-request" } } }Push notification callbacks
pushNotifications, the adapter also exposes config create, get, list, and delete through the same tool surface. Callback URLs are validated the same way as the target: remote callbacks must be HTTPS, plain HTTP only for localhost.Partner Admission
When a design partner has a narrow, expected contract, configure A2aPartnerPolicy so discovery fails closed rather than silently adapting to whatever the Agent Card declares today. The policy enforces four dimensions:
| Policy method | Rejects when |
|---|---|
with_required_tenant | The selected interface advertises a different tenant id |
require_skill | The Agent Card does not expose the required skill id |
require_security_scheme | A required scheme name is missing from the card or its security requirements |
allow_interface_origin | No supported interface is advertised from an allowed origin |
Discovery errors are operator-visible and include the failing tenant, interface, or scheme contract, so mismatches are easy to diagnose.
Durable Task Correlation
Long-running A2A tasks frequently outlive the adapter process. Setting with_task_registry_file persists one fail-closed binding per observed task id. The binding records:
- chio tool name
- Selected interface URL
- Protocol binding
- Tenant and partner label
- Last observed task state and its source
Follow-up calls are rejected unless the task_id was previously recorded for the same tool, server, binding, interface, and tenant. That closes a common replay surface where a caller might try to poll a task that never belonged to them.
Cross-Organisation Delegation
The most interesting A2A scenario is cross-organisation delegation: an agent at org A calls an agent at org B, which in turn calls a tool at org C. Without chio, only the final hop is visible to any single operator. With chio on both sides, the full chain is bound into each receipt:
org-a caller
-> a2a adapter (chio, org-a) signed receipt R1, call_chain = [a]
-> a2a server (org-b agent)
-> chio kernel (org-b) signed receipt R2, call_chain = [a, b]
-> tool at org-c signed receipt R3, call_chain = [a, b, c]The upstream task lineage is carried through governed_intent.call_chain, not attached as freeform operator notes. chio preserves that delegated provenance in the signed receipt and later projects it through /v1/reports/authorization-context or chio trust authorization-context list, alongside derived authorization-detail scope for commerce and metered-billing context.
Do not re-sign at the boundary
What Is Not Shipped Yet
The adapter has moved past a transport skeleton, but a few surfaces remain on the roadmap:
- Deeper long-running task lifecycle surfaces beyond
GetTask,SubscribeToTask,CancelTask, and push-notification config CRUD - Custom or non-standard auth schemes beyond bearer, HTTP Basic, API key, OAuth, OpenID, and mTLS
- Broader federation and partner onboarding flows beyond adapter-local admission policy and task correlation
Next Steps
- LangGraph · orchestrate multi-agent graphs with per-node capability scoping
- Temporal · enforce capabilities across durable workflow activities
- Wrap an MCP Server · the same pattern applied to the Model Context Protocol