UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

255 lines (254 loc) • 14.2 kB
import { inlineUntrusted } from "./untrusted-text.js"; import { markTightenMessage } from "./safety-axis-vocab.js"; import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js"; import { normalizeReadRootCandidate } from "./read-root-candidate.js"; import {} from "./runner/gate-exit.js"; import { traceHookCrash } from "./gate-crash-trace.js"; import { writeProtectionMandated, writeProtectionTightenDecision, writeProtectionUnresolved } from "./write-protect.js"; import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js"; import { thrownText } from "./thrown-value.js"; import { refuseOutOfContractDecision } from "./tool-policy.js"; import { asksUnstated, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js"; export async function runGateFold(pass) { const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass; const boundedAwaitDeadlineMs = createGateDeadline(input, notifier); pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) }; pass.decision = adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" }; { const refusedBy = ledger.fromDecision(pass.decision); if (refusedBy !== undefined) pass.deniedBy = refusedBy; if (refusedBy !== undefined) pass.denyCause = ledger.causeOf(pass.decision); const attributed = ledger.classifierRoundOf(pass.decision); if (attributed !== undefined) pass.classifierRound = attributed; } if ((pass.decision.action === "allow" || pass.decision.action === "ask") && pass.decision.updatedInput !== undefined) { pass.policyRewrite = pass.decision.updatedInput; } else if (pass.hookAsk?.updatedInput !== undefined) { pass.policyRewrite = pass.hookAsk.updatedInput; } if (pass.decision.action === "allow" && pass.hookAsk) { pass.decision = { action: "ask", ...(pass.hookAsk.message !== undefined ? { message: pass.hookAsk.message } : {}), decisionReason: "hook", ...(pass.hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}), ...(pass.hookAsk.updatedInput !== undefined ? { updatedInput: pass.hookAsk.updatedInput } : {}), }; } else if (pass.decision.action === "ask" && pass.hookAsk) { const hookMessage = pass.hookAsk.message; const foldedMessage = hookMessage !== undefined && hookMessage !== pass.decision.message ? `${pass.decision.message !== undefined ? `${pass.decision.message} ` : ""}(a PreToolUse hook also asks: ${hookMessage})` : pass.decision.message; pass.decision = { ...pass.decision, decisionReason: "hook", ...(foldedMessage !== undefined ? { message: foldedMessage } : {}), ...(pass.hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}), }; } if ((pass.decision.action === "allow" || pass.decision.action === "ask") && input.shellReadCheck !== undefined) { const station = input.shellReadCheck; const judged = station.judge(pass.req, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal, { toolCallId }); if (judged !== null) { let verdict; try { verdict = await withGateTimeout(judged, boundedAwaitDeadlineMs(), pass.callSignal ?? input.abortSignal, "shellReadStation"); } catch (err) { const kind = gateWaitFailureOf(err); if (kind !== "aborted" && input.abortSignal?.aborted !== true) { traceHookCrash(input, new Error(`shell read station ${kind === "timeout" ? "timed out" : "threw"} for tool "${toolName}" (toolCallId ${toolCallId}) — where this call reads could not be established: ${thrownText(err)}`), notifier); } verdict = station.unanswered(pass.req, kind === "timeout" ? "the read station timed out" : "the read station could not answer"); } applyShellReadVerdict(pass, verdict, toolName, input.shellGated === true); } } if (input.requiresUserInteraction === true && pass.decision.action === "allow") { pass.decision = { action: "ask", message: markTightenMessage(toolName, "interaction"), decisionReason: "safety" }; pass.tightenedBy = "safety"; } if (input.egress && pass.decision.action === "allow") { pass.decision = { action: "ask", message: markTightenMessage(toolName, "egress"), decisionReason: "safety", }; pass.tightenedBy = "safety"; } const alreadyAsking = pass.decision.action === "ask"; if ((pass.decision.action === "allow" || alreadyAsking) && (input.irreversibility === "always" || input.irreversibility === "maybe")) { let tighten = !alreadyAsking; const withdrawStationOffer = () => { if (alreadyAsking && pass.shellReadOffer === true && pass.decision.action === "ask" && pass.decision.probeReadRootCandidate !== undefined) { const { probeReadRootCandidate: _withdrawn, ...kept } = pass.decision; pass.decision = kept; } }; if (input.irreversibility === "always" || !input.reversibilityProbe) withdrawStationOffer(); let probeReason; let probeCause; let probeMandated; let probeReadRootCandidate; if (input.irreversibility === "maybe" && input.reversibilityProbe) { let reversible = false; const probeArgs = pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput; const probeTimeoutMs = boundedAwaitDeadlineMs(); try { const verdict = await withGateTimeout(Promise.resolve(input.reversibilityProbe(probeArgs)), probeTimeoutMs, input.abortSignal); reversible = verdict?.reversible === true; if (!reversible) { const raw = verdict?.reason; if (typeof raw === "string" && raw.trim().length > 0) probeReason = inlineUntrusted(raw, PROBE_REASON_MAX); probeCause = normalizeProbeCause(verdict?.cause); if (verdict?.mandated === true) probeMandated = true; if (verdict?.boundaryDeclined === true) pass.probeBoundaryDeclined = true; probeReadRootCandidate = normalizeReadRootCandidate(verdict?.readRootCandidate); withdrawStationOffer(); } } catch (err) { reversible = false; pass.probeUnanswered = true; withdrawStationOffer(); if (input.abortSignal?.aborted !== true) traceHookCrash(input, err, notifier); } tighten = !alreadyAsking && !reversible; } if (tighten) { pass.decision = { action: "ask", message: markTightenMessage(toolName, "irreversible"), decisionReason: "safety", ...(probeReason !== undefined ? { probeReason } : {}), ...(probeCause !== undefined ? { probeCause } : {}), ...(probeMandated === true ? { probeMandated: true } : {}), ...(probeReadRootCandidate !== undefined ? { probeReadRootCandidate } : {}), }; pass.tightenedBy = input.shellGated === true ? "shell_gate" : "safety"; } else if (alreadyAsking && probeMandated === true && pass.decision.action === "ask") { pass.decision = { ...pass.decision, probeMandated: true }; } } if (input.peerMessage === true && pass.decision.action === "allow" && input.autoMode !== undefined) { pass.decision = { action: "ask", message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`, decisionReason: "safety", }; pass.tightenedBy = "safety"; } const writeProtectionStanding = pass.decision.action; if ((writeProtectionStanding === "allow" || writeProtectionStanding === "ask") && input.writeProtectionCheck !== undefined) { const judged = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal); const verdict = judged === null ? undefined : await awaitWriteProtectionJudgment(judged, boundedAwaitDeadlineMs(), pass.callSignal ?? input.abortSignal, (err) => { const kind = gateWaitFailureOf(err); if (kind === "aborted" || input.abortSignal?.aborted === true) return; traceHookCrash(input, new Error(`write-protection judge ${kind === "timeout" ? "timed out" : "threw"} for tool "${toolName}" (toolCallId ${toolCallId}) — this call's write target could not be judged: ${thrownText(err)}`), notifier); }); if (verdict !== undefined && writeProtectionMandated(verdict)) pass.writeProtected = true; if (verdict !== undefined && writeProtectionUnresolved(verdict)) pass.writeProtectionUnresolved = true; const decision = verdict === undefined ? undefined : writeProtectionTightenDecision(toolName, verdict); if (decision !== undefined && writeProtectionStanding === "allow") { pass.decision = decision; if (decision.action === "ask") pass.tightenedBy = "safety"; else pass.deniedBy = "write_protection"; } } if (asksUnstated(pass.decision, permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.seat, input.permissionMode?.())) { pass.decision = { action: "ask", message: permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.question ?? unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) }; pass.tightenedBy = "unstated"; } return await pass.afterFold(pass); } function shellReadAskMessage(toolName, finding) { return finding === "outside_roots" ? `tool "${toolName}" reads outside the directories allowed for this session — explicit approval required` : `tool "${toolName}" reads where the session's read boundary could not establish what is read — explicit approval required`; } function applyShellReadVerdict(pass, verdict, toolName, shellGated) { const account = verdict.sentence !== undefined && verdict.sentence.trim().length > 0 ? inlineUntrusted(verdict.sentence, PROBE_REASON_MAX) : undefined; if (verdict.writesPlaced === true) pass.shellWritesPlaced = true; if (verdict.writeProtected === true) pass.writeProtected = true; if (verdict.unplacedOptions !== undefined) pass.shellUnplacedOptions = verdict.unplacedOptions; if (verdict.finding === "unestablishable" || verdict.finding === "declined" || verdict.unestablished === true) pass.shellReadUnestablished = true; switch (verdict.disposition) { case "vouch": return; case "refuse": pass.decision = { action: "deny", message: verdict.sentence ?? `tool "${toolName}" reads what the session's read boundary refuses` }; pass.deniedBy = "read_boundary"; if (verdict.details !== undefined) pass.denyDetails = verdict.details; return; case "mandated": { if (verdict.details !== undefined) pass.shellReadDetails = verdict.details; if (pass.decision.action !== "allow") { if (pass.decision.action === "ask") pass.decision = { ...pass.decision, probeMandated: true }; return; } const candidate = normalizeReadRootCandidate(verdict.readRootCandidate); pass.decision = { action: "ask", message: shellReadAskMessage(toolName, verdict.finding), decisionReason: "safety", probeMandated: true, ...(account !== undefined ? { probeReason: account } : {}), ...(candidate !== undefined ? { probeReadRootCandidate: candidate } : {}), }; if (candidate !== undefined) pass.shellReadOffer = true; pass.tightenedBy = shellGated ? "shell_gate" : "safety"; return; } case "ordinary": { verdict.admit?.(); if (verdict.details !== undefined) pass.shellReadDetails = verdict.details; if (pass.decision.action === "allow") { const candidate = normalizeReadRootCandidate(verdict.readRootCandidate); pass.decision = { action: "ask", message: shellReadAskMessage(toolName, verdict.finding), decisionReason: "safety", ...(account !== undefined ? { probeReason: account } : {}), ...(candidate !== undefined ? { probeReadRootCandidate: candidate } : {}), }; if (candidate !== undefined) pass.shellReadOffer = true; pass.tightenedBy = shellGated ? "shell_gate" : "safety"; } if (verdict.finding === "declined") pass.probeBoundaryDeclined = true; return; } default: { const exhaustive = verdict.disposition; return exhaustive; } } }