@sema-agent/core 5.40.0 → 5.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,283 @@
1
+ import { emitTrace } from "../trace.js";
2
+ import { StubExecutionEnv } from "../stub-env.js";
3
+ import { hasBackgroundShell } from "../background-shell.js";
4
+ import { defaultTaskRegistry } from "../task-registry.js";
5
+ import { bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, pdfModelCapabilitiesOf, resolveReadDenyBuiltins, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
6
+ import { resolveKey } from "../../tools/fs/safety.js";
7
+ import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
8
+ import { deliverEngineNotice } from "../types.js";
9
+ import { rebaseWorkspacePath } from "./prepare-workspace-restore.js";
10
+ const readFaceClampAnnouncedSinks = new WeakSet();
11
+ let readFaceClampConsoleAnnounced = false;
12
+ export function __resetReadFaceClampAnnouncement() {
13
+ readFaceClampConsoleAnnounced = false;
14
+ }
15
+ export function resolveHandsLessReadFace(input) {
16
+ const { resume, fullShellReachable, spec, deps } = input;
17
+ let handsLessResolvedFace;
18
+ let readDenyAdditionsNormalized = [];
19
+ {
20
+ const liveFace = resolveReadFace({
21
+ specReadFace: spec.readFace,
22
+ depsReadFace: deps.readFace,
23
+ readOnlyMount: spec.handsReadOnly === true,
24
+ orgGoverned: deps.permissionRuleOrg !== undefined,
25
+ fullShellReachable,
26
+ });
27
+ const seed = resume?.seed.readFace;
28
+ handsLessResolvedFace = resume !== undefined && (seed === undefined || seed.face === "roots") ? "roots" : liveFace;
29
+ const denyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seed?.denyEntries ?? [])];
30
+ resolveReadDenyBuiltins({
31
+ ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: deps.readDenyBuiltinTiers } : {}),
32
+ ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: deps.readDenyBuiltinExclude } : {}),
33
+ });
34
+ if (denyAdditions.length > 0) {
35
+ readDenyAdditionsNormalized = compileReadDeny(denyAdditions, "readDenyPatterns", { tiers: [] }).entries;
36
+ }
37
+ }
38
+ return {
39
+ readFileStateForCheckpoint: undefined,
40
+ seedContextFiles: undefined,
41
+ handsCwdRef: undefined,
42
+ workspaceStateSettle: undefined,
43
+ wsSnapshot: undefined,
44
+ rebaseWsPath: (p) => p,
45
+ backgroundTaskToolsActive: false,
46
+ resolvedReadFace: undefined,
47
+ readDenyAdditionsNormalized,
48
+ handsLessResolvedFace,
49
+ additionalRootsCanonical: [],
50
+ additionalReadRootsCanonical: [],
51
+ attachmentRootCanonical: undefined,
52
+ readDenyMatcher: undefined,
53
+ shellGatedBash: false,
54
+ shellGatedMonitor: false,
55
+ };
56
+ }
57
+ export async function prepareHandsMount(input) {
58
+ const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
59
+ let readFileStateForCheckpoint;
60
+ let seedContextFiles;
61
+ let handsCwdRef;
62
+ let workspaceStateSettle;
63
+ let wsSnapshot;
64
+ let rebaseWsPath = (p) => p;
65
+ let backgroundTaskToolsActive = false;
66
+ let resolvedReadFace;
67
+ let readDenyAdditionsNormalized = [];
68
+ let handsLessResolvedFace;
69
+ let shellGatedBash = false;
70
+ let shellGatedMonitor = false;
71
+ const additionalRootsCanonical = [];
72
+ const additionalReadRootsCanonical = [];
73
+ let attachmentRootCanonical;
74
+ let readDenyMatcher;
75
+ if (handsEnabled) {
76
+ const rootRaw = taskRootFinal;
77
+ const canon = await executionEnv.canonicalPath(rootRaw);
78
+ const rootCanonical = canon.ok ? canon.value : rootRaw;
79
+ attachmentRootCanonical = rootCanonical;
80
+ const canonicalizeExtraDirs = async (dirs, field, sink) => {
81
+ for (const dir of dirs ?? []) {
82
+ if (!dir || !dir.trim())
83
+ continue;
84
+ const c = await executionEnv.canonicalPath(dir);
85
+ if (c.ok) {
86
+ sink.push(c.value);
87
+ }
88
+ else {
89
+ deps.onError?.(new Error(`${field} entry skipped (cannot canonicalize): ${dir}`), {
90
+ phase: "config",
91
+ sessionId,
92
+ });
93
+ emitTrace(deps.tracer, () => ({
94
+ kind: "config.additional_directory_skipped",
95
+ version: 1,
96
+ taskId: hostTaskId,
97
+ entry: dir,
98
+ ...(field !== "additionalDirectories" ? { field } : {}),
99
+ reason: `${c.error.code}: ${c.error.message}`,
100
+ ts: Date.now(),
101
+ }));
102
+ }
103
+ }
104
+ };
105
+ await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
106
+ await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical);
107
+ if (spec.envFacts?.scratchpadDir) {
108
+ let c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
109
+ if (!c.ok && c.error.code === "not_found") {
110
+ const marker = await executionEnv.joinPath([spec.envFacts.scratchpadDir, ".sema-scratchpad"]);
111
+ const exclusiveCreate = executionEnv.writeFileExclusive?.bind(executionEnv);
112
+ if (marker.ok && exclusiveCreate) {
113
+ const w = await exclusiveCreate(marker.value, "");
114
+ if (w.ok || w.error.code === "already_exists")
115
+ c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
116
+ }
117
+ }
118
+ if (c.ok)
119
+ additionalRootsCanonical.push(c.value);
120
+ }
121
+ for (const a of [additionalRootsCanonical, additionalReadRootsCanonical])
122
+ a.splice(0, a.length, ...new Set(a));
123
+ const readFileState = new Map((resume?.seed.readFileState ?? []).map(([k, v]) => [rebaseRestoredPath(k), v]));
124
+ readFileStateForCheckpoint = readFileState;
125
+ seedContextFiles = async (files) => {
126
+ for (const f of files) {
127
+ const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
128
+ if (rk.ok)
129
+ seedReadFileStateFromContext(readFileState, rk.key, f.content);
130
+ }
131
+ };
132
+ const handsIncludeShell = !(executionEnv instanceof StubExecutionEnv);
133
+ const handsReadOnly = spec.handsReadOnly === true;
134
+ backgroundTaskToolsActive = handsIncludeShell && !handsReadOnly && hasBackgroundShell(executionEnv);
135
+ wsSnapshot = resume !== undefined || !(handsIncludeShell && !handsReadOnly) ? undefined : await session.getWorkspaceState().catch(() => undefined);
136
+ rebaseWsPath = (p) => (wsSnapshot === undefined || wsSnapshot.taskRoot === rootCanonical ? p : rebaseWorkspacePath(p, wsSnapshot.taskRoot, rootCanonical));
137
+ if (handsIncludeShell && !handsReadOnly) {
138
+ handsCwdRef = { current: rootCanonical };
139
+ if (resume?.seed.handsCwd !== undefined)
140
+ handsCwdRef.current = rebaseRestoredPath(resume.seed.handsCwd);
141
+ else if (wsSnapshot?.handsCwd !== undefined)
142
+ handsCwdRef.current = rebaseWsPath(wsSnapshot.handsCwd);
143
+ workspaceStateSettle = {
144
+ rootCanonical,
145
+ restoredHandsCwd: handsCwdRef.current !== rootCanonical ? handsCwdRef.current : undefined,
146
+ ...(resume !== undefined ? { baselineUnknown: true } : {}),
147
+ };
148
+ }
149
+ const seedReadFaceSection = resume?.seed.readFace;
150
+ const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
151
+ const readDenyBuiltinCfg = {
152
+ ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: [...deps.readDenyBuiltinTiers] } : {}),
153
+ ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: [...deps.readDenyBuiltinExclude] } : {}),
154
+ };
155
+ readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg);
156
+ readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries;
157
+ let liveReadFace = resolveReadFace({
158
+ specReadFace: spec.readFace,
159
+ depsReadFace: deps.readFace,
160
+ readOnlyMount: handsReadOnly,
161
+ orgGoverned: deps.permissionRuleOrg !== undefined,
162
+ fullShellReachable,
163
+ onDeploymentClamp: () => {
164
+ const sink = deps.onNotice;
165
+ if (typeof sink === "function") {
166
+ if (readFaceClampAnnouncedSinks.has(sink))
167
+ return;
168
+ readFaceClampAnnouncedSinks.add(sink);
169
+ }
170
+ else {
171
+ if (readFaceClampConsoleAnnounced)
172
+ return;
173
+ readFaceClampConsoleAnnounced = true;
174
+ }
175
+ deliverEngineNotice(sink, deploymentReadFaceClampNotice());
176
+ },
177
+ });
178
+ if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots"))
179
+ liveReadFace = "roots";
180
+ resolvedReadFace = liveReadFace;
181
+ if (resume === undefined && spec.sessionId !== undefined) {
182
+ const prior = await session.buildContext().catch(() => undefined);
183
+ for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) {
184
+ const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, liveReadFace);
185
+ if (rk.ok)
186
+ seedReadFileStateFromTranscript(readFileState, rk.key, rec.content, rec.at);
187
+ }
188
+ }
189
+ const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
190
+ ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
191
+ ...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
192
+ ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
193
+ ...(readDenyBuiltinCfg.tiers !== undefined ? { readDenyBuiltinTiers: readDenyBuiltinCfg.tiers } : {}),
194
+ ...(readDenyBuiltinCfg.exclude !== undefined ? { readDenyBuiltinExclude: readDenyBuiltinCfg.exclude } : {}),
195
+ readFace: liveReadFace,
196
+ includeShell: handsIncludeShell,
197
+ readOnly: handsReadOnly,
198
+ ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
199
+ taskRegistry: defaultTaskRegistry,
200
+ taskOwner: hostTaskId,
201
+ taskScope,
202
+ oneShot: spec.oneShot,
203
+ ...(sessionId !== undefined ? { sessionId } : {}),
204
+ ...(internals?.onTaskNotification !== undefined ? { taskNotification: internals.onTaskNotification } : {}),
205
+ ...(internals?.detachHub !== undefined ? { detachHub: internals.detachHub } : {}),
206
+ ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
207
+ pdfModelCapabilities: pdfModelCapabilitiesOf(model),
208
+ ...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}),
209
+ ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
210
+ ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
211
+ ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
212
+ ...(deps.hands?.readCyberReminder !== undefined ? { readCyberReminder: deps.hands.readCyberReminder } : {}),
213
+ beforeWrite: async (w) => {
214
+ const deploymentGate = deps.hands?.beforeWrite;
215
+ if (deploymentGate) {
216
+ const verdict = await deploymentGate(w);
217
+ if (verdict !== undefined && verdict.ok === false)
218
+ return verdict;
219
+ }
220
+ return memoryWriteGateRef.current?.(w);
221
+ },
222
+ mountBackgroundTaskTools: false,
223
+ monitorToolActive: backgroundTaskToolsActive && !(toolFaceSnapshot.exclude?.includes("Monitor") ?? false),
224
+ });
225
+ {
226
+ const bandNames = new Set(band.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
227
+ const shadowed = (spec.tools ?? []).filter((t) => [t.name, ...(t.aliases ?? [])].some((n) => bandNames.has(n)));
228
+ if (shadowed.length > 0) {
229
+ const names = shadowed.map((t) => `"${t.name}"`).join(", ");
230
+ 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 });
231
+ }
232
+ }
233
+ tools.push(...band.map((t) => firstPartyOffload(t)));
234
+ const shellGate = effectiveShellGate;
235
+ if (shellGate === "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
236
+ deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — no shell safety-axis ` +
237
+ `fold applies to this run (commands are adjudicated by the ordinary policy/hook chain only). ` +
238
+ `Set spec.shellGate to "classify" or "always" if this deployment expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
239
+ }
240
+ if (shellGate !== "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
241
+ const bashTierBefore = irreversibilityTier.get("Bash");
242
+ shellGatedBash = !egressTools.has("Bash") && bashTierBefore !== "always" && bashTierBefore !== "maybe";
243
+ const bashEffectiveTier = shellGate === "always" || bashTierBefore === "always" ? "always" : "maybe";
244
+ irreversibilityTier.set("Bash", bashEffectiveTier);
245
+ irreversibleTools.add("Bash");
246
+ const shellReadBoundary = () => ({
247
+ roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
248
+ ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
249
+ denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
250
+ ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
251
+ });
252
+ if (shellGate === "classify" && shellGatedBash)
253
+ reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
254
+ if (backgroundTaskToolsActive) {
255
+ const monitorTierBefore = irreversibilityTier.get("Monitor");
256
+ shellGatedMonitor = !egressTools.has("Monitor") && monitorTierBefore !== "always" && monitorTierBefore !== "maybe";
257
+ const monitorEffectiveTier = shellGate === "always" || monitorTierBefore === "always" ? "always" : "maybe";
258
+ irreversibilityTier.set("Monitor", monitorEffectiveTier);
259
+ irreversibleTools.add("Monitor");
260
+ if (shellGate === "classify" && shellGatedMonitor)
261
+ reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
262
+ }
263
+ }
264
+ }
265
+ return {
266
+ readFileStateForCheckpoint,
267
+ seedContextFiles,
268
+ handsCwdRef,
269
+ workspaceStateSettle,
270
+ wsSnapshot,
271
+ rebaseWsPath,
272
+ backgroundTaskToolsActive,
273
+ resolvedReadFace,
274
+ readDenyAdditionsNormalized,
275
+ handsLessResolvedFace,
276
+ additionalRootsCanonical,
277
+ additionalReadRootsCanonical,
278
+ attachmentRootCanonical,
279
+ readDenyMatcher,
280
+ shellGatedBash,
281
+ shellGatedMonitor,
282
+ };
283
+ }
@@ -37,11 +37,7 @@ export declare function __resetMaterializeEnvAnnouncements(): void;
37
37
  * seam below (console latch only): tests here legitimately reuse ONE sink across prepares to pin
38
38
  * the per-sink dedup itself, so the seam must be able to re-arm a still-referenced sink. */
39
39
  export declare function __resetToolModelGateAnnouncements(): void;
40
- /** Test seam (mirrors `__resetMalformedNoticeSeatAnnouncement`): never called by production code.
41
- * Deliberately asymmetric — it resets only the console latch: the WeakSet arm needs no seam
42
- * because a test resets it by minting a fresh sink function (identity IS the ledger key), while
43
- * the console arm's key is the process itself, which only this seam can refresh. */
44
- export declare function __resetReadFaceClampAnnouncement(): void;
40
+ export { __resetReadFaceClampAnnouncement } from "./prepare-hands-readface.js";
45
41
  /**
46
42
  * design/164 件四 — how long before an execution environment's declared `lifetimeMs` expires the engine
47
43
  * stops the run and checkpoints it. The margin has to cover ONE suspend: pausing/snapshotting the
@@ -56,7 +56,8 @@ import { prepareMemory } from "./prepare-memory.js";
56
56
  import { limitConfigError, prepareConfigDoors } from "./prepare-config-doors.js";
57
57
  import { NAMESPACED_NAME_SHAPES, prepareSafetyScan } from "./prepare-safety-scan.js";
58
58
  import { prepareAcquireReconcile } from "./prepare-acquire-reconcile.js";
59
- import { prepareWorkspaceRestore, rebaseWorkspacePath, remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./prepare-workspace-restore.js";
59
+ import { prepareHandsMount, resolveHandsLessReadFace } from "./prepare-hands-readface.js";
60
+ import { prepareWorkspaceRestore, remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./prepare-workspace-restore.js";
60
61
  import { defaultPromptProvider, buildEnvironmentContext, formatLocalDate, isValidTimeZone, PROJECT_CONTEXT_FRAMING } from "../../prompts/default.js";
61
62
  import { applyGitFrameGuard, probeGitStatusLane } from "./git-status-frame.js";
62
63
  import { assemblePrompt } from "../../prompt-assembly/assemble.js";
@@ -76,7 +77,7 @@ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.j
76
77
  import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
77
78
  import { createMonitorTool } from "../../tools/monitor.js";
78
79
  import { createWorktreeTools } from "../../tools/worktree.js";
79
- import { applyCompactionToReadFileState, bashReversibilityProbe, compileReadDeny, createHandsToolkit, deploymentReadFaceClampNotice, isReadDedupStubResult, resolveReadDenyBuiltins, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
80
+ import { applyCompactionToReadFileState, isReadDedupStubResult, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS } from "../../tools/fs/index.js";
80
81
  import { decodeTextBytes } from "../../tools/fs/encoding.js";
81
82
  import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool, classifyQuestionOutcome, isLiveQuestionFace, validateAskQuestions, } from "../ask-question.js";
82
83
  import { createSchedulerTools } from "../../tools/scheduler-tools.js";
@@ -86,8 +87,7 @@ import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestrati
86
87
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
87
88
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
88
89
  import { resolveKey } from "../../tools/fs/safety.js";
89
- import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
90
- import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
90
+ import { BINDING_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
91
91
  import { boundInputHashOf } from "../canonical-json.js";
92
92
  import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
93
93
  import { durableParkGapFor } from "../park-selfcheck.js";
@@ -191,11 +191,7 @@ function announceToolModelGate(onNotice, modelId, gate) {
191
191
  }
192
192
  }
193
193
  }
194
- const readFaceClampAnnouncedSinks = new WeakSet();
195
- let readFaceClampConsoleAnnounced = false;
196
- export function __resetReadFaceClampAnnouncement() {
197
- readFaceClampConsoleAnnounced = false;
198
- }
194
+ export { __resetReadFaceClampAnnouncement } from "./prepare-hands-readface.js";
199
195
  const DEFAULT_MAX_SUSPENDS = 5;
200
196
  const ULTRA_REASONING_TIERS = new Set(["xhigh", "max"]);
201
197
  const DEFAULT_RESOURCE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
@@ -1036,14 +1032,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1036
1032
  resourceSpentMicroUsd: priorLedger?.spentMicroUsd ?? 0,
1037
1033
  suspendCount: priorSuspendCount,
1038
1034
  };
1039
- let readFileStateForCheckpoint;
1040
- let seedContextFiles;
1041
- let handsCwdRef;
1042
- let workspaceStateSettle;
1043
- let wsSnapshot;
1044
- let rebaseWsPath = (p) => p;
1045
1035
  let worktreeSessionRef;
1046
- let backgroundTaskToolsActive = false;
1047
1036
  let suspendForResource;
1048
1037
  let suspendForPlatformLimit;
1049
1038
  let suspendForReview;
@@ -1209,219 +1198,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1209
1198
  foldProtocolAxes(a2a.toolAxes, "A2A");
1210
1199
  const callIssuedAtRef = {};
1211
1200
  const memoryWriteGateRef = {};
1212
- const additionalRootsCanonical = [];
1213
- const additionalReadRootsCanonical = [];
1214
- let attachmentRootCanonical;
1215
- let readDenyMatcher;
1216
- if (handsEnabled) {
1217
- const rootRaw = taskRootFinal;
1218
- const canon = await executionEnv.canonicalPath(rootRaw);
1219
- const rootCanonical = canon.ok ? canon.value : rootRaw;
1220
- attachmentRootCanonical = rootCanonical;
1221
- const canonicalizeExtraDirs = async (dirs, field, sink) => {
1222
- for (const dir of dirs ?? []) {
1223
- if (!dir || !dir.trim())
1224
- continue;
1225
- const c = await executionEnv.canonicalPath(dir);
1226
- if (c.ok) {
1227
- sink.push(c.value);
1228
- }
1229
- else {
1230
- deps.onError?.(new Error(`${field} entry skipped (cannot canonicalize): ${dir}`), {
1231
- phase: "config",
1232
- sessionId,
1233
- });
1234
- emitTrace(deps.tracer, () => ({
1235
- kind: "config.additional_directory_skipped",
1236
- version: 1,
1237
- taskId: hostTaskId,
1238
- entry: dir,
1239
- ...(field !== "additionalDirectories" ? { field } : {}),
1240
- reason: `${c.error.code}: ${c.error.message}`,
1241
- ts: Date.now(),
1242
- }));
1243
- }
1244
- }
1245
- };
1246
- await canonicalizeExtraDirs(spec.additionalDirectories, "additionalDirectories", additionalRootsCanonical);
1247
- await canonicalizeExtraDirs(spec.additionalReadDirectories, "additionalReadDirectories", additionalReadRootsCanonical);
1248
- if (spec.envFacts?.scratchpadDir) {
1249
- let c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
1250
- if (!c.ok && c.error.code === "not_found") {
1251
- const marker = await executionEnv.joinPath([spec.envFacts.scratchpadDir, ".sema-scratchpad"]);
1252
- const exclusiveCreate = executionEnv.writeFileExclusive?.bind(executionEnv);
1253
- if (marker.ok && exclusiveCreate) {
1254
- const w = await exclusiveCreate(marker.value, "");
1255
- if (w.ok || w.error.code === "already_exists")
1256
- c = await executionEnv.canonicalPath(spec.envFacts.scratchpadDir);
1257
- }
1258
- }
1259
- if (c.ok)
1260
- additionalRootsCanonical.push(c.value);
1261
- }
1262
- for (const a of [additionalRootsCanonical, additionalReadRootsCanonical])
1263
- a.splice(0, a.length, ...new Set(a));
1264
- const readFileState = new Map((resume?.seed.readFileState ?? []).map(([k, v]) => [rebaseRestoredPath(k), v]));
1265
- readFileStateForCheckpoint = readFileState;
1266
- seedContextFiles = async (files) => {
1267
- for (const f of files) {
1268
- const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
1269
- if (rk.ok)
1270
- seedReadFileStateFromContext(readFileState, rk.key, f.content);
1271
- }
1272
- };
1273
- const handsIncludeShell = !(executionEnv instanceof StubExecutionEnv);
1274
- const handsReadOnly = spec.handsReadOnly === true;
1275
- backgroundTaskToolsActive = handsIncludeShell && !handsReadOnly && hasBackgroundShell(executionEnv);
1276
- wsSnapshot = resume !== undefined || !(handsIncludeShell && !handsReadOnly) ? undefined : await session.getWorkspaceState().catch(() => undefined);
1277
- rebaseWsPath = (p) => (wsSnapshot === undefined || wsSnapshot.taskRoot === rootCanonical ? p : rebaseWorkspacePath(p, wsSnapshot.taskRoot, rootCanonical));
1278
- if (handsIncludeShell && !handsReadOnly) {
1279
- handsCwdRef = { current: rootCanonical };
1280
- if (resume?.seed.handsCwd !== undefined)
1281
- handsCwdRef.current = rebaseRestoredPath(resume.seed.handsCwd);
1282
- else if (wsSnapshot?.handsCwd !== undefined)
1283
- handsCwdRef.current = rebaseWsPath(wsSnapshot.handsCwd);
1284
- workspaceStateSettle = {
1285
- rootCanonical,
1286
- restoredHandsCwd: handsCwdRef.current !== rootCanonical ? handsCwdRef.current : undefined,
1287
- ...(resume !== undefined ? { baselineUnknown: true } : {}),
1288
- };
1289
- }
1290
- const seedReadFaceSection = resume?.seed.readFace;
1291
- const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
1292
- const readDenyBuiltinCfg = {
1293
- ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: [...deps.readDenyBuiltinTiers] } : {}),
1294
- ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: [...deps.readDenyBuiltinExclude] } : {}),
1295
- };
1296
- readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns", readDenyBuiltinCfg);
1297
- readDenyAdditionsNormalized = compileReadDeny(readDenyAdditions, "readDenyPatterns", { tiers: [] }).entries;
1298
- let liveReadFace = resolveReadFace({
1299
- specReadFace: spec.readFace,
1300
- depsReadFace: deps.readFace,
1301
- readOnlyMount: handsReadOnly,
1302
- orgGoverned: deps.permissionRuleOrg !== undefined,
1303
- fullShellReachable,
1304
- onDeploymentClamp: () => {
1305
- const sink = deps.onNotice;
1306
- if (typeof sink === "function") {
1307
- if (readFaceClampAnnouncedSinks.has(sink))
1308
- return;
1309
- readFaceClampAnnouncedSinks.add(sink);
1310
- }
1311
- else {
1312
- if (readFaceClampConsoleAnnounced)
1313
- return;
1314
- readFaceClampConsoleAnnounced = true;
1315
- }
1316
- deliverEngineNotice(sink, deploymentReadFaceClampNotice());
1317
- },
1318
- });
1319
- if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots"))
1320
- liveReadFace = "roots";
1321
- resolvedReadFace = liveReadFace;
1322
- if (resume === undefined && spec.sessionId !== undefined) {
1323
- const prior = await session.buildContext().catch(() => undefined);
1324
- for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) {
1325
- const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, liveReadFace);
1326
- if (rk.ok)
1327
- seedReadFileStateFromTranscript(readFileState, rk.key, rec.content, rec.at);
1328
- }
1329
- }
1330
- const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
1331
- ...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
1332
- ...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
1333
- ...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
1334
- ...(readDenyBuiltinCfg.tiers !== undefined ? { readDenyBuiltinTiers: readDenyBuiltinCfg.tiers } : {}),
1335
- ...(readDenyBuiltinCfg.exclude !== undefined ? { readDenyBuiltinExclude: readDenyBuiltinCfg.exclude } : {}),
1336
- readFace: liveReadFace,
1337
- includeShell: handsIncludeShell,
1338
- readOnly: handsReadOnly,
1339
- ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
1340
- taskRegistry: defaultTaskRegistry,
1341
- taskOwner: hostTaskId,
1342
- taskScope,
1343
- oneShot: spec.oneShot,
1344
- ...(sessionId !== undefined ? { sessionId } : {}),
1345
- ...(internals?.onTaskNotification !== undefined ? { taskNotification: internals.onTaskNotification } : {}),
1346
- ...(internals?.detachHub !== undefined ? { detachHub: internals.detachHub } : {}),
1347
- ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
1348
- pdfModelCapabilities: pdfModelCapabilitiesOf(model),
1349
- ...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}),
1350
- ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
1351
- ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
1352
- ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
1353
- ...(deps.hands?.readCyberReminder !== undefined ? { readCyberReminder: deps.hands.readCyberReminder } : {}),
1354
- beforeWrite: async (w) => {
1355
- const deploymentGate = deps.hands?.beforeWrite;
1356
- if (deploymentGate) {
1357
- const verdict = await deploymentGate(w);
1358
- if (verdict !== undefined && verdict.ok === false)
1359
- return verdict;
1360
- }
1361
- return memoryWriteGateRef.current?.(w);
1362
- },
1363
- mountBackgroundTaskTools: false,
1364
- monitorToolActive: backgroundTaskToolsActive && !(toolFaceSnapshot.exclude?.includes("Monitor") ?? false),
1365
- });
1366
- {
1367
- const bandNames = new Set(band.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
1368
- const shadowed = (spec.tools ?? []).filter((t) => [t.name, ...(t.aliases ?? [])].some((n) => bandNames.has(n)));
1369
- if (shadowed.length > 0) {
1370
- const names = shadowed.map((t) => `"${t.name}"`).join(", ");
1371
- 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 });
1372
- }
1373
- }
1374
- tools.push(...band.map((t) => firstPartyOffload(t)));
1375
- const shellGate = effectiveShellGate;
1376
- if (shellGate === "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
1377
- deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — no shell safety-axis ` +
1378
- `fold applies to this run (commands are adjudicated by the ordinary policy/hook chain only). ` +
1379
- `Set spec.shellGate to "classify" or "always" if this deployment expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
1380
- }
1381
- if (shellGate !== "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
1382
- const bashTierBefore = irreversibilityTier.get("Bash");
1383
- shellGatedBash = !egressTools.has("Bash") && bashTierBefore !== "always" && bashTierBefore !== "maybe";
1384
- const bashEffectiveTier = shellGate === "always" || bashTierBefore === "always" ? "always" : "maybe";
1385
- irreversibilityTier.set("Bash", bashEffectiveTier);
1386
- irreversibleTools.add("Bash");
1387
- const shellReadBoundary = () => ({
1388
- roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
1389
- ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
1390
- denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
1391
- ...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
1392
- });
1393
- if (shellGate === "classify" && shellGatedBash)
1394
- reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
1395
- if (backgroundTaskToolsActive) {
1396
- const monitorTierBefore = irreversibilityTier.get("Monitor");
1397
- shellGatedMonitor = !egressTools.has("Monitor") && monitorTierBefore !== "always" && monitorTierBefore !== "maybe";
1398
- const monitorEffectiveTier = shellGate === "always" || monitorTierBefore === "always" ? "always" : "maybe";
1399
- irreversibilityTier.set("Monitor", monitorEffectiveTier);
1400
- irreversibleTools.add("Monitor");
1401
- if (shellGate === "classify" && shellGatedMonitor)
1402
- reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
1403
- }
1404
- }
1405
- }
1406
- else {
1407
- const liveFace = resolveReadFace({
1408
- specReadFace: spec.readFace,
1409
- depsReadFace: deps.readFace,
1410
- readOnlyMount: spec.handsReadOnly === true,
1411
- orgGoverned: deps.permissionRuleOrg !== undefined,
1412
- fullShellReachable,
1413
- });
1414
- const seed = resume?.seed.readFace;
1415
- handsLessResolvedFace = resume !== undefined && (seed === undefined || seed.face === "roots") ? "roots" : liveFace;
1416
- const denyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seed?.denyEntries ?? [])];
1417
- resolveReadDenyBuiltins({
1418
- ...(deps.readDenyBuiltinTiers !== undefined ? { tiers: deps.readDenyBuiltinTiers } : {}),
1419
- ...(deps.readDenyBuiltinExclude !== undefined ? { exclude: deps.readDenyBuiltinExclude } : {}),
1420
- });
1421
- if (denyAdditions.length > 0) {
1422
- readDenyAdditionsNormalized = compileReadDeny(denyAdditions, "readDenyPatterns", { tiers: [] }).entries;
1423
- }
1424
- }
1201
+ const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes };
1202
+ const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
1203
+ const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher } = handsReadFace;
1204
+ resolvedReadFace = handsReadFace.resolvedReadFace;
1205
+ readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
1206
+ handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
1207
+ shellGatedBash = handsReadFace.shellGatedBash;
1208
+ shellGatedMonitor = handsReadFace.shellGatedMonitor;
1425
1209
  if (backgroundTaskToolsActive || workflowToolsActive) {
1426
1210
  toolEffects.set("TaskOutput", "read");
1427
1211
  toolEffects.set("TaskStop", "write");
@@ -3821,6 +3605,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3821
3605
  const mintedAt = Date.now();
3822
3606
  const cp = {
3823
3607
  token,
3608
+ checkpointId: mintCheckpointId(),
3824
3609
  version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
3825
3610
  scope,
3826
3611
  sessionId,
@@ -3903,6 +3688,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3903
3688
  const mintedAt = Date.now();
3904
3689
  const cp = {
3905
3690
  token,
3691
+ checkpointId: mintCheckpointId(),
3906
3692
  version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
3907
3693
  scope,
3908
3694
  sessionId,
@@ -4164,6 +3950,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4164
3950
  const mintedAt = Date.now();
4165
3951
  cp = {
4166
3952
  token,
3953
+ checkpointId: mintCheckpointId(),
4167
3954
  version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
4168
3955
  scope,
4169
3956
  sessionId,
@@ -1,6 +1,6 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
- import { deliverDelegationLifecycle } from "../types.js";
3
+ import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
4
4
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
5
5
  import { snapshotActorAssertion } from "../../internal/llm.js";
6
6
  import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
@@ -2068,6 +2068,11 @@ export class Runner {
2068
2068
  for (const p of payloads)
2069
2069
  this.pendingSessionNotifications.pend(notificationSessionId, p);
2070
2070
  };
2071
+ prepared.harness.onUndrainedUserInputs = (counts) => {
2072
+ for (const notice of undrainedUserInputNotices(counts, spec.taskId)) {
2073
+ deliverEngineNotice(this.deps.onNotice, notice);
2074
+ }
2075
+ };
2071
2076
  prepared.harness.onEngineNoteConsumed = (p) => {
2072
2077
  const peer = p?.peer;
2073
2078
  if (peer !== undefined && Array.isArray(peer.hopChain))