@sema-agent/core
Version:
Stateless, task-oriented AI agent core
261 lines (260 loc) • 14.8 kB
JavaScript
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;
}
}
}