UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

260 lines (259 loc) • 15 kB
import { types } from "node:util"; import { ASK_CLASSES, ASK_ORIGINS, isAskClass, isAskOrigin, isPersistedRuleMandate, PERSISTED_RULE_MANDATES } from "./ask-origin.js"; import { AUTO_MODE_FALLBACK_CAUSES, isAutoModeFallbackCause } from "./auto-mode.js"; import { thrownText } from "./thrown-value.js"; const ENGINE_REFUSALS = new WeakSet(); export const refusedOutOfContract = (message) => { const refused = Object.freeze({ action: "deny", message, decisionReason: "rule" }); ENGINE_REFUSALS.add(refused); return refused; }; export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d); const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"]); const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 }); const GUARDS = Object.keys({ dangerous_delete: 0, unverifiable_delete: 0, unverifiable_shell_text: 0, transcript_write: 0, artifact_shared_read: 0 }); const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0, persisted_rows: 0 }); const AUTHORED_BY_WORDS = Object.keys({ deployment: 0 }); const memberOf = (words) => { const set = new Set(words); return (v) => set.has(v); }; const CLOSED_MEMBERS = [ { member: "action", words: DECISION_ACTIONS, has: memberOf(DECISION_ACTIONS), required: true }, { member: "decisionReason", words: DECISION_REASONS, has: memberOf(DECISION_REASONS) }, { member: "origin", words: ASK_ORIGINS, has: isAskOrigin }, { member: "askClass", words: ASK_CLASSES, has: isAskClass }, { member: "classes", words: ASK_CLASSES, has: isAskClass, list: true }, { member: "foldedAskClasses", words: ASK_CLASSES, has: isAskClass, list: true }, { member: "mandate", words: PERSISTED_RULE_MANDATES, has: isPersistedRuleMandate }, { member: "guards", words: GUARDS, has: memberOf(GUARDS), list: true }, { member: "ruleUnreadable", words: RULE_UNREADABLE_WORDS, has: memberOf(RULE_UNREADABLE_WORDS) }, { member: "authoredBy", words: AUTHORED_BY_WORDS, has: memberOf(AUTHORED_BY_WORDS) }, { member: "classifierNoRuling", words: AUTO_MODE_FALLBACK_CAUSES, has: isAutoModeFallbackCause }, { member: "requiresRealApproval", words: [true, false], has: (v) => v === true || v === false }, { member: "probeMandated", words: [true], has: (v) => v === true }, { member: "mandated", words: [true], has: (v) => v === true }, ]; const RETIRED_WINDOW_MEMBER = "autoDenyAfterMs"; export const RETIRED_WINDOW_MEMBER_DENY_MESSAGE = `a permission decision's "denialLimitFallback" carries "${RETIRED_WINDOW_MEMBER}" — the decision names the counts only; ` + `the auto-deny window is decided by the ask's route (a live approver arms it, every other route runs none) and cannot be ` + `declared by a policy; denied fail-closed rather than silently replacing the declared number`; export const carriesRetiredWindow = (fallback) => typeof fallback === "object" && fallback !== null && fallback[RETIRED_WINDOW_MEMBER] !== undefined; const OPERAND_MEMBERS = Object.keys({ shown: 0, total: 0 }); const NESTED_CONTRACTS = [ { holder: "", member: "denialLimitFallback", members: Object.keys({ consecutive: 0, total: 0, limit: 0, autoDenyAfterMs: 0 }), required: Object.keys({ consecutive: 0, total: 0, limit: 0 }), }, { holder: "", member: "probeCause", members: Object.keys({ code: 0, roots: 0, further: 0 }), required: Object.keys({ code: 0, roots: 0 }) }, { holder: "probeCause.", member: "roots", members: OPERAND_MEMBERS, required: OPERAND_MEMBERS }, { holder: "probeCause.", member: "further", members: OPERAND_MEMBERS, required: OPERAND_MEMBERS }, ]; const nonNegativeInteger = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0; const NESTED_VALUE_FENCES = { denialLimitFallback: { consecutive: nonNegativeInteger, total: nonNegativeInteger, limit: (v) => v === "consecutive" || v === "total" }, probeCause: { code: (v) => typeof v === "string" && v.length > 0 }, "probeCause.roots": { total: nonNegativeInteger }, "probeCause.further": { total: nonNegativeInteger }, }; const NESTED_LIST_MEMBERS = { "probeCause.roots": { shown: { words: ["a string"], has: (x) => typeof x === "string" } }, "probeCause.further": { shown: { words: ["a string"], has: (x) => typeof x === "string" } }, }; function renderForeignValue(v) { try { return JSON.stringify(String(v).slice(0, 64)); } catch { return `[unprintable ${typeof v}]`; } } export function screenContractValues(contract, opts) { for (const { member, words, has, list, required } of CLOSED_MEMBERS) { if (opts?.skip?.has(member) === true) continue; const v = contract[member]; if (v === undefined) { if (required === true && opts?.optional?.has(member) !== true) return refusedOutOfContract(`a permission decision's "${member}" is absent — the closed set is ${words.join("/")}; denied fail-closed rather than reading a word outside it as an allow`); continue; } if (list === true) { const received = receiveList(v, member, words, has); if (received.kind === "refused") return received.refused; contract[member] = received.held; continue; } if (has(v)) continue; return unrecognized(member, v, words); } if (carriesRetiredWindow(contract.denialLimitFallback)) return refusedOutOfContract(RETIRED_WINDOW_MEMBER_DENY_MESSAGE); return undefined; } const unrecognized = (member, v, words) => refusedOutOfContract(`a permission decision carries an unrecognized "${member}" value (${renderForeignValue(v)}) — ` + `the closed set is ${words.join("/")}; denied fail-closed rather than letting consumers branch on a value this layer cannot classify`); function receiveList(v, member, words, has) { if (typeof v !== "object" || v === null) return { kind: "refused", refused: unrecognized(member, v, words) }; const ordinary = ordinaryChain(v); if (ordinary.kind === "not") return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" ${ordinary.why} — the engine reads a list off an ordinary array's own property descriptors and runs none of the carrier's code; denied fail-closed`) }; if (!Array.isArray(v)) return { kind: "refused", refused: unrecognized(member, v, words) }; let own; try { own = Object.getOwnPropertyDescriptors(v); } catch (e) { return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" threw while its property descriptors were looked at (${thrownText(e)}) — not an ordinary array; denied fail-closed`) }; } const length = Object.hasOwn(own, "length") ? own.length : undefined; if (length === undefined || !Object.hasOwn(length, "value") || typeof length.value !== "number") return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" has no plain length — not an ordinary array; denied fail-closed`) }; const held = []; for (let i = 0; i < length.value; i++) { const d = Object.hasOwn(own, String(i)) ? own[String(i)] : undefined; if (d === undefined) return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" has no element at index ${i} (a hole) — the closed set is ${words.join("/")}; denied fail-closed rather than reading a hole as a word`) }; if (!Object.hasOwn(d, "value")) return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" element ${i} is held by an accessor — list elements must be own data properties, not values supplied through an accessor; denied fail-closed`) }; if (!has(d.value)) return { kind: "refused", refused: unrecognized(member, d.value, words) }; held.push(d.value); } return { kind: "held", held }; } export function receiveNestedRecords(contract) { const records = Object.assign(Object.create(null), { "": contract }); for (const { holder, member, members, required } of NESTED_CONTRACTS) { const held = records[holder]; if (held === undefined) continue; const v = held[member]; if (v === undefined) continue; const path = `${holder}${member}`; const shape = `a nested contract record is a plain object holding ${required.map((r) => `"${r}"`).join(", ")}`; if (typeof v !== "object" || v === null) { return refusedOutOfContract(`a permission decision's "${path}" is ${renderForeignValue(v)} (not an object) — ${shape}; denied fail-closed rather than reading a value outside the contract as a record`); } const nested = receiveContract(v, members, `${path}.`); if (nested.kind === "refused") return nested.refused; if (Array.isArray(v)) return refusedOutOfContract(`a permission decision's "${path}" is an array — ${shape}; denied fail-closed rather than reading a list's properties as a record`); const missing = required.find((r) => nested.held[r] === undefined); if (missing !== undefined) return refusedOutOfContract(`a permission decision's "${path}" record is missing its required member "${missing}" — ${shape}; denied fail-closed rather than reading a partial record`); for (const [m, { words, has }] of Object.entries(NESTED_LIST_MEMBERS[path] ?? {})) { const value = nested.held[m]; if (value === undefined) continue; const received = receiveList(value, `${path}.${m}`, words, has); if (received.kind === "refused") return received.refused; nested.held[m] = received.held; } for (const [m, ok] of Object.entries(NESTED_VALUE_FENCES[path] ?? {})) { const value = nested.held[m]; if (value !== undefined && !ok(value)) return refusedOutOfContract(`a permission decision's "${path}.${m}" is ${renderForeignValue(value)} — outside what the record's type admits; denied fail-closed rather than reading a value this layer cannot classify`); } held[member] = records[`${path}.`] = nested.held; } return undefined; } export function nestedRecordsOf(receipt) { const ownRecord = (o, k) => (typeof o === "object" && o !== null && Object.hasOwn(o, k) ? o[k] : undefined); const out = []; for (const { holder, member } of NESTED_CONTRACTS) { const record = ownRecord(holder.split(".").filter(Boolean).reduce(ownRecord, receipt), member); if (typeof record === "object" && record !== null) out.push(record); } return out; } const spellsAbsence = (d) => Object.hasOwn(d, "value") && d.value === undefined; function ordinaryChain(carrier) { const chain = []; const seen = new Set(); try { for (let o = carrier; o !== null; o = Object.getPrototypeOf(o)) { if (types.isProxy(o)) return { kind: "not", why: o === carrier ? "is a Proxy" : "has a Proxy on its prototype chain" }; if (seen.has(o)) return { kind: "not", why: "has a cyclic prototype chain" }; seen.add(o); chain.push(o); } } catch (e) { return { kind: "not", why: `threw while its prototype chain was looked at (${thrownText(e)})` }; } return { kind: "chain", chain }; } export function receiveContract(carrier, members, path, retired) { const what = path === "" ? "a permission decision" : `a permission decision's "${path.slice(0, -1)}" record`; const ordinary = ordinaryChain(carrier); if (ordinary.kind === "not") { return { kind: "refused", refused: refusedOutOfContract(`${what} ${ordinary.why} — the engine reads a decision off an ordinary object's own property descriptors and runs none of the carrier's code; denied fail-closed`) }; } let own; try { own = Object.getOwnPropertyDescriptors(carrier); } catch (e) { return { kind: "refused", refused: refusedOutOfContract(`${what} threw while its property descriptors were looked at (${thrownText(e)}) — not an ordinary object; denied fail-closed`) }; } const nearest = (m) => { try { if (Object.hasOwn(own, m)) return { kind: "found", d: own[m] }; for (let i = 1; i < ordinary.chain.length; i++) { const d = Object.getOwnPropertyDescriptor(ordinary.chain[i], m); if (d !== undefined) return { kind: "found", d }; } return { kind: "found", d: undefined }; } catch (e) { return { kind: "refused", refused: refusedOutOfContract(`${what} threw while "${path}${m}" was looked for on its prototype chain (${thrownText(e)}) — not an ordinary object; denied fail-closed`) }; } }; const held = Object.create(null); for (const m of members) { const d = Object.hasOwn(own, m) ? own[m] : undefined; if (d !== undefined && !Object.hasOwn(d, "value")) { return { kind: "refused", refused: refusedOutOfContract(`${what}'s contract member "${path}${m}" is held by an accessor — contract members must be own data properties, not values supplied through an accessor; denied fail-closed`) }; } if (d !== undefined && d.enumerable === true) { held[m] = d.value; continue; } const found = nearest(m); if (found.kind === "refused") return found; const reached = found.d; if (reached !== undefined && !spellsAbsence(reached)) { return { kind: "refused", refused: refusedOutOfContract(`${what} supplies "${path}${m}" other than as its own enumerable property (its prototype or a non-enumerable property) — ` + `contract fields must be own properties, so every reader sees the one value the engine read; denied fail-closed`), }; } } for (const [m, replacement] of Object.entries(retired ?? {})) { const found = nearest(m); if (found.kind === "refused") return found; if (found.d !== undefined && !spellsAbsence(found.d)) { return { kind: "refused", refused: refusedOutOfContract(`a permission decision carries the retired "${m}" member — ${replacement}; denied fail-closed rather than executing a decision with a member this layer does not read`) }; } } return { kind: "held", held }; }