@sema-agent/core
Version:
Stateless, task-oriented AI agent core
71 lines (70 loc) • 3.46 kB
JavaScript
import { combinePolicies } from "./tool-policy.js";
import { clampPermissionMode } from "./ask-origin.js";
import { CLAMP_SEATS, composeClampSeat, loosensClampSeat } from "./child-clamp-seats.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 ?? [])])];
}
}
{
const b = base.allowTools;
const o = overrides.allowTools;
if (b !== undefined && o !== undefined && !b.includes("*")) {
const wider = o.filter((entry) => !b.includes(entry));
if (wider.length > 0)
throw new TaskSpecTightenError(`tightenTaskSpec: cannot widen allowTools beyond the base list (override names ${JSON.stringify(wider)}; the override may only narrow)`);
}
const kept = o ?? b;
if (kept !== undefined)
merged.allowTools = [...kept];
else
delete merged.allowTools;
}
for (const seat of CLAMP_SEATS) {
const b = base[seat];
const o = overrides[seat];
if (loosensClampSeat(seat, b, o)) {
throw new TaskSpecTightenError(`tightenTaskSpec: cannot loosen ${seat} from ${JSON.stringify(b)} to ${JSON.stringify(o)} (override may only tighten)`);
}
if (b !== undefined || o !== undefined)
Object.assign(merged, { [seat]: composeClampSeat(seat, b, o) });
}
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.mods !== undefined || overrides.mods !== undefined) {
merged.mods = [...(base.mods ?? []), ...(overrides.mods ?? [])];
}
return merged;
}