Hello ACP
Open a session through the official ACP client. Run your document tool under a bounded grant and verify the tool update’s signed decision.
Expose a governed tool through an ACP agent session. The official Python client starts the supplied Rust agent, initializes the connection and sends your text as a prompt. Tool updates carry the actual output and its signed receipt, including a refusal when the session exhausts its allowance.
Run the complete ACP 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 acp_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 | completed | allow | 54e15eb3a95f5696… |
2 | completed | allow | ad7632b1cad084b6… |
3 | failed | deny | 97253d897a6f0c36… |
Captured with agent-client-protocol 0.12.1. 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
ACP uses newline-delimited JSON-RPC over the child process’s stdio. Initialization negotiates protocol version 1; session/new returns a session owned by this caller. Each session/prompt creates a fresh kernel operation. JSON-RPC correlation IDs can be reused after a response without reusing a previous execution.
initialized = await client.initialize(
protocol_version=PROTOCOL_VERSION,
client_capabilities=ClientCapabilities(),
client_info=Implementation(name="chio-docs-client", version="1.0.0"),
)
session = await client.new_session(cwd=str(ROOT), mcp_servers=[])
result = await client.prompt(
session_id=session.session_id,
prompt=[TextContentBlock(type="text", text=text)],
)The agent emits a session/update tool call, a message chunk and the terminal prompt response. The tool update’s rawInput, rawOutput and _meta.chio.receipt make its result independently checkable. A failed tool can still end a prompt normally; read the tool status and signed decision together.
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 checks that the tool notification and terminal response carry the same evidence.
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 ACP first call · complete signed receipt 106 lines · complete file
{
"action": {
"parameter_hash": "ced943f7d4b9016abf9870f03ea4177d658fc7304016417da10e8af45f460aa4",
"parameters": {
"text": "Chio governs the tools behind an ACP agent"
}
},
"boundary_class": "prevent",
"capability_id": "cap-01a08ea1-de21-7130-8ff4-40ba8880b7ec",
"content_hash": "d43c61337f20d4757305b71d53be031a5205ecb11515715884df023b220b8f39",
"decision": {
"verdict": "allow"
},
"id": "54e15eb3a95f56967e68ccd7b465f70aee36d79969992d9eade52d3c74a4d96e",
"kernel_key": "b47ea36c693b9e785582f2dcdad76d52b4a58028cb76baa151316bfb46c65e82",
"metadata": {
"admission_operation": {
"compensation_status": "not_compensated",
"coordinator_lease_epoch": 1,
"coordinator_lease_id": "01a08ea1-dd8c-76c3-80a3-3cf8f8b878d5",
"operation_id": "23cb88dc1d45068a2a3c5c06f4914f321114b0b33e8a4c1f8292af453f130cda",
"projected_dispatch_state": "terminal",
"projected_operation_version": 8,
"projected_state": "completed",
"request_binding_hash": "78a183c7de4b1f3643f2e575f1460bc02806c7d57528aad912c9103d9913171e",
"request_id": "acp-prompt:acp-84de6ba3f7b1c894f950257d21dfa3e833b62bab1e78761e445bf372e289aec4:1",
"request_namespace_digest": "6e65a980e77d2fc1b9216735b61041fbd9ee47347d887543d253a8f734bf3c2a",
"retained_dispatch_commit": {
"committed_version": 6,
"coordinator_lease_epoch": 1,
"coordinator_lease_id": "01a08ea1-dd8c-76c3-80a3-3cf8f8b878d5",
"provider_attempt": {
"attempt_id": "attempt:23cb88dc1d45068a2a3c5c06f4914f321114b0b33e8a4c1f8292af453f130cda",
"operation_id": "23cb88dc1d45068a2a3c5c06f4914f321114b0b33e8a4c1f8292af453f130cda",
"transport_id": "kernel-tool-server:documents",
"transport_key_epoch": 1
},
"store_fence": {
"lease_id": "01a08ea1-dd8c-76c3-80a3-3cf8f8b878d5",
"owner_epoch": 1,
"store_uuid": "01a08ea1-dc2d-7467-bff9-d1a2f5c015e8"
}
},
"schema": "chio.admission-receipt.v1",
"store_fence": {
"lease_id": "01a08ea1-dd8c-76c3-80a3-3cf8f8b878d5",
"owner_epoch": 1,
"store_uuid": "01a08ea1-dc2d-7467-bff9-d1a2f5c015e8"
},
"tool_outcome_id": "c88b090cbf798558cd05e1053441c5dcbd5d6b0c1ec88ac5146a3d25d42d01af",
"tool_outcome_version": 2,
"trusted_time_unix_ms": 1789099368148
},
"attribution": {
"delegation_depth": 0,
"grant_index": 0,
"issuer_key": "b47ea36c693b9e785582f2dcdad76d52b4a58028cb76baa151316bfb46c65e82",
"subject_key": "8bf3cb8bb468ec947e742267054c4795c84660dd3a8ac114fa95a8f887ac1589"
},
"chio_receipt_signing_nonce": "rcpt-01a08ea1-def8-772b-ba5d-06099fd305a8",
"receipt_context": {
"request_id": "acp-prompt:acp-84de6ba3f7b1c894f950257d21dfa3e833b62bab1e78761e445bf372e289aec4:1"
},
"route_selection": {
"candidates": [
{
"available": true,
"routeId": "native-route",
"selectedProtocols": [
"acp",
"native"
],
"targetProtocol": "native"
}
],
"decision": "select",
"requestedTargetProtocol": "native",
"routeSelectionId": "d09a1f38b92e97e66778f7b34403fd51fef069b85094048470a28d728e5c85c1",
"selectedProtocols": [
"acp",
"native"
],
"selectedRouteId": "native-route",
"selectedTargetProtocol": "native",
"sourceProtocol": "acp"
},
"stream": {
"chunk_hashes": [
"2e2bd9af8085931de5436f5a344bc74dce2c4e6b8025f64782e180f314140165",
"b2538f4ac125b83068662cf8f4dc7966e50d1bcbfce089b5824edea204d91624"
],
"chunks_expected": 2,
"chunks_received": 2,
"total_bytes": 77
}
},
"policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
"receipt_kind": "mediated_decision",
"redaction_mode": "none",
"signature": "dcca249bf2fe2e340d53f3ce8e6b67d1b179a578fd13ccadaef217291178ab3e4a0c1ee11a6a7fc688b078cdcede00594b75c6071a6329cf7cb13b4d366d900e",
"timestamp": 1789099368,
"tool_name": "count_words",
"tool_origin": "caller_executed",
"tool_server": "documents",
"trust_level": "mediated"
}Read ACP third call · complete signed refusal 66 lines · complete file
{
"action": {
"parameter_hash": "ced943f7d4b9016abf9870f03ea4177d658fc7304016417da10e8af45f460aa4",
"parameters": {
"text": "Chio governs the tools behind an ACP agent"
}
},
"boundary_class": "prevent",
"capability_id": "cap-01a08ea1-de21-7130-8ff4-40ba8880b7ec",
"content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
"decision": {
"guard": "kernel",
"reason": "invocation budget exhausted for capability cap-01a08ea1-de21-7130-8ff4-40ba8880b7ec",
"verdict": "deny"
},
"id": "97253d897a6f0c36f9156748ecdaf8017a9b90404154cc1bb8a45a8f46f34bee",
"kernel_key": "b47ea36c693b9e785582f2dcdad76d52b4a58028cb76baa151316bfb46c65e82",
"metadata": {
"attribution": {
"delegation_depth": 0,
"issuer_key": "b47ea36c693b9e785582f2dcdad76d52b4a58028cb76baa151316bfb46c65e82",
"subject_key": "8bf3cb8bb468ec947e742267054c4795c84660dd3a8ac114fa95a8f887ac1589"
},
"budget_authority": {
"authority_profile": "authoritative_hold_event",
"guarantee_level": "single_node_atomic",
"metering_profile": "max_cost_preauthorize_then_reconcile_actual"
},
"chio_receipt_signing_nonce": "rcpt-01a08ea1-e08c-7246-b627-10d14d90d1a5",
"receipt_context": {
"request_id": "acp-prompt:acp-84de6ba3f7b1c894f950257d21dfa3e833b62bab1e78761e445bf372e289aec4:3"
},
"route_selection": {
"candidates": [
{
"available": true,
"routeId": "native-route",
"selectedProtocols": [
"acp",
"native"
],
"targetProtocol": "native"
}
],
"decision": "select",
"requestedTargetProtocol": "native",
"routeSelectionId": "f890ef4ad7fcf6b081f7309117c8ebcd86dc598d6099a0b03f070ae8f84fdbdf",
"selectedProtocols": [
"acp",
"native"
],
"selectedRouteId": "native-route",
"selectedTargetProtocol": "native",
"sourceProtocol": "acp"
}
},
"policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
"receipt_kind": "mediated_decision",
"redaction_mode": "none",
"signature": "2cec8e474137df19597413afcb58f413f5122aa1ee6ccea9f1ba417ddffb32d9d581a076c8dd9efa4dd68faf2039fca5ff51fc45136ce623b33d0690d52b5401",
"timestamp": 1789099368,
"tool_name": "count_words",
"tool_origin": "caller_executed",
"tool_server": "documents",
"trust_level": "mediated"
}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/acp-host.rs 52 lines · complete file
#[path = "../host.rs"]
mod host;
#[path = "../service.rs"]
mod service;
use chio_acp_edge::{AcpAgentConnection, AcpEdgeConfig, AcpKernelExecutionContext, ChioAcpEdge};
use std::{
io::{BufRead, Read, Write},
path::PathBuf,
};
fn main() -> anyhow::Result<()> {
let directory = PathBuf::from(
std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("Usage: acp-host NEW_SESSION_DIRECTORY"))?,
);
let host = host::boot(&directory)?;
let edge = ChioAcpEdge::new(AcpEdgeConfig::default(), vec![host.manifest])?;
let mut connection = AcpAgentConnection::new(edge, service::TOOL.into(), "text".into())?;
let execution = AcpKernelExecutionContext {
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 mut reader = std::io::stdin().lock();
let mut writer = std::io::stdout().lock();
loop {
let mut line = Vec::new();
if (&mut reader).take(1_048_577).read_until(b'\n', &mut line)? == 0 {
break;
}
anyhow::ensure!(line.len() <= 1_048_576, "ACP message exceeds one MiB");
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
let message: serde_json::Value = serde_json::from_slice(&line)?;
for response in connection.handle_jsonrpc(message, &host.kernel, &execution) {
serde_json::to_writer(&mut writer, &response)?;
writeln!(&mut writer)?;
}
writer.flush()?;
}
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 acp_client.py 167 lines · complete file
# /// script
# requires-python = ">=3.11,<3.15"
# dependencies = ["agent-client-protocol==0.12.1", "chio-sdk"]
# [tool.uv.sources]
# chio-sdk = { path = "sdk/chio-py" }
# ///
"""Use the official ACP client to initialize an agent, open a session and run governed tools."""
import argparse
import asyncio
import json
import os
from pathlib import Path
import subprocess
import uuid
from acp import Client, PROTOCOL_VERSION, RequestError, connect_to_agent
from acp.schema import ClientCapabilities, Implementation, TextContentBlock
from chio.invariants import canonicalize_json, sha256_hex_utf8, verify_receipt_with_trusted_signers
ROOT = Path(__file__).resolve().parent
class Reviewer(Client):
def __init__(self):
self.updates = []
async def session_update(self, session_id, update, **kwargs):
self.updates.append({"session_id": session_id, "update": update.model_dump(by_alias=True, exclude_none=True)})
async def request_permission(self, **kwargs):
raise RequestError.method_not_found("No client-side permissions are exposed by this example")
class RecordedStdio:
"""ACP's message transport, with the actual newline frames retained."""
def __init__(self, process, wire):
self.process, self.wire = process, wire
async def send(self, message):
self.process.stdin.write((json.dumps(message) + "\n").encode())
await self.process.stdin.drain()
self.wire.append({"direction": "client", "message": message})
async def receive(self):
line = await self.process.stdout.readline()
if not line:
return None
message = json.loads(line)
self.wire.append({"direction": "server", "message": message})
return message
async def close(self):
self.process.stdin.close()
await self.process.stdin.wait_closed()
def verify(result, update, text, key, capability, session_id):
chio = result["_meta"]["chio"]
receipt = chio["receipt"]
if not verify_receipt_with_trusted_signers(receipt, [key])["ok"]:
raise ValueError("Invalid receipt or untrusted kernel")
if receipt["capability_id"] != capability["id"] or chio["receiptId"] != receipt["id"]:
raise ValueError("Result belongs to another grant or receipt")
if update["_meta"]["chio"] != chio:
raise ValueError("Tool update and terminal result name different evidence")
request_id = receipt["metadata"]["receipt_context"]["request_id"]
if update["toolCallId"] != request_id or not request_id.startswith("acp-prompt:" + session_id + ":"):
raise ValueError("The tool update is not associated with this signed operation")
arguments = {"text": text}
if receipt["action"]["parameters"] != arguments or update["rawInput"] != arguments:
raise ValueError("Receipt binds different text")
if receipt["action"]["parameter_hash"] != sha256_hex_utf8(canonicalize_json(arguments)):
raise ValueError("Input hash differs")
allowed = receipt["decision"]["verdict"] == "allow"
if (update["status"] == "completed") != allowed:
raise ValueError("Tool status differs from the signed decision")
if allowed:
output = update["rawOutput"]
chunks = output.get("stream") if isinstance(output, dict) else None
if chunks is not None:
hashes = [sha256_hex_utf8(canonicalize_json(chunk)) for chunk in chunks]
if hashes != receipt["metadata"]["stream"]["chunk_hashes"]:
raise ValueError("Stream chunk differs from its signed hash")
digest = sha256_hex_utf8("".join(hashes))
count = chunks[0]
else:
digest, count = sha256_hex_utf8(canonicalize_json(output)), output
if digest != receipt["content_hash"]:
raise ValueError("The tool output differs from its signed hash")
if count != {"words": len(text.split()), "bytes": len(text.encode())}:
raise ValueError("The tool did not count the supplied document")
return receipt
async def exercise(binary, directory, text):
wire = []
with (directory.parent / (directory.name + ".log")).open("w") as log:
process = await asyncio.create_subprocess_exec(str(binary), str(directory), cwd=ROOT,
stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=log, limit=4_194_304)
try:
reviewer = Reviewer()
client = connect_to_agent(reviewer, RecordedStdio(process, wire))
initialized = await client.initialize(protocol_version=PROTOCOL_VERSION,
client_capabilities=ClientCapabilities(), client_info=Implementation(name="chio-docs-client", version="1.0.0"))
if initialized.protocol_version != PROTOCOL_VERSION:
raise ValueError("Client and agent did not negotiate the same ACP version")
session = await client.new_session(cwd=str(ROOT), mcp_servers=[])
# The operator selects its own host's public key before any tool prompt.
key = (directory / "kernel-public-key.txt").read_text().strip()
capability = json.loads((directory / "capability.json").read_text())
records = []
for number in range(3):
offset = len(reviewer.updates)
result = (await client.prompt(session_id=session.session_id,
prompt=[TextContentBlock(type="text", text=text)])).model_dump(by_alias=True, exclude_none=True)
updates = reviewer.updates[offset:]
calls = [entry["update"] for entry in updates if entry["update"]["sessionUpdate"] == "tool_call"]
if len(calls) != 1:
raise ValueError("Each prompt must return one associated tool update")
receipt = verify(result, calls[0], text, key, capability, session.session_id)
if receipt["decision"]["verdict"] != ("allow" if number < 2 else "deny"):
raise ValueError("The third prompt must exhaust the two-invocation grant")
records.append({"result": result, "updates": updates, "verified": True})
print(calls[0]["status"] + ": " + receipt["id"])
try:
await client.prompt(session_id="not-an-owned-session", prompt=[TextContentBlock(type="text", text=text)])
except RequestError:
unknown_refused = True
else:
raise ValueError("An unknown session was accepted")
(directory / "acp-capture.json").write_text(json.dumps({
"client": {"package": "agent-client-protocol", "version": "0.12.1"},
"protocol_version": initialized.protocol_version, "session_id": session.session_id,
"trusted_kernel_key": key, "runs": records, "wire": wire,
"verification": {"ok": True, "unknown_session_refused": unknown_refused, "quota_refused": True}
}, indent=2) + "\n")
print("Evidence:", directory.relative_to(ROOT) / "acp-capture.json")
finally:
if process.returncode is None:
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=10)
except asyncio.TimeoutError:
process.kill()
await process.wait()
if directory.exists():
(directory / "wire.json").write_text(json.dumps(wire, indent=2) + "\n")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("text", nargs="?", default="Chio governs the tools behind an ACP agent")
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", "acp-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" / ("acp-" + str(uuid.uuid4()))
asyncio.run(asyncio.wait_for(exercise(target / "debug/acp-host", directory, args.text), timeout=90))
if __name__ == "__main__":
main()Choose the delivery contract
This example maps text prompts to one bounded blocking tool. It advertises no filesystem, terminal, multimodal, session-loading or client MCP-server support. The owning process is the caller’s trust boundary; use a separate process for another principal. The host finishes a call before reading its next frame, so it does not promise mid-call cancellation.
For agent discovery and calls over HTTP, continue with Hello A2A. Both use the same registered document tool and authority configuration.
Reference: earlier custom ACP 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 a client access to your tool
An agentic operating system needs a way to expose useful operations while retaining control of their execution. Chio’s ACP edge presents the operation to a client, resolves its published name, and sends the call through the kernel with the host’s signed capability.
This complete application counts words in your document. It uses the same Rust tool and host as Hello A2A, through Chio’s ACP-shaped stdio methods. The supplied client implements that profile; a drop-in connection to an arbitrary editor is not established by this example.
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 acp sample.mdALLOW 6 words, 43 UTF-8 bytes
Receipt: 22a09e5a3c3883eeb470e128bb26bc6128ae45e1650f88d849bf00d19fb37712
Evidence: runs/71bcadb5-5c69-456c-9c70-b24ab5dc5fe2Replace sample.md with your own UTF-8 file, up to 65,536 bytes. The client reads it and sends its text to count_words. Chio governs that invocation; the client’s initial file read belongs to the application. The output names your evidence directory, which will differ from this capture.
Read the published tool description
The client first sends session/list_capabilities. This complete response advertises count_words, its tool category, its permission requirement and its bridge caveats:
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"capabilities": [
{
"bridgeFidelity": {
"caveats": [
"stream-capable tools execute through deferred `tool/stream` tasks and surface output when resumed via `tool/resume` rather than as incremental push updates",
"partial output is preserved only inside the resumed terminal payload, not incremental ACP updates",
"cancellation is available on deferred `tool/stream` tasks via `tool/cancel`; blocking `tool/invoke` remains terminal",
"generic Chio tools are exposed through ACP's tool category rather than a native ACP primitive"
],
"kind": "adapted"
},
"category": "tool",
"description": "Count whitespace-separated words and UTF-8 bytes",
"id": "count_words",
"name": "count_words",
"requiresPermission": true
}
],
"metadata": {
"chio": {
"authoritative": true,
"authorityPath": "cross_protocol_orchestrator",
"claimEligible": true,
"compatibilityOnly": false,
"invokeMode": "blocking_or_deferred_task",
"lifecycle": {
"toolCancel": "supported",
"toolInvoke": "blocking_terminal_result",
"toolResume": "supported",
"toolStream": "deferred_task_resume"
},
"permissionPreviewOnly": true,
"receiptBearingInvoke": true,
"runtimeLifecycle": {
"blockingEntrypoint": "tool/invoke",
"cancelEntrypoint": "tool/cancel",
"claimEligible": true,
"compatibilityOnly": false,
"followUpEntrypoint": "tool/resume",
"partialOutputDelivery": "resumed_terminal_payload",
"streamDelivery": "resumed_terminal_payload",
"streamEntrypoint": "tool/stream",
"surface": "acp_authoritative"
},
"streamDelivery": "resumed_terminal_payload"
}
}
}
}An advertised ACP capability is a description. The signed kernel capability is a separate object that the host issues to the agent. requiresPermission is true under the default edge configuration; this host supplies the scoped token that satisfies kernel authorization. Discovery does not grant permission, and the flag does not mean this example prompts a human before every call.
Published capability wire type
pub struct AcpCapability {
/// Capability identifier (matches the Chio tool name).
pub id: String,
/// Human-readable name.
pub name: String,
/// Description of the capability.
pub description: String,
/// The ACP category this maps to (e.g., "tool", "fs", "terminal").
pub category: AcpCategory,
/// Whether the capability requires explicit permission.
pub requires_permission: bool,
/// Fidelity assessment for this mapping.
pub bridge_fidelity: BridgeFidelity,
}Connect discovery to authority
Source
Invoke the operation and inspect its result
{
"jsonrpc": "2.0",
"id": 2,
"method": "tool/invoke",
"params": {
"capabilityId": "count_words",
"arguments": {
"text": "One governed tool, two protocol surfaces.\n\n"
}
}
}{
"id": 2,
"jsonrpc": "2.0",
"result": {
"data": {
"stream": [
{
"bytes": 43,
"words": 6
},
{
"content": [
{
"text": "Document counted",
"type": "text"
}
]
}
]
},
"metadata": {
"chio": {
"receiptId": "22a09e5a3c3883eeb470e128bb26bc6128ae45e1650f88d849bf00d19fb37712",
"decision": "allow",
"authorityPath": "cross_protocol_orchestrator"
}
},
"success": true
}
}The host binds the request ID to the governed invocation. The tool returns a word count and a completion message, collated under result.data.stream. The projection keeps the receipt ID and decision metadata; download the complete response for its embedded receipt and bridge context.
Create a deferred task
python3 client.py acp sample.md --deferredTask acp-task-1: working; execution pending
ALLOW 6 words, 43 UTF-8 bytes
Receipt: 8e2ce93d1ce3554bd3fea97f11af1a5e116c369748aa1bd76ede07f54bc17277
Evidence: runs/059427dc-47e2-4ccd-967c-1ff5c3a0f50dtool/stream first returns a working task. The tool has not run at this point, and receiptPending is true:
{
"id": 2,
"jsonrpc": "2.0",
"result": {
"task": {
"id": "acp-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."
}
}
}Resolve the task through the kernel
The client takes the returned task ID and sends tool/resume. This is the step that executes the deferred request and returns the signed decision:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tool/resume",
"params": {
"taskId": "acp-task-1"
}
}{
"id": 3,
"jsonrpc": "2.0",
"result": {
"result": {
"data": {
"stream": [
{
"bytes": 43,
"words": 6
},
{
"content": [
{
"text": "Document counted",
"type": "text"
}
]
}
]
},
"metadata": {
"chio": {
"receiptId": "8e2ce93d1ce3554bd3fea97f11af1a5e116c369748aa1bd76ede07f54bc17277",
"decision": "allow",
"authorityPath": "cross_protocol_orchestrator"
}
},
"success": true
},
"task": {
"id": "acp-task-1",
"metadata": {
"chio": {
"receiptId": "8e2ce93d1ce3554bd3fea97f11af1a5e116c369748aa1bd76ede07f54bc17277",
"decision": "allow",
"authorityPath": "cross_protocol_orchestrator"
}
},
"status": "completed"
}
}
}Chunks are collated in the terminal result; the edge does not push incremental tool events. Read the complete resolved response or the full exchange. Task state remains in this edge process and does not survive its exit.
Cancel before execution
python3 client.py acp sample.md --cancelTask acp-task-1: working; execution pending
Task acp-task-1: cancelled; no tool execution receipt
Evidence: runs/6eae898f-9d5f-48a8-baaf-172613c60844tool/cancel changes the working task to cancelled. It creates no tool execution receipt and consumes no invocation in this project. A later tool/resume does not execute the cancelled request. Read the complete cancellation response.
Change the session’s authority
The host reads this application configuration and issues a signed grant for count_words:
{
"schema": "document-tools.authority.v1",
"tool": "count_words",
"max_invocations": 2,
"lifetime_seconds": 1800
}python3 client.py acp sample.md --repeat 3ALLOW 6 words, 43 UTF-8 bytes
Receipt: 035cd6b933692aeaa665305152a9bad362c774af3354654a751b42a9322b3ec1
ALLOW 6 words, 43 UTF-8 bytes
Receipt: ab40dcda9eea3d95c566c36f41f8ed717f4fa9fd263708ff61048fa5fdb43ace
DENY invocation budget exhausted for capability cap-01a085f0-a7f1-76a2-bb7c-6c9732e0785e
Receipt: ce078cae6df064189d463beb165d7ae0e9b3f85470b5bc11f635d753919dd694
Evidence: runs/946bcc67-a0f6-4edd-b2b7-f5cda7259948Edit max_invocations to 3, then run with --repeat 4. Three distinct calls succeed and the fourth is denied, without rebuilding. The host saves the exact configuration and hashes its bytes into the policy context. Unknown fields are rejected.
authority.json configures this host’s capability issuance; it is not HushSpec. Repeats share one grant. A new program start creates a fresh session, so a deployed operator must authenticate callers and control issuance rather than treating this example as a global account allowance.
Inspect the complete signed capability and original execution context
{
"schema": "chio.capability.v1",
"id": "cap-01a085f0-a23b-71e2-8001-164b961c364e",
"issuer": "b8283c55f336b9dda33bbf43757a2b514950933b5be72a23af46121ede60e7f7",
"subject": "06a5d841637655f52ddfa5b9075cfc77cdd6c6c0bdc5ca48112d89f6993fdc15",
"scope": {
"grants": [
{
"server_id": "documents",
"tool_name": "count_words",
"operations": [
"invoke"
],
"max_invocations": 2
}
]
},
"issued_at": 1788953535,
"expires_at": 1788955335,
"signature": "3e2109e2912e733b03cdc33d86750c41243da89edbbff77dc839276fb3533acf1142d65225abc7cfdeea4677a24ac2662724d7ac0d7a78ae65be5549ac81c804"
}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 = AcpKernelExecutionContext {
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 complete signed record
The first allowed count produced this complete kernel receipt. Its action identifies the supplied text and tool; its stream metadata binds the returned chunks. The surrounding ACP response is not itself the signed artifact.
{
"id": "22a09e5a3c3883eeb470e128bb26bc6128ae45e1650f88d849bf00d19fb37712",
"timestamp": 1788953535,
"capability_id": "cap-01a085f0-a23b-71e2-8001-164b961c364e",
"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-a1e5-7b62-9daa-e163cbb01704",
"operation_id": "d8d91e046714c368850685f386509b3f7c76ac42604095bbbae4740381b6666e",
"projected_dispatch_state": "terminal",
"projected_operation_version": 8,
"projected_state": "completed",
"request_binding_hash": "80ddf27890bdf2d4080c84dce6d5a6b1a16600d1be699a70fc48ed538a19e8ef",
"request_id": "acp-jsonrpc:2",
"request_namespace_digest": "96eb2f4cc661e18e6f9f2dd87a21eda208d5828033bcd3620114b34b7788ec17",
"retained_dispatch_commit": {
"committed_version": 6,
"coordinator_lease_epoch": 1,
"coordinator_lease_id": "01a085f0-a1e5-7b62-9daa-e163cbb01704",
"provider_attempt": {
"attempt_id": "attempt:d8d91e046714c368850685f386509b3f7c76ac42604095bbbae4740381b6666e",
"operation_id": "d8d91e046714c368850685f386509b3f7c76ac42604095bbbae4740381b6666e",
"transport_id": "kernel-tool-server:documents",
"transport_key_epoch": 1
},
"store_fence": {
"lease_id": "01a085f0-a1e5-7b62-9daa-e163cbb01704",
"owner_epoch": 1,
"store_uuid": "01a085f0-a144-7430-8a9a-d370d4eba33e"
}
},
"schema": "chio.admission-receipt.v1",
"store_fence": {
"lease_id": "01a085f0-a1e5-7b62-9daa-e163cbb01704",
"owner_epoch": 1,
"store_uuid": "01a085f0-a144-7430-8a9a-d370d4eba33e"
},
"tool_outcome_id": "181abdfb9cf1cf92fa1b5f0bcf87f48159e00338e734d3fc0b28b2779581b2e0",
"tool_outcome_version": 2,
"trusted_time_unix_ms": 1788953535251
},
"attribution": {
"delegation_depth": 0,
"grant_index": 0,
"issuer_key": "b8283c55f336b9dda33bbf43757a2b514950933b5be72a23af46121ede60e7f7",
"subject_key": "06a5d841637655f52ddfa5b9075cfc77cdd6c6c0bdc5ca48112d89f6993fdc15"
},
"chio_receipt_signing_nonce": "rcpt-01a085f0-a35c-7861-aa6a-c60b884f5ae1",
"receipt_context": {
"request_id": "acp-jsonrpc:2"
},
"route_selection": {
"candidates": [
{
"available": true,
"routeId": "native-route",
"selectedProtocols": [
"acp",
"native"
],
"targetProtocol": "native"
}
],
"decision": "select",
"requestedTargetProtocol": "native",
"routeSelectionId": "2cd301c879c3cfc8f7a9de10cf42860432c8838e8c16b2d558bf7e38a8c5b6f5",
"selectedProtocols": [
"acp",
"native"
],
"selectedRouteId": "native-route",
"selectedTargetProtocol": "native",
"sourceProtocol": "acp"
},
"stream": {
"chunk_hashes": [
"d453126289fdef1956695f0dfd57d82e211d255969ed5ffcf0b54cd59a054f95",
"b2538f4ac125b83068662cf8f4dc7966e50d1bcbfce089b5824edea204d91624"
],
"chunks_expected": 2,
"chunks_received": 2,
"total_bytes": 77
}
},
"trust_level": "mediated",
"kernel_key": "b8283c55f336b9dda33bbf43757a2b514950933b5be72a23af46121ede60e7f7",
"signature": "723b9df2e6159449623cd38a3dd7479d820adeb36003c9cf8544b7c803eb5ae86de3e5fcf4ed1a778b903ec1feb49010f9dab77f4e7f3c7284d964c0c3096b01"
}Download the allowed receipt and quota-denial receipt. The denial is signed evidence of refusal; a valid signature does not turn its decision into an allow.
Find and verify your own result
Replace your-run-id with the directory printed by your command. Read denials from this isolated local store, then verify the first exported receipt against the key your host selected before executing requests:
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": "22a09e5a3c3883eeb470e128bb26bc6128ae45e1650f88d849bf00d19fb37712",
"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. A remote verifier must obtain the expected key through trusted configuration. Continue with offline verification for that workflow.
Build on the complete client
This is the complete client used above. It keeps the host open for the run, discovers the operation, uses returned task IDs and preserves failures. The included session.py handles bounded framing, timeouts and process cleanup.
"""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 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,
})
}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(())
}Replace the document operation
This complete service is shared with Hello A2A. It validates the text and supplies two result chunks. Change the handler, manifest and issued grant together when exposing another operation.
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_tool service
impl ToolServerConnection for HelloToolServer {
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 acp",
"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 name = arguments
.get("name")
.and_then(Value::as_str)
.unwrap_or("world");
Ok(Some(ToolServerStreamResult::Complete(ToolCallStream {
chunks: vec![
ToolCallChunk {
data: json!({"content": [{"type": "text", "text": format!("hello from acp, {name}")}]}),
},
ToolCallChunk {
data: json!({"content": [{"type": "text", "text": "resume complete"}]}),
},
],
})))
}
}Publish a faithful description
The manifest marks count_words as read-only and declares its text schema. Streaming, partial-output and cancellation hints describe the delivery behavior. ACP discovery does not include the full input schema, so this owned client and host share the declared operation contract.
Original greeting manifest
pub fn demo_manifest() -> ToolManifest {
ToolManifest {
schema: "chio.manifest.v1".to_string(),
server_id: SERVER_ID.to_string(),
name: "Hello ACP Server".to_string(),
description: Some("A tiny receipt-bearing ACP hello surface".to_string()),
version: "0.1.0".to_string(),
tools: vec![ToolDefinition {
name: TOOL_NAME.to_string(),
description: "Return a greeting payload".to_string(),
input_schema: json!({
"type": "object",
"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: "hello-acp-manifest".to_string(),
}
}Bind each call to its request ID
The host uses invoke_with_request_id for blocking calls. Each new operation needs a new ID; a retry of the same operation keeps its ID. The mapping also preserves the difference between numeric and string IDs:
use chio_acp_edge::{AcpKernelExecutionContext, ChioAcpEdge};
use chio_kernel::ChioKernel;
use serde_json::{json, Value};
pub fn handle(
edge: &ChioAcpEdge,
request: Value,
kernel: &ChioKernel,
execution: &AcpKernelExecutionContext,
) -> Option<Value> {
if request["method"] != "tool/invoke" {
return edge
.handle_jsonrpc(request, kernel, execution)
.as_value()
.cloned();
}
// This host requires an ID for a blocking tool invocation. Encode its type
// too: JSON-RPC 1 and "1" must not become the same kernel request.
let id = request.get("id")?.clone();
let error = |code, message: &str| {
Some(json!({
"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}
}))
};
if request["jsonrpc"] != "2.0" || !(id.is_string() || id.is_number()) {
return error(
-32600,
"Expected JSON-RPC 2.0 and a string or number request ID",
);
}
let params = &request["params"];
let Some(capability) = params["capabilityId"]
.as_str()
.filter(|s| !s.trim().is_empty())
else {
return error(-32602, "capabilityId must be a nonempty string");
};
let Some(arguments) = params.get("arguments").filter(|v| v.is_object()) else {
return error(-32602, "arguments must be an object");
};
let request_id = format!("acp-jsonrpc:{id}");
match edge.invoke_with_request_id(
&request_id,
capability,
arguments.clone(),
kernel,
execution,
) {
Ok(result) => Some(json!({"jsonrpc": "2.0", "id": id, "result": result})),
Err(reason) => error(-32603, &reason.to_string()),
}
}The generic blocking handler in the inspected public source derives its kernel ID from the tool name and Unix seconds. In a rapid three-call run, it returned the same cached allow receipt three times. This application uses the existing request-ID-aware API, so distinct calls receive distinct decisions and consume the intended allowance.
Original generic greeting serve loop
pub fn serve_reader<R, W>(reader: R, mut writer: W) -> HelloAcpResult<()>
where
R: BufRead,
W: Write,
{
let 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(())
}Read category and fidelity as descriptions
count_words falls into the generic tool category. Its adapted rating carries four captured caveats: deferred execution, terminal-only partial output, cancellation before execution, and the generic tool mapping. These describe translation behavior; they do not issue authority to a client.
Read the public category-inference implementation
fn infer_acp_category(tool: &ToolDefinition, default: AcpCategory) -> AcpCategory {
let name_lower = tool.name.to_lowercase();
if name_lower.contains("read_file")
|| name_lower.contains("write_file")
|| name_lower.contains("list_dir")
|| name_lower.starts_with("fs_")
{
AcpCategory::Filesystem
} else if name_lower.contains("terminal")
|| name_lower.contains("exec")
|| name_lower.contains("shell")
|| name_lower.contains("command")
{
AcpCategory::Terminal
} else if name_lower.contains("browser")
|| name_lower.contains("navigate")
|| name_lower.contains("screenshot")
{
AcpCategory::Browser
} else {
default
}
}Interpret the result before continuing
| Result | Meaning and next action |
|---|---|
ALLOW and a receipt | The kernel authorized the operation. Inspect the count and retain the signed record. |
Invocation budget exhausted | The session consumed its grant. Inspect the signed denial; change authority.json only if more calls are intended. |
Working and receiptPending | The tool has not executed. Resolve the returned task ID or cancel it. |
Cancelled | The deferred request will not execute. There is no tool execution receipt for that state change. |
Protocol or transport error | Read the message. Unknown tool names stop before dispatch; missing frames do not establish a kernel allow or denial. |
Reproduce the original greeting verifier
The public hello-acp example remains available as an implementation check with a fixed greeting. Its captured outputs and assertions are retained here separately from the document application.
Original discovery, invoke and resume checks
Smoke commands, captured results and assertions
$ cd examples/hello-acp
$ ./smoke.shhello-acp smoke passed artifacts: <chio-source>/examples/hello-acp/.artifacts/20260905T114346Z invoke receipt: 78f990d4aca07ce33ae323c2349a0d4398dea32273eb9d7f397c1b06d4ecf851 stream receipt: 37ce5334393eee9bc3e50b4480a01761297d64e365738dd0687471366d9f2a53
$ jq '.result.capabilities[0]' list-capabilities.json{
"bridgeFidelity": {
"caveats": [
"stream-capable tools execute through deferred `tool/stream` tasks and surface output when resumed via `tool/resume` rather than as incremental push updates",
"partial output is preserved only inside the resumed terminal payload, not incremental ACP updates",
"cancellation is available on deferred `tool/stream` tasks via `tool/cancel`; blocking `tool/invoke` remains terminal",
"generic Chio tools are exposed through ACP's tool category rather than a native ACP primitive"
],
"kind": "adapted"
},
"category": "tool",
"description": "Return a greeting payload",
"id": "hello_tool",
"name": "hello_tool",
"requiresPermission": true
}$ jq '.result | {success, data,
$ chio: (.metadata.chio | {decision, receiptId, capabilityId, authorityPath,
$ terminalState})}' tool-invoke.json{
"success": true,
"data": {
"stream": [
{
"content": [
{
"text": "hello from acp, world",
"type": "text"
}
]
},
{
"content": [
{
"text": "resume complete",
"type": "text"
}
]
}
]
},
"chio": {
"decision": "allow",
"receiptId": "78f990d4aca07ce33ae323c2349a0d4398dea32273eb9d7f397c1b06d4ecf851",
"capabilityId": "cap-01a07161-c35a-70e3-9b45-07f5fb47e7e0",
"authorityPath": "cross_protocol_orchestrator",
"terminalState": {
"state": "completed"
}
}
}$ jq '.result.metadata.chio.receipt | del(.metadata)' tool-invoke.json{
"action": {
"parameter_hash": "c05f3d430e01e24c936243d1e2525b8077c5649863eba0384ca2d860922b24e3",
"parameters": {
"name": "world"
}
},
"boundary_class": "prevent",
"capability_id": "cap-01a07161-c35a-70e3-9b45-07f5fb47e7e0",
"content_hash": "98f164497bff463c9240fde7b03e123b0a54044d2e99fe898a95636244de4a43",
"decision": {
"verdict": "allow"
},
"id": "78f990d4aca07ce33ae323c2349a0d4398dea32273eb9d7f397c1b06d4ecf851",
"kernel_key": "1abd58e32ebbc19287189b52946ec8386189d0021cccfa60be28be479447ba97",
"policy_hash": "hello-acp-policy",
"receipt_kind": "mediated_decision",
"redaction_mode": "none",
"signature": "df85abc8229742ed6b3b6cfde147f919882639f72621bab13f7300afb3d0cdc32e55302fcfb82455de2cafb4677395569c0dd537a5c504351195723d594d3d09",
"timestamp": 1788608627,
"tool_name": "hello_tool",
"tool_origin": "caller_executed",
"tool_server": "hello-acp-srv",
"trust_level": "mediated"
}$ cat tool-stream.json{
"id": 3,
"jsonrpc": "2.0",
"result": {
"task": {
"id": "acp-task-1",
"metadata": {
"chio": {
"authoritative": true,
"authorityPath": "cross_protocol_orchestrator",
"capabilityId": null,
"claimEligible": true,
"compatibilityOnly": false,
"decision": "pending",
"lifecycle": {
"toolCancel": "supported",
"toolInvoke": "blocking_terminal_result",
"toolResume": "supported",
"toolStream": "deferred_task_resume"
},
"receipt": null,
"receiptBearing": false,
"receiptId": null,
"receiptPending": true,
"runtimeLifecycle": {
"blockingEntrypoint": "tool/invoke",
"cancelEntrypoint": "tool/cancel",
"claimEligible": true,
"compatibilityOnly": false,
"followUpEntrypoint": "tool/resume",
"partialOutputDelivery": "resumed_terminal_payload",
"streamDelivery": "resumed_terminal_payload",
"streamEntrypoint": "tool/stream",
"surface": "acp_authoritative"
}
}
},
"status": "working",
"statusMessage": "Task accepted for authoritative deferred execution."
}
}
}$ jq '{task: {id: .result.task.id, status: .result.task.status},
$ result: (.result.result | {success, data,
$ chio: (.metadata.chio | {decision, receiptId, capabilityId})})}' tool-resume.json{
"task": {
"id": "acp-task-1",
"status": "completed"
},
"result": {
"success": true,
"data": {
"stream": [
{
"content": [
{
"text": "hello from acp, world",
"type": "text"
}
]
},
{
"content": [
{
"text": "resume complete",
"type": "text"
}
]
}
]
},
"chio": {
"decision": "allow",
"receiptId": "37ce5334393eee9bc3e50b4480a01761297d64e365738dd0687471366d9f2a53",
"capabilityId": "cap-01a07161-c35a-70e3-9b45-07f5fb47e7e0"
}
}
}$ jq '.result.metadata.chio | {lifecycle, invokeMode, permissionPreviewOnly,
$ receiptBearingInvoke, streamDelivery}' list-capabilities.json{
"lifecycle": {
"toolCancel": "supported",
"toolInvoke": "blocking_terminal_result",
"toolResume": "supported",
"toolStream": "deferred_task_resume"
},
"invokeMode": "blocking_or_deferred_task",
"permissionPreviewOnly": true,
"receiptBearingInvoke": true,
"streamDelivery": "resumed_terminal_payload"
}$ ARTIFACTS=.artifacts/latest
$ wc -c < "$ARTIFACTS/logs/edge.log"0
assert listed["result"]["capabilities"][0]["id"] == "hello_tool", listed
assert invoked["result"]["success"] is True, invoked
assert invoked["result"]["metadata"]["chio"]["authorityPath"] == "cross_protocol_orchestrator", invoked
assert invoked["result"]["metadata"]["chio"]["receiptId"], invoked
assert streamed["result"]["task"]["status"] == "working", streamed
assert streamed["result"]["task"]["metadata"]["chio"]["receiptPending"] is True, streamed
assert resumed["result"]["task"]["status"] == "completed", resumed
assert resumed["result"]["result"]["metadata"]["chio"]["receiptId"], resumedChoose the edge or the proxy
Use this edge when you own the tool and your client implements the shown ACP-shaped methods. To put Chio in front of an existing agent process, follow Wrap an ACP Server. That proxy is a different integration direction. This project opens no network listener and does not establish compatibility with an arbitrary editor.
Continue with the same operation
Switch the client command to a2a and follow Hello A2A with the same document and authority file. The Bridges reference covers the wider contract. For an existing API, OpenAPI Sidecar provides the application path.