@sema-agent/core
Version:
Stateless, task-oriented AI agent core
141 lines (140 loc) • 8.01 kB
JavaScript
import { cardDisclosureOf } from "../allow-entry.js";
import { primaryActivityArg } from "../arg-summary.js";
import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
import { advertisedWritableRootsSeat, isLiveApproverSeat, resolveAsk } from "../tool-policy.js";
import { composeCallSignal, raceAbort } from "./abort-race.js";
import { askMandateMember } from "../ask-origin.js";
import { gateAskCarry, lateAskSettlementObserver } from "./denial-limit-arms.js";
import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
import { settleReadRootGrant } from "./read-root-grant.js";
import { asThrownError } from "../thrown-value.js";
function createDenyObserverNotifier(hooks, hookTimeoutMs, signal, report) {
if (hooks?.permissionDenied === undefined)
return undefined;
return async (payload) => {
try {
const seat = await runHookSeat("permissionDenied", { timeoutMs: hookTimeoutMs, signal }, (sig) => hooks.permissionDenied({ ...payload, signal: sig }));
if (seat.expired)
report(hookSeatExpiredError("permissionDenied", hookTimeoutMs, seat.cause, "the deny observation was abandoned; the deny itself is unchanged"));
}
catch (err) {
report(err);
}
};
}
const sanitizePreview = (node, depth = 0) => {
if (depth > 6)
return undefined;
if (typeof node === "string") {
return node.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "\u2400");
}
if (node === null || typeof node !== "object")
return node;
if (Array.isArray(node))
return node.map((v) => sanitizePreview(v, depth + 1));
const out = {};
for (const [k, v] of Object.entries(node)) {
out[sanitizePreview(k, depth + 1)] = sanitizePreview(v, depth + 1);
}
return out;
};
function resolveApprovalPreview(tools, toolName, args) {
const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
if (t?.approvalPreview === undefined)
return {};
try {
const raw = t.approvalPreview(args);
if (raw === undefined)
return {};
const bytes = JSON.stringify(raw);
if (bytes === undefined)
return { withheld: "unavailable" };
if (bytes.length > 16_384) {
return { preview: { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` }, withheld: "oversize" };
}
return { preview: sanitizePreview(raw) };
}
catch {
return { withheld: "unavailable" };
}
}
export function prepareAskLane(input) {
const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
if (!gateMachineryActive)
return { askLane: undefined };
const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
const adjudicate = effectivePolicy
? (req, callSignal) => {
const signal = composedCallSignal(callSignal);
return raceAbort(Promise.resolve(effectivePolicy.check({ ...req, budget: budgetSnapshot, ...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}), ...advertisedWritableRootsSeat(advertisedWritableDirs) }, signal)), signal, () => ({
action: "deny",
message: "policy check aborted (task timed out or cancelled)",
}));
}
: undefined;
const approvalPreviewOf = (toolName, args) => resolveApprovalPreview(tools, toolName, args);
const resolveAskBound = async (decision, req, callSignal) => {
if (inheritedUnavailableAsks.delete(req.toolCallId)) {
return {
action: "deny",
message: `approval for "${req.toolName}" requires an ancestor task's approval that cannot be resolved here ` +
`(no live approver reachable, or a durable-park mandate applies), and no durable approval park is ` +
`available — denied fail-closed (the inherited constraint stands).`,
decisionReason: "mode",
settlement: { kind: "approver_unavailable", who: { party: "none" }, when: now() },
approverUnavailable: true,
};
}
const grantReuse = consumeInheritedAskGrant(inheritedAskGrants, onAsk, humanReviewRef, decision, req);
if (grantReuse !== undefined)
return grantReuse;
const t0 = now();
const resolved = await resolveAsk({
toolName: req.toolName,
toolCallId: req.toolCallId,
args: req.args,
...(() => {
const p = approvalPreviewOf(req.toolName, req.args);
return { ...(p.preview !== undefined ? { preview: p.preview } : {}), ...(p.withheld !== undefined ? { previewWithheld: p.withheld } : {}) };
})(),
...ruleOffersOf(req, req.args, decision.action === "ask" ? decision : undefined),
...cardDisclosureOf(decision, `approval required for "${req.toolName}"`),
...askSourceIdentity(),
...riskAxesOf(req),
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
...(decision.action === "ask" ? askMandateMember(decision) : {}),
...(decision.action === "ask" ? gateAskCarry(decision, isLiveApproverSeat(onAsk), autoModeDenialTracking.current) : {}),
...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
...(decision.action === "ask" && decision.probeReadRootCandidate !== undefined ? { readRootCandidate: decision.probeReadRootCandidate } : {}),
...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
}, onAsk, composedCallSignal(callSignal), lateAskSettlementObserver({ toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
const waitMs = Math.max(0, now() - t0);
const granting = settleReadRootGrant(resolved.readRootGrant, grantReadRoot, { toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, signal: composedCallSignal(callSignal) });
if (granting !== undefined)
await granting;
if (resolved.approverUnavailable !== true && resolved.settlement?.kind !== "task_aborted") {
humanReviewRef.count += 1;
humanReviewRef.totalWaitMs += waitMs;
const toolArg = primaryActivityArg(req.args);
humanReviewRef.gates.push({
kind: "human",
waitMs,
decision: resolved.action === "allow" ? "allow" : "deny",
toolName: req.toolName,
toolCallId: req.toolCallId,
...(toolArg !== undefined ? { toolArg } : {}),
});
}
return resolved;
};
const notifyPermissionDenied = createDenyObserverNotifier(hooks, hookTimeoutMs, abortController.signal, notifyOwnHookCrash);
const notifyHookError = (err) => {
try {
deps.onError?.(asThrownError(err), { phase: "hook", sessionId });
}
catch {
}
};
return { askLane: { composedCallSignal, adjudicate, approvalPreviewOf, resolveAskBound, notifyPermissionDenied, notifyHookError } };
}