Skip to Content
Numinance
Evidence

Record chain

The canonical form, fields, hash links and signatures of an evidence/v1 record, how a verifier walks the chain, and why denied actions are recorded.

View as .mdUpdated
Available in public spec
The record format and the chain rules are public and normative for evidence/v1 (spec 1.0.0). The records are written by the proprietary runtime; any verifier can read them.

A record is the runner's signed statement that an action happened, under which decision, with which outcome. The chain proves those statements are in order and unaltered under a key you trust.

When to read this page

Read it when you write or review a verifier, when you need to explain to a security team why a chain cannot be quietly edited, or when a chain_broken or record_invalid finding needs a diagnosis. Operators who only run numi evidence verify can start at Offline verification.

Mechanism

Every action a change takes, including lifecycle transitions and refused actions, becomes one record. The record is written by the runner identity. The agent that did the work does not write, edit or sign its own account. Three properties tie the records into a chain:

  1. Each record names the hash of the previous record (previous_record_hash), absent only at sequence 1.
  2. Each record carries its own record_hash, the SHA-256 of its canonical bytes without record_hash and signature.
  3. Each record is signed with Ed25519 over a context string and a digest that includes the chain fields, so a record cannot be moved to another position or another change.
record 1 --hash--> record 2 --hash--> record 3 ... record n (ChangeClosed.v1) --> seal (optional)
   |                  |                  |
 signed             signed             signed          one runner key, pinned by the verifier

A verifier does not take any of this on faith. It re-derives record_hash from the bytes as stored, compares it with the stored value, follows the link to the previous record, and only then checks the signature against the key it was told to trust.

One real record

This is record 2 of the conformance fixture closed-author: the runner moving the change from new to planning. The fixture is a generated golden export with fixed test keys, not a real engineering run. The line is shown exactly as stored, in canonical form.

conformance/evidence/v1/exports/closed-author/records.ndjson (line 2)
{"action":"ChangeTransitioned.v1","action_outcome":"succeeded","actor_kind":"runner","actor_principal":"runner-1","canon_version":2,"change_id":"chg_01010101010101010101010101010101","ev_id":"ev_02020202020202020202020202020202","lifecycle":{"author_run_id":"run_09090909090909090909090909090909","from":"new","row_version":1,"to":"planning"},"policy_bundle_digest":"sha256:95d78bff6f47b59c8787f8a980af18faa3ca6d34401348abf4afc6f8642ba2de","previous_record_hash":"sha256:badc6909e742411775874470cfd60d09ec13e2d81018d3f74027ca1145276067","profile_digest":"sha256:1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0","record_hash":"sha256:ca2bbf4b81dcd943e8c2630b8cadb01e19d0b6743eb80a721c7cbc1b9c70865e","risk_class":"READ","sequence":2,"signature":"p2fWkY5ME54o0d7/iu0F8XGp9ZViGYKh3dUUceXjYuTgOd8J9wY98ZtzU5oiPEwHIrZwSIibNTxQ3W3vMgFqAQ==","signing_key_id":"sha256:34750f98bd59fcfc946da45aaabe933be154a4b5094e1c4abf42866505f3c97e","timestamp":"2026-11-01T12:00:02Z"}

Note what is absent: no inputs, no executor_id. In canonical form an empty field is not written at all, so the record is exactly the fields that carry information.

Exact fields

The JSON Schema is schemas/evidence/v1/record.schema.json. The keys, with the rules a verifier enforces:

KeyPresenceRule
ev_idrequiredev_ and 32 lowercase hexadecimal characters
change_idrequiredchg_ and 32 lowercase hexadecimal characters
sequencerequiredinteger, 1 to 2^53-1
actionrequireda capability port.verb, or a versioned lifecycle event such as ChangeClosed.v1; at most 128 bytes
actor_principalrequired1 to 256 bytes of printable ASCII, no leading or trailing space
actor_kindrequiredauthor-agent, reviewer-agent, release-pipeline, human-approver, runner, break-glass; broker or egress-proxy on a receipt record only
actor_run_idoptionalrun_ and 32 hex; required for author-agent and reviewer-agent
policy_decision_idoptionaldec_ and 32 hex; required on a capability record
grant_idoptionalgrant_ and 32 hex; required on a capability record unless it was denied
risk_classrequiredREAD, LOW, MEDIUM, HIGH or PRIVILEGED
profile_digest, policy_bundle_digestrequiredthe profile and policy bundle in force, as digests
plan_digest, executor_id, model_idoptionalthe plan, executor and model the action ran under
sandbox_image_digest, sandbox_profileoptional, togetherset exactly together; process, container, gvisor or microvm
inputs, outputsoptionalartifacts {ref, sha256}, at most 1024 each. Digests, never content
action_outcomerequiredsucceeded, partial, failed or denied
corrects_record_hashoptionalnames an earlier record this one corrects
previous_record_hashoptionalabsent exactly at sequence 1
record_hash, signature, signing_key_idrequired in an exportsee below
timestamprequiredRFC 3339 UTC
canon_versionrequired2
lifecycleoptional{from, to, row_version, author_run_id?, max_repair_attempts?} on lifecycle events with outcome succeeded only

Three details matter for people who build tooling:

  • The four identities appear as actor_kind: author-agent, reviewer-agent, release-pipeline and human-approver. The verifier checks the field's form. The separation of duties itself is enforced by policy before the action; the chain is where you read that it held.
  • inputs and outputs name artifacts by reference and digest. The chain holds no diff content, no log text and no credentials.
  • A record that has an executor_id has both sandbox_profile and sandbox_image_digest. An action cannot be attributed to an executor without saying where it ran.

Canonical form, digests and signatures

Every digest and signature is computed over bytes, so the bytes are specified. The canonical form of a record is deterministic JSON, and it is not RFC 8785 (JCS): key order and number rules differ.

RuleConsequence
Members sorted by the UTF-8 bytes of the keyTwo producers cannot disagree on order
No whitespace outside strings, no trailing newlineThe line is the record
Integers only: no sign, leading zero, fraction or exponent1.0 and 01 are refused
An empty field is absent"inputs":[] is not canonical
Enums are names, bytes are padded base64, timestamps are UTC with ZOne spelling for every value
Digest is sha256: and 64 lowercase hexUppercase hex is not a digest

A verifier reads each document strictly: no duplicate or unknown keys, no case-variant keys, types exactly as defined, and finally the decoded model written back in canonical form must equal the input byte for byte. That last step refuses every non-canonical spelling at once, which is why a record cannot be altered by whitespace or key tricks and still pass.

record_hash    = sha256( canonical(record without record_hash and signature) )
signing_digest = sha256( canonical(record without signature) )
domain_tag     = "numinance.evidence.v1.Record"
signature      = Ed25519( runner_key, domain_tag || signing_digest )

The signature covers sequence, previous_record_hash and record_hash, so it binds position as well as content. A signature made in one context never verifies in another: the attestation, the manifest and the checkpoint each have their own context string. A key id is the digest of the raw 32-byte public key.

How a verifier walks the chain

A verifier is told the change (from summary.json) and walks records.ndjson from the first line. For record n it applies these rules, and the first failure ends the walk:

StepFailure code
The line decodes strictly and satisfies the record validity rulesrecord_invalid
record_hash is present, the chain has not already ended, change_id matches, ev_id is newchain_broken
sequence equals n, previous_record_hash equals the hash of record n-1, and re-derived record_hash equals the stored onechain_broken
The signature verifies under the key named by signing_key_id, which must be a trusted keysignature_invalid, signer_untrusted

Limits: a line is at most 3 MiB, the file at most 256 MiB, and a chain at most 32768 records (too_many_records). A chain ends at ChangeClosed.v1, ChangeAbandoned.v1 or ChangeRolledBack.v1. Only an optional seal record, EvidenceSealed.v1, may follow, with exactly one output (the checkpoint reference) and no inputs. A chain whose last record is neither terminal nor a seal "may be a prefix of a longer chain", and the verifier reports it as incomplete instead of passing it.

Denied actions are records

Policy is evaluated before every privileged action, outside the model. When policy refuses, the runner still writes a record, with action_outcome of denied. A denied capability record carries its policy_decision_id but may omit grant_id. This is the denial from the closed-basic fixture: the runner was refused scm.merge_pull_request.

conformance/evidence/v1/exports/closed-basic/records.ndjson (line 3)
{"action":"scm.merge_pull_request","action_outcome":"denied","actor_kind":"runner","actor_principal":"runner-1","actor_run_id":"run_03030303030303030303030303030303","canon_version":2,"change_id":"chg_01010101010101010101010101010101","ev_id":"ev_03030303030303030303030303030303","grant_id":"grant_03030303030303030303030303030303","policy_bundle_digest":"sha256:95d78bff6f47b59c8787f8a980af18faa3ca6d34401348abf4afc6f8642ba2de","policy_decision_id":"dec_03030303030303030303030303030303","previous_record_hash":"sha256:144c4c24bc7c5825b4dfd423b754df5ad8b7f0c4a746bbacdb8a997f2504b656","profile_digest":"sha256:1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0","record_hash":"sha256:571265f468fcd1ec01661959ad4e53b196a7c8275965d313aeaf302a943da296","risk_class":"READ","sequence":3,"signature":"xVUEZLD31a/yOX6EzJmq49HX+7OdE9k8YlxnIkkNypegICpsWx+RwVNFV5FkGdvcK0BtPwZgmBjLzbKsSpiHCw==","signing_key_id":"sha256:34750f98bd59fcfc946da45aaabe933be154a4b5094e1c4abf42866505f3c97e","timestamp":"2026-11-01T12:00:03Z"}

The summary counts them: that export's summary.json has "denied":1. The denial matters because a history that lists only successes cannot show that the boundary held. Here the boundary is visible: the attempt, the decision that refused it, and the next record in the chain.

Corrections instead of edits

Nothing is updated or deleted. A correction is a new record whose corrects_record_hash names an earlier record. A verifier checks only that the named record exists earlier in the chain. It does not interpret the correction.

Failure modes and safeguards

SymptomFindingWhat it usually means
A record edited after signingchain_broken, with the message "a record does not match its record_hash"The bytes changed; the real example is in Offline verification
A record removed or reorderedchain_brokenSequence or link no longer matches
A record signed by an unexpected keysigner_untrustedThe key is not one you pinned; see Trust levels
Non-canonical spelling (key order, escapes, number form)record_invalidA producer bug or tampering; a verifier never "fixes" the line
A record after a terminal record other than a sealchain_brokenThe chain was extended after it ended

What the chain does not say: it holds no clock beyond each record's timestamp, and timestamps are not checked against each other or against real time. It proves these records, in this order, were signed by the pinned runner key. It does not by itself prove they are the whole chain. Completeness rests on the terminal record, the signed manifest and the transparency log; see Anchoring and receipts and Threat model and limits.

On this page