UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

141 lines (140 loc) • 8.01 kB
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 } }; }