NOA Mandate Core
The open-source core of NOA Mandate: Apache-licensed receipt protocols, reference implementations, and conformance artifacts for independently built integrations.
NOA Mandate is the parent brand. NOA Mandate Core is this open-source project; NOA Mandate Enterprise is the separately maintained commercial product. Enterprise services and customer data are outside this repository. Package names, CLI commands, and versioned protocol identifiers remain unchanged.
NOA Receipt is an open protocol for signed, independently verifiable receipts of AI-agent actions: before an agent does something real, a governance layer decides allow · hold for a human · block, and emits a tamper-evident, hash-chained record that anyone can verify offline — no account, no network, no dependency on us. This repository is the kernel: the protocol, the reference implementations, and the conformance suite that holds each of them to the identical verdict on every vector it is run against — which is not every vector for every implementation, and the matrix says per class exactly which.
One concrete case. An agent wired to a payments tool decides to send 7,000 — pick whichever
currency you like; the rule is written against the amount, not the symbol. The call never reaches the
payments tool: the proxy in front of it holds the call and writes a signed DEFERRED receipt
instead. A request goes to the configured approver client, and the session stays blocked apart from
the one exact retry, which goes through only after a holder of that key has signed. If nobody signs,
the call is never forwarded.
When someone does sign, the chain reads DEFERRED → ALLOWED → EXECUTED, each link signed and
hash-chained onto the one before it. Six months later, when someone disputes that the transfer was
ever authorized at all, the other side can check that chain offline — no account, no network, nothing
to ask us for — and it states which agent, which action, the hash of the exact parameters, which
approver identifier signed, and in which order.
The ceiling on that case, in plain words. What the chain establishes is that a key holder
approved this exact intent before the call was allowed through. It does not establish what the
payments tool then did: this system never watches the executor, and the remote system of record is
the only witness to a side effect (NC-1.3). So the code emits a reason code that says as much,
rather than a friendlier one — HUMAN_APPROVED_INTENT_NOT_EXECUTION_BOUND, approval of an intent,
without binding to the execution that follows (NC-6.6). The plain HUMAN_APPROVED token stays
reserved in the union, is emitted by nothing, and a test fails if anything starts emitting it. Two
further edges belong in the same breath: the approver identifier is an opaque string, and binding it
to a real person is your identity system's job rather than ours (NC-3.2); and what is gated is the
path through the proxy, so an agent that can reach the payments tool by another route was never
inside this boundary (NC-6.6). Every one of those codes is a numbered, normative entry in
NON-CLAIMS.md.
Every version number on this page comes from a self-updating registry badge, and this README (plus SECURITY.md) is gated mechanically on every push by
scripts/lint-doc-truth.mjsagainst 8 specific rules: a version literal next to a package name must match the npm registry, a claimed npm publication must actually resolve, no hardcoded test count, every relative link resolves, the attack/malformed-vector and implementation counts are re-derived from the repository, the quickstart'sbashblocks are executed end to end,npx noa(a stranger's package) is refused, and the license badge matchespackage.json. It checks those specific, countable claims — not prose, tone or completeness, and deleting a claim always satisfies it. A README that can drift silently is a README nobody can rely on.
Tamper-evident provenance: it proves a record was produced under the stated rules and was not altered afterwards — never that the action was right, and never that the action happened. What we deliberately do not claim →
60-second quickstart
Copy-paste, in an empty directory. Nothing here contacts NOA, and every block below is executed by CI on every push — if one of them stops working, the build goes red.
mkdir -p noa-quickstart && cd noa-quickstart
npm init -y > /dev/null
npm install noa-receipt # no runtime dependencies; Node >= 20 stdlib only
# Sign a receipt for an action, then write the chain and the PUBLIC keyring to disk.
node --input-type=module <<'EOF'
import { writeFileSync } from "node:fs";
import { generateKeyPair, buildReceipt } from "noa-receipt";
const kp = generateKeyPair("demo-key-1");
const receipt = buildReceipt(
{
id: "rcpt_0",
ts: new Date().toISOString(),
scope: { chain: "quickstart:demo" },
agent: { id: "quickstart-agent", model: "vendor/model-v1", principal: "SERVICE" },
action: {
id: "payment.refund",
canonical: "payment.refund",
riskClass: "LOW",
paramsHash: "sha256:" + "0".repeat(64), // never carry raw params — only their hash
reversible: false,
rollbackRef: null,
},
governance: { mode: "on", verdict: "EXECUTED", ruleId: "low-risk-auto", approval: null, sandboxed: false },
},
null, // no previous receipt: this is the genesis of the chain
{ kid: kp.kid, privateKey: kp.privateKey },
);
writeFileSync("chain.json", JSON.stringify([receipt], null, 2));
writeFileSync("keyring.json", JSON.stringify({ [kp.kid]: kp.publicKey }, null, 2));
console.log("wrote chain.json + keyring.json");
EOF
# Verify it offline, in a separate process. Prints status VALID and exits 0.
./node_modules/.bin/noa verify chain.json --keyring keyring.json
# Now alter one field of the signed record and watch the chain reject it.
sed 's/"quickstart-agent"/"someone-elses-agent"/' chain.json > tampered.json
if ./node_modules/.bin/noa verify tampered.json --keyring keyring.json; then
echo "UNEXPECTED: a tampered chain verified"; exit 1
else
echo "rejected as expected, exit code $? (2 = TAMPERED)"
fi
Exit codes are CI-ready: 0 VALID · 1 UNVERIFIED (no keyring supplied) · 2 TAMPERED · 3
MALFORMED · 4 usage · 5 UNTRUSTED (identity binding failed) · 6 witness quorum incomplete ·
9 KEY_RETIRED (authentic signature by a lifecycle-retired key; refused for current use, see
below). Historical mode additionally uses 7 for an authenticated checkpoint conflict and 8 for
cryptographic integrity failure, so shell consumers do not have to relabel either as legacy
TAMPERED or MALFORMED.
⚠ The CLI binary is called
noa, but the npm package namednoais not ours — it belongs to an unrelated third party. Run the local binary as above, ornpx --package noa-receipt noa verify …, which names the package explicitly. This README is linted for that mistake.
The verifier is honest about its own limits in the same output: without a keyring it will not claim VALID, and without a checkpoint it warns that tail-truncation cannot be detected offline.
Historical verification after key retirement
verifyHistoricalChain and CLI --purpose historical are the versioned
noa.historical-verification/0.1 side contract. They retain a lifecycle key's public material to
check old receipt bytes while leaving verifyChain and every current-use authorization surface
fail-closed on a retired key.
On the default (current-use) path, verifyChain first authenticates a retired key's signature
against that retained public material. A forged or altered receipt or checkpoint naming the key
stays TAMPERED. An authentic one yields KEY_RETIRED (CLI exit 9), but only when every other
check passed; any other failure in the input outranks it. KEY_RETIRED is a refusal, never an
acceptance: signaturesVerified and tailChecked stay false, and it does not establish that the
signature was made before retirement. Its reason and one key-retired: warning (alongside the
usual completeness caveats) point to --purpose historical, the only path that can attribute such a
signature, through an independently trusted checkpoint. Scope: that meaning of KEY_RETIRED holds
on verifyChain/verifyChainText, the noa verify CLI, noa --serve and the witnessed
(--anchors/--trust-set) path. The standalone helpers verifyCheckpoint, the COSE verifiers and the compliance check refuse a
retired key too, and they also authenticate first: a forged or altered signature naming a retired
kid gets the helper's integrity refusal, and only an authentic one is answered as retired.
receiptFromCose names a retired envelope key only after both signatures authenticate, so that
retirement never stands in for a forged or malformed receipt.
resolveVerificationKey does the same when the caller passes the signed message and signature.
It is a key resolver by design: without those two arguments it authenticates nothing, its "retired"
answer names the key state only, and a current kid resolves with no signature check, so a caller
that verifies an artifact passes them and checks the signature itself, as every such caller here
does. A historical checkpoint is authenticated only through the separately
supplied checkpointKeyring; receipt trust is never a fallback. Its signing key must also differ
from every receipt signer by both key ID and decoded SPKI bytes. Separate keys do not establish
separate organizations, so organizationalIndependence remains UNVERIFIED.
An authenticated checkpoint over the presented head yields HEAD_ANCHORED; one over a real lower
receipt yields PREFIX_ANCHORED. The latter has top-level classification PARTIAL, which is the
stable replacement for the earlier informal DEGRADED label. A checkpoint at or after a covered
signer's retiredAt is UNATTRIBUTABLE; attribution requires the checkpoint to strictly predate
retirement. When the lifecycle explicitly supplies validFrom, attribution also requires
validFrom <= checkpoint.ts; equality is valid. The full interval is therefore
[validFrom, retiredAt). The legacy two-field {publicKey, retiredAt} entry remains valid and has
no lower bound: verifiers do not invent activation history. An explicit empty or inverted interval
(validFrom >= retiredAt) is rejected as RECEIPT_ROOT_INVALID.
The checkpoint key's own explicit validFrom is also an inclusive lower bound on
checkpoint.ts. A checkpoint before either activation bound is UNVERIFIED with
CHECKPOINT_BEFORE_ACTIVATION; integrity and the authenticated head/prefix mapping remain intact,
but attribution is withheld. Absent/null checkpoint-key activation retains the unbounded legacy
behavior. A lifecycle-retired checkpoint key remains WITNESS_KEY_RETIRED: its own signed
timestamp cannot prove that it existed before that key was retired.
Lifecycle and checkpoint instants use the receipt profile's RFC 3339 grammar: one to nine fractional
digits are accepted, and RFC 3339's T/Z separators are case-insensitive (t/z are valid).
Comparisons retain nanosecond precision. A checkpoint ahead of the supplied bytes or contradicting
the hash at its sequence is CONFLICT. Missing bytes are reported as
MISSING_RELATIVE_TO_CHECKPOINT; the API never infers deliberate suppression without separate
presenter-possession/omission evidence.
node dist/src/cli.js verify conformance/survivable-retirement/chain.json \
--purpose historical \
--keyring conformance/survivable-retirement/receipt-keyring-retired.json \
--checkpoint conformance/survivable-retirement/checkpoints/exact-before-retirement.json \
--checkpoint-keyring conformance/survivable-retirement/checkpoint-keyring.json
The canonical corpus is regenerated by npm test; npm run check:survivable-retirement requires
the same versioned result and process exit from TypeScript, Python, Go, Rust, and C#. Its
currentUse half pins the default-purpose refusal order under the same lifecycle roots; each port
declares whether it implements lifecycle keyrings on that path, and a port that does not is still
run on every case and must refuse without reporting KEY_RETIRED.
How it works
- Decide before, not observe after. A policy classifies an action — safe, risky, forbidden — and the governance layer allows it, holds it for a human, or blocks it.
- Commit to the decision. A receipt records which agent · what action · under which policy · what verdict · reversible how, signed with Ed25519. Signatures are mandatory and the signing key is bound into the hash.
- Chain it. Each receipt link-hashes the previous one, so altering any past record breaks every record after it.
- Never carry raw parameters. Only
action.paramsHashtravels, so a receipt is publishable without leaking the payload. - Verify anywhere. Verification is a pure function of bytes: offline, no account, no network.
Five language-specific verifier implementations are checked for cross-implementation verdict
parity through a two-hop comparison chain (Python against the TypeScript reference; Go, Rust,
and C# against Python). The exact coverage and caveats are in
conformance/MATRIX.md; organizational and implementation independence are not established.
{
"spec": "noa.receipt/0.1",
"action": { "canonical": "payment.refund", "riskClass": "HIGH" },
"governance": { "verdict": "EXECUTED", "approval": { "by": "approver_01J9X4Q2" } },
"chain": { "seq": 42, "prevHash": "sha256:…", "hash": "sha256:…" }
}
Full wire format: docs/receipt-spec.md. Production key handling:
docs/trust-root-checklist.md. A worked
deferred → rejected → executed story: examples/killer-demo/demo.mjs.
What we deliberately do NOT claim
This is the part of the project that took the most engineering to be able to state precisely, and it is normative: NON-CLAIMS.md. Past overclaims are not edited away — they are kept in CORRECTIONS.md.
- A signature proves someone said this, never this is true (NC-1.1) and never this is still true (NC-1.2).
- A receipt does not prove the described action actually occurred; the remote system of record is the only witness to a side effect, and it does not sign our receipts (NC-1.3).
- A valid chain does not prove completeness — it cannot prove no receipt was withheld (NC-1.5).
- An approval proves a key holder authorized these bytes; not that a human understood them (NC-3.1), and not that the approval screen was ever rendered (NC-3.4).
- The same-realm TypeScript verifier does not meet the security objective against an attacker who runs code in your process; the separate-process CLI is what holds today (NC-6.0, NC-6.2).
- A verdict handed back to a compromised caller is not an enforcement control (NC-6.6).
- No certification is claimed or in progress — no SOC 2, no ISO 27001, no FedRAMP (NC-5.3).
v1.0does not claim external anchoring; nothing here contacts a witness or a log (NC-4.3, NC-4.5).
Read THREAT-MODEL.md before you rely on any of this.
What is live, and what is roadmap
Live — measured in this repository, gated on every push:
- The
noa.receipt/0.1wire format, frozen, with a JSON Schema and a published spec. - Three packages on npm under Apache-2.0 (the badges above are the live versions).
- Five language-specific verifier implementations checked for cross-implementation verdict
parity through a two-hop comparison chain in CI — Python against the TypeScript reference, then
Go, Rust, and C# against Python, with no partial credit per vector class. This does not establish
organizational or implementation independence; see
conformance/MATRIX.md. - A conformance corpus of 21 attack vectors and 9 malformed vectors that the verifiers must
reject on every push — plus the companions that must keep passing, because a rule that refuses
everything is an outage rather than a control — with the per-class coverage, and the three classes
(
hash,impersonation,dup-key) the cross-implementation runner does not explicitly tag for TypeScript, written down inconformance/MATRIX.mdrather than glossed. - An offline CLI verifier that runs in its own process with no third-party dependencies.
- A runtime human-approval gate in the MCP proxy: a risky call is held as a signed
DEFERREDreceipt until a human approves it, producing aDEFERRED → ALLOWED → EXECUTEDchain (--approval-rules).
Roadmap — specified, decided or planned, and NOT shipped. The public decision roadmap is 04_ROADMAP.md; it is not release evidence for any particular revision.
- A semantic interoperability contract for consumers, and an externally reproducible conformance profile (roadmap §1–§2).
- Resolution of the documented protocol-quality gaps — canonical encoding, COSE companion behaviour, error taxonomy, key lifecycle, revocation and freshness (§3).
- Standards engagement. An individual Internet-Draft
draft-noa-scitt-ai-agent-receipt-01exists; that is not working-group adoption, and this project does not call itself standardized (§4). - A controlled pilot with a real relying-party decision. Package downloads are distribution evidence,
never adoption evidence (§5).
Repository-artifact status:
PROTOTYPE, with activeSPECIFICATIONwork. This is not evidence ofPILOT,STANDARDIZATION, orPRODUCTIONstatus.
Packages
Further modules live under packages/ — gate, relay, evidence, approval artifacts,
framework adapters, signer sidecar, TSA anchor. They are part of this repository's gates but are
not published to npm, and this README does not present them as if they were.
Language-specific verifier implementations
Five verifiers are held to one bar: for every conformance vector an implementation
must produce the identical verdict to its own comparison target — a two-hop chain, not five direct
comparisons to TypeScript: Python is compared directly to the TS reference, and Go, Rust and C# are
each compared directly to Python (not to TS), exactly as the table below and
conformance/MATRIX.md state. One mismatch fails the whole class for that
implementation — no partial credit, because a single silently-accepted attack is a complete security
failure regardless of how many adjacent checks still pass.
All five run in the five-verifier-conformance job of
.github/workflows/ci.yml on every push and pull request. This proves
cross-implementation verdict parity for the covered vectors and comparison topology. Organizational
independence and independent decision paths are not established, so this repository does not claim
independent implementations.
Working in this repository
npm ci
npm run build # tsc
npm test # build + regenerate vectors + the kernel suite
node dist/src/cli.js verify conformance/vectors/valid-chain.json \
--keyring conformance/vectors/keyring.json \
--checkpoint conformance/vectors/checkpoint.json
Test counts are deliberately not printed in this file: a number here would be stale the day after it was typed, and the doc-truth gate rejects one. CI is the live count.
Security
Report privately through GitHub security advisories; the policy, the supported-version table and the disclosure expectations are in SECURITY.md. The public threat classes, residual risks, and explicit limits are in THREAT-MODEL.md and NON-CLAIMS.md.
Contributing
Start with CONTRIBUTING.md, VERSIONING.md, and CODE_OF_CONDUCT.md. Issues and discussions are welcome — emitters and acceptors of receipts especially.
The one rule worth stating here: a claim in this repository is a measurement, not an aspiration. Adding a non-claim is an ordinary commit; removing one is a reviewed event that has to run the proof.