@sema-agent/core
Version:
Stateless, task-oriented AI agent core
81 lines (80 loc) • 4.52 kB
JavaScript
import { refusalOf } from "./tool-policy.js";
import { allowRuleShadowOf } from "./allow-entry.js";
import { inlineUntrusted } from "./untrusted-text.js";
import { failedHandlerClause } from "./mods/stations.js";
const STANDS = Object.freeze({ kind: "stands" });
function readToolCheck(heard, standing, call, mayClear) {
const { toolName } = call;
const refused = (message) => ({ kind: "denied", deny: refusalOf({ action: "deny", message, decisionReason: "hook" }, call.judged) });
if (heard.kind === "aborted")
return refused(`the task was cancelled while a tool.check handler was still checking this call to "${toolName}"; the call was NOT executed (fail-closed)`);
if (heard.kind === "failed")
return refused(`a tool.check handler ${failedHandlerClause(heard)} while checking this call to "${toolName}"; the call was NOT executed (fail-closed)`);
if (heard.by === undefined && heard.decision === standing.action)
return STANDS;
const { who, why } = speakerOf(heard);
const rewritten = call.rewrite !== undefined ? { updatedInput: call.rewrite } : {};
if (heard.decision === "deny")
return refused(`tool "${toolName}" denied by ${who}${why}`);
if (heard.decision === "ask") {
return standing.action === "ask"
? { kind: "asked", ask: { ...standing, decisionReason: "hook", ...(heard.reason !== undefined ? { message: heard.reason } : {}) }, minted: false }
: { kind: "asked", ask: { ...questionOf(heard, toolName), ...rewritten }, minted: true };
}
if (standing.action === "allow")
return STANDS;
if (mayClear?.() === true)
return { kind: "cleared", allow: { action: "allow", message: `${who} allowed this call`, decisionReason: "hook", ...rewritten } };
return { kind: "shadowed", shadows: [allowRuleShadowOf("hook", heard.by !== undefined ? [heard.by] : [])] };
}
function speakerOf(heard) {
return { who: heard.by !== undefined ? `extension "${inlineUntrusted(heard.by, 80)}"` : "an extension", why: heard.reason !== undefined ? `: ${inlineUntrusted(heard.reason, 500)}` : "" };
}
function questionOf(heard, toolName) {
const { who, why } = speakerOf(heard);
return { action: "ask", message: `tool "${toolName}" needs approval (asked by ${who}${why})`, decisionReason: "hook" };
}
export async function readToolChecks(chains, standing, call, project) {
let raised = false;
let current = standing;
let moved;
const shadows = [];
for (const chain of chains) {
const heard = await chain.raise(project(current, chain.mayClear));
if (heard === undefined)
continue;
raised = true;
const reading = readToolCheck(heard, current, call, chain.mayClear);
if (reading.kind === "denied")
return reading;
if (reading.kind === "asked" && chain.settle !== undefined && heard.kind === "answered") {
const settled = await chain.settle(questionOf(heard, call.toolName), call.request, call.signal);
if (settled.action === "deny")
return { kind: "denied", deny: refusalOf(settled, call.judged) };
if (settled.action === "allow") {
if (settled.updatedInput !== undefined)
return { kind: "approved", allow: { ...settled, updatedInput: settled.updatedInput }, ...(moved?.kind === "asked" ? { outstanding: moved.ask } : {}) };
continue;
}
const ask = current.action === "ask" ? current : settled;
moved = { kind: "asked", ask, minted: current.action !== "ask" || (moved?.kind === "asked" && moved.minted), marked: true };
current = ask;
continue;
}
if (reading.kind === "shadowed")
shadows.push(...reading.shadows);
if (reading.kind === "asked") {
moved = { ...reading, minted: reading.minted || (moved?.kind === "asked" && moved.minted), ...(moved?.kind === "asked" && moved.marked === true ? { marked: true } : {}) };
current = reading.ask;
}
if (reading.kind === "cleared") {
moved = reading;
current = reading.allow;
}
}
if (!raised)
return undefined;
if (moved?.kind === "asked" && shadows.length > 0)
return { ...moved, ask: { ...moved.ask, allowRuleShadowed: [...(moved.ask.allowRuleShadowed ?? []), ...shadows] } };
return moved ?? (shadows.length > 0 ? { kind: "shadowed", shadows } : STANDS);
}