Govern Any Provider
Send Anthropic and Bedrock tool calls through the Chio kernel and return provider-native results under each call's original ID.
On this page
A multi-provider agent keeps one execution authority. Anthropic tool_use blocks and Bedrock toolUse blocks reach the same kernel as OpenAI function calls, and the kernel admits each one before its registered tool runs.
Each provider adapter lifts the native call into one shared ToolInvocation, and your host supplies the capability and target server. After the kernel decides, the adapter lowers the result into the provider's own reply format under the originating call ID.
Three formats lift into one invocation the kernel admits
A ToolInvocation names the provider, the tool, the canonical-JSON argument bytes, and a ProvenanceStamp that binds the call to its upstream request ID and principal. The provenance describes where the call came from; authorization still comes from the capability your host resolves.
κcapabilityσscopegguardsBbudget
Lift, admit, lower, with the same call ID
The adapter lifts the native call into a ToolInvocation that names the provider, the tool, the canonical-JSON argument bytes, and a provenance stamp. The kernel admits it and dispatches the registered tool, and the adapter lowers the output into a native reply with the same call ID. Govern Any Provider.
An allowed call runs the registered tool once, and the adapter lowers its output into a native reply with the same call ID. A refused call comes back as the provider's error form, and the tool never runs. A streamed call waits at the block boundary until its verdict arrives.
1. Add the adapters to an embedded host
Start from the embedded host project, which creates a kernel, registers an application-owned tool, issues a bounded capability, and saves receipts. Then add the provider crates your host needs.
[dependencies]
chio-anthropic-tools-adapter = { git = "https://github.com/backbay-labs/chio" }
chio-bedrock-converse-adapter = { git = "https://github.com/backbay-labs/chio" }
chio-tool-call-fabric = { git = "https://github.com/backbay-labs/chio" }
chio-kernel = { git = "https://github.com/backbay-labs/chio" }
chio-core = { package = "chio-core-types", git = "https://github.com/backbay-labs/chio" }
serde_json = "1"
anyhow = "1"
aws-config = "1" # Bedrock host setup
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Run cargo build. You should see it resolve the adapter, fabric, and kernel crates from https://github.com/backbay-labs/chio; keep only the providers you use. Live requests need provider credentials. The fixtures and replay checks in step 6 run offline, and a long-lived service needs durable state configured on its kernel.
2. Execute each invocation once and keep its output
The kernel's provider_verdict module turns a ToolInvocation into a kernel request. This helper validates the invocation, evaluates it, runs the registered tool on allow, and returns the verdict with the executed output for lowering.
use chio_core::{capability::token::CapabilityToken, canonical::canonical_json_bytes};
use chio_kernel::{AgentId, ChioKernel, ServerId, ToolCallOutput};
use chio_kernel::provider_verdict::{build_tool_call_request, verdict_result_from_response};
use chio_tool_call_fabric::{ToolInvocation, ToolResult, VerdictResult};
pub async fn execute_invocation(
kernel: &ChioKernel,
invocation: &ToolInvocation,
capability: CapabilityToken,
agent_id: AgentId,
server_id: ServerId,
) -> anyhow::Result<(VerdictResult, ToolResult)> {
invocation.validate()?;
let request = build_tool_call_request(invocation, capability, agent_id, server_id)?;
let response = kernel.evaluate_tool_call(&request).await?;
let verdict = verdict_result_from_response(invocation, &response);
let output = match (&verdict, response.output) {
(VerdictResult::Allow { .. }, Some(ToolCallOutput::Value(value))) => value,
(VerdictResult::Allow { .. }, Some(ToolCallOutput::Stream(_))) => {
anyhow::bail!("handle stream output in the host; this invocation already executed")
}
_ => serde_json::Value::Null,
};
Ok((verdict, ToolResult(canonical_json_bytes(&output)?)))
}You should see one evaluation per invocation; the adapter makes no second backend call. The host supplies the complete capability, agent, and target server, and the adapter's principal label replaces none of them. The basic builder carries the provider call ID, tool name, and argument JSON, and leaves the full provenance stamp and the extra authorization fields out: for a grant that requires DPoP, an execution nonce, an approval, or model metadata, set those fields on the built request before evaluation.
The fabric verdict is Allow or Deny. verdict_result_from_response lowers a pending approval, and an allow that has not reached the completed terminal state, to Deny, and it emits an empty redaction list. The deny reason's rule_id reads kernel.pending_approval or kernel.execution_nonce_preflight for those two cases; the receipt holds the full decision. verdict_for_provider_invocation evaluates the same request but returns only the verdict, so calling the tool again after it would repeat execution.
3. Mediate an Anthropic Messages turn
chio-anthropic-tools-adapter owns the transport to api.anthropic.com. send_messages forwards a native messages.create body to /v1/messages with the x-api-key and pinned anthropic-version: 2023-06-01 headers, then lifts every tool_use block in the response.
use std::sync::Arc;
use chio_anthropic_tools_adapter::{
anthropic_transport_from_env, AnthropicAdapter, AnthropicAdapterConfig,
};
use chio_tool_call_fabric::{ToolResult, VerdictResult};
// ::new pins api_version to ANTHROPIC_VERSION ("2023-06-01"). workspace_id
// populates Principal::AnthropicWorkspace on every emitted ProvenanceStamp.
let config = AnthropicAdapterConfig::new(
"anthropic-front", // server_id
"Anthropic Messages", // server_name
"1.0.0", // server_version
std::env::var("CHIO_SERVER_PUBLIC_KEY")?, // hex Ed25519 public key
"wks_prod", // Anthropic workspace id
);
// Reads x-api-key from ANTHROPIC_API_KEY.
let transport = anthropic_transport_from_env()?;
let adapter = AnthropicAdapter::new(config, Arc::new(transport));
// Forward the messages.create body; lift every tool_use block in the reply.
let invocations = adapter.send_messages(&request_body).await?;
for invocation in &invocations {
invocation.validate()?; // fail closed before the kernel
let (verdict, output) = execute_invocation(
&kernel, invocation, capability.clone(), agent_id.clone(), server_id.clone(),
).await?;
// Lower the verdict + executed output into a tool_result content block
// for the next user turn. The request_id is the tool_use id (toolu_...).
let block = adapter.lower_tool_result_block(
&invocation.provenance.request_id,
verdict,
output,
)?;
// block.is_error is true on deny; its content carries the DenyReason.
}You should see one tool_result block per tool_use ID. On deny the block has is_error: true and its content describes the DenyReason, so the model receives the refusal. An input refusal prevents dispatch; a post-invocation refusal can follow tool execution. To drive the batch path yourself, lift_batch takes a ProviderRequest holding a response payload and returns the same Vec<ToolInvocation>.
The native computer_use, bash, and text_editor tools pass two gates: the computer-use cargo feature and a server_tools entry in the manifest. AnthropicAdapter::new starts with a deny-all gate, so build with new_with_manifest when you use them; the native tool gate entry lists the names.
4. Mediate a Bedrock Converse turn
chio-bedrock-converse-adapter drives the Bedrock Runtime Converse operation through the AWS SDK for Rust, SigV4-signed, and lifts every toolUse block. BEDROCK_REGION is us-east-1 and BEDROCK_CONVERSE_API_VERSION is bedrock.converse.v1; BedrockAdapterConfig::validate and BedrockAdapter::new reject any other pair, and the transport re-checks the region on every call.
use std::sync::Arc;
use aws_config::BehaviorVersion;
use chio_bedrock_converse_adapter::{
AwsSdkTransport, BedrockAdapter, BedrockAdapterConfig, ConverseRequest,
DEFAULT_IAM_PRINCIPALS_CONFIG_PATH,
};
use chio_tool_call_fabric::{ToolResult, VerdictResult};
// ::new pins region to us-east-1 and api_version to bedrock.converse.v1.
let config = BedrockAdapterConfig::new(
"bedrock-front",
"Bedrock Converse",
"1.0.0",
std::env::var("CHIO_SERVER_PUBLIC_KEY")?,
"arn:aws:iam::123456789012:role/ChioAgentRole", // overwritten on the signed path
"123456789012",
);
let sdk_config = aws_config::defaults(BehaviorVersion::latest())
.region("us-east-1")
.load()
.await;
let transport = AwsSdkTransport::from_sdk_config(&sdk_config)?;
// Production init resolves the IAM caller from a signed map before any
// tool traffic can be lifted (see below).
let adapter = BedrockAdapter::new_with_signed_iam_principals_config_from_sts(
config,
Arc::new(transport),
&sts_provider,
DEFAULT_IAM_PRINCIPALS_CONFIG_PATH,
&verifier,
&expected_identity,
).await?;
// model_id is a tool-capable model enabled for your AWS account.
// Drive one Converse turn and lift every toolUse block.
let request = ConverseRequest::new(model_id, messages);
let invocations = adapter.converse(request).await?;
for invocation in &invocations {
let (verdict, output) = execute_invocation(
&kernel, invocation, capability.clone(), agent_id.clone(), server_id.clone(),
).await?;
// response.0 is Bedrock JSON: { "toolResult": { toolUseId, content, status } }
let response = adapter.lower_tool_result(
&invocation.provenance.request_id, // toolUseId
verdict,
output,
)?;
}You should see one toolResult per toolUseId. On allow, lower_tool_result applies JSON Pointer redactions and wraps the output as Bedrock content; on deny it emits status: "error" carrying a chio denial payload with the verdict, receipt ID, and reason. When the response carries a toolConfig, lift_batch refuses any lifted tool name the config does not declare, as Malformed.
Bedrock has no per-request principal header, so the adapter resolves the calling IAM identity once per process from an operator-signed map, before any tool traffic is lifted. The IAM caller entry gives the file format and its fail-closed conditions.
5. Gate streamed tool-call blocks
Both adapters drive the streaming state machine in chio-tool-call-fabric. It buffers frames from content_block_start through content_block_stop, or contentBlockStart through contentBlockStop on Bedrock, evaluates the completed block, then flushes it on allow or drops it on deny. StreamPhase walks Idle, Buffering, Emitting, Closed.
// Anthropic SSE: buffer each tool_use block through content_block_stop,
// evaluate it, and release its bytes only if the verdict allows.
let gated = adapter.gate_sse_stream(&sse_bytes, |invocation| {
evaluate_block(invocation) // host: Result<VerdictResult, ProviderError>
})?;
// gated.bytes: SSE frames that are safe to forward downstream.
// gated.invocations: the tool_use blocks evaluated, in stream order.
// gated.verdicts: the verdict returned for each, in the same order.
// Bedrock is the mirror image: gate_converse_stream buffers each block
// from contentBlockStart through contentBlockStop and releases it on
// allow, returning a GatedConverseStream. That struct adds a fourth
// field, "events", which GatedSseStream does not have.You should see only allowed blocks in gated.bytes. The callback is synchronous and yours: define whether it authorizes forwarding or executes the tool, enforce its deadline, and keep the downstream consumer from executing a completed invocation a second time.
A block is capped at DEFAULT_MAX_BUFFERED_BLOCK_BYTES (1 MiB) and DEFAULT_MAX_BUFFERED_RAW_FRAMES (4096 frames), and an overflow fails the stream. The adapters leave timing to your evaluator: they preserve the ProviderError::VerdictBudgetExceeded it returns, with observed_ms and budget_ms, and fail the stream closed, so a slow verdict never becomes an allow. The streaming path refuses an allow that carries redactions; apply redactions on the non-streaming path.
6. Verify the adapters, then one refused call
The first three checks run without provider credentials. A live refused call after the list ends in a signed receipt.
- Construction holds your pinned values.
BedrockAdapter::newreturns aResultand rejects any region or API version other than the pinned pair, so a misconfigured deployment fails at boot. The Anthropic config pins its API version the same way. - A lifted invocation validates. Call
invocation.validate()before the kernel, every time, replay paths included. Assert on a mismatched principal variant and on non-canonical argument bytes. - A capture replays. Run
chio replay traffic --from <capture> --jsonover a recorded stream and assertokandpasses; add--againstto compare verdicts. - A recognized native tool is refused without its manifest entry. Construct with
new, then confirm each recognized native-tool call fails closed even with thecomputer-usefeature compiled.
$ chio replay traffic \
--from ./capture.ndjson --json{"first_error":null,"from":"<chio-source>/crates/products/chio-cli/tests/replay_traffic/fixtures/clean_match.ndjson","ok":true,"passes":1,"schema":"chio-tee-frame.v1","total":1}You should see "ok":true with every frame passing. Then send one live call outside the grant: the Anthropic reply carries is_error: true, and the Bedrock reply's chio payload names the receipt ID of the signed refusal.
Each adapter pins its provider's API and identity
Three crates implement the ProviderAdapter trait. Each pins a provider API version and stamps a principal from that provider's own identity boundary.
| Adapter | Native call | Principal | Pinned API |
|---|---|---|---|
chio-anthropic-tools-adapter | tool_use | AnthropicWorkspace | 2023-06-01 |
chio-bedrock-converse-adapter | toolUse | BedrockIam | bedrock.converse.v1 |
chio-openai-adapter | function_call | OpenAiOrg | responses.2026-04-25 |
Gemini, Groq, Mistral, Cohere, and Ollama use the same ToolInvocation, ProviderError, and VerdictResult vocabulary in their own lift and lower functions without implementing the trait. All eight appear in ProviderId, listed in the reference below.
Tool-call fabric reference
Try ProviderId, NonCanonicalArguments, iam_principals, RateLimited, or sign_provenance.
Provider identities
chio-tool-call-fabric defines the shape a native adapter's lift produces, the verdict shape its lower consumes, and the streaming state machine. It holds no provider-specific logic and performs no I/O. Each ProviderId carries a Principal variant scoped to that provider's native identity boundary.
| ProviderId | Principal variant | Identity scope |
|---|---|---|
open_ai | OpenAiOrg | org_id |
anthropic | Anthropic | workspace_id |
bedrock | BedrockIam | caller_arn, account_id, optional assumed_ |
gemini | GeminiProject | project_id |
groq | GroqProject | project_id |
mistral | MistralProject | project_id |
cohere | CohereOrg | org_id |
ollama | OllamaHost | host (local daemon, no upstream identity provider) |
Lift and lower contract
A ToolInvocation is what each adapter's lift emits.
pub struct ToolInvocation {
pub provider: ProviderId,
pub tool_name: String,
/// Canonical-JSON bytes (RFC 8785). Stored as raw bytes so the kernel can
/// hash without re-serializing.
pub arguments: Vec<u8>,
pub provenance: ProvenanceStamp,
}pub struct ProvenanceStamp {
pub provider: ProviderId,
pub request_id: String,
pub api_version: String,
pub principal: Principal,
pub received_at: SystemTime,
}arguments is a byte vector, so a serialized ToolInvocation shows it as an array of integers. This is the Anthropic single-tool fixture the crate pins its wire contract against; its seventeen bytes decode to {"ticker":"ANTH"}.
$ jq . crates/protocol/chio-tool-call-fabric/fixtures/lift_lower/anthropic/single_tool_use.json{
"arguments": [
123,
34,
116,
105,
99,
107,
101,
114,
34,
58,
34,
65,
78,
84,
72,
34,
125
],
"provenance": {
"api_version": "anthropic.2023-06-01",
"principal": {
"kind": "anthropic_workspace",
"workspace_id": "wks_chio_demo"
},
"provider": "anthropic",
"received_at": {
"nanos_since_epoch": 0,
"secs_since_epoch": 1745452803
},
"request_id": "msg_01abcdEFGHJK1234567890mn"
},
"provider": "anthropic",
"tool_name": "get_stock_price"
}The keys are alphabetical because the file is canonical JSON, and api_version here is the fixture's own label rather than the adapter constant. The Bedrock fixtures carry the richer principal.
$ jq '.provenance.principal' \
crates/protocol/chio-tool-call-fabric/fixtures/lift_lower/bedrock/assumed_role.json{
"account_id": "123456789012",
"assumed_role_session_arn": "arn:aws:sts::123456789012:assumed-role/ChioAgentRole/session-1",
"caller_arn": "arn:aws:iam::123456789012:role/ChioAgentRole",
"kind": "bedrock_iam"
}ToolInvocation::validate is the fail-closed check an adapter or replay consumer runs on a value that crossed a boundary. It enforces what the struct shape alone cannot:
providerequalsprovenance.provider.- The
Principalvariant matches the provider; a Bedrock invocation carrying anAnthropicWorkspaceprincipal is rejected. tool_name,request_id, andapi_versionare non-empty, free of surrounding whitespace, and free of control characters.- The same rules hold for the principal's fields:
org_id,workspace_id,caller_arn,account_id,assumed_role_session_arn,project_id, andhost. argumentsre-canonicalizes to itself byte for byte. Bytes that are not JSON fail first withInvalidArgumentJson; JSON that is not canonical fails withNonCanonicalArguments.
On the way back, the kernel returns a VerdictResult, which the adapter lowers into the provider's tool-result shape.
pub enum VerdictResult {
Allow {
redactions: Vec<Redaction>,
receipt_id: ReceiptId,
},
Deny {
reason: DenyReason,
receipt_id: ReceiptId,
},
}pub enum DenyReason {
PolicyDeny { rule_id: String },
GuardDeny { guard_id: String, detail: String },
CapabilityExpired,
PrincipalUnknown,
BudgetExceeded,
}A Redaction is a JSON Pointer path plus a replacement string, applied to the tool result on the allow path; a path that does not start with / is refused. The kernel-side conversion always returns an empty redaction list, so redactions come from an adapter or a caller. Three opaque byte wrappers keep wire handling in the adapter: ProviderRequest (upstream request bytes an adapter lifts), ProviderResponse (bytes lower hands back to the transport), and ToolResult (canonical-JSON tool output for consistent downstream verification).
Anthropic native tool gate
The adapter recognizes the native names computer_use, bash, and text_editor, plus their date-suffixed forms, and fails closed on them unless both gates are open: the computer-use cargo feature is compiled in (it also adds the anthropic-beta: computer-use-2025-01-24 header), and the manifest's server_tools list names the matching stable entry. Only tools named in server_tools pass ensure_tool_allowed.
| Adapter- | Manifest entry |
|---|---|
computer_use, computer_ | computer_use |
bash, bash_YYYYMMDD | bash |
text_editor, text_ | text_editor |
Any 8-digit date suffix folds onto the bare name, so a wire bump from bash_20241022 to a later date stays behind the same entry. Build the gate from a validated manifest with AnthropicAdapter::new_with_manifest; AnthropicAdapter::new denies every recognized native-tool call, even with the feature compiled. Other names skip this gate, and their kernel capability checks still apply. Match support for a new provider toolset to the adapter's pinned beta contract, never to a date suffix alone.
The field name server_tools refers to the Chio manifest. Computer, Bash, and text-editor calls are client-executed tools, which Anthropic's tool reference distinguishes from provider-hosted server tools.
Bedrock IAM caller
new_with_signed_iam_principals_config_from_sts resolves STS GetCallerIdentity once per process, loads config/iam_principals.toml, checks the Sigstore bundle beside it (the TOML path with .sigstore-bundle.json appended, per sigstore_bundle_path) through the chio-attest-verify verifier, and maps the caller ARN to Principal::BedrockIam.
# iam_principals.toml
# Default signed-config path for Bedrock IAM principal mapping.
# Replace these sample mappings with operator-owned values and publish a
# matching Sigstore bundle at iam_principals.toml.sigstore-bundle.json.
default_action = "deny"
config_version = 1
[[mapping]]
match = "arn:aws:iam::123456789012:role/ChioAgentRole"
owner = "team-alpha"
notes = "sample exact role mapping"
[[mapping]]
match = "arn:aws:sts::123456789012:assumed-role/ChioAgentRole/*"
owner = "team-alpha"
notes = "sample assumed-role session mapping"The mapping list is ordered and the first match wins; each match is an exact ARN or a * wildcard pattern. Loading rejects a config_version other than 1, a default_action other than deny, an empty mapping list, and any entry with a blank match or owner (crates/protocol/chio-bedrock-converse-adapter/src/iam_principals.rs:272-300). The shipped file is a sample: replace its mappings with operator-owned values and publish a matching Sigstore bundle beside it.
Resolution fails closed on a missing config file, a missing Sigstore bundle, a rejected signature, invalid TOML, an unsupported config_version, or a caller ARN with no matching entry. The bundle is checked before the TOML is parsed, so a signature problem masks any content problem behind it. For an STS assumed-role caller, the adapter keeps the session ARN in assumed_role_session_arn and the canonical role ARN in caller_arn. principal_owner() and matched_iam_principal_pattern() return the resolved owner and the matched pattern. The mapping is one case of binding an execution environment to a Chio principal, which Workload Identity covers.
Provider errors
Both adapters map upstream failures to the ProviderError taxonomy: seven named classes plus Other. Other wraps an anyhow::Error, and neither adapter crate depends on anyhow, so neither produces it. Anything an adapter cannot place lands in Malformed and fails closed.
pub enum ProviderError {
#[error("rate limited by upstream: retry after {retry_after_ms}ms")]
RateLimited { retry_after_ms: u64 },
#[error("upstream content policy denied request: {0}")]
ContentPolicy(String),
#[error("tool arguments failed schema validation: {0}")]
BadToolArgs(String),
#[error("upstream 5xx ({status}): {body}")]
Upstream5xx { status: u16, body: String },
#[error("transport timeout after {ms}ms")]
TransportTimeout { ms: u64 },
#[error("verdict latency budget exceeded ({observed_ms}ms > {budget_ms}ms); fail-closed")]
VerdictBudgetExceeded { observed_ms: u64, budget_ms: u64 },
#[error("malformed upstream payload: {0}")]
Malformed(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}RateLimited. HTTP 429 on the Anthropic path,ThrottlingExceptionon Bedrock.retry_after_msis a constant,0for Anthropic and1000for Bedrock; neither adapter reads a provider retry hint, and the Anthropic transport surfaces no response headers.ContentPolicy. HTTP 403 from the provider, on the Anthropic path only. A refusal inside a 200 response is not parsed into it, and a Bedrock guardrail intervention arrives as another class.BadToolArgs. Atool_use.inputortoolUse.inputthat cannot become canonical JSON object arguments; also any non-403 4xx on the HTTP path, and a malformed tool result on the Bedrock lower path.Upstream5xx. Any status in the 500 range, which is where an overload response lands. Bedrock also routesModelTimeoutExceptionhere, with status 408.TransportTimeout. A local per-call timeout, classified apart from upstream timeout envelopes.VerdictBudgetExceeded. Your evaluator reported missing its own budget. The adapter preservesobserved_msandbudget_msand fails the stream closed.Malformed. Impossible or out-of-order native payloads; the fail-closed default.
Detached provenance
A kernel receipt signs its own fields, and the basic invocation-to-request builder leaves the full stamp out of the kernel request. When an auditor needs the upstream identity (provider, request ID, principal) as asserted by a trusted adapter, sign the stamp separately. sign_provenance serializes it to RFC 8785 canonical JSON, signs those bytes, and returns a SignedProvenance envelope carrying the stamp, the signed bytes, the algorithm, the public key, and the detached signature.
use chio_tool_call_fabric::{sign_provenance, verify_signed_provenance};
// Sign the stamp with the adapter's signing backend.
let envelope = sign_provenance(&invocation.provenance, &backend)?;
// verify_signed_provenance runs three checks in order:
// 1. envelope.algorithm matches the signature algorithm,
// 2. envelope.signed_bytes re-canonicalize from stamp byte-for-byte,
// 3. the signature verifies against public_key over signed_bytes.
// On success it returns the public key that signed the stamp.
let signer = verify_signed_provenance(&envelope)?;Compare the returned signer with a configured trusted authority before accepting the claim: the signature check uses the key inside the envelope, which by itself names neither your expected adapter nor a provider-authenticated principal. The envelope is canonical-JSON serializable, so it fits an audit log line, a receipt attachment, or a standalone attestation. Its cryptography is the same used for capability tokens and receipts; the adapter adds no key handling.
Pinned contracts and offline replay
Byte-stable canonical-JSON fixtures pin the lift and lower contract under crates/protocol/chio-tool-call-fabric/fixtures/lift_lower/{openai,anthropic,bedrock}/, three files each, and a test asserts each re-serializes to the same bytes; the two fixture transcripts above read them. chio-provider-conformance keeps its own NDJSON capture corpus under crates/protocol/chio-provider-conformance/fixtures/ and replays it through each adapter behind per-provider cargo features.
chio replay traffic reads a chio-tee-frame.v1 NDJSON stream and deserializes a ToolInvocation from each frame. With no other flags it validates.
$ chio replay traffic \
--from ./capture.ndjsonchio replay traffic: validating <chio-source>/crates/products/chio-cli/tests/replay_traffic/fixtures/clean_match.ndjson (schema=chio-tee-frame.v1) line 1: ok (01H7ZZZZZZZZZZZZZZZZZZZZZA) chio replay traffic: 1/1 frames passed
--against <policy-path> re-executes each captured call in an ephemeral kernel and compares the recomputed verdict with the captured one. The policy is the one you name, so a replay answers what that policy would have done. That path refuses a policy carrying post-output guards or concrete-server grants, which a pre-output replay cannot honor.
A frame's upstream.system is one of six: openai, anthropic, aws.bedrock, mcp, a2a, and acp; the five vocabulary-only providers have no frame system. A capture goes wrong in two ways: a frame that does not parse, and a schema name that does not match.
$ chio replay traffic \
--from ./malformed.ndjsonchio replay traffic: validating <chio-source>/crates/products/chio-cli/tests/replay_traffic/fixtures/parse_error.ndjson (schema=chio-tee-frame.v1) line 1: FAIL exit=30 ndjson parse error on line 1: key must be a string at line 1 column 2 chio replay traffic: 0/1 frames passed
$ chio replay traffic \
--from ./capture.ndjson --schema chio-tee-frame.v2chio replay traffic: validating <chio-source>/crates/products/chio-cli/tests/replay_traffic/fixtures/clean_match.ndjson (schema=chio-tee-frame.v2) line 1: FAIL exit=40 schema-version gate failed: expected schema name "chio-tee-frame.v1", got "chio-tee-frame.v2" chio replay traffic: 0/1 frames passed
Symptoms and fixes
NonCanonicalArgumentson a value you serialized. The bytes are JSON but not RFC 8785 canonical, most often unsorted keys. Re-serialize canonically. Bytes that are not JSON raiseInvalidArgumentJson, a different bug.- A validation failure names the principal. The
Principalvariant differs fromprovenance.provider, or one of its fields is empty, padded with whitespace, or carries a control character. - Bedrock construction refuses before any traffic. The region or API version is not the pinned pair. The transport re-checks the region on every call, so a later override fails too.
- IAM principal resolution fails at startup. One of the six fail-closed conditions under Bedrock IAM caller holds, or the mapping list is empty.
- A recognized native tool fails closed with the feature compiled. The manifest is the second and authoritative gate. Build with
new_with_manifestand name the stable entry inserver_tools. - A stream fails instead of forwarding an allowed block. The block exceeded the buffer caps, or the allow carried redactions.
- A Bedrock tool name comes back
Malformed. The response carried atoolConfigthat does not declare the lifted name: the lift refuses a tool the turn never offered. - An approval-gated call reads as a plain deny. The kernel-to-fabric conversion maps
PendingApprovaltoDenywithrule_idkernel.pending_approval. Read that reason or the receipt to tell them apart.