Chio/Docs

Receipts & Audit

Every tool call that reaches the kernel produces a signed receipt, whether the call was allowed, denied, cancelled, or interrupted. Receipts are the immutable audit trail. They carry the policy hash applied, per-guard evidence, the kernel signing key, and an algorithm envelope. Periodic Merkle checkpoints commit batches of receipts to a root that can be anchored out of band so deletion or rewriting is detectable.

Source

Verified against crates/chio-core-types/src/receipt.rs, crates/chio-core-types/src/canonical.rs, crates/chio-kernel/src/checkpoint.rs, crates/chio-kernel/src/receipt_store.rs, and the SQLite bootstrap at crates/chio-store-sqlite/src/receipt_store/bootstrap.rs.

ChioReceipt

The signed-on-the-wire shape:

crates/chio-core-types/src/receipt.rs
pub struct ChioReceipt {
    pub id: String,                           // UUIDv7 recommended
    pub timestamp: u64,                       // Unix seconds
    pub capability_id: String,                // The capability that was exercised
    pub tool_server: String,
    pub tool_name: String,
    pub action: ToolCallAction,               // Parameters + parameter_hash
    pub decision: Decision,                   // The kernel verdict
    pub content_hash: String,                 // SHA-256 of evaluated content
    pub policy_hash: String,                  // SHA-256 of the applied policy
    pub evidence: Vec<GuardEvidence>,         // Per-guard records
    pub metadata: Option<serde_json::Value>,  // Stream / accounting details
    pub trust_level: TrustLevel,              // Mediation strength
    pub tenant_id: Option<String>,            // Multi-tenant isolation
    pub kernel_key: PublicKey,                // For verification without lookup
    pub algorithm: Option<SigningAlgorithm>,  // Absent means Ed25519
    pub signature: Signature,                 // Over canonical body
}

Decision

The receipt's decision is a four-variant enum. Note this differs from the kernel's in-flight Verdict (Allow / Deny / PendingApproval): the receipt records the terminal outcome, which includes interruption and incomplete states.

crates/chio-core-types/src/receipt.rs
pub enum Decision {
    Allow,
    Deny { reason: String, guard: String },
    Cancelled { reason: String },
    Incomplete { reason: String },
}

On the wire the variant is tagged verdict with snake_case values: allow, deny, cancelled, incomplete.

TrustLevel

Records how the kernel mediated the call. Defaults to Mediated; older receipts without the field deserialize to this default.

crates/chio-core-types/src/receipt.rs
pub enum TrustLevel {
    Mediated,    // Synchronous in-line mediation (strongest, default)
    Verified,    // Embedded kernel observed the call but did not mediate
                 // through a separate trust boundary
    Advisory,    // Shadow-mode / observability-only; caller may have
                 // proceeded regardless of the verdict
}

GuardEvidence

Each guard that ran during evaluation contributes one record:

crates/chio-core-types/src/receipt.rs
pub struct GuardEvidence {
    pub guard_name: String,           // e.g. "ForbiddenPathGuard"
    pub verdict: bool,                // true = passed, false = denied
    pub details: Option<String>,      // Optional diagnostic
}

External-guard adapters serialize their structured evidence (Bedrock category breakdown, VirusTotal detection counts, etc.) into details as a JSON string. The shape of that JSON is owned by the provider crate and versioned there.


Canonical JSON

Receipts are signed over canonical JSON (RFC 8785 / JCS), not arbitrary serde_json::to_string output. The implementation lives in chio_core_types::canonical. The rules:

  • Object keys are sorted by UTF-16 code unit comparison (not byte or ASCII order).
  • Numbers use the shortest representation matching ECMAScript JSON.stringify().
  • Strings use minimal escaping. Only required characters are escaped.
  • No whitespace between tokens. The output is a single line.

The signing input is ChioReceiptBody: every field of ChioReceipt except algorithm and signature. Single-tenant receipts (where tenant_id is None) skip that field from canonical JSON to remain byte-identical with pre-multitenant receipts.

Do not roll your own canonicalization

TypeScript, Python, Go, and Rust must all produce identical bytes for the same logical receipt body. Use the JCS implementation bundled with each language's chio SDK rather than your runtime's default JSON serializer.

SigningBackend

The kernel signs receipts through a trait so FIPS algorithms slot in without changing call sites:

crates/chio-core-types/src/crypto.rs
pub trait SigningBackend: Send + Sync {
    fn algorithm(&self) -> SigningAlgorithm;
    fn public_key(&self) -> PublicKey;
    fn sign(&self, message: &[u8]) -> Result<Signature, Error>;
}

Three implementations:

  • Ed25519Backend · default, no feature flag required.
  • P256Backend · gated on the fips crate feature.
  • P384Backend · gated on the fips crate feature.

Verification reads the algorithm off the self-describing prefix on the PublicKey and Signature; the envelope algorithm field is informational. Building without the fips feature causes P-256 / P-384 receipts to fail verification with Ok(false) rather than attempting an unsupported curve.


ReceiptStore Trait

Receipts land in any backend that satisfies ReceiptStore. The core append surface:

crates/chio-kernel/src/receipt_store.rs
pub trait ReceiptStore: Send + Sync {
    fn append_chio_receipt(&self, receipt: &ChioReceipt) -> Result<(), ReceiptStoreError>;
    fn append_chio_receipt_returning_seq(
        &self,
        receipt: &ChioReceipt,
    ) -> Result<Option<u64>, ReceiptStoreError> {
        self.append_chio_receipt(receipt)?;
        Ok(None)
    }
    fn append_child_receipt(&self, receipt: &ChildRequestReceipt) -> Result<(), ReceiptStoreError>;

    // Checkpointing
    fn store_checkpoint(&self, _checkpoint: &KernelCheckpoint) -> Result<(), ReceiptStoreError>;
    fn load_checkpoint_by_seq(&self, _seq: u64) -> Result<Option<KernelCheckpoint>, ReceiptStoreError>;
    fn load_latest_checkpoint(&self) -> Result<Option<KernelCheckpoint>, ReceiptStoreError>;

    // Capability lineage, federated evidence, and lineage statements omitted here.
}

Most trait methods have default implementations so backends only override what they actually persist. The full kernel queries receipts via ReceiptQuery:

crates/chio-kernel/src/receipt_store.rs
pub struct ReceiptQuery {
    pub capability_id: Option<String>,
    pub tool_server: Option<String>,
    pub tool_name: Option<String>,
    pub outcome: Option<String>,    // "allow" | "deny" | "cancelled" | "incomplete"
    pub since: Option<u64>,         // Unix seconds, inclusive
    pub until: Option<u64>,         // Unix seconds, inclusive
    pub min_cost: Option<u64>,      // Minor units
    pub max_cost: Option<u64>,
    pub cursor: Option<u64>,        // Forward pagination, exclusive
    pub limit: usize,               // Capped at MAX_QUERY_LIMIT (200)
    pub agent_subject: Option<String>,  // Hex-encoded subject key
    pub tenant_filter: Option<String>,
}

SqliteReceiptStore

The production reference implementation lives in chio-store-sqlite. It owns an r2d2 connection pool and a strict-tenant-isolation flag. The schema for tool receipts:

crates/chio-store-sqlite/src/receipt_store/bootstrap.rs
CREATE TABLE chio_tool_receipts (
    seq INTEGER PRIMARY KEY AUTOINCREMENT,
    receipt_id TEXT NOT NULL UNIQUE,
    timestamp INTEGER NOT NULL,
    capability_id TEXT NOT NULL,
    subject_key TEXT,
    issuer_key TEXT,
    grant_index INTEGER,
    tool_server TEXT NOT NULL,
    tool_name TEXT NOT NULL,
    decision_kind TEXT NOT NULL,
    policy_hash TEXT NOT NULL,
    content_hash TEXT NOT NULL,
    raw_json TEXT NOT NULL
);

CREATE INDEX idx_chio_tool_receipts_timestamp ON chio_tool_receipts(timestamp);
CREATE INDEX idx_chio_tool_receipts_capability ON chio_tool_receipts(capability_id);
CREATE INDEX idx_chio_tool_receipts_subject ON chio_tool_receipts(subject_key);
CREATE INDEX idx_chio_tool_receipts_grant ON chio_tool_receipts(capability_id, grant_index);
CREATE INDEX idx_chio_tool_receipts_tool ON chio_tool_receipts(tool_server, tool_name);
CREATE INDEX idx_chio_tool_receipts_decision ON chio_tool_receipts(decision_kind);

The full canonical receipt JSON lives in raw_json; the columnar fields exist for query performance only. The store rebuilds receipts from raw_json on read and re-verifies signatures.


Retention

Retention is a runtime concern, not a database trigger. The kernel uses RetentionConfig to rotate receipts older than retention_days or larger than max_size_bytes:

crates/chio-kernel/src/receipt_store.rs
pub struct RetentionConfig {
    pub retention_days: u64,   // Default: 90
    pub max_size_bytes: u64,   // Default: 10 GB (10_737_418_240)
    pub archive_path: String,  // Default: "receipts-archive.sqlite3"
    pub tenant_id: Option<String>,
}

Rotation moves aged-out receipts into a separate read-only SQLite archive while keeping their checkpoint relationships intact, so an archived receipt is still verifiable against the Merkle root that committed it. Per-tenant rotation is a separate concern: setting tenant_id on the config scopes the rotation to one tenant's evidence and leaves others untouched.


Merkle Checkpoints

Periodically the kernel batches receipts into a Merkle tree and signs the root. The schema is chio.checkpoint_statement.v1.

crates/chio-kernel/src/checkpoint.rs
pub struct KernelCheckpointBody {
    pub schema: String,                    // "chio.checkpoint_statement.v1"
    pub checkpoint_seq: u64,               // Monotonic counter
    pub batch_start_seq: u64,
    pub batch_end_seq: u64,
    pub tree_size: usize,
    pub merkle_root: Hash,
    pub issued_at: u64,
    pub kernel_key: PublicKey,
    pub previous_checkpoint_sha256: Option<String>,
}

Default batch size is DEFAULT_CHECKPOINT_BATCH_SIZE = 100; the kernel emits a checkpoint every 100 receipts unless the operator overrides checkpoint_batch_size on kernel config. A value of 0 disables checkpointing entirely (web3 deployments forbid this).

The chain links via previous_checkpoint_sha256: each checkpoint hashes its predecessor, so any deletion or rewriting upstream invalidates every checkpoint downstream of the gap. tree_size is monotonic; auditors can verify leaf-count continuity by walking the chain.

rendering…
Receipt path: kernel evaluates, signs, persists, then commits a Merkle batch on the configured cadence.

Inclusion proofs are ReceiptInclusionProof records that name a checkpoint, a leaf index, and a sibling-hash path. Anchoring the signed root to a transparency log, blockchain, or notary service is out of band: the checkpoint statement is portable evidence that the kernel committed to a specific receipt set at a specific time.


Multi-Tenant Isolation

tenant_id on ChioReceipt is the isolation key. Two rules govern its lifecycle:

  • Derived, not declared. The kernel sets tenant_id from the authenticated session's enterprise identity context. It is never taken from a caller-provided field on the request, because caller choice would defeat the isolation intent.
  • Visible in queries via tenant_filter. A query with tenant_filter = Some(id) returns rows where tenant_id = id OR tenant_id IS NULL, keeping legacy pre-multitenant receipts visible. Strict mode (SqliteReceiptStore::with_strict_tenant_isolation(true)) excludes the NULL fallback set.

HTTP edge derives tenant_id

At the HTTP edge, derive tenant_filter from the authenticated session, not from a query parameter. A caller-supplied filter is a confused-deputy bug.

Worked Example: Verify a Receipt

rust
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);
}

Next Steps

Receipts & Audit · Chio Docs