UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

433 lines (432 loc) • 26.3 kB
import { emitTrace } from "../trace.js"; import { StubExecutionEnv } from "../stub-env.js"; import { hasBackgroundShell } from "../background-shell.js"; import { defaultTaskRegistry } from "../task-registry.js"; import { bashReversibilityProbe, activeReadDenyBuiltinTiers, readDenyBuiltinConfigOf, compileReadDeny, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, backgroundOutputReadExemption, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js"; import { resolveKey, rootAdmittingArtifactStore, sha256 } from "../../tools/fs/safety.js"; import { thrownText } from "../thrown-value.js"; import { carryReadFace } from "../../tools/fs/read-deny-home.js"; import { EMPTY_READ_ROOTS, afterCallTables, denySeatsOf, onPinnedRead, pathTablesCompile, readRootsOver } from "./person-path-rows.js"; import { createShellOverflowSpoolFence } from "../../tools/fs/fs-shared.js"; import { createShellReadStation } from "../../tools/fs/shell-read-check.js"; import { createFileReadStation } from "../../tools/fs/file-read-check.js"; import { isShellFaceToolName, predictMountedToolNames, readTargetToolNames } from "../tool-registry.js"; import { normalizeFileText } from "../../tools/fs/encoding.js"; import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js"; import { deliverEngineNotice } from "../types.js"; import { readRootGrantOver } from "./read-root-grant.js"; import { sessionSeatAnchorHolds } from "./session-read-file-states.js"; import { rebaseWorkspacePath } from "./workspace-path.js"; import { RosterBuilder } from "../tool-roster.js"; import { unionAddOnly } from "../child-clamp-seats.js"; import { assertStringArraySeat } from "../read-face-seats.js"; import { modePolicyOf } from "../mode-policy.js"; import { valveLeaves } from "../../agents/tool-filter.js"; const faceClampAnnouncedSinks = { read: new WeakSet(), write: new WeakSet() }; const faceClampConsoleAnnounced = { read: false, write: false }; function announceDeploymentFaceClamp(kind, sink) { if (typeof sink === "function") { if (faceClampAnnouncedSinks[kind].has(sink)) return; faceClampAnnouncedSinks[kind].add(sink); } else { if (faceClampConsoleAnnounced[kind]) return; faceClampConsoleAnnounced[kind] = true; } deliverEngineNotice(sink, deploymentFaceClampNotice(kind)); } export function __resetReadFaceClampAnnouncement() { faceClampConsoleAnnounced.read = false; faceClampConsoleAnnounced.write = false; } function writeFaceReader(input, onDeploymentClamp) { const { spec, deps, resume, fullShellReachable, permissionMode } = input; const seats = { kind: "write", readOnlyMount: spec.handsReadOnly === true, orgGoverned: deps.permissionRuleStore?.partitions.org === true, fullShellReachable }; resolveContainmentFace({ ...seats, specFace: spec.writeFace, depsFace: deps.writeFace, ...(onDeploymentClamp !== undefined ? { onDeploymentClamp } : {}) }); const recorded = resume === undefined ? "live" : resume.seed.writeFace?.face; const folded = (word) => (recorded === "roots" ? "roots" : word === "open" && recorded !== "open" && recorded !== "live" ? undefined : word); const faceUnder = (modeFace) => resolveContainmentFace({ ...seats, specFace: folded(spec.writeFace), depsFace: folded(deps.writeFace), ...(modeFace !== undefined ? { modeFace } : {}) }); return { inForce: () => faceUnder(modePolicyOf(permissionMode.current).writeFace), fenced: faceUnder("open") === "roots" }; } export function resolveHandsLessReadFace(input) { const { resume, fullShellReachable, spec, deps } = input; assertStringArraySeat(deps.readDenyPatterns, "RunnerDeps.readDenyPatterns", "compiler-owned"); let handsLessResolvedFace; let handsLessResolvedWriteFace; let readDenyAdditionsNormalized = []; let readDenyBuiltinTiersActive = []; { const liveFace = resolveContainmentFace({ kind: "read", specFace: spec.readFace, depsFace: deps.readFace, readOnlyMount: spec.handsReadOnly === true, orgGoverned: deps.permissionRuleStore?.partitions.org === true, fullShellReachable, }); const seed = resume?.seed.readFace; handsLessResolvedFace = resume !== undefined && (seed === undefined || seed.face === "roots") ? "roots" : liveFace; handsLessResolvedWriteFace = writeFaceReader(input); const denyAdditions = unionAddOnly("readDenyPatterns", deps.readDenyPatterns, spec.readDenyPatterns, seed?.denyEntries); readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers(readDenyBuiltinConfigOf({ tiers: deps.readDenyBuiltinTiers, exclude: deps.readDenyBuiltinExclude })); if (denyAdditions.length > 0) { readDenyAdditionsNormalized = compileReadDeny(denyAdditions, "readDenyPatterns", { tiers: [] }).entries; } } return { readFileStateForCheckpoint: undefined, seedContextFiles: undefined, handsCwdRef: undefined, workspaceStateSettle: undefined, wsSnapshot: undefined, rebaseWsPath: (p) => p, backgroundTaskToolsActive: false, shellReadCheck: undefined, shellReadStationArmed: false, shellReadAdmission: undefined, readAdmission: undefined, resolvedReadFace: undefined, resolvedWriteFace: undefined, readDenyAdditionsNormalized, readDenyBuiltinTiersActive, handsLessResolvedFace, handsLessResolvedWriteFace, additionalRootsCanonical: [], additionalReadRootsCanonical: [], grantReadRoot: undefined, artifactStoreRef: undefined, attachmentRootCanonical: undefined, readDenyMatcher: undefined, readRoots: EMPTY_READ_ROOTS, pathTables: undefined, envHandToolNames: new Set(), shellGatedTools: new Set(), sealReadStateSeat: undefined, }; } export async function prepareHandsMount(input) { const envHandToolNames = new Set(); const { handsEnabled, executionEnv, taskRootCanonical, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGate, advertisedWritableDirs, firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input; assertStringArraySeat(spec.additionalDirectories, "TaskSpec.additionalDirectories", "strings"); assertStringArraySeat(spec.additionalReadDirectories, "TaskSpec.additionalReadDirectories", "strings"); assertStringArraySeat(deps.readDenyPatterns, "RunnerDeps.readDenyPatterns", "compiler-owned"); let readFileStateForCheckpoint; let sealReadStateSeat; let seedContextFiles; let handsCwdRef; let workspaceStateSettle; let wsSnapshot; let rebaseWsPath = (p) => p; let backgroundTaskToolsActive = false; let shellReadStation; let shellReadTools = new Set(); let fileReadStation; let resolvedReadFace; let resolvedWriteFace; let readDenyAdditionsNormalized = []; let readDenyBuiltinTiersActive = []; let handsLessResolvedFace; const handsLessResolvedWriteFace = undefined; const shellGatedTools = new Set(); const additionalRootsCanonical = []; const additionalReadRootsCanonical = []; let grantReadRoot; const artifactStoreRef = { current: undefined }; let attachmentRootCanonical; let readDenyMatcher; let pathTables; let readRoots = EMPTY_READ_ROOTS; if (handsEnabled) { const rootCanonical = taskRootCanonical; attachmentRootCanonical = rootCanonical; const canonicalizeExtraDirs = async (dirs, field, sink) => { for (const dir of dirs ?? []) { if (!dir || !dir.trim()) continue; const c = await (advertisedWritableDirs?.canonicalizeDeclaredRoot(dir) ?? executionEnv.canonicalPath(dir)); if (c.ok) { sink.push(c.value); } else { deps.onError?.(new Error(`${field} entry skipped (cannot canonicalize): ${dir}`), { phase: "config", sessionId, }); emitTrace(deps.tracer, () => ({ kind: "config.additional_directory_skipped", version: 1, taskId: hostTaskId, entry: dir, ...(field !== "additionalDirectories" ? { field } : {}), reason: `${c.error.code}: ${c.error.message}`, ts: Date.now(), })); } } }; await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical); await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical); if (advertisedWritableDirs !== undefined) { if (spec.envFacts?.scratchpadDir) await advertisedWritableDirs.admit({ path: spec.envFacts.scratchpadDir, advertiser: "scratchpad", materializeMarker: ".sema-scratchpad" }); additionalRootsCanonical.push(...advertisedWritableDirs.roots); } for (const a of [additionalRootsCanonical, additionalReadRootsCanonical]) a.splice(0, a.length, ...new Set(a)); grantReadRoot = readRootGrantOver({ env: executionEnv, writeRoots: additionalRootsCanonical, readRoots: additionalReadRootsCanonical, artifactStore: artifactStoreRef }); const readFileState = new Map((resume?.seed.readFileState ?? []).map(([k, v]) => [rebaseRestoredPath(k), v])); readFileStateForCheckpoint = readFileState; seedContextFiles = async (files) => { for (const f of files) { const rk = await readRoots.ready().then(() => resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, readRoots(), undefined, undefined, resolvedReadFace)); if (rk.ok) seedReadFileStateFromContext(readFileState, rk.key, f.content); } }; const handsIncludeShell = !(executionEnv instanceof StubExecutionEnv); const handsReadOnly = spec.handsReadOnly === true; backgroundTaskToolsActive = handsIncludeShell && !handsReadOnly && hasBackgroundShell(executionEnv); const shellFaceHere = [...predictMountedToolNames(new Set(backgroundTaskToolsActive ? ["hands", "backgroundTasks"] : ["hands"]))].filter(isShellFaceToolName); wsSnapshot = resume !== undefined || !(handsIncludeShell && !handsReadOnly) ? undefined : await session.getWorkspaceState().catch(() => undefined); rebaseWsPath = (p) => (wsSnapshot === undefined || wsSnapshot.taskRoot === rootCanonical ? p : rebaseWorkspacePath(p, wsSnapshot.taskRoot, rootCanonical)); if (handsIncludeShell && !handsReadOnly) { handsCwdRef = { current: rootCanonical }; if (resume?.seed.handsCwd !== undefined) handsCwdRef.current = rebaseRestoredPath(resume.seed.handsCwd); else if (wsSnapshot?.handsCwd !== undefined) handsCwdRef.current = rebaseWsPath(wsSnapshot.handsCwd); workspaceStateSettle = { rootCanonical, restoredHandsCwd: handsCwdRef.current !== rootCanonical ? handsCwdRef.current : undefined, ...(resume !== undefined ? { baselineUnknown: true } : {}), }; } const seedReadFaceSection = resume?.seed.readFace; const readDenyAdditions = unionAddOnly("readDenyPatterns", deps.readDenyPatterns, spec.readDenyPatterns, seedReadFaceSection?.denyEntries); const readDenyBuiltinCfg = readDenyBuiltinConfigOf({ tiers: deps.readDenyBuiltinTiers, exclude: deps.readDenyBuiltinExclude }); const canonicalize = async (dirs) => { const out = []; for (const d of dirs) await canonicalizeExtraDirs([d], "persistedReadRule", out).catch((err) => deps.onError?.(new Error(`persistedReadRule entry skipped (its lookup threw): ${d}: ${thrownText(err)}`), { phase: "config", sessionId })); return out; }; const tables = (pathTables = await input.personRows.attachTables(pathTablesCompile({ env: executionEnv, additions: readDenyAdditions, builtin: readDenyBuiltinCfg, legRows: input.denyRuleRows, leads: input.headLeads, configRoot: deps.agentConfigDir, canonicalize }))); readDenyMatcher = tables.readDeny; readRoots = readRootsOver({ own: () => [...additionalRootsCanonical, ...additionalReadRootsCanonical], tables, ready: input.personRows.tablesReady, store: () => artifactStoreRef.current, onHeld: (entry, store) => emitTrace(deps.tracer, () => ({ kind: "config.additional_directory_skipped", version: 1, taskId: hostTaskId, entry, field: "persistedReadRule", reason: `admits_artifact_store: the local artifact store ${store} lies inside it or it inside the store — the Artifact tool's ledger and stored files stay outside every root the file tools reach`, ts: Date.now() })) }); readDenyBuiltinTiersActive = activeReadDenyBuiltinTiers(readDenyBuiltinCfg); readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries; let liveReadFace = resolveContainmentFace({ kind: "read", specFace: spec.readFace, depsFace: deps.readFace, readOnlyMount: handsReadOnly, orgGoverned: deps.permissionRuleStore?.partitions.org === true, fullShellReachable, onDeploymentClamp: () => announceDeploymentFaceClamp("read", deps.onNotice), }); if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots")) liveReadFace = "roots"; resolvedReadFace = liveReadFace; resolvedWriteFace = writeFaceReader(input, () => announceDeploymentFaceClamp("write", deps.onNotice)); const sessionSeats = input.internals?.sessionReadStates; let seatLeafId; try { seatLeafId = (await session.getStorage().getLeafId()) ?? null; } catch { seatLeafId = undefined; } const judgeKey = async (path) => { const rk = await readRoots.ready().then(() => resolveKey(executionEnv, rootCanonical, path, undefined, undefined, readRoots(), undefined, undefined, liveReadFace)); return rk.ok ? rk.key : undefined; }; if (resume === undefined && spec.sessionId !== undefined) { const seatRow = spec.resumeAt === undefined && seatLeafId !== undefined ? sessionSeats?.get(sessionId) : undefined; const inheritedSeat = seatRow !== undefined && (await sessionSeatAnchorHolds((id) => session.getEntry(id), seatRow.anchorLeafId, seatLeafId)) ? seatRow.state : undefined; if (inheritedSeat !== undefined) { await inheritSessionReadFileState(readFileState, inheritedSeat, judgeKey); } else { const prior = await session.buildContext().catch(() => undefined); for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) { const rk = await judgeKey(rec.path); if (rk === undefined) continue; if ("content" in rec) seedReadFileStateFromTranscript(readFileState, rk, rec.content, rec.at); else readFileState.set(rk, { ...rec.observation, lastReadAt: rec.at }); } } } if (input.restoredFilePaths !== undefined) { for (const abs of input.restoredFilePaths) { const k = await judgeKey(abs); if (k === undefined) continue; const entry = readFileState.get(k); if (entry === undefined || entry.seededFromContext === true) continue; const cur = await executionEnv.readTextFile(abs); if (cur.ok && entry.hash === sha256(normalizeFileText(cur.value))) continue; readFileState.delete(k); } } if (seatLeafId !== undefined) sessionSeats?.adopt(sessionId, readFileState, seatLeafId); sealReadStateSeat = sessionSeats !== undefined && seatLeafId !== undefined ? (ownCommittedTailEntryId) => { if (ownCommittedTailEntryId !== undefined) sessionSeats.reanchor(sessionId, readFileState, ownCommittedTailEntryId); } : undefined; const shellReadBoundary = (toolCallId) => { const { read: denySeat, write: writeDenySeat } = denySeatsOf(tables.forCall(toolCallId)); return { roots: [rootCanonical, ...readRoots.forCall(toolCallId)()], ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}), ...denySeat, ...writeDenySeat, ...(input.personRows.unreadFor(toolCallId) ? { unreadPartitions: true } : {}), writeRoots: [rootCanonical, ...additionalRootsCanonical], ...(resolvedWriteFace !== undefined ? { writeFace: resolvedWriteFace.inForce() } : {}), ...((judge) => (judge !== undefined ? { writeTargetJudge: judge } : {}))(input.writeTargetJudge?.()), ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}), }; }; if (handsIncludeShell) { const spoolFence = handsReadOnly ? createShellOverflowSpoolFence(executionEnv) : undefined; const readonlyBoundary = (toolCallId) => { if (spoolFence === undefined) return undefined; const { read: denySeat, write: writeDenySeat } = denySeatsOf(tables.forCall(toolCallId)); return { roots: [rootCanonical, ...readRoots.forCall(toolCallId)()], cwd: rootCanonical, ...denySeat, ...writeDenySeat, ...(input.personRows.unreadFor(toolCallId) ? { unreadPartitions: true } : {}), alsoInside: spoolFence }; }; shellReadTools = new Set(shellFaceHere); shellReadStation = createShellReadStation({ env: executionEnv, tools: shellReadTools, seatOf: () => (handsReadOnly ? "readonly" : "full_shell"), boundaryOf: (_tool, _cwd, toolCallId) => readonlyBoundary(toolCallId) ?? shellReadBoundary(toolCallId), rootsCanonical: true, homeViews: tables.readDeny.homeViews, onAdmissionRefused: (toolCallId) => void input.toolOccurrences.refuse(toolCallId, "read_boundary"), }); } if (!handsReadOnly) { const bgOutputExempt = backgroundOutputReadExemption(defaultTaskRegistry, { taskOwner: hostTaskId, taskScope, ...(sessionId !== undefined ? { sessionId } : {}) }, executionEnv); fileReadStation = createFileReadStation({ env: executionEnv, rootCanonical, roots: readRoots, readDeny: tables.readDeny, ...(liveReadFace !== undefined ? { readFace: liveReadFace } : {}), ...("homeDir" in executionEnv && executionEnv.homeDir !== undefined ? { homeDir: executionEnv.homeDir } : {}), tools: readTargetToolNames(), excluded: (key) => artifactStoreRef.current !== undefined && rootAdmittingArtifactStore(artifactStoreRef.current, [key]) !== undefined, exempt: (key) => bgOutputExempt?.(key) === true, }); } const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, { ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}), readDenyHomes: carryReadFace([], { readDeny: tables.readDeny, writeDeny: tables.writeDeny, readRoots }), readFace: liveReadFace, writeFaceLive: resolvedWriteFace.inForce, reminderMark: input.reminderMark, reminderDisclosureCounts: input.reminderDisclosureCounts, ...(input.modelFileReaderSeat !== undefined ? { modelFileReaderSeat: input.modelFileReaderSeat } : {}), ...(input.trackFileEdit !== undefined ? { trackFileEdit: input.trackFileEdit } : {}), ...(input.onFileEdited !== undefined ? { onFileEdited: input.onFileEdited } : {}), includeShell: handsIncludeShell, readOnly: handsReadOnly, ...(shellReadStation !== undefined ? { shellReadAdmission: shellReadStation.admit } : {}), ...(fileReadStation !== undefined ? { readAdmission: fileReadStation.admitted } : {}), ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}), taskRegistry: defaultTaskRegistry, taskOwner: hostTaskId, taskScope, oneShot: spec.oneShot, ...(sessionId !== undefined ? { sessionId } : {}), retainBackgroundProcesses: spec.retainBackgroundProcesses === true, ...(internals?.onTaskNotification !== undefined ? { taskNotification: internals.onTaskNotification } : {}), ...(internals?.detachHub !== undefined ? { detachHub: internals.detachHub } : {}), onToolProgress: (body) => input.toolProgressRef.current?.emit(body), queuedHumanInput: () => input.harnessRef.current?.hasPendingUserInput() === true, ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}), pdfModelCapabilities: pdfModelCapabilitiesOf(model), ...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}), ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}), ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}), ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}), beforeWrite: async (w) => { const deploymentGate = deps.hands?.beforeWrite; if (deploymentGate) { const verdict = await deploymentGate(w); if (verdict !== undefined && verdict.ok === false) return verdict; } return memoryWriteGate?.(w); }, mountBackgroundTaskTools: false, monitorToolActive: backgroundTaskToolsActive && valveLeaves(spec, toolFaceSnapshot.selector)({ name: "Monitor" }), }); { const bandNames = new Set(band.flatMap((t) => [t.name, ...(t.aliases ?? [])])); const shadowed = (spec.tools ?? []).filter((t) => [t.name, ...(t.aliases ?? [])].some((n) => bandNames.has(n))); if (shadowed.length > 0) { const names = shadowed.map((t) => `"${t.name}"`).join(", "); deps.onError?.(new Error(`spec.tools ${shadowed.length > 1 ? "entries" : "entry"} ${names} collide${shadowed.length > 1 ? "" : "s"} with the built-in hands tool band — the built-in wins (registered later; the harness tool Map is last-write-wins) and the caller tool never dispatches this task. Rename the caller tool, or don't inject executionEnv if you intend to replace the band.`), { phase: "config", sessionId }); } } roster.mountAll(band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(afterCallTables(input.personRows, t)))), { source: "builtin", mountedBy: "hands" }); const shellFloor = effectiveShellGate; if (!(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) { const installShellSeat = (name) => { const tierBefore = irreversibilityTier.get(name); const seatOwned = !egressTools.has(name) && tierBefore !== "always" && tierBefore !== "maybe"; const tier = shellFloor === "always" || tierBefore === "always" ? "always" : "maybe"; irreversibilityTier.set(name, tier); irreversibleTools.add(name); if (seatOwned && tier === "maybe") reversibilityProbes.set(name, bashReversibilityProbe(undefined, shellReadBoundary)); if (seatOwned) shellGatedTools.add(name); }; for (const name of shellFaceHere) installShellSeat(name); } } return { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, shellReadCheck: onPinnedRead(input.personRows, [...(shellReadStation !== undefined ? [{ check: shellReadStation.check, tools: shellReadTools }] : []), ...(fileReadStation !== undefined ? [fileReadStation] : [])]), shellReadStationArmed: shellReadStation !== undefined, shellReadAdmission: shellReadStation?.admit, readAdmission: fileReadStation?.admitted, resolvedReadFace, resolvedWriteFace, handsLessResolvedWriteFace, readDenyAdditionsNormalized, readDenyBuiltinTiersActive, handsLessResolvedFace, additionalRootsCanonical, additionalReadRootsCanonical, grantReadRoot, artifactStoreRef, attachmentRootCanonical, readDenyMatcher, readRoots, pathTables, envHandToolNames, shellGatedTools, sealReadStateSeat, }; }