UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

233 lines (232 loc) • 13.9 kB
import { readDenyEntryProblem } from "../tools/fs/read-deny.js"; import { COMPLIANCE_CAPABILITIES, complianceCapabilityProblem } from "./compliance.js"; import { writeProtectedEntryIdentity, writeProtectedEntryProblem } from "./write-protect.js"; function readDenyIdentity(entry) { return typeof entry === "string" ? `i:${entry}` : `${entry.caseSensitive === true ? "s" : "i"}:${entry.pattern}`; } function complianceIdentity(cap) { return cap; } const MAX_SCRIPT_LIST_ENTRIES = 16; const MAX_SCRIPT_ENTRY_CHARS = 256; const MAX_SCRIPT_DENY_WILDCARDS_PER_SEGMENT = 1; function assertScriptEntryLength(text, at, what, why) { if (text.length > MAX_SCRIPT_ENTRY_CHARS) throw new Error(`${at} is ${text.length} characters — a workflow script's ${what} is limited to ${MAX_SCRIPT_ENTRY_CHARS} (${why}).`); } function assertScriptDenyPatternBounded(pattern, at) { assertScriptEntryLength(pattern, at, "deny pattern", "the matcher runs on every read of the child's run"); for (const segment of pattern.split("/")) { const wildcards = segment.split("*").length - 1; if (wildcards > MAX_SCRIPT_DENY_WILDCARDS_PER_SEGMENT) { throw new Error(`${at} has a path segment with ${wildcards} \`*\` wildcards — a workflow script's deny pattern allows at most ` + `${MAX_SCRIPT_DENY_WILDCARDS_PER_SEGMENT} per path segment (the same expressiveness the engine's built-in deny table ` + `uses). Each \`*\` compiles to a greedy match, and more than one in a single segment makes the judgment cost grow ` + `superlinearly with the path length, on every read the child performs. Write the intent as separate entries ` + `(the set is a union, so more entries deny strictly more) or anchor it with literal segments.`); } } } function readDenyScriptEntry(el, at) { if (typeof el === "string") { if (el.length === 0) throw new Error(`${at} must be a non-empty pattern string`); assertScriptDenyPatternBounded(el, at); return el; } if (typeof el !== "object" || el === null || Array.isArray(el)) throw new Error(`${at} must be a pattern string or an object { pattern, caseSensitive? }`); const o = el; const pattern = o.pattern; const caseSensitive = o.caseSensitive; if (typeof pattern !== "string" || pattern.length === 0) throw new Error(`${at}.pattern must be a non-empty string, e.g. ".ssh" or "secrets/*.json"`); if (caseSensitive !== undefined && typeof caseSensitive !== "boolean") throw new Error(`${at}.caseSensitive must be a boolean`); assertScriptDenyPatternBounded(pattern, at); return caseSensitive === undefined ? { pattern } : { pattern, caseSensitive }; } function writeProtectedScriptEntry(el, at) { const why = "the judge runs on every write the child names"; if (typeof el === "string") { if (el.length === 0) throw new Error(`${at} must be a non-empty name string`); assertScriptEntryLength(el, at, "write-protected name", why); return el; } if (typeof el !== "object" || el === null || Array.isArray(el)) throw new Error(`${at} must be a name string or an object { name, kind }`); const o = el; const name = o.name; const kind = o.kind; if (typeof name !== "string" || name.length === 0) throw new Error(`${at}.name must be a non-empty string, e.g. ".npmrc" or ".config/app"`); if (typeof kind !== "string") throw new Error(`${at}.kind must be a string ("basename", "segment" or "segment-run")`); assertScriptEntryLength(name, at, "write-protected name", why); return { name, kind }; } function complianceScriptEntry(el, at) { if (typeof el !== "string") throw new Error(`${at} must be a capability word (${COMPLIANCE_CAPABILITIES.join(", ")})`); return el; } const READ_DENY_SCRIPT_LAW = Object.freeze({ atMost: MAX_SCRIPT_LIST_ENTRIES, shape: 'deny entries (a "/"-separated segment run such as ".ssh", or { pattern, caseSensitive? })', why: "Every entry is judged against every path the child reads, for the child's whole run; the deployment's own entries, and whichever built-in tiers the deployment has on (by default the credentials and home agent-config tiers), are in force on top of these.", entry: readDenyScriptEntry, }); const WRITE_PROTECTED_SCRIPT_LAW = Object.freeze({ atMost: MAX_SCRIPT_LIST_ENTRIES, shape: 'write-protected names (a literal name such as ".npmrc", a "/"-separated run such as ".config/app", or { name, kind })', why: "Every row is judged against every write the child names, for the child's whole run; the deployment's own table is in force beside these.", entry: writeProtectedScriptEntry, }); const COMPLIANCE_SCRIPT_LAW = Object.freeze({ atMost: MAX_SCRIPT_LIST_ENTRIES, shape: `capability words (${COMPLIANCE_CAPABILITIES.join(", ")})`, why: "The capability set is closed; a longer list can only repeat it.", entry: complianceScriptEntry, }); export const CLAMP_SET = Object.freeze({ handsReadOnly: Object.freeze({ law: "restricting", value: true, script: true, carrier: "clamp" }), interactiveTools: Object.freeze({ law: "restricting", value: false, script: true, carrier: "clamp" }), approvalReach: Object.freeze({ law: "restricting", value: "none", script: true, carrier: "clamp" }), readFace: Object.freeze({ law: "restricting", value: "roots", script: true, carrier: "clamp" }), writeFace: Object.freeze({ law: "restricting", value: "roots", script: true, carrier: "clamp" }), readDenyPatterns: Object.freeze({ law: "addOnly", identity: readDenyIdentity, entry: readDenyEntryProblem, script: READ_DENY_SCRIPT_LAW, deploymentLayer: "readDenyPatterns", carrier: "clamp" }), writeProtectedAdditions: Object.freeze({ law: "addOnly", identity: writeProtectedEntryIdentity, entry: writeProtectedEntryProblem, script: WRITE_PROTECTED_SCRIPT_LAW, deploymentLayer: "writeProtectedPaths", carrier: "clamp" }), complianceAdditionalDenies: Object.freeze({ law: "addOnly", identity: complianceIdentity, entry: complianceCapabilityProblem, script: COMPLIANCE_SCRIPT_LAW, deploymentLayer: "compliancePostureResolver", carrier: "clamp" }), oneShot: Object.freeze({ law: "restricting", value: true, script: true, carrier: "specSeat" }), memoryPersistenceCapable: Object.freeze({ law: "restricting", value: false, script: true, carrier: "specSeat" }), fileHistory: Object.freeze({ law: "restricting", value: "disabled", script: false, carrier: "specSeat" }), promptForm: Object.freeze({ law: "restricting", value: "minimal", script: false, carrier: "specSeat" }), }); const ROWS = Object.freeze(Object.entries(CLAMP_SET)); export const CLAMP_SEATS = Object.freeze(ROWS.map(([seat]) => seat)); export const SCRIPT_SEATS = Object.freeze(ROWS.filter(([, row]) => row.script !== false).map(([seat]) => seat)); export function composeClampSeat(seat, a, b) { const row = CLAMP_SET[seat]; if (row.law === "restricting") return a === row.value || b === row.value ? row.value : (b ?? a); const lists = [a, b]; return unionAddOnly(seat, ...lists); } function notAListSentence(owner, value) { return `${owner} must be an array or absent (got ${value === null ? "null" : typeof value}) — a non-array here would be iterated character by character, so it refuses loudly instead.`; } export function unionAddOnly(seat, ...lists) { const row = CLAMP_SET[seat]; const identity = row.identity; const seen = new Set(); const out = []; for (const list of lists) { if (list === undefined) continue; if (!Array.isArray(list)) throw clampRecordError(notAListSentence(seat, list)); for (const e of list) { if (row.entry(e, seat) !== undefined) { out.push(e); continue; } const key = identity(e); if (!seen.has(key)) (seen.add(key), out.push(e)); } } return out; } export function assertAddOnlySeats(spec) { for (const [seat, row] of ROWS) { if (row.law !== "addOnly") continue; const value = spec[seat]; if (value === undefined) continue; const owner = `TaskSpec.${seat}`; if (!Array.isArray(value)) throw clampRecordError(notAListSentence(owner, value)); const entries = value; for (const e of entries) { const problem = row.entry(e, owner); if (problem !== undefined) throw clampRecordError(problem); } } } export function loosensClampSeat(seat, base, override) { const row = CLAMP_SET[seat]; return row.law === "restricting" && base === row.value && override !== undefined && override !== row.value; } function projected(seat, v) { const row = CLAMP_SET[seat]; if (row.law === "restricting") return v === row.value ? row.value : undefined; return Array.isArray(v) && v.length > 0 ? Object.freeze(v.map((e) => (typeof e === "object" && e !== null ? { ...e } : e))) : undefined; } export function clampSourceOf(record, carrier) { const r = (record ?? {}); return Object.fromEntries(ROWS.filter(([, row]) => carrier === undefined || row.carrier === carrier).map(([seat]) => [seat, r[seat]])); } export function foldClampSources(...records) { return Object.fromEntries(CLAMP_SEATS.map((seat) => [seat, records.reduce((acc, rec) => composeClampSeat(seat, acc, rec?.[seat]), undefined)])); } export function clampSeatsOf(source, carrier) { const out = {}; for (const [seat, row] of ROWS) { if (carrier !== undefined && row.carrier !== carrier) continue; const v = projected(seat, source[seat]); if (v !== undefined) out[seat] = v; } return out; } export function clampRecordError(message) { const e = new Error(message); e.code = "config.invalid_argument"; return e; } export function assertClampRecord(record, owner, carrier, extra = []) { if (record === undefined) return; if (record === null || typeof record !== "object") throw clampRecordError(`${owner} must be a record of clamp seats, got ${record === null ? "null" : typeof record}`); const legal = [...ROWS.filter(([, row]) => carrier === undefined || row.carrier === carrier).map(([seat]) => seat), ...extra]; const unknown = Object.keys(record).filter((k) => record[k] !== undefined && !legal.includes(k)); if (unknown.length > 0) { throw clampRecordError(`${owner} names ${unknown.map((k) => JSON.stringify(k)).join(", ")}, which the clamp table does not carry there — the legal seats are ${legal.join(", ")}. Refused rather than ignored: a seat nobody reads would read to the caller as wired.`); } } export function clampValuesOf(...records) { return clampSeatsOf(foldClampSources(...records)); } export function childClampSeats(source) { return clampSeatsOf(source, "clamp"); } export const CHILD_CLAMP_SEATS_NOT_CARRIED = Object.freeze({ toolPolicy: "not a clamp a child spec copies: a parent's denies reach its children through the ancestor-constraint chain the child's own prepare folds, not by putting the parent's policy object on the child seat", shellGate: "travels on the inherited-gate lane (`InheritedGate.shellGate`, a trusted internals seat), not on the child spec — a spec-level copy would be a second source for the same clamp", permissionMode: "travels on the inherited-gate lane (`InheritedGate.permissionMode` / `permissionModeCeiling` — the parent's declared word and its LIVE seat): a child with no word of its own inherits that ceiling on every read, and a word it does declare is clamped under it; a spec-level copy would freeze the parent's word at spawn and be a second source for the same clamp", onAsk: "per-lane by construction: the delegation lane wraps the parent's approver with spawn provenance before it becomes the child's seat, and a lane with no approver must hand down none (a parent that declared nobody can be asked hands down no approver — its frozen seat is empty — and the declaration rides `approvalReach`)", mods: "the task seat travels on the parent spec seat (a child dispatches it as its own, its events carrying the child's identity), not as a clamp — a registration adds a handler, it does not narrow a value", allowTools: "the tool selector's own layer — a parent bounds its children through the selector's ceiling (`RunInternals.toolCeiling`, a trusted internals seat every engine lane fills), not by copying its own list onto the child seat, which would be a second source for the same bound", excludeTools: "the deny half of the same layer as `allowTools` above, same reading: the parent's reaches its children through the ceiling", deferTools: "roster disclosure control rather than containment: the lanes that copy it do so beside these seats, and the workflow lane authors its child roster from the governance baseline instead of the host's", }); export const ENGINE_CHILD_SPEC_CONSTRUCTORS = Object.freeze([ "src/agents/subagent.ts#buildChildSpec", "src/agents/subagent.ts#observerSpec", "src/agents/subagent.ts#resumeSpec", "src/agents/verify.ts#verifierSpec", "src/agents/verify.ts#fixLegSpec", "src/orchestration/workflow-primitives.ts#workflowChildClamps", ]); export function postureUnderClamps(posture, clamps) { return posture === "interactive" && (clamps.interactiveTools === false || clamps.approvalReach === "none") ? "headless" : posture; } export function engineHeadlessLegSeats() { return { interactiveTools: false, approvalReach: "none", interactionPosture: "headless" }; }