Chio/Docs
LOGIN · JOIN

PlatformShared State

Cluster

Receipt Aggregation

Receipts replicate as append-only streams, lineage as an upsert, checkpoints not at all. What a fleet can prove from that, and what it cannot.

Where the neighbours stop

Receipts & Audit owns the receipt itself: the signed body, canonical JSON, the signing backend, and how a Merkle checkpoint commits a batch. Retention & Archive owns the only path that removes one from a live database, and the checkpoint-aligned watermark that path needs. Replication & Convergence owns the pull round that moves rows between nodes. This page owns what those streams add up to across many nodes under one authority, and the published ceiling on the answer. The package that carries the result off the cluster is Evidence Export.

One log per node, no log for the fleet

The bounded operational profile files the receipt and checkpoint plane at guarantee class local-only, and states the shipped truth and the non-claim in the same row.

PlaneGuarantee classShipped truthExplicit non-claim
Receipt and checkpoint planelocal-onlySigned local audit evidence, immutable local checkpoints, local continuity summaries, and inclusion proofs over checkpointed claim-log batches that may contain tool and child receipts.No public transparency-log, cross-node append-only coverage, or strong non-repudiation semantics.

Read the fourth column as the boundary of this page. Receipts replicate; checkpoints do not. The shared per-peer pull round names its streams by function pointer, and none of them is a checkpoint:

crates/platform/chio-control-plane/src/trust_control/cluster/deltas.rsrust
fn peer_pullers() -> [Puller; 4] {
    [
        sync_peer_budgets,
        sync_peer_tool_receipts,
        sync_peer_child_receipts,
        sync_peer_lineage,
    ]
}

Revocations replicate on their own round budget, and the capability authority arrives as a full snapshot. The word checkpoint does not appear in the trust-control router or its path constants at all, and nothing in the sync loop reads or writes kernel_checkpoints on a peer. A follower re-appends each replicated receipt into its own claim log at its own entry sequence and, if it checkpoints at all, signs with its own kernel key. Log identity is derived from that key:

crates/kernel/chio-kernel/src/checkpoint.rsrust
#[must_use]
pub fn checkpoint_log_id(checkpoint: &KernelCheckpoint) -> String {
    let log_key_bytes: Vec<u8> = match checkpoint.body.kernel_key.algorithm() {
        SigningAlgorithm::Ed25519 => checkpoint.body.kernel_key.as_bytes().to_vec(),
        SigningAlgorithm::P256 | SigningAlgorithm::P384 | SigningAlgorithm::Hybrid => {
            checkpoint.body.kernel_key.to_hex().into_bytes()
        }
    };
    format!("local-log-{}", sha256_hex(&log_key_bytes))
}

Two nodes holding the same set of receipts therefore produce two logs, not one, with different sequence numbers over different orderings and different roots. Aggregation across a cluster is aggregation of receipts, not of logs.


What is on disk, and which of it moves

ObjectTableReplicated by
Tool receipt, signed by the kernel that mediated the callchio_tool_receipts/v1/internal/receipts/tools/delta
Child receipt, one governed sub-request under a parentchio_child_receipts/v1/internal/receipts/children/delta
Capability lineage snapshotcapability_lineage/v1/internal/lineage/delta
Claim-log entry: the single per-node ordering a checkpoint commits overclaim_receipt_log_entriesNothing. It is a trigger-written projection, rebuilt locally on each node.
Signed kernel checkpoint, its publication metadata, and its optional trust-anchor bindingkernel_checkpoints, checkpoint_publication_metadata, checkpoint_publication_trust_anchor_bindingsNothing. No delta route exists.
Imported bilateral evidence sharefederated_share_tool_receipts, federated_share_capability_lineagePOST /v1/evidence/import, into tables held apart from the local ones.

The claim log is the join that makes checkpointing possible over two receipt families at once. Both receipt tables project into it through AFTER INSERT triggers, under one autoincrementing sequence and one uniqueness constraint on the receipt id:

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rssql
CREATE TABLE IF NOT EXISTS claim_receipt_log_entries (
    entry_seq INTEGER PRIMARY KEY AUTOINCREMENT,
    receipt_id TEXT NOT NULL UNIQUE,
    receipt_kind TEXT NOT NULL,
    source_seq INTEGER NOT NULL,
    timestamp INTEGER NOT NULL,
    ...
    raw_json TEXT NOT NULL,
    CHECK (receipt_kind IN ('tool_receipt', 'child_receipt'))
);

A checkpoint batch is a contiguous range of entry_seq, so a single Merkle tree can carry tool and child leaves together. validate_checkpoint_against_claim_log reloads exactly that range, rebuilds the tree, and refuses any checkpoint whose tree_size or merkle_root disagrees with the rows on disk.

Three evidence classes

Every node and edge in the lineage projection carries one of three classes. chio-lineage defines them as EvidenceClass, whose doc comment names spec/PROTOCOL.md as the source of the values; the receipt and capability types carry the parallel GovernedProvenanceEvidenceClass (aliased ProvenanceEvidenceClass) from chio-core-types. Read the third column as what the ingest paths actually emit, not as what the class names could cover.

ClassWhat earns itExample
AssertedCaller-supplied or imported attributes this kernel neither signed nor checked.Every tool-call and capability node folded from an OTEL exporter frame, and a delegated call_chain field on an outward report.
ObservedLocal runtime truth: rows this store itself wrote.Replay-corpus receipt rows, and the receipt node plus its ToolCallToReceipt edge when an OTEL frame carries correlation_source_chio_receipt_id. The tool-call node on that same frame stays Asserted.
VerifiedIndependently signed or proof-checked.One case in the crate: a ReceiptLineageParent edge in the replay-corpus ingest whose signed statement verifies.

Verified is narrower than the class name suggests

The module doc lists signed session anchors, verified continuation tokens, and checked checkpoint or anchor proofs as things that would earn Verified. No ingest path emits it for any of them. Outside tests, EvidenceClass::Verified is written in exactly one place in chio-lineage: ingest_replay_corpus.rs. Treat the rest of that list as reserved vocabulary.

That one upgrade is a single predicate, and it checks the binding as well as the signature. It runs over replay-corpus fixture rows, not over the live store:

crates/observability/chio-lineage/src/ingest_replay_corpus.rsrust
fn signed_lineage_statement_verifies(row: &CorpusReceiptRow, parent: &str) -> bool {
    let Some(statement) = row.signed_lineage_statement.as_ref() else {
        return false;
    };
    statement.parent_receipt_id == parent
        && statement.child_receipt_id == row.receipt_id
        && statement.evidence_class == ProvenanceEvidenceClass::Verified
        && matches!(statement.verify_signature(), Ok(true))
}

A statement that fails any of the four conjuncts leaves the edge Observed. Nothing in the ingest path promotes on the strength of the declared class alone.


From one append to one bundle

rendering
One receipt, from the node that mediated the call to an evidence bundle. The dashed edge is the only one that crosses a node boundary; checkpoints and inclusion proofs are rebuilt per node.
sourcecrates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rs:1060-1175at fe56570

Append, forward, re-read

POST /v1/receipts/tools, POST /v1/receipts/children, and POST /v1/lineage all open the same way: validate the service token, then hand the body to forward_post_to_leader. A follower forwards and returns the leader response verbatim; the leader writes locally and then runs respond_after_leader_visible_write, which re-reads under an admin read context and answers 500 with tool receipt was not visible on the leader after write when the read comes back empty. That re-read is not a point-load: it is a filtered list bounded to MAX_LIST_LIMIT rows, ordered seq DESC and scanned for the id, so a burst of more than 200 newer receipts matching the same capability, tool, and decision between the write and the re-read would report a durable write as a 500. It is one re-read, not a poll. The forwarding rules, the quorum and lease checks in front of them, and the 503 catalogue belong to Leader & Failover.

Every receipt append verifies before it queues. append_verified_chio_receipt_record calls ensure_chio_receipt_verified first, so a receipt whose signature or action parameter hash does not check never reaches the commit actor. That applies to receipts arriving over the wire from a peer exactly as it applies to locally minted ones. Read the scope of that check precisely: it verifies the receipt against the key the receipt itself carries, at the ReceiptCryptoFloor::AllowHybrid floor, with no policy handle in scope. It does not check that key against a trust anchor or an issuer allowlist. Whoever clears Cluster Peer Auth can push any internally consistent self-signed receipt into a follower’s claim log.

Replicate, then rebuild

A pull round applies each receipt page through the same append_chio_receipt and append_child_receipt the public routes use, so a replicated receipt is signature-checked on arrival, takes a fresh local sequence, and fires the claim-log trigger on the follower. The lineage stream is different: sync_peer_lineage calls upsert_capability_snapshot, which runs validate_snapshot_for_transport and, against an existing row, ensure_snapshots_compatible. Neither verifies a signature. Three consequences follow.

  • The streams are non-dense. Receipt sequences are AUTOINCREMENT written with ON CONFLICT DO NOTHING and pruned by retention, so gaps are legitimate. The page guard is require_forward_progress, not contiguity. Lineage paginates on the capability_lineage rowid, which is non-dense for its own reason: an upsert on an existing capability keeps its rowid, and deletes leave holes. A follower cannot distinguish a row that was pruned from a row it never received, which is why cross-node append-only coverage is a non-claim rather than a bug.
  • The delta stream is admin-scoped. Both receipt delta handlers read with ReceiptReadContext::admin_service() and are gated only by validate_cluster_peer_auth. Tenant isolation is a property of the read routes below, not of the wire between peers, which is why Cluster Peer Auth is load-bearing here.
  • Ordering is local. Two nodes that receive the same receipts in different orders assign different entry_seq values, so their checkpoint batch boundaries and roots differ even when their row sets converge.

Checkpoint, where a signer exists

The background signer is installed by the kernel at attach, when checkpoint_batch_size is greater than zero and the store reports checkpoint support. DEFAULT_CHECKPOINT_BATCH_SIZE is 100; a value of 0 disables automatic checkpointing, and the attach then refuses any retention policy, because a retention watermark can only advance to a checkpoint boundary. A web3-enabled deployment refuses 0 outright, through validate_web3_evidence_prerequisites. The builder is incremental: it loops while claim_log_max_seq - checkpointed_entry_seq >= max_batch, rebuilding one bounded batch at a time from the cached head rather than re-verifying the chain, and each batch passes ensure_claim_log_range_contiguous before it is built, so a hole in the claim log stops the chain rather than being checkpointed over.

The trust-control service installs no signer. Every handler opens the store with a bare SqliteReceiptStore::open(path). A control-plane node that only replicates therefore accumulates receipts and produces no checkpoints of its own; checkpoints on that file come from a kernel attached to it, or from chio receipt checkpoint create --kernel-seed-file. Plan the fleet accordingly: chio receipt checkpoint status reports the gap, and chio receipt audit runs the claim-log projection check plus a full checkpoint-chain verification.

The operator verbs do not reach a cluster

receipt audit, receipt checkpoint status, and receipt checkpoint create all route through local_receipt_db_path, which rejects a configured --control-url with remote receipt operator operations are not supported in this release and then requires an existing --receipt-db file. Checkpoint operations are per-file, run on the box that holds the database. Their --max-batch defaults to 1024, which is not the kernel’s 100: create run by hand builds a differently shaped batch than the background signer would have.

Both readings against a database holding two receipts and no checkpoint yet. next_range is the batch each verb would build next, which is why the two disagree: status reports the whole uncheckpointed prefix and audit reports the first batch it would verify.

read the gapbash
chio --receipt-db ./.chio/receipts.db receipt checkpoint status
chio --receipt-db ./.chio/receipts.db receipt audit
chio receipt checkpoint status and chio receipt audit stdoutbash
$ chio --receipt-db ./.chio/receipts.db receipt checkpoint status
status: healthy
committed_entry_seq: 2
checkpoint_seq: none
checkpointed_entry_seq: 0
next_range: 1..=2
retention_watermark_entry_seq: none

$ chio --receipt-db ./.chio/receipts.db receipt audit
status: healthy
committed_entry_seq: 2
checkpoint_seq: none
checkpointed_entry_seq: 0
next_range: 1..=1
retention_watermark_entry_seq: none

Point either verb at a control plane and it refuses rather than proxying, which is the callout above as the operator meets it:

chio receipt audit against a control URLbash
$ chio --control-url https://ctl-a.internal:8940 \
    --receipt-db ./.chio/receipts.db receipt audit
error [urn:chio:error:cli:other]: receipt audit requires local --receipt-db; remote receipt operator operations are not supported in this release
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.
exit=1

Roll up

Reads resolve a principal before they touch a store, and the resolved ReceiptReadContext is what the SQL filters on. Tenancy & Read Scoping owns that resolution. What matters for aggregation is which rollups a tenant token can reach at all.

RollupRoutePrincipal
Cursor-paged receipt query, filters on capability, tool, outcome, cost, subject, and timeGET /v1/receipts/queryAdmin or tenant. Two clamps: the route applies list_limit (DEFAULT_LIST_LIMIT = 50, MAX_LIST_LIMIT = 200), then the store clamps again at MAX_QUERY_LIMIT = 200.
Per-agent receipts, the same query with agentSubject pinned from the pathGET /v1/agents/{subject_key}/receiptsAdmin or tenant.
Point-load one receipt by idGET /v1/receipts/tools?receiptId=Admin only: a by-id load is not tenant-scoped, so a tenant token is refused 403.
Child receipt listingGET /v1/receipts/childrenAdmin only, until child receipts carry tenant attribution.
Aggregates: summary plus by agent, by tool, and by hour or day bucketGET /v1/receipts/analyticsAdmin only.
Operator report and comptroller reportGET /v1/reports/operator, /v1/reports/comptroller-surfaceAdmin only.
Capability lineage snapshot and full delegation chainGET /v1/lineage/{capability_id} and /chainAdmin service token. These two go through validate_service_auth, so a tenant read token is not accepted at all.
Evidence bundle exportPOST /v1/evidence/exportAdmin, or a tenant token whose query is narrowed to its own boundary before the store opens.
Bilateral evidence importPOST /v1/evidence/importAdmin service token, and it forwards to the leader like any other write.

Analytics returns counts by decision plus three derived ratios: reliability as allows over terminal outcomes (allow plus cancelled plus incomplete, denials excluded), compliance as non-denials over total, and budget utilization as charged over charged plus attempted. Each is None rather than zero when its denominator is zero. The grouped dimensions are truncated top-N lists, not complete ones: group_limit defaults to 50 and clamps at MAX_ANALYTICS_GROUP_LIMIT = 200 per dimension, and the time bucket defaults to a day. Only summary covers every matching row. Every one of those numbers is computed over the rows on the node answering the request, which for a clustered node means the rows its own pull rounds have caught up on.

chio-lineage performs no file or network I/O; it folds frames and rows already read from disk into an in-memory graph, and chio lineage query|diff|roots reads JSON dumps rather than a live cluster. The persistent recursive-CTE walk lives in chio-store-sqlite/src/lineage_cte.rs behind a lineage cargo feature that is off by default, so a stock build of the receipt store does not carry it. Both paths carry the same DEFAULT_DEPTH_LIMIT = 20 constant, but neither enforces it on its own: the in-memory walk takes a QueryBounds whose default also caps rows at 10,000 and returns a typed TruncationMarker, while the CTE takes depth and row caps as arguments and returns a plain truncated boolean alongside depth_reached. A deep delegation graph returns a bounded answer that says it is bounded, in two different shapes.

Prove

Inclusion proofs are not stored. build_evidence_export_bundle rebuilds the tree from receipts_canonical_bytes_range over the checkpoint’s full claim-log range, child leaves included, and reads the leaf index for each selected tool receipt off it. That is the aggregation step that matters here: a proof is only as wide as one node’s claim log, and anything outside a checkpointed range is named in uncheckpointed_receipts rather than dropped.

Every live export reports transparency_preview

publication_state is trust_anchored only when every publication in the set carries a validated trust-anchor binding sharing one trust_anchor_ref. Those bindings are read from checkpoint_publication_trust_anchor_bindings, and nothing outside tests writes that table: no control-plane route, no CLI verb, and no call to record_checkpoint_publication_trust_anchor_binding anywhere in crates/. A bundle from POST /v1/evidence/export therefore always reports transparency_preview, with the reason stated in the artifact: no trust anchor is attached, so log identity and prefix growth remain transparency-preview claims. The one non-test producer of a trust-anchored publication in the tree is chio-mercury’s assurance-release command, which rewrites a proof-package file rather than a node’s own export.

Child receipts get a second, blunter bound before any of that, decided by the query alone and recorded in the manifest as one of three scopes. That trichotomy, the package format, the offline verifier, and the import path are Evidence Export.


Anchoring is a separate lane

Turning a local checkpoint into evidence a third party can check without trusting the operator is the job of chio-anchor. It projects a KernelCheckpoint into a Web3CheckpointStatement, assembles an inclusion proof straight from an evidence bundle, and normalizes multi-lane proofs across an EVM root registry, a Bitcoin OpenTimestamps super-root, and a Solana memo. Its verifiers are fail-closed in the directions that matter here: build_anchor_inclusion_proof_from_evidence_bundle refuses a receipt listed in uncheckpointed_receipts, and requires exactly one matching receipt, one inclusion proof, and one checkpoint; verify_checkpoint_publication_records rejects a publication that carries neither a trust-anchor binding nor a successor witness. The economic and settlement side of that lane is On-Chain Settlement.

Anchoring runs outside the serving path

chio-anchor is a library an operator drives, not a service the cluster runs. No node, cluster, or control-plane process publishes a checkpoint to a chain while serving traffic, so a receipt is durable and verifiable the moment it is signed and gains a chain witness only when someone runs the anchoring job. Size your recovery story on the local record, and treat the chain witness as a later, coarser confirmation.

No report may collapse asserted into verified

The rule is written into the profile, not left to reviewer discipline:

docs/standards/CHIO_BOUNDED_OPERATIONAL_PROFILE.mdtext
- delegated `call_chain` fields remain `asserted` on outward surfaces unless
  Chio can back them with observed local lineage or verified signed provenance
  artifacts
- no report or export surface may collapse `asserted` lineage into `verified`
  truth
- checkpoint continuity records support local audit and
  `transparency_preview` claims only; current checkpoints are built over the
  local claim log, so child receipts with persisted canonical bytes can be
  covered without turning the local log into public transparency
- child receipt inclusion-proof export remains deferred unless an evidence
  package explicitly exports child receipt proof rows

The authorization-context report obeys it structurally rather than by convention: it counts asserted_call_chain_receipts, observed_call_chain_receipts, and verified_call_chain_receipts as three separate summary fields, alongside counts of how many receipts carried a session anchor, a request-lineage record, and a receipt-lineage statement. A call_chain_receipts total sits beside them, but it is a count of receipts carrying any class at all: nothing sums asserted into verified, and a reader who wants a verified count reads the verified field. The profile files that whole report family at informational-only: a derived projection over signed receipt metadata, which does not upgrade asserted call-chain fields into verified upstream truth.


Guarantees and limits

StatusClaimEvidence
ShippedTool receipts, child receipts, and capability lineage replicate to every configured peer as cursor-paged streams. The two receipt streams are signature-verified again on arrival, against the key each receipt carries. The lineage stream is an upsert checked for transport validity and field compatibility, not for a signature.peer_pullers in cluster/deltas.rs; ensure_chio_receipt_verified and ensure_child_receipt_verified ahead of every write job; upsert_capability_snapshot; test append_child_receipt_rejects_invalid_signature
ShippedOne claim log per node orders both receipt families, and a checkpoint commits a contiguous range of it under a signed Merkle root chained to its predecessor digest.claim_receipt_log_entries and its two projection triggers; build_checkpoint_with_previous, validate_checkpoint_predecessor
ShippedAn evidence export refuses to run without an explicit read boundary, names every receipt it could not prove, and can be configured to fail rather than ship an unproved one.EvidenceExportQuery::validate_read_boundary; uncheckpointed_receipts; validate_evidence_bundle_requirements
Proved by testTwo checkpoints that claim the same checkpoint_seq, fork the same log at the same cumulative tree size, or cite the same predecessor digest, are detected as equivocation and reject the whole set. The first kind does not compare log ids, so feeding one summary the checkpoints of two nodes reads as equivocation rather than as two logs. Another reason not to merge them.detect_checkpoint_equivocation_reports_conflicting_sequence, checkpoint_rejects_same_log_same_tree_size_fork, validate_checkpoint_transparency_rejects_predecessor_fork
Proved by testEvery leaf of a 100-receipt batch produces a verifying inclusion proof, and tampered bytes do not verify against the same root.inclusion_proof_all_100_leaves_verify, inclusion_proof_tampered_bytes_fail
Proved by testA tenant-scoped export sees only its own receipts and omits child receipts entirely, and a bundle carrying an uncheckpointed receipt cannot be projected into an anchor inclusion proof.tenant_scoped_evidence_export_cannot_see_other_tenants, tenant_scoped_evidence_export_omits_child_receipts_without_tenant_join, evidence_bundle_rejects_uncheckpointed_receipts
ModeledThe anchor emergency-control predicates, lane classification, indexer cursor arithmetic, and the witness-policy algebra are model-checked over bounded symbolic inputs. verify_proof_bundle is explicitly out of symbolic scope and is covered by a fuzz target and integration tests instead.chio-anchor/src/kani_public_harnesses.rs scope note; chio-anchor/src/fuzz.rs
PlannedA persisted transparency log id. Today log_id is derived from the checkpoint signing key, and its field doc says so: it stands in "until an explicit persisted transparency log ID is available". Read as intent, not as a scheduled item: no RFC or ADR number backs it.Field doc on CheckpointPublication::log_id
Not wiredTrust-anchored publication on a node’s own export. The binding type, its validation, and the summary path that consumes it all ship, but no route, CLI verb, or non-test call site writes a binding, so a live export never leaves transparency_preview. publication_profile_version is a free-form operator string with no constant behind it: phase4-preview.v1 appears only in test fixtures, and the one non-test producer stamps chio.mercury.trust_network.append_only.v1.No caller of record_checkpoint_publication_trust_anchor_binding outside tests; chio-mercury assurance-release trust_network.rs
PlannedChild-receipt inclusion-proof export. Child receipts are checkpointed as claim-log leaves, but collect_inclusion_proofs_for_export emits proof rows for tool receipts only.Profile, bounded receipt semantics; chio-store-sqlite/src/evidence_export.rs
UnsupportedCross-node append-only coverage. Receipt sequences are non-dense by construction and retention prunes them, so a follower cannot distinguish a pruned row from a row it never received, and no guard tries to.Profile non-claim; require_forward_progress in sync_peer_tool_receipts and sync_peer_child_receipts
UnsupportedA public transparency log, and strong non-repudiation. Checkpoints are local, their log identity is a signing key, and nothing publishes or witnesses them off the node that built them.Profile non-claim; checkpoint_log_id; the absence of any checkpoint delta route
UnsupportedA fleet-wide linearizable view. A report answers from the node that served it, over the rows its pull rounds have caught up on. Trust-control reads are leader-local: bounded clustered visibility, no globally linearizable control-plane view.Profile, trust-control reads row; open_receipt_store per handler
UnsupportedMerging imported bilateral evidence into local truth. An imported share lands in federated_share_* tables keyed by share id, never in the local receipt tables, so it never enters the claim log or any local checkpoint. One local write does happen: the import can set federated_parent_capability_id on an existing capability_lineage row as a bridge, which is the only local state an import touches.import_federated_evidence_share in receipt_store/bootstrap/federated.rs
UnsupportedIssuer authentication on a replicated receipt. Verification runs against the key inside the receipt at the AllowHybrid floor, with a parameter-hash check; no trust anchor, issuer allowlist, or policy floor is consulted. Cluster peer auth is the only thing standing between a peer and a follower’s claim log.ensure_chio_receipt_verified_with_context in receipt_store/support/receipt_verify.rs
UnsupportedRemote checkpoint operations. chio receipt audit and the checkpoint verbs refuse a --control-url and require a local --receipt-db file, so an operator checkpoints and audits per node, on the node.local_receipt_db_path in cli/trust/receipt/health.rs

Next Steps

Receipt Aggregation · Chio Docs