UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

75 lines (74 loc) • 3.99 kB
import { combinePolicies } from "./tool-policy.js"; import { clampPermissionMode } from "./ask-origin.js"; export class TaskSpecTightenError extends Error { constructor(message) { super(message); this.name = "TaskSpecTightenError"; } } export function tightenTaskSpec(base, overrides) { const merged = { ...base, ...overrides }; if (base.toolPolicy && overrides.toolPolicy) { merged.toolPolicy = combinePolicies(base.toolPolicy, overrides.toolPolicy); } else { merged.toolPolicy = overrides.toolPolicy ?? base.toolPolicy; } for (const field of ["excludeTools", "deferTools"]) { const b = base[field]; const o = overrides[field]; if (b !== undefined || o !== undefined) { merged[field] = [...new Set([...(b ?? []), ...(o ?? [])])]; } } for (const faceKey of ["readFace", "writeFace"]) { if (base[faceKey] === "roots" && overrides[faceKey] === "open") { throw new TaskSpecTightenError(`tightenTaskSpec: cannot loosen ${faceKey} from "roots" to "open" (override may only tighten)`); } if (base[faceKey] !== undefined || overrides[faceKey] !== undefined) { merged[faceKey] = base[faceKey] === "roots" || overrides[faceKey] === "roots" ? "roots" : (overrides[faceKey] ?? base[faceKey]); } } if (base.readDenyPatterns !== undefined || overrides.readDenyPatterns !== undefined) { const seen = new Set(); const union = []; for (const e of [...(base.readDenyPatterns ?? []), ...(overrides.readDenyPatterns ?? [])]) { const key = typeof e === "string" ? `i:${e}` : `${e.caseSensitive === true ? "s" : "i"}:${e.pattern}`; if (seen.has(key)) continue; seen.add(key); union.push(e); } merged.readDenyPatterns = union; } if (base.handsReadOnly === true && overrides.handsReadOnly === false) { throw new TaskSpecTightenError("tightenTaskSpec: cannot loosen handsReadOnly from true to false (override may only tighten)"); } if (base.handsReadOnly !== undefined || overrides.handsReadOnly !== undefined) { merged.handsReadOnly = base.handsReadOnly === true || overrides.handsReadOnly === true; } if (base.permissionMode !== undefined && overrides.permissionMode !== undefined) { const { effective, clamped } = clampPermissionMode(overrides.permissionMode, base.permissionMode); if (clamped) { throw new TaskSpecTightenError(`tightenTaskSpec: permissionMode override "${overrides.permissionMode}" would loosen the base "${base.permissionMode}" (the override may only tighten; a bypass override needs a bypass base, acceptEdits and auto never adopt each other)`); } merged.permissionMode = effective; } else if (base.permissionMode !== undefined || overrides.permissionMode !== undefined) { merged.permissionMode = overrides.permissionMode ?? base.permissionMode; } if (base.shellGate !== undefined || overrides.shellGate !== undefined) { merged.shellGate = base.shellGate ?? overrides.shellGate; } if (base.onAsk !== undefined && overrides.onAsk !== undefined && base.onAsk !== overrides.onAsk) { throw new TaskSpecTightenError("tightenTaskSpec: conflicting onAsk on base and overrides — onAsk cannot be merged automatically " + "(the approver must be coordinated with the policy); set it in exactly one of base/overrides"); } merged.onAsk = base.onAsk ?? overrides.onAsk; if (base.hooks !== undefined && overrides.hooks !== undefined) { throw new TaskSpecTightenError("tightenTaskSpec: hooks set on both base and overrides cannot be merged automatically " + "(hook stacking is caller-specific); set hooks in exactly one of base/overrides, or compose them yourself"); } merged.hooks = base.hooks ?? overrides.hooks; return merged; }