UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

245 lines (244 loc) • 12.1 kB
import { createFsWriteGatePolicy } from "./fs-write-gate-policy.js"; import { isProtectedWrite } from "./tool-registry.js"; import { rosterEntryNamed } from "./tool-roster.js"; import { wireOf } from "./tool-name-readings.js"; import { clampPermissionMode, isPermissionMode, PERMISSION_MODES } from "./ask-origin.js"; import { argvOf, lexShellText } from "../shell/lex.js"; import { COMMAND_RULE_TOOL } from "./permission-rule-model.js"; import { createSafeNotifier } from "./safe-notify.js"; import { assertNoRetiredKeys, RETIRED_TASK_SPEC_KEYS } from "./retired-keys.js"; import { resolveOutputStyleFrame } from "./output-style.js"; import { CheckpointError, isCheckpointConfigRefusalCode, recordedModeSeed } from "./checkpoint-store.js"; export const PERMISSION_MODE_FOLD_REASONS = Object.freeze(["operator_deny", "face_absent", "inherited_ceiling"]); export const ACCEPT_EDITS_FILE_COMMANDS = Object.freeze(["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"]); const MODE_POLICY_TABLE = { plan: { enablePlanMode: true, writeGate: "plan", fileCommands: [], writeFace: undefined, unstated: "ask" }, acceptEdits: { enablePlanMode: false, writeGate: "accept_root", fileCommands: ACCEPT_EDITS_FILE_COMMANDS, writeFace: undefined, unstated: "ask" }, default: { enablePlanMode: false, writeGate: "ask", fileCommands: [], writeFace: undefined, unstated: "ask" }, auto: { enablePlanMode: false, writeGate: "ask", fileCommands: [], writeFace: undefined, unstated: "ask" }, dontAsk: { enablePlanMode: false, writeGate: "ask", fileCommands: [], writeFace: undefined, unstated: "ask" }, bypassPermissions: { enablePlanMode: false, writeGate: "none", fileCommands: [], writeFace: "open", unstated: "none" }, }; export function modePolicyOf(mode) { return MODE_POLICY_TABLE[mode]; } export function modeAcceptsFileCommand(mode, toolName, args) { const commands = modePolicyOf(mode).fileCommands; if (commands.length === 0 || toolName !== COMMAND_RULE_TOOL) return false; const command = args?.command; if (typeof command !== "string" || args.run_in_background === true) return false; const lexed = lexShellText(command); if ("reject" in lexed || lexed.segments.length === 0 || lexed.substitutions.length > 0) return false; return lexed.segments.every((segment) => { if (segment.connector === "|" || segment.connector === "&" || segment.tokens.some((t) => t.kind !== "word")) return false; const head = argvOf(segment)[0]; return head !== undefined && head.assignment === undefined && head.expansions.length === 0 && head.raw === head.folded && commands.includes(head.raw); }); } export function mountedToolDeclaresReadOnly(tools, toolName) { return tools.findLast((t) => t.name === toolName)?.mcpReadOnlyHint === true; } export function asksUnstated(d, seat, mode) { return d.action === "allow" && d.decisionReason === undefined && seat === "passthrough" && mode !== undefined && modePolicyOf(mode).unstated === "ask"; } export const unstatedAskMessage = (toolName) => `The agent requested permission to use ${toolName}, but it has not been granted yet.`; export function permissionSeatAt(input, args) { if (input.permissionSeatOf !== undefined) return input.permissionSeatOf(args); return input.permissionSeat === "passthrough" ? { seat: "passthrough" } : undefined; } export function mountedToolPermissionSeat(tools, toolName) { return tools.findLast((t) => t.name === toolName)?.permissionSeat === "passthrough" ? "passthrough" : undefined; } export function mountedToolRequiresUserInteraction(tools, toolName) { return tools.findLast((t) => t.name === toolName)?.mcpRequiresUserInteraction === true; } const ENGINE_ROSTER_SOURCES = new Set(["builtin", "synthetic"]); export function planRefusesWrite(req, roster, readOnlyDeclared) { if (isProtectedWrite(req)) return true; const row = roster === undefined ? undefined : rosterEntryNamed(roster, wireOf("mode.planRosterRow", req)); return row !== undefined && !ENGINE_ROSTER_SOURCES.has(row.source) && row.effect !== "read" && !(row.effectProvenance === "default" && readOnlyDeclared?.(row.name) === true); } const planAsk = (message) => ({ action: "ask", message, decisionReason: "mode" }); export function createModeGatePolicy(modeRef, seats) { const exemptDirs = seats.exemptDirs !== undefined && seats.exemptDirs.length > 0 ? { exemptDirs: seats.exemptDirs } : {}; const askGate = createFsWriteGatePolicy({ env: seats.env, rootPath: seats.rootPath, ...exemptDirs, defaultWrite: "ask" }); const acceptRootGate = createFsWriteGatePolicy({ env: seats.env, rootPath: seats.rootPath, acceptDirs: seats.acceptDirs !== undefined && seats.acceptDirs.length > 0 ? [...seats.acceptDirs] : [seats.rootPath], ...exemptDirs, defaultWrite: "ask" }); const planGate = createFsWriteGatePolicy({ env: seats.env, rootPath: seats.rootPath, ...exemptDirs, defaultWrite: "ask", defaultAsk: (path, note) => planAsk(`Cannot write to ${path} while in plan mode.${note ?? ""}`) }); return { async check(req, signal) { const gate = modePolicyOf(modeRef.current).writeGate; if (gate === "none") return { action: "allow" }; if (gate === "plan") { if (!planRefusesWrite(req, seats.roster?.current, seats.readOnlyDeclared)) return { action: "allow" }; if (!isProtectedWrite(req)) return planAsk(`Cannot call ${req.toolName} while in plan mode.`); return planGate.check(req, signal); } return (gate === "ask" ? askGate : acceptRootGate).check(req, signal); }, }; } export function modeRequestAfterResume(seat, recorded, outcome) { const exitsPlan = outcome?.gate === "plan_review" && outcome.decision !== "reject" && recorded.permissionMode === "plan"; if (!exitsPlan) return seat ?? recorded.permissionMode; return seat !== undefined && seat !== "plan" ? seat : recorded.prePlanMode; } export function refusePermissionModeDoors(spec) { assertNoRetiredKeys(spec, RETIRED_TASK_SPEC_KEYS, "TaskSpec"); const mode = spec.permissionMode; if (mode !== undefined && !isPermissionMode(mode)) { const e = new Error(`permissionMode must be one of ${PERMISSION_MODES.map((w) => JSON.stringify(w)).join(", ")} when present (got ${JSON.stringify(mode)}) — omit it for the default mode; an unrecognized word is refused rather than read as any mode.`); e.code = "config.permission_mode_invalid"; throw e; } const floor = spec.shellGate; if (floor !== undefined && floor !== "always") { const e = new Error(`shellGate must be the floor word "always" when present (got ${JSON.stringify(floor)}) — the doctrine values "off" / "classify" are retired: the shell's shape tier is installed on every full shell and the permission mode (permissionMode) decides what its asks become; omit shellGate for no operator floor.`); e.code = "config.shell_gate_value_retired"; throw e; } return mode; } export function refuseResumeDoors(config, recordedGate) { screenAs("taskConfig", () => { refusePermissionModeDoors(config); resolveOutputStyleFrame(config.outputStyle); }); screenAs("inheritedGate", () => void recordedModeSeed(recordedGate)); } function screenAs(field, screen) { try { screen(); } catch (e) { if (e instanceof Error && "code" in e && isCheckpointConfigRefusalCode(e.code)) throw new CheckpointError("checkpoint.config_refused", e.message, { field, code: e.code }); throw e; } } export class PermissionModeController { requestedBySpec; ceiling; steers = []; arming; onFold; prePlan; constructor(requestedBySpec, ceiling, opts) { this.requestedBySpec = requestedBySpec; this.ceiling = ceiling; this.onFold = opts?.onFold; if (requestedBySpec === "plan") this.prePlan = opts?.prePlanMode; } get prePlanMode() { return this.declared === "plan" ? this.prePlan : undefined; } get requested() { return this.steers.at(-1)?.word ?? this.requestedBySpec; } ceilingNow(observe) { if (this.ceiling === undefined || typeof this.ceiling === "string") return this.ceiling; return observe ? (this.ceiling.observed ?? this.ceiling.current) : this.ceiling.current; } get declared() { return this.declaration().effective; } declaration(observe = false) { const ceiling = this.ceilingNow(observe); if (ceiling === undefined) return { effective: this.requested ?? "default", clamped: false }; return clampPermissionMode(this.requested, ceiling); } armedForAuto = false; faultSeat = createSafeNotifier(); get current() { const effective = this.receipt().effective; const arming = this.arming; if (effective === "auto" && !this.armedForAuto && arming?.armAuto !== undefined) { this.armedForAuto = true; void new Promise((resolve) => resolve(arming.armAuto?.())).catch((e) => this.faultSeat.notifyAsync(() => arming.onArmFault?.(e), "permissionMode.onArmFault")); } return effective; } get observed() { return this.receiptOf(true).effective; } receipt() { return this.receiptOf(false); } receiptOf(observe) { const { effective: declared, clamped } = this.declaration(observe); const requested = this.requested; const base = requested !== undefined ? { requested } : {}; const a = this.arming; const armingFold = a === undefined ? undefined : declared === "bypassPermissions" && a.caps?.bypassPermissions === false ? "operator_deny" : declared === "auto" && !a.autoFacePresent ? "face_absent" : declared === "auto" && a.caps?.autoMode === false ? "operator_deny" : undefined; if (armingFold !== undefined) return { ...base, effective: "default", foldedBy: armingFold }; if (clamped) return { ...base, effective: declared, foldedBy: "inherited_ceiling" }; return { ...base, effective: declared }; } ceilingRebound(before) { const after = this.receipt(); if (after.foldedBy === "inherited_ceiling" && (before.foldedBy !== "inherited_ceiling" || before.effective !== after.effective)) this.onFold?.(after); } bindArming(arming) { const before = this.receipt(); this.arming = arming; const receipt = this.receipt(); if (receipt.foldedBy !== undefined && receipt.foldedBy !== before.foldedBy) this.onFold?.(receipt); return receipt; } async steer(mode) { const entry = { word: mode }; const before = this.declared; this.steers.push(entry); const receipt = this.receipt(); const arming = this.arming; if (receipt.effective === "auto" && arming?.armAuto !== undefined) { this.armedForAuto = true; try { await arming.armAuto(); } catch (e) { this.armedForAuto = false; const at = this.steers.indexOf(entry); if (at >= 0) this.steers.splice(at, 1); throw e; } } const at = this.steers.indexOf(entry); if (at > 0) this.steers.splice(0, at); if (this.steers.at(-1) === entry) { if (mode !== "plan") this.prePlan = undefined; else if (before !== "plan") this.prePlan = before; } if (receipt.foldedBy !== undefined) this.onFold?.(receipt); return receipt; } }