Chio/Docs

PlatformContracts

Receipts & Audit

The kernel signs receipts for allowed, denied, canceled, and interrupted tool calls, then checkpoints them in a Merkle tree.

Source

Verified against crates/core/chio-core-types/src/receipt/body.rs, 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.

ChioReceipt

The signed wire format:

crates/core/chio-core-types/src/receipt/body.rs
pub struct ChioReceipt {
    pub id: String,                           // Content-addressed, from the canonical body
    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: Option<Decision>,           // Present only for mediated decisions
    pub receipt_kind: ReceiptKind,            // mediated_decision / trace_observation / advisory_evaluation
    pub boundary_class: BoundaryClass,        // prevent / detect_only / advisory_only / cannot_see
    pub observation_outcome: Option<ObservationOutcome>, // observed / evaluated / dropped
    pub tool_origin: ToolOrigin,              // Where the effect executed relative to Chio
    pub redaction_mode: RedactionMode,        // none / summary / redacted
    pub actor_chain: Vec<ActorRef>,           // Signed actor attribution chain
    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 bbs_projection_version: Option<String>, // Bound into id when BBS material is present
    pub kernel_key: PublicKey,                // For verification without lookup
    pub bbs_signature: Option<BbsReceiptSignature>, // BBS selective-disclosure material
    pub algorithm: Option<SigningAlgorithm>,  // Absent means Ed25519
    pub signature: Signature,                 // Over canonical body
}

Note decision is Option<Decision>: not every receipt carries a mediated verdict. Trace and advisory receipts record an outcome through receipt_kind, boundary_class, and observation_outcome instead. All of the semantic fields above are part of the signed body.

Decision

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

crates/core/chio-core-types/src/receipt/decision.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/core/chio-core-types/src/receipt/kinds.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/core/chio-core-types/src/receipt/metadata.rs
pub struct GuardEvidence {
    pub guard_name: String,           // Guard::name(), e.g. "forbidden-path"
    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, signature, and bbs_signature. The BBS material is excluded so the base signature commits to the receipt semantics while selective-disclosure material can be attached or stripped without breaking it. 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 uses a trait for receipt signing, allowing FIPS algorithms without changes at call sites:

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

    // Default impl delegates to sign_bytes over the canonical bytes.
    fn sign_canonical_bytes(
        &self,
        canonical: &CanonicalBytes<CanonicalJsonWitness>,
    ) -> Result<Signature> {
        self.sign_bytes(canonical.as_bytes())
    }
}

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

Any backend that implements ReceiptStore can persist receipts. The core append methods are:

crates/kernel/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 override only the data they persist. The kernel queries receipts via ReceiptQuery:

crates/kernel/chio-kernel/src/receipt_query.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>,  // Never authority on its own
    pub read_context: Option<ReceiptReadContext>, // Resolved read boundary
}

tenant_filter does not grant authority by itself. The explicit read_context — a boundary (AdminAll or TenantScoped), a source (LocalOperator, AdminService, or AuthenticatedTenant), and an include_null_tenant flag — is what authorizes and shapes the query. Remote and control-plane callers construct it from authenticated context before the query reaches the store. See Multi-Tenant Isolation below.


SqliteReceiptStore

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

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.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 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/kernel/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>,
    pub check_interval_secs: u64,  // Default: 3600 (rotation-evaluation cadence)
    pub explicit_cutoff_unix_secs: Option<u64>,  // Internal, no wire form
}

A kernel maintenance worker evaluates rotation every check_interval_secs (default hourly). 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/kernel/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.
  • Scoped by an explicit read context. Whether a query sees untenanted (NULL tenant_id) rows is governed by ReceiptReadContext, which ReceiptQuery carries as read_context. Its include_null_tenant flag tracks the context's source, not its boundary. Both local-operator constructors set it true local_operator_admin_all() (an AdminAll boundary) and local_operator_tenant(tenant) (a TenantScoped one) alike. The remote-sourced constructors keep it false: admin_service() and authenticated_tenant(tenant). So an authenticated-tenant query does not see legacy NULL-tenant rows: NULL-row visibility follows the read context's source, an explicit decision, not a blanket tenant_filter behavior.

HTTP edge resolves the read context

At the HTTP edge, resolve ReceiptReadContext from the authenticated session, not from a query parameter, and treat tenant_filter as a narrowing filter that is never authority on its own. A caller-supplied boundary 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