UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

261 lines (260 loc) • 14.8 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 { traceHookCrash } from "./gate-crash-trace.js"; import { writeProtectionMandated, writeProtectionTightenDecision } from "./write-protect.js"; import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js"; import { shellReadFaceWords } from "../tools/fs/shell-read-face.js"; import { failedJudgmentDeny } from "./runner/policy-throw.js"; import { thrownText } from "./thrown-value.js"; import { argsAsTheToolReads } from "./effective-path-target.js"; import { foldHookAsk, refuseOutOfContractDecision } from "./tool-policy.js"; import { applyUnansweredSilence, readStationAnswer, silencedBy } from "./shell-read-silence.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 = Object.freeze({ toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) }); let unread; try { const stamped = input.stampCallTarget?.(pass.req, callSignal ?? input.abortSignal); if (stamped !== undefined) pass.req = Object.freeze(await stamped); } catch (error) { unread = { error }; } pass.decision = unread !== undefined ? failedJudgmentDeny(unread.error, callSignal ?? input.abortSignal, (e) => traceHookCrash(input, e, notifier), "the call's target reading", { toolName, toolCallId }) : adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" }; { const read = ledger.fromDecision(pass.decision); pass.decision = read.decision; if (read.deniedBy !== undefined) pass.deniedBy = read.deniedBy; if (read.deniedBy !== 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 = argsAsTheToolReads(pass.req, pass.decision.updatedInput); } else if (pass.hookAsk?.updatedInput !== undefined) { pass.policyRewrite = argsAsTheToolReads(pass.req, pass.hookAsk.updatedInput); } if (pass.hookAsk !== undefined && pass.decision.action !== "deny") { pass.decision = foldHookAsk(pass.decision, pass.hookAsk); } 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 check = input.writeProtectionCheck; const judgedArgs = pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput; const verdict = await awaitWriteProtectionJudgment((deadline) => check(toolName, judgedArgs, pass.req.face, input.trackedCwd?.(), deadline), 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; const decision = verdict === undefined ? undefined : writeProtectionTightenDecision(toolName, verdict); if (decision !== undefined && (writeProtectionStanding === "allow" || decision.action === "deny")) { 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, verdict) { const { verb, participle } = shellReadFaceWords(verdict.face); if (verdict.finding !== "outside_roots") return `tool "${toolName}" ${verb} where the session's read boundary could not establish what is ${participle} — explicit approval required`; return `tool "${toolName}" ${verb} outside the directories allowed for this session — explicit approval required`; } function applyShellReadVerdict(pass, answer, toolName, shellGated) { const reading = readStationAnswer(answer, { toolName, toolCallId: pass.toolCallId, report: (e) => traceHookCrash(pass.input, e, pass.notifier) }); const verdict = reading.verdict; 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.targetUnresolved === true) pass.targetUnresolved = true; if (verdict.unplacedOptions !== undefined) pass.shellUnplacedOptions = verdict.unplacedOptions; if (reading.kind === "silence" || verdict.finding === "unestablishable" || verdict.finding === "declined" || verdict.unestablished === true) pass.shellReadUnestablished = true; if (reading.kind === "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; } if (reading.kind === "silence") { applyUnansweredSilence(pass, verdict, { message: shellReadAskMessage(toolName, verdict), account, tightenedBy: shellGated ? "shell_gate" : "safety" }); return; } switch (reading.kind) { case "vouch": 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, ...silencedBy(verdict, pass.decision) }; return; } const candidate = normalizeReadRootCandidate(verdict.readRootCandidate); pass.decision = { action: "ask", message: shellReadAskMessage(toolName, verdict), decisionReason: "safety", probeMandated: true, ...silencedBy(verdict, {}), ...(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 === "ask") pass.decision = { ...pass.decision, ...silencedBy(verdict, pass.decision) }; if (pass.decision.action === "allow") { const candidate = normalizeReadRootCandidate(verdict.readRootCandidate); pass.decision = { action: "ask", message: shellReadAskMessage(toolName, verdict), decisionReason: "safety", ...silencedBy(verdict, {}), ...(account !== undefined ? { probeReason: account } : {}), ...(candidate !== undefined ? { probeReadRootCandidate: candidate } : {}), }; if (candidate !== undefined) pass.shellReadOffer = true; pass.tightenedBy = shellGated ? "shell_gate" : "safety"; } if (verdict.finding === "declined" || verdict.finding === "unestablishable") pass.probeBoundaryDeclined = true; return; } default: { const exhaustive = reading.kind; return exhaustive; } } }