UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

277 lines (276 loc) • 14.3 kB
import { argsAsTheToolReads, stampArgsReading } from "./effective-path-target.js"; import { abortedSeatDeny, decisionText, describeThrown, isEngineRefusal, ownArgs, ownedReading, refuseOutOfContractDecision, unaskedSeatAnswer } from "./tool-policy.js"; import { createSettlementLedger, engineFailClosedExit, hookDenyExit, unownedArgsExit } from "./runner/gate-exit.js"; import { classicEventBase, dispatchClassic, ENGINE_CORE_ANSWER, engineCore, failedHandlerClause, judgedInputOf, observePermissionDenied, recordScreeningChain } from "./mods/stations.js"; import { runGateFold } from "./gate-fold.js"; import { runGateLanes } from "./gate-lanes.js"; export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-reads.js"; import { brandPolicyAskClass } from "./ask-class.js"; import { isAskOrigin, screenAskClassMembers } from "./ask-origin.js"; import { isAutoModeFallbackCause } from "./auto-mode.js"; import { inlineUntrusted } from "./untrusted-text.js"; export { formatHookFeedback } from "./reminder-mint.js"; import { createSafeNotifier } from "./safe-notify.js"; import { wireOf } from "./tool-name-readings.js"; const reflectApply = Reflect.apply; const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const getPrototypeOf = Object.getPrototypeOf; const objectPrototype = Object.prototype; function findEnvSlot(env, key) { for (let o = env; o !== null && o !== objectPrototype; o = getPrototypeOf(o)) { const desc = getOwnPropertyDescriptor(o, key); if (desc !== undefined) return { holder: o, desc }; } return undefined; } function readEnvPrimitive(env, key) { const slot = findEnvSlot(env, key); if (slot === undefined) return undefined; return slot.desc.get !== undefined ? reflectApply(slot.desc.get, env, []) : slot.desc.value; } function createCwdReader(env) { const slot = findEnvSlot(env, "cwd"); if (slot === undefined) return undefined; const getter = slot.desc.get; const initial = getter !== undefined ? reflectApply(getter, env, []) : slot.desc.value; if (typeof initial !== "string") return undefined; let last = initial; return () => { const desc = getOwnPropertyDescriptor(slot.holder, "cwd"); const value = desc === undefined ? undefined : desc.get !== undefined ? desc.get === getter ? reflectApply(desc.get, env, []) : undefined : desc.value; if (typeof value === "string") last = value; return last; }; } export function createHookEnvCapabilities(env) { const face = Object.create(null); const canonicalPath = readEnvPrimitive(env, "canonicalPath"); const exists = readEnvPrimitive(env, "exists"); const readLink = readEnvPrimitive(env, "readLink"); const cwd = createCwdReader(env); if (typeof canonicalPath === "function") { face.canonicalPath = (path, abortSignal) => reflectApply(canonicalPath, env, [path, abortSignal]); } if (typeof exists === "function") { face.exists = (path, abortSignal) => reflectApply(exists, env, [path, abortSignal]); } if (typeof readLink === "function") { face.readLink = (path, abortSignal) => reflectApply(readLink, env, [path, abortSignal]); } if (cwd !== undefined) { face.cwd = cwd; } return Object.freeze(face); } const IDENTITY_AGENT_NAME_MAX = 80; export function mintHookInvocationIdentity(facts) { return Object.freeze(Object.assign(Object.create(null), { sessionId: facts.sessionId, taskId: facts.taskId, legKind: facts.legKind, isDelegatedChild: facts.isDelegatedChild, ...(facts.insideFork === true ? { insideFork: true } : {}), ...(facts.agentName !== undefined ? { agentName: inlineUntrusted(facts.agentName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}), ...(facts.spawnName !== undefined ? { spawnName: inlineUntrusted(facts.spawnName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}), ...(facts.agentType !== undefined ? { agentType: inlineUntrusted(facts.agentType.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}), ...(facts.parentToolCallId !== undefined ? { parentToolCallId: facts.parentToolCallId } : {}), })); } function screenPreToolUseResult(r) { if (r === undefined || r === null || r === false) return r; return refuseOutOfContractDecision(r, { reasonIsNonInput: true }); } function preToolUseAnswerViolation(answer) { const r = screenPreToolUseResult(answer); if (r === undefined || r === null || r === false || !isEngineRefusal(r)) return undefined; return typeof r.message === "string" ? r.message : "the PreToolUse answer was refused by the engine"; } function preToolUseRefusedReason(subject, failed) { if (failed === undefined) { return `the task was cancelled while a PreToolUse hook was still screening ${subject}; the call was NOT executed ` + `(fail-closed). No verdict was reached — this is the run ending, not the tool failing.`; } const tail = failed.cause === "threw" ? ` This is a failure of the deployment's hook, NOT of the tool itself — an identical retry reaches the same hook and fails the same way.` : failed.cause === "timeout" ? ` This is a failure of the deployment's hook, NOT of the tool itself — the screening face never returned a verdict, so nothing was decided about this call either way.` : ""; return `a PreToolUse hook ${failedHandlerClause(failed)} while screening ${subject}; the call was NOT executed (fail-closed).${tail}`; } export function createPreToolUseScreening(input) { const bus = input.modBus; if (bus === undefined || !bus.hasHandlers("classic.PreToolUse")) return undefined; const { toolCallId, toolName } = input.event; const screenedName = wireOf("gate.preToolUse", input.event); const eventOf = (args) => { const cwd = input.trackedCwd?.(); return { ...classicEventBase(input.identity, input.observedPermissionMode?.()), tool: screenedName, tool_use_id: toolCallId, input: judgedInputOf(args), ...(typeof cwd === "string" ? { cwd } : {}), ...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}), }; }; return { screen: async (args, subject) => { let outcome; try { outcome = await dispatchClassic(bus, "classic.PreToolUse", eventOf(args), { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), checkAnswer: preToolUseAnswerViolation }); } catch (err) { return { kind: "refused", reason: preToolUseRefusedReason(subject, { kind: "failed", registration: "engine", cause: "threw", reason: `could not raise the screening: ${describeThrown(err)}`, skipped: [] }) }; } if (outcome === undefined || outcome.kind === "aborted") return { kind: "refused", reason: preToolUseRefusedReason(subject, undefined) }; if (outcome.kind === "failed") return { kind: "refused", reason: preToolUseRefusedReason(subject, outcome) }; const r = screenPreToolUseResult(outcome.answer); return { kind: "answered", answer: r === null || r === false ? undefined : r, ...(outcome.answeredBy !== undefined ? { answeredBy: outcome.answeredBy } : {}) }; }, deniedReason: (answer) => decisionText(answer) ?? `tool "${toolName}" blocked by a PreToolUse hook`, }; } export function createPreToolUseConstraintPolicy(chain, env) { const judges = (p) => !p.observe; const minted = brandPolicyAskClass({ check: async (req, signal) => { if (!chain.hasHandlers("classic.PreToolUse", judges)) return { action: "allow" }; const unasked = unaskedSeatAnswer(req, signal, { aborted: `the inherited PreToolUse screening of "${req.toolName}" was not consulted: the run or turn was already interrupted`, unreachable: `the inherited PreToolUse screening of "${req.toolName}" was not consulted: nobody can be asked about this call` }, "hook"); if (unasked !== undefined) return unasked; const subject = `this call to "${req.toolName}"`; let outcome; try { outcome = await chain.dispatch("classic.PreToolUse", { tool: req.toolName, tool_use_id: req.toolCallId, input: judgedInputOf(req.args), ...(env !== undefined ? { env } : {}) }, { bottom: engineCore, participant: judges, ...(signal !== undefined ? { signal } : {}), checkAnswer: (a) => (a === ENGINE_CORE_ANSWER ? undefined : preToolUseAnswerViolation(a)), }); } catch (err) { return { action: "deny", message: preToolUseRefusedReason(subject, { kind: "failed", registration: "engine", cause: "threw", reason: `could not raise the screening: ${describeThrown(err)}`, skipped: [] }), decisionReason: "hook" }; } if (outcome.kind === "aborted") return abortedSeatDeny(req, `the inherited PreToolUse screening of "${req.toolName}" ended when the run or turn was interrupted`, "hook"); if (outcome.kind === "failed") return { action: "deny", message: preToolUseRefusedReason(subject, outcome), decisionReason: "hook" }; const r = screenPreToolUseResult(outcome.answer); if (r === undefined || r === null || r === false) return { action: "allow" }; if (r.action === "deny") { return { action: "deny", message: decisionText(r) ?? `tool "${req.toolName}" blocked by a PreToolUse hook`, decisionReason: "hook" }; } if (r.action === "ask") { return { action: "ask", message: decisionText(r) ?? `approval required for "${req.toolName}" (inherited PreToolUse hook)`, decisionReason: "hook", ...(r.requiresRealApproval === true ? { requiresRealApproval: true } : {}), ...(r.updatedInput !== undefined ? { updatedInput: r.updatedInput } : {}), }; } const stated = r.action === "allow" ? { decisionReason: "hook" } : {}; return r.updatedInput !== undefined ? { action: "allow", updatedInput: r.updatedInput, ...stated } : { action: "allow", ...stated }; }, }, "hook"); recordScreeningChain(minted, chain); return minted; } export function askCarryRowMembers(carry) { if (carry === undefined) return {}; const origin = carry.origin; const why = carry.classifierNoRuling; const classed = screenAskClassMembers(carry.askClass, carry.classes); return { ...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}), ...(isAutoModeFallbackCause(why) ? { classifierNoRuling: why } : {}), ...(isAskOrigin(origin) ? { origin } : {}), ...(classed !== undefined ? classed : {}), }; } export async function runToolGate(input) { const { event, adjudicate, resolveAsk, suspendAsk } = input; const { toolCallId, toolName } = event; const callSignal = input.callSignal; const modelInput = event.input; const owned = ownArgs(modelInput); const args = owned.ok ? owned.value : modelInput; const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined); const notifyPermissionDeniedSeat = (payload) => { const mode = input.observedPermissionMode?.(); const seen = mode !== undefined ? { ...payload, permissionMode: mode } : payload; const onHostObserverFailure = (err) => void notifier.notify(() => { throw err; }, "toolGate.permissionDenied"); return observePermissionDenied({ legBus: input.modBus, hostObserver: input.permissionDenied, payload: seen, signal: input.abortSignal, onHostObserverFailure }); }; const screening = createPreToolUseScreening(input); const pass = { input, event, adjudicate, resolveAsk, suspendAsk, toolName, toolCallId, callSignal, notifier, ledger: createSettlementLedger({ toolCallId, toolName }), notifyPermissionDenied: notifyPermissionDeniedSeat, screening, afterFold: runGateLanes, currentInput: args, req: Object.freeze({ toolName, args, toolCallId, ...(() => { const f = input.toolFaceOf?.(toolName); const read = event.readArguments; if (f !== undefined && read !== undefined) stampArgsReading(f, ownedReading(read)); return f !== undefined ? { face: f } : {}; })() }), preToolContext: [], hookAsk: undefined, parkFailed: undefined, deniedBy: undefined, denyCause: undefined, decision: { action: "allow" }, tightenedBy: undefined, policyRewrite: undefined, }; if (!owned.ok) return await unownedArgsExit(pass, owned.reason); if (screening !== undefined) { const screened = await screening.screen(pass.currentInput, `this call to "${toolName}"`); if (screened.kind === "refused") { return await engineFailClosedExit(pass, screened.reason); } const r = screened.answer; if (r) { if (r.additionalContext) { pass.preToolContext.push(r.additionalContext); } if (r.action === "deny") { return hookDenyExit(pass, screening.deniedReason(r)); } if (r.updatedInput !== undefined) { pass.currentInput = argsAsTheToolReads(pass.req, r.updatedInput); } if (r.action === "ask") { pass.hookAsk = r; } if (r.action === "allow") pass.hookAllowedBy = screened.answeredBy !== undefined ? [screened.answeredBy] : []; } } return await runGateFold(pass); }