Hello A2A
Call your Rust tool through the official A2A client. Discover the agent, count supplied text, and verify its signed result.
Give another agent a useful operation over HTTP, with a discoverable skill, a bounded grant and a signed record of every call. The official A2A Python client connects to the supplied Rust host, counts your text twice and receives a verifiable refusal when its allowance runs out.
Run the complete A2A application
Download the project, extract it anywhere and open its directory. Builder Setup supplies the persistent Rust and uv prerequisites. The project includes its Python SDK source and pins its Rust dependencies to the public source recorded in its manifest.
uv run --locked a2a_client.py "Review the release checklist"The command builds the host, owns its lifetime and verifies the results. Each run creates a private state directory with its authority, receipt database and complete exchange. A model key is unnecessary: this agent performs a deterministic document operation that you can replace with your own tool.
| Call | Protocol result | Signed decision | Retained receipt |
|---|---|---|---|
1 | TASK_ | allow | 630d12d93d82f777… |
2 | TASK_ | allow | a979760889013889… |
3 | TASK_ | deny | ab00d19788881954… |
Captured with a2a-sdk 1.1.2. Your identifiers will differ. The third call is an expected refusal; the verification command succeeds only when both permitted work and refusal match the configured two-call grant.
Follow the actual protocol
The host exposes an Agent Card and an authenticated JSON-RPC endpoint on an available loopback port. The client uses A2A 1.0 SendMessage with structured tool arguments, then checks GetTask against the original result. A retried message ID returns the same task without spending another invocation.
message = Message(
message_id=message_id,
role=Role.ROLE_USER,
parts=[Part(data=ParseDict({"text": text}, ProtobufValue()))],
)
response = await client.send_message(SendMessageRequest(
message=message,
metadata={"chio": {"targetSkillId": "count_words"}},
))Read Agent Card · captured discovery 37 lines · complete file
{
"name": "Chio document analyst",
"description": "Count caller-provided text under a bounded signed grant",
"supportedInterfaces": [
{
"url": "http://127.0.0.1:33067",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"version": "0.1.0",
"capabilities": {
"streaming": false,
"pushNotifications": false
},
"defaultInputModes": [
"application/json"
],
"defaultOutputModes": [
"application/json",
"text/plain"
],
"skills": [
{
"id": "count_words",
"name": "count_words",
"description": "Count whitespace-separated words and UTF-8 bytes",
"inputModes": [
"application/json"
],
"outputModes": [
"application/json",
"text/plain"
]
}
]
}Read SendMessage · actual client request 23 lines · complete file
{
"method": "SendMessage",
"params": {
"message": {
"messageId": "4d04eaf4-407f-439b-bee3-4ee84b36f85a",
"role": "ROLE_USER",
"parts": [
{
"data": {
"text": "Chio gives agent operations explicit authority and evidence"
}
}
]
},
"metadata": {
"chio": {
"targetSkillId": "count_words"
}
}
},
"id": "5217355c-5550-4563-a280-2e7ed06d5d4d",
"jsonrpc": "2.0"
}Bind the result to the operation
The client selects the kernel key from its own host before sending work. It verifies the signature, capability, exact input and parameter hash, operation ID and output hash. Streamed output is checked chunk by chunk before the aggregate digest. It also verifies every projected artifact and the retrieved task.
if not verify_receipt_with_trusted_signers(receipt, [key])["ok"]:
raise ValueError("Invalid receipt or untrusted kernel")| Evidence | Question it answers |
|---|---|
Trusted signature | Did the kernel selected by the operator sign this exact record? |
Input and operation identity | Does the record describe this call, under this grant? |
Output hashes | Are these the bytes or chunks bound to the signed result? |
Decision and tool status | Was the requested operation permitted and did it return the expected count? |
Read A2A first call · complete signed receipt 106 lines · complete file
{
"trust_level": "mediated",
"boundary_class": "prevent",
"action": {
"parameter_hash": "f0d237fe0bfdf5ee7bde0d96d6555723e2effa1281eb6d1d4ce514494b165e77",
"parameters": {
"text": "Chio gives agent operations explicit authority and evidence"
}
},
"redaction_mode": "none",
"tool_name": "count_words",
"policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
"kernel_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383",
"tool_server": "documents",
"content_hash": "8acf8908e7aeb801d9ee99e2965eb052c1d90e2e04f97d50be02ae149d079bd6",
"metadata": {
"route_selection": {
"selectedProtocols": [
"a2a",
"native"
],
"selectedRouteId": "native-route",
"requestedTargetProtocol": "native",
"selectedTargetProtocol": "native",
"candidates": [
{
"selectedProtocols": [
"a2a",
"native"
],
"routeId": "native-route",
"available": true,
"targetProtocol": "native"
}
],
"decision": "select",
"routeSelectionId": "fe26d5b5a64ff93b186824e0cc61311ed384ac1dcfeeafb8d3d399b6fc33a0d0",
"sourceProtocol": "a2a"
},
"admission_operation": {
"retained_dispatch_commit": {
"committed_version": 6,
"provider_attempt": {
"transport_key_epoch": 1,
"attempt_id": "attempt:5cd58f78cab93490cd27f7590ec734ea33284f55e3d250ff1dd687ba21deaef5",
"operation_id": "5cd58f78cab93490cd27f7590ec734ea33284f55e3d250ff1dd687ba21deaef5",
"transport_id": "kernel-tool-server:documents"
},
"coordinator_lease_epoch": 1,
"store_fence": {
"lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3",
"store_uuid": "01a08ea1-c9d3-77f1-84e1-57c471839cb6",
"owner_epoch": 1
},
"coordinator_lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3"
},
"tool_outcome_id": "2df324d9b326f2ade4bb9c97a674cf2d7345b3031f2c1d16cf840622af0692f9",
"trusted_time_unix_ms": 1789099363717,
"projected_operation_version": 8,
"projected_state": "completed",
"schema": "chio.admission-receipt.v1",
"request_namespace_digest": "08d7f6a8ba0a6a0ebe77a058222095ae5782509e270c73f75e7fb5fcc0c87ebc",
"coordinator_lease_epoch": 1,
"operation_id": "5cd58f78cab93490cd27f7590ec734ea33284f55e3d250ff1dd687ba21deaef5",
"compensation_status": "not_compensated",
"request_binding_hash": "b9483b689b03e16d9cd0b41ba788d3a880574b0884877dcbef158c35a8a7c6c0",
"tool_outcome_version": 2,
"projected_dispatch_state": "terminal",
"request_id": "a2a-message:814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825:4d04eaf4-407f-439b-bee3-4ee84b36f85a",
"store_fence": {
"lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3",
"store_uuid": "01a08ea1-c9d3-77f1-84e1-57c471839cb6",
"owner_epoch": 1
},
"coordinator_lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3"
},
"receipt_context": {
"request_id": "a2a-message:814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825:4d04eaf4-407f-439b-bee3-4ee84b36f85a"
},
"attribution": {
"grant_index": 0,
"delegation_depth": 0,
"subject_key": "814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825",
"issuer_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383"
},
"stream": {
"chunks_received": 2,
"chunks_expected": 2,
"total_bytes": 77,
"chunk_hashes": [
"c59ae49b669759807ff89ce98ab61530df38c047104676a7defc2e7d301324e3",
"b2538f4ac125b83068662cf8f4dc7966e50d1bcbfce089b5824edea204d91624"
]
},
"chio_receipt_signing_nonce": "rcpt-01a08ea1-cdaa-748c-a529-317ec183de4d"
},
"id": "630d12d93d82f7773ba04bae94376d77818db661c69231e4ffe88753b70ccaa0",
"tool_origin": "caller_executed",
"capability_id": "cap-01a08ea1-cbce-7282-b0ab-190a5b50e499",
"signature": "f9e2165a32ff8182770dfb750e28277c5668b250080ace54c6a5146c3ac59d8ae02d1e697709944bac2ac00691dfa3dd495f541c90a33ae6ce9fc181bfd34507",
"decision": {
"verdict": "allow"
},
"timestamp": 1789099363,
"receipt_kind": "mediated_decision"
}Read A2A third call · complete signed refusal 66 lines · complete file
{
"trust_level": "mediated",
"boundary_class": "prevent",
"action": {
"parameter_hash": "f0d237fe0bfdf5ee7bde0d96d6555723e2effa1281eb6d1d4ce514494b165e77",
"parameters": {
"text": "Chio gives agent operations explicit authority and evidence"
}
},
"redaction_mode": "none",
"tool_name": "count_words",
"policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
"kernel_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383",
"tool_server": "documents",
"content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
"metadata": {
"route_selection": {
"selectedProtocols": [
"a2a",
"native"
],
"selectedRouteId": "native-route",
"requestedTargetProtocol": "native",
"selectedTargetProtocol": "native",
"candidates": [
{
"selectedProtocols": [
"a2a",
"native"
],
"routeId": "native-route",
"available": true,
"targetProtocol": "native"
}
],
"decision": "select",
"routeSelectionId": "9ed946ad973f8e988786f0cd80309546d67d773f44fe50c3c70c4341aa6819ee",
"sourceProtocol": "a2a"
},
"receipt_context": {
"request_id": "a2a-message:814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825:0c31844e-c67f-4477-9420-b36e1fa6efa9"
},
"attribution": {
"delegation_depth": 0,
"subject_key": "814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825",
"issuer_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383"
},
"budget_authority": {
"guarantee_level": "single_node_atomic",
"metering_profile": "max_cost_preauthorize_then_reconcile_actual",
"authority_profile": "authoritative_hold_event"
},
"chio_receipt_signing_nonce": "rcpt-01a08ea1-cf53-7211-8b17-ff6d409298d9"
},
"id": "ab00d1978888195456fb894b842506d65caebd08c31734a03b55d0778e5f104e",
"tool_origin": "caller_executed",
"capability_id": "cap-01a08ea1-cbce-7282-b0ab-190a5b50e499",
"signature": "e76242b95a04cbb9628addab399033f95c71ef570b5bbc4a749fbb3e5a2be365c941d946f9b2e80034ad1d6fed84cf55ab832ab8433df835f49ad0d8eb650700",
"decision": {
"guard": "kernel",
"verdict": "deny",
"reason": "invocation budget exhausted for capability cap-01a08ea1-cbce-7282-b0ab-190a5b50e499"
},
"timestamp": 1789099364,
"receipt_kind": "mediated_decision"
}Download the complete captured exchange, including the selected public key and verification results. The original record is retained; the table above only shortens identifiers for reading.
Make the operation yours
Replace the handler in src/service.rs, update its manifest schema and adjust authority.json to grant the intended operation. The host registers the tool, creates a durable kernel and issues the bounded capability. Keep the verifier’s input and output checks aligned with your new contract.
Read authority.json 6 lines · complete file
{
"schema": "document-tools.authority.v1",
"tool": "count_words",
"max_invocations": 2,
"lifetime_seconds": 1800
}Read src/service.rs 93 lines · complete file
use chio_kernel::{
KernelError, NestedFlowBridge, ToolCallChunk, ToolCallStream, ToolServerConnection,
ToolServerStreamResult,
};
use chio_manifest::{ToolDefinition, ToolManifest};
use serde_json::{json, Value};
pub const SERVER: &str = "documents";
pub const TOOL: &str = "count_words";
pub const TEXT_LIMIT: usize = 65_536;
pub struct DocumentTools;
fn count(tool: &str, arguments: &Value) -> Result<Value, KernelError> {
if tool != TOOL {
return Err(KernelError::ToolNotRegistered(tool.to_owned()));
}
let text = arguments
.get("text")
.and_then(Value::as_str)
.ok_or_else(|| KernelError::RequestIncomplete("text must be a string".into()))?;
if text.len() > TEXT_LIMIT {
return Err(KernelError::RequestIncomplete(
"text exceeds 65536 UTF-8 bytes".into(),
));
}
Ok(json!({"words": text.split_whitespace().count(), "bytes": text.len()}))
}
#[async_trait::async_trait]
impl ToolServerConnection for DocumentTools {
fn server_id(&self) -> &str {
SERVER
}
fn tool_names(&self) -> Vec<String> {
vec![TOOL.into()]
}
async fn invoke(
&self,
tool: &str,
arguments: Value,
_nested: Option<&mut dyn NestedFlowBridge>,
) -> Result<Value, KernelError> {
count(tool, &arguments)
}
async fn invoke_stream(
&self,
tool: &str,
arguments: Value,
_nested: Option<&mut dyn NestedFlowBridge>,
) -> Result<Option<ToolServerStreamResult>, KernelError> {
let result = count(tool, &arguments)?;
// These chunks are collated by the edge into one terminal response.
Ok(Some(ToolServerStreamResult::Complete(ToolCallStream {
chunks: vec![
ToolCallChunk { data: result },
ToolCallChunk {
data: json!({"content": [
{"type": "text", "text": "Document counted"}
]}),
},
],
})))
}
}
pub fn manifest(public_key: String) -> ToolManifest {
ToolManifest {
schema: "chio.manifest.v1".into(),
server_id: SERVER.into(),
name: "Document tools".into(),
version: "1.0.0".into(),
description: Some("Count words in caller-provided text".into()),
tools: vec![ToolDefinition {
name: TOOL.into(),
description: "Count whitespace-separated words and UTF-8 bytes".into(),
input_schema: json!({
"type": "object", "properties": {"text": {"type": "string"}},
"required": ["text"], "x-chio-streaming": true,
"x-chio-partial-output": true, "x-chio-cancellation": true
}),
output_schema: None,
pricing: None,
has_side_effects: false,
latency_hint: None,
}],
server_tools: Vec::new(),
required_permissions: None,
public_key,
}
}Read src/bin/a2a-host.rs 133 lines · complete file
#[path = "../host.rs"]
mod host;
#[path = "../service.rs"]
mod service;
use axum::{
extract::{DefaultBodyLimit, State},
http::{HeaderMap, StatusCode},
routing::{get, post},
Json, Router,
};
use chio_a2a_edge::{A2aEdgeConfig, A2aKernelExecutionContext, ChioA2aEdge};
use serde_json::{json, Value};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
struct App {
edge: Mutex<ChioA2aEdge>,
kernel: chio_kernel::ChioKernel,
execution: A2aKernelExecutionContext,
token: String,
card: Value,
blocking: chio_example_runtime::tasks::BlockingPool,
}
async fn invoke(
State(app): State<Arc<App>>,
headers: HeaderMap,
Json(message): Json<Value>,
) -> Result<Json<Value>, (StatusCode, &'static str)> {
let supplied = headers
.get("authorization")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.strip_prefix("Bearer "))
.unwrap_or("");
if !chio_example_runtime::credential_matches(supplied, &app.token) {
return Err((
StatusCode::UNAUTHORIZED,
"A valid application credential is required",
));
}
let pool = app.blocking.clone();
let response = pool
.run(move || {
let mut edge = app
.edge
.lock()
.map_err(|_| anyhow::anyhow!("Protocol state is unavailable"))?;
Ok(edge
.handle_v1_jsonrpc(message, &app.kernel, &app.execution)
.into_value())
})
.await
.map_err(|error| {
eprintln!("Protocol execution refused: {error}");
(
StatusCode::SERVICE_UNAVAILABLE,
"Protocol execution unavailable",
)
})?;
response
.map(Json)
.ok_or((StatusCode::BAD_REQUEST, "A2A requests require an ID"))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let directory = PathBuf::from(
std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("Usage: a2a-host NEW_SESSION_DIRECTORY"))?,
);
let host = host::boot(&directory)?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let endpoint = format!("http://{}", listener.local_addr()?);
let edge = ChioA2aEdge::new(
A2aEdgeConfig {
agent_name: "Chio document analyst".into(),
agent_description: "Count caller-provided text under a bounded signed grant".into(),
endpoint_url: endpoint.clone(),
..A2aEdgeConfig::default()
},
vec![host.manifest],
)?;
let token = chio_core::crypto::Keypair::generate().public_key().to_hex();
std::fs::write(directory.join("client-token.txt"), &token)?;
let execution = A2aKernelExecutionContext {
agent_id: host.capability.subject.to_hex(),
capability: host.capability,
dpop_proof: None,
execution_nonce: None,
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
model_metadata: None,
};
let app = Arc::new(App {
card: edge.agent_card_v1(),
edge: Mutex::new(edge),
kernel: host.kernel,
execution,
token,
blocking: chio_example_runtime::tasks::BlockingPool::new(2),
});
let router = Router::new()
.route("/", post(invoke))
.route(
"/.well-known/agent-card.json",
get(|State(app): State<Arc<App>>| async move { Json(app.card.clone()) }),
)
.layer(DefaultBodyLimit::max(65_536))
.with_state(app.clone());
std::fs::write(
directory.join("server.json"),
serde_json::to_vec_pretty(&json!({"endpoint":endpoint}))?,
)?;
eprintln!(
"A2A 1.0 listening at {endpoint}; credential: {}",
directory.join("client-token.txt").display()
);
let signal = chio_example_runtime::shutdown_signal()?;
let served = axum::serve(listener, router)
.with_graceful_shutdown(signal)
.await;
let drained = app.blocking.drain(std::time::Duration::from_secs(15)).await;
served?;
drained?;
Ok(())
}Read src/host.rs 124 lines · complete file
use chio_core::{
capability::{
scope::{ChioScope, Operation, ToolGrant},
token::CapabilityToken,
},
crypto::Keypair,
};
use chio_kernel::{
ChioKernel, KernelConfig, DEFAULT_CHECKPOINT_BATCH_SIZE, DEFAULT_MAX_STREAM_DURATION_SECS,
DEFAULT_MAX_STREAM_TOTAL_BYTES,
};
use chio_manifest::ToolManifest;
use std::path::Path;
pub struct Host {
pub kernel: ChioKernel,
pub capability: CapabilityToken,
pub manifest: ToolManifest,
}
pub fn boot(directory: &Path) -> anyhow::Result<Host> {
let authority_bytes = std::fs::read("authority.json")?;
let config: serde_json::Value = serde_json::from_slice(&authority_bytes)?;
let fields = config
.as_object()
.ok_or_else(|| anyhow::anyhow!("Authority must be an object"))?;
anyhow::ensure!(
fields.keys().all(
|key| ["schema", "tool", "max_invocations", "lifetime_seconds"].contains(&key.as_str())
),
"Unknown authority field"
);
anyhow::ensure!(
config["schema"] == "document-tools.authority.v1",
"Unknown authority schema"
);
anyhow::ensure!(
config["tool"] == crate::service::TOOL,
"This host grants only count_words"
);
let call_limit = config["max_invocations"]
.as_u64()
.filter(|v| (1..=50).contains(v))
.ok_or_else(|| anyhow::anyhow!("max_invocations must be an integer from 1 to 50"))?
as u32;
let lifetime = config["lifetime_seconds"]
.as_u64()
.filter(|v| (1..=3600).contains(v))
.ok_or_else(|| anyhow::anyhow!("lifetime_seconds must be an integer from 1 to 3600"))?;
// A new application run gets a fresh session. Refuse to reuse its directory.
std::fs::create_dir(directory)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700))?;
}
std::fs::write(directory.join("authority.json"), &authority_bytes)?;
let authority =
chio_control_plane::DurableAdmissionRuntime::open(&directory.join("admission.db"))?;
let keypair = authority.kernel_keypair();
// The host chooses this key before any receipt exists.
std::fs::write(
directory.join("kernel-public-key.txt"),
keypair.public_key().to_hex(),
)?;
let mut kernel = ChioKernel::new(KernelConfig {
keypair,
ca_public_keys: Vec::new(),
max_delegation_depth: 3,
policy_hash: chio_core::sha256_hex(&authority_bytes),
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: DEFAULT_MAX_STREAM_DURATION_SECS,
max_stream_total_bytes: DEFAULT_MAX_STREAM_TOTAL_BYTES,
require_web3_evidence: false,
allow_ephemeral_receipt_log: true,
allow_ephemeral_revocation_store: true,
checkpoint_batch_size: DEFAULT_CHECKPOINT_BATCH_SIZE,
retention_config: None,
memory_budget: chio_kernel::MemoryBudgetConfig::defaults(),
deadlines: chio_kernel::HotPathDeadlineConfig::default(),
});
kernel.set_receipt_store(Box::new(chio_store_sqlite::SqliteReceiptStore::open(
directory.join("receipts.db"),
)?))?;
authority.attach(&mut kernel)?;
let service_key = Keypair::generate();
let manifest = crate::service::manifest(service_key.public_key().to_hex());
let signed = chio_manifest::sign_manifest(&manifest, &service_key)?;
chio_manifest::verify_manifest(&signed, &service_key.public_key())?;
std::fs::write(
directory.join("manifest.json"),
serde_json::to_vec_pretty(&signed)?,
)?;
kernel.register_tool_server(Box::new(crate::service::DocumentTools));
let agent = Keypair::generate();
let capability = kernel.issue_capability(
&agent.public_key(),
ChioScope {
grants: vec![ToolGrant {
server_id: crate::service::SERVER.into(),
tool_name: crate::service::TOOL.into(),
operations: vec![Operation::Invoke],
constraints: Vec::new(),
max_invocations: Some(call_limit),
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..ChioScope::default()
},
lifetime,
)?;
std::fs::write(
directory.join("capability.json"),
serde_json::to_vec_pretty(&capability)?,
)?;
Ok(Host {
kernel,
capability,
manifest,
})
}Read a2a_client.py 161 lines · complete file
# /// script
# requires-python = ">=3.11"
# dependencies = ["a2a-sdk==1.1.2", "chio-sdk"]
# [tool.uv.sources]
# chio-sdk = { path = "sdk/chio-py" }
# ///
"""Call a real Chio HTTP agent with the official A2A Python client."""
import argparse
import asyncio
import json
import os
from pathlib import Path
import subprocess
import time
import uuid
import httpx
from a2a.client import A2ACardResolver
from a2a.client.transports.jsonrpc import JsonRpcTransport
from a2a.types import GetTaskRequest, Message, Part, Role, SendMessageRequest
from google.protobuf.json_format import MessageToDict, ParseDict
from google.protobuf.struct_pb2 import Value as ProtobufValue
from chio.invariants import canonicalize_json, sha256_hex_utf8, verify_receipt_with_trusted_signers
ROOT = Path(__file__).resolve().parent
def projected_parts(value):
if isinstance(value, str):
return [{"text": value}]
if isinstance(value, dict) and isinstance(value.get("content"), list):
return [{"text": part["text"]} for part in value["content"] if isinstance(part.get("text"), str)]
if isinstance(value, (dict, list)):
return [{"data": value}]
return [{"text": json.dumps(value, separators=(",", ":"))}]
def verify_task(task, text, key):
receipt = task["metadata"]["chio"]["receipt"]
if not verify_receipt_with_trusted_signers(receipt, [key])["ok"]:
raise ValueError("Invalid receipt or untrusted kernel")
if task["metadata"]["chio"]["receiptId"] != receipt["id"]:
raise ValueError("Task references another receipt")
if receipt["action"]["parameters"] != {"text": text}:
raise ValueError("Receipt binds different tool input")
if sha256_hex_utf8(canonicalize_json({"text": text})) != receipt["action"]["parameter_hash"]:
raise ValueError("Input hash differs")
allowed = receipt["decision"]["verdict"] == "allow"
if allowed != (task["status"]["state"] == "TASK_STATE_COMPLETED"):
raise ValueError("Task status differs from the signed decision")
if allowed:
output = task["artifacts"][0]["parts"][0]["data"]
if output != {"words": len(text.split()), "bytes": len(text.encode())}:
raise ValueError("The agent did not return the actual document count")
retained = task["metadata"]["chio"]["retainedOutput"]
if retained["kind"] == "stream":
hashes = [sha256_hex_utf8(canonicalize_json(chunk)) for chunk in retained["chunks"]]
if hashes != receipt["metadata"]["stream"]["chunk_hashes"]:
raise ValueError("A retained stream chunk differs from its signed hash")
content_hash = sha256_hex_utf8("".join(hashes))
expected_parts = [part for chunk in retained["chunks"] for part in projected_parts(chunk)]
if retained["chunks"][0] != output:
raise ValueError("A2A projected another document count")
else:
content_hash = sha256_hex_utf8(canonicalize_json(retained["value"]))
expected_parts = projected_parts(retained["value"])
if retained["value"] != output:
raise ValueError("A2A projected another tool output")
if task["artifacts"][0]["parts"] != expected_parts:
raise ValueError("A projected artifact changed after the kernel signed its output")
if content_hash != receipt["content_hash"]:
raise ValueError("Tool output differs from its signed hash")
return receipt
async def exercise(endpoint, token, key, capability, text, directory):
async with httpx.AsyncClient(timeout=30) as public:
refused = await public.post(endpoint, json={"jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": {}})
if refused.status_code != 401:
raise ValueError("Missing application credential was not refused")
wire = []
async def capture(response):
await response.aread()
if response.request.method == "POST":
wire.append({"request": json.loads(response.request.content), "response": response.json()})
(directory / "wire.json").write_text(json.dumps(wire, indent=2) + "\n")
async with httpx.AsyncClient(headers={"Authorization": "Bearer " + token}, timeout=30,
event_hooks={"response": [capture]}) as http:
card = await A2ACardResolver(http, endpoint).get_agent_card()
client = JsonRpcTransport(http, card, endpoint)
records = []
for number in range(3):
message_id = str(uuid.uuid4())
request = SendMessageRequest(message=Message(message_id=message_id, role=Role.ROLE_USER,
parts=[Part(data=ParseDict({"text": text}, ProtobufValue()))]), metadata={"chio": {"targetSkillId": "count_words"}})
response = await client.send_message(request)
task = MessageToDict(response)["task"]
receipt = verify_task(task, text, key)
if receipt["decision"]["verdict"] != ("allow" if number < 2 else "deny"):
raise ValueError("The third call must hit the two-invocation grant")
expected_request = "a2a-message:" + capability["subject"] + ":" + message_id
if receipt["capability_id"] != capability["id"]:
raise ValueError("Receipt belongs to another capability")
if receipt["metadata"]["receipt_context"]["request_id"] != expected_request:
raise ValueError("Receipt belongs to another protocol message")
lookup = MessageToDict(await client.get_task(GetTaskRequest(id=task["id"])))
if lookup != task:
raise ValueError("GetTask returned another task or changed its evidence")
if number == 0:
retry = MessageToDict(await client.send_message(request))["task"]
if retry != task:
raise ValueError("An identical retry did not retain the original result")
records.append({"message_id": message_id, "task": task, "verified": True})
print(task["status"]["state"] + ": " + receipt["id"])
capture_record = {"client": {"package": "a2a-sdk", "version": "1.1.2", "transport": "JsonRpcTransport"},
"trusted_kernel_key": key, "agent_card": MessageToDict(card), "runs": records,
"wire": wire, "verification": {"ok": True, "missing_credential_refused": True,
"identical_retry_retained": True, "task_lookup_bound": True, "quota_refused": True}}
(directory / "a2a-capture.json").write_text(json.dumps(capture_record, indent=2) + "\n")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("text", nargs="?", default="Chio gives agent operations explicit authority and evidence")
args = parser.parse_args()
if not 0 < len(args.text.encode()) <= 60_000:
parser.error("text must contain 1 to 60000 UTF-8 bytes")
subprocess.run(["cargo", "build", "--locked", "--bin", "a2a-host"], cwd=ROOT, check=True)
target = Path(os.environ.get("CARGO_TARGET_DIR", ROOT / "target"))
if not target.is_absolute():
target = ROOT / target
os.umask(0o077)
(ROOT / ".state").mkdir(exist_ok=True)
directory = ROOT / ".state" / ("a2a-" + str(uuid.uuid4()))
with (ROOT / ".state" / (directory.name + ".log")).open("w") as log:
process = subprocess.Popen([str(target / "debug/a2a-host"), str(directory)], cwd=ROOT,
stdout=log, stderr=log, stdin=subprocess.DEVNULL)
try:
deadline = time.monotonic() + 30
while not (directory / "server.json").exists():
if process.poll() is not None or time.monotonic() > deadline:
raise RuntimeError("A2A host did not become ready; inspect its .state log")
time.sleep(.1)
endpoint = json.loads((directory / "server.json").read_text())["endpoint"]
token = (directory / "client-token.txt").read_text().strip()
key = (directory / "kernel-public-key.txt").read_text().strip()
capability = json.loads((directory / "capability.json").read_text())
asyncio.run(exercise(endpoint, token, key, capability, args.text, directory))
print("Evidence:", directory.relative_to(ROOT) / "a2a-capture.json")
finally:
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
if __name__ == "__main__":
main()Choose the delivery contract
This example is a blocking HTTP agent. It advertises JSON input and JSON/text output, with no SSE, push notifications or asynchronous continuations. Task lookup is scoped to the caller and retained for five minutes, up to 128 results; SQLite kernel receipts persist after shutdown. Missing application credentials are refused before execution.
For a caller-owned stdio session, continue with Hello ACP. Both use the same registered document tool and authority configuration.
Reference: earlier custom A2A tool profile and captured examples
This reference preserves the earlier custom methods and their recorded behavior. Use the official-client application above for the standard protocol. The complete project retains these older clients for comparison.
Give other agents a governed operation
Chio’s Rust kernel lets an agentic operating system expose tools across protocol boundaries while retaining control of execution. An agent-facing skill identifies the operation; a signed capability determines the authority to call it. A terminal result carries the kernel’s signed decision.
This project makes that path concrete: count words in a document you supply, then try a deferred task and a refused third call. You own the tool, host, client and evidence. It uses Chio’s A2A JSON-RPC profile over local stdio. The included Agent Card describes the skill; the project opens no HTTP listener.
Count your document
Use the same extracted project as the official client. The custom profile accepts a UTF-8 document file. Its launcher builds the host and runs the local client:
python3 run.py a2a sample.mdALLOW 6 words, 43 UTF-8 bytes
Receipt: 37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26
Evidence: runs/ac367b60-a095-4fcd-bc94-ecb0c8537ccdReplace sample.md with your own UTF-8 file, up to 65,536 bytes. The sample contains six whitespace-separated words. Each run prints its own evidence directory; receipt and directory IDs will differ. The client reads your file before sending its text. Chio mediates the tool invocation, while that initial file read belongs to the application.
Discover the document skill
The host saves this complete Agent Card as discovery.json in your run directory. It advertises count_words and explains the edge’s delivery behavior through bridgeFidelity:
{
"name": "Document tools",
"description": "Count caller-provided text under a Chio capability",
"version": "0.1.0",
"supportedInterfaces": [
{
"url": "http://127.0.0.1:8080/a2a",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"capabilities": {
"streaming": true,
"pushNotifications": false,
"stateTransitionHistory": false
},
"defaultInputModes": [
"text"
],
"defaultOutputModes": [
"text"
],
"skills": [
{
"id": "count_words",
"name": "count_words",
"description": "Count whitespace-separated words and UTF-8 bytes",
"tags": [],
"inputModes": [
"text"
],
"outputModes": [
"text"
],
"bridgeFidelity": {
"kind": "adapted",
"caveats": [
"stream-capable tools execute through `message/stream` deferred tasks; output is surfaced on follow-up `task/get` rather than incremental transport updates",
"stream chunks are collated into the terminal task payload instead of pushed as incremental A2A events",
"partial output is preserved only in the terminal task payload, not incremental updates",
"cancellation is available only for deferred `message/stream` tasks; blocking `message/send` remains terminal"
]
}
}
]
}The card’s HTTP URL is advertising metadata. The supplied client launches the host on stdio; deploying an HTTP A2A endpoint requires a transport and an appropriate endpoint URL. Discovery describes what the host exposes. It does not issue a capability to the caller.
Send structured arguments and read the result
The client supplies a data part containing the tool’s text argument and explicitly selects the skill through metadata.chio.targetSkillId. A text part alone is translated into a message argument; it would not supply this tool’s text field.
{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{
"type": "data",
"data": {
"text": "One governed tool, two protocol surfaces.\n\n"
}
}
]
},
"metadata": {
"chio": {
"targetSkillId": "count_words"
}
}
}
}{
"id": 1,
"jsonrpc": "2.0",
"result": {
"id": "a2a-task-1",
"message": {
"parts": [
{
"data": {
"bytes": 43,
"words": 6
},
"type": "data"
},
{
"text": "Document counted",
"type": "text"
}
],
"role": "agent"
},
"metadata": {
"chio": {
"receiptId": "37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26",
"decision": "allow",
"authorityPath": "cross_protocol_orchestrator"
}
},
"status": "completed"
}
}The result contains the count and a receipt ID. This projection omits the embedded receipt and detailed bridge metadata; download the complete response. Even this blocking call uses the tool’s stream implementation, whose two chunks are collated into the terminal message.
Follow the deferred task
Source
Create now, execute on resolution
python3 client.py a2a sample.md --deferredTask a2a-task-1: working; execution pending
ALLOW 6 words, 43 UTF-8 bytes
Receipt: 622bd0bc98733b0ac5bd0af57aecc375b0317b4fc2dd996e024581af5fb2c546
Evidence: runs/dae5e03a-dc59-49ea-a7cf-5317faa62f2amessage/stream creates a working task. No tool has executed and no tool receipt exists at that point. The client keeps the returned task ID and uses it in task/get:
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"id": "a2a-task-1",
"metadata": {
"chio": {
"receiptId": null,
"decision": "pending",
"receiptPending": true,
"receiptBearing": false,
"authorityPath": "cross_protocol_orchestrator"
}
},
"status": "working",
"statusMessage": "Task accepted for authoritative deferred execution."
}
}{
"jsonrpc": "2.0",
"id": 2,
"method": "task/get",
"params": {
"taskId": "a2a-task-1"
}
}task/get runs the kernel decision and returns the terminal result with its receipt. Read the complete resolved response or the full request/response exchange. The task ID comes from the actual response, so the client does not assume a particular counter value.
Cancel before the tool runs
python3 client.py a2a sample.md --cancelTask a2a-task-1: working; execution pending
Task a2a-task-1: cancelled; no tool execution receipt
Evidence: runs/7deae84d-2e93-43ed-8d79-307a71b00f31{
"id": 2,
"jsonrpc": "2.0",
"result": {
"id": "a2a-task-1",
"metadata": {
"chio": {
"receiptId": null,
"decision": "cancelled",
"receiptBearing": false,
"authorityPath": "cross_protocol_orchestrator"
}
},
"status": "cancelled",
"statusMessage": "Task cancelled by caller."
}
}The cancellation response is task metadata, not a signed tool execution receipt. It does not consume an invocation in this project. Resolving that cancelled task does not run the tool. Read the complete cancellation response.
Change what the session permits
The host issues one signed grant for count_words. This application configuration sets its limit and lifetime:
{
"schema": "document-tools.authority.v1",
"tool": "count_words",
"max_invocations": 2,
"lifetime_seconds": 1800
}python3 client.py a2a sample.md --repeat 3ALLOW 6 words, 43 UTF-8 bytes
Receipt: 26813f4e9094993fff97f483df764d50a6c7d39eb11d4a2c4f3a672e32ddcaed
ALLOW 6 words, 43 UTF-8 bytes
Receipt: ad20488f54339f7163e6710ab34f46e2d226bb5dc9184f96d87ed42336a460d1
DENY invocation budget exhausted for capability cap-01a085f0-9bdd-73c2-a6bf-0f2150c4e2e2
Receipt: 60b1a0e8e90b1a5cffbf615fb26b82a3fc16c38c67ec009a69c7b8e1b97b1e82
Evidence: runs/19cb6711-4f3c-468d-8d42-fea5a4f1d357Change max_invocations to 3 in authority.json, then run with --repeat 4. Three calls succeed and the fourth is denied; no rebuild is required. The host hashes these configuration bytes into the policy context and saves the exact configuration with the run.
This file configures capability issuance for this application; it is not HushSpec. Unknown fields are rejected. Repeats share one grant, while an explicit new program start creates a fresh session. A deployed operator must authenticate callers and control that issuance. The host implementation below shows the mechanism.
Inspect the complete issued capability and original execution-context example
{
"schema": "chio.capability.v1",
"id": "cap-01a085f0-9547-74a1-bde1-efec269b5f4d",
"issuer": "e7919f3983edc12eef4ec27392ae19f1637a035602f7892c7b331aa2760fee6a",
"subject": "716d0870f526f3bced3f59f180f9a0040558291412b6ebb7a60b2aaf4f6c2959",
"scope": {
"grants": [
{
"server_id": "documents",
"tool_name": "count_words",
"operations": [
"invoke"
],
"max_invocations": 2
}
]
},
"issued_at": 1788953531,
"expires_at": 1788955331,
"signature": "460ec183e96bc836d24a888e6276985bfd67a7c6fb77955d8078f4d1f49199e7306b134d59090e47672bc4b95b3f14e32ee1a5df87b053a6458868916393930f"
}let capability = kernel
.issue_capability(
&agent.public_key(),
ChioScope {
grants: vec![ToolGrant {
server_id: SERVER_ID.to_string(),
tool_name: TOOL_NAME.to_string(),
operations: vec![Operation::Invoke],
constraints: vec![],
max_invocations: None,
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..ChioScope::default()
},
300,
)
.map_err(|error| -> Box<dyn Error + Send + Sync> {
format!("issue capability: {error}").into()
})?;
let execution = A2aKernelExecutionContext {
capability,
agent_id: agent.public_key().to_hex(),
dpop_proof: None,
execution_nonce: None,
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
model_metadata: None,
};Keep the signed decision
This is the complete receipt from the first allowed count. Its action binds the supplied text; its metadata records the stream chunk hashes and bridge context. The surrounding JSON-RPC task envelope is not itself the signed receipt.
{
"id": "37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26",
"timestamp": 1788953531,
"capability_id": "cap-01a085f0-9547-74a1-bde1-efec269b5f4d",
"tool_server": "documents",
"tool_name": "count_words",
"action": {
"parameters": {
"text": "One governed tool, two protocol surfaces.\n\n"
},
"parameter_hash": "8388b82fbbe71be998f4f0f277f05dc14446b16a52da29070bb44233d3824e73"
},
"decision": {
"verdict": "allow"
},
"receipt_kind": "mediated_decision",
"boundary_class": "prevent",
"tool_origin": "caller_executed",
"redaction_mode": "none",
"content_hash": "5170e2c24e777080378742d62d0915795e8f6b8897b489613fc6ceb17c2ae212",
"policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
"metadata": {
"admission_operation": {
"compensation_status": "not_compensated",
"coordinator_lease_epoch": 1,
"coordinator_lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
"operation_id": "4732ae8861ba703f1d190c2d4d873193f4242a702727f23f7cf7912e95e24272",
"projected_dispatch_state": "terminal",
"projected_operation_version": 8,
"projected_state": "completed",
"request_binding_hash": "7e82269b6a27a31d9b6253a48c10665c0b33fde826fec41c8fd54900f6923059",
"request_id": "a2a-a2a-task-1",
"request_namespace_digest": "75867a2970677197f6cbba2268fea29cb2d6bff896d9e36773434c1632001d53",
"retained_dispatch_commit": {
"committed_version": 6,
"coordinator_lease_epoch": 1,
"coordinator_lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
"provider_attempt": {
"attempt_id": "attempt:4732ae8861ba703f1d190c2d4d873193f4242a702727f23f7cf7912e95e24272",
"operation_id": "4732ae8861ba703f1d190c2d4d873193f4242a702727f23f7cf7912e95e24272",
"transport_id": "kernel-tool-server:documents",
"transport_key_epoch": 1
},
"store_fence": {
"lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
"owner_epoch": 1,
"store_uuid": "01a085f0-9450-78d2-87c6-01dc8b1adec7"
}
},
"schema": "chio.admission-receipt.v1",
"store_fence": {
"lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
"owner_epoch": 1,
"store_uuid": "01a085f0-9450-78d2-87c6-01dc8b1adec7"
},
"tool_outcome_id": "7088d2985a9a9864ea29764e20b61b4e0284d1a0c160ba91bd1f8a97f01bc7e2",
"tool_outcome_version": 2,
"trusted_time_unix_ms": 1788953531927
},
"attribution": {
"delegation_depth": 0,
"grant_index": 0,
"issuer_key": "e7919f3983edc12eef4ec27392ae19f1637a035602f7892c7b331aa2760fee6a",
"subject_key": "716d0870f526f3bced3f59f180f9a0040558291412b6ebb7a60b2aaf4f6c2959"
},
"chio_receipt_signing_nonce": "rcpt-01a085f0-9659-76f0-ab18-426e795d1c2b",
"receipt_context": {
"request_id": "a2a-a2a-task-1"
},
"route_selection": {
"candidates": [
{
"available": true,
"routeId": "native-route",
"selectedProtocols": [
"a2a",
"native"
],
"targetProtocol": "native"
}
],
"decision": "select",
"requestedTargetProtocol": "native",
"routeSelectionId": "fe26d5b5a64ff93b186824e0cc61311ed384ac1dcfeeafb8d3d399b6fc33a0d0",
"selectedProtocols": [
"a2a",
"native"
],
"selectedRouteId": "native-route",
"selectedTargetProtocol": "native",
"sourceProtocol": "a2a"
},
"stream": {
"chunk_hashes": [
"d453126289fdef1956695f0dfd57d82e211d255969ed5ffcf0b54cd59a054f95",
"b2538f4ac125b83068662cf8f4dc7966e50d1bcbfce089b5824edea204d91624"
],
"chunks_expected": 2,
"chunks_received": 2,
"total_bytes": 77
}
},
"trust_level": "mediated",
"kernel_key": "e7919f3983edc12eef4ec27392ae19f1637a035602f7892c7b331aa2760fee6a",
"signature": "2eaa28637470570231bfd7bf65e0263a16403ed846c39301ea02774b26f21aca89a6ff4d55a0b5ae8bfbcad1ab6fc2af9c5abd51921f5f0007c0c34466c03a09"
}Download the allowed receipt and quota-denial receipt. A failed operation can still have a valid signature; signature validity and authorization are separate conclusions.
Find and verify evidence from your run
Replace your-run-id with the directory printed by your command. The first command reads denials from this isolated local store; the second verifies your first exported receipt against the key selected by your host:
chio --receipt-db runs/your-run-id/receipts.db receipt list --admin-all --outcome deny
target/release/verify-protocol-receipt runs/your-run-id/receipt-1.json runs/your-run-id/kernel-public-key.txt{
"authorized": true,
"crypto_error": null,
"crypto_floor": "allow_classical",
"decision": {
"verdict": "allow"
},
"ok": true,
"parameter_hash_valid": true,
"receipt_id": "37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26",
"receipt_id_valid": true,
"signature_valid": true,
"signer_trusted": true
}ok covers the expected signer, signature, receipt ID and parameter hash. authorized additionally requires an allow decision. For a remote host, obtain the expected key through trusted configuration. Offline receipt verification develops that trust boundary.
Build on the complete application
The client below contains the actual protocol calls, task-ID handling and result interpretation used above. It imports the bounded transport helper included in the same download.
"""Count your document through Chio's A2A or ACP stdio edge profile."""
import argparse
import json
import sys
from pathlib import Path
from session import Session
def objects(value):
if isinstance(value, dict):
yield value
for child in value.values():
yield from objects(child)
elif isinstance(value, list):
for child in value:
yield from objects(child)
def call_parameters(protocol, text):
if protocol == "a2a":
# A data part passes the tool's structured arguments without renaming them.
return {"message": {"role": "user", "parts": [
{"type": "data", "data": {"text": text}}
]}, "metadata": {"chio": {"targetSkillId": "count_words"}}}
return {"capabilityId": "count_words", "arguments": {"text": text}}
def receipt_of(response):
return next((item["receipt"] for item in objects(response)
if isinstance(item.get("receipt"), dict)
and "signature" in item["receipt"]), None)
def print_result(session, response, number):
if "error" in response:
print("ERROR " + json.dumps(response["error"]))
return 2
receipt = receipt_of(response)
if receipt is None:
raise RuntimeError("No signed kernel receipt in the terminal response")
session.save(f"receipt-{number}.json", receipt)
decision = receipt["decision"]
if decision["verdict"] != "allow":
print(decision["verdict"].upper() + " " + str(decision.get("reason", "See receipt")))
print("Receipt: " + receipt["id"])
return 2
count = next((item for item in objects(response.get("result"))
if isinstance(item.get("words"), int)
and isinstance(item.get("bytes"), int)), None)
if count is None:
raise RuntimeError("Allowed response did not contain the document count")
print(f'ALLOW {count["words"]} words, {count["bytes"]} UTF-8 bytes')
print("Receipt: " + receipt["id"])
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("protocol", choices=["a2a", "acp"])
parser.add_argument("document", type=Path)
parser.add_argument("--repeat", type=int, default=1, choices=range(1, 6),
help="Repeat within one grant (1–5); a refusal exits 2")
lifecycle = parser.add_mutually_exclusive_group()
lifecycle.add_argument("--deferred", action="store_true", help="Create then resolve a deferred task")
lifecycle.add_argument("--cancel", action="store_true", help="Create then cancel without executing")
args = parser.parse_args()
with args.document.open("rb") as source:
data = source.read(65_537)
if len(data) > 65_536:
parser.error("Document exceeds 65536 UTF-8 bytes")
text = data.decode("utf-8")
protocol = args.protocol
methods = ("message/send", "message/stream", "task/get", "task/cancel") if protocol == "a2a" else (
"tool/invoke", "tool/stream", "tool/resume", "tool/cancel")
params = call_parameters(protocol, text)
with Session(protocol) as session:
if protocol == "acp":
discovery = session.request("session/list_capabilities", {})
if "error" in discovery:
raise RuntimeError("Discovery failed: " + json.dumps(discovery["error"]))
session.save("discovery.json", discovery)
for number in range(1, args.repeat + 1):
if args.deferred or args.cancel:
pending = session.request(methods[1], params)
if "error" in pending:
print("ERROR " + json.dumps(pending["error"]))
return 2
task = pending["result"] if protocol == "a2a" else pending["result"]["task"]
task_id = task["id"]
print(f'Task {task_id}: {task["status"]}; execution pending')
response = session.request(methods[3] if args.cancel else methods[2], {"taskId": task_id})
if args.cancel:
if "error" in response:
print("ERROR " + json.dumps(response["error"]))
return 2
cancelled = response["result"] if protocol == "a2a" else response["result"]["task"]
if cancelled["status"] != "cancelled" or receipt_of(response) is not None:
raise RuntimeError("Unexpected cancellation result; inspect the saved response")
print(f'Task {task_id}: {cancelled["status"]}; no tool execution receipt')
continue
else:
response = session.request(methods[0], params)
status = print_result(session, response, number)
if status:
return status
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, UnicodeError, RuntimeError, ValueError) as error:
print("Error: " + str(error), file=sys.stderr)
raise SystemExit(1)Read the complete stdio transport and host setup
"""A bounded JSON-RPC stdio session for the document-tools host."""
import json
from pathlib import Path
import queue
import subprocess
import threading
import uuid
class Session:
def __init__(self, protocol):
self.protocol = protocol
self.project = Path(__file__).resolve().parent
self.directory = self.project / "runs" / str(uuid.uuid4())
self.messages = queue.Queue(maxsize=8)
self.wire = []
self.request_id = 0
self.process = None
self.closed = False
def __enter__(self):
binary = self.project / "target/release/protocol-tools"
if not binary.is_file():
raise RuntimeError("Build the host first: cargo build --release --locked")
self.directory.parent.mkdir(mode=0o700, exist_ok=True)
self.process = subprocess.Popen(
[str(binary), self.protocol, str(self.directory)], cwd=self.project,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, encoding="utf-8", bufsize=1,
)
self.reader = threading.Thread(target=self._read, daemon=True)
self.reader.start()
return self
def _read(self):
try:
while not self.closed:
line = self.process.stdout.readline(2_097_153)
if not line:
self.messages.put(None, timeout=1)
return
if len(line.encode("utf-8")) > 2_097_152:
raise RuntimeError("Response exceeds two MiB")
self.messages.put(json.loads(line), timeout=1)
except (ValueError, OSError, RuntimeError, queue.Full) as error:
try:
self.messages.put(error, timeout=1)
except queue.Full:
self.process.terminate()
def request(self, method, params):
if self.closed or self.process is None:
raise RuntimeError("Session is not open")
self.request_id += 1
frame = {"jsonrpc": "2.0", "id": self.request_id,
"method": method, "params": params}
encoded = json.dumps(frame, ensure_ascii=False) + "\n"
if len(encoded.encode("utf-8")) > 1_048_576:
raise ValueError("Request exceeds one MiB")
self.wire.append({"direction": "client", "message": frame})
try:
self.process.stdin.write(encoded)
self.process.stdin.flush()
response = self.messages.get(timeout=30)
except (OSError, queue.Empty) as error:
raise RuntimeError("Host transport failed or timed out; inspect stderr") from error
if response is None:
raise RuntimeError("Host exited before responding; inspect stderr")
if isinstance(response, Exception):
raise response
if not isinstance(response, dict) or response.get("id") != self.request_id:
raise RuntimeError("Response did not match the outstanding request")
self.wire.append({"direction": "server", "message": response})
self.save(f"response-{self.request_id}.json", response)
return response
def save(self, name, value):
(self.directory / name).write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def close(self):
if self.closed:
return
self.closed = True
if self.process:
try:
self.process.stdin.close()
self.process.wait(timeout=10)
except (OSError, subprocess.TimeoutExpired):
self.process.terminate()
try:
self.process.wait(timeout=3)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
self.reader.join(timeout=2)
self.process.stdout.close()
if self.directory.is_dir():
(self.directory / "wire.ndjson").write_text(
"".join(json.dumps(frame, ensure_ascii=False) + "\n" for frame in self.wire),
encoding="utf-8")
print("Evidence: " + str(self.directory.relative_to(self.project)))
def __exit__(self, *_):
self.close()use chio_core::{
capability::{
scope::{ChioScope, Operation, ToolGrant},
token::CapabilityToken,
},
crypto::Keypair,
};
use chio_kernel::{
ChioKernel, KernelConfig, DEFAULT_CHECKPOINT_BATCH_SIZE, DEFAULT_MAX_STREAM_DURATION_SECS,
DEFAULT_MAX_STREAM_TOTAL_BYTES,
};
use chio_manifest::ToolManifest;
use std::path::Path;
pub struct Host {
pub kernel: ChioKernel,
pub capability: CapabilityToken,
pub manifest: ToolManifest,
}
pub fn boot(directory: &Path) -> anyhow::Result<Host> {
let authority_bytes = std::fs::read("authority.json")?;
let config: serde_json::Value = serde_json::from_slice(&authority_bytes)?;
let fields = config
.as_object()
.ok_or_else(|| anyhow::anyhow!("Authority must be an object"))?;
anyhow::ensure!(
fields.keys().all(
|key| ["schema", "tool", "max_invocations", "lifetime_seconds"].contains(&key.as_str())
),
"Unknown authority field"
);
anyhow::ensure!(
config["schema"] == "document-tools.authority.v1",
"Unknown authority schema"
);
anyhow::ensure!(
config["tool"] == crate::service::TOOL,
"This host grants only count_words"
);
let call_limit = config["max_invocations"]
.as_u64()
.filter(|v| (1..=50).contains(v))
.ok_or_else(|| anyhow::anyhow!("max_invocations must be an integer from 1 to 50"))?
as u32;
let lifetime = config["lifetime_seconds"]
.as_u64()
.filter(|v| (1..=3600).contains(v))
.ok_or_else(|| anyhow::anyhow!("lifetime_seconds must be an integer from 1 to 3600"))?;
// A new application run gets a fresh session. Refuse to reuse its directory.
std::fs::create_dir(directory)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700))?;
}
std::fs::write(directory.join("authority.json"), &authority_bytes)?;
let authority =
chio_control_plane::DurableAdmissionRuntime::open(&directory.join("admission.db"))?;
let keypair = authority.kernel_keypair();
// The host chooses this key before any receipt exists.
std::fs::write(
directory.join("kernel-public-key.txt"),
keypair.public_key().to_hex(),
)?;
let mut kernel = ChioKernel::new(KernelConfig {
keypair,
ca_public_keys: Vec::new(),
max_delegation_depth: 3,
policy_hash: chio_core::sha256_hex(&authority_bytes),
allow_sampling: false,
allow_sampling_tool_use: false,
allow_elicitation: false,
max_stream_duration_secs: DEFAULT_MAX_STREAM_DURATION_SECS,
max_stream_total_bytes: DEFAULT_MAX_STREAM_TOTAL_BYTES,
require_web3_evidence: false,
allow_ephemeral_receipt_log: true,
allow_ephemeral_revocation_store: true,
checkpoint_batch_size: DEFAULT_CHECKPOINT_BATCH_SIZE,
retention_config: None,
memory_budget: chio_kernel::MemoryBudgetConfig::defaults(),
deadlines: chio_kernel::HotPathDeadlineConfig::default(),
});
kernel.set_receipt_store(Box::new(chio_store_sqlite::SqliteReceiptStore::open(
directory.join("receipts.db"),
)?))?;
authority.attach(&mut kernel)?;
let service_key = Keypair::generate();
let manifest = crate::service::manifest(service_key.public_key().to_hex());
let signed = chio_manifest::sign_manifest(&manifest, &service_key)?;
chio_manifest::verify_manifest(&signed, &service_key.public_key())?;
std::fs::write(
directory.join("manifest.json"),
serde_json::to_vec_pretty(&signed)?,
)?;
kernel.register_tool_server(Box::new(crate::service::DocumentTools));
let agent = Keypair::generate();
let capability = kernel.issue_capability(
&agent.public_key(),
ChioScope {
grants: vec![ToolGrant {
server_id: crate::service::SERVER.into(),
tool_name: crate::service::TOOL.into(),
operations: vec![Operation::Invoke],
constraints: Vec::new(),
max_invocations: Some(call_limit),
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..ChioScope::default()
},
lifetime,
)?;
std::fs::write(
directory.join("capability.json"),
serde_json::to_vec_pretty(&capability)?,
)?;
Ok(Host {
kernel,
capability,
manifest,
})
}Replace the document operation
The complete service validates text, counts it and emits two chunks. Replace this handler when building your own capability, then update its manifest and grant together. The host registers it before issuing authority.
use chio_kernel::{
KernelError, NestedFlowBridge, ToolCallChunk, ToolCallStream, ToolServerConnection,
ToolServerStreamResult,
};
use chio_manifest::{ToolDefinition, ToolManifest};
use serde_json::{json, Value};
pub const SERVER: &str = "documents";
pub const TOOL: &str = "count_words";
pub const TEXT_LIMIT: usize = 65_536;
pub struct DocumentTools;
fn count(tool: &str, arguments: &Value) -> Result<Value, KernelError> {
if tool != TOOL {
return Err(KernelError::ToolNotRegistered(tool.to_owned()));
}
let text = arguments
.get("text")
.and_then(Value::as_str)
.ok_or_else(|| KernelError::RequestIncomplete("text must be a string".into()))?;
if text.len() > TEXT_LIMIT {
return Err(KernelError::RequestIncomplete(
"text exceeds 65536 UTF-8 bytes".into(),
));
}
Ok(json!({"words": text.split_whitespace().count(), "bytes": text.len()}))
}
#[async_trait::async_trait]
impl ToolServerConnection for DocumentTools {
fn server_id(&self) -> &str {
SERVER
}
fn tool_names(&self) -> Vec<String> {
vec![TOOL.into()]
}
async fn invoke(
&self,
tool: &str,
arguments: Value,
_nested: Option<&mut dyn NestedFlowBridge>,
) -> Result<Value, KernelError> {
count(tool, &arguments)
}
async fn invoke_stream(
&self,
tool: &str,
arguments: Value,
_nested: Option<&mut dyn NestedFlowBridge>,
) -> Result<Option<ToolServerStreamResult>, KernelError> {
let result = count(tool, &arguments)?;
// These chunks are collated by the edge into one terminal response.
Ok(Some(ToolServerStreamResult::Complete(ToolCallStream {
chunks: vec![
ToolCallChunk { data: result },
ToolCallChunk {
data: json!({"content": [
{"type": "text", "text": "Document counted"}
]}),
},
],
})))
}
}
pub fn manifest(public_key: String) -> ToolManifest {
ToolManifest {
schema: "chio.manifest.v1".into(),
server_id: SERVER.into(),
name: "Document tools".into(),
version: "1.0.0".into(),
description: Some("Count words in caller-provided text".into()),
tools: vec![ToolDefinition {
name: TOOL.into(),
description: "Count whitespace-separated words and UTF-8 bytes".into(),
input_schema: json!({
"type": "object", "properties": {"text": {"type": "string"}},
"required": ["text"], "x-chio-streaming": true,
"x-chio-partial-output": true, "x-chio-cancellation": true
}),
output_schema: None,
pricing: None,
has_side_effects: false,
latency_hint: None,
}],
server_tools: Vec::new(),
required_permissions: None,
public_key,
}
}Original hello_task service for source comparison
impl ToolServerConnection for HelloStreamServer {
fn server_id(&self) -> &str {
SERVER_ID
}
fn tool_names(&self) -> Vec<String> {
vec![TOOL_NAME.to_string()]
}
async fn invoke(
&self,
tool_name: &str,
arguments: Value,
_nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
) -> Result<Value, KernelError> {
if tool_name != TOOL_NAME {
return Err(KernelError::ToolNotRegistered(tool_name.to_string()));
}
Ok(json!({"message": "hello from a2a", "arguments": arguments}))
}
async fn invoke_stream(
&self,
tool_name: &str,
arguments: Value,
_nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
) -> Result<Option<ToolServerStreamResult>, KernelError> {
if tool_name != TOOL_NAME {
return Err(KernelError::ToolNotRegistered(tool_name.to_string()));
}
let text = arguments
.get("text")
.and_then(Value::as_str)
.unwrap_or("world");
Ok(Some(ToolServerStreamResult::Complete(ToolCallStream {
chunks: vec![
ToolCallChunk {
data: json!({
"type": "text",
"text": format!("hello from a2a, {text}")
}),
},
ToolCallChunk {
data: json!({
"content": [{
"type": "text",
"text": "stream complete"
}]
}),
},
],
})))
}
}Describe the operation accurately
The manifest declares count_words as read-only and supplies its text schema. Streaming, partial-output and cancellation hints describe how the edge may present it; those descriptions do not expand the signed grant.
Original greeting manifest
pub fn demo_manifest() -> ToolManifest {
ToolManifest {
schema: "chio.manifest.v1".to_string(),
server_id: SERVER_ID.to_string(),
name: "Hello A2A Server".to_string(),
description: Some("A tiny receipt-bearing A2A hello surface".to_string()),
version: "0.1.0".to_string(),
tools: vec![ToolDefinition {
name: TOOL_NAME.to_string(),
description: "Return a collated greeting".to_string(),
input_schema: json!({
"type": "object",
"x-chio-streaming": true,
"x-chio-partial-output": true
}),
output_schema: None,
pricing: None,
has_side_effects: false,
latency_hint: None,
}],
server_tools: Vec::new(),
required_permissions: None,
public_key: "hello-a2a-manifest".to_string(),
}
}Connect the edge to the kernel
The host supplies its resolved capability and agent identity to handle_jsonrpc. Requests without a response frame produce no output line. Frames are bounded, and the client stops on transport failure.
mod acp;
mod host;
mod service;
use chio_a2a_edge::{A2aEdgeConfig, A2aKernelExecutionContext, ChioA2aEdge};
use chio_acp_edge::{AcpEdgeConfig, AcpKernelExecutionContext, ChioAcpEdge};
use serde_json::Value;
use std::{
io::{BufRead, Read, Write},
path::PathBuf,
};
fn main() -> anyhow::Result<()> {
let mut args = std::env::args().skip(1);
let protocol = args
.next()
.ok_or_else(|| anyhow::anyhow!("Usage: protocol-tools a2a|acp NEW_SESSION_DIRECTORY"))?;
anyhow::ensure!(protocol == "a2a" || protocol == "acp", "Choose a2a or acp");
let directory = PathBuf::from(
args.next()
.ok_or_else(|| anyhow::anyhow!("Missing new session directory"))?,
);
anyhow::ensure!(args.next().is_none(), "Unexpected extra argument");
let host = host::boot(&directory)?;
let mut a2a = ChioA2aEdge::new(
A2aEdgeConfig {
agent_name: "Document tools".into(),
agent_description: "Count caller-provided text under a Chio capability".into(),
// This card field is advertising metadata. This example opens no HTTP listener.
endpoint_url: "http://127.0.0.1:8080/a2a".into(),
..A2aEdgeConfig::default()
},
vec![host.manifest.clone()],
)?;
let acp = ChioAcpEdge::new(AcpEdgeConfig::default(), vec![host.manifest.clone()])?;
let a2a_context = A2aKernelExecutionContext {
capability: host.capability.clone(),
agent_id: host.capability.subject.to_hex(),
dpop_proof: None,
execution_nonce: None,
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
model_metadata: None,
};
let acp_context = AcpKernelExecutionContext {
capability: host.capability.clone(),
agent_id: host.capability.subject.to_hex(),
dpop_proof: None,
execution_nonce: None,
governed_intent: None,
approval_token: None,
approval_tokens: Vec::new(),
threshold_approval_proposal: None,
supplemental_authorization: None,
model_metadata: None,
};
if protocol == "a2a" {
std::fs::write(
directory.join("discovery.json"),
serde_json::to_vec_pretty(&a2a.agent_card())?,
)?;
}
let mut reader = std::io::stdin().lock();
let mut writer = std::io::stdout().lock();
loop {
let mut line = Vec::new();
let size = (&mut reader).take(1_048_577).read_until(b'\n', &mut line)?;
if size == 0 {
break;
}
anyhow::ensure!(size <= 1_048_576, "Request exceeds one MiB");
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
let request: Value = serde_json::from_slice(&line)?;
let response = if protocol == "a2a" {
a2a.handle_jsonrpc(request, &host.kernel, &a2a_context)
.as_value()
.cloned()
} else {
acp::handle(&acp, request, &host.kernel, &acp_context)
};
if let Some(response) = response {
serde_json::to_writer(&mut writer, &response)?;
writeln!(&mut writer)?;
writer.flush()?;
}
}
Ok(())
}Original greeting serve loop
pub fn serve_reader<R, W>(reader: R, mut writer: W) -> HelloA2aResult<()>
where
R: BufRead,
W: Write,
{
let mut state = build_demo_state()?;
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let message: Value = serde_json::from_str(&line)?;
let response = state
.edge
.handle_jsonrpc(message, &state.kernel, &state.execution);
if let Some(response) = response.as_value() {
serde_json::to_writer(&mut writer, response)?;
writeln!(&mut writer)?;
writer.flush()?;
}
}
Ok(())
}Understand the delivery boundary
| Situation | What to expect |
|---|---|
Blocking call | A terminal task includes collated output and the signed decision. |
Deferred task | The edge retains the request; task/get triggers execution. No incremental events are pushed. |
Cancellation | A working task can be cancelled before execution. No tool receipt is created for that state change. |
Process exit | Deferred task state and revocations are process-local. Saved receipts remain on disk. |
Unknown skill | The edge rejects the request before tool execution. A protocol error alone is not a signed kernel denial. |
Read the advertised caveats
This document tool is adapted. Its four captured caveats describe deferred execution, collation, terminal-only partial output and cancellation before execution. Fidelity describes translation behavior; it does not prove that a particular caller is authorized. When multiple manifests expose the same tool name, the A2A edge publishes qualified identifiers instead of silently merging ambiguous skills.
Read the public fidelity implementation
fn evaluate_bridge_fidelity(
tool: &ToolDefinition,
target_protocol: DiscoveryProtocol,
) -> BridgeFidelity {
let registry = authoritative_target_registry();
let hints = semantic_hints_for_tool(tool);
let lifecycle = runtime_lifecycle_contract(RuntimeLifecycleSurface::A2aAuthoritative);
if !hints.publish {
return BridgeFidelity::Unsupported {
reason: "publication disabled by x-chio-publish=false".to_string(),
};
}
if !registry.supports_target_protocol(target_protocol) {
return BridgeFidelity::Unsupported {
reason: format!(
"A2A authoritative execution does not yet have a registered `{target_protocol}` target executor"
),
};
}
if hints.approval_required {
return BridgeFidelity::Unsupported {
reason: "requires interactive approval semantics that the current A2A edge cannot truthfully project".to_string(),
};
}
let mut caveats = Vec::new();
if tool.has_side_effects {
caveats.push(
"A2A publication cannot project protocol-native permission prompts; callers must rely on Chio capability enforcement".to_string(),
);
}
if hints.streams_output {
caveats.push(format!(
"stream-capable tools execute through `{}` deferred tasks; output is surfaced on follow-up `{}` rather than incremental transport updates",
lifecycle.stream_entrypoint, lifecycle.follow_up_entrypoint
));
caveats.push(
"stream chunks are collated into the terminal task payload instead of pushed as incremental A2A events".to_string(),
);
}
if hints.partial_output {
caveats.push(
"partial output is preserved only in the terminal task payload, not incremental updates"
.to_string(),
);
}
if hints.supports_cancellation {
caveats.push(format!(
"cancellation is available only for deferred `{}` tasks; blocking `{}` remains terminal",
lifecycle.stream_entrypoint, lifecycle.blocking_entrypoint
));
}
if caveats.is_empty() {
BridgeFidelity::Lossless
} else {
BridgeFidelity::Adapted { caveats }
}
}Reproduce the original greeting verifier
The public hello-a2a example remains useful for checking the edge implementation. Its fixed greeting and timestamped artifacts are a verifier workflow. The document-tools application above accepts your input and retains your own run.
Original smoke commands, captured outputs and assertions
$ cd examples/hello-a2a
$ ./smoke.shhello-a2a smoke passed artifacts: <chio-source>/examples/hello-a2a/.artifacts/20260905T114341Z send receipt: 31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45 stream receipt: 95bae6361e72230e60819e50fedf6c827a3e97b94b380c38f67ea2ff255651fe
$ cat agent-card.json{
"capabilities": {
"pushNotifications": false,
"stateTransitionHistory": false,
"streaming": true
},
"defaultInputModes": [
"text"
],
"defaultOutputModes": [
"text"
],
"description": "Chio-governed tools exposed as A2A skills",
"name": "Chio A2A Edge",
"skills": [
{
"bridgeFidelity": {
"caveats": [
"stream-capable tools execute through `message/stream` deferred tasks; output is surfaced on follow-up `task/get` rather than incremental transport updates",
"stream chunks are collated into the terminal task payload instead of pushed as incremental A2A events",
"partial output is preserved only in the terminal task payload, not incremental updates"
],
"kind": "adapted"
},
"description": "Return a collated greeting",
"id": "hello_task",
"inputModes": [
"text"
],
"name": "hello_task",
"outputModes": [
"text"
],
"tags": []
}
],
"supportedInterfaces": [
{
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0",
"url": "http://localhost:8080"
}
],
"version": "0.1.0"
}$ jq '.result | {id, status, message,
$ chio: (.metadata.chio | {decision, receiptId, capabilityId, authorityPath,
$ a2aSurface, streamProjection, terminalState})}' send-response.json{
"id": "a2a-task-1",
"status": "completed",
"message": {
"parts": [
{
"data": {
"text": "hello from a2a, world",
"type": "text"
},
"type": "data"
},
{
"text": "stream complete",
"type": "text"
}
],
"role": "agent"
},
"chio": {
"decision": "allow",
"receiptId": "31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45",
"capabilityId": "cap-01a07161-b8f8-72a3-871f-c07bf1afecff",
"authorityPath": "cross_protocol_orchestrator",
"a2aSurface": "authoritative_blocking_send",
"streamProjection": "collated_final_message",
"terminalState": {
"state": "completed"
}
}
}$ jq '.result.metadata.chio.receipt | del(.metadata)' send-response.json{
"action": {
"parameter_hash": "3a477a27451b71eaf6dc49c80b0e2e4c80f3fc5060884497c4246ea2b44d0790",
"parameters": {
"message": "world"
}
},
"boundary_class": "prevent",
"capability_id": "cap-01a07161-b8f8-72a3-871f-c07bf1afecff",
"content_hash": "9f60be4661ed367601d088804c409177d3fea01b9f1ff79fcb2a1cdd4e12de42",
"decision": {
"verdict": "allow"
},
"id": "31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45",
"kernel_key": "133de73c5809534ce22ac8d1c5b12f06b1e59dd875a0ced4592c612fc57874ca",
"policy_hash": "hello-a2a-policy",
"receipt_kind": "mediated_decision",
"redaction_mode": "none",
"signature": "df30178a057d1849b44fea8f750ceececee128c377ab78692d34423a08cec9a1ebcb6a9e8103faba21c37707e219202a322dbdda3a4b99141fe514c9906bbc00",
"timestamp": 1788608624,
"tool_name": "hello_task",
"tool_origin": "caller_executed",
"tool_server": "hello-a2a-srv",
"trust_level": "mediated"
}$ cat stream-created.json{
"id": 2,
"jsonrpc": "2.0",
"result": {
"id": "a2a-task-2",
"metadata": {
"chio": {
"authoritative": true,
"authorityPath": "cross_protocol_orchestrator",
"capabilityId": null,
"claimEligible": true,
"compatibilityOnly": false,
"decision": "pending",
"lifecycle": {
"messageSend": "blocking_terminal_task",
"messageStream": "deferred_task_poll",
"taskCancel": "supported",
"taskGet": "supported"
},
"receipt": null,
"receiptBearing": false,
"receiptId": null,
"receiptPending": true,
"runtimeLifecycle": {
"blockingEntrypoint": "message/send",
"cancelEntrypoint": "task/cancel",
"claimEligible": true,
"compatibilityOnly": false,
"followUpEntrypoint": "task/get",
"partialOutputDelivery": "collated_terminal_payload",
"streamDelivery": "collated_terminal_payload",
"streamEntrypoint": "message/stream",
"surface": "a2a_authoritative"
}
}
},
"status": "working",
"statusMessage": "Task accepted for authoritative deferred execution."
}
}$ jq '.result | {id, status, message,
$ chio: (.metadata.chio | {decision, receiptId, capabilityId})}' task-get-response.json{
"id": "a2a-task-2",
"status": "completed",
"message": {
"parts": [
{
"data": {
"text": "hello from a2a, world",
"type": "text"
},
"type": "data"
},
{
"text": "stream complete",
"type": "text"
}
],
"role": "agent"
},
"chio": {
"decision": "allow",
"receiptId": "95bae6361e72230e60819e50fedf6c827a3e97b94b380c38f67ea2ff255651fe",
"capabilityId": "cap-01a07161-b8f8-72a3-871f-c07bf1afecff"
}
}$ ARTIFACTS=.artifacts/latest
$ jq -r '.skills[0].bridgeFidelity.kind' "$ARTIFACTS/agent-card.json"
$ jq -r '.result.metadata.chio.receiptId' "$ARTIFACTS/send-response.json"
$ jq -r '.result.metadata.chio.receiptPending' "$ARTIFACTS/stream-created.json"
$ jq -r '.result.metadata.chio.receiptId' "$ARTIFACTS/task-get-response.json"adapted 31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45 true 95bae6361e72230e60819e50fedf6c827a3e97b94b380c38f67ea2ff255651fe
agent_card = json.loads(
subprocess.check_output(
[str(example_root / "run-edge.sh"), "agent-card"],
cwd=example_root,
text=True,
)
)
(artifacts / "agent-card.json").write_text(json.dumps(agent_card, indent=2), encoding="utf-8")
assert agent_card["skills"][0]["id"] == "hello_task", agent_cardassert send_response["result"]["status"] == "completed", send_response
assert send_response["result"]["metadata"]["chio"]["authorityPath"] == "cross_protocol_orchestrator", send_response
assert send_response["result"]["metadata"]["chio"]["receiptId"], send_response
assert stream_created["result"]["status"] == "working", stream_created
assert stream_created["result"]["metadata"]["chio"]["receiptPending"] is True, stream_created
assert task_resolved["result"]["status"] == "completed", task_resolved
assert task_resolved["result"]["metadata"]["chio"]["receiptId"], task_resolvedChoose the direction of the integration
Use this edge when you own the operation and want to expose an agent-facing skill. To call an existing remote A2A service from Chio, use the consumer-side A2A adapter. To govern an existing HTTP API, continue with OpenAPI Sidecar.
Keep the tool and change the protocol
Continue to Hello ACP with this same project and document. The Bridges reference defines the wider translation and fidelity contract; the provider integration project connects model-selected calls to an owned application.