UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

422 lines (421 loc) • 23.2 kB
import { COMMAND_RULE_TOOL, PATH_RULE_FACES, escapeForDisclosure, parseRuleText, programRunReachOf, ruleReachesProgramRun, ruleToolGrammarOf } from "./permission-rule-model.js"; import { isNameableToolName, ruleSpellingOf } from "./permission-rule-syntax.js"; import { isShellFaceToolName, pathTargetOf } from "./tool-registry.js"; import { writerOf } from "./permission-rule-store.js"; import { canonicalize } from "./canonical-json.js"; import { types } from "node:util"; import { describeFault, receive } from "./mods/kernel/receive.js"; import { DEFAULT_CAPS } from "./permission-rules.js"; export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable"; export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000; export function settleOrgVerdictWithin(p, fallback, opts) { return new Promise((resolve) => { let settled = false; const finish = (v, cause) => { if (settled) return; settled = true; clearTimeout(timer); opts.signal?.removeEventListener("abort", onAbort); if (cause !== undefined) { try { opts.onFallback?.(cause); } catch { } } resolve(v); }; const onAbort = () => finish(fallback, "aborted"); const timer = setTimeout(() => finish(fallback, "timeout"), opts.timeoutMs); if (opts.signal?.aborted === true) { finish(fallback, "aborted"); return; } opts.signal?.addEventListener("abort", onAbort); p.then((v) => finish(v), () => finish(fallback)); }); } export const ORG_RULE_DECISION_REASON = "org_rule"; export const ORG_FETCHED_AT_SKEW_ALLOWANCE_MS = 5 * 60_000; export const UNGOVERNED_ORG_RESOLUTION = Object.freeze({ source: "org", status: "ungoverned", rules: [], disclosures: [] }); export function createOrgRulePartition(cfg) { if (cfg === null || typeof cfg !== "object") { throw new Error("the org partition config must be an object naming a snapshot source and a staleness bound"); } if (cfg.source === undefined || typeof cfg.source.current !== "function") { throw new Error("org-governed is declared but no OrgRuleSnapshotProvider is wired as `org.source` — a governed deployment without an org source is a configuration contradiction; refusing to boot rather than running silently ungoverned"); } if (typeof cfg.stalenessBoundMs !== "number" || !Number.isFinite(cfg.stalenessBoundMs) || cfg.stalenessBoundMs <= 0) { throw new Error(`org stalenessBoundMs must be a finite positive number of milliseconds (got ${escapeForDisclosure(cfg.stalenessBoundMs)})`); } const provider = cfg.source; const now = cfg.now ?? Date.now; let memoryState; const readState = async () => { const state = cfg.persistence !== undefined ? await cfg.persistence.readOrgState() : memoryState; return state === undefined ? undefined : orgStateCopyOf(state); }; const writeState = async (state) => { if (cfg.persistence !== undefined) await cfg.persistence.installOrgState(structuredClone(state)); else memoryState = structuredClone(state); }; let chain = Promise.resolve(); const serialize = (fn) => { const run = chain.then(fn, fn); chain = run.then(() => undefined, () => undefined); return run; }; const resolve = async () => { const disclosures = []; if (cfg.persistence === undefined) { disclosures.push("org rule state has no durable persistence wired — the last-known-good snapshot and the anti-rollback mark do not survive a restart"); } let fetched; let fetchFailed = false; const settled = await settleOrgVerdictWithin(provider.current().then((v) => ({ v }), (e) => ({ e })), { timedOut: true }, { timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS }); try { if ("timedOut" in settled) throw new Error(`the org snapshot source did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms`); if ("e" in settled) throw settled.e; fetched = orgSnapshotCopyOf(settled.v); } catch (err) { fetchFailed = true; disclosures.push(`org snapshot source failed: ${escapeForDisclosure(err instanceof Error ? err.message : err)}`); } return await settleOrgVerdictWithin(serialize(async () => { const nowMs = now(); const guardedRead = async (context) => { try { return await readState(); } catch (err) { disclosures.push(`org state ${context} read failed: ${escapeForDisclosure(err instanceof Error ? err.message : err)}`); return undefined; } }; const installed = await (async () => { const persisted = await guardedRead("persistence"); if (fetched === undefined) return persisted; const invalid = validateOrgSnapshot(fetched, nowMs); if (invalid !== undefined) { disclosures.push(`org snapshot refused: ${invalid}; the previously installed snapshot (if any) remains in force`); return persisted; } const persistedFloor = persisted === undefined ? undefined : antiRollbackFloor(persisted); if (persisted !== undefined && !persistedMarkIsCoherent(persisted)) { disclosures.push(`the durable org state carries a high-water mark (${escapeForDisclosure(persisted.revisionHighWater)}) that is not a finite number at or above its own snapshot revision (${escapeForDisclosure(persisted.snapshot?.revision)}) — the anti-rollback fence is being held at ${escapeForDisclosure(persistedFloor)} instead, and the durable state should be repaired`); } if (persistedFloor !== undefined && fetched.revision < persistedFloor) { disclosures.push(`org snapshot revision ${fetched.revision} is below the installed high-water mark ${escapeForDisclosure(persistedFloor)} — refusing the rollback; the current deny set is unchanged`); return persisted; } if (persisted !== undefined && fetched.revision === persisted.snapshot.revision && sameOrgPolicyContent(fetched, persisted.snapshot) && fetched.fetchedAtMs <= persisted.snapshot.fetchedAtMs) { return persisted; } if (persisted !== undefined && fetched.revision === persisted.snapshot.revision && !sameOrgPolicyContent(fetched, persisted.snapshot)) { disclosures.push(`org snapshot revision ${fetched.revision} equals the installed revision but carries DIFFERENT policy content — refusing the swap; the current deny set is unchanged`); return persisted; } const state = { revisionHighWater: Math.max(persistedFloor !== undefined && Number.isFinite(persistedFloor) ? persistedFloor : 0, fetched.revision), snapshot: fetched, installedAtMs: nowMs, }; try { await writeState(state); } catch (err) { const reread = await guardedRead("reconciliation"); if (reread !== undefined && antiRollbackFloor(reread) >= state.revisionHighWater) { disclosures.push(`org snapshot install was superseded by a concurrent install at revision ${escapeForDisclosure(reread.snapshot.revision)} — continuing on the newer installed state`); return reread; } disclosures.push(`org snapshot install failed (${escapeForDisclosure(err instanceof Error ? err.message : err)}) and no superseding installed state exists — org adjudication is UNAVAILABLE`); return undefined; } return state; })(); const decisionMs = now(); const withinBound = (snap) => decisionMs - snap.fetchedAtMs <= cfg.stalenessBoundMs && snap.fetchedAtMs <= decisionMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS; if (installed !== undefined) { if (installed.snapshot.fetchedAtMs > decisionMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS) { disclosures.push(`the installed org snapshot claims a FUTURE observation time (${escapeForDisclosure(installed.snapshot.fetchedAtMs)} vs now ${decisionMs}) — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`); return { source: "org", status: "unavailable", rules: [], disclosures }; } const servingRefusal = validateOrgSnapshot(installed.snapshot, decisionMs); if (servingRefusal !== undefined) { disclosures.push(`the installed org snapshot (revision ${escapeForDisclosure(installed.snapshot.revision)}) does not meet this version's validator: ${servingRefusal} — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`); return { source: "org", status: "unavailable", rules: [], disclosures }; } if (withinBound(installed.snapshot)) { const freshlyInstalled = fetched !== undefined && installed.snapshot === fetched; if (!freshlyInstalled && !fetchFailed && fetched === undefined) { disclosures.push("org snapshot source returned no snapshot — continuing on the last-known-good"); } const degraded = describeUnreadableOrgRows(readListedOrgRules(installed.snapshot.rules).unreadable, installed.snapshot.revision); if (degraded !== undefined) disclosures.push(degraded); return { source: "org", status: freshlyInstalled ? "fresh" : "last-known-good", rules: structuredClone(installed.snapshot.rules), revision: installed.snapshot.revision, disclosures, }; } disclosures.push(`the newest org snapshot was observed ${decisionMs - installed.snapshot.fetchedAtMs}ms ago (bound ${cfg.stalenessBoundMs}ms) — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`); return { source: "org", status: "unavailable", rules: [], disclosures }; } disclosures.push("no org snapshot has ever been installed — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask"); return { source: "org", status: "unavailable", rules: [], disclosures }; }), { source: "org", status: "unavailable", rules: [], disclosures: [...disclosures, `org state persistence did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms — org adjudication is UNAVAILABLE; the consuming gate must tighten every terminal allow to a real-approval ask`], }, { timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS }); }; return { resolve }; } function antiRollbackFloor(state) { const mark = typeof state.revisionHighWater === "number" && Number.isFinite(state.revisionHighWater) ? state.revisionHighWater : undefined; const own = typeof state.snapshot?.revision === "number" && Number.isFinite(state.snapshot.revision) ? state.snapshot.revision : undefined; if (mark === undefined) return own ?? Number.NEGATIVE_INFINITY; return own === undefined ? mark : Math.max(mark, own); } function persistedMarkIsCoherent(state) { return (typeof state.revisionHighWater === "number" && Number.isFinite(state.revisionHighWater) && typeof state.snapshot?.revision === "number" && Number.isFinite(state.snapshot.revision) && state.revisionHighWater >= state.snapshot.revision); } export function sameOrgPolicyContent(a, b) { if (a.rules.length !== b.rules.length) return false; const policy = (r) => canonicalize(orgRowPolicyOf(r)); return a.rules.every((r, i) => policy(b.rules[i]) === policy(r)); } const DISCLOSED_UNREADABLE_ROWS = 20; function reachOfRowHead(text, behavior) { const head = typeof text === "string" ? ruleSpellingOf(text)?.toolName : undefined; if (head === undefined || !isNameableToolName(head)) return "every"; if (head === COMMAND_RULE_TOOL || isShellFaceToolName(head)) return { words: [COMMAND_RULE_TOOL], faces: [] }; if (ruleToolGrammarOf(head) === undefined) return "every"; const column = Object.hasOwn(PATH_RULE_FACES, head) ? PATH_RULE_FACES[head]?.[behavior] : undefined; return { words: [head], faces: column ?? [] }; } function rowReaches(reach, words, face) { if (reach === "every") return true; return reach.words.some((w) => words.includes(w)) || (face !== undefined && reach.faces.includes(face)); } function listRefusalOf(rules) { if (typeof rules === "object" && rules !== null) { if (types.isProxy(rules)) return "rules is a Proxy, not an array"; const held = HELD_LIST_REFUSALS.get(rules); if (held !== undefined) return held; } if (!Array.isArray(rules)) return "rules is not an array"; const cap = DEFAULT_CAPS.maxRules; return rules.length > cap ? `rules list has ${rules.length} entries; the cap is ${cap} — refused whole (not truncated)` : undefined; } export const ORG_ROW_POLICY_MEMBERS = Object.freeze(["rule", "behavior"]); const ORG_SNAPSHOT_ENVELOPE_MEMBERS = Object.freeze(["revision", "fetchedAtMs", "signature"]); const ORG_STATE_MEMBERS = Object.freeze(["revisionHighWater", "installedAtMs"]); function scalarMembersOf(value, members) { const held = {}; if (typeof value !== "object" || value === null) return held; for (const k of members) { const v = Object.hasOwn(value, k) ? value[k] : undefined; if (typeof v === "string" || typeof v === "boolean" || v === null || (typeof v === "number" && Number.isFinite(v))) held[k] = v; } return held; } function orgRowPolicyOf(row) { return scalarMembersOf(row, ORG_ROW_POLICY_MEMBERS); } const ORG_RULES_DEPTH = 2; const ORG_RULES_RECEIPT = Object.freeze({ accessors: "call-once", depth: ORG_RULES_DEPTH, others: "copy", contain: true, isProxy: (v) => types.isProxy(v) }); export function orgRuleListOf(rules) { const refused = listRefusalOf(rules); if (refused !== undefined) return { refused }; const received = receive(rules, ORG_RULES_RECEIPT); if (!received.ok) return { refused: `rules could not be received (${describeFault(received.fault)})` }; const copy = received.value; return { rows: Array.from({ length: copy.length }, (_, i) => orgRowPolicyOf(Object.hasOwn(copy, i) ? copy[i] : undefined)) }; } const HELD_LIST_REFUSALS = new WeakMap(); function heldRefusalOf(words) { const held = Object.freeze({}); HELD_LIST_REFUSALS.set(held, words); return held; } function orgSnapshotCopyOf(v) { if (v === undefined) return undefined; if (typeof v === "object" && v !== null && types.isProxy(v)) throw new Error("the snapshot is a Proxy, not data"); const envelope = scalarMembersOf(v, ORG_SNAPSHOT_ENVELOPE_MEMBERS); const rules = typeof v === "object" && v !== null ? v.rules : undefined; const refusal = listRefusalOf(rules); if (refusal !== undefined) return { ...envelope, rules: heldRefusalOf(refusal) }; const list = orgRuleListOf(rules); if ("refused" in list) throw new Error(`the snapshot's ${list.refused}`); return { ...envelope, rules: list.rows }; } function orgStateCopyOf(state) { if (typeof state === "object" && state !== null && types.isProxy(state)) throw new Error("the persisted org state is a Proxy, not data"); const snapshot = typeof state === "object" && state !== null ? state.snapshot : undefined; return { ...scalarMembersOf(state, ORG_STATE_MEMBERS), snapshot: orgSnapshotCopyOf(snapshot ?? null) }; } function readOrgRow(row, index) { const { rule: text, behavior: said } = orgRowPolicyOf(row); const behavior = said === "ask" ? "ask" : "deny"; const unreadable = (code, detail) => ({ kind: "unreadable", index, rule: typeof text === "string" ? text : text === undefined ? "<unreadable>" : escapeForDisclosure(text), code, ...(detail !== undefined ? { detail } : {}), behavior, reach: reachOfRowHead(text, behavior), }); if (typeof text !== "string" || text === "") return unreadable("invalid.grammar", "no rule text"); if (said !== "deny" && said !== "ask") return unreadable("invalid.grammar", said === undefined ? "no behavior" : `behavior "${escapeForDisclosure(said)}" is not deny or ask`); const parsed = parseRuleText(text, said); if ("reject" in parsed) return unreadable(parsed.reject.code); if (parsed.rule.tool !== COMMAND_RULE_TOOL) return unreadable("unsupported.tool"); return { kind: "read", index, rule: text, behavior: said, parsed: parsed.rule }; } export function readOrgRules(rules) { const list = orgRuleListOf(rules); if ("refused" in list) return list; const rows = list.rows.map((row, i) => readOrgRow(row, i + 1)); const unreadable = rows.filter((r) => r.kind === "unreadable"); return { rows, unreadable, verdictFor(call) { const words = typeof call.tool === "string" ? [call.tool] : call.tool; const command = words.includes(COMMAND_RULE_TOOL) && typeof call.command === "string" ? call.command : undefined; let ask; let unjudged; for (const row of rows) { if (row.kind === "unreadable") { if (!rowReaches(row.reach, words, call.face)) continue; const verdict = { behavior: row.behavior, rule: row.rule, unreadable: unreadableOrgRowReason(row) }; if (row.behavior === "deny") return verdict; ask ??= verdict; continue; } if (command === undefined) continue; const outcome = programRunReachOf(row.parsed, command); if (outcome.reach === "not_reached") continue; if (outcome.reach === "unreadable") { unjudged ??= { behavior: "ask", rule: row.rule, unreadable: outcome.reason }; continue; } if (row.behavior === "deny") return { behavior: "deny", rule: row.rule }; ask ??= { behavior: "ask", rule: row.rule }; } return unjudged ?? ask; }, }; } export function unenforceableOrgRules(rules) { return readListedOrgRules(rules).unreadable.map((u) => ({ rule: u.rule, code: u.code })); } function readListedOrgRules(rules) { const reading = readOrgRules(rules); if ("refused" in reading) throw new TypeError(`the org rules cannot be read: ${reading.refused}`); return reading; } export function unreadableOrgRowReason(row) { return `row ${row.index} of the organization snapshot is not a rule this layer reads (${row.code}${row.detail !== undefined ? `: ${row.detail}` : ""}); republish it in the shell tool's command grammar`; } export function orgUnreadableClause(reason) { return `which could not be read against it (${reason})`; } export function describeUnreadableOrgRows(unreadable, revision) { if (unreadable.length === 0) return undefined; const named = unreadable .slice(0, DISCLOSED_UNREADABLE_ROWS) .map((u) => `row ${u.index} "${escapeForDisclosure(u.rule)}" (${u.code}${u.detail !== undefined ? `: ${u.detail}` : ""})`) .join(", "); const more = unreadable.length - DISCLOSED_UNREADABLE_ROWS; return (`the org snapshot${revision !== undefined ? ` (revision ${escapeForDisclosure(revision)})` : ""} is served DEGRADED: ${unreadable.length} row(s) this layer cannot read — ${named}${more > 0 ? ` and ${more} more` : ""}; ` + `each stands at its strictest reading over the calls it may reach (a deny row, or one whose behavior cannot be read, refuses them; an ask row asks a person), and every readable row stands as published — republish the named rows in the shell tool's command grammar`); } function validateOrgSnapshot(s, nowMs) { if (typeof s.revision !== "number" || !Number.isFinite(s.revision)) return `revision is not a finite number (${escapeForDisclosure(s.revision)})`; if (typeof s.fetchedAtMs !== "number" || !Number.isFinite(s.fetchedAtMs)) return `fetchedAtMs is not a finite number (${escapeForDisclosure(s.fetchedAtMs)})`; if (s.fetchedAtMs > nowMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS) { return `fetchedAtMs is ${s.fetchedAtMs - nowMs}ms in the future (allowance ${ORG_FETCHED_AT_SKEW_ALLOWANCE_MS}ms) — a future-dated observation would satisfy the staleness bound indefinitely`; } return listRefusalOf(s.rules); } export function orgRuleVerdictFor(rules, call) { return readListedOrgRules(rules).verdictFor(call); } export function orgRuleShadows(orgRules, rule) { return compileOrgShadowPredicate(orgRules)(rule); } export function compileOrgShadowPredicate(orgRules) { const { rows } = readListedOrgRules(orgRules); const denies = rows.flatMap((r) => (r.kind === "read" && r.behavior === "deny" ? [r.parsed] : [])); const standing = rows.flatMap((r) => (r.kind === "unreadable" && r.behavior === "deny" ? [r.reach] : [])); return (rule) => { if (standing.length > 0) { const word = isShellFaceToolName(rule.tool) ? COMMAND_RULE_TOOL : rule.tool; const target = pathTargetOf(rule.tool); const faces = Object.hasOwn(PATH_RULE_FACES, rule.tool) ? (PATH_RULE_FACES[rule.tool]?.allow ?? []) : target !== undefined ? [target.access === "read" ? "read" : "write"] : [undefined]; if (standing.some((reach) => faces.some((face) => rowReaches(reach, [word], face)))) return true; } if (rule.tool !== COMMAND_RULE_TOOL || denies.length === 0) return false; return denies.some((d) => ruleReachesProgramRun(d, rule.command)); }; } export function isWritablePermissionRuleStore(store) { return writerOf(store) !== undefined; }