Chio/Docs

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 aloneA2A + chio
Transport and Agent Card discoveryCapability validation and guard evaluation at the adapter boundary
Skill advertisement via Agent CardOne chio tool per skill, with stable tool names and scoping
Auth negotiation is per-integration glueDeclared schemes matched to configured credentials, fail closed otherwise
Cross-agent trust is implicitDelegated 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.json on a base URL, or a full Agent Card URL consumed directly
  • Both JSONRPC and HTTP+JSON bindings
  • Blocking SendMessage, streaming SendStreamingMessage, and follow-up GetTask, SubscribeToTask, and CancelTask
  • 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

The adapter requires 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:

json
{
  "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 declarationchio behaviour
bearer, OAuth2-bearer, OpenID-bearerBearer-style Authorization header
httpAuthSecurityScheme basicHTTP Basic authentication
oauth2SecuritySchemeClient-credentials token acquisition against the declared endpoint
openIdConnectSecuritySchemeOIDC discovery followed by client-credentials
mtlsSecuritySchemeMutual TLS using the configured client identity
apiKeySecuritySchemeNamed 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:

bash
# 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.key

Both 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:

bash
chio tools list --server a2a:agent.example.com

Configuring 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:

rust
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 part
  • data: structured JSON sent as an A2A data part
  • context_id, task_id, reference_task_ids
  • metadata for SendMessageRequest.metadata
  • message_metadata for Message.metadata
  • history_length, requires the Agent Card to advertise capabilities.stateTransitionHistory
  • return_immediately
  • stream: adapter-local opt-in for SendStreamingMessage

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

ts
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

python
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:

json
{
  "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:

json
{ "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

If the upstream agent advertises 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 methodRejects when
with_required_tenantThe selected interface advertises a different tenant id
require_skillThe Agent Card does not expose the required skill id
require_security_schemeA required scheme name is missing from the card or its security requirements
allow_interface_originNo 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:

text
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

When relaying an A2A request from one org to another, do not strip and re-sign the call chain. Boundary adapters must extend the chain, not replace it. A chain that starts at the boundary makes prior hops invisible to downstream auditors and defeats the entire point of delegated provenance.

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