Open format

Anyone can check it.

runback.cassette/v1

How every Runback record is built, so you, your auditor or anyone else can check one without us.

sample-cassette.jsonSHA-256 in this browser
  1. 0run…
  2. 1llm…
  3. 2tool…
  4. 3llm…
  5. 4tool…
  6. 5run…

Checking…

Fetching /sample-cassette.json.

Format

Three parts. One holds the evidence.

Every input the agent touched that could change between runs (context, tools, retrieval) is fixed in the record at execution. Replay the agent against those inputs, and any change in its decision shows.

manifestdigests · signature · metadata
runname · status · timingchecked against
eventsordered, hash-chainedsigned source of truth
A summary that disagrees with the chain loses. Verification recomputes run from events, never the reverse.

runback.cassette/v1 is this spec's name and the schema string older records declare. A record built today declares runback.audit/v2. Every verifier below accepts both.

Events

Event fields.

Every event shares a base shape, then adds fields by type. Timestamps are ISO 8601 with milliseconds and a Z suffix by convention; runtime validation checks only for a non-empty string. These four types are the ones the checks read. The full list, including reasoning and env events, is in the event reference.

  • baseschema_version, run_id, span_id, parent_span_id, seq, ts_start, ts_end, type. Every event has these. parent_span_id is null for the root run span; ts_end is null while a step is in flight.
  • runphase ("start"|"end"), name, input, output, status, error, metadata. Two per run. The outcome is the phase: "end" one, checked against run.status by the consistent check.
  • llmmodel: { provider, model_id } (an object, not a bare string), request: { system, messages[], tools[], params }, response: { text, reasoning, finish_reason, tool_calls[] }, usage: { input_tokens, output_tokens, total_tokens }, latency_ms, error.
  • tooltool_name, tool_call_id, input, output, latency_ms, error. A blocked call adds policy_block: { rule, detail } and policy_evaluated: { passed }.
Canonicalisation

Written the same way, every time.

Every hash is computed over a canonical JSON string, not the raw object, so key order can never change a digest. The algorithm is RFC 8785-equivalent, with one deliberate divergence from a naive implementation. The check at the top of this page runs exactly this, in your browser.

canonical(v)Pseudocode
// canonical(v) — RFC 8785-equivalent, applied before every hash:
if v === null || typeof v !== "object":  return JSON.stringify(v)
if Array.isArray(v):                     return "[" + v.map(canonical).join(",") + "]"
else (object):
  keys = Object.keys(v).sort()           // native JS .sort() — UTF-16 code-unit order
  emit each "key":canonical(value) IN THAT SORTED ORDER, comma-joined, wrapped in { }
  — properties whose value is undefined are omitted (matches JSON.stringify)
  — keys are serialised straight from the sorted array, never rebuilt into a
    fresh {} — rebuilding re-triggers V8's own key order, which hoists
    integer-like keys ("0","1","42") first regardless of sort. That single
    line is the actual difference between this and a naive "sort + stringify".

// Numbers: ECMAScript Number::toString via JSON.stringify (RFC 8785 §3.2.2.3
// is defined against this). -0 serialises as 0.
// Strings: whatever JSON.stringify escapes — no extra escaping added.
// All hashes and signatures below are lowercase hex, never base64.
Event chain

Change one byte, and it shows.

Each event is chained to the one before it. Change any event and its hash breaks, and every hash after it. The chain is append-only by construction, not by policy.

The event chainPseudocode
// Event chain — web/lib/audit.ts, packages/verify/index.jsh_0 = ""                                          // empty-string seed, not sha256("")h_i = SHA256_hex( h_{i-1} + canonical(event_i) )  // event_i with its own _hash field removed// events are hashed in array order (not re-sorted by seq for this chain)content_digest = h_n                              // the terminal hash; sha256("") if zero events
Replay

Replayable, not just readable.

The event chain proves the record was not edited. It does not prove the record is what the agent actually saw and did. That is a second, independent digest over the non-deterministic surface alone: every model call and tool call, addressed by its inputs.

The oracle-stream digestPseudocode
// Oracle-stream / cassette digest — packages/replay/src/{cassette,digest}.ts
// A SEPARATE chain from the event chain above — it's what makes a record
// re-executable, not just internally consistent. Events are sorted by seq
// first; "run" and "reasoning" events don't enter it.

oracleEntryOf(event):
  llm  → { kind: "llm",  key: sha256("llm:"  + model_id + ":" + canonical(request, projected)), output: error ? {error} : response }
  tool → { kind: "tool", key: sha256("tool:" + tool_name + ":" + canonical(input, projected)),  output: error ? {error} : output   }
  env  → { kind, key, output } directly (kind: now | date | random | uuid | fetch)

chainStep(prev, entry) = SHA256_hex( prev + canonical({ kind, key, output }) )
cassette_digest = final chainStep result, in seq order (sha256("") if no oracle entries)
entry_count     = number of oracle entries (usually < event_count — run/reasoning excluded)

// Optional per-event "key_projection: { keep?, drop? }" strips volatile fields
// (nonces, request IDs, timestamps) from the input before it's hashed into the
// key, so two semantically-identical calls key identically. Applied identically
// by the SDK, the server, and the verifier — same function, three call sites.
Signing

Who made it, provably.

The chain and both digests establish internal consistency only. The algorithm is public, so anyone can author a self-consistent record. The signature is what proves who made it.

SigningPseudocode
// Signing — web/lib/audit.ts. Preference order: Ed25519, then HMAC-SHA256, then unsigned.payload = content_digest + ":" + cassette_digest        // UTF-8, colon-joined hex digests Ed25519:      crypto.sign(null, payload, privateKey)     // null digest — Ed25519 doesn't pre-hash              signature hex-encoded; manifest.signature.pubkey carries the SPKI PEM              public key inline, so a record verifies fully offlineHMAC-SHA256:  createHmac("sha256", key).update(payload).digest("hex")              proves custody of a shared secret, not identity — no non-repudiation // A rotated-out key (*_PRIVATE_KEY_PREVIOUS) still verifies records it signed.// The published Ed25519 key: GET /.well-known/runback-audit-key.pem
Verification

The six checks.

Any SHA-256 implementation can run all six. They are what runback.dev/verify, @runback/verify and the API below run: the same function, three call sites.

  1. schemaThe record declares a format this verifier knows.
  2. consistentThe run summary agrees with the signed events. A summary that contradicts its own events is rejected however intact the chain is.
  3. chainEvery event's recomputed hash matches the one stored beside it.
  4. digestThe recomputed terminal hash matches the manifest's content digest.
  5. cassetteThe recomputed oracle-stream digest matches the manifest's replay digest.
  6. signatureUnsigned, no-key, invalid, valid-unpinned, or valid. Sound maths signed by a key this verifier does not pin is valid-unpinned — not a pass.

The first five are integrity — the record is internally sound. The sixth is origin. A record that passes integrity but not signature is unverified, never valid: the chain algorithm is published, so anyone can author a self-consistent record.

Verify

Verify a record.

Paste a record at runback.dev/verify, or load the real one at /sample-cassette.json.

The check runs on Runback's server and stores nothing. To keep the record off our infrastructure entirely, use the CLI.

Implement

Use the format.

  • Receiving a recordVerify it at runback.dev/verify, POST it to /api/audit/verify, or run @runback/verify. No account and no Runback dependency to check integrity. Read the verdict, not just whether it parsed: unverified is not the same claim as valid.
  • Producing the formatImplement the canonicalisation, the event chain and the oracle-stream digest exactly as above. All three are needed to pass every check, not just chain and digest. The reference implementations are @runback/verify on npm, and packages/replay with web/lib/audit.ts in the Runback source.
  • Regulatory useDesigned against the record-keeping obligations in EU AI Act Article 12 and APRA CPS 230 incident documentation: automatic event logging, with traceability and integrity. Whether a deployment satisfies either is for your assessor to decide; no regulator certifies a file format. The format supplies the evidence that argument needs: manifest.generated_at, a signature that names the producer, and a chain any third party can re-derive without our software.

Download it. Check it. Build with it.