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.
133 lines (132 loc) • 7.07 kB
JavaScript
import { policyHash, readSetHash } from "./dsl.js";
import { evaluateParsed } from "./eval.js";
import { parseDocument } from "../bytes.js";
import { inertOptions } from "../opts.js";
import { canonicalize } from "../jcs.js";
import { sha256Prefixed, sha256Hex } from "../hash.js";
import { validateReceiptShapeParsed } from "../schema.js";
import { receiptHashInput } from "../canonicalize.js";
import { verifyEd25519 } from "../keys.js";
import { signingMessage, RECEIPT_SIG_DOMAIN } from "../signing.js";
import { arrayIncludes, arrayJoin, mapGet, mapSet, newMap, arraySlice, arrayEvery, objectGetOwnPropertyNames, isArray, jsonStringify } from "../intrinsics.js";
import { parseVerificationKeyring } from "../verification-keyring.js";
export function complianceCommit(policy, inputs) {
return {
policyHash: policyHash(policy),
readSetHash: readSetHash(policy),
inputsHash: sha256Prefixed(canonicalize(inputs)),
verdict: evaluateParsed(policy, inputs).verdict,
};
}
const COMPLIANCE_OPTION_SCHEMA = Object.freeze(Object.assign(Object.create(null), {
keyring: { kind: "document" },
identityManifest: { kind: "document" },
}));
export function verifyReceiptCompliance(receipt, policy, inputs, opts = {}) {
const admitted = inertOptions(COMPLIANCE_OPTION_SCHEMA, opts, "options");
if (!admitted.ok)
return { ok: false, reason: admitted.reason };
const o = admitted.value;
const rParsed = parseDocument(receipt, "receipt");
if (!rParsed.ok)
return { ok: false, reason: rParsed.reason };
const pParsed = parseDocument(policy, "policy");
if (!pParsed.ok)
return { ok: false, reason: pParsed.reason };
const iParsed = parseDocument(inputs, "inputs");
if (!iParsed.ok)
return { ok: false, reason: iParsed.reason };
try {
if (typeof rParsed.value !== "object" || rParsed.value === null) {
return { ok: false, reason: "receipt is not an object" };
}
const snap = rParsed.value;
const c = snap.governance?.compliance;
if (!c)
return { ok: false, reason: "receipt carries no governance.compliance commitment" };
const policySnap = pParsed.value;
const inputsSnap = iParsed.value;
const haveKeyring = o.keyring !== undefined;
let keyringParsed;
if (haveKeyring) {
const kParsed = parseVerificationKeyring(o.keyring, "keyring");
if (!kParsed.ok)
return { ok: false, reason: kParsed.reason };
keyringParsed = kParsed.value;
}
if (!haveKeyring) {
return {
ok: false,
reason: "no keyring supplied — the compliance carrier cannot be authenticated, and the L2 hash proof " +
"over an unauthenticated receipt proves nothing (its governance.compliance block is attacker-mutable). " +
"Pass { keyring } (and { identityManifest } to bind WHICH agent signed).",
};
}
let attribution = "KID_LEVEL";
{
const verification = keyringParsed;
const keyring = verification.keyring;
const shape = validateReceiptShapeParsed(snap);
if (!shape.ok)
return { ok: false, reason: `carrier receipt malformed: ${arrayJoin(shape.errors, "; ")}` };
const hashInput = receiptHashInput(snap);
if ("sha256:" + sha256Hex(hashInput) !== snap.chain.hash) {
return { ok: false, reason: "carrier receipt hash mismatch — not authentic" };
}
if (verification.retiredKids[snap.sig.kid] === true) {
return {
ok: false,
reason: `carrier receipt signing key ${jsonStringify(snap.sig.kid)} is retired; signer-chosen receipt time is not an independent witness`,
};
}
const pub = keyring[snap.sig.kid];
if (!pub)
return { ok: false, reason: `carrier receipt signing key "${snap.sig.kid}" not in keyring` };
if (!verifyEd25519(pub, signingMessage(RECEIPT_SIG_DOMAIN, hashInput), snap.sig.value)) {
return { ok: false, reason: "carrier receipt signature not authenticated" };
}
if (o.identityManifest !== undefined) {
const mParsed = parseDocument(o.identityManifest, "identityManifest");
if (!mParsed.ok)
return { ok: false, reason: mParsed.reason };
const live = mParsed.value;
if (typeof live !== "object" || live === null || isArray(live)) {
return { ok: false, reason: "identityManifest must be an object (agent.id -> kid[])" };
}
const manifest = newMap();
const aids = objectGetOwnPropertyNames(live);
for (let ai = 0; ai < aids.length; ai++) {
const aid = aids[ai];
const kidsLive = live[aid];
if (!isArray(kidsLive)) {
return { ok: false, reason: `identityManifest["${aid}"] must be an array of kid strings` };
}
const kids = arraySlice(kidsLive);
if (!arrayEvery(kids, (k) => typeof k === "string")) {
return { ok: false, reason: `identityManifest["${aid}"] must be an array of kid strings` };
}
mapSet(manifest, aid, kids);
}
const allowed = mapGet(manifest, snap.agent.id);
if (allowed === undefined || !arrayIncludes(allowed, snap.sig.kid)) {
return { ok: false, reason: `agent "${snap.agent.id}" not authorized for signing key "${snap.sig.kid}" (identity manifest)` };
}
attribution = "AGENT_BOUND";
}
}
if (policyHash(policySnap) !== c.policyHash)
return { ok: false, reason: "policyHash mismatch — supplied policy is not the committed one" };
if (readSetHash(policySnap) !== c.readSetHash)
return { ok: false, reason: "readSetHash mismatch" };
if (sha256Prefixed(canonicalize(inputsSnap)) !== c.inputsHash)
return { ok: false, reason: "inputsHash mismatch — supplied inputs are not the recorded ones" };
const ev = evaluateParsed(policySnap, inputsSnap);
if (c.verdict !== undefined && ev.verdict !== c.verdict) {
return { ok: false, reason: `verdict mismatch — recorded decision does not reproduce (recorded ${c.verdict}, re-run ${ev.verdict})`, policyVerdict: ev.verdict, ruleFired: ev.ruleFired };
}
return { ok: true, policyVerdict: ev.verdict, ruleFired: ev.ruleFired, attribution };
}
catch {
return { ok: false, reason: "compliance check failed closed: an input could not be reduced to inert data (a hostile getter, a proxy trap, or a non-plain object)" };
}
}