UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

255 lines (254 loc) • 18.3 kB
import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js"; import { mountedToolRequiresUserInteraction } from "../mode-policy.js"; import { complianceCallDenial } from "../compliance.js"; import { formatHookFeedback, runToolGate } from "../hooks.js"; import { screenGateOutcome } from "../gate-outcome.js"; import { cloneObserverInput, directDeny, frozenInputSnapshot } from "./gate-exit.js"; import { emitTrace } from "../trace.js"; import { deliverEngineNotice } from "../engine-notice.js"; import { askUnresolvableNotice } from "../ask-unresolvable-notice.js"; import { approvalRouteOf } from "../wiring-manifest.js"; import { classifierUnavailableSeat } from "./classifier-ladder.js"; import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js"; import { callFaceSeat, rosterToolName, survivorsByName, toolCallFaceOf } from "../tool-roster.js"; import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js"; import { createApprovedCallScreen, createToolResultStation } from "./call-hook-stations.js"; const PEER_MESSAGE_NAMES = new Set([SEND_MESSAGE_TOOL_NAME]); function screenGateResult(result) { if (result.suspend !== undefined) return { defects: [] }; const gate = result.gate; if (gate === undefined) return { defects: ["a tool-gate result carried no outcome record on a terminal exit — every terminal exit mints one; the frame carries none"] }; const defects = screenGateOutcome(gate, { reading: "producer" }); const blocked = result.block === true; if ((gate.disposition.kind === "denied") !== blocked) { defects.push(`a tool-gate result's outcome reads "${gate.disposition.kind}" on ${blocked ? "a blocked" : "an executing"} call — the two faces of one verdict disagree; the frame carries no outcome`); } return defects.length > 0 ? { defects } : { gate, defects }; } export function maybeHumanRejectionHalt(input) { const d = input.gate?.disposition; const s = input.gate?.settlement; const bare = d?.kind === "denied" && d.deniedBy === "ask_resolution" && s?.kind === "human_refused" && s.note === undefined; if (!bare || input.isDelegatedChild) return undefined; return { reason: `This tool call was NOT executed: the user rejected the "${input.toolName}" tool call in the same ` + `assistant message, which stops the rest of the batch. Nothing was run for this call — ` + `re-issue it after the user's direction only if it is still needed.`, details: { error: "gate.batch_halted", code: "gate.batch_halted", rejectedToolCallId: input.toolCallId, rejectedToolName: input.toolName, }, }; } export function prepareGateStations(input) { const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, toolOccurrences, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, handsCwdRef, stampCallTarget, hookEnvFace, writeProtectionCheck, shellReadCheck, readFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, askRoute, permissionMode, toolMarkSources, a2aIsolatePeers, ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, frameSettlement, seatInputsFor, modBus, inheritedToolChecks } = input; const releasesAnnounced = new Map(); const onModeReleased = (info) => { const table = info.readUnestablished === true ? input.readDenyInForce(info.toolCallId) : undefined; if (table !== undefined && table.tiers.length + table.rows.length > 0) { const named = [...(table.tiers.length > 0 ? [`built-in tiers ${table.tiers.join(", ")}`] : []), ...table.rows.slice(0, 8), ...(table.rows.length > 8 ? [`${table.rows.length - 8} more`] : [])].join("; "); deliverEngineNotice(deps.onNotice, { code: "permission.read_deny_unapplied", message: `the ${info.release.mode} permission mode released "${info.toolName}" although a read-deny table is in force (${named}): the read boundary could not establish what this command reads, so no row of the table was applied to it`, detail: { mode: info.release.mode, toolCallId: info.toolCallId, tiers: [...table.tiers], rows: table.rows.slice(0, 8), rowCount: table.rows.length, sessionId, runId }, }); } const key = `${info.release.mode}\u0000${info.release.askClass}`; const count = (releasesAnnounced.get(key) ?? 0) + 1; releasesAnnounced.set(key, count); if (count !== 1) return; deliverEngineNotice(deps.onNotice, { code: "permission.ask_released_by_mode", message: `the ${info.release.mode} permission mode released a ${info.release.askClass} ask on "${info.toolName}" without putting it to anyone (further releases of this pair in this run ride each call's tool_end.gate.disposition.released)`, detail: { mode: info.release.mode, askClass: info.release.askClass, ...(info.release.origin !== undefined ? { origin: info.release.origin } : {}), ...(info.release.mandate !== undefined ? { mandate: info.release.mandate } : {}), count, firstToolCallId: info.toolCallId, sessionId, runId }, }); }; if (askLane !== undefined && parkAsk !== undefined) { const { adjudicate, resolveAskBound, notifyPermissionDenied, notifyHookError } = askLane; const { resolveContentAsk, suspendAsk } = parkAsk; for (const t of survivorsByName(tools).values()) { const eff = t.effect; if (eff !== undefined && !toolEffects.has(t.name)) toolEffects.set(t.name, eff); } const writeProtectionArmed = writeProtectionCheck !== undefined && tools.some((t) => PATH_CONFINABLE_WRITE_TOOLS.has(t.name)); toolCallGateArmedRef.armed = effectivePolicy !== undefined || modBus.hasHandlers("classic.PreToolUse") || egressTools.size > 0 || irreversibleTools.size > 0 || spec.enablePlanMode === true || complianceDenies.has("web_fetch") || writeProtectionArmed || shellReadCheck !== undefined; if (toolCallGateArmedRef.armed) { harness.on("tool_call", async (called) => { const e = { ...called, toolName: rosterToolName(toolRosterDeltas.current, called.toolName) }; const call = { toolName: e.toolName, ...callFaceSeat(toolCallFaceOf(toolRosterDeltas.current, called.toolName)) }; toolOccurrences.ensure({ toolCallId: e.toolCallId, spelledName: called.toolName, wireName: e.toolName, args: called.input }); const tierSeat = seatOf("gate.shellSeat", irreversibilityTier, call); const markSources = new Set(heldValues("gate.markSources", toolMarkSources, call).flatMap((s) => [...s])); blockedToolCalls.delete(e.toolCallId); inheritedAskGrants.delete(e.toolCallId); { const complianceDeny = heldValues("gate.compliance", { get: (n) => complianceCallDenial(complianceDenies, n) }, call)[0]; if (complianceDeny !== undefined) { if (blockedTracked) blockedToolCalls.add(e.toolCallId); const gate = directDeny("compliance"); const finalInput = frozenInputSnapshot(e.input); toolOccurrences.gateRound(e.toolCallId, gate, finalInput); if (notifyPermissionDenied) { await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(finalInput), toolCallId: e.toolCallId, reason: complianceDeny, gate, identity: hookIdentity, permissionMode: permissionMode.observed }); } return { block: true, reason: formatHookFeedback(complianceDeny, reminderMark), preToolContext: [] }; } } let result; try { result = await runToolGate({ onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: hostTaskId, site: f.site, message: f.error.message, ts: Date.now() })), event: e, ...(e.signal !== undefined ? { callSignal: e.signal } : {}), identity: hookIdentity, reminderMark, modBus, ...(inheritedToolChecks.length > 0 ? { inheritedToolChecks } : {}), observedPermissionMode: () => permissionMode.observed, ...(handsCwdRef !== undefined ? { trackedCwd: () => handsCwdRef.current } : {}), ...(stampCallTarget !== undefined ? { stampCallTarget } : {}), ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}), toolFaceOf: (name) => toolCallFaceOf(toolRosterDeltas.current, name === e.toolName ? called.toolName : name), adjudicate, resolveAsk: (...args) => toolOccurrences.whileAwaitingUser(args[1].toolCallId, () => resolveAskBound(...args)), suspendAsk, resolveContentAsk: (...args) => toolOccurrences.whileAwaitingUser(args[0].toolCallId, () => resolveContentAsk(...args)), egress: heldBy("gate.egress", egressTools, call), peerMessage: heldBy("gate.peerMessage", PEER_MESSAGE_NAMES, call), ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}), ...(shellReadCheck !== undefined ? { shellReadCheck } : {}), readFace, irreversibility: tierSeat?.value, reversibilityProbe: tierSeat?.read(reversibilityProbes), abortSignal: abortController.signal, onHookError: notifyHookError, shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true, ...(autoModeDecider.current !== undefined ? { autoMode: { decider: autoModeDecider.current, ...(autoModeDenialTracking.current !== undefined ? { denialTracking: autoModeDenialTracking.current } : {}), unavailable: classifierUnavailableSeat({ station: "leg", ...(autoModeDecider.current.timeoutMs !== undefined ? { budgetMs: autoModeDecider.current.timeoutMs } : {}), route: approvalRouteOf(askRoute), onNotice: deps.onNotice, sessionId, runId }) } } : {}), permissionMode: () => permissionMode.current, askSeatForm: askRoute.form, ...(markSources.size > 0 ? { toolMarkSources: markSources } : {}), ...seatInputsFor(e.toolName, wireOf("gate.skillActivationSeat", call)), ...(mountedToolRequiresUserInteraction(tools, e.toolName) ? { requiresUserInteraction: true } : {}), ...(a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}), onModeReleased, ancestorArbitrationOf: (toolCallId) => ancestorArbitrations.get(toolCallId), onHeadlessDenialLimit: stopForDenialLimit, onAskUnresolvable: (info) => deliverEngineNotice(deps.onNotice, askUnresolvableNotice({ sessionId, ...info })), ...(permissionRuleLane ? { persistedRules: { adjudicate: permissionRuleLane.adjudicate, onResolved: (info) => emitTrace(deps.tracer, () => ({ kind: "permission.persisted_rule_allowed", version: 2, taskId: hostTaskId, toolName: info.toolName, toolCallId: info.toolCallId, rules: [...info.rules], ts: Date.now(), })), }, } : {}), onReadOnlyAllowed: (info) => emitTrace(deps.tracer, () => ({ kind: "permission.read_only_allowed", version: 1, taskId: hostTaskId, toolName: info.toolName, toolCallId: info.toolCallId, command: info.command, ts: Date.now(), })), ...(permissionRuleOrgLane ? { orgRules: { adjudicate: permissionRuleOrgLane.adjudicate, contentAskToolMounted: questionToolMounted, onUnavailable: (info) => emitTrace(deps.tracer, () => ({ kind: "permission.org_snapshot_unavailable", version: 1, taskId: hostTaskId, toolName: info.toolName, toolCallId: info.toolCallId, message: info.message, ts: Date.now(), })), }, } : {}), isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId), ...(sandboxAdmissionArmed ? { sandboxAdmission: { askClassesOf: (toolCallId) => foldAskClasses.get(toolCallId), boundaryCapable: sandboxBoundaryCapable(call), onAdmitted: emitSandboxAdmitted, }, } : {}), }); } finally { inheritedUnavailableAsks.delete(e.toolCallId); inheritedAskGrants.delete(e.toolCallId); foldAskClasses.delete(e.toolCallId); ancestorArbitrations.delete(e.toolCallId); } const ancestorAdmitted = ancestorSandboxAdmissions.get(e.toolCallId); ancestorSandboxAdmissions.delete(e.toolCallId); if (ancestorAdmitted !== undefined && !result.block && !result.suspend) { emitSandboxAdmitted({ toolCallId: e.toolCallId, toolName: e.toolName, askClasses: ancestorAdmitted, boundary: "sandbox_internal" }); result.preToolContext.push(`permission.sandbox_admitted: this call's inherited pending approval was auto-admitted because the deployment declares an isolated sandbox execution env and the request stays inside it. Recorded for audit.`); } if (!result.block && !result.suspend && result.preToolContext.length > 0) { preToolContexts.set(e.toolCallId, result.preToolContext); } if (blockedTracked && (result.block || result.suspend)) { blockedToolCalls.add(e.toolCallId); } const screened = screenGateResult(result); for (const defect of screened.defects) deps.onError?.(new Error(defect), { phase: "config", sessionId }); if (screened.gate !== undefined) toolOccurrences.gateRound(e.toolCallId, screened.gate, result.finalInput); if (result.block) { const haltRemaining = maybeHumanRejectionHalt({ gate: screened.gate, isDelegatedChild: delegation.isDelegatedChild === true, toolName: e.toolName, toolCallId: e.toolCallId, }); const details = result.details !== undefined ? { details: result.details } : {}; if (haltRemaining !== undefined) { batchHaltRef.current = { rejectedToolCallId: e.toolCallId }; return { block: true, reason: result.reason, ...details, haltRemaining }; } return { block: true, reason: result.reason, ...details }; } if (result.suspend === undefined) toolOccurrences.release(e.toolCallId); return result.updatedInput !== undefined ? { updatedInput: result.updatedInput } : undefined; }); } } const toolResultStation = createToolResultStation({ modBus, blockedToolCalls, toolRosterDeltas, preToolContexts, abortController, spec, handsCwdRef, hookEnvFace, hookIdentity, permissionMode, reminderMark, frameSettlement }); if (toolResultStation !== undefined) harness.on("tool_result", toolResultStation); const approvedCallScreen = createApprovedCallScreen({ modBus, permissionMode, abortController, handsCwdRef, hookEnvFace, hookIdentity, preToolContexts }); return { toolCallStationArmed: toolCallGateArmedRef.armed, toolResultStationArmed: toolResultStation !== undefined, ...(approvedCallScreen !== undefined ? { approvedCallScreen } : {}) }; }