Chio/Docs
LOGIN · JOIN

BuildProtocols

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.

Run the official ACP clientbash
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.

CallProtocol resultSigned decisionRetained 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.

acp_client.py · initialize, create a session and promptpython
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.

acp_client.py · verify the selected signerpython
if not verify_receipt_with_trusted_signers(receipt, [key])["ok"]:
    raise ValueError("Invalid receipt or untrusted kernel")
EvidenceQuestion 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
ACP first call · complete signed receiptjson
{
  "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
ACP third call · complete signed refusaljson
{
  "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
authority.jsonjson
{
  "schema": "document-tools.authority.v1",
  "tool": "count_words",
  "max_invocations": 2,
  "lifetime_seconds": 1800
}
Read src/service.rs 93 lines · complete file
src/service.rsrust
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
src/bin/acp-host.rsrust
#[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
src/host.rsrust
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
acp_client.pypython
# /// 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:

Run the custom profilebash
python3 run.py acp sample.md
Captured ACP resulttext
ALLOW 6 words, 43 UTF-8 bytes
Receipt: 22a09e5a3c3883eeb470e128bb26bc6128ae45e1650f88d849bf00d19fb37712
Evidence: runs/71bcadb5-5c69-456c-9c70-b24ab5dc5fe2

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

Captured ACP discovery · completejson
{
  "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
crates/protocol/chio-acp-edge/src/types.rs6-19rust
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

Discovery names the tool; a separate signed grant authorizes execution
Discovery names the tool; a separate signed grant authorizes executionThe client discovers count_words in a capability advertisement. The host resolves that name to its registered operation. A separately issued signed grant limits kernel execution to two calls. Allowed dispatch reaches the document tool and the kernel retains a signed decision.Capability advertisementcount_words · permission requiredDiscoverYour ACP clientSend tool/invoke with an IDAPPLICATION HOSTResolve count_wordsPublished name → operationdocuments / count_wordsPublishSigned capability tokenIssued to this agentTwo calls · 30 minutesChio kernelCheck authority before dispatchAllowedDocument toolCount caller-provided textSigned decisionRetained in the receipt storeDiscovery names the tool; a separate signed grant authorizes executionThe client discovers count_words in a capability advertisement. The host resolves that name to its registered operation. A separately issued signed grant limits kernel execution to two calls. Allowed dispatch reaches the document tool and the kernel retains a signed decision.Capability advertisementcount_wordsrequiresPermission: trueDiscoverYour ACP clienttool/invoke + request IDAPPLICATION HOSTResolve the tool namedocuments / count_wordsSignedgrantTwo calls30 minutesKernelCheckauthoritySigneddecisionReceipt storeDocumenttoolCount textAllowed requests reach the tool.
A published tool name resolves to a registered operation. The host’s signed grant limits kernel execution, and the resulting decision is retained separately from discovery metadata.
Source

Invoke the operation and inspect its result

Captured tool/invoke request · completejson
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tool/invoke",
  "params": {
    "capabilityId": "count_words",
    "arguments": {
      "text": "One governed tool, two protocol surfaces.\n\n"
    }
  }
}
Captured tool/invoke response · selected metadatajson
{
  "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

Run the deferred ACP pathbash
python3 client.py acp sample.md --deferred
Captured deferred ACP resulttext
Task acp-task-1: working; execution pending
ALLOW 6 words, 43 UTF-8 bytes
Receipt: 8e2ce93d1ce3554bd3fea97f11af1a5e116c369748aa1bd76ede07f54bc17277
Evidence: runs/059427dc-47e2-4ccd-967c-1ff5c3a0f50d

tool/stream first returns a working task. The tool has not run at this point, and receiptPending is true:

Captured working ACP task · selected metadatajson
{
  "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:

Captured tool/resume request · completejson
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tool/resume",
  "params": {
    "taskId": "acp-task-1"
  }
}
Captured resumed ACP result · selected metadatajson
{
  "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

Create and cancel an ACP taskbash
python3 client.py acp sample.md --cancel
Captured ACP cancellationtext
Task acp-task-1: working; execution pending
Task acp-task-1: cancelled; no tool execution receipt
Evidence: runs/6eae898f-9d5f-48a8-baaf-172613c60844

tool/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:

authority.json · completejson
{
  "schema": "document-tools.authority.v1",
  "tool": "count_words",
  "max_invocations": 2,
  "lifetime_seconds": 1800
}
Exhaust one ACP session’s allowance · exits 2bash
python3 client.py acp sample.md --repeat 3
Captured ACP allowance exhaustiontext
ALLOW 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-f5cda7259948

Edit 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
Captured ACP capability · completejson
{
  "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"
}
examples/hello-acp/src/lib.rs133-166rust
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.

Captured ACP kernel receipt · completejson
{
  "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:

Inspect your ACP runbash
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
Captured ACP verification resultjson
{
  "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.

client.py · completepython
"""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
session.py · completepython
"""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()
protocol-tools/src/host.rsL1–124rust
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,
    })
}

Open the complete source file

protocol-tools/src/main.rsL1–93rust
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(())
}

Open the complete source file

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.

protocol-tools/src/service.rsL1–93rust
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,
    }
}

Open the complete source file

Original hello_tool service
examples/hello-acp/src/lib.rs23-71rust
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
examples/hello-acp/src/lib.rs101-126rust
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:

protocol-tools/src/acp.rsL1–52rust
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()),
    }
}

Open the complete source file

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
examples/hello-acp/src/lib.rs183-207rust
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
crates/protocol/chio-acp-edge/src/bridge.rs65-87rust
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

ResultMeaning 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
hello-acp · smoketranscript
$ cd examples/hello-acp
$ ./smoke.sh
hello-acp smoke passed
artifacts: <chio-source>/examples/hello-acp/.artifacts/20260905T114346Z
invoke receipt: 78f990d4aca07ce33ae323c2349a0d4398dea32273eb9d7f397c1b06d4ecf851
stream receipt: 37ce5334393eee9bc3e50b4480a01761297d64e365738dd0687471366d9f2a53
exit 0
hello-acp · capabilitytranscript
$ 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
}
exit 0
hello-acp · tool-invoketranscript
$ 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"
    }
  }
}
exit 0
hello-acp · invoke-receipttranscript
$ 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"
}
exit 0allow
hello-acp · tool-streamtranscript
$ 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."
    }
  }
}
exit 0
hello-acp · tool-resumetranscript
$ 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"
    }
  }
}
exit 0
hello-acp · lifecycletranscript
$ 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"
}
exit 0
hello-acp · edge-logtranscript
$ ARTIFACTS=.artifacts/latest
$ wc -c < "$ARTIFACTS/logs/edge.log"
0
exit 0
examples/hello-acp/smoke.sh96-105python
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"], resumed

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