Chio/Docs
LOGIN · JOIN

BuildProtocols

Hello A2A

Call your Rust tool through the official A2A client. Discover the agent, count supplied text, and verify its signed result.

Give another agent a useful operation over HTTP, with a discoverable skill, a bounded grant and a signed record of every call. The official A2A Python client connects to the supplied Rust host, counts your text twice and receives a verifiable refusal when its allowance runs out.

Run the complete A2A application

Download the project, extract it anywhere and open its directory. Builder Setup supplies the persistent Rust and uv prerequisites. The project includes its Python SDK source and pins its Rust dependencies to the public source recorded in its manifest.

Run the official A2A clientbash
uv run --locked a2a_client.py "Review the release checklist"

The command builds the host, owns its lifetime and verifies the results. Each run creates a private state directory with its authority, receipt database and complete exchange. A model key is unnecessary: this agent performs a deterministic document operation that you can replace with your own tool.

CallProtocol resultSigned decisionRetained receipt
1
TASK_STATE_COMPLETED
allow
630d12d93d82f777…
2
TASK_STATE_COMPLETED
allow
a979760889013889…
3
TASK_STATE_FAILED
deny
ab00d19788881954…

Captured with a2a-sdk 1.1.2. Your identifiers will differ. The third call is an expected refusal; the verification command succeeds only when both permitted work and refusal match the configured two-call grant.

Follow the actual protocol

The host exposes an Agent Card and an authenticated JSON-RPC endpoint on an available loopback port. The client uses A2A 1.0 SendMessage with structured tool arguments, then checks GetTask against the original result. A retried message ID returns the same task without spending another invocation.

a2a_client.py · send a structured messagepython
message = Message(
    message_id=message_id,
    role=Role.ROLE_USER,
    parts=[Part(data=ParseDict({"text": text}, ProtobufValue()))],
)
response = await client.send_message(SendMessageRequest(
    message=message,
    metadata={"chio": {"targetSkillId": "count_words"}},
))
Read Agent Card · captured discovery 37 lines · complete file
Agent Card · captured discoveryjson
{
  "name": "Chio document analyst",
  "description": "Count caller-provided text under a bounded signed grant",
  "supportedInterfaces": [
    {
      "url": "http://127.0.0.1:33067",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "version": "0.1.0",
  "capabilities": {
    "streaming": false,
    "pushNotifications": false
  },
  "defaultInputModes": [
    "application/json"
  ],
  "defaultOutputModes": [
    "application/json",
    "text/plain"
  ],
  "skills": [
    {
      "id": "count_words",
      "name": "count_words",
      "description": "Count whitespace-separated words and UTF-8 bytes",
      "inputModes": [
        "application/json"
      ],
      "outputModes": [
        "application/json",
        "text/plain"
      ]
    }
  ]
}
Read SendMessage · actual client request 23 lines · complete file
SendMessage · actual client requestjson
{
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "4d04eaf4-407f-439b-bee3-4ee84b36f85a",
      "role": "ROLE_USER",
      "parts": [
        {
          "data": {
            "text": "Chio gives agent operations explicit authority and evidence"
          }
        }
      ]
    },
    "metadata": {
      "chio": {
        "targetSkillId": "count_words"
      }
    }
  },
  "id": "5217355c-5550-4563-a280-2e7ed06d5d4d",
  "jsonrpc": "2.0"
}

Bind the result to the operation

The client selects the kernel key from its own host before sending work. It verifies the signature, capability, exact input and parameter hash, operation ID and output hash. Streamed output is checked chunk by chunk before the aggregate digest. It also verifies every projected artifact and the retrieved task.

a2a_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 A2A first call · complete signed receipt 106 lines · complete file
A2A first call · complete signed receiptjson
{
  "trust_level": "mediated",
  "boundary_class": "prevent",
  "action": {
    "parameter_hash": "f0d237fe0bfdf5ee7bde0d96d6555723e2effa1281eb6d1d4ce514494b165e77",
    "parameters": {
      "text": "Chio gives agent operations explicit authority and evidence"
    }
  },
  "redaction_mode": "none",
  "tool_name": "count_words",
  "policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
  "kernel_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383",
  "tool_server": "documents",
  "content_hash": "8acf8908e7aeb801d9ee99e2965eb052c1d90e2e04f97d50be02ae149d079bd6",
  "metadata": {
    "route_selection": {
      "selectedProtocols": [
        "a2a",
        "native"
      ],
      "selectedRouteId": "native-route",
      "requestedTargetProtocol": "native",
      "selectedTargetProtocol": "native",
      "candidates": [
        {
          "selectedProtocols": [
            "a2a",
            "native"
          ],
          "routeId": "native-route",
          "available": true,
          "targetProtocol": "native"
        }
      ],
      "decision": "select",
      "routeSelectionId": "fe26d5b5a64ff93b186824e0cc61311ed384ac1dcfeeafb8d3d399b6fc33a0d0",
      "sourceProtocol": "a2a"
    },
    "admission_operation": {
      "retained_dispatch_commit": {
        "committed_version": 6,
        "provider_attempt": {
          "transport_key_epoch": 1,
          "attempt_id": "attempt:5cd58f78cab93490cd27f7590ec734ea33284f55e3d250ff1dd687ba21deaef5",
          "operation_id": "5cd58f78cab93490cd27f7590ec734ea33284f55e3d250ff1dd687ba21deaef5",
          "transport_id": "kernel-tool-server:documents"
        },
        "coordinator_lease_epoch": 1,
        "store_fence": {
          "lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3",
          "store_uuid": "01a08ea1-c9d3-77f1-84e1-57c471839cb6",
          "owner_epoch": 1
        },
        "coordinator_lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3"
      },
      "tool_outcome_id": "2df324d9b326f2ade4bb9c97a674cf2d7345b3031f2c1d16cf840622af0692f9",
      "trusted_time_unix_ms": 1789099363717,
      "projected_operation_version": 8,
      "projected_state": "completed",
      "schema": "chio.admission-receipt.v1",
      "request_namespace_digest": "08d7f6a8ba0a6a0ebe77a058222095ae5782509e270c73f75e7fb5fcc0c87ebc",
      "coordinator_lease_epoch": 1,
      "operation_id": "5cd58f78cab93490cd27f7590ec734ea33284f55e3d250ff1dd687ba21deaef5",
      "compensation_status": "not_compensated",
      "request_binding_hash": "b9483b689b03e16d9cd0b41ba788d3a880574b0884877dcbef158c35a8a7c6c0",
      "tool_outcome_version": 2,
      "projected_dispatch_state": "terminal",
      "request_id": "a2a-message:814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825:4d04eaf4-407f-439b-bee3-4ee84b36f85a",
      "store_fence": {
        "lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3",
        "store_uuid": "01a08ea1-c9d3-77f1-84e1-57c471839cb6",
        "owner_epoch": 1
      },
      "coordinator_lease_id": "01a08ea1-cb39-77d9-85fc-7a57d23c2ea3"
    },
    "receipt_context": {
      "request_id": "a2a-message:814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825:4d04eaf4-407f-439b-bee3-4ee84b36f85a"
    },
    "attribution": {
      "grant_index": 0,
      "delegation_depth": 0,
      "subject_key": "814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825",
      "issuer_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383"
    },
    "stream": {
      "chunks_received": 2,
      "chunks_expected": 2,
      "total_bytes": 77,
      "chunk_hashes": [
        "c59ae49b669759807ff89ce98ab61530df38c047104676a7defc2e7d301324e3",
        "b2538f4ac125b83068662cf8f4dc7966e50d1bcbfce089b5824edea204d91624"
      ]
    },
    "chio_receipt_signing_nonce": "rcpt-01a08ea1-cdaa-748c-a529-317ec183de4d"
  },
  "id": "630d12d93d82f7773ba04bae94376d77818db661c69231e4ffe88753b70ccaa0",
  "tool_origin": "caller_executed",
  "capability_id": "cap-01a08ea1-cbce-7282-b0ab-190a5b50e499",
  "signature": "f9e2165a32ff8182770dfb750e28277c5668b250080ace54c6a5146c3ac59d8ae02d1e697709944bac2ac00691dfa3dd495f541c90a33ae6ce9fc181bfd34507",
  "decision": {
    "verdict": "allow"
  },
  "timestamp": 1789099363,
  "receipt_kind": "mediated_decision"
}
Read A2A third call · complete signed refusal 66 lines · complete file
A2A third call · complete signed refusaljson
{
  "trust_level": "mediated",
  "boundary_class": "prevent",
  "action": {
    "parameter_hash": "f0d237fe0bfdf5ee7bde0d96d6555723e2effa1281eb6d1d4ce514494b165e77",
    "parameters": {
      "text": "Chio gives agent operations explicit authority and evidence"
    }
  },
  "redaction_mode": "none",
  "tool_name": "count_words",
  "policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
  "kernel_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383",
  "tool_server": "documents",
  "content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
  "metadata": {
    "route_selection": {
      "selectedProtocols": [
        "a2a",
        "native"
      ],
      "selectedRouteId": "native-route",
      "requestedTargetProtocol": "native",
      "selectedTargetProtocol": "native",
      "candidates": [
        {
          "selectedProtocols": [
            "a2a",
            "native"
          ],
          "routeId": "native-route",
          "available": true,
          "targetProtocol": "native"
        }
      ],
      "decision": "select",
      "routeSelectionId": "9ed946ad973f8e988786f0cd80309546d67d773f44fe50c3c70c4341aa6819ee",
      "sourceProtocol": "a2a"
    },
    "receipt_context": {
      "request_id": "a2a-message:814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825:0c31844e-c67f-4477-9420-b36e1fa6efa9"
    },
    "attribution": {
      "delegation_depth": 0,
      "subject_key": "814d212f0f40657b29caadb395b494fd6bb07eb0e431ca43d05b03271c5fd825",
      "issuer_key": "76cbb3c9ea1166015df0e8a21f61d3f3114486ee16da30989651c5760c2fc383"
    },
    "budget_authority": {
      "guarantee_level": "single_node_atomic",
      "metering_profile": "max_cost_preauthorize_then_reconcile_actual",
      "authority_profile": "authoritative_hold_event"
    },
    "chio_receipt_signing_nonce": "rcpt-01a08ea1-cf53-7211-8b17-ff6d409298d9"
  },
  "id": "ab00d1978888195456fb894b842506d65caebd08c31734a03b55d0778e5f104e",
  "tool_origin": "caller_executed",
  "capability_id": "cap-01a08ea1-cbce-7282-b0ab-190a5b50e499",
  "signature": "e76242b95a04cbb9628addab399033f95c71ef570b5bbc4a749fbb3e5a2be365c941d946f9b2e80034ad1d6fed84cf55ab832ab8433df835f49ad0d8eb650700",
  "decision": {
    "guard": "kernel",
    "verdict": "deny",
    "reason": "invocation budget exhausted for capability cap-01a08ea1-cbce-7282-b0ab-190a5b50e499"
  },
  "timestamp": 1789099364,
  "receipt_kind": "mediated_decision"
}

Download the complete captured exchange, including the selected public key and verification results. The original record is retained; the table above only shortens identifiers for reading.

Make the operation yours

Replace the handler in src/service.rs, update its manifest schema and adjust authority.json to grant the intended operation. The host registers the tool, creates a durable kernel and issues the bounded capability. Keep the verifier’s input and output checks aligned with your new contract.

Read authority.json 6 lines · complete file
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/a2a-host.rs 133 lines · complete file
src/bin/a2a-host.rsrust
#[path = "../host.rs"]
mod host;
#[path = "../service.rs"]
mod service;

use axum::{
    extract::{DefaultBodyLimit, State},
    http::{HeaderMap, StatusCode},
    routing::{get, post},
    Json, Router,
};
use chio_a2a_edge::{A2aEdgeConfig, A2aKernelExecutionContext, ChioA2aEdge};
use serde_json::{json, Value};
use std::{
    path::PathBuf,
    sync::{Arc, Mutex},
};

struct App {
    edge: Mutex<ChioA2aEdge>,
    kernel: chio_kernel::ChioKernel,
    execution: A2aKernelExecutionContext,
    token: String,
    card: Value,
    blocking: chio_example_runtime::tasks::BlockingPool,
}

async fn invoke(
    State(app): State<Arc<App>>,
    headers: HeaderMap,
    Json(message): Json<Value>,
) -> Result<Json<Value>, (StatusCode, &'static str)> {
    let supplied = headers
        .get("authorization")
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "))
        .unwrap_or("");
    if !chio_example_runtime::credential_matches(supplied, &app.token) {
        return Err((
            StatusCode::UNAUTHORIZED,
            "A valid application credential is required",
        ));
    }
    let pool = app.blocking.clone();
    let response = pool
        .run(move || {
            let mut edge = app
                .edge
                .lock()
                .map_err(|_| anyhow::anyhow!("Protocol state is unavailable"))?;
            Ok(edge
                .handle_v1_jsonrpc(message, &app.kernel, &app.execution)
                .into_value())
        })
        .await
        .map_err(|error| {
            eprintln!("Protocol execution refused: {error}");
            (
                StatusCode::SERVICE_UNAVAILABLE,
                "Protocol execution unavailable",
            )
        })?;
    response
        .map(Json)
        .ok_or((StatusCode::BAD_REQUEST, "A2A requests require an ID"))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let directory = PathBuf::from(
        std::env::args()
            .nth(1)
            .ok_or_else(|| anyhow::anyhow!("Usage: a2a-host NEW_SESSION_DIRECTORY"))?,
    );
    let host = host::boot(&directory)?;
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
    let endpoint = format!("http://{}", listener.local_addr()?);
    let edge = ChioA2aEdge::new(
        A2aEdgeConfig {
            agent_name: "Chio document analyst".into(),
            agent_description: "Count caller-provided text under a bounded signed grant".into(),
            endpoint_url: endpoint.clone(),
            ..A2aEdgeConfig::default()
        },
        vec![host.manifest],
    )?;
    let token = chio_core::crypto::Keypair::generate().public_key().to_hex();
    std::fs::write(directory.join("client-token.txt"), &token)?;
    let execution = A2aKernelExecutionContext {
        agent_id: host.capability.subject.to_hex(),
        capability: host.capability,
        dpop_proof: None,
        execution_nonce: None,
        governed_intent: None,
        approval_token: None,
        approval_tokens: Vec::new(),
        threshold_approval_proposal: None,
        supplemental_authorization: None,
        model_metadata: None,
    };
    let app = Arc::new(App {
        card: edge.agent_card_v1(),
        edge: Mutex::new(edge),
        kernel: host.kernel,
        execution,
        token,
        blocking: chio_example_runtime::tasks::BlockingPool::new(2),
    });
    let router = Router::new()
        .route("/", post(invoke))
        .route(
            "/.well-known/agent-card.json",
            get(|State(app): State<Arc<App>>| async move { Json(app.card.clone()) }),
        )
        .layer(DefaultBodyLimit::max(65_536))
        .with_state(app.clone());
    std::fs::write(
        directory.join("server.json"),
        serde_json::to_vec_pretty(&json!({"endpoint":endpoint}))?,
    )?;
    eprintln!(
        "A2A 1.0 listening at {endpoint}; credential: {}",
        directory.join("client-token.txt").display()
    );
    let signal = chio_example_runtime::shutdown_signal()?;
    let served = axum::serve(listener, router)
        .with_graceful_shutdown(signal)
        .await;
    let drained = app.blocking.drain(std::time::Duration::from_secs(15)).await;
    served?;
    drained?;
    Ok(())
}
Read src/host.rs 124 lines · complete file
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 a2a_client.py 161 lines · complete file
a2a_client.pypython
# /// script
# requires-python = ">=3.11"
# dependencies = ["a2a-sdk==1.1.2", "chio-sdk"]
# [tool.uv.sources]
# chio-sdk = { path = "sdk/chio-py" }
# ///
"""Call a real Chio HTTP agent with the official A2A Python client."""
import argparse
import asyncio
import json
import os
from pathlib import Path
import subprocess
import time
import uuid

import httpx
from a2a.client import A2ACardResolver
from a2a.client.transports.jsonrpc import JsonRpcTransport
from a2a.types import GetTaskRequest, Message, Part, Role, SendMessageRequest
from google.protobuf.json_format import MessageToDict, ParseDict
from google.protobuf.struct_pb2 import Value as ProtobufValue
from chio.invariants import canonicalize_json, sha256_hex_utf8, verify_receipt_with_trusted_signers

ROOT = Path(__file__).resolve().parent


def projected_parts(value):
    if isinstance(value, str):
        return [{"text": value}]
    if isinstance(value, dict) and isinstance(value.get("content"), list):
        return [{"text": part["text"]} for part in value["content"] if isinstance(part.get("text"), str)]
    if isinstance(value, (dict, list)):
        return [{"data": value}]
    return [{"text": json.dumps(value, separators=(",", ":"))}]


def verify_task(task, text, key):
    receipt = task["metadata"]["chio"]["receipt"]
    if not verify_receipt_with_trusted_signers(receipt, [key])["ok"]:
        raise ValueError("Invalid receipt or untrusted kernel")
    if task["metadata"]["chio"]["receiptId"] != receipt["id"]:
        raise ValueError("Task references another receipt")
    if receipt["action"]["parameters"] != {"text": text}:
        raise ValueError("Receipt binds different tool input")
    if sha256_hex_utf8(canonicalize_json({"text": text})) != receipt["action"]["parameter_hash"]:
        raise ValueError("Input hash differs")
    allowed = receipt["decision"]["verdict"] == "allow"
    if allowed != (task["status"]["state"] == "TASK_STATE_COMPLETED"):
        raise ValueError("Task status differs from the signed decision")
    if allowed:
        output = task["artifacts"][0]["parts"][0]["data"]
        if output != {"words": len(text.split()), "bytes": len(text.encode())}:
            raise ValueError("The agent did not return the actual document count")
        retained = task["metadata"]["chio"]["retainedOutput"]
        if retained["kind"] == "stream":
            hashes = [sha256_hex_utf8(canonicalize_json(chunk)) for chunk in retained["chunks"]]
            if hashes != receipt["metadata"]["stream"]["chunk_hashes"]:
                raise ValueError("A retained stream chunk differs from its signed hash")
            content_hash = sha256_hex_utf8("".join(hashes))
            expected_parts = [part for chunk in retained["chunks"] for part in projected_parts(chunk)]
            if retained["chunks"][0] != output:
                raise ValueError("A2A projected another document count")
        else:
            content_hash = sha256_hex_utf8(canonicalize_json(retained["value"]))
            expected_parts = projected_parts(retained["value"])
            if retained["value"] != output:
                raise ValueError("A2A projected another tool output")
        if task["artifacts"][0]["parts"] != expected_parts:
            raise ValueError("A projected artifact changed after the kernel signed its output")
        if content_hash != receipt["content_hash"]:
            raise ValueError("Tool output differs from its signed hash")
    return receipt


async def exercise(endpoint, token, key, capability, text, directory):
    async with httpx.AsyncClient(timeout=30) as public:
        refused = await public.post(endpoint, json={"jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": {}})
        if refused.status_code != 401:
            raise ValueError("Missing application credential was not refused")
    wire = []
    async def capture(response):
        await response.aread()
        if response.request.method == "POST":
            wire.append({"request": json.loads(response.request.content), "response": response.json()})
            (directory / "wire.json").write_text(json.dumps(wire, indent=2) + "\n")
    async with httpx.AsyncClient(headers={"Authorization": "Bearer " + token}, timeout=30,
                                 event_hooks={"response": [capture]}) as http:
        card = await A2ACardResolver(http, endpoint).get_agent_card()
        client = JsonRpcTransport(http, card, endpoint)
        records = []
        for number in range(3):
            message_id = str(uuid.uuid4())
            request = SendMessageRequest(message=Message(message_id=message_id, role=Role.ROLE_USER,
                parts=[Part(data=ParseDict({"text": text}, ProtobufValue()))]), metadata={"chio": {"targetSkillId": "count_words"}})
            response = await client.send_message(request)
            task = MessageToDict(response)["task"]
            receipt = verify_task(task, text, key)
            if receipt["decision"]["verdict"] != ("allow" if number < 2 else "deny"):
                raise ValueError("The third call must hit the two-invocation grant")
            expected_request = "a2a-message:" + capability["subject"] + ":" + message_id
            if receipt["capability_id"] != capability["id"]:
                raise ValueError("Receipt belongs to another capability")
            if receipt["metadata"]["receipt_context"]["request_id"] != expected_request:
                raise ValueError("Receipt belongs to another protocol message")
            lookup = MessageToDict(await client.get_task(GetTaskRequest(id=task["id"])))
            if lookup != task:
                raise ValueError("GetTask returned another task or changed its evidence")
            if number == 0:
                retry = MessageToDict(await client.send_message(request))["task"]
                if retry != task:
                    raise ValueError("An identical retry did not retain the original result")
            records.append({"message_id": message_id, "task": task, "verified": True})
            print(task["status"]["state"] + ": " + receipt["id"])
        capture_record = {"client": {"package": "a2a-sdk", "version": "1.1.2", "transport": "JsonRpcTransport"},
            "trusted_kernel_key": key, "agent_card": MessageToDict(card), "runs": records,
            "wire": wire, "verification": {"ok": True, "missing_credential_refused": True,
                "identical_retry_retained": True, "task_lookup_bound": True, "quota_refused": True}}
        (directory / "a2a-capture.json").write_text(json.dumps(capture_record, indent=2) + "\n")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("text", nargs="?", default="Chio gives agent operations explicit authority and evidence")
    args = parser.parse_args()
    if not 0 < len(args.text.encode()) <= 60_000:
        parser.error("text must contain 1 to 60000 UTF-8 bytes")
    subprocess.run(["cargo", "build", "--locked", "--bin", "a2a-host"], cwd=ROOT, check=True)
    target = Path(os.environ.get("CARGO_TARGET_DIR", ROOT / "target"))
    if not target.is_absolute():
        target = ROOT / target
    os.umask(0o077)
    (ROOT / ".state").mkdir(exist_ok=True)
    directory = ROOT / ".state" / ("a2a-" + str(uuid.uuid4()))
    with (ROOT / ".state" / (directory.name + ".log")).open("w") as log:
        process = subprocess.Popen([str(target / "debug/a2a-host"), str(directory)], cwd=ROOT,
                                   stdout=log, stderr=log, stdin=subprocess.DEVNULL)
        try:
            deadline = time.monotonic() + 30
            while not (directory / "server.json").exists():
                if process.poll() is not None or time.monotonic() > deadline:
                    raise RuntimeError("A2A host did not become ready; inspect its .state log")
                time.sleep(.1)
            endpoint = json.loads((directory / "server.json").read_text())["endpoint"]
            token = (directory / "client-token.txt").read_text().strip()
            key = (directory / "kernel-public-key.txt").read_text().strip()
            capability = json.loads((directory / "capability.json").read_text())
            asyncio.run(exercise(endpoint, token, key, capability, args.text, directory))
            print("Evidence:", directory.relative_to(ROOT) / "a2a-capture.json")
        finally:
            if process.poll() is None:
                process.terminate()
            try:
                process.wait(timeout=10)
            except subprocess.TimeoutExpired:
                process.kill()
                process.wait()


if __name__ == "__main__":
    main()

Choose the delivery contract

This example is a blocking HTTP agent. It advertises JSON input and JSON/text output, with no SSE, push notifications or asynchronous continuations. Task lookup is scoped to the caller and retained for five minutes, up to 128 results; SQLite kernel receipts persist after shutdown. Missing application credentials are refused before execution.

For a caller-owned stdio session, continue with Hello ACP. Both use the same registered document tool and authority configuration.

Reference: earlier custom A2A tool profile and captured examples

This reference preserves the earlier custom methods and their recorded behavior. Use the official-client application above for the standard protocol. The complete project retains these older clients for comparison.

Give other agents a governed operation

Chio’s Rust kernel lets an agentic operating system expose tools across protocol boundaries while retaining control of execution. An agent-facing skill identifies the operation; a signed capability determines the authority to call it. A terminal result carries the kernel’s signed decision.

This project makes that path concrete: count words in a document you supply, then try a deferred task and a refused third call. You own the tool, host, client and evidence. It uses Chio’s A2A JSON-RPC profile over local stdio. The included Agent Card describes the skill; the project opens no HTTP listener.

Count your document

Use the same extracted project as the official client. The custom profile accepts a UTF-8 document file. Its launcher builds the host and runs the local client:

Run the custom profilebash
python3 run.py a2a sample.md
Captured A2A resulttext
ALLOW 6 words, 43 UTF-8 bytes
Receipt: 37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26
Evidence: runs/ac367b60-a095-4fcd-bc94-ecb0c8537ccd

Replace sample.md with your own UTF-8 file, up to 65,536 bytes. The sample contains six whitespace-separated words. Each run prints its own evidence directory; receipt and directory IDs will differ. The client reads your file before sending its text. Chio mediates the tool invocation, while that initial file read belongs to the application.

Discover the document skill

The host saves this complete Agent Card as discovery.json in your run directory. It advertises count_words and explains the edge’s delivery behavior through bridgeFidelity:

Captured Agent Card · completejson
{
  "name": "Document tools",
  "description": "Count caller-provided text under a Chio capability",
  "version": "0.1.0",
  "supportedInterfaces": [
    {
      "url": "http://127.0.0.1:8080/a2a",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "stateTransitionHistory": false
  },
  "defaultInputModes": [
    "text"
  ],
  "defaultOutputModes": [
    "text"
  ],
  "skills": [
    {
      "id": "count_words",
      "name": "count_words",
      "description": "Count whitespace-separated words and UTF-8 bytes",
      "tags": [],
      "inputModes": [
        "text"
      ],
      "outputModes": [
        "text"
      ],
      "bridgeFidelity": {
        "kind": "adapted",
        "caveats": [
          "stream-capable tools execute through `message/stream` deferred tasks; output is surfaced on follow-up `task/get` rather than incremental transport updates",
          "stream chunks are collated into the terminal task payload instead of pushed as incremental A2A events",
          "partial output is preserved only in the terminal task payload, not incremental updates",
          "cancellation is available only for deferred `message/stream` tasks; blocking `message/send` remains terminal"
        ]
      }
    }
  ]
}

The card’s HTTP URL is advertising metadata. The supplied client launches the host on stdio; deploying an HTTP A2A endpoint requires a transport and an appropriate endpoint URL. Discovery describes what the host exposes. It does not issue a capability to the caller.

Send structured arguments and read the result

The client supplies a data part containing the tool’s text argument and explicitly selects the skill through metadata.chio.targetSkillId. A text part alone is translated into a message argument; it would not supply this tool’s text field.

Captured message/send request · completejson
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "message/send",
  "params": {
    "message": {
      "role": "user",
      "parts": [
        {
          "type": "data",
          "data": {
            "text": "One governed tool, two protocol surfaces.\n\n"
          }
        }
      ]
    },
    "metadata": {
      "chio": {
        "targetSkillId": "count_words"
      }
    }
  }
}
Captured message/send response · selected metadatajson
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "id": "a2a-task-1",
    "message": {
      "parts": [
        {
          "data": {
            "bytes": 43,
            "words": 6
          },
          "type": "data"
        },
        {
          "text": "Document counted",
          "type": "text"
        }
      ],
      "role": "agent"
    },
    "metadata": {
      "chio": {
        "receiptId": "37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26",
        "decision": "allow",
        "authorityPath": "cross_protocol_orchestrator"
      }
    },
    "status": "completed"
  }
}

The result contains the count and a receipt ID. This projection omits the embedded receipt and detailed bridge metadata; download the complete response. Even this blocking call uses the tool’s stream implementation, whose two chunks are collated into the terminal message.

Follow the deferred task

A deferred task can execute or be cancelled before execution
A deferred task can execute or be cancelled before executionmessage/stream creates a working task without executing the tool. task/get runs the kernel decision and returns a completed or failed result with a signed receipt. task/cancel changes the working task to cancelled without a tool execution receipt.message/streamCreate a deferred requestWorking taskTask ID retained by this edgereceiptPending: truetask/gettask/cancelExecute through the kernelCheck the signed grantDispatch if allowedCancelledRemove pending executionNo tool execution receiptCompleted or failedResult + signed decisionThe edge retains task statefor this process. Tool chunksarrive in the terminal result.A deferred task can execute or be cancelled before executionmessage/stream creates a working task without executing the tool. task/get runs the kernel decision and returns a completed or failed result with a signed receipt. task/cancel changes the working task to cancelled without a tool execution receipt.message/streamCreate a deferred requestWorking taskTask ID retainedreceiptPending: truetask/gettask/cancelKernelCheck grantThen dispatchCanceledNo toolexecutionCompletedor failedResult +signed decisionNo toolreceiptTask state lives in this process.
A deferred task executes when the caller resolves it. Cancelling before execution returns task state without a tool execution receipt.
Source

Create now, execute on resolution

Run the deferred pathbash
python3 client.py a2a sample.md --deferred
Captured deferred A2A resulttext
Task a2a-task-1: working; execution pending
ALLOW 6 words, 43 UTF-8 bytes
Receipt: 622bd0bc98733b0ac5bd0af57aecc375b0317b4fc2dd996e024581af5fb2c546
Evidence: runs/dae5e03a-dc59-49ea-a7cf-5317faa62f2a

message/stream creates a working task. No tool has executed and no tool receipt exists at that point. The client keeps the returned task ID and uses it in task/get:

Captured working task · selected metadatajson
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "id": "a2a-task-1",
    "metadata": {
      "chio": {
        "receiptId": null,
        "decision": "pending",
        "receiptPending": true,
        "receiptBearing": false,
        "authorityPath": "cross_protocol_orchestrator"
      }
    },
    "status": "working",
    "statusMessage": "Task accepted for authoritative deferred execution."
  }
}
Captured task/get request · completejson
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "task/get",
  "params": {
    "taskId": "a2a-task-1"
  }
}

task/get runs the kernel decision and returns the terminal result with its receipt. Read the complete resolved response or the full request/response exchange. The task ID comes from the actual response, so the client does not assume a particular counter value.

Cancel before the tool runs

Create and cancel a taskbash
python3 client.py a2a sample.md --cancel
Captured A2A cancellationtext
Task a2a-task-1: working; execution pending
Task a2a-task-1: cancelled; no tool execution receipt
Evidence: runs/7deae84d-2e93-43ed-8d79-307a71b00f31
Cancelled task · selected metadatajson
{
  "id": 2,
  "jsonrpc": "2.0",
  "result": {
    "id": "a2a-task-1",
    "metadata": {
      "chio": {
        "receiptId": null,
        "decision": "cancelled",
        "receiptBearing": false,
        "authorityPath": "cross_protocol_orchestrator"
      }
    },
    "status": "cancelled",
    "statusMessage": "Task cancelled by caller."
  }
}

The cancellation response is task metadata, not a signed tool execution receipt. It does not consume an invocation in this project. Resolving that cancelled task does not run the tool. Read the complete cancellation response.

Change what the session permits

The host issues one signed grant for count_words. This application configuration sets its limit and lifetime:

authority.json · completejson
{
  "schema": "document-tools.authority.v1",
  "tool": "count_words",
  "max_invocations": 2,
  "lifetime_seconds": 1800
}
Exhaust one session’s allowance · exits 2bash
python3 client.py a2a sample.md --repeat 3
Captured A2A allowance exhaustiontext
ALLOW 6 words, 43 UTF-8 bytes
Receipt: 26813f4e9094993fff97f483df764d50a6c7d39eb11d4a2c4f3a672e32ddcaed
ALLOW 6 words, 43 UTF-8 bytes
Receipt: ad20488f54339f7163e6710ab34f46e2d226bb5dc9184f96d87ed42336a460d1
DENY invocation budget exhausted for capability cap-01a085f0-9bdd-73c2-a6bf-0f2150c4e2e2
Receipt: 60b1a0e8e90b1a5cffbf615fb26b82a3fc16c38c67ec009a69c7b8e1b97b1e82
Evidence: runs/19cb6711-4f3c-468d-8d42-fea5a4f1d357

Change max_invocations to 3 in authority.json, then run with --repeat 4. Three calls succeed and the fourth is denied; no rebuild is required. The host hashes these configuration bytes into the policy context and saves the exact configuration with the run.

This file configures capability issuance for this application; it is not HushSpec. Unknown fields are rejected. Repeats share one grant, while an explicit new program start creates a fresh session. A deployed operator must authenticate callers and control that issuance. The host implementation below shows the mechanism.

Inspect the complete issued capability and original execution-context example
Captured capability · completejson
{
  "schema": "chio.capability.v1",
  "id": "cap-01a085f0-9547-74a1-bde1-efec269b5f4d",
  "issuer": "e7919f3983edc12eef4ec27392ae19f1637a035602f7892c7b331aa2760fee6a",
  "subject": "716d0870f526f3bced3f59f180f9a0040558291412b6ebb7a60b2aaf4f6c2959",
  "scope": {
    "grants": [
      {
        "server_id": "documents",
        "tool_name": "count_words",
        "operations": [
          "invoke"
        ],
        "max_invocations": 2
      }
    ]
  },
  "issued_at": 1788953531,
  "expires_at": 1788955331,
  "signature": "460ec183e96bc836d24a888e6276985bfd67a7c6fb77955d8078f4d1f49199e7306b134d59090e47672bc4b95b3f14e32ee1a5df87b053a6458868916393930f"
}
examples/hello-a2a/src/lib.rs137-170rust
let capability = kernel
    .issue_capability(
        &agent.public_key(),
        ChioScope {
            grants: vec![ToolGrant {
                server_id: SERVER_ID.to_string(),
                tool_name: TOOL_NAME.to_string(),
                operations: vec![Operation::Invoke],
                constraints: vec![],
                max_invocations: None,
                max_cost_per_invocation: None,
                max_total_cost: None,
                dpop_required: None,
            }],
            ..ChioScope::default()
        },
        300,
    )
    .map_err(|error| -> Box<dyn Error + Send + Sync> {
        format!("issue capability: {error}").into()
    })?;

let execution = A2aKernelExecutionContext {
    capability,
    agent_id: agent.public_key().to_hex(),
    dpop_proof: None,
    execution_nonce: None,
    governed_intent: None,
    approval_token: None,
    approval_tokens: Vec::new(),
    threshold_approval_proposal: None,
    supplemental_authorization: None,
    model_metadata: None,
};

Keep the signed decision

This is the complete receipt from the first allowed count. Its action binds the supplied text; its metadata records the stream chunk hashes and bridge context. The surrounding JSON-RPC task envelope is not itself the signed receipt.

Captured A2A kernel receipt · completejson
{
  "id": "37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26",
  "timestamp": 1788953531,
  "capability_id": "cap-01a085f0-9547-74a1-bde1-efec269b5f4d",
  "tool_server": "documents",
  "tool_name": "count_words",
  "action": {
    "parameters": {
      "text": "One governed tool, two protocol surfaces.\n\n"
    },
    "parameter_hash": "8388b82fbbe71be998f4f0f277f05dc14446b16a52da29070bb44233d3824e73"
  },
  "decision": {
    "verdict": "allow"
  },
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "tool_origin": "caller_executed",
  "redaction_mode": "none",
  "content_hash": "5170e2c24e777080378742d62d0915795e8f6b8897b489613fc6ceb17c2ae212",
  "policy_hash": "56f393294872b8f8213479eff52ad3fb3e61581cc62a08410ddbbad21c4e745a",
  "metadata": {
    "admission_operation": {
      "compensation_status": "not_compensated",
      "coordinator_lease_epoch": 1,
      "coordinator_lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
      "operation_id": "4732ae8861ba703f1d190c2d4d873193f4242a702727f23f7cf7912e95e24272",
      "projected_dispatch_state": "terminal",
      "projected_operation_version": 8,
      "projected_state": "completed",
      "request_binding_hash": "7e82269b6a27a31d9b6253a48c10665c0b33fde826fec41c8fd54900f6923059",
      "request_id": "a2a-a2a-task-1",
      "request_namespace_digest": "75867a2970677197f6cbba2268fea29cb2d6bff896d9e36773434c1632001d53",
      "retained_dispatch_commit": {
        "committed_version": 6,
        "coordinator_lease_epoch": 1,
        "coordinator_lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
        "provider_attempt": {
          "attempt_id": "attempt:4732ae8861ba703f1d190c2d4d873193f4242a702727f23f7cf7912e95e24272",
          "operation_id": "4732ae8861ba703f1d190c2d4d873193f4242a702727f23f7cf7912e95e24272",
          "transport_id": "kernel-tool-server:documents",
          "transport_key_epoch": 1
        },
        "store_fence": {
          "lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
          "owner_epoch": 1,
          "store_uuid": "01a085f0-9450-78d2-87c6-01dc8b1adec7"
        }
      },
      "schema": "chio.admission-receipt.v1",
      "store_fence": {
        "lease_id": "01a085f0-94ed-7220-8034-549fdd67c544",
        "owner_epoch": 1,
        "store_uuid": "01a085f0-9450-78d2-87c6-01dc8b1adec7"
      },
      "tool_outcome_id": "7088d2985a9a9864ea29764e20b61b4e0284d1a0c160ba91bd1f8a97f01bc7e2",
      "tool_outcome_version": 2,
      "trusted_time_unix_ms": 1788953531927
    },
    "attribution": {
      "delegation_depth": 0,
      "grant_index": 0,
      "issuer_key": "e7919f3983edc12eef4ec27392ae19f1637a035602f7892c7b331aa2760fee6a",
      "subject_key": "716d0870f526f3bced3f59f180f9a0040558291412b6ebb7a60b2aaf4f6c2959"
    },
    "chio_receipt_signing_nonce": "rcpt-01a085f0-9659-76f0-ab18-426e795d1c2b",
    "receipt_context": {
      "request_id": "a2a-a2a-task-1"
    },
    "route_selection": {
      "candidates": [
        {
          "available": true,
          "routeId": "native-route",
          "selectedProtocols": [
            "a2a",
            "native"
          ],
          "targetProtocol": "native"
        }
      ],
      "decision": "select",
      "requestedTargetProtocol": "native",
      "routeSelectionId": "fe26d5b5a64ff93b186824e0cc61311ed384ac1dcfeeafb8d3d399b6fc33a0d0",
      "selectedProtocols": [
        "a2a",
        "native"
      ],
      "selectedRouteId": "native-route",
      "selectedTargetProtocol": "native",
      "sourceProtocol": "a2a"
    },
    "stream": {
      "chunk_hashes": [
        "d453126289fdef1956695f0dfd57d82e211d255969ed5ffcf0b54cd59a054f95",
        "b2538f4ac125b83068662cf8f4dc7966e50d1bcbfce089b5824edea204d91624"
      ],
      "chunks_expected": 2,
      "chunks_received": 2,
      "total_bytes": 77
    }
  },
  "trust_level": "mediated",
  "kernel_key": "e7919f3983edc12eef4ec27392ae19f1637a035602f7892c7b331aa2760fee6a",
  "signature": "2eaa28637470570231bfd7bf65e0263a16403ed846c39301ea02774b26f21aca89a6ff4d55a0b5ae8bfbcad1ab6fc2af9c5abd51921f5f0007c0c34466c03a09"
}

Download the allowed receipt and quota-denial receipt. A failed operation can still have a valid signature; signature validity and authorization are separate conclusions.

Find and verify evidence from your run

Replace your-run-id with the directory printed by your command. The first command reads denials from this isolated local store; the second verifies your first exported receipt against the key selected by your host:

Inspect your own 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 verification resultjson
{
  "authorized": true,
  "crypto_error": null,
  "crypto_floor": "allow_classical",
  "decision": {
    "verdict": "allow"
  },
  "ok": true,
  "parameter_hash_valid": true,
  "receipt_id": "37e2462a17beedccde4d27de471fc3dbb3c47f155489780b3707f3bf30222c26",
  "receipt_id_valid": true,
  "signature_valid": true,
  "signer_trusted": true
}

ok covers the expected signer, signature, receipt ID and parameter hash. authorized additionally requires an allow decision. For a remote host, obtain the expected key through trusted configuration. Offline receipt verification develops that trust boundary.

Build on the complete application

The client below contains the actual protocol calls, task-ID handling and result interpretation used above. It imports the bounded transport helper included in the same download.

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 stdio 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

Replace the document operation

The complete service validates text, counts it and emits two chunks. Replace this handler when building your own capability, then update its manifest and grant together. The host registers it before issuing authority.

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_task service for source comparison
examples/hello-a2a/src/lib.rs23-76rust
impl ToolServerConnection for HelloStreamServer {
    fn server_id(&self) -> &str {
        SERVER_ID
    }

    fn tool_names(&self) -> Vec<String> {
        vec![TOOL_NAME.to_string()]
    }

    async fn invoke(
        &self,
        tool_name: &str,
        arguments: Value,
        _nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
    ) -> Result<Value, KernelError> {
        if tool_name != TOOL_NAME {
            return Err(KernelError::ToolNotRegistered(tool_name.to_string()));
        }
        Ok(json!({"message": "hello from a2a", "arguments": arguments}))
    }

    async fn invoke_stream(
        &self,
        tool_name: &str,
        arguments: Value,
        _nested_flow_bridge: Option<&mut dyn NestedFlowBridge>,
    ) -> Result<Option<ToolServerStreamResult>, KernelError> {
        if tool_name != TOOL_NAME {
            return Err(KernelError::ToolNotRegistered(tool_name.to_string()));
        }
        let text = arguments
            .get("text")
            .and_then(Value::as_str)
            .unwrap_or("world");
        Ok(Some(ToolServerStreamResult::Complete(ToolCallStream {
            chunks: vec![
                ToolCallChunk {
                    data: json!({
                        "type": "text",
                        "text": format!("hello from a2a, {text}")
                    }),
                },
                ToolCallChunk {
                    data: json!({
                        "content": [{
                            "type": "text",
                            "text": "stream complete"
                        }]
                    }),
                },
            ],
        })))
    }
}

Describe the operation accurately

The manifest declares count_words as read-only and supplies its text schema. Streaming, partial-output and cancellation hints describe how the edge may present it; those descriptions do not expand the signed grant.

Original greeting manifest
examples/hello-a2a/src/lib.rs106-130rust
pub fn demo_manifest() -> ToolManifest {
    ToolManifest {
        schema: "chio.manifest.v1".to_string(),
        server_id: SERVER_ID.to_string(),
        name: "Hello A2A Server".to_string(),
        description: Some("A tiny receipt-bearing A2A hello surface".to_string()),
        version: "0.1.0".to_string(),
        tools: vec![ToolDefinition {
            name: TOOL_NAME.to_string(),
            description: "Return a collated greeting".to_string(),
            input_schema: json!({
                "type": "object",
                "x-chio-streaming": true,
                "x-chio-partial-output": true
            }),
            output_schema: None,
            pricing: None,
            has_side_effects: false,
            latency_hint: None,
        }],
        server_tools: Vec::new(),
        required_permissions: None,
        public_key: "hello-a2a-manifest".to_string(),
    }
}

Connect the edge to the kernel

The host supplies its resolved capability and agent identity to handle_jsonrpc. Requests without a response frame produce no output line. Frames are bounded, and the client stops on transport failure.

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

Original greeting serve loop
examples/hello-a2a/src/lib.rs198-222rust
pub fn serve_reader<R, W>(reader: R, mut writer: W) -> HelloA2aResult<()>
where
    R: BufRead,
    W: Write,
{
    let mut state = build_demo_state()?;

    for line in reader.lines() {
        let line = line?;
        if line.trim().is_empty() {
            continue;
        }
        let message: Value = serde_json::from_str(&line)?;
        let response = state
            .edge
            .handle_jsonrpc(message, &state.kernel, &state.execution);
        if let Some(response) = response.as_value() {
            serde_json::to_writer(&mut writer, response)?;
            writeln!(&mut writer)?;
            writer.flush()?;
        }
    }

    Ok(())
}

Understand the delivery boundary

SituationWhat to expect
Blocking call
A terminal task includes collated output and the signed decision.
Deferred task
The edge retains the request; task/get triggers execution. No incremental events are pushed.
Cancellation
A working task can be cancelled before execution. No tool receipt is created for that state change.
Process exit
Deferred task state and revocations are process-local. Saved receipts remain on disk.
Unknown skill
The edge rejects the request before tool execution. A protocol error alone is not a signed kernel denial.

Read the advertised caveats

This document tool is adapted. Its four captured caveats describe deferred execution, collation, terminal-only partial output and cancellation before execution. Fidelity describes translation behavior; it does not prove that a particular caller is authorized. When multiple manifests expose the same tool name, the A2A edge publishes qualified identifiers instead of silently merging ambiguous skills.

Read the public fidelity implementation
crates/protocol/chio-a2a-edge/src/bridge.rs77-134rust
fn evaluate_bridge_fidelity(
    tool: &ToolDefinition,
    target_protocol: DiscoveryProtocol,
) -> BridgeFidelity {
    let registry = authoritative_target_registry();
    let hints = semantic_hints_for_tool(tool);
    let lifecycle = runtime_lifecycle_contract(RuntimeLifecycleSurface::A2aAuthoritative);
    if !hints.publish {
        return BridgeFidelity::Unsupported {
            reason: "publication disabled by x-chio-publish=false".to_string(),
        };
    }
    if !registry.supports_target_protocol(target_protocol) {
        return BridgeFidelity::Unsupported {
            reason: format!(
                "A2A authoritative execution does not yet have a registered `{target_protocol}` target executor"
            ),
        };
    }
    if hints.approval_required {
        return BridgeFidelity::Unsupported {
            reason: "requires interactive approval semantics that the current A2A edge cannot truthfully project".to_string(),
        };
    }
    let mut caveats = Vec::new();
    if tool.has_side_effects {
        caveats.push(
            "A2A publication cannot project protocol-native permission prompts; callers must rely on Chio capability enforcement".to_string(),
        );
    }
    if hints.streams_output {
        caveats.push(format!(
            "stream-capable tools execute through `{}` deferred tasks; output is surfaced on follow-up `{}` rather than incremental transport updates",
            lifecycle.stream_entrypoint, lifecycle.follow_up_entrypoint
        ));
        caveats.push(
            "stream chunks are collated into the terminal task payload instead of pushed as incremental A2A events".to_string(),
        );
    }
    if hints.partial_output {
        caveats.push(
            "partial output is preserved only in the terminal task payload, not incremental updates"
                .to_string(),
        );
    }
    if hints.supports_cancellation {
        caveats.push(format!(
            "cancellation is available only for deferred `{}` tasks; blocking `{}` remains terminal",
            lifecycle.stream_entrypoint, lifecycle.blocking_entrypoint
        ));
    }

    if caveats.is_empty() {
        BridgeFidelity::Lossless
    } else {
        BridgeFidelity::Adapted { caveats }
    }
}

Reproduce the original greeting verifier

The public hello-a2a example remains useful for checking the edge implementation. Its fixed greeting and timestamped artifacts are a verifier workflow. The document-tools application above accepts your input and retains your own run.

Original smoke commands, captured outputs and assertions
hello-a2a · smoketranscript
$ cd examples/hello-a2a
$ ./smoke.sh
hello-a2a smoke passed
artifacts: <chio-source>/examples/hello-a2a/.artifacts/20260905T114341Z
send receipt: 31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45
stream receipt: 95bae6361e72230e60819e50fedf6c827a3e97b94b380c38f67ea2ff255651fe
exit 0
hello-a2a · agent-cardtranscript
$ cat agent-card.json
{
  "capabilities": {
    "pushNotifications": false,
    "stateTransitionHistory": false,
    "streaming": true
  },
  "defaultInputModes": [
    "text"
  ],
  "defaultOutputModes": [
    "text"
  ],
  "description": "Chio-governed tools exposed as A2A skills",
  "name": "Chio A2A Edge",
  "skills": [
    {
      "bridgeFidelity": {
        "caveats": [
          "stream-capable tools execute through `message/stream` deferred tasks; output is surfaced on follow-up `task/get` rather than incremental transport updates",
          "stream chunks are collated into the terminal task payload instead of pushed as incremental A2A events",
          "partial output is preserved only in the terminal task payload, not incremental updates"
        ],
        "kind": "adapted"
      },
      "description": "Return a collated greeting",
      "id": "hello_task",
      "inputModes": [
        "text"
      ],
      "name": "hello_task",
      "outputModes": [
        "text"
      ],
      "tags": []
    }
  ],
  "supportedInterfaces": [
    {
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0",
      "url": "http://localhost:8080"
    }
  ],
  "version": "0.1.0"
}
exit 0
hello-a2a · send-responsetranscript
$ jq '.result | {id, status, message,
$       chio: (.metadata.chio | {decision, receiptId, capabilityId, authorityPath,
$                                a2aSurface, streamProjection, terminalState})}' send-response.json
{
  "id": "a2a-task-1",
  "status": "completed",
  "message": {
    "parts": [
      {
        "data": {
          "text": "hello from a2a, world",
          "type": "text"
        },
        "type": "data"
      },
      {
        "text": "stream complete",
        "type": "text"
      }
    ],
    "role": "agent"
  },
  "chio": {
    "decision": "allow",
    "receiptId": "31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45",
    "capabilityId": "cap-01a07161-b8f8-72a3-871f-c07bf1afecff",
    "authorityPath": "cross_protocol_orchestrator",
    "a2aSurface": "authoritative_blocking_send",
    "streamProjection": "collated_final_message",
    "terminalState": {
      "state": "completed"
    }
  }
}
exit 0
hello-a2a · send-receipttranscript
$ jq '.result.metadata.chio.receipt | del(.metadata)' send-response.json
{
  "action": {
    "parameter_hash": "3a477a27451b71eaf6dc49c80b0e2e4c80f3fc5060884497c4246ea2b44d0790",
    "parameters": {
      "message": "world"
    }
  },
  "boundary_class": "prevent",
  "capability_id": "cap-01a07161-b8f8-72a3-871f-c07bf1afecff",
  "content_hash": "9f60be4661ed367601d088804c409177d3fea01b9f1ff79fcb2a1cdd4e12de42",
  "decision": {
    "verdict": "allow"
  },
  "id": "31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45",
  "kernel_key": "133de73c5809534ce22ac8d1c5b12f06b1e59dd875a0ced4592c612fc57874ca",
  "policy_hash": "hello-a2a-policy",
  "receipt_kind": "mediated_decision",
  "redaction_mode": "none",
  "signature": "df30178a057d1849b44fea8f750ceececee128c377ab78692d34423a08cec9a1ebcb6a9e8103faba21c37707e219202a322dbdda3a4b99141fe514c9906bbc00",
  "timestamp": 1788608624,
  "tool_name": "hello_task",
  "tool_origin": "caller_executed",
  "tool_server": "hello-a2a-srv",
  "trust_level": "mediated"
}
exit 0allow
hello-a2a · stream-createdtranscript
$ cat stream-created.json
{
  "id": 2,
  "jsonrpc": "2.0",
  "result": {
    "id": "a2a-task-2",
    "metadata": {
      "chio": {
        "authoritative": true,
        "authorityPath": "cross_protocol_orchestrator",
        "capabilityId": null,
        "claimEligible": true,
        "compatibilityOnly": false,
        "decision": "pending",
        "lifecycle": {
          "messageSend": "blocking_terminal_task",
          "messageStream": "deferred_task_poll",
          "taskCancel": "supported",
          "taskGet": "supported"
        },
        "receipt": null,
        "receiptBearing": false,
        "receiptId": null,
        "receiptPending": true,
        "runtimeLifecycle": {
          "blockingEntrypoint": "message/send",
          "cancelEntrypoint": "task/cancel",
          "claimEligible": true,
          "compatibilityOnly": false,
          "followUpEntrypoint": "task/get",
          "partialOutputDelivery": "collated_terminal_payload",
          "streamDelivery": "collated_terminal_payload",
          "streamEntrypoint": "message/stream",
          "surface": "a2a_authoritative"
        }
      }
    },
    "status": "working",
    "statusMessage": "Task accepted for authoritative deferred execution."
  }
}
exit 0
hello-a2a · task-gettranscript
$ jq '.result | {id, status, message,
$       chio: (.metadata.chio | {decision, receiptId, capabilityId})}' task-get-response.json
{
  "id": "a2a-task-2",
  "status": "completed",
  "message": {
    "parts": [
      {
        "data": {
          "text": "hello from a2a, world",
          "type": "text"
        },
        "type": "data"
      },
      {
        "text": "stream complete",
        "type": "text"
      }
    ],
    "role": "agent"
  },
  "chio": {
    "decision": "allow",
    "receiptId": "95bae6361e72230e60819e50fedf6c827a3e97b94b380c38f67ea2ff255651fe",
    "capabilityId": "cap-01a07161-b8f8-72a3-871f-c07bf1afecff"
  }
}
exit 0
hello-a2a · inspecttranscript
$ ARTIFACTS=.artifacts/latest
$ jq -r '.skills[0].bridgeFidelity.kind' "$ARTIFACTS/agent-card.json"
$ jq -r '.result.metadata.chio.receiptId' "$ARTIFACTS/send-response.json"
$ jq -r '.result.metadata.chio.receiptPending' "$ARTIFACTS/stream-created.json"
$ jq -r '.result.metadata.chio.receiptId' "$ARTIFACTS/task-get-response.json"
adapted
31c8189574c8a025f7c78952e1a52a9b61515a8dbf919cf1d904c52b31a0de45
true
95bae6361e72230e60819e50fedf6c827a3e97b94b380c38f67ea2ff255651fe
exit 0
examples/hello-a2a/smoke.sh19-27python
agent_card = json.loads(
    subprocess.check_output(
        [str(example_root / "run-edge.sh"), "agent-card"],
        cwd=example_root,
        text=True,
    )
)
(artifacts / "agent-card.json").write_text(json.dumps(agent_card, indent=2), encoding="utf-8")
assert agent_card["skills"][0]["id"] == "hello_task", agent_card
examples/hello-a2a/smoke.sh100-108python
assert send_response["result"]["status"] == "completed", send_response
assert send_response["result"]["metadata"]["chio"]["authorityPath"] == "cross_protocol_orchestrator", send_response
assert send_response["result"]["metadata"]["chio"]["receiptId"], send_response

assert stream_created["result"]["status"] == "working", stream_created
assert stream_created["result"]["metadata"]["chio"]["receiptPending"] is True, stream_created

assert task_resolved["result"]["status"] == "completed", task_resolved
assert task_resolved["result"]["metadata"]["chio"]["receiptId"], task_resolved

Choose the direction of the integration

Use this edge when you own the operation and want to expose an agent-facing skill. To call an existing remote A2A service from Chio, use the consumer-side A2A adapter. To govern an existing HTTP API, continue with OpenAPI Sidecar.

Keep the tool and change the protocol

Continue to Hello ACP with this same project and document. The Bridges reference defines the wider translation and fidelity contract; the provider integration project connects model-selected calls to an owned application.