Chio/Docs

Node State on Disk

See which files a node keeps, which process may write each one, and which checks refuse a stale or foreign database.

Concept chio@5f63e6e

On this page

Revocations, budget reservations, and signed decisions survive a restart because each lives in a SQLite file that exactly one serving process may write.

chio-store-sqlite backs every durable store trait the kernel defines. The portable decision core stays independent of storage; the runtime coordinates durable mutations, and the shape of each file decides how. Identify the file you hold before you choose a recovery procedure.

Authority shares one connection; receipts read through a pool

The store opens files in two arrangements, and the arrangement decides which connection writes.

Store writersChoose a path.
authority database · one connectionbudgetrevocationadmission operationstool outcomesand 5 more storescone transactionsreceipt database · one writerappendsqueue 1024wcommit actor≤ 64 per commitallow returns after commitρ8 readersqueue saturatedauthority database · one connectionbudgetrevocationadmission opstool outcomesand 5 more storescone transactionsreceipt database · one writerappendsqueue 1024wcommit actor≤ 64 per commitallow returns after commitρ8 readersqueue saturated

An allow returns after its receipt commits

Every receipt write goes to the commit actor on the single writer connection. The actor drains up to 64 appends into one transaction, waiting at most 500 microseconds for each next append, and the default append blocks until its batch commits. Fail-Closed Semantics.

The two connection shapes of one node's SQLite state: four of the nine logical stores drawn by name, the receipt store's reader pool and commit actor at their default sizes. The order of appends is illustrative; the verified checkpoint head, the lock root, and the WAL files are left out.

The authority database is one file that SqliteAuthorityStore provisions and serves through one Connection behind a mutex. Its logical stores take clones of that connection through open_alongside: budget, revocation, admission operations, tool outcomes, FROST custody, the economic state cache, fiscal state, channel lifecycle, and the channel release publisher. None reopens the file or starts a second writer, which is what lets a budget authorization and its admission commit land in one transaction.

Own-file stores take a path and open it directly: SqliteReceiptStore, SqliteRevocationStore, SqliteBudgetStore, SqliteApprovalStore, SqliteBatchApprovalStore, SqliteExecutionNonceStore, SqliteMemoryProvenanceStore, SqliteEncryptedBlobStore, and SqliteCapabilityAuthority. Two share a file on purpose: chio api protect opens the receipt store, then co-locates the approval store in the same database. Finding-market queries also have a read-only companion path, whose readers hold no write authority and may trail an in-flight writer transaction.

Read more in Budget Store, which uses the shared transaction.

A mediated allow returns after its receipt commits

Every receipt write runs on the single writer connection through the commit actor. The actor groups up to 64 appends into one transaction, and the default append blocks until its batch commits, so the caller sees an allow only once its receipt is on disk.

The writer refuses any append it cannot commit. A full channel of 1024 appends returns sqlite receipt commit queue saturated, a caller deadline fails closed with ReceiptStoreError::Timeout, and a writer whose verified checkpoint head cannot be seeded refuses every append. The kernel's pre-dispatch gate reads the writer's liveness and refuses calls while it reports saturated, wedged, or dead, before any tool runs.

A writer fault therefore reaches the caller as a refusal before dispatch. To recover a poisoned head, restart the process so the actor reseeds, or run chio receipt audit --repair with the kernel stopped.

Read more in Fail-Closed Semantics.

One process owns an authority database, and every write proves it

SQLite serializes transactions, but it does not serialize recovery ownership or tell a stale worker that it lost the writer role. The serving owner does both: one row that binds the database's identity to the process allowed to mutate it now.

  1. Provision. SqliteAuthorityStore::provision creates the file at mode 0600, mints a UUID-v7 store id, creates one lock inode, and writes the owner row in one transaction. It runs inside the serving process at startup, under the uid that later checks enforce.
  2. Claim. open_serving takes the lock for the serving lifetime, refuses any file whose path, device, inode, mode, or link count changed, then closes the previous lease, increments owner_epoch, and opens a fresh lease_id.
  3. Fence. The caller holds a StoreMutationFence of store_uuid, lease_id, and owner_epoch. Every mutation re-reads the owner row inside its own transaction and returns Fenced { expected_epoch, actual_epoch } on any mismatch, before a row changes.

Two more checks guard the file itself. A change made outside the serving connection moves PRAGMA data_version and poisons the owner with OutcomeUnknown, and a rollback anchor in the lock file catches a superseded snapshot restored under a live lock root. Once a database carries an owner row, a standalone open of one of its logical stores is refused.

Serving-owner table schema
crates/platform/chio-store-sqlite/src/serving_owner.rssql
CREATE TABLE chio_serving_owner (
    singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
    store_uuid TEXT UNIQUE NOT NULL,
    database_path TEXT NOT NULL,
    database_device INTEGER NOT NULL CHECK (database_device >= 0),
    database_inode INTEGER NOT NULL CHECK (database_inode >= 0),
    lock_root TEXT NOT NULL,
    lock_device INTEGER NOT NULL CHECK (lock_device >= 0),
    lock_inode INTEGER NOT NULL CHECK (lock_inode >= 0),
    owner_epoch INTEGER NOT NULL DEFAULT 0 CHECK (owner_epoch >= 0),
    lease_id TEXT,
    opened_at_ms INTEGER
);

Read more in Node Runtime Contract.

A store refuses a file it cannot identify, before it writes

Before ownership, pragmas, or any write, every store passes the same gate, which checks that the file is a Chio store of its kind at a revision this binary reads. The refusal an operator meets most is the mistyped path:

Terminal · a revocation database handed to --receipt-dbbash
chio receipt checkpoint status --receipt-db ./state/revocations.sqlite
echo $?
Output
error[CHIO-CLI-RECEIPT-STORE]: receipt database ./state/revocations.sqlite is not an initialized Chio receipt store: missing table chio_tool_receipts
  help: pass --receipt-db the path of the receipt store; check its permissions and schema health
  code: urn:chio:error:cli:receipt-store
  docs: https://chio.computer/docs/reference/errors#error-cli-receipt-store
1

You should see the anchor check name the missing table and exit with status 1, with nothing written. A foreign file is refused as ForeignDatabase, another Chio store as MismatchedStore, and a newer revision as FutureSchema; none of the three modifies the file. Both halves of the stamp are readable with any SQLite client, where 1128810831 is 0x4348494f, ASCII CHIO:

Terminal · reading the stamp off a storebash
sqlite3 ./state/revocations.sqlite "PRAGMA application_id; PRAGMA journal_mode;"
Output
1128810831
wal

WAL needs one host, one writing instance, and a local disk

Every durable store opens in WAL mode with synchronous = FULL and busy_timeout = 5000. WAL coordinates readers and writers through a shared-memory index that works only when every connection is on one host and a local filesystem.

Give every Chio database a local disk and exactly one writing instance; NFS, Amazon EFS, Filestore, and Azure Files are unsafe for WAL, and two serving processes can race the receipt store's verified head. The ECS reference task attaches a per-task block volume and expects desiredCount: 1. A store is more than one file, which matters to any backup script:

Terminal · one governed call, then the directory (owner and mtime normalized)bash
chio --receipt-db ./receipts.sqlite3 --session-db ./sessions.sqlite3 \
    check --policy policy.yaml --server pricing --tool quote --params '{"sku":"sku-118","percent":10}'
ls -la
ls -la sessions.sqlite3.locks
Output
-rw-r--r--   1 chio chio      636 policy.yaml
-rw-r--r--   1 chio chio  1007616 receipts.sqlite3
-rw-------   1 chio chio  1507328 sessions.sqlite3
-rw-------   1 chio chio       65 sessions.sqlite3.kernel.pub
-rw-------   1 chio chio       65 sessions.sqlite3.kernel.seed
drwxr-xr-x   4 chio chio      128 sessions.sqlite3.locks

-rw-------   1 chio chio      339 .chio-path-6c1464fd638c23b0c2d18bd7979daf5df45748c70a498e5e05d52ece3fb3dc3b.identity
-rw-------   1 chio chio     2048 019fbe46-df16-7bb1-9fc6-fa1440bd44b6.lock

You should see the session database bring its kernel keypair and a lock directory, all at 0600. The -wal and -shm siblings exist while a connection is open and are checkpointed away on a clean close, so a backup that copies the .sqlite3 file alone under an active writer misses its latest commits; Backup & Restore uses .backup for that reason.

Cloud Run and Azure Container Apps have no per-instance disk, so their reference manifests keep the log on an in-memory volume that WAL accepts and every revision recycle erases. The sidecar boots on an in-memory path only with --allow-ephemeral-receipts; for durable receipts there, front a client-server audit store or use a platform with a per-instance disk. Serving one file from several processes is outside this SQLite profile; replicated authority belongs to the cluster.

Store layouts, contracts, schemas, and refusals

Sidecar layout on disk

A sidecar started with chio api protect --receipt-store /var/lib/chio/receipts.db produces this layout.

PathHoldsOpened by
receipts.db
Receipt log plus the co-located approval store
SqliteReceiptStore::open, then SqliteApprovalStore::open_colocated_with_receipt_store
receipts.db.revocations
The revocation list, so a revocation outlives a restart
SqliteRevocationStore::open
receipts.db.authority-locks/
Private lock root, created mode 0700
prepare_authority_lock_root
.../authority.db
The joint authority database; the sidecar wires its admission-operation and tool-outcome stores
SqliteAuthorityStore::provision, then open_serving
.../<store_uuid>.lock
The serving lock inode and the two-slot rollback anchor inside it
serving_owner::rollback_anchor
.../.chio-path-<sha256>.identity
Path-identity continuity marker, keyed by the canonical database path
serving_owner::path_identity

Provisioning creates every authority schema in authority.db (budget, revocation, FROST, the economic state cache, fiscal, and the rest), and the sidecar wires two of them; its revocation list stays in the sibling file. The same authority store opened by DurableAdmissionRuntime::open in chio-control-plane puts its lock root at <path>.locks and points the budget and revocation stores at the authority file.

Receipt writer and reader pools

The receipt store keeps two pools in chio-store-sqlite/src/lib.rs: DEFAULT_READER_POOL_MAX_SIZE = 8 and DEFAULT_WRITER_POOL_MAX_SIZE = 1. Every write runs on the writer through the commit actor's run_write, including session anchors, request lineage, lineage statements, child receipts, and manual checkpoint creation. Both pools open the file read-write; routing keeps the readers read-only, and reader_pool_never_begins_a_write_transaction forces PRAGMA query_only = ON on all eight reader connections, then drives the whole routed write path to completion.

  • RECEIPT_GROUP_COMMIT_MAX_BATCH = 64. The most appends the actor drains into one transaction.
  • RECEIPT_GROUP_COMMIT_FLUSH_DELAY. 500 microseconds, the longest the actor waits for each next append.
  • Channel depth. A bounded channel sixteen times the batch deep (1024); a full channel returns ReceiptStoreError::Pool("sqlite receipt commit queue saturated").

The deadline variants append_chio_receipt_with_timeout and run_write_receipt_with_timeout fail closed with ReceiptStoreError::Timeout { operation, timeout_ms }. That is a caller-side deadline only: the command is already on the channel, so the actor still owns it and may still commit it, and expiry leaves the in-flight counter elevated for the liveness probe to read.

At start the actor seeds a verified head once, running full claim-log validation and a full checkpoint-chain verification on the writer connection. Each later append verifies only the delta: one indexed latest-checkpoint read plus a digest compare, and an aggregate over rows past the head. Writer health reports a poisoned head until seeding succeeds. SqliteStoreOptions::incremental_verification is fixed at open; set false, it keeps full per-append verification for A/B checks on a suspect database. The authority database has no pool: one connection, one mutex, cloned into every logical store.

Provisioning

SqliteAuthorityStore::provision runs before anything serves. A missing path is created with create_new, mode 0600, and O_NOFOLLOW. An existing path is adopted only when it is a regular file, mode exactly 0600, owned by the effective user, with link count one, and only when its authority tables are pristine. The database's parent directory and the lock root must be owned by the effective user and neither group- nor world-writable.

It mints a canonical UUID-v7 store id, creates exactly one <store_uuid>.lock inode in the lock root (mode 0600, link count one, the lock root's own uid and gid), and records the device and inode of both files. The owner table and its single row are created in one IMMEDIATE transaction, so an interrupted provision rolls both back and leaves a recoverable database. Files and both parent directories are fsynced. A repeat provision is idempotent only when the UUID and inode metadata match exactly. It runs from DurableAdmissionRuntime::open and from the sidecar's startup path.

Opening and the epoch claim

open_serving canonicalizes the path, takes an advisory lock on the lock root, re-reads the provisioning record, and refuses a database whose path, device, inode, mode, or link count no longer matches; symlink and hardlink aliases are refused. It then takes try_lock on the lock inode for the serving lifetime and re-validates the record with the lock held, so a race cannot slip a different file underneath.

In one IMMEDIATE transaction it closes the previous lease row at the current admission-authority head, increments owner_epoch, writes a fresh lease_id, and inserts the new open lease. Lease history in chio_serving_leases is append-and-close-only: triggers ABORT any delete and any update other than the one legal transition from open to closed. A startup check requires the leases to form a dense chain of epochs whose authority intervals tile without gaps.

The rollback anchor in the lock file has two slots recording commit heads and chain digests. The path-identity marker is local continuity evidence: restoring or deleting the lock root removes the marker and the anchor together. Serving ownership requires Unix file identity; metadata_device and metadata_inode return Invalid on other targets.

Open refusals

ErrorCondition
NotProvisioned
No owner table: the database was never provisioned
PartialProvision
Owner table present, singleton row missing
AlreadyServing
Another process holds the serving lock
Invalid
Path, device, inode, mode, link count, or store UUID changed; schema differs from the canonical definition; a lease trigger was substituted or dropped
OutcomeUnknown
A commit whose durable outcome cannot be established, or a database changed outside its serving connection

SqliteRevocationStore::open on a provisioned authority file reports that the store requires the joint serving owner at its current epoch, and writes nothing.

Schema stamp refusals

crates/platform/chio-store-sqlite/src/schema_version.rs27-51rust
/// ASCII "CHIO" as a big-endian `i32`, stamped into every Chio operator store.
pub const CHIO_SQLITE_APPLICATION_ID: i32 = 0x4348_494f;

/// Table holding each co-located store's schema revision, keyed by a stable
/// per-store identifier. `PRAGMA user_version` is database-wide and so cannot
/// give the receipt and approval stores (which the sidecar keeps in one file)
/// independent revisions; a keyed table can.
const SCHEMA_VERSION_TABLE: &str = "chio_store_schema_versions";

/// Failure to validate or apply a store's schema stamp.
#[derive(Debug, thiserror::Error)]
pub enum SchemaVersionError {
    #[error("sqlite error: {0}")]
    Sqlite(#[from] rusqlite::Error),
    #[error("database application_id {found:#x} is not a Chio store (expected {expected:#x})")]
    ForeignDatabase { found: i32, expected: i32 },
    #[error(
        "database carries the Chio application_id but none of this store's tables ({expected_anchors:?}); refusing to open another store's database"
    )]
    MismatchedStore { expected_anchors: Vec<String> },
    #[error(
        "database schema version {found} is newer than this binary supports ({supported}); refusing to open"
    )]
    FutureSchema { found: i32, supported: i32 },
}

The revision lives in chio_store_schema_versions under a stable key per store, because PRAGMA user_version is database-wide and co-located stores need independent revisions. The receipt store and the approval store in one sidecar file each sit at their own revision.

RefusalTriggerPrevents
ForeignDatabase
A non-Chio application_id, or an unstamped file with user tables that are none of this store's anchors
Chio tables written into an unrelated file
MismatchedStore
The Chio application_id with no anchor table of this store
Two stores commingled in one file
FutureSchema
The on-disk revision for this store key exceeds what the binary supports
A lower-revision binary misreading a newer schema

Anchors identify the store: the receipt store answers to chio_tool_receipts and to the table names http_receipts and tool_receipts; the joint authority open passes capability_grant_budgets and revoked_capabilities. Because the two shorter receipt names are generic, an unstamped file adopted on either also needs a receipt payload column (raw_json or receipt_json).

A database with no user tables is adoptable, so a fresh file and an unstamped one both open. The application_id is read first, so a file stamped by another application is ForeignDatabase even with no tables. A refused foreign database keeps application_id 0, a future-schema database keeps its revision, and a table-driven test sweeps binary revisions 0 to 3 against on-disk revisions 0 to 5, asserting that a refusal never mutates the file and an accepted open never downgrades it. The gate runs on the open path only, so appends pay nothing for it.

Pragmas per store

All durable stores set WAL, synchronous = FULL, and busy_timeout = 5000. The serving owner, revocation, budget, approval, and receipt stores set foreign_keys = ON; the capability authority and execution-nonce stores do not, so neither file enforces referential integrity. The receipt store adds auto_vacuum = INCREMENTAL and is the store that reads its settings back: journal_mode must be WAL, synchronous 2, busy_timeout at least 5000, and foreign_keys 1, or the open is refused, so a filesystem that declines WAL stops the process at startup.

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rsrust
let journal_mode: String = connection.query_row("PRAGMA journal_mode", [], |row| row.get(0))?;
if !journal_mode.eq_ignore_ascii_case("wal") {
    return Err(ReceiptStoreError::Conflict(format!(
        "sqlite receipt store journal_mode must be WAL, got {journal_mode}"
    )));
}

is_in_memory_sqlite_path recognizes :memory:, file::memory:, and any file:...?mode=memory URI as non-durable.

The ReceiptStore trait

Any backend that implements ReceiptStore can persist receipts. Two methods are required and the rest carry defaults, so a backend overrides only what it persists. The block shows five methods; the rest of the trait (receipt_store.rs:418-909) covers timeout-aware appends, canonical-bytes reads, pending settlement observations, capability lineage, federated evidence, and retention hooks.

crates/kernel/chio-kernel/src/receipt_store.rsrust
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>;
    // ...
    fn store_checkpoint(&self, _checkpoint: &KernelCheckpoint) -> Result<(), ReceiptStoreError> {
        Err(ReceiptStoreError::Conflict(
            "receipt checkpoint storage is not supported by this receipt store".to_string(),
        ))
    }
    // ...
    fn load_checkpoint_by_seq(
        &self,
        _checkpoint_seq: u64,
    ) -> Result<Option<KernelCheckpoint>, ReceiptStoreError> {
        Ok(None)
    }
    // ...
}

The checkpoint defaults are behavior: a store without checkpointing refuses to store one with “receipt checkpoint storage is not supported by this receipt store” and reports none on read, so checkpoint_batch_size and store capability must agree before a deployment claims a Merkle chain. The kernel queries receipts through ReceiptQuery:

crates/kernel/chio-kernel/src/receipt_query.rsrust
pub struct ReceiptQuery {
    /// Filter by capability ID (exact match).
    pub capability_id: Option<String>,
    /// Filter by tool server name (exact match).
    pub tool_server: Option<String>,
    /// Filter by tool name (exact match).
    pub tool_name: Option<String>,
    /// Filter by decision outcome (maps to decision_kind column:
    /// "allow", "deny", "cancelled", "incomplete").
    pub outcome: Option<String>,
    /// Include only receipts with timestamp >= since (Unix seconds, inclusive).
    pub since: Option<u64>,
    /// Include only receipts with timestamp <= until (Unix seconds, inclusive).
    pub until: Option<u64>,
    /// Include only receipts with financial cost_charged >= min_cost (minor units).
    /// Receipts without financial metadata are excluded when this filter is set.
    pub min_cost: Option<u64>,
    /// Include only receipts with financial cost_charged <= max_cost (minor units).
    /// Receipts without financial metadata are excluded when this filter is set.
    pub max_cost: Option<u64>,
    /// Currency for cost filters. Required when either cost bound is present.
    pub cost_currency: Option<String>,
    /// Cursor for forward pagination: return only receipts with seq > cursor (exclusive).
    pub cursor: Option<u64>,
    /// Maximum number of receipts to return per page (capped at MAX_QUERY_LIMIT).
    pub limit: usize,
    /// Filter by agent subject public key (hex-encoded Ed25519). Resolved through
    /// capability_lineage JOIN -- does not replay issuance logs.
    pub agent_subject: Option<String>,
    /// Optional tenant narrowing filter. This is never authority by itself:
    /// callers must also provide a matching explicit `read_context`.
    pub tenant_filter: Option<String>,
    /// Explicit read context resolved from authenticated authority.
    pub read_context: Option<ReceiptReadContext>,
}

limit is capped at MAX_QUERY_LIMIT, which is 200. cost_currency is required whenever either cost bound is set, and receipts without financial metadata drop out of a cost-filtered result. tenant_filter narrows; the explicit read_context authorizes and shapes the query with a boundary (AdminAll or TenantScoped), a source (LocalOperator, AdminService, or AuthenticatedTenant), and an include_null_tenant flag. Remote and control-plane callers build it from authenticated context before the query reaches the store; see multi-tenant isolation.

SqliteReceiptStore schema

The reference implementation in chio-store-sqlite reads through the pool and writes through the commit actor. The tool receipt table:

crates/platform/chio-store-sqlite/src/receipt_store/bootstrap/open.rssql
CREATE TABLE IF NOT EXISTS 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,
    cost_currency TEXT CHECK (
        cost_currency IS NULL OR (
            typeof(cost_currency) = 'text' AND
            length(cost_currency) = 3 AND
            cost_currency NOT GLOB '*[^A-Z]*'
        )
    ),
    cost_charged_be BLOB CHECK (
        (cost_currency IS NULL AND cost_charged_be IS NULL) OR
        (
            cost_currency IS NOT NULL AND
            typeof(cost_charged_be) = 'blob' AND
            length(cost_charged_be) = 8
        )
    )
);

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

A migration adds tenant_id with ALTER TABLE chio_tool_receipts ADD COLUMN tenant_id TEXT and an index beside it, so the column is nullable and rows written before multi-tenancy read back with a NULL tenant. cost_currency must be three uppercase letters or NULL, and cost_charged_be an 8-byte blob present exactly when a currency is, so no row records an amount without its unit. The canonical receipt JSON lives in raw_json; the columns exist for query performance, and the store rebuilds receipts from raw_json on read and re-verifies signatures.

Contracts and their evidence

  1. One mutable owner per file, by an OS lock at open and an epoch fence in every mutation transaction: serving_owner.rs, tests concurrent_process_open_is_fenced_before_serving, provisioned_store_rejects_independent_mutable_opens, stale_serving_epoch_fences_budget_and_revocation_access.
  2. No write into a foreign, mistargeted, or future-schema file, and no change to it on refusal: schema_version.rs and its test module.
  3. Receipt writes serialize on one connection through the group-commit actor: run_write and the actor loop in receipt_store.rs, test reader_pool_never_begins_a_write_transaction.
  4. A restored stale snapshot is detected: serving_owner/rollback_anchor.rs, tests budget_only_snapshot_rollback_is_rejected_by_the_global_anchor and revocation_only_snapshot_rollback_is_rejected_by_the_global_anchor.
  5. The kernel refuses on an unhealthy writer before any tool runs: ReceiptWriterLiveness::healthy in chio-kernel/src/receipt_store.rs. The receipt is durable before the allow returns.

Next: keep a revocation across restarts