Wrap an ACP Server
Embed the ACP proxy in your host so each file and terminal request an agent sends to the editor is checked before the editor acts.
On this page
In ACP the agent asks the editor to read files, write files, and start terminals. With chio-acp-proxy between them, each of those requests passes the proxy's guards, and a kernel capability check when you install one, before the editor acts.
The editor and the agent keep speaking ACP over stdio. You embed the proxy crate in the Rust binary that spawns the agent; tool-call events the agent reports become audit entries, and signed receipts once a kernel signer is installed. chio cert then summarizes a session into a signed compliance certificate.
The proxy checks requests on their way to the editor
ACP, the Agent Client Protocol, connects editors and IDE sidecars to coding agents over JSON-RPC 2.0 on stdio. It is message-based: the agent drives the session, calls back into the editor with methods like fs/read_text_file, fs/write_text_file, and terminal/create, and reports its tool calls in session/update notifications.
κcapabilityσscopegguardsBbudget
A guarded request reaches the editor
The proxy intercepts guarded methods on the agent-to-editor leg, because in ACP the agent asks the editor to read a file or start a terminal. The kernel checks the capability and FsGuard, which rejects relative paths and .. traversal and judges a symlink by its target, then the proxy forwards the request and the receipt is signed. Wrap an ACP Server.
Guarded methods are intercepted on the agent-to-editor leg. With a kernel-backed checker installed, the capability check runs first; then FsGuard rejects relative paths and .. traversal and judges a symlink by its target, and TerminalGuard checks the command. A refused request goes back to the agent as JSON-RPC error -32000, and the editor never receives it. A capability refusal is signed as its own admission record; a built-in guard refusal is recorded in the proxy's error and log.
On the return path the proxy observes each session/update carrying a tool-call event, hashes it, and appends an audit entry, which a kernel signer promotes into a signed receipt before the update reaches the editor with Chio metadata attached.
1. Add the proxy to your host
The proxy is a Rust library you drive from your own binary. Add it from the distribution repository.
cargo add chio-acp-proxy --git https://github.com/backbay-labs/chio Updating git repository `https://github.com/backbay-labs/chio`
Adding chio-acp-proxy (git) to dependenciesYou should see the crate added as a git dependency. Your host supplies the agent's executable and ACP launch arguments, the trusted public key for receipts, the canonical project root, and the editor's bidirectional I/O.
Before you begin
Use Rust 1.94 or newer. Any agent that implements the supported ACP methods over stdio JSON-RPC can sit behind the proxy, which spawns its command and pipes JSON-RPC through. Decide two allowlists: the filesystem path prefixes the agent may read or write, and the terminal commands it may launch through ACP's terminal interface.
2. Start the proxy with its allowlists
Build an AcpProxyConfig from the agent command and the public key that verifies receipts, then add path prefixes and commands. with_allowed_path_prefix and with_allowed_command are both repeatable.
use chio_acp_proxy::{AcpProxy, AcpProxyConfig, AcpProxyError};
pub fn start_proxy(
agent_command: &str,
agent_args: Vec<String>,
public_key: &str,
project_root: &str,
) -> Result<AcpProxy, AcpProxyError> {
// AcpProxyConfig::new(agent_command, public_key). All guard lists
// start empty (deny-all); each builder call widens one of them.
let config = AcpProxyConfig::new(agent_command, public_key)
.with_agent_args(agent_args)
.with_server_id("workspace")
// Filesystem prefixes the agent may read or write (FsGuard).
.with_allowed_path_prefix(project_root)
// Terminal commands the agent may launch (TerminalGuard).
.with_allowed_command("cargo")
.with_allowed_command("git");
// Standalone: built-in FsGuard + TerminalGuard only. No signer, no
// capability checker: audit entries are unsigned, and terminal
// lifecycle calls (terminal/kill, terminal/release) are denied outright.
AcpProxy::start(config)
}Route editor messages through process_client_message and agent messages through process_agent_message. Forward permitted messages in their original direction, send a Block response back to its sender, and retain the entries returned by ForwardWithReceipt. Your host owns the I/O loop and shutdown.
The guards take plain lists from the builder. HushSpec blocks such as path_allowlist and shell_commands belong to the MCP path; the ACP proxy has no policy-file ingestion and shares no policy loader with it.
3. Install a kernel signer and capability checker
Start the proxy with a ReceiptSigner and a CapabilityChecker to sign receipts and check capabilities live. The crate ships KernelReceiptSigner and KernelCapabilityChecker for this.
use chio_acp_proxy::{AcpAttestationMode, AcpProxy};
// signer: impl ReceiptSigner, checker: impl CapabilityChecker, both
// backed by your kernel (KernelReceiptSigner / KernelCapabilityChecker).
let proxy = AcpProxy::start_with_kernel(
config,
Some(Box::new(signer)), // promote audit entries into signed receipts
Some(Box::new(checker)), // gate terminal lifecycle + capability checks
AcpAttestationMode::Required, // signing failures fail closed
)?;The fourth argument sets the attestation mode. BestEffort, the default, logs a signing failure and lets the operation proceed; Required fails the call closed when its receipt cannot be signed. With a checker installed, terminal/kill and terminal/release require a parameter-bound Chio receipt naming the session and terminal.
4. Open one file inside the prefix and one outside it
Connect your editor and open a session the way you did without the proxy. The proxy is transparent on the wire, so a failure here is a spawn problem. Then ask the agent to open a file under the prefix you passed, and one directory above it.
- Inside the prefix. The read succeeds.
- Outside the prefix. The editor surfaces
access denied: fs read denied for path: .... Confirm this refusal at the editor and in the proxy log: a built-in guard refusal blocks the request before any tool-call event exists to sign.
You should see both outcomes as ordinary tool results and errors in the editor, with no client-side change.
5. Read the receipts for the session
List the receipts the proxy wrote, scoped to its server id. A local read names its boundary with exactly one of --tenant <id> or --admin-all.
chio --receipt-db ./receipts.sqlite receipt list \
--tool-server workspace --admin-allYou should see JSON Lines, one ChioReceipt per line, with ACP fields under metadata.acp: sessionId, toolCallId, enforcementMode, and the authorization linkage. Check enforcementMode on each line. cryptographically_enforced means a live capability check allowed the operation and the proxy bound that authorization to the event; audit_only means the proxy observed the event without one.
All audit_only where you expected enforcement means the capability context did not bind; the symptoms in the reference below name the causes. Pipe the stream to jq to fold it into the view you need.
The start call decides what a session can prove
The guards run in every mode. What changes is whether events are signed and whether terminal lifecycle calls can pass.
| Start | Checks | terminal/kill, release | Events | Signing failure |
|---|---|---|---|---|
AcpProxy::start | FsGuard, TerminalGuard | Denied, -32000 | Unsigned audit entries | No signer |
start_with_kernel, BestEffort | Guards and capability checks | Capability-checked | Signed receipts | Logged, call proceeds |
start_with_kernel, Required | Guards and capability checks | Capability-checked | Signed receipts | Call fails closed |
ACP proxy reference
Try FsGuard, terminal/kill, enforcementMode, acp-session, or cert generate.
Method handling
The proxy treats the wire as a symmetric JSON-RPC stream and routes each message by the AcpMethod discriminator it parses from the request.
| ACP method | Proxy action | Check |
|---|---|---|
initialize, authenticate | Forwarded unchanged | None |
session/new, session/load, session/list, session/, session/ | Forwarded unchanged | None |
session/prompt | Forwarded unchanged | None |
session/ | Interposed, agent to editor | Permission-kind mapping |
fs/, fs/ | Guarded | FsGuard |
terminal/create | Guarded | Capability, then TerminalGuard |
terminal/kill, terminal/ | Gated | Capability only |
terminal/output, terminal/ | Forwarded unchanged | None |
session/cancel | Forwarded, clears state | None |
session/update (notification) | Observed | Hashed and recorded |
- Handshake and sessions. The proxy keeps no session registry; the session id it writes onto an entry comes from the message that carried it. A prompt has no guard and no record of its own; what the agent does with it shows up later as tool-call events.
- Permission requests. The four ACP permission options map to Chio allow and deny decisions, and an unknown option maps to deny. The mapping is logged, not written to an audit entry, and the editor's own UI still makes the choice.
- Files. The path must be absolute, and
..traversal is rejected before anything else. The path is then canonicalized, so a symlink is judged on its target. Reads and writes check the same prefix set. - Terminals.
terminal/createis capability-checked first when a checker is installed, thenTerminalGuardrequires an allowed command and rejects shell metacharacters in arguments; acwdparameter also goes throughFsGuard.terminal/killandterminal/releaseroute throughintercept_terminal_lifecycle, which fails closed: with no checker both are denied with-32000, because no built-in guard covers process lifecycle. Output and wait calls on an allowed terminal pass unchecked. - Cancel.
session/cancel, in either direction, purges the session's pending capability contexts and every live context bound to it, so authorization from a canceled turn cannot be reused by the next one. - Updates.
ToolCallandToolCallUpdateevents are parsed, hashed, and emitted as audit entries or signed receipts.
Guard failures return on the original request id with server error code -32000. The message names the operation class and the value that failed: access denied: fs read denied for path: /etc/shadow, access denied: terminal denied: command not allowed: rm, access denied: terminal denied: suspicious argument: $(whoami). Traversal has its own shape, path traversal detected: {path}. The editor surfaces each as an ordinary tool error.
Guard allowlists
Both guards are built from plain lists on AcpProxyConfig, with no glob or regex grammar.
FsGuardtakes prefixes fromwith_allowed_path_prefixand checksfs/read_text_fileandfs/write_text_file. It fails closed: an empty path, a relative path, and an empty prefix set all deny, and a path containing..is rejected before any prefix is consulted. The surviving path is canonicalized through the filesystem, which stops a symlink out of the tree. Prefixes match on path boundaries, so/home/dev/projectnever matches/home/dev/project_evil.TerminalGuardtakes commands fromwith_allowed_commandand checksterminal/create. An empty command or an empty list denies, the command must match an entry exactly, and arguments carrying any of six shell metacharacters are rejected: a backtick, the sequence$(,|,;, a newline, and a carriage return. The proxy launches throughexecve, so the check guards a command that later reaches a shell.
let config = AcpProxyConfig::new(agent_command, public_key)
.with_agent_args(agent_args)
.with_server_id("workspace")
// FsGuard prefix set. One list covers both reads and writes, so scope
// it to what the agent may WRITE, not only what it may read.
.with_allowed_path_prefix("/home/dev/project")
// TerminalGuard allowlist. Build, test, and VCS only: everything
// else denies at terminal/create.
.with_allowed_command("cargo")
.with_allowed_command("git")
.with_allowed_command("rg");Reads and writes share one prefix set, so a wide prefix grants broad write access; keep it as narrow as the workflow allows. Keep the command list to the minimum the agent needs; the allowlist defines the command boundary. To govern terminal/kill and terminal/release, install a CapabilityChecker through start_with_kernel.
Server id and session capability ids
with_server_id, default chio-acp-proxy, writes a stable identity string into audit entries and receipts, which compliance queries use to join ACP evidence with MCP and A2A receipts in one query. Keep it stable across restarts; changing it means updating scoped capabilities and receipt queries, and needs no key rotation.
A receipt's capability id is the id of the capability that authorized the operation. When none did, the signer synthesizes acp-session:<session_id> from the ACP session, the prefix chio cert generate matches on.
Audit entry fields
A session/update carrying a ToolCall event produces an audit entry, as does a ToolCallUpdate that carries a status; a status-less update produces nothing. Each entry arrives with a SHA-256 content hash and the session, tool-call, and server identifiers filled in.
pub struct AcpToolCallAuditEntry {
pub tool_call_id: String,
pub title: String,
pub kind: Option<String>,
pub status: String,
pub session_id: String,
/// Seconds since the Unix epoch (UTC).
pub timestamp: String,
pub server_id: String,
/// SHA-256 hex digest of the canonical JSON representation of
/// the originating tool-call event.
pub content_hash: String,
/// The capability ID that authorized the live operation, when known.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capability_id: Option<String>,
/// The authoritative Chio receipt emitted during the live authorization check.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_receipt_id: Option<String>,
/// Kernel request id that produced `authorization_receipt_id`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_request_id: Option<String>,
/// ACP tool-call id signed into the live authorization receipt.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_tool_call_id: Option<String>,
/// Correlation id signed into the live authorization receipt.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_correlation_id: Option<String>,
/// ACP operation signed into the live authorization receipt.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_operation: Option<String>,
/// ACP resource signed into the live authorization receipt.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_resource: Option<String>,
/// Canonical SHA-256 hash of the full authorized ACP parameters.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_parameter_hash: Option<String>,
/// Whether the event was tied to live cryptographic enforcement.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enforcement_mode: Option<AcpEnforcementMode>,
}- Serialization is camelCase, so
tool_call_idgoes on the wire astoolCallId. timestampis aStringholding seconds since the Unix epoch in decimal digits:"timestamp": "1744993921".contentHashis the SHA-256 of the canonical JSON of the event's declared ACP fields:toolCallId,title,kind, andstatusfor aToolCall, andtoolCallIdplusstatusfor an update. Extra keys the agent sends are excluded, so an unrecognized field cannot move the digest.- The eight
authorization*fields are set together when a live capability check authorized the operation, so an entry that shows one shows all eight. enforcementModehas two values,cryptographically_enforcedandaudit_only. Filesystem and terminal requests carry no ACP tool-call id, so an enforced context is buffered per session and bound later by matching the update event'skind. If zero or several pending contexts match, the binding is refused and the entry recordsaudit_only.
What the signed receipt carries
With a signer installed, the entry becomes a ChioReceipt signed with the kernel's Ed25519 key. The enforcement mode alone decides two of its fields:
let (decision, trust_level, semantics) = match enforcement_mode {
AcpEnforcementMode::AuditOnly => (
None,
chio_core::receipt::kinds::TrustLevel::Verified,
chio_core::receipt::metadata::ReceiptSemanticFields::trace_detect_only(),
),
AcpEnforcementMode::CryptographicallyEnforced => (
Some(Decision::Allow),
chio_core::receipt::kinds::TrustLevel::Mediated,
chio_core::receipt::metadata::ReceiptSemanticFields::mediated_prevent(),
),
};An audit_only entry signs as a trace observation with no decision and trust_level: verified; a cryptographically_enforced one signs as a mediated decision carrying allow and trust_level: mediated. The event signer constructs no deny decision: a request a built-in guard rejects is never forwarded, so it returns a JSON-RPC error and a log entry. A kernel-backed capability check signs its own admission refusal before this signer runs; inspect that authorization receipt and the proxy log beside the session-update receipts. The rest of the body:
let body = ChioReceiptBody {
// `ChioReceipt::sign` replaces this with the canonical
// content-addressed receipt id (`chio_receipt_id`), so the literal
// value here is only a pre-signing id for debug logging. The actual
// per-event uniqueness flows through `action.parameters` (status,
// title, kind, content_hash) and through `content_hash`, both of
// which the canonical id input pulls in. Encode the discriminator
// into the pre-signing id so tracing logs and any code path that
// inspects the pre-signing body can tell `running` and
// terminal `tool_call_update` events apart, and so a future change
// that bypasses content-addressing does not silently collide on
// `acp-{tool_call_id}`.
id: format!(
"acp-{}-{}-{}",
entry.tool_call_id, entry.status, entry.content_hash
),
timestamp,
capability_id: entry
.capability_id
.clone()
.unwrap_or_else(|| format!("acp-session:{}", entry.session_id)),
tool_server: request.tool_server.clone(),
tool_name: request.tool_name.clone(),
action,
decision,
receipt_kind: semantics.receipt_kind,
boundary_class: semantics.boundary_class,
observation_outcome: semantics.observation_outcome,
tool_origin: semantics.tool_origin,
redaction_mode: semantics.redaction_mode,
actor_chain: semantics.actor_chain.clone(),
content_hash: entry.content_hash.clone(),
policy_hash: String::new(),
evidence: Vec::new(),
metadata: Some(serde_json::json!({
"acp": {
"sessionId": entry.session_id,
"toolCallId": entry.tool_call_id,
"capabilityId": entry.capability_id,
"authorizationReceiptId": entry.authorization_receipt_id,
"authorizationRequestId": entry.authorization_request_id,
"authorizationToolCallId": entry.authorization_tool_call_id,
"authorizationCorrelationId": entry.authorization_correlation_id,
"authorizationOperation": entry.authorization_operation,
"authorizationResource": entry.authorization_resource,
"authorizationParameterHash": entry.authorization_parameter_hash,
"enforcementMode": enforcement_mode,
},
// Mirror the live authorization request id at the canonical
// `receipt_context.request_id` path so downstream verifiers can
// reconstruct the request-receipt linkage uniformly across the
// authorization receipt (written by the kernel) and the
// consumer receipt (written here). When the entry was promoted
// through a cryptographically enforced check, this matches the
// `metadata.receipt_context.request_id` already on the
// referenced authorization receipt.
"receipt_context": {
"request_id": entry.authorization_request_id,
"session_id": entry.session_id,
"tool_call_id": entry.tool_call_id,
"authorization_receipt_id": entry.authorization_receipt_id,
"authorization_correlation_id": entry.authorization_correlation_id,
"authorization_operation": entry.authorization_operation,
"authorization_resource": entry.authorization_resource,
"authorization_parameter_hash": entry.authorization_parameter_hash,
}
})),
trust_level,
tenant_id: authorization_context
.as_ref()
.and_then(|context| context.tenant_id.clone()),
kernel_key: self.keypair.public_key(),
bbs_projection_version: None,
};policy_hashandevidenceare empty. A kernel-backed checker runs the configured kernel pipeline and records its result in the linked authorization receipt, which holds the policy and guard evidence.action.parametersis the event. It holdstool_call_id,title,kind,status, andauthorization_parameter_hash, with no top-levelpathorcommandkey. On the enforced path the original ACP parameters appear one level down, asoperation_payload.acp-session:<session_id>is the fallback. It is used only when the entry carries no capability id, theaudit_onlycase. An enforced receipt carries the presented capability token's own id, and the two never appear together.- The metadata has two objects.
metadata.acpis camelCase with eleven keys.metadata.receipt_contextis snake_case and repeats the linkage fields under the canonical path a kernel-written receipt uses, so a verifier joins an ACP receipt to its authorization receipt without knowing which side wrote which.
Compliance certificates
A compliance certificate is the signed summary over one session. chio cert generate reads the receipts whose capability id starts with acp-session:<session_id> and emits a signed ComplianceCertificate. That prefix appears only on audit_only receipts, so a certificate summarizes what the proxy observed; enforced receipts carry their token's capability id and fall outside the prefix query.
Each selected receipt must repeat the session id at metadata.acp.sessionId, and the selected rows must be consecutive in the store. A database interleaving ACP receipts with MCP or A2A ones refuses on chain discontinuity, so give an ACP session its own receipt database.
# Generate a signed certificate over one session's receipts.
chio cert generate --session-id sess_01HZ7Q8YB3N8G0XHB0T6KQ4F9R \
--receipt-db ./receipts.sqlite \
--budget-limit 600 \
--output ./session-cert.json
# Verify it against the trusted kernel key. Add --full --receipt-db to
# re-verify every underlying receipt signature, not only the certificate's.
chio cert verify --certificate ./session-cert.json \
--trusted-kernel-pubkey ./kernel.pub
# Inspect the body without verifying.
chio cert inspect --certificate ./session-cert.jsongenerate requires --session-id and --receipt-db; with --output and no --json it writes the file and prints nothing. Generation is fail-fast: it produces a certificate or an error, never a certificate recording a failure. A session with no receipts says so:
$ chio cert generate \
--session-id demo-session \
--receipt-db ./receipts.db \
--authority-seed-file ./authority.seederror[CHIO-CLI-SESSION-EMPTY]: no receipts found for session demo-session help: pass the --session-id of a session with receipts in the store code: urn:chio:error:cli:session-empty docs: https://chio.computer/docs/reference/errors#error-cli-session-empty
chio cert inspect prints the body without checking a signature: thirteen lines in this order, plus an Anomalies: block between Guards: and Signer key: when there are any.
$ chio cert inspect --certificate ./cert.jsonSession ID: demo-session Schema: chio.compliance.certificate.v1 Issued at: 1756944000 Receipt count: 3 First receipt: 1756943000 Last receipt: 1756944000 Signatures: valid Chain: continuous Scope: compliant Budget: compliant Guards: compliant Signer key: 31debe55d37c722768b137131caa6087080b2e0b60b94bd785d14575cfa498bc Kernel key: 31debe55d37c722768b137131caa6087080b2e0b60b94bd785d14575cfa498bc
Five lines are per-dimension verdicts, each with a failing spelling: INVALID, BROKEN, VIOLATED, EXCEEDED, BYPASSED. A certificate this CLI generates prints the five compliant spellings and an empty anomaly list; the failing spellings appear on a certificate from elsewhere, or one that was edited. The body also reports the receipt count, the session's first and last receipt in store order, and the signer and kernel keys, and it is signed with the kernel's Ed25519 key, so a downstream consumer attests to a whole session without replaying the raw receipts.
chio cert verify requires --certificate and --trusted-kernel-pubkey, prints one line, and sets the exit code. Against a body whose signature fails:
$ chio cert verify \
--certificate ./cert.json \
--trusted-kernel-pubkey ./kernel.pubFAIL: verification failed: certificate signature invalid
--full re-verifies every underlying receipt signature, which needs the receipt store, and refuses without it:
$ chio cert verify \
--certificate ./cert.json \
--trusted-kernel-pubkey ./kernel.pub \
--fullerror[CHIO-CLI-RECEIPT-STORE-REQUIRED]: full-bundle verification re-verifies every receipt signature and needs the receipt store help: pass --receipt-db <path>, or drop --full to check the certificate signature alone code: urn:chio:error:cli:receipt-store-required docs: https://chio.computer/docs/reference/errors#error-cli-receipt-store-required
Differences from the MCP adapter
| Dimension | MCP adapter | ACP proxy |
|---|---|---|
Shape | Request and response tool calls | Bidirectional JSON-RPC message stream |
Interception point | tools/call request | Typed AcpMethod, plus session/update |
Tool discovery | Proxied tools/list | Session-time advertisement, usually implicit |
Checks | Compiled HushSpec guard pipeline | FsGuard, TerminalGuard, capability checks |
Receipt trigger | Kernel- | Observed tool-call events |
Receipt capability id | The presented token's id | The token's id, or acp-session:<id> |
Without a kernel | Refuses to start without its databases | Guards plus unsigned audit entries |
Transport | stdio and Streamable HTTP | stdio, line-based JSON-RPC |
The MCP guard pipeline covers forbidden-path, shell-command, egress-allowlist, path-allowlist, mcp-tool, secret-leak, and patch-integrity; the ACP proxy adds capability-checked terminal lifecycle and permission-kind mapping. On both, a refusal before the kernel lacks a kernel receipt: MCP edge-selection refusals and ACP built-in guard refusals live in errors and logs. Choose the evidence stream for the boundary you are reviewing.
Symptoms and fixes
access denied: fs read deniedon every read. No prefix was supplied, or the project root is a symlink and the guard matched its canonical target. Add the canonical absolute path withwith_allowed_path_prefix, resolving symlinks yourself first.- A path traversal error in the log. The agent requested a path with
..segments. The guard is working; a legitimate request normalizes the path before sending. terminal/createdenied for a command that should work. The command is off theTerminalGuardlist, or the agent invoked it through a shell wrapper. Add it withwith_allowed_command, or have the agent call the binary directly.- Entries read
enforcementMode: audit_only. NoCapabilityCheckeris installed, or the session has no bound capability token. Start withstart_with_kerneland a checker so the operation is capability-checked before it is forwarded. - Audit entries are unsigned. The proxy runs through
AcpProxy::startwith noReceiptSigner. Usestart_with_kernelwith a signer, andAcpAttestationMode::Requiredto fail closed when signing fails. access denied: terminal denied: suspicious argumenton an argument that looks fine. The argument carries one of the six rejected characters; a carriage return from a Windows-authored file is the easy one to miss. Strip it; the guard has no escape syntax.terminal/killandterminal/releasedenied with-32000. NoCapabilityCheckeris installed. Install one throughstart_with_kernel; no allowlist opens these.- A built-in guard refusal has no session-update receipt. The request was blocked before forwarding, so the event signer had no tool-call event to sign. Retain the proxy error and log, and inspect any kernel admission receipt from the checker.
cert generatereports an empty session that produced receipts. Those receipts carry a capability id in place of theacp-session:fallback. Read them withchio receipt list --tool-serverand join onmetadata.acp.sessionId.- The upstream agent exits immediately. The
--boundary was misparsed, or the agent expects arguments the proxy did not forward. Find thespawning ACP agentlog line and run the same command outsidechio.
The mirror direction: chio-acp-edge
The sibling crate chio-acp-edge runs the other way: Chio exposes its own tools as an ACP server. The Hello ACP example's run-edge.sh and smoke.sh exercise session/list_capabilities, tool/invoke, deferred tool/stream, and tool/resume.