LearnSystem Architecture
Determinism and Replay
The same inputs produce the same verdict and the same bytes, and the two inputs the kernel cannot compute arrive as traits.
Overview
Determinism here is a byte-level claim, not a mood. Given the same request, the same capability, the same trusted issuers, the same clock reading, and the same guard pipeline, evaluation produces the same verdict; and given the same verdict and the same signing key, receipt construction produces the same bytes. That is what makes a receipt log replayable: a verifier can re-run the decision and compare the output byte for byte instead of trusting a summary of it.
Two things stand in the way of that property, and both are handled the same way. A pure function cannot read a clock and cannot generate randomness, so the kernel core does neither. It takes both as traits and lets the caller decide what to pass.
Model
The two inputs the kernel does not generate
clock.rs states the rule in its first line and then states the failure mode.
//! Abstract clock for capability time-bound enforcement.
//!
//! The kernel core never calls `std::time::SystemTime::now()`. All time
//! enters the pure evaluation surface through a `&dyn Clock` so that
//! browser, WASM, and embedded adapters can inject `Date.now()`,
//! `instant::now()`, or a fuzzed/mock clock for deterministic testing.
/// Abstract monotonic wall-clock exposing Unix seconds.
///
/// Implementations MUST return a value consistent with the signed
/// `issued_at` / `expires_at` fields on capabilities. The verdict path is
/// fail-closed against clock errors: if `now_unix_secs` returns a value in
/// the past of `issued_at` or past `expires_at`, the capability is rejected.
pub trait Clock {
/// Current Unix timestamp in seconds.
fn now_unix_secs(&self) -> u64;
}rng.rs is the mirror image, and it names what breaks when a caller supplies the wrong thing.
//! Abstract entropy source for receipt IDs and DPoP nonces.
//!
//! The kernel core never calls `OsRng` directly. Browser adapters route
//! to `crypto.getRandomValues()` through `getrandom`'s `js` feature;
//! WASI adapters route to the host's random API; mobile adapters use
//! `SecRandomCopyBytes` / `/dev/urandom`.
/// Trait boundary for cryptographically-secure random byte production.
///
/// Implementations MUST produce cryptographically strong randomness.
/// Failing to do so defeats replay protection (DPoP nonces) and
/// capability-ID unpredictability. The trait deliberately exposes only
/// `fill_bytes` so adapters can route to CSPRNG primitives native to
/// their platform without extra shims.
pub trait Rng {
/// Fill `dest` with cryptographically secure random bytes.
fn fill_bytes(&self, dest: &mut [u8]);
}The asymmetry between the two is the whole design. Injecting a fixed clock is a supported testing technique, and FixedClock exists in the same file for it. Injecting a fixed entropy source is not: the crate ships NullRng, which fills with zeros, and its own doc comment restricts it to test paths that should never need randomness. A caller that needs to mint receipt ids must supply a real generator.
Whose clock decides expiry
A capability carries signed issued_at and expires_at fields, and the kernel compares them against whatever now_unix_secs returns. So the answer to who decides expiry is: whoever supplies the clock to that evaluation. In a hosted deployment that is the kernel process. In a browser or mobile adapter it is the platform time source the adapter injects.
Clock trust is a deployment property
issued_at or past expires_at rejects the capability. A caller that supplies a wrong clock does not weaken the check, it moves the window. That is why a clock outside the enforcing party's control is an assumption a deployment states rather than a property the kernel provides.Canonical bytes
Determinism of the verdict is not enough on its own, because a receipt is signed over bytes. Chio serializes through a canonical JSON encoder whose output is fixed for a given logical value, so the same value signed in Rust, TypeScript, Python, or Go yields identical bytes.
//! Chio canonical JSON serialization.
//!
//! Produces byte-for-byte identical output for the same logical JSON value,
//! regardless of key insertion order or floating-point formatting quirks.
//! This is required for deterministic signing: the same value serialized in
//! Rust, TypeScript, Python, or Go must yield identical bytes.
//!
//! The format follows RFC 8785 (JSON Canonicalization Scheme).
//! - Object keys sorted by UTF-16 code unit comparison
//! - Numbers: shortest representation matching ECMAScript `JSON.stringify()`
//! - Strings: minimal JSON escaping for C0 controls, quotes, and reverse solidus
//! - No whitespace between tokensThe encoder also refuses what it cannot render identically everywhere. Integer magnitudes above the I-JSON safe bound of 2^53 - 1 are rejected before signing rather than silently coerced, because a value a conforming consumer cannot represent exactly is a value two verifiers can disagree about. The encoding carries a type-level witness: CanonicalBytes<CanonicalJsonWitness> can only be constructed through this serializer, so a byte string that reaches a signing call has been through it.
How it works
The corpus that pins it
The property is enforced by a gate rather than asserted. The replay corpus is a curated set of input scenarios, organized into ten family directories, that is replayed on every pull request; the produced receipts, the anchor checkpoint, and the Merkle root are byte-compared against checked-in goldens. Every fixture runs through the same execution context, and four controls make that context reproducible across machines and operating systems.
Every fixture flows through the same deterministic execution context so that
replay output is byte-identical across machines and operating systems:
- A fixed Ed25519 signing key is loaded from `tests/replay/test-key.seed`.
- The clock is pinned at `2026-01-01T00:00:00Z`.
- Nonces are strictly monotonic 16-byte values from an in-memory counter.
- Directory enumeration is forced into `LC_ALL=C` byte order.
Goldens are read back as raw `Vec<u8>` and byte-compared against a candidate
run, so any drift in whitespace, key order, or line endings is caught without
a `serde_json` round-trip masking it.Read the last paragraph as a decision about how to compare. Parsing a golden back into a value and comparing values would pass a run whose key order or whitespace had drifted. Comparing raw bytes fails it. The gate exists to catch exactly the drift a round trip would hide.
tests/replay/README.md:18-30at fe56570Replaying a log
The corpus gate pins the kernel against its own goldens. A deployment does the same thing to its own receipts with chio replay, which reads a directory of signed receipts or an NDJSON tee stream, re-verifies every signature, recomputes the Merkle root incrementally, and reports the first divergence by byte offset and JSON pointer.
chio replay ./receipts \
--trusted-kernel-pubkey ./kernel.pub \
--jsonThe exit code says which kind of divergence was found, which is what makes the command usable from a pipeline rather than only from a terminal.
| Exit | Meaning |
|---|---|
0 | All receipts or tee frames verify, and the root matches expectation. |
10 | Verdict drift: a receipt allow or deny decision differs from the current build for the same input. |
20 | Signature mismatch: Ed25519 verification failed on at least one receipt or frame tenant_sig. |
30 | Parse error: malformed JSON or a missing required field. |
40 | Schema mismatch: unsupported schema_version, or schema validation failed against the canonical-JSON schema set. |
50 | Redaction mismatch: redaction_pass_id unavailable, or rerunning the redaction manifest produces a different result. |
Exit 10 is the one that separates this from signature checking. A log whose signatures all verify can still be a log the current build would decide differently, and that is a policy or kernel change rather than tampering. chio replay traffic does the same for an NDJSON chio-tee-frame.v1 capture, and with --against re-executes every frame against a policy in pre-output mode, namespacing its receipts as replay:<run_id>:<frame_id> so they cannot collide with production receipts.
Guarantees and limits
Status: shipped. The clock and entropy boundaries are traits in chio-kernel-core, the encoder is chio-core-types, and the corpus gate runs in continuous integration on every pull request.
- Byte-exactness is checked, not assumed. Goldens are compared as raw bytes, so drift in whitespace, key order, or line endings fails the gate instead of passing through a parse and re-serialize.
- Goldens are pinned across versions. Updating one is a gated operation:
--blessis the only supported path, it requires theCHIO_BLESSenvironment gate and a stated reason, and direct edits under the goldens directory are out of policy. A cross-version compatibility matrix sits beside the corpus. - Determinism is a property of the core, not the deployment. The core computes no time and no randomness. A deployment that supplies a skewed clock gets a correct comparison against a wrong window, and one that supplies weak randomness defeats DPoP replay protection and capability-id unpredictability. Both are caller obligations the trait doc comments state as
MUST. - Limit: the corpus is a fixed set. The gate pins the behavior the fixtures exercise, across ten families including allow, deny by expiry, deny by revocation, deny by scope mismatch, guard rewrite, replay attack, and two tampering families. Behavior no fixture reaches is not pinned by it.
- Limit: replay compares against the current build.
chio replayre-evaluates a captured log against the binary running it. A verdict drift exit therefore reports a difference between two builds, and deciding which one is right is not the tool's job. - Limit: a tee frame is not a full request.
chio replay traffic --againstrejects policies carrying concrete-server grants or post-output guards, becausechio-tee-frame.v1carries neither the original tool-server id nor redacted response bytes.
Next steps
- The State a Verdict Depends On · the arguments a deterministic verdict is a function of
- Wire Protocol · the canonical encoding and the signed envelope in detail
- Differential Tests · the other lane that consumes byte-level determinism
- Receipts · the content-addressed record a replay recomputes
- CLI Reference · every flag on
chio replay