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
JavaScript
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));