UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

168 lines (167 loc) • 9.97 kB
import { markTightenMessage } from "../safety-axis-vocab.js"; import { classifierParseFailureDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js"; import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js"; import { classifierUnavailableArm } from "../classifier-unavailable-arm.js"; import { attestClassifierDenyCause, attestClassifierRound, carryEngineAttestations } from "../tool-policy.js"; import { SETTLEMENT_NOBODY_JUDGED } from "../gate-outcome.js"; import { deliverEngineNotice } from "../types.js"; import { inlineUntrusted } from "../untrusted-text.js"; import { askClassMembersOf, askOriginOf, classifyAsk } from "../ask-origin.js"; export function attachRebuiltDenialTrackers(entries, denialLimit) { if (entries === undefined) return undefined; const needsTracker = (pc) => pc.autoMode !== undefined && pc.autoMode.denialTracking === undefined; const boundsFor = new Map(); const liveTrackers = new Map(); for (const pc of entries) { if (pc.autoMode?.denialTracking !== undefined) liveTrackers.set(pc.autoMode.decider, pc.autoMode.denialTracking); } for (const pc of entries) { if (!needsTracker(pc) || liveTrackers.has(pc.autoMode.decider)) continue; const recorded = sanitizeAutoModeArmingRecipe(pc.autoMode.arming); if (recorded === undefined) continue; const merged = tightenDenialLimit(recorded.denialLimit, denialLimit); const seen = boundsFor.get(pc.autoMode.decider); boundsFor.set(pc.autoMode.decider, seen === undefined ? merged : tightenDenialLimit(seen, merged)); } const rebuiltTrackers = new Map(liveTrackers); const trackerForRebuilt = (decider) => { let t = rebuiltTrackers.get(decider); if (t === undefined) { t = createAutoModeDenialTracker(boundsFor.get(decider) ?? denialLimit); rebuiltTrackers.set(decider, t); } return t; }; return entries.map((pc) => (needsTracker(pc) ? { ...pc, autoMode: { ...pc.autoMode, denialTracking: trackerForRebuilt(pc.autoMode.decider) } } : pc)); } export function createDenialLimitStop(opts) { const gateStopRef = {}; const stopForDenialLimit = (info) => { if (gateStopRef.terminal !== undefined) return; const lane = info.settlement === undefined ? `no approver is wired to fall back to` : `the durable approval it parked was settled with nobody judging it ("${info.settlement}")`; const message = `too many classifier denials in headless mode — ${denialLimitSentence(info.fallback)} The run was stopped: ` + `the auto-mode classifier's denial limit falls back to a person, and ${lane} ` + `(latest blocked action: "${info.toolName}").`; const terminal = Object.assign(new Error(message), { code: "classifier.denial_limit" }); gateStopRef.terminal = terminal; deliverEngineNotice(opts.onNotice, { code: "classifier.denial_limit", message, detail: { sessionId: opts.sessionId, runId: opts.runId, toolName: info.toolName, toolCallId: info.toolCallId, consecutive: info.fallback.consecutive, total: info.fallback.total, limit: info.fallback.limit }, }); opts.abort(); }; return { gateStopRef, stopForDenialLimit }; } export const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }; function fallbackCarry(fallback, liveApprover, tracker) { if (fallback === undefined) return {}; return { denialLimitFallback: liveApprover && tracker !== undefined ? tracker.armTimedWindow(fallback) : unarmedWindow(fallback) }; } export function gateAskCarry(decision, liveApprover, tracker) { return { ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...(decision.askClass !== undefined && decision.classes !== undefined ? { askClass: decision.askClass, classes: decision.classes } : {}), ...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker), }; } export function inheritedAskCarry(judged, liveApprover, tracker) { return { origin: judged.origin, ...askClassMembersOf(judged.classification), ...fallbackCarry(judged.fallback, liveApprover, tracker), }; } export const inheritedAskFallbackMessage = (toolName) => `approval required for "${toolName}" (inherited parent policy)`; export async function judgeInheritedClassifier(opts) { const { autoMode, req } = opts; const facts = opts.facts ?? INHERITED_STATION_FACTS, m = facts.mandateMarks; const frames = [...(m.egress === true ? [markTightenMessage(req.toolName, "egress")] : []), ...(m.irreversibility === "always" ? [markTightenMessage(req.toolName, "irreversible")] : [])]; const ask = (frames.length > 0 ? carryEngineAttestations(opts.ask, { ...opts.ask, message: [opts.ask.message ?? inheritedAskFallbackMessage(req.toolName), ...frames].join(" — ") }) : opts.ask); const origin = askOriginOf(ask, facts); const classification = classifyAsk(ask, facts); const classifierStandsDown = () => { const live = opts.liveMode?.(); return live === "dontAsk" || live === "bypassPermissions" || live === "plan" || (autoMode?.liveMode !== undefined && autoMode.liveMode.current !== "auto"); }; if (autoMode === undefined || classifierStandsDown() || !classification.row.classifierEligible) { return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification }; } const verdict = await autoMode.decider .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal) .catch(() => ({ kind: "unavailable", cause: "error" })); if (verdict.kind === "allow" && classifierStandsDown()) return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification }; const attributed = (d) => (verdict.classifier !== undefined ? attestClassifierRound(d, verdict.classifier, req) : d); if (verdict.kind === "allow") { autoMode.denialTracking?.recordAllow(); return { kind: "allow", result: attributed({ action: "allow", decisionReason: "classifier", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) }; } if (verdict.kind === "unavailable") { const arm = classifierUnavailableArm({ ask, req, cause: verdict.cause, seat: opts.unavailableSeat, aborted: opts.signal.aborted }); if (arm.kind === "asked") return { kind: "resolve", ask: arm.ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification: classifyAsk(arm.ask, facts) }; return { kind: "deny", result: attributed(arm.deny) }; } const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier); const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : ""; const tracked = autoMode.denialTracking?.recordBlock(); if (tracked?.limitReached !== true) { const denied = { action: "deny", message: verdict.kind === "block" ? `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : `${reason} (${opts.subject}, at an inherited ancestor layer)`, decisionReason: "classifier", }; return { kind: "deny", result: attributed(verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", req) : denied) }; } const fallback = tracked.fallback; const minted = carryEngineAttestations(ask, { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback }); return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, facts), classification: classifyAsk(minted, facts) }; } export function headlessDenyAtFold(r) { return r.settlement !== undefined && SETTLEMENT_NOBODY_JUDGED[r.settlement.kind]; } export function headlessDenyAtRecheck(r) { return headlessDenyAtFold(r) || r.settlement?.kind === "approver_unavailable" || r.approverUnavailable === true; } export function settleDenialLimitFallback(opts) { const { fallback, resolved } = opts; if (fallback === undefined) return; if (resolved.action === "allow") { if (opts.mintedHere) opts.tracker?.recordAllow(); } else if (resolved.action === "deny" && opts.headless(resolved)) { opts.stop({ toolName: opts.toolName, toolCallId: opts.toolCallId, fallback }); } } export function lateAskSettlementObserver(args) { return (late) => { if (late.kind === "approve") { deliverEngineNotice(args.onNotice, { code: "task.late_approval", message: `an approval for "${args.toolName}" was not consumed: a run or turn interrupt released the ask wait, ` + `so the tool did NOT run and the approval was not honored (an unconsumed answer is the approver ` + `releasing its wait, never a verdict).`, detail: { toolName: args.toolName, toolCallId: args.toolCallId, sessionId: args.sessionId, runId: args.runId, ...(args.taskId !== undefined ? { taskId: args.taskId } : {}), }, }); return; } args.onError?.(late.error, { phase: "hook", sessionId: args.sessionId }); }; }