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