LearnAnatomy of a Governed Call
Receipts
The kernel signs a receipt for each mediated decision, recording the request, the decision, the guard evidence, and the charge.
Overview
A receipt is a signed JSON document for an agent's tool request. It records the request, the kernel's decision, and the guard evidence. For priced calls, it also records attribution and cost. See Autonomous Commerce for how receipts participate in billing. The kernel produces one receipt per decision, including denials. Receipts answer four questions:
- Who: which agent, using which capability token, on which tool server
- What: which tool, with what parameters
- When: Unix timestamp of the decision
- Result: the kernel's decision and whatever evidence the guards reported
Receipt coverage
allow, deny, cancelled, or incomplete. If the kernel mediated a tool call, the log contains a receipt. The same signed format also records trace observations and advisory evaluations without a kernel decision.Model
ChioReceipt fields
The ChioReceipt struct in crates/core/chio-core-types/src/receipt/body.rs is the whole record. 20 of its 24 fields feed the content-addressed id; the signature covers those plus the id itself, 21 in all; and 9 carry a serde rule that drops them from the wire when they are empty, absent, or at their default.
| Field | Type | On the wire | Purpose |
|---|---|---|---|
id | String | always | Content-addressed receipt ID derived from the canonical receipt body. |
timestamp | u64 | always | Unix timestamp (seconds) when the receipt was created. |
capability_id | String | always | ID of the capability token that was exercised (or presented). |
tool_server | String | always | Tool server that handled the invocation. |
tool_name | String | always | Tool that was invoked (or attempted). |
action | ToolCallAction | always | The action that was evaluated. |
decision | Option<Decision> | omitted when None | The Kernel's decision. Present only for mediated decisions. |
receipt_kind | ReceiptKind | always | Signed receipt semantic kind. |
boundary_class | BoundaryClass | always | Signed runtime boundary class. |
observation_outcome | Option<ObservationOutcome> | omitted when None | Signed observation outcome for trace and advisory records. |
tool_origin | ToolOrigin | always | Signed tool-origin classification. |
redaction_mode | RedactionMode | always | Signed redaction mode. |
actor_chain | Vec<ActorRef> | omitted when empty | Signed actor attribution chain. |
content_hash | String | always | SHA-256 hash of the evaluated content for this receipt. |
policy_hash | String | always | SHA-256 hash of the policy that was applied. |
evidence | Vec<GuardEvidence> | omitted when empty | Per-guard evidence collected during evaluation. |
metadata | Option<serde_json::Value> | omitted when None | Optional receipt metadata for stream/accounting details. |
trust_level | TrustLevel | always | Strength of kernel mediation that produced this receipt. |
tenant_id | Option<String> | omitted when None | Multi-tenant receipt isolation: tenant identifier for multi-tenant deployments. `None` in single-tenant mode; derived from the authenticated session's enterprise identity context and MUST NOT be taken from caller-provided request fields (caller choice would defeat the isolation intent). Serialized only when set. |
bbs_projection_version | Option<String> | omitted when None | BBS projection version bound into the receipt id when BBS material is present. |
kernel_key | PublicKey | always | The Kernel's public key (for verification without out-of-band lookup). |
bbs_signature | Option<BbsReceiptSignature> | omitted when None | Optional BBS material for selective disclosure over this receipt. |
algorithm | Option<SigningAlgorithm> | omitted when is_default_optional_algorithm | Signing algorithm used for [`ChioReceipt::signature`]. Informational only: verification dispatches off the self-describing encoding of the signature itself. |
signature | Signature | always | Signature over canonical JSON of [`ChioReceiptSigningBody`]. |
The id is content-addressed: it is a hash of the canonical id input, not a random UUID, so the same input always yields the same id and any edit changes it. The decision is optional: it is present for mediated decisions and absent on the trace and advisory receipt kinds described next. See the Receipt Format reference for the complete field-by-field shape and wire semantics.
{ "name": "Chio" }, }, }, "compensation_status": "not_compensated", "coordinator_lease_epoch": 1, "coordinator_lease_id": "01a06cb0-a712-7440-b724-c4fe54913090", "operation_id": "a78bde4b5404092f9eecca27e3d5109dcda0a870befee69f32e9dcbd49e12343", "projected_dispatch_state": "terminal", "projected_operation_version": 8, "projected_state": "completed", "request_binding_hash": "91dc5c706136b424cd8c4bb77df99232e8f40028b2217d5d2a1c84adfd246e70", "request_id": "check-001", "request_namespace_digest": "b6fe626fa0256f31deefc202a2efaecb2defd6a4ba93a5237dbc5f063948a651", "retained_dispatch_commit": { "committed_version": 6, "coordinator_lease_epoch": 1, "coordinator_lease_id": "01a06cb0-a712-7440-b724-c4fe54913090", "provider_attempt": { "attempt_id": "attempt:a78bde4b5404092f9eecca27e3d5109dcda0a870befee69f32e9dcbd49e12343", "operation_id": "a78bde4b5404092f9eecca27e3d5109dcda0a870befee69f32e9dcbd49e12343", "transport_id": "kernel-tool-server:hello", "transport_key_epoch": 1 }, "store_fence": { "lease_id": "01a06cb0-a712-7440-b724-c4fe54913090", "owner_epoch": 1, "store_uuid": "01a06cb0-a62a-7ef0-a2bc-fee0de0a9828" } }, "schema": "chio.admission-receipt.v1", "store_fence": { "lease_id": "01a06cb0-a712-7440-b724-c4fe54913090", "owner_epoch": 1, "store_uuid": "01a06cb0-a62a-7ef0-a2bc-fee0de0a9828" }, "tool_outcome_id": "74a24dc524ae52adfe5594445a9fabb7f54d5c8d4580be5124ceab8ca7a87144", "tool_outcome_version": 2, "trusted_time_unix_ms": 1788529911968 }, "delegation_depth": 0, "grant_index": 0, "issuer_key": "e08d69a6a3bf74cf4971a7e0a1ff3f566129fad32ec5580c3342316859ecf436", "subject_key": "bb82465acb67f9413d39ef26ba3ceda6b5cd6413739e41a58b33edeb2909d8d8" }, "receipt_context": { "request_id": "check-001" } },}
crates/core/chio-core-types/src/receipt/body.rs:42-109at fe56570Receipt kinds and boundary class
A group of signed fields identifies the record type and the portion of the call Chio observed or enforced. A verifier can assess the record's boundary from the signed fields alone.
| Field | Variants | Meaning |
|---|---|---|
receipt_kind | MediatedDecision, TraceObservation, AdvisoryEvaluation | Whether the record is an authorization decision or an observation the kernel logged without mediating |
boundary_class | Prevent, DetectOnly, AdvisoryOnly, CannotSee | What Chio could enforce on this call: full prevention down to no visibility at all |
observation_outcome | Observed, Evaluated, Dropped | For non-mediated records: whether the observation was seen, evaluated, or dropped |
tool_origin | CallerExecuted, HostExecutedProviderReported, HostExecutedUnmediated | Where the tool effect ran relative to Chio |
redaction_mode | None, Summary, Redacted | How much of the signed detail was redacted before storage or export |
trust_level | Mediated, Verified, Advisory | How the kernel participated: inline mediation, in-process authorization, or advisory evaluation the caller could ignore |
actor_chain | Vec<ActorRef> | The signed attribution chain of actors behind the request |
The two BBS fields, bbs_projection_version and bbs_signature, carry optional BBS+ material for selective disclosure. A holder can present selected receipt fields without revealing the rest of the receipt.
Decision variants
The Decision enum captures the outcomes of a mediated call. On the wire the variants serialize under a "verdict" tag with lowercase names: allow, deny, cancelled, and incomplete. The deny variant carries both reason and guard, the name of the blocking step.
/// The Kernel's verdict on a tool call.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "verdict", rename_all = "snake_case")]
pub enum Decision {
/// The tool call was allowed and executed.
Allow,
/// The tool call was denied.
Deny {
/// Human-readable reason for the denial.
reason: String,
/// The guard or validation step that triggered the denial.
guard: String,
},
/// The tool call was interrupted by explicit cancellation.
Cancelled {
/// Human-readable reason for the cancellation.
reason: String,
},
/// The tool call did not reach a complete terminal result.
Incomplete {
/// Human-readable reason for the incomplete terminal state.
reason: String,
},
}Each Deny records the step that rejected the call, so a verifier can identify the rule. When the capability check refuses a request before any guard runs, guard reads kernel.
ToolCallAction
The action field captures the parameters passed to the tool alongside a hash a reader can recompute.
/// Describes the tool call that was evaluated.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallAction {
/// The parameters that were passed to the tool (or attempted).
pub parameters: serde_json::Value,
/// SHA-256 hash of the canonical JSON of `parameters`.
pub parameter_hash: String,
}parameter_hash is the bare lowercase hex SHA-256 of the canonical JSON (RFC 8785) of the parameters, with no prefix. A reader can check it without checking the receipt signature.
Guard evidence
The evidence array records what each guard reported during evaluation. Reporting is the guard's choice: a guard returns evidence alongside its verdict, and GuardPipeline::evaluate concatenates whatever it gets, in registration order.
/// Evidence from a single guard's evaluation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GuardEvidence {
/// Name of the guard (e.g. "ForbiddenPathGuard").
pub guard_name: String,
/// Whether the guard passed (true) or denied (false).
pub verdict: bool,
/// Optional details about the guard's decision.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}guard_name holds the value Guard::name() returns, which for the default pipeline is the hyphenated runtime name rather than the struct name the doc comment shows. In registration order those names are forbidden-path, shell-command, egress-allowlist, path-allowlist, mcp-tool, secret-leak, and patch-integrity.
None of them reports anything when it allows: each returns GuardDecision::allow(), whose evidence vector is empty. A call that clears the default pipeline therefore ends with an empty array, and serde drops the key, which is why the quickstart receipts carry no evidence at all. A guard that does report on allow, such as AdvisoryPipeline, contributes one entry per signal it raised.
The pipeline adds an entry of its own only when a guard stops the call, and then it stops evaluating, so nothing after that guard appears.
Verdict::Deny => {
evidence.push(GuardEvidence {
guard_name: guard.name().to_string(),
verdict: false,
details: Some(
"action=deny; reason=guard denied request".to_string(),
),
});
return Ok(GuardDecision::deny(evidence));
}
}
}
Err(e) => {
// Fail closed: guard errors are treated as denials.
evidence.push(GuardEvidence {
guard_name: guard.name().to_string(),
verdict: false,
details: Some(format!("action=error; reason=fail-closed; error={e}")),
});
return Ok(GuardDecision::deny(evidence));
}details is absent where the guard reported none. See Guards for the pipeline itself.
Child request records
Nested operations within a parent tool call produce ChildRequestReceipt records. These link back to the parent through parent_request_id, forming a DAG of parent and child receipt chains across nested operations. Each child records its operation kind and how the operation ended: Completed, or Cancelled or Incomplete with a reason. The terminal state describes the lifecycle. The authorization verdict is the parent receipt's decision.
/// Signed audit record for a nested child request handled under a parent tool call.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChildRequestReceipt {
pub id: String,
pub timestamp: u64,
pub session_id: SessionId,
pub parent_request_id: RequestId,
pub request_id: RequestId,
pub operation_kind: OperationKind,
pub terminal_state: OperationTerminalState,
pub outcome_hash: String,
pub policy_hash: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
pub kernel_key: PublicKey,
/// Signing algorithm. Absent means Ed25519 (the default).
#[serde(default, skip_serializing_if = "is_default_optional_algorithm")]
pub algorithm: Option<SigningAlgorithm>,
pub signature: Signature,
}Child request receipts are signed and verified the same way standard receipts are, so the same non-repudiation argument covers nested operations.
How it works
Content hashing
content_hash is the bare lowercase hex SHA-256 of the evaluated content, with no sha256: prefix. crates/kernel/chio-kernel/src/receipt_support/receipt_content.rs computes it three ways:
- a value output hashes the canonical JSON bytes of that value;
- a streamed output hashes the concatenated per-chunk digests, not the raw payload, so the signing-boundary recompute reads the same preimage;
- no output at all hashes the four bytes
null.
The third case is the one a deny lands in: the call never reaches a tool, so its content_hash is the SHA-256 of null. Hash those four bytes and compare them with the quickstart deny receipt.
Policy hashing
policy_hash identifies the policy that decided the call. It is not a hash of the file: runtime_hash_for_hushspec in crates/platform/chio-control-plane/src/policy/util.rs builds a JSON fingerprint of the loaded policy, its format, kernel block, default capabilities, rule blocks, reputation extensions, and the digests of any auxiliary assets, then hashes that. Two policy files that differ only in comments or whitespace load to the same value, and a rule change moves it.
chio check prints that value as policy: alongside source:, the hash of the policy bytes and their assets. The receipt carries the runtime hash and not the source hash.
Policy pinning for compliance
Signing
The kernel signs receipts with its Ed25519 key. The signed object is not the receipt and not the receipt body: ChioReceiptSigningBody in crates/core/chio-core-types/src/receipt/signing.rs holds the id, the ChioReceiptIdInput the id was derived from, and the BBS signature when one is present. Signing runs two validations first and computes the id last.
/// Sign a receipt body with the Kernel's Ed25519 keypair.
pub fn sign(body: ChioReceiptBody, keypair: &Keypair) -> Result<Self> {
validate_bbs_receipt_binding(&body, None)?;
ensure_keypair_matches_embedded_key(&body.kernel_key, keypair, "receipt", "kernel_key")?;
let body = prepare_receipt_body_for_signing(body)?;
let signing_body = ChioReceiptSigningBody::from(&body);
let (signature, _bytes) = keypair.sign_canonical(&signing_body)?;
Ok(Self::from_signed_body(body, None, None, signature))
}validate_bbs_receipt_bindingrejects a body whose BBS material and projection version disagree.ensure_keypair_matches_embedded_keyrejects a signature by a key other than thekernel_keythe receipt embeds.prepare_receipt_body_for_signingvalidates the body's semantics, folds the caller-supplied nonce intometadataunderchio_receipt_signing_nonce, and setsidtochio_receipt_id(&body), the hex SHA-256 of the canonical JSON of the id input.- The keypair signs the canonical JSON of the signing body.
Canonical JSON defines signed bytes
Verifying a receipt
A third party verifies a receipt without contacting the kernel: the record embeds the kernel's public key. verify_signature checks four things, and re-deriving the id is one of them, so a receipt whose fields were edited fails even if its signature bytes were left alone.
/// Verify the receipt signature against the embedded kernel key.
pub fn verify_signature(&self) -> Result<bool> {
let body = self.body();
if body.validate_signable_semantics().is_err() {
return Ok(false);
}
if validate_bbs_receipt_binding(&body, self.bbs_signature.as_ref()).is_err() {
return Ok(false);
}
if chio_receipt_id(&body)? != self.id {
return Ok(false);
}
let signing_body =
ChioReceiptSigningBody::from_body_and_bbs(&body, self.bbs_signature.as_ref());
self.kernel_key
.verify_canonical(&signing_body, &self.signature)
}Checking the signature says the embedded key signed this record. It does not say the key belongs to a kernel you trust. Compare kernel_key against a trusted key registry for that.
The parameter hash inside action is independently checkable by the same method the kernel used to compute it.
/// Verify that `parameter_hash` matches the canonical hash of `parameters`.
pub fn verify_hash(&self) -> Result<bool> {
let canonical = canonical_json_bytes(&self.parameters)?;
let expected = sha256_hex(&canonical);
Ok(self.parameter_hash == expected)
}A receipt from the quickstart
The receipt below is the one the quickstart's allow produced. The starter policy grants hello_world on server hello, chio check allowed the call, and chio receipt list piped through jq selects it from the log. Running the quickstart yourself produces a receipt of the same shape with your own ids, keys, and timestamps.
$ chio --receipt-db .chio/receipts.db receipt list --admin-all \
| jq 'select(.decision.verdict == "allow")'{
"id": "448440b65fa15559a364d24512cbe4f08631befe2c3d2ab471dad7204b8b69c8",
"timestamp": 1788529911,
"capability_id": "cap-01a06cb0-a7d2-7e10-b32d-b985627e17e7",
"tool_server": "hello",
"tool_name": "hello_world",
"action": {
"parameters": {
"name": "Chio"
},
"parameter_hash": "319910ed4307f4c6486b4d282a01d4276a7aec7626c66be0f7eb2173d4a973a3"
},
"decision": {
"verdict": "allow"
},
"receipt_kind": "mediated_decision",
"boundary_class": "prevent",
"tool_origin": "caller_executed",
"redaction_mode": "none",
"content_hash": "8813cfed2cad28d238168ce79dde8ff3b39d38f2a5bea3661c7726d63f69f5ad",
"policy_hash": "69e943b96e9ce64d0264bb56bf8930e77bd4e68adf68fcf1395790dae03e6b55",
"metadata": {
"admission_operation": {
"compensation_status": "not_compensated",
"coordinator_lease_epoch": 1,
"coordinator_lease_id": "01a06cb0-a712-7440-b724-c4fe54913090",
"operation_id": "a78bde4b5404092f9eecca27e3d5109dcda0a870befee69f32e9dcbd49e12343",
"projected_dispatch_state": "terminal",
"projected_operation_version": 8,
"projected_state": "completed",
"request_binding_hash": "91dc5c706136b424cd8c4bb77df99232e8f40028b2217d5d2a1c84adfd246e70",
"request_id": "check-001",
"request_namespace_digest": "b6fe626fa0256f31deefc202a2efaecb2defd6a4ba93a5237dbc5f063948a651",
"retained_dispatch_commit": {
"committed_version": 6,
"coordinator_lease_epoch": 1,
"coordinator_lease_id": "01a06cb0-a712-7440-b724-c4fe54913090",
"provider_attempt": {
"attempt_id": "attempt:a78bde4b5404092f9eecca27e3d5109dcda0a870befee69f32e9dcbd49e12343",
"operation_id": "a78bde4b5404092f9eecca27e3d5109dcda0a870befee69f32e9dcbd49e12343",
"transport_id": "kernel-tool-server:hello",
"transport_key_epoch": 1
},
"store_fence": {
"lease_id": "01a06cb0-a712-7440-b724-c4fe54913090",
"owner_epoch": 1,
"store_uuid": "01a06cb0-a62a-7ef0-a2bc-fee0de0a9828"
}
},
"schema": "chio.admission-receipt.v1",
"store_fence": {
"lease_id": "01a06cb0-a712-7440-b724-c4fe54913090",
"owner_epoch": 1,
"store_uuid": "01a06cb0-a62a-7ef0-a2bc-fee0de0a9828"
},
"tool_outcome_id": "74a24dc524ae52adfe5594445a9fabb7f54d5c8d4580be5124ceab8ca7a87144",
"tool_outcome_version": 2,
"trusted_time_unix_ms": 1788529911968
},
"attribution": {
"delegation_depth": 0,
"grant_index": 0,
"issuer_key": "e08d69a6a3bf74cf4971a7e0a1ff3f566129fad32ec5580c3342316859ecf436",
"subject_key": "bb82465acb67f9413d39ef26ba3ceda6b5cd6413739e41a58b33edeb2909d8d8"
},
"chio_receipt_signing_nonce": "rcpt-01a06cb0-a8c2-7ad2-a78a-562ac4366063",
"receipt_context": {
"request_id": "check-001"
}
},
"trust_level": "mediated",
"kernel_key": "e08d69a6a3bf74cf4971a7e0a1ff3f566129fad32ec5580c3342316859ecf436",
"signature": "676bef1ac96b8519f5b316a98c05de7b46300e06983e0da58bb70db6d6c02f619222d07e44da61f2ca689015ab731d9c9e7727aeb826ad4690a2428f9fa82a01"
}The same log holds the quickstart's deny. The request named drop_tables, a tool no grant covers, so the capability check refused it before any guard ran: decision.guard is kernel and the reason names the scope. Neither receipt carries an evidence key; the field serializes only when the array holds at least one entry, per its skip_serializing_if attribute in crates/core/chio-core-types/src/receipt/body.rs.
$ chio --receipt-db .chio/receipts.db receipt list --admin-all \
| jq 'select(.decision.verdict == "deny")'{
"id": "28b6e2576ca2ccbd031e769d8d2bd504317161115a600f27771a7f932307a284",
"timestamp": 1788529912,
"capability_id": "cap-01a06cb0-aae3-7ad3-8ba7-01350a3e01a0",
"tool_server": "hello",
"tool_name": "drop_tables",
"action": {
"parameters": {},
"parameter_hash": "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
},
"decision": {
"verdict": "deny",
"reason": "requested tool drop_tables on server hello is not in capability scope",
"guard": "kernel"
},
"receipt_kind": "mediated_decision",
"boundary_class": "prevent",
"tool_origin": "caller_executed",
"redaction_mode": "none",
"content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
"policy_hash": "69e943b96e9ce64d0264bb56bf8930e77bd4e68adf68fcf1395790dae03e6b55",
"metadata": {
"attribution": {
"delegation_depth": 0,
"issuer_key": "3dbf7b1230475796d5c78cdcf441c84721d9e3c24362fbbfb4225e307d1b9fb4",
"subject_key": "af2da8097f2c133affd89145302f029c7a5854e4e3a692a92b0cd20f84d07b37"
},
"chio_receipt_signing_nonce": "rcpt-01a06cb0-ab51-70d0-bcb3-72a8b78fe057",
"receipt_context": {
"request_id": "check-001"
}
},
"trust_level": "mediated",
"kernel_key": "3dbf7b1230475796d5c78cdcf441c84721d9e3c24362fbbfb4225e307d1b9fb4",
"signature": "e89f7a9cbe6c6b207cf5ac0c3e197a3957d62d2994217a7dcaeeeb9869abef1233680328432ee7a502a76238eb6ee9e72b19715e2458a3ef85aebfbed0df9202"
}Financial metadata
When a tool call exercises a monetary grant, the receipt's metadata field includes a FinancialReceiptMetadata record under the "financial" key. The full field set is:
| Field | Type | Purpose |
|---|---|---|
grant_index | u32 | Index of the matching grant in the capability token's scope |
cost_charged | u64 | Cost charged for this invocation in currency minor units |
currency | String | ISO 4217 currency code |
budget_remaining | u64 | Remaining budget after this charge, in minor units |
budget_total | u64 | Total budget for this grant, in minor units |
delegation_depth | u32 | Depth of the delegation chain at the time of invocation |
root_budget_holder | String | Identifier of the root budget holder in the delegation chain |
payment_reference | Option<String> | External payment reference for settlement |
settlement_status | SettlementStatus | Settlement status for this charge |
cost_breakdown | Option<Value> | Itemized cost breakdown for audit purposes |
oracle_evidence | Option<OracleConversionEvidence> | Oracle price evidence used for cross-currency conversion |
attempted_cost | Option<u64> | Cost attempted but denied, populated only on denial receipts |
SettlementStatus reads NotApplicable when no external settlement applies, such as a pre-execution denial; Pending when settlement has started but is not final; Settled when the recorded charge is final for the current execution path; and Failed when execution completed but settlement failed or became invalid.
On denial receipts caused by budget exhaustion, attempted_cost records the cost that would have been charged while cost_charged is zero. For cross-currency invocations, the oracle_evidence field holds an OracleConversionEvidence record: the base and quote currencies, the integer rate_numerator and rate_denominator pair, the oracle source and feed_address, an updated_at timestamp with its max_age_seconds and cache_age_seconds freshness bounds, and the converted and original cost in units. The record is itself a signed oracle attestation: when the quote is signed it carries its own oracle_public_key and signature, so a verifier can check the rate against the oracle's key without trusting the kernel. See Economics for the full struct.
FinancialReceiptMetadata is the settled summary. Alongside it the same module defines the budget-hold lineage records that back it ( FinancialBudgetHoldAuthorityMetadata, FinancialBudgetAuthorizeReceiptMetadata, and FinancialBudgetTerminalReceiptMetadata ) which capture the authorize, lease, and terminal steps of the budget hold that check_and_increment_budget opens and later reconciles. Economics walks that lifecycle.
financial is one of the 11 top-level metadata keys the kernel reserves. Three others record admission and delivery: admission_operation (schema chio.admission-receipt.v1), the durable admission projection the allow receipt above carries; delivery_contract (schema chio.delivery-contract.v1), the expected digest, observed digest, and match result of a digest-constrained call; and finding_delivery (schema chio.finding.delivery.v1), the overlay for a purchased finding reveal. The kernel writes these blocks, merges them last, and rejects caller or hook metadata that already holds a reserved key, so a verifier can treat a block under a reserved key as kernel-authored and covered by the receipt signature. See The Delivery Contract for the digest comparison and the Receipt Format reference for the wire shape of the receipt around them.
Reading the log
chio receipt list queries the receipt log with filters. Every local read requires an explicit tenant boundary: pass --tenant <id> to scope the listing to one tenant, or --admin-all to read across all tenants as an administrative operation. Output is JSON Lines: one receipt per line.
# List all receipts for a specific tool server
$ chio receipt list --admin-all --tool-server srv-files
# Filter by tool name and outcome
$ chio receipt list --admin-all --tool-name file_read --outcome allow
# Time range: --since / --until take Unix seconds
$ chio receipt list --admin-all --since 1735689600 --until 1735776000
# Filter by cost (minor currency units)
$ chio receipt list --admin-all --min-cost 100 --max-cost 500 --cost-currency USD
# Page through results
$ chio receipt list --admin-all --limit 20 --cursor 1042Against the quickstart store, the unfiltered listing prints one line per receipt. Piping through jq narrows each line to the fields worth scanning.
$ chio --receipt-db .chio/receipts.db receipt list --admin-all \
| jq -r '[.decision.verdict, .tool_server, .tool_name, .id] | @tsv'allow hello hello_world 448440b65fa15559a364d24512cbe4f08631befe2c3d2ab471dad7204b8b69c8 deny hello drop_tables 28b6e2576ca2ccbd031e769d8d2bd504317161115a600f27771a7f932307a284
Dropping the read boundary is the failure worth meeting once. With neither flag, chio receipt list fails before touching the store with --tenant <id> or --admin-all is required for local receipt reads; with both, it fails with --tenant <id> and --admin-all are mutually exclusive. Both come from local_receipt_read_context in crates/products/chio-cli/src/cli/trust/receipt/list.rs. A remote read through --control-url takes neither flag and derives its scope from the control token instead.
| Filter | Flag | Description |
|---|---|---|
| Read boundary | --tenant / --admin-all | Required: scope to one tenant, or read across all tenants (mutually exclusive) |
| Capability | --capability | Filter by capability ID |
| Tool server | --tool-server | Filter by tool server ID |
| Tool name | --tool-name | Filter by tool name |
| Outcome | --outcome | Filter by decision outcome: allow, deny, cancelled, or incomplete |
| Start time | --since | Receipts with timestamp ≥ this Unix-seconds value |
| End time | --until | Receipts with timestamp ≤ this Unix-seconds value |
| Min cost | --min-cost | Minimum cost in minor currency units (financial receipts only) |
| Max cost | --max-cost | Maximum cost in minor currency units (financial receipts only) |
| Cost currency | --cost-currency | Currency for the cost filters, as a three-letter uppercase code |
| Page size | --limit | Maximum receipts per page, default 50 |
| Cursor | --cursor | Pagination cursor: sequence value to start after |
SIEM export
Receipts export to external security information and event management (SIEM) systems for centralized monitoring and alerting. chio-siem ships exporters for Splunk HEC, Elasticsearch, Datadog, Sumo Logic, OCSF, and a generic webhook, plus a CEF formatter that renders one CEF v0 event per receipt and leaves transport to one of the others. Each exporter implements a common Exporter trait and takes a Rust config struct; none of them is configured through policy YAML.
The five exporters that take an endpoint URL, Splunk, Elasticsearch, OCSF, Sumo Logic, and the webhook, call require_https_endpoint at construction and refuse a plain http:// URL. Datadog takes a site rather than a URL and composes https://http-intake.logs.<site>/api/v2/logs itself, then enforces an HttpEgressContract over it.
The Splunk HTTP Event Collector exporter streams receipt batches as newline-separated JSON event envelopes, each wrapping the full ChioReceipt under the event key.
pub struct SplunkConfig {
/// Splunk HEC endpoint URL (e.g. "https://splunk.example.com:8088").
pub endpoint: String,
/// HEC authentication token.
pub hec_token: String,
/// Splunk sourcetype for all exported events. Default: "chio:receipt".
pub sourcetype: String,
/// Optional index. Omit to use the HEC token's default index.
pub index: Option<String>,
/// Optional host field sent with each event envelope.
pub host: Option<String>,
/// HTTP request timeout. Default: 30 seconds.
pub timeout: Duration,
/// Typed HTTP egress contract enforced on every dispatch and redirect.
pub egress_contract: Option<HttpEgressContract>,
}The Elasticsearch exporter posts receipt batches through the /_bulk API, using receipt.id as the document _id so retries are idempotent.
pub struct ElasticConfig {
/// Elasticsearch endpoint URL (e.g. "https://es.example.com:9200").
pub endpoint: String,
/// Target index for all exported receipts. Default: "chio-receipts".
pub index_name: String,
/// Authentication method and credentials.
pub auth: ElasticAuthConfig,
/// HTTP request timeout. Default: 30 seconds.
pub timeout: Duration,
/// Typed HTTP egress contract enforced on every dispatch and redirect.
pub egress_contract: Option<HttpEgressContract>,
}
pub enum ElasticAuthConfig {
/// Sends `Authorization: ApiKey <key>`.
ApiKey(String),
/// HTTP Basic auth; the password is zeroized on drop.
Basic { username: String, password: Zeroizing<String> },
}Guarantees and limits
The store rejects rewrites
Receipts land in a SQLite log the schema keeps append-only. Triggers named chio_tool_receipts_reject_update and chio_tool_receipts_reject_delete abort any UPDATE or DELETE on the receipt table with tool receipts are immutable, and a matching pair covers child receipts. The log is Merkle-committed in batched checkpoints, so an inclusion proof verifies one receipt without replaying the log.
Protect the receipt log
What a receipt establishes
A verifying signature establishes that the embedded kernel key signed this id and this id input, and that the id still matches the fields the record carries. It does not establish that the key belongs to a kernel you trust, that the tool did what its output claims, or that the policy behind policy_hash was the right policy. boundary_class and trust_level record how much of the call Chio saw, and a CannotSee or Advisory record is evidence of an observation, not of enforcement. See Assurance Boundaries & Limits.
Who reads the log
Security teams inspect guard evidence and request history. Finance teams reconcile charges against budgets. Compliance teams compare the policy hash with the policy in force for a decision. Reputation systems derive denial, spending, and cancellation rates for agents and tool servers. These uses share the one signed format.
Next steps
- Receipt Format · the field-by-field wire shape and its schema
- The Mediated Call · where receipt signing sits in the evaluation order
- Economics · how a priced receipt reconciles against a budget
- Session-Aware Guards · using deny-rate and spending signals from receipt records
- Verifiable Evidence · checkpoints, inclusion proofs, and taking the log offline