UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

81 lines (80 loc) • 4.52 kB
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); }