Chio/Docs
LOGIN · JOIN

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.

Guide chio@5f63e6e

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.

Start

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:

A2A edgeChoose an outcome.

κ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.

One A2A call through the edge to the kernel and a tool server. Agent Card generation, skill projection, and fidelity evaluation are left out.Bridges
  1. Call. Your agent calls the tool named after the partner's skill id, such as review, in its Chio session.
  2. Admit. The kernel checks the capability behind the session and the grant's scope for that tool, then runs the configured guards.
  3. Send. An admitted call becomes one A2A request on the interface the adapter selected at discovery, with the credentials the partner's card requires.
  4. Record. The kernel signs a ChioReceipt for 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.

Terminalbash
cargo add chio-a2a-adapter

A2aAdapterConfig 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.

src/main.rs · in mainrust
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_tenant refuses a selected interface that advertises a different tenant id.
  • require_skill refuses a card that does not expose the required skill id.
  • require_security_scheme refuses a card whose schemes or security requirements lack the named scheme.
  • allow_interface_origin refuses 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.

caller.tscaller.py
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();
}

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.

caller.tscaller.py
// 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 },
});

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:

Receipts across three organizationstext
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 R3

The 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 argumentA2A operationExample or requirement
message, dataSendMessage{ "message": "..." }
stream: trueSendStreamingMessage{ "message": "...", "stream": true }
get_taskGetTask{ "get_task": { "id": "task-1", "history_length": 2 } }
subscribe_taskSubscribeToTask{ "subscribe_task": { "id": "task-1" } }
cancel_taskCancelTask{ "cancel_task": { "id": "task-1", "metadata": { "reason": "user-request" } } }
Push config modesPush-notification config create, get, list, deleteThe 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

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:

SendMessage metadatajson
{
  "chio": {
    "targetSkillId": "review",
    "targetSkillName": "Research"
  }
}

Authentication schemes

  • bearer, OAuth2-bearer, and OpenID-bearer: a bearer-style Authorization header.
  • httpAuthSecurityScheme basic: 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.
  • metadata for SendMessageRequest.metadata; message_metadata for Message.metadata.
  • history_length: requires the card to advertise capabilities.stateTransitionHistory.
  • return_immediately, and stream, the adapter-local opt-in for SendStreamingMessage.

Stream events and lifecycle validation

With stream: true, the kernel surfaces each upstream chunk as one stream event carrying the raw A2A object:

Stream event · illustrative task idjson
{
  "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.

Next: serve your own tools over A2A