receipt: verifiable custody of agent-produced records
An auditor who holds a clone of a rule corpus, and a few trust pins committed in their own repository, can decide offline whether the records in it are the ones a pinned key signed and pinned witnesses saw, and whether any release object seen at first contact has since been changed or deleted. receipt renders that verdict. Four inputs fix it: the tree object a named commit carries, a base tree when the auditor supplies one, the specification the auditor pinned, and the trust material the auditor owns; the verifier’s own clock bounds how far ahead a token may sit. Every record byte the verdict speaks for comes from the repository’s object store and is rehashed against its name; no record byte comes from a working directory. A pass establishes integrity of the release chain, authorship by the pinned key, existence no later than the witnessed times, closed-world binding of the corpus to its journal, and, with a base, preservation of every release object the base held. It does not establish how the bytes were produced, that the history is the only one the producer keeps, or that a chain regenerated before first contact is the original. Two repositories pin the package on their default branch, one of them in an optional extra, and call it from their verification paths; a third pins it on a branch short of its default, and a fourth shows its output.
Working paper. It describes receipt 0.6.1 on PyPI (2026-09-06). Its source differs from 0.6.0 (2026-09-05) in the signing module and the version string alone: two refusals the review below surfaced, noted where they apply. Code: github.com/TheAxiomFoundation/receipt. API reference: axiom.org/receipt/api.
1 Two paths
An encoded rule turns out to be wrong: a rate reads 0.15 where the statute says 0.17. The fix can arrive two ways. The pipeline path re-encodes. The encoder runs again, and the corrected file lands in a fresh release, signed and witnessed, with a journal row behind it. The shortcut edits the published file by hand and commits the edit. Both paths produce the same YAML. Run from a clone, receipt verify tells them apart after the fact (Figure 1). The re-encoded fix passes as release 0002 with its journal row, and the hand edit refuses, because the tree carries bytes the witnessed journal never bound. The command reads the commit’s tree from the object store, never the checkout, and its verdict names the commit and the tree it judged.
The command cannot tell an encoder run from bytes a producer wrote by hand and then journaled, signed and witnessed. That distinction needs a record of the run itself, and the corpora make it separately (Section 5). Nor does it see an edit that was never committed: the checkout is outside its subject.
The records that need this check are the ones agents produce faster than people can witness them. The Axiom Foundation’s software agents encode statutes into executable rules, and a shared validation workflow refuses a change under the guarded rule roots that no signed manifest covers. A signed corpus, the release history behind it, and the gate declarations the producer recorded are the record set, and the receipt package checks them. The question descends from the audit-log literature, where secure logs protect past entries after compromise and make append-only consistency auditable (Schneier and Kelsey 1999; Crosby and Wallach 2009). This setting asks the consumer’s version: what an outside verifier can conclude about a complete record corpus from a clone, against pins the producer cannot reach.
2 What a verdict establishes
Four inputs fix the verdict. The first is the tree object the named commit carries: the verifier resolves the commit once, authenticates its payload and its root tree, and rehashes every object it reads against the object name Git gave it. The second is a base tree, when the auditor names an ancestor commit; the verifier walks the candidate’s parents to reach it, so the base must be an ancestor. The third is the specification, a Python module the auditor reviews and pins by digest: the loader hashes the source bytes once, compares the digest before compiling, and refuses to run mismatching code. The fourth is the trust material that specification pins: the producer’s key fingerprint and, for each timestamp authority, the anchor file’s name and digest, the policy identifier, and digests of the signer certificate and signer key. The anchor bytes themselves come from the tree; the anchor-set digest below binds them. No record byte comes from the checkout or the index. One more thing enters: the verifier’s clock. A token more than 300 seconds ahead of it refuses, so a verdict holds for the clock it was rendered against.
The verifier resolves the candidate, enters the base when one was supplied and proves it an ancestor, and optionally checks the whole object store, which refuses as custody. The passes then run in a fixed order and stop at the first failure: history when a base was supplied, then custody, binding and declaration completeness. A closing re-audit of the repository’s control files and configuration follows; its failure voids custody, binding and declaration, keeps a history pass already recorded, and is reported as a custody refusal. A pass requires all three of custody, binding and declaration to have succeeded. An unexpected exception or a failure to render the result counts as a failure. The exit code carries the verdict: zero for a pass (and for help), one for a failed or aborted verification, two for a usage error. A specification that raises or exits on load refuses at the specification stage with the usage status, not the failure status.
Table 1 states what a pass establishes and what it does not.
| A pass establishes | A pass does not establish |
|---|---|
| Chain. A contiguous, content-addressed sequence of release manifests from genesis to head, each signed by the pinned producer key over its exact bytes, each carrying one token from every configured timestamp authority. | That this chain is the only one the producer keeps. One clone cannot expose a split view. |
| Time. Each release existed no later than each authority’s signed time, checked against the pinned root, policy, signer certificate and signer key without any network call. | When a release was created. Nothing bounds the signed time from above relative to the manifest’s recorded creation time, so a chain regenerated, re-signed and re-witnessed before first contact passes. |
| Preservation. With a base, every object under the release root that the base held keeps its mode and its object name in the candidate. | Preservation of anything added after the base, or that the candidate is the newest history. |
| Binding. Within the configured content roots, every file with a configured suffix is listed in the journal and hashes to its listed digest; an unlisted file, a missing file, a rewritten byte, a symlink where a file was recorded, and two sibling names anywhere in the tree that collide under ASCII case folding each refuse; under any of the five pinned chain paths, or in a directory above one, the colliding pair refuses in custody first. | How the bytes were produced, or that they read the statute correctly. |
| Declarations. Every gate the specification requires is declared in the journal, whose bytes hash to the value the head manifest records. | That any gate ran or passed. The verifier does not rerun them. |
| Identity. The commit and tree that were judged, the specification’s path and digest, the head manifest’s digest, the resolved base, and a digest of the anchor bytes custody consumed. | That any checkout equals the judged tree, or that the anchors the tree carries are trusted, unless the auditor pinned them. |
The right-hand column shrinks over time through one recipe. At first contact the auditor records the head manifest’s digest and the commit, and pins the specification’s digest and the anchor-set digest the verdict printed. Every later run supplies that commit as the base, expects the current candidate by name, and re-supplies the specification and anchor-set digests it pinned. From then on, a release object present at first contact cannot be rewritten or deleted without a refusal. A swapped key or authority cannot pass as the pinned one, and the specification cannot change under the auditor. The recipe touches the time and preservation rows, and two limits stay even there. The specification’s digest names the source bytes the loader read, not the files that source may read in turn, so an auditor reviews it as code once. And the recipe closes the regeneration window from first contact forward, never backward.
The anchor discipline has one more step. The anchor-set digest is computed from the anchor files the verified tree carries and compared with the auditor’s expectation before any cryptographic call, and the bytes custody consumed must match that digest afterward. Without a pinned expectation the verdict still passes, and says in its scope that trust in the anchors and in the specification code was not established.
Three callers reach the directory verifier through a private materialization of the selected tree: the composed command, the append gate and the base-chain verifier. A caller who runs the directory verifier on a directory of their own gets a verdict about that directory as it was read, with the exposure to a concurrent writer that implies.
3 Why the shape follows from the threat
The producer controls the repository, its CI, its key settings and its monitoring. The verifier controls one commit of its own. Each design rule keeps the verdict on the verifier’s side of that line.
Trust anchors live in the verifier’s committed code. Key fingerprints, timestamp-authority anchor names and digests, content roots, suffixes and required declarations sit in a specification the auditor commits and reviews. The package ships no trust anchor of its own and reads none from the environment. The chain and corpus specification objects check their trust values at construction: a fingerprint that is not a digest, an empty authority set, or a malformed release path refuses before any verification runs. The producer-key and append specifications carry no validator, and the key specification checks the scheme label alone. The keyring specification checks a nonempty key set, distinct identifiers and fingerprints, and a threshold between one and the key count, which a NaN threshold passed in 0.6.0, and nothing downstream closed that gap: the threshold check compared the satisfied count against the threshold and returned. 0.6.1 refuses any threshold that is not an integer at construction. The defaults that exist are mechanics, not trust. The candidate is HEAD unless named, names are held to the portable repertoire, and a receipt may precede its manifest’s recorded creation time by at most 300 seconds.
Rotation is loud. The release-chain lane pins one producer key by its fingerprint, and rotating it is an edit to that pin. The keyring API the encoder calls for its own signatures names current keys, a threshold and retired keys. The threshold call says whether retired keys may vouch: excluded, any retired key or signature presented refuses outright; allowed, they count toward the same threshold. The any-generation call, for a threshold of one, tries the current keys and then the retired ones; 0.6.1 lets the caller exclude the retired ones, as the threshold call already could. A malformed or mispinned public key is fatal in either call. In the threshold call, a missing signature or a missing public key is reported as absent, and a malformed signature whose key is present as failed; neither votes, so the check still passes when enough other keys satisfy the threshold. The any-generation call refuses a missing key or a malformed signature outright. There is no time window in which either generation silently works. A timestamp authority rotates in one of two ways. In the release-chain lane the specification pins each authority’s root, policy, signer certificate and signer key, and rotation is an edit to those pins. In the witness lane an authority rotates through a new immutable trust bundle with its own pinned identities, activated by the caller.
Verification runs offline on commodity tools. A full SHA-1 clone, Python 3.11 with the cryptography library at 42 or later, OpenSSL 3 and Git 2.36 suffice for the chain, time, signature and binding checks. A shallow, grafted, partial or SHA-256 clone refuses in custody. The verifier queries no authority. The workflow-attestation check is the one exception. It asks GitHub, through the GitHub CLI, whether the attestation service accepted the canonical subject for a commit under a workflow named by path and ref (Newman et al. 2022). That check trusts GitHub as a third party, pins the workflow by name rather than by its bytes, and takes the CLI’s exit status as its answer. The package supplies the pieces of a history sweep: the list of commits touching the protected tree, the enforcement epoch, the in-scope test and the repository slug; the caller composes them.
Specified gaps refuse. A missing token, a malformed row, an unknown key, a time out of range, a path that escapes its root, a name outside the repertoire: each refuses with a typed error. The verification libraries write nothing to stdout or stderr; the command renders acceptance and refusal, and exit codes carry the verdict. On the routes the differential harnesses cover (Section 4), the refusal texts are the production verifiers’ own, and a consumer’s own differential harness matches on them. Two base-ref routes match because the harness maps the package’s snapshot diagnostic onto the upstream wording, and the tree-subject checks add refusals upstream never emitted.
4 Evidence
Two kinds of evidence are checkable: the verdicts the released package renders on six scenarios over its own signed fixture, and the harnesses that hold the verifier to the production code it came from.
The package ships a corpus fixture that builds a fresh Git repository, an Ed25519 producer key, and two local RFC 3161 authorities with their own roots. It publishes two releases of a corpus of three content files and one attested file. The auditor’s specification lives outside the corpus, so a mutation of the corpus cannot change what the auditor expects. Four scenarios commit one mutation of a private clone; one mutates nothing, and one publishes a separate corpus from its own genesis. Each runs three ways. At first contact the command names --commit HEAD. With a base but no candidate pin it refuses as a usage error, because a base requires an expected commit. Pinned, it names --commit, --expect-commit and --base-ref at that scenario’s genesis. Table 2 gives the verdicts.
| Scenario | Change | First contact | Pinned to its genesis |
|---|---|---|---|
| pristine | none | PASS | PASS |
| hand edit | rules/tax/rate.yaml rewritten to 0.17 and committed; journal, manifests, signatures and tokens untouched |
FAIL, binding: content file 'rules/tax/rate.yaml' does not match its witnessed digest: … |
FAIL, binding |
| re-encode | the same change appended as a third signed, witnessed release with its journal row | PASS | PASS |
| swapped key | releases/anchors/producer-ed25519.pub replaced by another key |
FAIL, custody: producer public-key SPKI is not code-pinned: … |
FAIL, history: existing release file bytes changed relative to the base |
| regenerated | every manifest, signature and token rebuilt with the original key and authorities; content and journal unchanged | PASS | FAIL, history: existing release file was deleted relative to the base, naming the genesis token |
| dropped gate | published from its own genesis without ever declaring rulespec/compile |
FAIL, declaration: the witnessed journal does not declare a gate the pinned spec requires: … |
FAIL, declaration |
The hand edit and the re-encode leave the same rate in the same file, and only the journal tells them apart. The regenerated chain passes at first contact: custody for two releases, binding for the files, declarations for the gates, all genuine, all dated to the regeneration. It refuses only against a base, and the refusal names the first object the base held that the candidate lacks. The dropped gate passes custody and binding and refuses on the declaration the specification requires. The page at axiom.org/receipt shows these eighteen runs, and the capture recipe in the site’s repository regenerates them from the fixture.
The verifier has its own checks. The canonical-JSON module is PolicyEngine’s ledger file byte for byte (PolicyEngine, n.d.); a test pins its SHA-256. The release-chain and append-gate surfaces came from that ledger, and the witness and attestation surfaces from a forecasting repository’s record chain (Ghenis, n.d.), behind differential harnesses in the tradition of differential testing (McKeeman 1998). Each harness pins the upstream source files by SHA-256 and runs the unmodified upstream verifier as an oracle on real fixtures. The package must reproduce the oracle’s verdicts, accepted and refused alike, to the byte after two disclosed normalizations: surrounding whitespace, and the volatile identifiers OpenSSL prints on error lines. At v0.6.1 the four harnesses collect 108 cases: 43 release-chain, 28 append-gate, 17 witness and 20 attestation. Of those, 86 compare the package’s verdict against the oracle’s; the other 22 authenticate the oracle sources, exercise the commit subject where the directory oracle has no counterpart, or require the two to diverge. The witness fixture carries 53 snapshots, 52 available witnesses and real timestamp tokens; the attestation harness compares command construction against a local stub and claims no live-service equivalence. The 0.6 move from checkout to tree subject added cases the oracle cannot share. One rewrites a committed ledger row in the checkout only; the directory oracle refuses while the package accepts the unchanged commit. The signing module’s retired-key semantics match the release-key rotation procedure the statute corpus repository adopted (the Axiom Foundation, n.d.-b); they entered with direct tests and no oracle. The package’s remaining 1,471 tests pass at the tag.
5 Adoption
On 2026-09-10, two repositories pin receipt==0.6.1 on their default branch and call it from their verification paths, a third pins it on a branch that has not reached its default branch, and a fourth shows its output (Table 3).
| Repository | Pin | Surface called |
|---|---|---|
| TheAxiomFoundation/axiom-encode | receipt==0.6.1 |
threshold keyring check on apply manifests, through receipt.sign |
| ThesisInstitute/thesis | receipt==0.6.1 (custody extra) |
signature primitives in the snapshot signer and the chain verifier; producer signing armed |
PolicyEngine/chronicle, branch codex/thesis-ledger-facts |
receipt==0.6.1 |
release-chain and append-gate shims over receipt.release_chain and receipt.append_gate, whose shim headers require a fresh byte-equivalence proof against pinned legacy oracles before each receipt upgrade |
| TheAxiomFoundation/axiom.org | none in a dependency manifest; the capture recipe installs 0.6.1 | the receipt page shows eighteen fixture runs, byte-identical to the maintainer’s regeneration at v0.6.1 after masking the run-minted object ids, digests, times and temporary paths |
The New Zealand pilot (the Axiom Foundation, n.d.-a) ships, in its open pull request, an auditor guide, a specification and a witnessed journal. The guide’s recipe is written for receipt>=0.5 and its worked reproduction pins 0.5.0. Its base-ref recipe predates the 0.6 commit contract, which requires an expected commit alongside a base. The specification carries the custody key, two timestamp-authority anchors, one content root and six required gate identifiers. It commits a witnessed journal of 104 rows: 80 content files, 7 attested context paths and 17 gate declarations, every digest matching its committed blob.
A producer proposes a specification by shipping it in the repository. An auditor who verifies against it as found establishes consistency with a policy the producer shipped. Independent custody begins when the auditor reads the specification once, pins its digest and the anchor-set digest in the auditor’s own records, and supplies both on every later run. What a gate row does and does not establish is in Table 1.
A package pin fixes the verifier’s bytes; custody comes from the trust pins the auditor supplies. What an encoder run produced is a separate claim, and the foundation’s corpora make it separately. The encoder signs an apply manifest binding source, tool, generated-output digest and applied-file digests, and the shared validation workflow refuses a change to a guarded rule file that no manifest covers.1 That check runs in the producer’s trust domain, and it supplies what the custody verdict cannot (Table 1).
7 Limits
A pass establishes custody, not truth (Table 1). Freshness needs an expectation from outside the clone, and comparing head digests out of band is how two auditors learn that their views diverge. The base recipe in Section 2 reaches forward from first contact only. Later content may still be superseded through the journal, by design: the base protects release objects, not the current binding.
The consumer chooses the authorities and the verifier enforces the configured set; it cannot show that authorities fail independently. The release-chain lane requires one token from every configured authority for every release, so a release missing one authority’s token cannot pass until the consumer revises policy. The witness lane accepts a declared outage with a reason as a declaration and can verify a record with zero tokens. A consumer who needs a floor reads the returned status and token list and enforces it in their own code; the specification has no field for it. Certificates are validated as of the token’s signed time through OpenSSL without revocation checking, and no renewal protocol carries aging evidence forward (Gondrom et al. 2007).
The differential harnesses cover finite batteries, and each test file carries its pins, commands and exclusions, which is also its rerun recipe. The package inherits the assumptions of its primitives: signature schemes and hash functions unbroken, authorities honest about time. Ordinary object reads rehash with plain SHA-1; optional store-wide verification hands collision detection to Git’s own detector and requires Git 2.50. Key compromise before rotation defeats the signature layer for that generation.
The package verifies and supplies signing primitives; it does not produce manifests, witnesses or journals, which come from the producers’ own tooling. The package supplies no producer-side reference implementation and no inert specification format. A contributed producer-side evidence-record type, non-authorizing by design, is open (Storey 2026). The package is Python; producers on other stacks interoperate at the artifact layer, in canonical JSON, manifests, tokens and signatures.
8 Availability
receipt is on PyPI (MIT), with source, port diffs, review records and differential harnesses at github.com/TheAxiomFoundation/receipt, the package page at axiom.org/receipt, and the API reference at axiom.org/receipt/api.
Acknowledgements
Nathan Storey’s review and his contributed evidence-record proposal shaped the account of the producer side in Section 7.
References
Footnotes
rulespec-us at
d58cc0cecalls the shared workflow at7a1eb243with the generated-file guard on andguard-programs-rootoff (134 files underprograms/unguarded); manifest schemaaxiom-encode/applied-rulespec/v5, fields atsrc/axiom_encode/cli.py:503of29b30fb7. Observed 2026-09-05.↩︎