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