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