BuildGateways
A2A Adapter
Map a Google Agent-to-Agent server's advertised skills to Chio tools, then evaluate each call and sign a receipt.
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. A2A defines transport and discovery; the operator supplies enforcement, attribution, and audit.
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.
| 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 | Every hop is a validated capability and a signed receipt, so the caller behind a call is a fact the operator holds |
Current Scope
The adapter sends A2A-Version: 1.0 and selects any advertised interface whose protocolVersion begins 1., so a 1.x agent is in scope. It supports these features for 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
- Adapter-level request headers, query parameters, and cookies for partner-specific requirements
- 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. Production configurations must use HTTPS.One direction: this is the consuming side
chio-a2a-adapter is the outbound direction: chio calls a remote A2A agent and governs the call. Hosting chio's own tools as an A2A server (building an Agent Card and serving message/send, message/stream, task/get, and task/cancel) is the inbound counterpart, and it lives in a separate crate, chio-a2a-edge. If you are building an A2A server for chio, use the edge crate.Protocol Boundary
A2A v1.0.0 does not define a native skillId selector inside SendMessage. The adapter adds this local convention to top-level request metadata:
{
"chio": {
"targetSkillId": "research",
"targetSkillName": "Research"
}
}The convention gives Chio stable per-skill tool names and per-skill capability scopes without changing the A2A protocol.
The adapter sends only 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 behavior |
|---|---|
| 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
There is no dedicated chio a2a CLI subcommand. The documented way to expose the adapter is to embed it in a Rust host process backed by a chio kernel and register it with kernel.register_tool_server(...). On registration, the adapter discovers the Agent Card and every advertised skill becomes a Chio tool, invocable through the kernel like any other tool server. The complete builder is shown in Configuring the Adapter from Rust below.
Configuring the Adapter from Rust
Operators run the adapter as a long-lived process backed by a chio kernel. A2aAdapterConfig is a builder: set the target Agent Card URL and manifest public key, add transport credentials and partner policy, then register the discovered adapter with the kernel.
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, and it does not use an A2A client library. It opens a Chio session and calls the tool the adapter published, named after the A2A skill id. The kernel validates the capability behind the session, runs guards, calls the A2A server, and returns the result. Both SDKs below are the published clients: npm install @chio-protocol/sdk and pip install chio-sdk, which imports as chio.
import { ChioClient } from "@chio-protocol/sdk";
const client = ChioClient.withStaticBearer(
process.env.CHIO_BASE_URL!,
process.env.CHIO_AUTH_TOKEN!,
);
const session = await client.initialize({
clientInfo: { name: "partner-console", version: "0.1.0" },
});
try {
const result = await session.callTool("research", {
message: "Summarise the latest filings for tenant-alpha.",
metadata: { source: "partner-console" },
});
console.log(JSON.stringify(result, null, 2));
} finally {
await session.close();
}import json
import os
from chio import ChioClient
client = ChioClient.with_static_bearer(
os.environ["CHIO_BASE_URL"],
os.environ["CHIO_AUTH_TOKEN"],
)
session = client.initialize(
client_info={"name": "partner-console", "version": "0.1.0"}
)
try:
result = session.call_tool(
"research",
{
"message": "Summarise the latest filings for tenant-alpha.",
"metadata": {"source": "partner-console"},
},
).get("result", {})
print(json.dumps(result, indent=2))
finally:
session.close()Following Up on a Deferred Task
return_immediately makes the adapter hand back the A2A task instead of waiting for it. get_task is the follow-up mode on the same tool: it takes the task id and issues A2A GetTask rather than another SendMessage. It is mutually exclusive with every SendMessage field, so the follow-up call carries only get_task. The two calls below are the caller-side rendering of the pair the adapter's own test drives directly at crates/protocol/chio-a2a-adapter/src/tests/invoke_manifest.rs:58-80, which asserts the deferred call returns TASK_STATE_WORKING and the poll returns TASK_STATE_COMPLETED.
// The adapter answers with the A2A task rather than a terminal message.
const deferred = await session.callTool("research", {
message: "Start a long-running research task",
return_immediately: true,
});
// Poll it on the same tool. Only `get_task` may be present.
const followUp = await session.callTool("research", {
get_task: { id: "task-1", history_length: 2 },
});# The adapter answers with the A2A task rather than a terminal message.
deferred = session.call_tool(
"research",
{
"message": "Start a long-running research task",
"return_immediately": True,
},
).get("result", {})
# Poll it on the same tool. Only `get_task` may be present.
follow_up = session.call_tool(
"research",
{"get_task": {"id": "task-1", "history_length": 2}},
).get("result", {})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 instead of 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 API. 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 rejects an Agent Card that does not match the configured contract. The policy enforces four checks:
| 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 identify the failing tenant, interface, or scheme contract.
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. This prevents a caller from polling a task that is not associated with that caller context.
Cross-Organization Delegation
A2A supports cross-organization 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, each hop signs its own receipt:
org-a caller
-> a2a adapter (chio, org-a) signed receipt R1
-> a2a server (org-b agent)
-> chio kernel (org-b) signed receipt R2
-> tool at org-c signed receipt R3The link between those receipts is GovernedTransactionIntent.call_chain, and it is not this adapter that writes it. The adapter never reads or sets the field: call_chain has no occurrence anywhere in crates/protocol/chio-a2a-adapter/src. A caller that wants cross-organization lineage supplies the context on its governed intent, and the kernel validates it.
The context is one link, not a growing list of hops. GovernedCallChainContext (crates/core/chio-core-types/src/capability/governance.rs:184-196) carries a stable chain_id, the parent_request_id, an optional parent_receipt_id, the origin_subject at the root of the chain, and the delegator_subject that handed control to the current subject. The full path is reconstructed by following those parent pointers, not read off any single receipt. The kernel refuses a link that does not hold together: an empty chain_id, a parent_request_id equal to the current request, or an origin_subject or delegator_subject that disagrees with the validated capability lineage (crates/kernel/chio-kernel/src/kernel/governed_validation.rs:738-812).
Chio preserves that 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-root the chain at the boundary
chain_id and point the new link at the upstream request, rather than starting a fresh chain at the boundary. A chain that starts at the boundary makes prior hops unreachable to downstream auditors and loses the delegation history. Because this adapter does not populate the field, that is work the host process around it has to do.What the Adapter Covers
The adapter governs the A2A surface and stops there:
- The long-running task lifecycle is
GetTask,SubscribeToTask,CancelTask, and push-notification config CRUD. - Authentication is bearer, HTTP Basic, API key, OAuth, OpenID, or mTLS. A custom scheme terminates in front of the adapter and presents one of these.
- Admission is adapter-local: its own policy plus task correlation. Partner onboarding across a federation is the trust plane's job, not the adapter's.
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