Receipt Contract
Look up each signed receipt field, the decision variants, the bytes a signature covers, and how checkpoints commit to them.
On this page
A receipt is the kernel’s signed record of one decision: the request it evaluated, the verdict, the evidence its guards returned, and the key that signed it.
Field, decision, and metadata-key entries render from the receipt modules of the Chio source and change with them. Storage and retention operations belong to the Node pages.
Synopsis
A ChioReceipt is a canonical JSON object under the schema chio.receipt.v1. Its content-addressed id is the SHA-256 of the identity fields, and one signature covers the id, those fields, and any selective-disclosure material. The annotated receipt is the Quickstart refusal of a read of /secrets/.env.
decision and evidence
decision holds the verdict and the kernel’s reason. evidence lists each guard that ran and its verdict. It is empty here because the scope check refused the read before any guard ran. Read more in Guards.
/secrets/.env from the Quickstart, trimmed to the fields that identify the call, its decision, and its signer, with digests shortened. The complete record also carries a timestamp, a content hash, and metadata; the receipt format lists every field.Search for tenant_id, verdict, chain_root, or a metadata key.
ChioReceipt
A receipt has 24 fields under chio.receipt.v1, shared by three structs. The difference between them is what a verifier checks: ChioReceiptIdInput is the 20 fields the id is the SHA-256 of; ChioReceiptBody is those 20 plus the id; and ChioReceipt adds the three fields that carry the signature and its material.
| Field | Type | On the wire | In the id | What it records |
|---|---|---|---|---|
id | String | Always | No | Content- |
timestamp | u64 | Always | Yes | Unix timestamp (seconds) when the receipt was created. |
capability_id | String | Always | Yes | ID of the capability token that was exercised (or presented). |
tool_server | String | Always | Yes | Tool server that handled the invocation. |
tool_name | String | Always | Yes | Tool that was invoked (or attempted). |
action | ToolCallAction | Always | Yes | The action that was evaluated. |
decision | Option<Decision> | Omitted when None | Yes | The Kernel's decision. Present only for mediated decisions. |
receipt_kind | ReceiptKind | Always | Yes | Signed receipt semantic kind. |
boundary_class | BoundaryClass | Always | Yes | Signed runtime boundary class. |
observation_ | Option<Observation | Omitted when None | Yes | Signed observation outcome for trace and advisory records. |
tool_origin | ToolOrigin | Always | Yes | Signed tool-origin classification. |
redaction_mode | RedactionMode | Always | Yes | Signed redaction mode. |
actor_chain | Vec<ActorRef> | Omitted when empty | Yes | Signed actor attribution chain. |
content_hash | String | Always | Yes | SHA-256 hash of the evaluated content for this receipt. |
policy_hash | String | Always | Yes | SHA-256 hash of the policy that was applied. |
evidence | Vec<GuardEvidence> | Omitted when empty | Yes | Per-guard evidence collected during evaluation. |
metadata | Option<serde_ | Omitted when None | Yes | Optional receipt metadata for stream/ |
trust_level | TrustLevel | Always | Yes | Strength of kernel mediation that produced this receipt. |
tenant_id | Option<String> | Omitted when None | Yes | 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_ | Option<String> | Omitted when None | Yes | BBS projection version bound into the receipt id when BBS material is present. |
kernel_key | PublicKey | Always | Yes | The Kernel's public key (for verification without out-of-band lookup). |
bbs_signature | Option<Bbs | Omitted when None | No | Optional BBS material for selective disclosure over this receipt. |
algorithm | Option<Signing | Omitted when is_ | No | Signing algorithm used for [`Chio |
signature | Signature | Always | No | Signature over canonical JSON of [`Chio |
9 of the 24 fields carry a skip_serializing_if, so receipt JSON is a variable key set: an absent key means the default value. Anyone re-deriving the id needs this rule, because a key omitted from canonical JSON is omitted from the hash input.
decision is an Option. Trace and advisory receipts record their outcome through receipt_kind, boundary_class, and observation_outcome instead, and all three are in the id.
Decision
The decision is a 4-variant enum, internally tagged verdict with snake_case values, so the wire form is the flat object {"verdict": "deny", "reason": "...", "guard": "..."}. It differs from the kernel’s in-flight Verdict: the receipt records the terminal outcome, including cancellation and incompleteness.
| Variant | Wire value | Payload | Meaning |
|---|---|---|---|
Allow | allow | No payload | The tool call was allowed and executed. |
Deny | deny | reason, guard | The tool call was denied. |
Cancelled | cancelled | reason | The tool call was interrupted by explicit cancellation. |
Incomplete | incomplete | reason | The tool call did not reach a complete terminal result. |
Deny.guard holds the validation step that refused the call. Its doc comment reads "the guard or validation step that triggered the denial", and on the kernel path every deny site writes a scope literal fixed at that site: kernel, policy.deny, session_roots, finding_status, and their kin. The runtime names Guard::name() returns are kebab-case and appear in the reason string and in the guard_name of the matching evidence row, so join on evidence to find the guard.
TrustLevel
Records how the kernel mediated the call. The default is Mediated, and a receipt without the field deserializes to it.
pub enum TrustLevel {
/// Tool invocation was synchronously mediated by the kernel (the
/// strongest form: kernel observed the call inline and authorized it).
/// This is the default and the safest baseline.
#[default]
Mediated,
/// Authorization happened inline in the agent process (e.g. a
/// long-running orchestrator embedded the kernel via FFI). The kernel
/// observed the call but did not synchronously mediate it through a
/// separate trust boundary.
Verified,
/// Authorization was advisory only -- the kernel evaluated but the
/// caller may have proceeded regardless. Used for shadow-mode
/// integrations and observability-only deployments.
Advisory,
}GuardEvidence
Guards return zero or more evidence rows. The evidence vector is bound into the receipt ID and signature, and an empty vector is omitted during serialization. A missing row leaves open whether a guard ran: the pipeline stops at the first denial.
Guard Evidence defines row fields, runtime names, and producer behavior. A composed pipeline can synthesize a row for its denying child. The generic scoped external adapter carries a verdict, and an integration attaches provider findings explicitly.
Reserved metadata keys
metadata is a free-form JSON object with a reserved namespace. The kernel merges its typed blocks under these keys last and refuses a pre-existing collision from caller or hook metadata, so a block under a reserved key was written by the kernel and is covered by the receipt signature.
| Key | Constant | Schema | Block |
|---|---|---|---|
admission_ | ADMISSION_ | chio. | No doc comment on the constant. |
attribution | Receipt | None pinned | Universal receipt-side attribution for capability context. This metadata gives downstream analytics a deterministic local join path from a receipt to the capability subject and, when available, the matched grant within the capability scope. |
budget_ | BUDGET_ | None pinned | Budget- |
channel | CHANNEL_ | None pinned | Streamed-output channel accounting metadata block key. |
chio_ | CHIO_ | None pinned | No doc comment on the constant. |
delivery_ | DELIVERY_ | chio. | Delivery- |
financial | FINANCIAL_ | None pinned | Financial attribution and settlement metadata block key. |
finding_ | FINDING_ | chio. | Finding- |
finding_ | FINDING_ | chio. | Finding- |
governed_ | GOVERNED_ | None pinned | Governed- |
original_ | CHIO_ | None pinned | No doc comment on the constant. |
Two keys hold binding material rather than application blocks. chio_receipt_signing_nonce holds the caller-supplied nonce that bind_receipt_signing_nonce folds into the metadata before the id is computed, which is how a caller nonce reaches the signed bytes. original_metadata keeps a caller metadata value that is not a JSON object, so the nonce gets an object to live in and the original value survives. Both are written before chio_receipt_id runs, so both are inside the hash.
Canonical JSON
Receipts are signed over canonical JSON (RFC 8785, JCS), implemented in chio_core_types::canonical, in place of serde_json::to_string output.
- Object keys sort by UTF-16 code unit, which differs from byte and ASCII order.
- Numbers use the shortest representation matching ECMAScript
JSON.stringify(). - Strings use minimal escaping: only required characters are escaped.
- No whitespace separates tokens; the output is a single line.
The signature covers ChioReceiptSigningBody, which holds three things: the id, the 20-field ChioReceiptIdInput the id was derived from, and the bbs_signature when the receipt carries one. A verifier recomputes the id, compares it, and only then checks the signature, so a receipt whose id disagrees with its own body fails before any curve arithmetic runs.
The BBS material sits inside the signature. from_body_and_bbs takes it as a second argument at signing time, and verify_signature rebuilds the signing body with the receipt’s own bbs_signature, so selective-disclosure material cannot be attached to or stripped from a signed receipt. validate_bbs_receipt_binding runs first, and verify_signature returns Ok(false) whenever bbs_projection_version and bbs_signature disagree about whether BBS material is present or which projection version it claims.
A single-tenant receipt, with tenant_id set to None, omits the key from canonical JSON entirely, the general rule for the 9 omittable fields: an absent key and a null are different bytes. Each language’s chio SDK bundles a JCS implementation; use it so TypeScript, Python, Go, and Rust produce identical bytes for the same receipt body. The swarm artifacts that close a delegation graph reuse this canonical form over their own body rule, dropping only their signature field; Join & Terminal Receipts defines that closure.
SigningBackend
Receipt signing goes through a trait, so FIPS algorithms plug in without changes at call sites.
pub trait SigningBackend: Send + Sync {
/// Algorithm this backend produces.
fn algorithm(&self) -> SigningAlgorithm;
/// Public half of this backend's signing identity.
fn public_key(&self) -> PublicKey;
/// Produce a detached signature over `message`.
fn sign_bytes(&self, message: &[u8]) -> Result<Signature>;
/// Produce a detached signature over canonical JSON bytes.
fn sign_canonical_bytes(
&self,
canonical: &CanonicalBytes<CanonicalJsonWitness>,
) -> Result<Signature> {
self.sign_bytes(canonical.as_bytes())
}
}Ed25519Backend: the default, with no feature flag.P256Backend: behind thefipscrate feature.P384Backend: behind thefipscrate feature.
Verification reads the algorithm from the self-describing prefix on the PublicKey and Signature; the envelope algorithm field is informational. A build without the fips feature returns Ok(false) for P-256 and P-384 receipts and attempts no unsupported curve.
Merkle checkpoints
A checkpoint commits a batch to one root
The background signer reads a batch of canonical receipt bytes from the store, commits them to a Merkle root, and signs the checkpoint with the same kernel key. Verifiable evidence.
- receipt storeSQLite · chio-store-sqlite
Appends each signed receipt through
ReceiptStore::append_chio_receiptand keeps its canonical bytes by sequence number, so a batch can be rebuilt byte for byte. Checkpoints are written to the same store.Before export, the store remains under the producer’s control. It supplies no evidence to an outside verifier.
- checkpoint signerbackground · chio-store-sqlite
Every
checkpoint_batch_sizereceipts (100 by default; 0 turns it off), commits the batch’s canonical bytes to an RFC 6962 Merkle root and signs aKernelCheckpointwhose body carriesprevious_checkpoint_sha256and achain_root.With a checkpoint: whether a receipt was in its batch, from an inclusion proof against
merkle_root, and whether checkpoints chain.- kernel keyEd25519 by default
One keypair signs both receipts and checkpoints, and each signed body carries its public half as
kernel_key. FIPS builds sign with P-256 or P-384.The key identifies the signer. The recipient decides independently whether to trust it.
checkpoint.rs:139/// A signed kernel checkpoint statement.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KernelCheckpoint {
/// The signed body.
pub body: KernelCheckpointBody,
/// Ed25519 signature over canonical JSON of `body`.
pub signature: Signature,
}
After the verdict, the kernel builds the receipt body, computes its id with chio_receipt_id, signs it with ChioReceipt::sign_with_backend, and appends it with ReceiptStore::append_chio_receipt, which assigns its seq. Every checkpoint_batch_size receipts, MerkleTree::from_leaves builds the batch tree over their canonical bytes, and the signed KernelCheckpointBody is persisted with ReceiptStore::store_checkpoint.
The leaves are the canonical bytes of the receipts in the batch, so a checkpoint commits to exactly the bytes a verifier reproduces from the stored receipts. CHECKPOINT_SCHEMA, the identifier new issuance writes, is chio.checkpoint_statement.v2; chio.checkpoint_statement.v1 is the statement without a chain commitment, and is_supported_checkpoint_schema accepts both.
pub struct KernelCheckpointBody {
/// Schema identifier for new checkpoint issuance.
pub schema: String,
/// Monotonic checkpoint counter.
pub checkpoint_seq: u64,
/// First receipt seq in this batch.
pub batch_start_seq: u64,
/// Last receipt seq in this batch.
pub batch_end_seq: u64,
/// Number of leaves in the Merkle tree.
pub tree_size: usize,
/// Root from MerkleTree::from_leaves.
pub merkle_root: Hash,
/// Unix timestamp (seconds) when the checkpoint was issued.
pub issued_at: u64,
/// The kernel's signing key (public).
pub kernel_key: PublicKey,
/// Hash of the immediately preceding checkpoint body when this checkpoint extends a prior batch.
pub previous_checkpoint_sha256: Option<String>,
/// RFC 6962 root over the checkpoint-chain leaves for checkpoint_seq 1
/// through this checkpoint, one leaf per checkpoint binding its sequence,
/// entry range, and batch root (see [`checkpoint_chain_leaf_hash`]). This
/// is the commitment that consistency proofs verify against. Absent on
/// v1 checkpoints and on detached v2 checkpoints built without chain
/// context.
pub chain_root: Option<Hash>,
}The default batch size is DEFAULT_CHECKPOINT_BATCH_SIZE = 100: the kernel emits a checkpoint every hundred receipts unless the operator sets checkpoint_batch_size on the kernel config. A value of 0 installs no background signer, and prefix retention then archives only what a checkpoint already covers. Web3-enabled deployments refuse 0 with web3-enabled deployments require checkpoint_batch_size > 0.
The chain links two ways. previous_checkpoint_sha256 hashes the predecessor body, so a deletion or rewrite upstream invalidates every checkpoint after the gap. chain_root is the stronger form: an RFC 6962 root over one leaf per checkpoint, each binding that checkpoint’s sequence, entry range, and batch root, and the commitment consistency proofs verify against. Issuing one costs O(log n) hashes, because a long-lived writer keeps the frontier.
An inclusion proof is a ReceiptInclusionProof that names a checkpoint, a leaf index, and a sibling-hash path. The checkpoint statement is portable evidence that the kernel committed to a specific receipt set at a specific time; anchoring its signed root to a transparency log, a chain, or a notary is a separate operator step outside the kernel. Receipts replicate across a cluster while checkpoints stay per node, so a claim drawn from many nodes’ logs is narrower than one a node makes from its own checkpoints; Receipt Aggregation defines the difference.
Multi-tenant isolation
tenant_id on ChioReceipt is the isolation key, and two rules govern it.
- Derived from the session. The kernel sets
tenant_idfrom the authenticated session’s enterprise identity context, never from a caller-provided request field, since caller choice would defeat the isolation. - Scoped by an explicit read context. Whether a query sees untenanted rows (NULL
tenant_id) is governed byReceiptReadContext, whichReceiptQuerycarries asread_context. Itsinclude_null_tenantflag follows the context’s source. Both local-operator constructors set ittrue:local_operator_admin_all()(anAdminAllboundary) andlocal_operator_tenant(tenant)(aTenantScopedone). The remote-sourced constructors,admin_service()andauthenticated_tenant(tenant), keep itfalse, so an authenticated-tenant query sees no untenanted rows.
At the HTTP edge, resolve ReceiptReadContext from the authenticated session and treat tenant_filter as a narrowing filter. A boundary taken from a query parameter is a confused-deputy bug.
Storage and retention
ReceiptStore
The host persists receipts through ReceiptStore. Its append, query, checkpoint, and failure behavior is defined in Node State on Disk. A valid signature shows the record is intact; whether an append completed and the record is still retained is the store’s contract.
SqliteReceiptStore
SqliteReceiptStore keeps the receipt JSON beside indexed query columns, documented in the SQLite receipt schema. Verify the signed record; the columns serve lookup.
Retention
Retention changes where a signed record is found. Retention & Archive defines rotation, archive files, checkpoint alignment, and recovery. Keep the relevant checkpoint evidence with an archived receipt when a verifier needs inclusion in a retained history.
Verification
A verifier checks the signature, then the parameter hash, then the signing key it expected, and optionally inclusion in a checkpoint.
use chio_core_types::receipt::ChioReceipt;
let receipt: ChioReceipt = serde_json::from_str(json_blob)?;
// Step 1: signature.
assert!(receipt.verify_signature()?, "signature did not verify");
// Step 2: action hash matches parameters.
assert!(receipt.action.verify_hash()?, "parameter_hash drift");
// Step 3: expected kernel key.
assert_eq!(receipt.kernel_key, expected_kernel_pubkey);
// Step 4: optional, inclusion proof against a checkpoint root.
// Build a MerkleTree from the canonical bytes of the batch range,
// then verify ReceiptInclusionProof.path against batch_root.
println!("decision: {:?}", receipt.decision);
for ev in &receipt.evidence {
println!(" {} -> verdict={}", ev.guard_name, ev.verdict);
}Source
The field, decision, and metadata-key tables render from the receipt-fields dataset, read from crates/core/chio-core-types/src/receipt/ (body.rs, decision.rs, metadata.rs, and signing.rs) at the pinned commit. The rest follows crates/core/chio-core-types/src/canonical.rs, crates/kernel/chio-kernel/src/checkpoint.rs, crates/kernel/chio-kernel/src/receipt_query.rs, crates/kernel/chio-kernel/src/receipt_store.rs, and the SQLite bootstrap at crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rs.