Verify the receipt yourself — offline, no live CodeRifts API call needed.
Every CodeRifts verdict response can carry a signed chain_receipt. This repo is
a tiny, independent verifier -- a handful of dependency-free files in Node, one
file in Python -- that checks a receipt's signature (and, optionally, a whole chain of them) against the
published Ed25519 public key. If verification passes, the verdict provably came
from the holder of the CodeRifts signing key and, in a chain, follows an unbroken
link from the previous receipt. You do not have to take our word for it, and you
do not need any API key to check.
The exact byte format is frozen in RECEIPT_FORMAT.md.
- The receipt is a well-formed
base64url(body).base64url(signature)token. - The
body.kidmatches the active published key id. - The raw Ed25519 signature verifies over the exact signed bytes
(
crchain.v1|kid|fp|prev|caller|ts; v2 appends|reg; v3 appends|reg|ir; v4 appends|reg|ir|expires_at|bh). Thecrchain.v1prefix is the signed-format tag (not the envelope version) and does not change for v4 — see RECEIPT_FORMAT.md section 3. Version nesting is v1 ⊂ v2 ⊂ v3 ⊂ v4: a lower-version verifier never accepts a higher-version receipt. - For a chain: every receipt's signature is valid AND each non-genesis link's
prevequalssha256:+ SHA-256 of the previous token string. - v4 envelope binding (optional
--envelope <file>): the verifier recomputessha256:+ SHA-256 of the RFC 8785 (JCS) canonical decision envelope withreceiptanddecision_body_hashremoved, and requires it to equalbh. Mismatch ⇒ reasonbody_hash_mismatch. Independently, whenexpires_atis present andexpires_at + 30s(clock-skew leeway;CLOCK_SKEW_LEEWAY_MS) is in the past,statusisVERIFIED_EXPIRED(signature may still be authentic;validis false for that status). 0s leeway for destructive operations in production when the intended context declares them — this offline verifier hasenvironment(envelope /--environment) but no destructive / operation_class field, so it always uses the 30s default (never guessed from operation labels). v4 is issued only on envelope-bearing paths (bundle + MCP single-spec); see RECEIPT_FORMAT.md.
It does NOT recompute the verdict fingerprint fp from a verdict payload; fp
is verified as an opaque, signed binding (see RECEIPT_FORMAT.md section 2).
npx @coderifts/receipt-verifier "$(cat receipt.txt)" # one-off, nothing installed globally
npx @coderifts/receipt-verifier --from-commit <sha> --contract api/openapi.yaml
npm i -g @coderifts/receipt-verifier # or install the command
coderifts-receipt-verifier "$(cat receipt.txt)"
The command is cli.js (the coderifts-receipt-verifier bin); it reads the vendored key snapshot
shipped in the package (keys/coderifts-keys.json, pinned by its .sha256). As a library:
require('@coderifts/receipt-verifier') is verify.js; the other verifiers are
@coderifts/receipt-verifier/verify-grant.js and so on (see exports in package.json).
- Node: >= 18 (zero dependencies; uses
node:cryptoonly; the suite runs on 18, 20, 22 and 24).
MEASURED 2026-09-13, by doing what a reader does: downloading the files into an empty directory and running the command.
verify.js is the LIBRARY. It has not been a runnable CLI since 1129 (2026-08-28)
split the shared arity shim out of it, and node verify.js <token> now fails with
Cannot find module './arity' — and would print nothing even if that resolved,
because the CLI moved to cli.js. Fetching verify.js alone and running it, which
is what the older instructions said to do, cannot work.
Four files, and the command is node cli.js:
verify.js the verify logic
arity.js the shared (token, opts) shim, required at load time
cli.js the entry point
keys/coderifts-keys.json the vendored key snapshot cli.js reads by default
The simplest way to get them is the npm package (above), or a clone of the repo. --from-commit
also needs receipt-from-commit.js and commit-binding.js. To fetch the four by hand:
mkdir -p keys
for f in verify.js arity.js cli.js; do
curl -sSO "https://raw.githubusercontent.com/coderifts/receipt-verifier/main/$f"
done
curl -sS -o keys/coderifts-keys.json \
"https://raw.githubusercontent.com/coderifts/receipt-verifier/main/keys/coderifts-keys.json"
node cli.js "$(cat receipt.txt)"
Python is different and the difference is measured, not assumed: verify.py
imports the arity shim INSIDE the functions that need it, so the single file still
runs standalone. python3 verify.py <token> --keys <url|file> was confirmed to
return {"valid":true,"status":"VERIFIED_CURRENT",…} from an empty directory with
nothing but that one file.
- Python: >= 3.10 with the
cryptographypackage (pip install cryptography).
# Get a verdict and its receipt (this endpoint needs no API key), then verify.
RECEIPT=$(curl -s -X POST https://app.coderifts.com/api/v1/action-verdict \
-H 'Content-Type: application/json' \
-d '{"action_type":"tool_call","provenance":{"channel":"ci_manifest","issuer_trust":"trusted"},"tool":{"name":"get_customer","capabilities":["read"]},"memory":{"op":"read","namespace":"working","staleness_hours":1}}' \
| node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.stdout.write(JSON.parse(s).chain_receipt))")
# Default: vendored key snapshot (offline). --refresh-keys fetches the live registry.
node cli.js "$RECEIPT"
Output (exit code 0 = valid, 1 = invalid, 2 = usage error). The action-verdict
endpoint issues v:2 receipts (they carry reg):
{"valid":true,"payload":{"v":2,"kid":"2026-07-k1","fp":"sha256:...","prev":"null","caller":"anon","ts":"...","reg":"..."}}
Receipts from the /diff endpoint and the GitHub PR-check webhook are v:3 — they
additionally carry ir (the Change-IR hash), e.g. the demo receipt below.
RECEIPT=$(curl -s -X POST https://app.coderifts.com/api/v1/action-verdict \
-H 'Content-Type: application/json' \
-d '{"action_type":"tool_call","provenance":{"channel":"ci_manifest","issuer_trust":"trusted"},"tool":{"name":"get_customer","capabilities":["read"]},"memory":{"op":"read","namespace":"working","staleness_hours":1}}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['chain_receipt'])")
python3 verify.py "$RECEIPT"
The CLI default is already offline: it verifies against the vendored snapshot
keys/coderifts-keys.json. Pin a PEM with --key, or a local registry with
--keys <file>, for the same boundary. --keys https://…, --fetch, and
--refresh-keys are network calls — they are not offline.
# Save the current public key once (or commit a PEM you have pinned).
curl -s https://app.coderifts.com/api/v1/attestation/public-key \
| python3 -c "import sys,json;print(json.load(sys.stdin)['public_key_pem'])" > pub.pem
node cli.js "$RECEIPT" --key pub.pem --kid 2026-07-k1
python3 verify.py "$RECEIPT" --key pub.pem --kid 2026-07-k1
Put one token per line, oldest first, then:
node cli.js --chain receipts.txt --key pub.pem --kid 2026-07-k1
python3 verify.py --chain receipts.txt --key pub.pem --kid 2026-07-k1
The first receipt is reported as genesis (its prev is the literal null) or
continuation (it links to a token you did not supply). Every later link is
checked against the SHA-256 of the token before it.
The demo pull request (coderifts/demo PR #4) carries a live v:3 receipt in its
CodeRifts check comment. Copy that token and verify it — the default uses the
vendored key snapshot shipped in this package, so this is the one command a
reader runs to confirm the demo verdict is genuine, offline:
node cli.js "<paste the receipt token from the PR comment>"
# fully offline (same as the default): vendored snapshot, or pin a file/PEM
node cli.js "<token>" --keys keys/coderifts-keys.json
node cli.js "<token>" --key pub.pem
# live registry (NOT offline — this is a network call):
node cli.js "<token>" --refresh-keys
node cli.js "<token>" --keys https://app.coderifts.com/.well-known/coderifts-keys.json
Both print {"valid":true,...} and exit 0 for a genuine receipt.
Instead of a single pinned key, resolve the signing key by the receipt's kid
from the published append-only registry. This keeps receipts issued under a
now-retired key verifiable:
node cli.js "$RECEIPT" --keys https://app.coderifts.com/.well-known/coderifts-keys.json
python3 verify.py "$RECEIPT" --keys https://app.coderifts.com/.well-known/coderifts-keys.json
# or against a local copy of the registry:
node cli.js "$RECEIPT" --keys coderifts-keys.json
The registry is { "keys": [ { "kid", "public_key_pem", "status", "valid_from" } ] }.
--key and --keys are mutually exclusive.
Every artifact in this family attests something that happened — a decision, a grant, a commit, a coverage observation. A refusal can look like it produces nothing, so an operator wanting to show "the agent tried to deploy and the gate stopped it" assumes they have only their word for it.
They do not. A BLOCK mints a signed receipt exactly like an ALLOW, and this verifier already
checks it — no extra tooling, no separate artifact family.
The receipt payload alone will not tell you which it was: an ALLOW receipt and a BLOCK receipt
have the same field set, and neither carries the verdict. What carries the verdict is the
decision_result envelope, and payload.bh binds it. So refusal evidence is two files:
# the receipt, plus the envelope it was minted for
node cli.js "$(cat receipt.txt)" --envelope decision_result.json
# -> {"valid":true,"status":"VERIFIED_CURRENT", ...}The verifier recomputes the envelope's canonical body hash (RFC 8785, with receipt and
decision_body_hash removed) and requires it to equal payload.bh. Passing means the envelope in
front of you is the one that receipt was issued for. Read the verdict from that envelope:
decision, execution_action, operation, and blocking_reasons.
Editing the envelope breaks the binding — flipping "decision":"BLOCK" to "ALLOW" returns
INVALID_SIGNATURE / body_hash_mismatch. That is what makes it evidence rather than a claim.
What this proves: at the signed timestamp, a caller presented this change set under this intended operation, the gate returned this verdict, and the envelope has not been altered since.
What it does NOT prove — and the distinction matters more here than anywhere else in this
document: it does not prove the agent then stopped. It is evidence about a decision we issued,
never about what the caller did next. An agent that received STOP and shipped anyway through some
other path produces exactly this same receipt. Nothing in this family observes execution that did
not come back through us; a refusal receipt is proof of what the gate said, not of what the world
did. If you need evidence that the guarded path is the only path, that is a different question —
see @coderifts/bypass-probe, which measures it at your own installation and reports what it could
not reach.
A cr.exec.v1 grant is the short-lived, mutation-bound sibling of a chain
receipt. This repo ships verify-grant.js / verify_grant.py as the public,
dependency-light verifiers (same CLI feel as verify.js / verify.py).
What GRANT_CURRENT proves / does not prove (from
coderifts-app/docs/cr-exec-v1.md):
- Proves: an Ed25519 signature under a published
kidcovers the grant body; the grant names areceipt_digest(sha256 of a receipt token) — it is derived from a receipt, not a replacement;exphas not passed (30s clock-skew leeway); if you supplied intended audience / operation / target_id / after-payload (orscope_hash), those match. - Does not prove: that any gateway, agent host, or CI check enforced the
grant; that the underlying receipt is currently authorized (re-check the
receipt with
verify.js); one-use / atomic consumption; proof-of-possession (cnfis reserved, unimplemented). - Bearer / replay / TTL. Within TTL, a stolen grant authorizes the same operation/target/after-shape for any presenter. Replay against the same target is possible for bearer grants; deploy one-use profiles where atomicity is required. Default TTL is 300s.
- Retired
kid→UNKNOWN_KEY. Grants are live execution permission; receipts may forensically verify a retired key, grants must not.
# Node (zero deps). --keys is the registry; --key pins a PEM like verify.js.
node verify-grant.js "$GRANT" --keys https://app.coderifts.com/.well-known/coderifts-keys.json \
--intended-operation merge --intended-target "$TARGET" --intended-audience "$AUD" \
--intended-after-file after.txt --receipt "$RECEIPT"
# Python (cryptography, same as verify.py).
python3 verify_grant.py "$GRANT" --keys https://app.coderifts.com/.well-known/coderifts-keys.json \
--intended-operation merge --intended-target "$TARGET" --intended-audience "$AUD" \
--intended-after-file after.txt --receipt "$RECEIPT"
Exit 0 iff status is GRANT_CURRENT. JSON on stdout matches across JS and
Python (valid, status, reason?, decoded payload).
A cr.exec.attest.v1 token is the executor's signed commit statement — the
fourth chain artifact. Public verifiers: verify-attest.js / verify_attest.py.
Format freeze: RECEIPT_FORMAT.md (attestation section) and
coderifts-app/docs/cr-exec-attest-v1.md.
Executor keys are CUSTOMER-HELD. --keys is REQUIRED (a registry file
or URL). There is no default fetch — CodeRifts does not hold these keys.
Honesty (verbatim class from the spec):
The attestation proves:
a holder of the executor key asserts this commit
It does NOT prove:
- that the executor's code is unmodified (deploy attestation is out of scope, named as such — a later artifact, not this one);
- that a human saw anything;
- that the underlying grant is currently
GRANT_CURRENT(re-check the grant if live permission is required; this statement is historical); - that
result_digestis a CodeRifts fingerprint, a receipt digest, or an after-payload hash —result_digestis the executor's choice of bytes.
Retired kid uses the receipt-class historical rule:
ATTEST_RETIRED_KEY_VALID_AT_ISSUE when committed_at is inside
[valid_from, retired_at). Grants must not (live permission → UNKNOWN_KEY).
# --keys is required (customer-pinned executor registry). No default fetch.
node verify-attest.js "$ATTEST" --keys executor-keys.json
python3 verify_attest.py "$ATTEST" --keys executor-keys.json
# Optional cross-checks against the grant / receipt digest held by the caller:
node verify-attest.js "$ATTEST" --keys executor-keys.json \
--grant "$GRANT" --receipt-digest sha256:…
Exit 0 iff status is ATTEST_VALID or ATTEST_RETIRED_KEY_VALID_AT_ISSUE.
JSON on stdout matches across JS and Python.
Statuses: ATTEST_VALID, ATTEST_INVALID_SIGNATURE, ATTEST_UNKNOWN_KEY,
ATTEST_RETIRED_KEY_VALID_AT_ISSUE, ATTEST_MALFORMED, ATTEST_UNBOUND.
A compact receipt or execution attestation can be exported as a DSSE envelope carrying an in-toto Statement, so a system that already speaks in-toto (SLSA tooling, policy engines, admission controllers) can accept it without learning the compact format. Format freeze: RECEIPT_FORMAT.md section 9.
const { toDSSE, fromDSSE } = require('./to-dsse.js');
const envelope = toDSSE(token); // wrap — nothing is signed or checked here
const back = fromDSSE(envelope); // unwrap — byte-exact: back === token
verifyReceipt(back, { keyring }); // the SAME verifier decides, unchanged| field | value |
|---|---|
payloadType |
application/vnd.in-toto+json |
predicateType |
https://coderifts.com/attestations/agent-action-authorization/v1 |
The export is optional; the compact form stays primary. signatures[0].sig
is the artifact's own signature, copied rather than recomputed, and the predicate
carries the original base64url payload segment verbatim — which is why the
round-trip is byte-exact and the existing verifier accepts the result unchanged.
Honesty (verbatim class from the spec):
The DSSE envelope proves:
exactly what the compact artifact proves
It does NOT prove:
- that anything was re-verified — no signature is checked while building or
reading an envelope.
toDSSEwraps a known-invalid artifact just as happily; - anything the compact artifact does not already establish — a standard container does not strengthen a claim.
Verification stays with verify.js / verify-attest.js. A tampered envelope is
caught in one of two places, never silently: a rewritten decoded predicate field
is refused by fromDSSE (PREDICATE_MISMATCH), and a rewritten payload segment
unpacks but fails signature verification. RECEIPT_FORMAT.md section 9 states the
full invariant table an external verifier can rely on.
All seven verifiers accept (token, opts) with opts.ctx (keyring / publicKey / registry)
and opts.intended (cross-check bindings):
verifyReceipt(token, { ctx, envelope })
verifyChain(tokens, { ctx })
verifyExecutionGrant(token, { ctx, intended })
verifyExecutionAttestation(token, { ctx: { registry }, intended })
verifyToolsetAttestation(token, { ctx: { registry, entries }, intended })
verifyMonitoringAttestation(token, { ctx: { registry }, intended })
verifyBundle(bundle, { ctx, perSlot })The former 3-ary signatures remain as deprecated wrappers: they warn
once (DeprecationWarning) and forward into the unified shape:
verifyReceipt(token, ctx, opts) // deprecated wrapper
verifyChain(tokens, ctx, opts) // deprecated wrapper
verifyExecutionGrant(token, ctx, opts) // deprecated wrapper
verifyBundle(bundle, ctx, opts) // deprecated wrapper
verifyExecutionAttestation(token, opts) // 2-ary; throws on a 3rd (1128)
verifyToolsetAttestation(token, opts) // 2-ary; throws on a 3rd (1128)
verifyMonitoringAttestation(token, opts) // 2-ary; throws on a 3rd (1128)1128 still throws on a third argument to the original 2-ary verifiers
(verifyExecutionAttestation, verifyToolsetAttestation, verifyMonitoringAttestation).
A dropped third argument used to skip the cross-check and grade a mismatch valid.
Python mirrors: verify_receipt / verify_execution_grant / verify_chain accept a unified
dict as the second argument; a second+third positional pair warns once. Attest / monitor already
opts-based. The Python toolset verifier remains keyword-based —
verify_toolset_attestation(token, registry, entries, intended, now_ms) — and also accepts a
unified opts dict as the second argument. Extra positionals raise TypeError. Unifying the
remaining keyword-only spelling is a later versioned wave.
Do not detect the shape with Function.length. Parameters with defaults are not counted.
cli.js <receipt> [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>] [--refresh-keys] [--envelope <file>]
cli.js --chain receipts.txt [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>] [--refresh-keys]
verify.py <receipt> [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>] [--refresh-keys] [--envelope <file>]
verify.py --chain receipts.txt [--key pub.pem | --keys <url|file>] [--kid <kid>] [--fetch <url>] [--refresh-keys]
verify-attest.js <token> --keys <file|url> [--grant <token>] [--receipt-digest sha256:…]
verify_attest.py <token> --keys <file|url> [--grant <token>] [--receipt-digest sha256:…]
verify-bundle.js <bundle.json> --slot-keys <file> [--ctx <file>]
--key <pem>verify against a local SPKI PEM public key (offline).--keys <src>resolve the key bykidfrom a registry (URL or file); accepts active and retired keys. Mutually exclusive with--key. A URL is a network call; a file is offline.--kid <kid>expected key id; mismatch yieldsunknown_kid.--refresh-keysopt-in live fetch of the well-known registry. Mutually exclusive with--key/--keys. This is not offline.--fetch <url>opt-in key-discovery URL (default well-known registry), used only when--key/--keysare absent. Accepts the registry array (active + retired) and the legacy single-key body from/api/v1/attestation/public-key. This is not offline.- Default (no
--key/--keys/--fetch/--refresh-keys): the vendored snapshotkeys/coderifts-keys.json(offline). Refresh the pin: seekeys/README.md. --envelope <file>(v4) path to a JSON decision-envelope file. The verifier recomputes the RFC 8785 (JCS) canonical body hash (withreceiptanddecision_body_hashstripped) and requires it to equal the receipt'sbh. Mismatch ⇒body_hash_mismatch. Bothverify.jsandverify.pyread a file path (not an inline JSON string).
Example (v4 body-hash binding — use a real envelope file from an authorize / bundle path; do not invent a receipt token):
node cli.js "$RECEIPT" --envelope decision.json
python3 verify.py "$RECEIPT" --envelope decision.json
Error reasons (signature/structure failures; see RECEIPT_FORMAT.md section 6):
malformed_structure, bad_json, unknown_kid, signature_error,
signature_mismatch, delimiter_in_field, body_hash_mismatch,
retired_key_after_issue, KEY_REVOKED, KEY_RETIRED_AFTER_SIGNING.
Live status values include VERIFIED_CURRENT and, for v4 when
expires_at + 30s is in the past, VERIFIED_EXPIRED (signature authentic but
expired — valid is false). 30s clock-skew leeway on expiry; 0s for destructive
operations in production when the intended context declares them (this surface
has no destructive field, so the 30s default always applies). Full 12-status
taxonomy: RECEIPT_FORMAT.md section 6.
Verifies a cr.bundle.v1 document: several signed artifacts presented together, each graded in its
own slot, with the whole reported as VERIFIED / INVALID / EMPTY.
node verify-bundle.js bundle.json --slot-keys slot-keys.json--slot-keys, not --keys, and the difference is not cosmetic. The slots are signed by
different parties, and the slot verifiers do not take key material in the same shape: verifyReceipt
and verifyExecutionGrant read ctx.publicKey / ctx.expectedKid, while the attestation verifiers
read opts.registry. There is therefore no single registry to hand the bundle. This flag takes the
library's own opts object, unchanged:
{
"perSlot": {
"receipt": { "ctx": { "publicKey": "-----BEGIN PUBLIC KEY-----…", "expectedKid": "2026-07-k1" }, "opts": {} },
"execution_grant": { "ctx": { "publicKey": "…", "expectedKid": "…" }, "opts": { "intended": { } } }
}
}The CLI adds no key handling of its own, so it cannot disagree with the library about what a slot was checked against.
Exit codes: 0 VERIFIED · 1 INVALID or EMPTY · 2 usage or load error. A file that cannot be
read or parsed is a load error, never a verdict about someone's proof.
Note on EMPTY: a bundle whose only present slot is INVALID reports EMPTY, because the verdict
asks "did anything verify?" first (verify-bundle.js:239). The exit code is 1 either way; read
the slots array for what actually failed.
This is not coderifts verify-proof-bundle. That spelling is the future app-CLI verb
(roadmap 1107) and lives in the coderifts package. This repository ships no coderifts command
and has no package.json; like every other verifier here, it is invoked as node <file>.js.
There is a single active signing kid at any time. When CodeRifts rotates the
key, the previous kid is marked retired (never removed) in the append-only
registry at /.well-known/coderifts-keys.json. The CLI default loads the
vendored snapshot of that registry (keys/coderifts-keys.json) offline.
--refresh-keys (or --keys <url> / --fetch <url>) fetches the live
registry so a receipt signed under a newly-retired kid still resolves as
RETIRED_KEY_VALID_AT_ISSUE when ts predates retired_at. A single pinned
--key, or --fetch of the legacy /api/v1/attestation/public-key body,
still sees only the active kid — receipts signed under a previous kid then
fail as unknown_kid. --keys <file> remains the explicit local registry pin.
Refresh the vendored pin with the commands in keys/README.md.
retired_at is planned rotation: a receipt whose ts predates it still
verifies; ts >= retired_at is KEY_RETIRED_AFTER_SIGNING. revoked_at is
compromise: every receipt under that key is KEY_REVOKED, including those
signed before the timestamp — revocation cannot retroactively prove a receipt
was signed by an honest holder, it can only invalidate. Entries without either
field are unchanged (this verifier's reading; the app registry does not mint
revoked_at on key entries yet — follow-up).
test/consume-and-commit-vectors.json pins the contract a future
consumeAndCommit(grant, state_nonce, expected_state, mutation) service
must satisfy: committed | refused | indeterminate, with error codes
NONCE_EXPIRED, NONCE_ALREADY_CONSUMED, NONCE_GRANT_BINDING_MISMATCH,
NONCE_WRONG_EXECUTOR, TARGET_STATE_CONFLICT, PAYLOAD_HASH_MISMATCH,
EXECUTION_INDETERMINATE, ATTESTATION_PENDING.
These are input/expected fixtures, not signed tokens. There is no
verify-consume.js / verify_consume.py. The public verifier is
stateless and cannot detect a replayed nonce. The production consumer is
the stateful data plane (EP-2) and is not this repository. Do not treat
the presence of the JSON file as a verifier.
node test/gen-vectors.js # regenerate receipt vectors with a fresh EPHEMERAL key
node test/gen-grant-vectors.js # regenerate cr.exec.v1 grant vectors (ephemeral key)
node test/gen-attest-vectors.js # regenerate cr.exec.attest.v1 EG-A-* vectors (ephemeral key)
bash test/run.sh # every vector through JS and Python (receipts + grants)
test/run.sh fails if the two implementations ever disagree, if any vector does
not match its expected {valid, reason}, or if exit codes diverge. It covers v1,
v2, and v3 vectors (including v3 tamper cases: flipped ir, flipped reg, wrong
field order), the --keys registry-resolution path, and one real production
v3 receipt captured from coderifts/demo PR #4 (the live block in
vectors.json, verified against the live prod PUBLIC key). The regenerated
ephemeral-key vectors never use the production key; the live block is a frozen
captured artifact and is not regenerated. Implementations also support v4
(expires_at, bh, --envelope); see RECEIPT_FORMAT.md for the frozen byte
layout and status taxonomy.
A second, independent reference verifier lives in
contract-verify/. It is the verify half of the contract
discovery and attestation protocol: an agent fetches a small signed bundle
describing a tool's current API contract, checks it, and gets a verdict before
any tool code runs.
It is a sibling, not a duplicate. The two verify different objects:
| This repo (root) | contract-verify/ |
|
|---|---|---|
| Object verified | chain_receipt — a signed record that a verdict was issued |
A signed contract bundle advertised at a well-known endpoint |
| When it runs | After a decision, to prove provenance | Before a tool call, to gate execution |
| Checks | Ed25519 signature, kid, chain prev linkage, v4 envelope binding |
Signature, contract hash pin, freshness/TTL, rollback, verdict gate |
| Trust input | Published CodeRifts signing key | Caller-supplied trusted signer set |
Both are dependency-free and offline: root needs cryptography for Python,
contract-verify/ needs nothing but python3.
cd contract-verify
python3 -m unittest # 24 tests
python3 check_vectors.py # 9 cross-implementation conformance vectors
python3 demo_loop.py # full fetch + verify round trip on a local stdlib server
Its own README, schema (bundle.schema.json, SCHEMA.md) and conformance
vectors (test-vectors.json) travel with it. test/run.sh at the repo root does
not reach into contract-verify/; the two suites are run separately.
verify.js and its siblings are byte-copied into the packages that verify
offline, each under a VENDOR.sha256 pin naming a signed tag. That is the point
of the pin: a copy either matches the named release or the copying repo's own
test fails. It also means a copy does not move when this repo does — it moves
when someone re-vendors, deliberately.
Measured 2026-09-13, and stated here rather than left for a reader to
discover: commit 6c2d115 corrected the comment block at the top of
verify.js. The old wording said "the GitHub Action + other embedders use
these directly" and listed verifyChain first; that is false for the Action,
which calls verifyReceipt and never verifyChain. The correction names the
actual callers instead.
The downstream copies still carry the old comment. The difference is comment-only — 11 inserted and 3 deleted lines of JavaScript comment, no executable change — so every copy verifies identically to this one. What is wrong in them is a sentence about who calls the API, not what the API does.
They are deliberately not being re-vendored for it. Doing so faithfully means a
new signed tag here, a re-vendor and a pin rewrite in five repositories, five
test suites, and four npm releases whose changelogs would say "vendored comment
text". That spends four version numbers and a reader's attention on nothing
observable. Re-vendoring cheaply — copying from this repo's HEAD instead of a
tag — is worse: those pins name a signed released tag and tell the reader to
run git tag -v, and pointing them at an untagged commit would trade a property
a stranger can check for a comment correction.
The visible cost of waiting, so nobody debugs it twice: two of those
repositories (@coderifts/sdk and @coderifts/conformance) carry a test that
compares the vendored bytes against this repo when it is checked out beside
them. That test now fails locally for anyone with both repos on disk, and
skips in CI, where the source is absent. It is not a broken verifier and it is
not your working copy — it is this comment, and it clears at the next
re-vendor. The copies pick the correction up at the next substantive vendor
change, and until then this paragraph is where the discrepancy is recorded.
MIT.