noa-receipt
Version:
NOA Agent Action Receipt — open, offline-verifiable provenance for AI-agent actions. The governance/receipt organ only; the NOA brain is separate and proprietary.
293 lines (241 loc) • 17.6 kB
Markdown
# NOA — Agent Action Receipt
> The open kernel of the **NOA Mandate** family — the receipt protocol, reference
> implementations and conformance suite that NOA Mandate products build on.
**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 whoever your organization put behind the approver key — a phone, if that
is where you route it — 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](NON-CLAIMS.md).
[](https://github.com/NordenSoft/noa/actions/workflows/ci.yml)
[](https://github.com/NordenSoft/noa/actions/workflows/doc-truth.yml)
[](https://www.npmjs.com/package/noa-receipt)
[](https://www.npmjs.com/package/noa-mcp-adapter-core)
[](https://www.npmjs.com/package/noa-mcp-proxy)
[](LICENSE)
> 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.mjs`](scripts/lint-doc-truth.mjs) against 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's `bash` blocks are
> *executed* end to end, `npx noa` (a stranger's package) is refused, and the license badge matches
> `package.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 →](#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.
```bash
mkdir -p noa-quickstart && cd noa-quickstart
npm init -y > /dev/null
npm install noa-receipt # no runtime dependencies; Node >= 20 stdlib only
```
```bash
# 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
```
```bash
# Verify it offline, in a separate process. Prints status VALID and exits 0.
./node_modules/.bin/noa verify chain.json --keyring keyring.json
```
```bash
# 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.
> ⚠ **The CLI binary is called `noa`, but the npm package named `noa` is not ours** — it belongs to
> an unrelated third party. Run the local binary as above, or `npx --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.
## 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.paramsHash` travels, so a receipt is publishable
without leaking the payload.
- **Verify anywhere.** Verification is a pure function of bytes: offline, no account, no network, and
five independent implementations are held to identical verdicts through a two-hop comparison chain
(Python against the TS reference; Go, Rust and C# against Python) — with the exact per-class
coverage, caveats included, in [`conformance/MATRIX.md`](conformance/MATRIX.md).
```json
{
"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`](docs/receipt-spec.md). Production key handling:
[`docs/trust-root-checklist.md`](docs/trust-root-checklist.md). A worked
deferred → rejected → executed story: [`examples/killer-demo/demo.mjs`](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](NON-CLAIMS.md)**. Past overclaims are not edited away — they are kept
in [CORRECTIONS.md](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.0` does **not** claim external anchoring; nothing here contacts a witness or a log (NC-4.3,
NC-4.5).
Read [THREAT-MODEL.md](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.1` wire 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** independent verifier implementations, held to identical verdicts through a two-hop
comparison chain in CI — Python against the TS reference, then Go, Rust and C# against Python, no
partial credit per vector class — see the table below and
[`conformance/MATRIX.md`](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 in `conformance/MATRIX.md` rather 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 `DEFERRED`
receipt until a human approves it, producing a `DEFERRED → ALLOWED → EXECUTED` chain
(`--approval-rules`).
**Roadmap — specified, decided or planned, and NOT shipped.** The decision roadmap is
[04_ROADMAP.md](04_ROADMAP.md); the volatile execution state is
[00_CURRENT_STATE.md](00_CURRENT_STATE.md).
- 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 `-00` exists; 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).
- The isolated native trust boundary is **specified and not built**
([ADR-0002](docs/ADR-0002-isolated-native-trust-boundary.md)) — do not plan around it as if it
shipped. Likewise the hosted control-plane and the one-tap approval app.
Project status, in the vocabulary [AGENTS.md](AGENTS.md) defines: `PROTOTYPE`, with active
`SPECIFICATION` work. Not `PILOT`, not `STANDARDIZATION`, not `PRODUCTION`.
## Packages
| Package | npm | What it does |
|---|---|---|
| `noa-receipt` | [npm](https://www.npmjs.com/package/noa-receipt) | The kernel: build, sign, hash-chain and verify receipts, plus the `noa verify` CLI. No runtime dependencies. |
| `noa-mcp-adapter-core` | [npm](https://www.npmjs.com/package/noa-mcp-adapter-core) | The shared pre-flight decision engine — one `preCheck()` every MCP integration calls instead of re-deriving the policy. |
| `noa-mcp-proxy` | [npm](https://www.npmjs.com/package/noa-mcp-proxy) | A transparent MCP proxy in front of an existing, unmodified tool server: reflects its tool surface and gates every `tools/call`, fail-closed. |
Further modules live under [`packages/`](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.
## Independent implementations
Five verifiers, written separately, 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`](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.
| Implementation | Path | Conformance parity |
|---|---|---|
| TypeScript (reference) | [`src/`](src/) | Emits the signed vectors; agreement with the independent Python verifier is asserted on every push ([`impl-py/conformance.mjs`](impl-py/conformance.mjs)). |
| Python | [`impl-py/`](impl-py/) | Own JCS, own from-scratch RFC 8032 Ed25519, zero shared crypto with TS. Ground truth for the Go, Rust and C# verifiers below. |
| Go | [`impl-go/`](impl-go/) | Exit code must match the Python reference on every vector ([`impl-go/conformance_test.sh`](impl-go/conformance_test.sh)). |
| Rust | [`impl-rust/`](impl-rust/) | Same bar, same corpus ([`impl-rust/conformance.sh`](impl-rust/conformance.sh)). |
| C# | [`impl-csharp/`](impl-csharp/) | Same bar, same corpus ([`impl-csharp/conformance.sh`](impl-csharp/conformance.sh)). |
All five run in the `five-verifier-conformance` job of
[`.github/workflows/ci.yml`](.github/workflows/ci.yml) on every push and pull request. What this
proves is implementation independence on identical bytes; it does not prove *organizational*
independence, and this project does not claim it.
## 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](https://github.com/NordenSoft/noa/security/advisories/new); the policy,
the supported-version table and the disclosure expectations are in [SECURITY.md](SECURITY.md). The
adversarial history — what was found, what was fixed, and what was withdrawn as unachievable — is in
[THREAT-MODEL.md](THREAT-MODEL.md) and [NON-CLAIMS.md](NON-CLAIMS.md).
## Contributing
Start with [AGENTS.md](AGENTS.md) — it defines the evidence vocabulary this project argues in
(`NORMATIVE`, `OBSERVED`, `VERIFIED`, `ASSUMED`, `SOURCE_ABSENT`, `NON-CLAIM`) and the source-authority
order. Then [CONTRIBUTING.md](CONTRIBUTING.md), [VERSIONING.md](VERSIONING.md) and
[CODE_OF_CONDUCT.md](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.
## License
[Apache-2.0](LICENSE) · [NOTICE](NOTICE) · [noatrust.com](https://noatrust.com)