Chio/Docs
LOGIN · JOIN

Determinism and replay

Reproduce a decision by fixing its inputs, and check retained records with replay tools that do not repeat tool effects.

Concept chio@5f63e6e

On this page

Reproducible decisions let operators inspect past behavior and evaluate policy changes without repeating a tool’s effects. A reviewer may need to explain last Tuesday’s refusal or ask whether a proposed policy would have refused the same request. Those questions concern the decision and its inputs; running the tool again would perform new work.

Chio makes decisions reproducible by keeping their inputs explicit. The portable Rust core evaluates a request against a signed capability, trusted issuer keys, a supplied clock, and an ordered guard pipeline. It owns no clock, network connection, or database of its own, so the same inputs return the same value. Its signed records are canonical bytes, and the replay tools check what was retained without dispatching anything.

Reproducing a verdict, comparing two signed records, and checking a replay capture require different inputs and establish different results.

The same inputs reach the same verdict

An agent’s planning can vary from run to run while the portable decision logic stays reproducible. An evaluation is a function of its inputs: hold every input fixed and its value repeats; change one input and the value can change with it.

Evaluation as a function from inputs to a verdictXinputs
x=(r, κ, K, G, t)x = (\htmlClass{role-call}{r},\, \htmlClass{cor-parent}{\kappa},\, \htmlClass{cor-parent}{K},\, \htmlClass{role-seal}{G},\, \htmlClass{role-grant}{t})
tissued_atexpires_atsigned windowAllowDeny
E:X→{Allow, Deny}E : \mathcal{X} \to \{\htmlClass{role-seal}{\text{Allow}},\ \htmlClass{role-deny}{\text{Deny}}\}
x16x2
x2=x1⇒E(x2)=E(x1)=Allow\begin{aligned} &x_2 = x_1 \\ &\Rightarrow E(x_2) = E(x_1) = \htmlClass{role-seal}{\text{Allow}} \end{aligned}
drag x2 along its clock
Evaluation as a function from inputs to a verdictXinputs
x=(r, κ, K, G, t)x = (\htmlClass{role-call}{r},\, \htmlClass{cor-parent}{\kappa},\, \htmlClass{cor-parent}{K},\, \htmlClass{role-seal}{G},\, \htmlClass{role-grant}{t})
tissued_atexpires_atAllowDenyx1x2

The replay uses the captured inputs, so it reaches the same verdict, Allow.

E:X→{Allow, Deny}x=(r, κ, K, G, t)x2=x1⇒E(x2)=E(x1)=Allow\begin{gathered} E : \mathcal{X} \to \{\htmlClass{role-seal}{\text{Allow}},\ \htmlClass{role-deny}{\text{Deny}}\} \\[6pt] x = (\htmlClass{role-call}{r},\, \htmlClass{cor-parent}{\kappa},\, \htmlClass{cor-parent}{K},\, \htmlClass{role-seal}{G},\, \htmlClass{role-grant}{t}) \\[10pt] \begin{aligned} &x_2 = x_1 \\ &\Rightarrow E(x_2) = E(x_1) = \htmlClass{role-seal}{\text{Allow}} \end{aligned} \end{gathered}
In Rust · evaluate.rs:5713 lines
pub struct EvaluateInput<'a> {
    /// Tool call request being evaluated.
    pub request: &'a PortableToolCallRequest,
    /// The capability token authorising this call.
    pub capability: &'a CapabilityToken,
    /// Trusted issuer public keys (typically CA + kernel + authority).
    pub trusted_issuers: &'a [PublicKey],
    /// Clock used for time-bound enforcement.
    pub clock: &'a dyn Clock,
    /// Guard pipeline. Evaluated in order, fail-closed on deny or error.
    pub guards: &'a [&'a dyn Guard],
    // … session_filesystem_roots
}
One illustrative request, with hours on the clock. The same request, signed grant, trusted issuer, and clock return identical values; changing only the clock can change the verdict. Replay checks decisions without re-executing the tool, and a receipt alone does not hold every input.

The browser exercise runs a real signed grant through the same check. Its first two cases use the same request, grant, trusted issuer, and fixed clock, and they return identical values. Advancing only the clock past the grant’s expires_at returns capability has expired. The context comparison lists all five executed cases with their returned reasons.

Reproducibility therefore needs the complete context. Repeating an outside buyer’s pricing.quote request for sku-118 with the same parameters is not enough if the capability has expired, the issuer configuration has changed, or a runtime budget has been consumed. A receipt identifies a recorded decision; it does not carry every input needed to reconstruct that decision.

The core receives time through a clock interface and compares it with the capability’s signed validity window. The host supplies a trustworthy time source in production; a test supplies a fixed clock to reproduce a time-dependent decision. The evaluation also receives trusted issuer keys, the ordered guards, and any session filesystem roots. Richer entry points accept a cryptographic floor, trust-root context, and a delegated-budget registry. All of these, and the behavior of the supplied guards, belong to the reproducibility contract.

Entropy sits behind its own adapter boundary for operations that need unpredictable identifiers or nonces, and it is not an argument to EvaluateInput. Production adapters supply cryptographically secure randomness. A deterministic test generator makes tests repeatable and is unsuitable as a production nonce source.

Canonical JSON fixes the bytes for each value

Independent participants need to recover the same signed bytes from the records they exchange. Chio signs canonical JSON: the encoder fixes object-key order, number representation, escaping, and whitespace, so a supported logical value has exactly one byte representation. It rejects integer magnitudes beyond the I-JSON safe bound of 2^53 − 1.

Canonicalization as a map from JSON texts to byte stringsx ={ "ratio": 0.5, "path": "é.md", "max": 20 }key orderwhitespace\u00e90.50max:202120.0253JSON texts
[v]\htmlClass{role-seal}{[v]}
[v′][v']
refusedcanonical bytes{"max":20,"path":"é.md","ratio":0.5}C · 37 bytes{"max":21,"path":"é.md","ratio":0.5}C′ · 37 bytes⊥refused before signingx
canon⁡:JSON text→bytes∪{⊥}\operatorname{canon} : \text{JSON text} \to \text{bytes} \cup \{\htmlClass{role-deny}{\bot}\}
x≡y  ⟺  canon⁡(x)=canon⁡(y)canon⁡(canon⁡(x))=canon⁡(x)\begin{aligned} &x \equiv y \iff \operatorname{canon}(x) = \operatorname{canon}(y) \\ &\operatorname{canon}(\operatorname{canon}(x)) = \operatorname{canon}(x) \end{aligned}
canon⁡(x)=C\operatorname{canon}(x) = \htmlClass{role-seal}{C}
Canonicalization as a map from JSON texts to byte stringsx ={ "ratio": 0.5, "path": "é.md", "max": 20 }key orderwhitespace\u00e90.50max:202120.0253JSON texts
[v]\htmlClass{role-seal}{[v]}
[v′][v']
CC′⊥C ={"max":20,"path":"é.md","ratio":0.5}37 bytesC′ ={"max":21,"path":"é.md","ratio":0.5}37 bytes⊥refused before signingx

The text denotes the same value as C, so it canonicalizes to C, 37 bytes.

canon⁡:JSON text→bytes∪{⊥}x≡y  ⟺  canon⁡(x)=canon⁡(y)canon⁡(canon⁡(x))=canon⁡(x)canon⁡(x)=C\begin{aligned} &\operatorname{canon} : \text{JSON text} \to \text{bytes} \cup \{\htmlClass{role-deny}{\bot}\} \\[4pt] &x \equiv y \iff \operatorname{canon}(x) = \operatorname{canon}(y) \\ &\operatorname{canon}(\operatorname{canon}(x)) = \operatorname{canon}(x) \\[6pt] &\operatorname{canon}(x) = \htmlClass{role-seal}{C} \end{aligned}
In Rust · canonical.rs:3714 lines
const I_JSON_MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
// …
fn write_canonical_object(map: &Map<String, Value>, out: &mut String) -> Result<()> {
    let mut pairs: Vec<_> = map.iter().collect();
    // RFC 8785: sort object keys by UTF-16 code unit comparison.
    pairs.sort_by(|(a, _), (b, _)| cmp_utf16_code_units(a.as_str(), b.as_str()));
    // …
}
// … strict text entry: visit_u64
        if v > I_JSON_MAX_SAFE_INTEGER {
            return Err(de::Error::custom(format!(
                "integer {v} outside I-JSON safe range [-(2^53-1), 2^53-1]"
            )));
        }
Illustrative texts through Chio’s strict text entry point, with their canonical bytes worked out by hand. One byte form exists only for the same supported logical value: this entry point refuses an integer written as 20.0 rather than guess what was meant, and any integer beyond 253 − 1.

Canonical bytes make records comparable, and they also make clear what a comparison establishes. A byte-identical receipt requires the entire signing input to match: request and policy bindings, decision, metadata, timestamps, receipt semantics, signing nonce, and key. Two allowed calls with the same arguments legitimately produce different receipt bytes. Each comparison answers its own question:

The questionWhat answers it
Did the same decision repeat?The verdict and reason from an evaluation with the same inputs.
Is this the same record?A byte-identical canonical body and signature.
Was it changed after signing?Signature verification under the key that signed it.

The canonical encoder implements the serialization boundary, receipt signing binds the signing nonce and canonical content into each record, and the wire protocol specifies the envelopes readers exchange.

Each replay mode answers one question

Select a replay check from the material you retained and the conclusion you need. A valid signature, a matching root, and a reproduced policy decision are separate results, and a mode reports only the ones it checks.

ModeWhat it checks
replay trafficValidates captured tee frames. Include --tenant-pubkey to verify their signatures; validation without that key skips signatures.
replay traffic --againstCompares captured invocations with a supplied policy in pre-output mode. Requires the tenant key and compares verdict, guard, and reason.
replay --from-teeValidates signed frames and folds their canonical bytes into a root. --expect-root adds comparison with an independently supplied root.
replay <receipt-log>Checks receipt structure and signatures. Ordinary receipt-only verdict re-derivation is unavailable: missing policy, capability, and guard context produces exit 10.
Interpret replay exit codes
ExitMeaning
0The selected checks completed. Root comparison applies only when an expected root was supplied.
10Traffic decision or attribution drift, or unavailable receipt verdict re-derivation.
20Signature or key failure; also a root mismatch.
30Parse or input error, empty traffic capture, or unsupported pre-output policy.
40Unsupported schema or invalid invocation.
50Unsupported redaction-pass identifier.

Read an exit alongside its report and the selected mode. Exit 10 alone does not show that two builds disagree about the original request. The replay implementation and its policy-comparison path define these checks.

Verify a capture, then compare a candidate policy

Suppose you are reviewing a policy change and want to know whether recorded invocations would be admitted differently, without dispatching those tools again. Authenticate the capture first; then compare it with the candidate policy.

Download the architecture exercise and use the CLI from Installation. The exercise contains a signed frame, a tenant public key, an allow-all candidate policy, and a deliberately altered frame. Its key signs only this exercise.

Inside architecture-labbash
# Authenticate the original recorded denial.
chio replay traffic --from recorded-deny.ndjson \
  --tenant-pubkey tenant.pub --json

# Compare that invocation with an allow-all candidate policy.
chio replay traffic --from recorded-deny.ndjson \
  --tenant-pubkey tenant.pub --against candidate-policy.yaml \
  --run-id architecture-tour --json

# The frame was edited without resigning: verification must fail.
chio replay traffic --from tampered.ndjson \
  --tenant-pubkey tenant.pub --json

The recorded run produced three exits, one per command:

CommandExitWhat it established
verification01 of 1 frame passes; the tenant signature verifies.
comparison10Guard attribution changed: captured policy, replayed kernel.receipt_persistence. Both verdicts are deny.
tampered20The edited frame fails tenant signature verification.
replay-comparison.json · the reported driftjson
{
  "captured": {
    "verdict": "deny",
    "guard": "policy",
    "reason": "drift"
  },
  "replay": {
    "verdict": "deny",
    "guard": "kernel.receipt_persistence",
    "reason": "internal error: durable receipt persistence unavailable: no receipt store configured"
  }
}

Read the comparison before drawing a conclusion about policy. Both verdicts are deny: the replay kernel refused because durable receipt persistence was unavailable, so the run stopped at a runtime prerequisite before policy evaluation could decide anything. Treat that as a stop condition. The selected CLI’s comparison path must provide the runtime prerequisites for policy evaluation; until it does, use the controlled evaluator exercise for reproducibility and the authenticated capture checks for evidence.

replay-tampered.json · capturedjson
{
  "first_error": {
    "exit_code": 20,
    "line": 1,
    "message": "tenant signature verification failed: signature verify failed: signature error: Verification equation was not satisfied"
  },
  "from": "tampered.ndjson",
  "ok": false,
  "passes": 0,
  "schema": "chio-tee-frame.v1",
  "total": 1
}

The third command edits the denial reason while preserving the frame’s schema. Its invalid signature is why authentication comes before trusting content. For real captures, obtain the tenant key through a trusted channel; a key supplied alongside untrusted evidence authenticates nothing. Keep each JSON report with its exit status, since the report names the inputs and policy behind a difference and the exit code does not. The exercise retains its verification and comparison reports.

Policy comparison creates an ephemeral kernel for the run, then a fresh agent, capability, and session context for each frame, and compares the resulting decision and attribution with the capture. A tee frame does not retain the original tool-server ID or the response bytes, so this mode rejects concrete-server grants and post-output guards. It also leaves the original identity, delegated authority, runtime budget, and external effect unreconstructed. The optional --run-id namespaces report attribution; the signing keys it generates are new on every run.

See the replay traffic reference for exact arguments. For a retained receipt, such as the buyer’s approved quote for sku-301, Export evidence verifies the record; that verification establishes the record, and the original call stays unreconstructed.

Read the tests at their stated boundary

The replay corpus checks deterministic fixture artifacts against raw-byte goldens. Its generator builds synthetic records from each manifest’s expected verdict and fixed context. That catches encoding and fixture drift; establishing that those expected decisions are correct needs the full kernel, which the generator does not run.

Separate receipt property tests exercise deterministic signing and repeated Merkle construction, and differential tests compare named production functions with reference models. Each supplies evidence for the behavior it exercises. Together they stop short of a blanket replay guarantee for every runtime path or historical release.

Inspect the test subjects

Replay never reverses an external effect. Establishing what a tool changed requires evidence from that execution and its environment, and recovering an interrupted operation uses the runtime’s durable admission and recovery state.

Follow a decision or its evidence