@sema-agent/core
Version:
Stateless, task-oriented AI agent core
366 lines (365 loc) • 17.1 kB
JavaScript
import { abortedSeatDeny, decisionText, describeThrown, receiveHookResult, isEngineRefusal } from "./tool-policy.js";
import { asThrownError } from "./thrown-value.js";
import { createSettlementLedger, engineFailClosedExit, hookDenyExit } from "./runner/gate-exit.js";
import { traceHookCrash } from "./gate-crash-trace.js";
import { runGateFold } from "./gate-fold.js";
import { runGateLanes } from "./gate-lanes.js";
export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-reads.js";
export { cloneObserverInput } from "./runner/gate-exit.js";
import { brandPolicyAskClass } from "./ask-class.js";
import { isAskOrigin, screenAskClassMembers } from "./ask-origin.js";
import { inlineUntrusted } from "./untrusted-text.js";
export { formatHookFeedback } from "./reminder-mint.js";
import { createSafeNotifier } from "./safe-notify.js";
import { wireOf } from "./tool-name-readings.js";
export const DEFAULT_HOOK_TIMEOUT_MS = 600_000;
export const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
const POST_ABORT_GRACE_MS = 1_000;
export function postAbortGraceMs(remainingMs) {
return Math.max(0, Math.min(POST_ABORT_GRACE_MS, remainingMs));
}
const badHookTimeoutReported = new WeakSet();
export function resolveHookTimeoutMs(supplied, report, owner) {
if (supplied === undefined)
return DEFAULT_HOOK_TIMEOUT_MS;
if (Number.isFinite(supplied) && supplied >= 0 && supplied <= MAX_HOOK_TIMEOUT_MS)
return supplied;
if (report !== undefined && !(owner !== undefined && badHookTimeoutReported.has(owner))) {
if (owner !== undefined)
badHookTimeoutReported.add(owner);
try {
report(new Error(`Hooks.timeoutMs must be a non-negative finite number no greater than ${MAX_HOOK_TIMEOUT_MS} (got ${String(supplied)}) — ` +
`every hook seat falls back to the ${DEFAULT_HOOK_TIMEOUT_MS}ms default`));
}
catch {
}
}
return DEFAULT_HOOK_TIMEOUT_MS;
}
export async function runHookSeat(seat, bound, call) {
void seat;
const timeoutMs = resolveHookTimeoutMs(bound.timeoutMs, bound.onBadTimeout, bound.owner);
const taskSignal = bound.signal;
const controller = new AbortController();
const pending = Promise.resolve(call(controller.signal));
const startedAt = performance.now();
return await new Promise((resolve, reject) => {
let settled = false;
let listening = false;
let timer;
const clearTimer = () => {
if (timer !== undefined)
clearTimeout(timer);
timer = undefined;
};
const finish = (act) => {
if (settled)
return;
settled = true;
clearTimer();
if (listening)
taskSignal?.removeEventListener("abort", onAbort);
act();
};
const expire = (cause) => finish(() => {
void pending.then(undefined, () => {
});
resolve({ expired: true, cause });
queueMicrotask(() => {
try {
controller.abort();
}
catch {
}
});
});
const collapseToGrace = () => {
if (settled)
return;
clearTimer();
timer = setTimeout(() => expire("aborted"), postAbortGraceMs(timeoutMs - (performance.now() - startedAt)));
};
const onAbort = () => (bound.abortEnds === true ? expire("aborted") : collapseToGrace());
timer = setTimeout(() => expire("timeout"), timeoutMs);
if (taskSignal !== undefined) {
if (taskSignal.aborted) {
onAbort();
}
else {
taskSignal.addEventListener("abort", onAbort, { once: true });
listening = true;
}
}
pending.then((value) => finish(() => resolve({ expired: false, value })), (err) => finish(() => reject(asThrownError(err))));
});
}
export function hookSeatExpiredError(seat, timeoutMs, cause, consequence) {
return new Error(cause === "timeout"
? `the deployment's ${seat} hook did not answer within its ${timeoutMs}ms bound (Hooks.timeoutMs) — ${consequence}`
: `the deployment's ${seat} hook had not answered when the task was cancelled — ${consequence}`);
}
const reflectApply = Reflect.apply;
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const getPrototypeOf = Object.getPrototypeOf;
const objectPrototype = Object.prototype;
function findEnvSlot(env, key) {
for (let o = env; o !== null && o !== objectPrototype; o = getPrototypeOf(o)) {
const desc = getOwnPropertyDescriptor(o, key);
if (desc !== undefined)
return { holder: o, desc };
}
return undefined;
}
function readEnvPrimitive(env, key) {
const slot = findEnvSlot(env, key);
if (slot === undefined)
return undefined;
return slot.desc.get !== undefined ? reflectApply(slot.desc.get, env, []) : slot.desc.value;
}
function createCwdReader(env) {
const slot = findEnvSlot(env, "cwd");
if (slot === undefined)
return undefined;
const getter = slot.desc.get;
const initial = getter !== undefined ? reflectApply(getter, env, []) : slot.desc.value;
if (typeof initial !== "string")
return undefined;
let last = initial;
return () => {
const desc = getOwnPropertyDescriptor(slot.holder, "cwd");
const value = desc === undefined
? undefined
: desc.get !== undefined
? desc.get === getter
? reflectApply(desc.get, env, [])
: undefined
: desc.value;
if (typeof value === "string")
last = value;
return last;
};
}
export function createHookEnvCapabilities(env) {
const face = Object.create(null);
const canonicalPath = readEnvPrimitive(env, "canonicalPath");
const exists = readEnvPrimitive(env, "exists");
const readLink = readEnvPrimitive(env, "readLink");
const cwd = createCwdReader(env);
if (typeof canonicalPath === "function") {
face.canonicalPath = (path, abortSignal) => reflectApply(canonicalPath, env, [path, abortSignal]);
}
if (typeof exists === "function") {
face.exists = (path, abortSignal) => reflectApply(exists, env, [path, abortSignal]);
}
if (typeof readLink === "function") {
face.readLink = (path, abortSignal) => reflectApply(readLink, env, [path, abortSignal]);
}
if (cwd !== undefined) {
face.cwd = cwd;
}
return Object.freeze(face);
}
const IDENTITY_AGENT_NAME_MAX = 80;
export function mintHookInvocationIdentity(facts) {
return Object.freeze(Object.assign(Object.create(null), {
sessionId: facts.sessionId,
taskId: facts.taskId,
legKind: facts.legKind,
isDelegatedChild: facts.isDelegatedChild,
...(facts.insideFork === true ? { insideFork: true } : {}),
...(facts.agentName !== undefined ? { agentName: inlineUntrusted(facts.agentName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
...(facts.spawnName !== undefined ? { spawnName: inlineUntrusted(facts.spawnName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
...(facts.agentType !== undefined ? { agentType: inlineUntrusted(facts.agentType.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
...(facts.parentToolCallId !== undefined ? { parentToolCallId: facts.parentToolCallId } : {}),
}));
}
function preToolUseCrashReason(subject, err) {
const cause = inlineUntrusted(describeThrown(err), 200);
return (`a PreToolUse hook crashed while screening ${subject}; the call was NOT executed (fail-closed). ` +
`This is a failure of the deployment's hook, NOT of the tool itself — an identical retry reaches the ` +
`same hook and fails the same way. Hook error: ${cause || "(no message)"}`);
}
function preToolUseUnansweredReason(subject, timeoutMs, cause) {
return cause === "timeout"
? `a PreToolUse hook did not answer within its ${timeoutMs}ms bound while screening ${subject}; the call was ` +
`NOT executed (fail-closed). This is a failure of the deployment's hook, NOT of the tool itself — the ` +
`screening face never returned a verdict, so nothing was decided about this call either way.`
: `the task was cancelled while a PreToolUse hook was still screening ${subject}; the call was NOT executed ` +
`(fail-closed). No verdict was reached — this is the run ending, not the tool failing.`;
}
function screenPreToolUseResult(r) {
if (r === undefined)
return undefined;
return receiveHookResult(r);
}
export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash, timeoutMs) {
return brandPolicyAskClass({
check: async (req, signal) => {
if (signal?.aborted)
return abortedSeatDeny(req, `the inherited PreToolUse screening of "${req.toolName}" was not consulted: the run or turn was already interrupted`, "hook");
let r;
const boundMs = resolveHookTimeoutMs(timeoutMs, onCrash);
try {
const seat = await runHookSeat("preToolUse", { timeoutMs: boundMs }, (sig) => preToolUse(req.toolName, req.args, { toolCallId: req.toolCallId, toolName: req.toolName, signal: sig, ...(env !== undefined ? { env } : {}) }));
if (seat.expired) {
try {
onCrash?.(hookSeatExpiredError("preToolUse", boundMs, seat.cause, `the inherited screening of "${req.toolName}" was refused fail-closed`));
}
catch {
}
return { action: "deny", message: preToolUseUnansweredReason(`this call to "${req.toolName}"`, boundMs, seat.cause), decisionReason: "hook" };
}
r = screenPreToolUseResult(seat.value);
}
catch (err) {
try {
onCrash?.(err);
}
catch {
}
return { action: "deny", message: preToolUseCrashReason(`this call to "${req.toolName}"`, err), decisionReason: "hook" };
}
if (r === undefined)
return { action: "allow" };
if (r.action === "deny") {
return { action: "deny", message: decisionText(r) ?? `tool "${req.toolName}" blocked by a PreToolUse hook`, decisionReason: "hook" };
}
if (r.action === "ask") {
return {
action: "ask",
message: decisionText(r) ?? `approval required for "${req.toolName}" (inherited PreToolUse hook)`,
decisionReason: "hook",
...(r.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
...(r.updatedInput !== undefined ? { updatedInput: r.updatedInput } : {}),
};
}
const stated = r.action === "allow" ? { decisionReason: "hook" } : {};
return r.updatedInput !== undefined ? { action: "allow", updatedInput: r.updatedInput, ...stated } : { action: "allow", ...stated };
},
}, "hook");
}
export function askCarryRowMembers(carry) {
if (carry === undefined)
return {};
const origin = carry.origin;
const classed = screenAskClassMembers(carry.askClass, carry.classes);
return {
...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
...(isAskOrigin(origin) ? { origin } : {}),
...(classed !== undefined ? classed : {}),
};
}
export function createPreToolUseScreening(preToolUse, input, hookSeatMs, notifier) {
if (preToolUse === undefined)
return undefined;
const { toolCallId, toolName } = input.event;
const screenedName = wireOf("gate.preToolUse", input.event);
const hookCtx = (seatSignal) => ({
toolCallId,
toolName: screenedName,
...(seatSignal !== undefined ? { signal: seatSignal } : {}),
...(() => {
const c = input.trackedCwd?.();
return typeof c === "string" ? { cwd: c } : {};
})(),
...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}),
...(input.identity !== undefined ? { identity: input.identity } : {}),
});
const bound = { timeoutMs: hookSeatMs, ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), abortEnds: true };
return {
screen: (args) => runHookSeat("preToolUse", bound, (sig) => preToolUse(screenedName, args, hookCtx(sig))),
screened: screenPreToolUseResult,
crashReason: preToolUseCrashReason,
unansweredReason: async (subject, cause) => {
if (cause === "timeout")
traceHookCrash(input, hookSeatExpiredError("preToolUse", hookSeatMs, cause, `${subject} was NOT executed (fail-closed) and the screening face was signalled to stop`), notifier);
return preToolUseUnansweredReason(subject, hookSeatMs, cause);
},
deniedReason: (answer) => decisionText(answer) ?? `tool "${toolName}" blocked by a PreToolUse hook`,
refusedReason: (answer) => String(answer.message ?? `the PreToolUse hook's result for "${toolName}" was refused by the engine`),
};
}
export async function runToolGate(input) {
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
const { toolCallId, toolName } = event;
const callSignal = input.callSignal;
const modelInput = event.input;
const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
const observerBound = { timeoutMs: hookSeatMs, ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}) };
const notifyPermissionDeniedSeat = async (payload) => {
if (input.permissionDenied === undefined)
return;
await notifier.notifyAsync(async () => {
const seat = await runHookSeat("permissionDenied", observerBound, (sig) => input.permissionDenied?.({ ...payload, signal: sig }));
if (seat.expired)
throw hookSeatExpiredError("permissionDenied", hookSeatMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged");
}, "toolGate.permissionDenied");
};
const screening = createPreToolUseScreening(preToolUse, input, hookSeatMs, notifier);
const pass = {
input,
event,
adjudicate,
resolveAsk,
suspendAsk,
toolName,
toolCallId,
callSignal,
notifier,
ledger: createSettlementLedger({ toolCallId, toolName }),
notifyPermissionDenied: notifyPermissionDeniedSeat,
screening,
afterFold: runGateLanes,
currentInput: modelInput,
req: { toolName, args: modelInput, toolCallId, ...(() => { const f = input.toolFaceOf?.(toolName); return f !== undefined ? { face: f } : {}; })() },
preToolContext: [],
hookAsk: undefined,
parkFailed: undefined,
deniedBy: undefined,
denyCause: undefined,
decision: { action: "allow" },
tightenedBy: undefined,
policyRewrite: undefined,
};
if (screening !== undefined) {
let r;
let unanswered;
try {
const seat = await screening.screen(pass.currentInput);
if (seat.expired)
unanswered = seat.cause;
else
r = screening.screened(seat.value);
}
catch (err) {
const reason = screening.crashReason(`this call to "${toolName}"`, err);
traceHookCrash(input, err, notifier);
return await engineFailClosedExit(pass, reason);
}
if (unanswered !== undefined) {
const reason = await screening.unansweredReason(`this call to "${toolName}"`, unanswered);
return await engineFailClosedExit(pass, reason);
}
if (r) {
if (isEngineRefusal(r)) {
return await engineFailClosedExit(pass, screening.refusedReason(r));
}
if (r.additionalContext) {
pass.preToolContext.push(r.additionalContext);
}
if (r.action === "deny") {
return hookDenyExit(pass, screening.deniedReason(r));
}
if (r.updatedInput !== undefined) {
pass.currentInput = r.updatedInput;
}
if (r.action === "ask") {
pass.hookAsk = r;
}
if (r.action === "allow")
pass.hookAllowed = true;
}
}
return await runGateFold(pass);
}