A2A Adapter
Turn each skill on a partner's A2A agent into a tool your agents call through the kernel, with a signed receipt for every call.
On this page
Each skill on a partner's A2A Agent Card becomes a Chio tool your agents call under grants you control. The kernel checks the caller's grant for that skill before a single A2A message leaves your host.
The adapter, chio-a2a-adapter, is the outbound side: Chio calls a remote A2A 1.x agent and governs the call. To serve your own tools as an A2A agent, use the A2A edge, chio-a2a-edge, described in Bridges.
The kernel admits each skill call before the adapter speaks A2A
A2A defines discovery and transport. Chio adds admission and a signed record at the point where your agent's call becomes an outbound A2A request:
κcapabilityσscopegguardsBbudget
An allowed call returns a completed task
The edge resolves the target skill and hands the call to the kernel. An Allow verdict with a completed terminal state returns a task with status completed, the converted tool result, and the signed receipt in its metadata. A message/stream task runs this path the first time task/get resolves it. A2A edge.
- Call. Your agent calls the tool named after the partner's skill id, such as
review, in its Chio session. - Admit. The kernel checks the capability behind the session and the grant's scope for that tool, then runs the configured guards.
- Send. An admitted call becomes one A2A request on the interface the adapter selected at discovery, with the credentials the partner's card requires.
- Record. The kernel signs a
ChioReceiptfor the decision. A refused call never reaches the partner.
Each kernel-mediated hop validates its own capability and retains its own signed decision. Your host carries the context that links one hop to the next, as step 5 shows.
1. Register the partner's agent with your kernel
The adapter is a Rust library that runs in your host process beside the kernel; it has no CLI of its own. Add the crate to the host.
cargo add chio-a2a-adapterA2aAdapterConfig is a builder: set the target Agent Card URL and the manifest public key, add transport credentials and partner policy, then register the discovered adapter. The excerpt assumes an initialized kernel and a host-selected manifest key.
use chio_a2a_adapter::{A2aAdapter, A2aAdapterConfig, A2aPartnerPolicy};
let adapter = A2aAdapter::discover(
A2aAdapterConfig::new(
"https://agent.halden.example",
manifest_public_key_hex,
)
.with_tls_root_ca_pem(tls_root_ca_pem)
.with_mtls_client_auth_pem(
client_cert_chain_pem,
client_private_key_pem,
)
.with_request_header("X-Partner", "larkfield")
.with_oauth_client_credentials(client_id, client_secret)
.with_oauth_scope("a2a.invoke")
.with_partner_policy(
A2aPartnerPolicy::new("halden")
.with_required_tenant("larkfield")
.require_skill("review")
.require_security_scheme("oauthAuth")
.allow_interface_origin("https://agent.halden.example"),
)
.with_task_registry_file(".chio/a2a-task-registry.json")
)?;
kernel.register_tool_server(Box::new(adapter));Discovery reads /.well-known/agent-card.json from a base URL, or uses a full Agent Card URL directly. Every advertised skill becomes a Chio tool, invocable through the kernel like any other tool server. Supply the partner's TLS material and OAuth credentials through your deployment configuration before discovery, and keep the task-registry directory private.
2. Admit only the agent you expect
A2aPartnerPolicy turns the partner's expected contract into discovery checks. An Agent Card that differs fails discovery, and the error names the failing tenant, interface, or scheme.
with_required_tenantrefuses a selected interface that advertises a different tenant id.require_skillrefuses a card that does not expose the required skill id.require_security_schemerefuses a card whose schemes or security requirements lack the named scheme.allow_interface_originrefuses a card with no supported interface on an allowed origin.
The adapter sends only credentials that satisfy a security requirement the card declares. When a card demands a scheme the adapter does not implement, the invocation is refused locally, before any request goes upstream; terminate a custom scheme in front of the adapter and present one it supports.
3. Call the skill from your agent
The calling agent uses no A2A client library. It opens a Chio session with ChioClient, the lower-level session client in @chio-protocol/sdk and chio-sdk, and calls the tool the adapter published. Set CHIO_BASE_URL to the host's MCP endpoint and CHIO_AUTH_TOKEN to an authorized caller credential.
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: "larkfield-console", version: "0.1.0" },
});
try {
const result = await session.callTool("review", {
message: "Review PR #421 against the tests-are-evidence policy.",
metadata: { source: "larkfield-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": "larkfield-console", "version": "0.1.0"}
)
try:
result = session.call_tool(
"review",
{
"message": "Review PR #421 against the tests-are-evidence policy.",
"metadata": {"source": "larkfield-console"},
},
).get("result", {})
print(json.dumps(result, indent=2))
finally:
session.close()You should see the partner's reply as the tool result: a message for a blocking call, or an A2A task when the agent defers. The host must expose the registered review tool and grant it to this caller; without that grant, the kernel refuses the call and the partner receives nothing.
4. Follow a deferred task through the same tool
return_immediately makes the adapter return the A2A task instead of waiting for it. get_task then issues A2A GetTask on the same tool, and because follow-up modes exclude every SendMessage field, the poll carries only get_task. Run these inside the session's try block.
// The adapter answers with the A2A task rather than a terminal message.
const deferred = await session.callTool("review", {
message: "Review the open pull requests on issues #412 to #419",
return_immediately: true,
});
// Validate deferred and retain its returned task id as taskId before polling.
// Only `get_task` may be present in this follow-up.
const followUp = await session.callTool("review", {
get_task: { id: taskId, history_length: 2 },
});# The adapter answers with the A2A task rather than a terminal message.
deferred = session.call_tool(
"review",
{
"message": "Review the open pull requests on issues #412 to #419",
"return_immediately": True,
},
).get("result", {})
# Validate deferred and retain its returned task id as task_id before polling.
# Only get_task may be present in this follow-up.
follow_up = session.call_tool(
"review",
{"get_task": {"id": task_id, "history_length": 2}},
).get("result", {})You should see the deferred call return a task in TASK_STATE_WORKING and the poll return TASK_STATE_COMPLETED, the pair the adapter's own test asserts. Take the poll's taskId or task_id from the validated deferred result.
With with_task_registry_file, the adapter persists one binding per observed task id and refuses a follow-up whose task_id was not recorded for the same tool, server, binding, interface, and tenant. The binding survives adapter recreation and process restarts. It binds the route and partner context; per-caller task access remains a host or upstream check. Without a configured registry, the adapter skips this correlation check.
5. Read the receipt each hop signs
When your agent calls a partner's agent, which calls a tool at a third organization, Chio on each side signs one receipt per hop:
fixer at Larkfield
-> a2a adapter (chio, Larkfield) signed receipt R1
-> a2a server (Halden's fixer-2)
-> chio kernel (Halden) signed receipt R2
-> tool at a third org signed receipt R3The receipts link through GovernedTransactionIntent.call_chain, which your host supplies on its governed intent; the adapter passes requests through and leaves that field to the host. When the host relays a request to another organization, it carries the upstream chain_id and points the new link at the upstream request. A chain restarted at the boundary hides the upstream hops from downstream auditors.
Each link is one GovernedCallChainContext: a stable chain_id, the parent_request_id, an optional parent_receipt_id, the origin_subject at the root, and the delegator_subject that handed control to the current subject. The kernel refuses an empty chain_id, a parent_request_id equal to the current request, and an origin or delegator subject that disagrees with the validated capability lineage. Follow the parent pointers to rebuild the full path.
You should see that provenance on each signed receipt. /v1/reports/authorization-context and chio trust authorization-context list project it, alongside derived authorization-detail scope for commerce and metered-billing context.
The tool's arguments select one A2A operation
Each generated tool accepts a superset of SendMessage fields plus adapter-local modes. A follow-up or task-management mode excludes every SendMessage field.
| Tool argument | A2A operation | Example or requirement |
|---|---|---|
message, data | SendMessage | { "message": "..." } |
stream: true | SendStreamingMessage | { "message": "...", "stream": true } |
get_task | GetTask | { "get_task": { "id": "task-1", "history_length": 2 } } |
subscribe_task | SubscribeToTask | { "subscribe_task": { "id": "task-1" } } |
cancel_task | CancelTask | { "cancel_task": { "id": "task-1", "metadata": { "reason": "user-request" } } } |
| Push config modes | Push-notification config create, get, list, delete | The card advertises pushNotifications |
Task ids above are illustrative. Each upstream chunk of a stream is one stream event, and a streamed chunk is the raw A2A object, such as a statusUpdate; the full field list and an example chunk are in the reference below.
A2A adapter protocol and configuration
Search for protocolVersion, targetSkillId, mtlsSecurityScheme, or history_length.
Protocol version and bindings
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 speaks both the JSONRPC and HTTP+JSON bindings: blocking SendMessage, streaming SendStreamingMessage, follow-up GetTask, SubscribeToTask, and CancelTask, and push-notification config create, get, list, and delete.
Remote targets require https; plain http is accepted only for localhost. Push-notification callback URLs follow the same rule.
Skill selector metadata
A2A v1.0.0 defines no native skillId selector inside SendMessage. The adapter adds this local convention to top-level request metadata, which gives Chio stable per-skill tool names and per-skill capability scopes without changing the protocol:
{
"chio": {
"targetSkillId": "review",
"targetSkillName": "Research"
}
}Authentication schemes
- bearer, OAuth2-bearer, and OpenID-bearer: a bearer-style
Authorizationheader. httpAuthSecuritySchemebasic: HTTP Basic authentication.oauth2SecurityScheme: client-credentials token acquisition against the declared endpoint.openIdConnectSecurityScheme: OIDC discovery, then client credentials.mtlsSecurityScheme: mutual TLS with the configured client identity.apiKeySecurityScheme: a named API key in header, query, or cookie position.
Adapter-level request headers, query parameters, and cookies cover partner-specific requirements. Admission is adapter-local, its own partner policy plus task correlation; partner onboarding across a federation belongs to the trust control plane.
Tool fields
message: plain text sent as an A2A text part.data: structured JSON sent as an A2A data part.context_id,task_id,reference_task_ids.metadataforSendMessageRequest.metadata;message_metadataforMessage.metadata.history_length: requires the card to advertisecapabilities.stateTransitionHistory.return_immediately, andstream, the adapter-local opt-in forSendStreamingMessage.
Stream events and lifecycle validation
With stream: true, the kernel surfaces each upstream chunk as one stream event carrying the raw A2A object:
{
"statusUpdate": {
"taskId": "task-1",
"status": {
"state": "TASK_STATE_COMPLETED"
}
}
}Lifecycle payload validation fails closed. SendMessage task responses, GetTask results, and streamed task, statusUpdate, and artifactUpdate events must carry their required fields (id, status.state, taskId, and artifact where applicable). A malformed payload is rejected before it reaches the kernel.
Task registry binding
Each persisted binding records the Chio tool name, the selected interface URL, the protocol binding, the tenant and partner label, and the last observed task state and its source.
Source
- Deferred call and poll test:
crates/protocol/chio-a2a-adapter/src/tests/invoke_manifest.rs:58-80. - Call-chain context:
crates/core/chio-core-types/src/capability/governance.rs:184-196. - Call-chain validation:
crates/kernel/chio-kernel/src/kernel/governed_validation.rs:738-812.