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.

118 lines (117 loc) • 4.34 kB
import { DEFAULT_VERDICT } from "./dsl.js"; import { validatePolicyParsed } from "./validate.js"; import { parseDocument } from "../bytes.js"; import { hasOwn, objectKeys, isSafeInteger, arraySome, arrayEvery, arrayLength, isArray } from "../intrinsics.js"; export const REF_EVAL_VERSION = "noa-refeval/0.2"; export class PolicyError extends Error { constructor(message) { super(message); this.name = "PolicyError"; } } function assertScalar(v, where) { const t = typeof v; if (t === "string" || t === "boolean") return; if (t === "number") { if (!isSafeInteger(v)) throw new PolicyError(`non-integer/unsafe number at ${where}`); return; } throw new PolicyError(`non-scalar value at ${where}`); } function ownGet(inputs, path) { return hasOwn(inputs, path) ? inputs[path] : undefined; } function cmp(a, b) { if (typeof a !== typeof b) throw new PolicyError("type mismatch in comparison"); if (typeof a === "number") return a < b ? -1 : a > b ? 1 : 0; if (typeof a === "boolean") return (a ? 1 : 0) - (b ? 1 : 0); const s = a, t = b; return s < t ? -1 : s > t ? 1 : 0; } function match(c, inputs) { switch (c.op) { case "and": return arrayEvery(c.clauses, (x) => match(x, inputs)); case "or": return arraySome(c.clauses, (x) => match(x, inputs)); case "not": return !match(c.clause, inputs); case "exists": return ownGet(inputs, c.path) !== undefined; case "absent": return ownGet(inputs, c.path) === undefined; case "in": { const v = ownGet(inputs, c.path); if (v === undefined) return false; return arraySome(c.values, (x) => { assertScalar(x, `rule.in.values`); return cmp(v, x) === 0; }); } default: { const v = ownGet(inputs, c.path); if (v === undefined) return false; assertScalar(c.value, `rule.${c.op}.value`); const k = cmp(v, c.value); switch (c.op) { case "eq": return k === 0; case "ne": return k !== 0; case "lt": return k < 0; case "le": return k <= 0; case "gt": return k > 0; case "ge": return k >= 0; } } } } export function evaluate(policy, inputs) { const pParsed = parseDocument(policy, "policy"); if (!pParsed.ok) return { verdict: "DENY", ruleFired: "policy-invalid", engine: REF_EVAL_VERSION }; const iParsed = parseDocument(inputs, "inputs"); if (!iParsed.ok) return { verdict: "DENY", ruleFired: "eval-error", engine: REF_EVAL_VERSION }; return evaluateParsed(pParsed.value, iParsed.value); } export function evaluateParsed(policy, inputs) { const pv = validatePolicyParsed(policy); if (!pv.ok) { return { verdict: "DENY", ruleFired: "policy-invalid", engine: REF_EVAL_VERSION }; } if (typeof inputs !== "object" || inputs === null || isArray(inputs)) { return { verdict: "DENY", ruleFired: "input-invalid", engine: REF_EVAL_VERSION }; } try { const rpn = arrayLength(policy.requiredPaths); for (let rpi = 0; rpi < rpn; rpi++) { const p = policy.requiredPaths[rpi]; if (!hasOwn(inputs, p)) { return { verdict: "DENY", ruleFired: `required-input-absent:${p}`, engine: REF_EVAL_VERSION }; } } const ikeys = objectKeys(inputs); const ikn = arrayLength(ikeys); for (let iki = 0; iki < ikn; iki++) { const key = ikeys[iki]; assertScalar(inputs[key], `input.${key}`); } const rn = arrayLength(policy.rules); for (let ri = 0; ri < rn; ri++) { const rule = policy.rules[ri]; if (match(rule.when, inputs)) { return { verdict: rule.then, ruleFired: rule.id, engine: REF_EVAL_VERSION }; } } return { verdict: DEFAULT_VERDICT, ruleFired: null, engine: REF_EVAL_VERSION }; } catch { return { verdict: "DENY", ruleFired: "eval-error", engine: REF_EVAL_VERSION }; } }