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.

183 lines (182 loc) • 10.1 kB
import { RECEIPT_SPEC } from "./types.js"; import { parseDocument } from "./bytes.js"; import { arrayPush, arrayIncludes, arrayConcat, hasOwn, objectKeys, isArray, arrayLength, strIsWellFormed, strCodePointCount, isSafeInteger } from "./intrinsics.js"; import { membership } from "./inert.js"; import { isSha256Hash, isParamsHash, isRfc3339Instant } from "./scan.js"; const isRiskClass = membership(["LOW", "MEDIUM", "HIGH", "CRITICAL", "IRREVERSIBLE"]); const isPrincipal = membership(["HUMAN", "SERVICE", "POLICY", "SANDBOX_SIM"]); const isMode = membership(["off", "shadow", "approvals_on", "on"]); const isVerdict = membership([ "ALLOWED", "BLOCKED", "DEFERRED", "EXECUTED", "FAILED", "ROLLED_BACK", "SIMULATED", ]); function isPlainObject(v) { return typeof v === "object" && v !== null && !isArray(v); } function has(obj, k) { return hasOwn(obj, k); } function checkExactKeys(obj, required, optional, path, errors) { const allowed = arrayConcat(required, optional); const keys = objectKeys(obj); const kn = arrayLength(keys); for (let ki = 0; ki < kn; ki++) { const k = keys[ki]; if (!arrayIncludes(allowed, k)) arrayPush(errors, `${path}: unknown field "${k}"`); } const rn = arrayLength(required); for (let ri = 0; ri < rn; ri++) { const k = required[ri]; if (!has(obj, k)) arrayPush(errors, `${path}: missing required field "${k}"`); } } function str(v) { return typeof v === "string" && strIsWellFormed(v); } export function validateReceiptShape(receipt) { const parsed = parseDocument(receipt, "receipt"); if (!parsed.ok) return { ok: false, errors: [parsed.reason] }; return validateReceiptShapeParsed(parsed.value); } export function validateReceiptShapeParsed(value) { const errors = []; if (!isPlainObject(value)) { return { ok: false, errors: ["receipt: not an object"] }; } const r = value; try { checkExactKeys(r, ["spec", "id", "ts", "scope", "agent", "action", "governance", "chain", "sig"], [], "receipt", errors); if (r.spec !== RECEIPT_SPEC) arrayPush(errors, `receipt.spec: must be "${RECEIPT_SPEC}"`); if (!str(r.id) || r.id.length === 0 || strCodePointCount(r.id) > 128) arrayPush(errors, "receipt.id: non-empty string ≤128 chars"); if (!str(r.ts) || !isRfc3339Instant(r.ts)) arrayPush(errors, "receipt.ts: must be RFC 3339 UTC timestamp"); if (isPlainObject(r.scope)) { checkExactKeys(r.scope, ["chain"], ["tenant"], "receipt.scope", errors); if (!str(r.scope.chain) || r.scope.chain.length === 0) arrayPush(errors, "receipt.scope.chain: non-empty string"); if (has(r.scope, "tenant") && !str(r.scope.tenant)) arrayPush(errors, "receipt.scope.tenant: string"); } else arrayPush(errors, "receipt.scope: object required"); if (isPlainObject(r.agent)) { checkExactKeys(r.agent, ["id", "principal"], ["model"], "receipt.agent", errors); if (!str(r.agent.id) || r.agent.id.length === 0) arrayPush(errors, "receipt.agent.id: non-empty string"); if (!isPrincipal(r.agent.principal)) arrayPush(errors, "receipt.agent.principal: invalid enum"); if (has(r.agent, "model") && r.agent.model !== null && !str(r.agent.model)) arrayPush(errors, "receipt.agent.model: string or null"); } else arrayPush(errors, "receipt.agent: object required"); if (isPlainObject(r.action)) { checkExactKeys(r.action, ["id", "canonical", "riskClass", "paramsHash", "reversible"], ["rollbackRef"], "receipt.action", errors); if (!str(r.action.id) || r.action.id.length === 0) arrayPush(errors, "receipt.action.id: non-empty string"); if (!str(r.action.canonical) || r.action.canonical.length === 0) arrayPush(errors, "receipt.action.canonical: non-empty string"); if (!isRiskClass(r.action.riskClass)) arrayPush(errors, "receipt.action.riskClass: invalid enum"); if (!str(r.action.paramsHash) || !isParamsHash(r.action.paramsHash)) arrayPush(errors, "receipt.action.paramsHash: must match (sha256|hmac-sha256):<64 hex>"); if (typeof r.action.reversible !== "boolean") arrayPush(errors, "receipt.action.reversible: boolean"); if (has(r.action, "rollbackRef") && r.action.rollbackRef !== null && !str(r.action.rollbackRef)) arrayPush(errors, "receipt.action.rollbackRef: string or null"); } else arrayPush(errors, "receipt.action: object required"); if (isPlainObject(r.governance)) { checkExactKeys(r.governance, ["mode", "verdict", "sandboxed"], ["ruleId", "approval", "compliance"], "receipt.governance", errors); if (!isMode(r.governance.mode)) arrayPush(errors, "receipt.governance.mode: invalid enum"); if (!isVerdict(r.governance.verdict)) arrayPush(errors, "receipt.governance.verdict: invalid enum"); if (typeof r.governance.sandboxed !== "boolean") arrayPush(errors, "receipt.governance.sandboxed: boolean"); if (has(r.governance, "ruleId") && r.governance.ruleId !== null && !str(r.governance.ruleId)) arrayPush(errors, "receipt.governance.ruleId: string or null"); if (has(r.governance, "approval") && r.governance.approval !== null) { if (isPlainObject(r.governance.approval)) { checkExactKeys(r.governance.approval, ["by", "at"], [], "receipt.governance.approval", errors); if (!str(r.governance.approval.by)) arrayPush(errors, "receipt.governance.approval.by: string"); if (!str(r.governance.approval.at) || !isRfc3339Instant(r.governance.approval.at)) arrayPush(errors, "receipt.governance.approval.at: RFC 3339 UTC"); } else arrayPush(errors, "receipt.governance.approval: object or null"); } if (has(r.governance, "compliance") && r.governance.compliance !== null) { const c = r.governance.compliance; if (isPlainObject(c)) { checkExactKeys(c, ["policyHash", "readSetHash", "inputsHash"], ["verdict"], "receipt.governance.compliance", errors); const cflds = ["policyHash", "readSetHash", "inputsHash"]; for (let cfi = 0; cfi < 3; cfi++) { const k = cflds[cfi]; if (!str(c[k]) || !isSha256Hash(c[k])) arrayPush(errors, `receipt.governance.compliance.${k}: sha256:<64 hex>`); } if (has(c, "verdict") && c.verdict !== "ALLOW" && c.verdict !== "DENY") arrayPush(errors, 'receipt.governance.compliance.verdict: must be "ALLOW" or "DENY"'); } else arrayPush(errors, "receipt.governance.compliance: object or null"); } } else arrayPush(errors, "receipt.governance: object required"); if (isPlainObject(r.agent) && isPlainObject(r.action) && isPlainObject(r.governance)) { const principal = r.agent.principal; const verdict = r.governance.verdict; const sandboxed = r.governance.sandboxed; const reversible = r.action.reversible; const rollbackRefPresent = has(r.action, "rollbackRef") && r.action.rollbackRef !== null; if (principal === "SANDBOX_SIM" && sandboxed === false) arrayPush(errors, 'receipt.governance.sandboxed: must be true when agent.principal is "SANDBOX_SIM"'); if (verdict === "SIMULATED" && sandboxed === false) arrayPush(errors, 'receipt.governance.sandboxed: must be true when governance.verdict is "SIMULATED"'); if (reversible === false && rollbackRefPresent) arrayPush(errors, "receipt.action.rollbackRef: must be absent or null when action.reversible is false"); if (verdict === "ROLLED_BACK" && reversible === false) arrayPush(errors, 'receipt.action.reversible: must be true when governance.verdict is "ROLLED_BACK"'); } if (isPlainObject(r.chain)) { checkExactKeys(r.chain, ["seq", "prevHash", "hash"], [], "receipt.chain", errors); if (typeof r.chain.seq !== "number" || !isSafeInteger(r.chain.seq) || r.chain.seq < 0) arrayPush(errors, "receipt.chain.seq: non-negative safe integer"); if (r.chain.prevHash !== null && (!str(r.chain.prevHash) || !isSha256Hash(r.chain.prevHash))) arrayPush(errors, "receipt.chain.prevHash: sha256:<64 hex> or null"); if (!str(r.chain.hash) || !isSha256Hash(r.chain.hash)) arrayPush(errors, "receipt.chain.hash: sha256:<64 hex>"); } else arrayPush(errors, "receipt.chain: object required"); if (isPlainObject(r.sig)) { checkExactKeys(r.sig, ["alg", "kid", "value"], [], "receipt.sig", errors); if (r.sig.alg !== "ed25519") arrayPush(errors, 'receipt.sig.alg: must be "ed25519"'); if (!str(r.sig.kid) || r.sig.kid.length === 0) arrayPush(errors, "receipt.sig.kid: non-empty string"); if (!str(r.sig.value) || r.sig.value.length === 0) arrayPush(errors, "receipt.sig.value: non-empty string"); } else arrayPush(errors, "receipt.sig: object required (signatures are mandatory in v0.1)"); } catch (e) { return { ok: false, errors: [`receipt: structural-validation error: ${e.message}`] }; } return { ok: errors.length === 0, errors }; }