UNPKG

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
# 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). [![CI](https://github.com/NordenSoft/noa/actions/workflows/ci.yml/badge.svg)](https://github.com/NordenSoft/noa/actions/workflows/ci.yml) [![doc-truth](https://github.com/NordenSoft/noa/actions/workflows/doc-truth.yml/badge.svg)](https://github.com/NordenSoft/noa/actions/workflows/doc-truth.yml) [![npm noa-receipt](https://img.shields.io/npm/v/noa-receipt?label=noa-receipt)](https://www.npmjs.com/package/noa-receipt) [![npm noa-mcp-adapter-core](https://img.shields.io/npm/v/noa-mcp-adapter-core?label=noa-mcp-adapter-core)](https://www.npmjs.com/package/noa-mcp-adapter-core) [![npm noa-mcp-proxy](https://img.shields.io/npm/v/noa-mcp-proxy?label=noa-mcp-proxy)](https://www.npmjs.com/package/noa-mcp-proxy) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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)