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.

246 lines (245 loc) • 8.8 kB
import { generateKeyPair, signEd25519 } from "./keys.js"; import { sha256Digest } from "./hash.js"; import { verifyChain } from "./verify.js"; export const REHEARSAL_SIG_DOMAIN = "NOA-KernelRehearsal-v0.1-sig"; export const PROTO_ID = "noa-kernel-rehearsal/0"; export const MAX_FRAME = 64 * 1024 * 1024; export const VERSION = 0x01; export const TYPE = { HELLO: 0x01, REQUEST: 0x02, RESPONSE: 0x03, PROTOCOL_ERROR: 0x7f, }; export const ERR = { FRAME_OVERSIZE: 0x01, FRAME_UNDERSIZE: 0x02, BAD_VERSION: 0x03, BAD_TYPE: 0x04, BAD_REQUEST_GRAMMAR: 0x05, ID_NOT_MONOTONIC: 0x06, FRAME_TIMEOUT: 0x07, EOF_MID_FRAME: 0x08, INTERNAL: 0x09, }; export const TAG = { DOCUMENT: 0x01, KEYRING: 0x02, CHECKPOINT: 0x03, IDENTITY: 0x04 }; const REQUEST_FIXED_HEADER = 1 + 1 + 8 + 32 + 1; const OP_VERIFY_CHAIN = 0x01; export function parseRequest(payload) { if (payload.length < REQUEST_FIXED_HEADER) return { ok: false, code: ERR.FRAME_UNDERSIZE }; const id = payload.readBigUInt64BE(2); const nonce = payload.subarray(10, 42); const op = payload[42]; if (op !== OP_VERIFY_CHAIN) return { ok: false, code: ERR.BAD_REQUEST_GRAMMAR }; const sections = {}; let lastTag = 0; let off = REQUEST_FIXED_HEADER; while (off < payload.length) { if (off + 5 > payload.length) return { ok: false, code: ERR.BAD_REQUEST_GRAMMAR }; const tag = payload[off]; const len = payload.readUInt32BE(off + 1); off += 5; if (tag <= lastTag || tag > TAG.IDENTITY) return { ok: false, code: ERR.BAD_REQUEST_GRAMMAR }; if (off + len > payload.length) return { ok: false, code: ERR.BAD_REQUEST_GRAMMAR }; sections[tag] = payload.subarray(off, off + len); off += len; lastTag = tag; } const document = sections[TAG.DOCUMENT]; if (document === undefined) return { ok: false, code: ERR.BAD_REQUEST_GRAMMAR }; const out = { ok: true, id, nonce, document }; if (sections[TAG.KEYRING] !== undefined) out.keyring = sections[TAG.KEYRING]; if (sections[TAG.CHECKPOINT] !== undefined) out.checkpoint = sections[TAG.CHECKPOINT]; if (sections[TAG.IDENTITY] !== undefined) out.identityManifest = sections[TAG.IDENTITY]; return out; } export function envelopePreimage(requestDigest, requestId, nonce, verdictBytes) { const idBuf = Buffer.alloc(8); idBuf.writeBigUInt64BE(requestId); return Buffer.concat([ Buffer.from(REHEARSAL_SIG_DOMAIN + ":", "utf8"), requestDigest, idBuf, nonce, sha256Digest(verdictBytes), ]); } function u32(n) { const b = Buffer.alloc(4); b.writeUInt32BE(n); return b; } export function buildHelloPayload(publicKeySpkiDer) { const protoId = Buffer.from(PROTO_ID, "ascii"); const pkLen = Buffer.alloc(2); pkLen.writeUInt16BE(publicKeySpkiDer.length); return Buffer.concat([ Buffer.from([VERSION, TYPE.HELLO, protoId.length]), protoId, u32(MAX_FRAME), pkLen, publicKeySpkiDer, ]); } export function buildResponsePayload(requestPayload, requestId, nonce, verdictBytes, privateKeyB64) { const requestDigest = sha256Digest(requestPayload); const sig = Buffer.from(signEd25519(privateKeyB64, envelopePreimage(requestDigest, requestId, nonce, verdictBytes)), "base64"); const idBuf = Buffer.alloc(8); idBuf.writeBigUInt64BE(requestId); return Buffer.concat([ Buffer.from([VERSION, TYPE.RESPONSE]), idBuf, nonce, requestDigest, u32(verdictBytes.length), verdictBytes, sig, ]); } const LABEL_BANNER = "noa --serve: PROTOCOL REHEARSAL (ADR-0002 Stage 0.5) — NOT a security boundary, NOT the isolated " + "kernel. Same-realm TypeScript behind a process boundary is still poisonable inside the process. " + "Spec: docs/kernel-wire-protocol.md"; export function runServe(args) { let frameTimeoutMs = 30_000; for (let i = 0; i < args.length; i++) { const a = args[i]; if (a === "--frame-timeout-ms") { const v = args[++i]; const n = Number(v); if (v === undefined || !Number.isSafeInteger(n) || n <= 0) { process.stderr.write("error: --frame-timeout-ms must be a positive integer\n"); return Promise.resolve(4); } frameTimeoutMs = n; } else { process.stderr.write(`error: unknown --serve flag: ${a}\n`); return Promise.resolve(4); } } process.stderr.write(LABEL_BANNER + "\n"); const kp = generateKeyPair("kernel-rehearsal-ephemeral"); const writeFrame = (payload) => { process.stdout.write(Buffer.concat([u32(payload.length), payload])); }; writeFrame(buildHelloPayload(Buffer.from(kp.publicKey, "base64"))); return new Promise((resolve) => { let acc = Buffer.alloc(0); let need = null; let lastId = 0n; let timer = null; let done = false; const clearTimer = () => { if (timer !== null) { clearTimeout(timer); timer = null; } }; const finish = (code) => { if (done) return; done = true; clearTimer(); process.stdin.pause(); resolve(code); }; const protoError = (code) => { try { writeFrame(Buffer.from([VERSION, TYPE.PROTOCOL_ERROR, code])); } catch { } finish(1); }; const handlePayload = (payload) => { if (payload.length < 2) return ERR.FRAME_UNDERSIZE; if (payload[0] !== VERSION) return ERR.BAD_VERSION; if (payload[1] !== TYPE.REQUEST) return ERR.BAD_TYPE; const parsed = parseRequest(payload); if (!parsed.ok) return parsed.code; if (parsed.id < 1n || parsed.id <= lastId) return ERR.ID_NOT_MONOTONIC; lastId = parsed.id; let verdictBytes; try { const opts = {}; if (parsed.keyring !== undefined) opts.keyring = parsed.keyring; if (parsed.checkpoint !== undefined) opts.checkpoint = parsed.checkpoint; if (parsed.identityManifest !== undefined) opts.identityManifest = parsed.identityManifest; const verdict = verifyChain(parsed.document, opts); verdictBytes = Buffer.from(JSON.stringify(verdict), "utf8"); } catch { return ERR.INTERNAL; } try { writeFrame(buildResponsePayload(payload, parsed.id, parsed.nonce, verdictBytes, kp.privateKey)); } catch { return ERR.INTERNAL; } return null; }; const pump = () => { for (;;) { if (need === null) { if (acc.length < 4) break; const declared = acc.readUInt32BE(0); acc = acc.subarray(4); if (declared > MAX_FRAME) return protoError(ERR.FRAME_OVERSIZE); if (declared === 0) return protoError(ERR.FRAME_UNDERSIZE); need = declared; } if (acc.length < need) break; const payload = acc.subarray(0, need); acc = acc.subarray(need); need = null; const err = handlePayload(payload); if (err !== null) return protoError(err); } clearTimer(); if (!done && (need !== null || acc.length > 0)) { timer = setTimeout(() => protoError(ERR.FRAME_TIMEOUT), frameTimeoutMs); } }; process.stdin.on("data", (chunk) => { if (done) return; acc = acc.length === 0 ? Buffer.from(chunk) : Buffer.concat([acc, chunk]); pump(); }); process.stdin.on("end", () => { if (done) return; if (need !== null || acc.length > 0) protoError(ERR.EOF_MID_FRAME); else finish(0); }); process.stdin.on("error", () => { if (!done) protoError(ERR.EOF_MID_FRAME); }); }); }