@sema-agent/core
Version:
Stateless, task-oriented AI agent core
233 lines (232 loc) • 13.9 kB
JavaScript
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" };
}