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.

209 lines (208 loc) • 7.46 kB
#!/usr/bin/env node import { readSync, openSync, fstatSync, closeSync, constants as fsConstants } from "node:fs"; import { verifyChain } from "./verify.js"; import { verifyChainWitnessed } from "./federation/verify-witnessed.js"; const MAX_FILE_BYTES = 64 * 1024 * 1024; const EXIT = { VALID: 0, UNVERIFIED: 1, TAMPERED: 2, MALFORMED: 3, USAGE: 4, UNTRUSTED: 5, WITNESS_INCOMPLETE: 6, }; function usage(msg) { if (msg) process.stderr.write(`error: ${msg}\n`); process.stderr.write("usage: noa verify <receipts.json> [--keyring <keyring.json>] [--checkpoint <checkpoint.json>] " + "[--identity <manifest.json>] [--anchors <anchors.json> --trust-set <trust.json> [--max-anchor-age-ms <n>]]\n" + " noa --serve [--frame-timeout-ms <n>] (PROTOCOL REHEARSAL, ADR-0002 Stage 0.5 — NOT a security\n" + " boundary, NOT the isolated kernel; docs/kernel-wire-protocol.md)\n"); process.exit(EXIT.USAGE); } function readDocumentText(path) { const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0); let fd; try { fd = openSync(path, flags); } catch { usage(`cannot open file: ${path}`); } let text; try { let st; try { st = fstatSync(fd); } catch { usage(`cannot inspect file: ${path}`); } if (!st.isFile()) usage(`not a regular file: ${path}`); if (st.size > MAX_FILE_BYTES) usage(`file too large (>${MAX_FILE_BYTES} bytes): ${path}`); try { const chunks = []; const chunk = Buffer.allocUnsafe(64 * 1024); let total = 0; for (;;) { const remaining = MAX_FILE_BYTES + 1 - total; const n = readSync(fd, chunk, 0, Math.min(chunk.length, remaining), null); if (n === 0) break; total += n; if (total > MAX_FILE_BYTES) usage(`file too large (>${MAX_FILE_BYTES} bytes): ${path}`); chunks.push(Buffer.from(chunk.subarray(0, n))); } text = Buffer.concat(chunks, total).toString("utf8"); } catch { usage(`cannot read file: ${path}`); } } finally { closeSync(fd); } return text; } function statusToExit(status) { switch (status) { case "VALID": return EXIT.VALID; case "UNVERIFIED": return EXIT.UNVERIFIED; case "UNTRUSTED": return EXIT.UNTRUSTED; case "TAMPERED": return EXIT.TAMPERED; default: return EXIT.MALFORMED; } } function main(argv) { const args = argv.slice(2); if (args.length === 0) usage(); const cmd = args[0]; if (cmd !== "verify") usage(`unknown command: ${cmd}`); let receiptsPath; let keyringPath; let checkpointPath; let identityPath; let anchorsPath; let trustSetPath; let maxAnchorAgeMs; for (let i = 1; i < args.length; i++) { const a = args[i]; if (a === "--keyring") { const v = args[++i]; if (v === undefined || v.startsWith("--")) usage("--keyring requires a path"); keyringPath = v; } else if (a === "--checkpoint") { const v = args[++i]; if (v === undefined || v.startsWith("--")) usage("--checkpoint requires a path"); checkpointPath = v; } else if (a === "--identity") { const v = args[++i]; if (v === undefined || v.startsWith("--")) usage("--identity requires a path"); identityPath = v; } else if (a === "--anchors") { const v = args[++i]; if (v === undefined || v.startsWith("--")) usage("--anchors requires a path"); anchorsPath = v; } else if (a === "--trust-set") { const v = args[++i]; if (v === undefined || v.startsWith("--")) usage("--trust-set requires a path"); trustSetPath = v; } else if (a === "--max-anchor-age-ms") { const v = args[++i]; if (v === undefined || v.startsWith("--")) usage("--max-anchor-age-ms requires a value"); const n = Number(v); if (!Number.isSafeInteger(n) || n < 0) usage("--max-anchor-age-ms must be a non-negative integer"); maxAnchorAgeMs = n; } else if (a.startsWith("--")) usage(`unknown flag: ${a}`); else if (!receiptsPath) receiptsPath = a; else usage(`unexpected argument: ${a}`); } if (!receiptsPath) usage("missing <receipts.json>"); const witnessMode = anchorsPath !== undefined || trustSetPath !== undefined; if (witnessMode && (anchorsPath === undefined || trustSetPath === undefined)) { usage("--anchors and --trust-set must be supplied together"); } if (maxAnchorAgeMs !== undefined && !witnessMode) { usage("--max-anchor-age-ms requires --anchors and --trust-set"); } let receipts; const opts = {}; let anchors; let trustSet; try { receipts = readDocumentText(receiptsPath); if (keyringPath) opts.keyring = readDocumentText(keyringPath); if (checkpointPath) opts.checkpoint = readDocumentText(checkpointPath); if (identityPath) opts.identityManifest = readDocumentText(identityPath); if (anchorsPath) anchors = readDocumentText(anchorsPath); if (trustSetPath) trustSet = readDocumentText(trustSetPath); } catch (e) { process.stderr.write(`error: ${e.message}\n`); return EXIT.MALFORMED; } if (witnessMode) { const wopts = { anchors: anchors, trustSet: trustSet, }; if (opts.checkpoint !== undefined) wopts.checkpoint = opts.checkpoint; if (opts.identityManifest !== undefined) wopts.identityManifest = opts.identityManifest; if (maxAnchorAgeMs !== undefined) wopts.freshness = { now: Date.now(), maxAgeMs: maxAnchorAgeMs }; const result = verifyChainWitnessed(receipts, opts.keyring, wopts); process.stdout.write(JSON.stringify(result, null, 2) + "\n"); const chainExit = statusToExit(result.chain.status); if (chainExit !== EXIT.VALID) { process.stderr.write(`chain did not verify (${result.chain.status}): witness acceptance is moot\n`); return chainExit; } if (result.witness.classification === "QUORUM_CONFIRMED") return EXIT.VALID; process.stderr.write(`witness acceptance failed (${result.witness.classification}): ${result.witness.reason}\n`); return EXIT.WITNESS_INCOMPLETE; } const result = verifyChain(receipts, opts); process.stdout.write(JSON.stringify(result, null, 2) + "\n"); return statusToExit(result.status); } if (process.argv[2] === "--serve") { const { runServe } = await import("./serve.js"); process.exit(await runServe(process.argv.slice(3))); } process.exit(main(process.argv));