# 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](NON-CLAIMS.md).

[![CI](https://github.com/NordenSoft/noa-mandate-core/actions/workflows/ci.yml/badge.svg)](https://github.com/NordenSoft/noa-mandate-core/actions/workflows/ci.yml)
[![doc-truth](https://github.com/NordenSoft/noa-mandate-core/actions/workflows/doc-truth.yml/badge.svg)](https://github.com/NordenSoft/noa-mandate-core/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 ·
`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 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.

## 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.

```bash
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.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.
  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`](conformance/MATRIX.md); organizational and implementation independence
  are not established.

```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 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`](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 public decision roadmap is
[04_ROADMAP.md](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-01`](https://datatracker.ietf.org/doc/draft-noa-scitt-ai-agent-receipt/01/) 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).
Repository-artifact status: `PROTOTYPE`, with active `SPECIFICATION` work. This is not evidence of
`PILOT`, `STANDARDIZATION`, or `PRODUCTION` status.

## 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.

## 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`](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 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. 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](https://github.com/NordenSoft/noa-mandate-core/security/advisories/new); the policy,
the supported-version table and the disclosure expectations are in [SECURITY.md](SECURITY.md). The
public threat classes, residual risks, and explicit limits are in
[THREAT-MODEL.md](THREAT-MODEL.md) and [NON-CLAIMS.md](NON-CLAIMS.md).

## Contributing

Start with [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)
