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.

133 lines (132 loc) • 7.07 kB
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)" }; } }