@the-open-engine/zeroshot 6.9.1 → 6.10.1

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.
Files changed (65) hide show
  1. package/cli/commands/inspect.js +1 -0
  2. package/cli/index.js +17 -1
  3. package/cluster-hooks/block-ask-user-question.py +2 -3
  4. package/cluster-hooks/block-dangerous-git.py +2 -3
  5. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  6. package/lib/agent-cli-provider/adapters/claude.js +57 -3
  7. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  8. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/codex.js +32 -2
  10. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  11. package/lib/agent-cli-provider/adapters/common.js +1 -1
  12. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  13. package/lib/agent-cli-provider/adapters/index.d.ts +1 -0
  14. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  15. package/lib/agent-cli-provider/adapters/index.js +8 -0
  16. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  17. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  18. package/lib/agent-cli-provider/contract-options.js +3 -0
  19. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  20. package/lib/agent-cli-provider/index.d.ts +1 -1
  21. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  22. package/lib/agent-cli-provider/index.js +2 -1
  23. package/lib/agent-cli-provider/index.js.map +1 -1
  24. package/lib/agent-cli-provider/provider-registry.d.ts +9 -0
  25. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  26. package/lib/agent-cli-provider/provider-registry.js +3 -0
  27. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  28. package/lib/agent-cli-provider/types.d.ts +10 -2
  29. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  30. package/lib/agent-cli-provider/types.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/agent/agent-context-builder.js +90 -4
  33. package/src/agent/agent-context-sources.js +20 -2
  34. package/src/agent/agent-lifecycle.js +43 -8
  35. package/src/agent/agent-task-executor.js +557 -314
  36. package/src/agent/guidance-queue.js +29 -7
  37. package/src/agent/provider-session.js +277 -0
  38. package/src/agent-cli-provider/adapters/claude.ts +64 -4
  39. package/src/agent-cli-provider/adapters/codex.ts +47 -3
  40. package/src/agent-cli-provider/adapters/common.ts +1 -1
  41. package/src/agent-cli-provider/adapters/index.ts +10 -0
  42. package/src/agent-cli-provider/contract-options.ts +7 -0
  43. package/src/agent-cli-provider/index.ts +1 -0
  44. package/src/agent-cli-provider/provider-registry.ts +13 -1
  45. package/src/agent-cli-provider/types.ts +13 -6
  46. package/src/agent-wrapper.js +44 -8
  47. package/src/claude-task-runner.js +153 -44
  48. package/src/ledger-sequence.js +63 -0
  49. package/src/ledger.js +114 -29
  50. package/src/orchestrator.js +40 -2
  51. package/src/task-spawn-cleanup-ownership.js +82 -0
  52. package/src/worktree-claude-config.js +193 -85
  53. package/task-lib/attachable-watcher.js +101 -253
  54. package/task-lib/command-spec-cleanup.js +219 -0
  55. package/task-lib/commands/clean.js +35 -5
  56. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  57. package/task-lib/commands/kill.js +117 -4
  58. package/task-lib/commands/resume.js +29 -6
  59. package/task-lib/commands/status.js +4 -0
  60. package/task-lib/provider-helper-runtime.js +1 -0
  61. package/task-lib/provider-session-capture.js +138 -0
  62. package/task-lib/runner.js +70 -5
  63. package/task-lib/store.js +126 -12
  64. package/task-lib/watcher-output-runtime.js +284 -0
  65. package/task-lib/watcher.js +131 -242
@@ -5,15 +5,11 @@
5
5
  * Runs detached from parent, provides Unix socket for attach clients.
6
6
  */
7
7
 
8
- import { appendFileSync, unlinkSync } from 'fs';
9
- import { unlink } from 'fs/promises';
10
- import { updateTask } from './store.js';
11
- import {
12
- detectProviderFatalError,
13
- detectProviderStreamingModeError,
14
- recoverProviderStructuredOutput,
15
- supportsProviderStructuredOutputRecovery,
16
- } from './provider-helper-runtime.js';
8
+ import { appendFileSync } from 'fs';
9
+ import { getTask, updateTask } from './store.js';
10
+ import { createCommandSpecCleanup } from './command-spec-cleanup.js';
11
+ import { createProviderSessionCapture } from './provider-session-capture.js';
12
+ import * as watcherOutputRuntime from './watcher-output-runtime.js';
17
13
  import { createRequire } from 'module';
18
14
 
19
15
  // ═══════════════════════════════════════════════════════════════════════════
@@ -22,8 +18,9 @@ import { createRequire } from 'module';
22
18
  // ═══════════════════════════════════════════════════════════════════════════
23
19
 
24
20
  const [, , taskIdArg, cwdArg, logFileArg, argsJsonArg, configJsonArg] = process.argv;
25
- let commandSpecCleanup = [];
26
- let cleanupStarted = false;
21
+ let commandCleanup = watcherOutputRuntime.COMMAND_CLEANUP_UNINITIALIZED;
22
+ let crashStarted = false;
23
+ let server = null;
27
24
 
28
25
  function emergencyLog(msg) {
29
26
  if (logFileArg) {
@@ -37,37 +34,47 @@ function emergencyLog(msg) {
37
34
  }
38
35
  }
39
36
 
40
- function crashWithError(error, source) {
41
- const timestamp = Date.now();
42
- const errorMsg = error instanceof Error ? error.stack || error.message : String(error);
43
-
44
- emergencyLog(`\n[${timestamp}][CRASH] ${source}: ${errorMsg}\n`);
45
- emergencyLog(`[${timestamp}][CRASH] Process terminating due to unhandled error\n`);
46
- cleanupCommandSpecSync();
37
+ function stopAttachableProvider(exitObserved = false) {
38
+ return watcherOutputRuntime.terminateWatcherProvider(server, { exitObserved });
39
+ }
47
40
 
41
+ async function failWatcher(error, source) {
42
+ if (crashStarted) return;
43
+ crashStarted = true;
48
44
  if (taskIdArg) {
49
- try {
50
- updateTask(taskIdArg, {
51
- status: 'failed',
52
- error: `${source}: ${errorMsg}`,
53
- socketPath: null,
54
- });
55
- } catch (updateError) {
56
- emergencyLog(`[${timestamp}][CRASH] Failed to update task status: ${updateError.message}\n`);
57
- }
45
+ await watcherOutputRuntime.completeWatcherFailure({
46
+ taskId: taskIdArg,
47
+ error,
48
+ source,
49
+ commandCleanup,
50
+ terminateProvider: stopAttachableProvider,
51
+ updateTask,
52
+ emergencyLog,
53
+ terminalUpdates: { socketPath: null },
54
+ });
58
55
  }
59
56
 
60
57
  process.exit(1);
61
58
  }
62
59
 
63
60
  process.on('uncaughtException', (error) => {
64
- crashWithError(error, 'uncaughtException');
61
+ void failWatcher(error, 'uncaughtException');
65
62
  });
66
63
 
67
64
  process.on('unhandledRejection', (reason) => {
68
- crashWithError(reason, 'unhandledRejection');
65
+ void failWatcher(reason, 'unhandledRejection');
69
66
  });
70
67
 
68
+ const persistedTask = taskIdArg ? getTask(taskIdArg) : null;
69
+ if (persistedTask) {
70
+ commandCleanup = createCommandSpecCleanup(
71
+ persistedTask.commandCleanup || { cleanup: [], cleanupMetadata: [] },
72
+ (cleanupPath, error) => {
73
+ emergencyLog(`[${Date.now()}][CLEANUP] Failed to delete ${cleanupPath}: ${error.message}\n`);
74
+ }
75
+ );
76
+ }
77
+
71
78
  const require = createRequire(import.meta.url);
72
79
  const { AttachServer } = require('../src/attach');
73
80
  const { getTaskSocketPath } = require('../src/attach/socket-paths');
@@ -84,188 +91,57 @@ const commandSpec = config.commandSpec || {
84
91
  env: config.env || {},
85
92
  cleanup: [],
86
93
  };
87
- commandSpecCleanup = commandSpec.cleanup || [];
88
- let server = null;
89
-
90
94
  const socketPath = getTaskSocketPath(taskId);
91
95
 
92
96
  function log(msg) {
93
97
  appendFileSync(logFile, msg);
94
98
  }
95
99
 
96
- const providerName = normalizeProviderName(config.provider || 'claude');
97
- const enableRecovery = supportsProviderStructuredOutputRecovery(providerName);
98
-
99
- const env = { ...process.env, ...(commandSpec.env || {}) };
100
- const command = commandSpec.binary;
101
- const finalArgs = [...(commandSpec.args || args)];
102
-
103
- const silentJsonMode =
104
- config.outputFormat === 'json' &&
105
- config.jsonSchema &&
106
- config.silentJsonOutput &&
107
- supportsProviderStructuredOutputRecovery(providerName);
108
-
109
- let finalResultJson = null;
100
+ const { providerName, env, command, finalArgs } = watcherOutputRuntime.resolveWatcherCommand(
101
+ config,
102
+ commandSpec,
103
+ args,
104
+ normalizeProviderName
105
+ );
106
+ const providerSessionCapture = createProviderSessionCapture({
107
+ providerName,
108
+ taskId,
109
+ updateTask,
110
+ log,
111
+ requestedSessionId: persistedTask?.requestedResumeSessionId || null,
112
+ initialSessionId: persistedTask?.sessionId || null,
113
+ initialSessionIdConflict: persistedTask?.sessionIdConflict === true,
114
+ });
110
115
  let outputBuffer = '';
111
- let streamingModeError = null;
112
- let fatalError = null;
113
-
114
- function splitBufferLines(buffer, chunk) {
115
- const nextBuffer = buffer + chunk;
116
- const lines = nextBuffer.split('\n');
117
- const remaining = lines.pop() || '';
118
- return { lines, remaining };
119
- }
120
-
121
- function maybeHandleFatalError(line, timestamp) {
122
- if (fatalError) {
123
- return false;
124
- }
125
-
126
- const detected = detectProviderFatalError(providerName, line);
127
- if (!detected) {
128
- return false;
129
- }
130
-
131
- fatalError = detected;
132
-
133
- if (silentJsonMode) {
134
- log(`[${timestamp}]${line}\n`);
135
- }
136
- log(`[${timestamp}][FATAL] ${detected}\n`);
137
116
 
117
+ function stopProviderAfterFatalOutput(timestamp) {
138
118
  if (server) {
139
119
  server.stop('SIGTERM').catch((error) => {
140
120
  log(`[${timestamp}][FATAL] Attach server stop failed: ${error.message}\n`);
141
121
  });
142
122
  }
143
- return true;
144
- }
145
-
146
- function captureStreamingError(line, timestamp) {
147
- const detectedError = detectProviderStreamingModeError(providerName, line);
148
- if (!detectedError) {
149
- return false;
150
- }
151
-
152
- streamingModeError = { ...detectedError, timestamp };
153
- return true;
154
- }
155
-
156
- function maybeCaptureStructuredOutput(line) {
157
- try {
158
- const json = JSON.parse(line);
159
- if (json.structured_output) {
160
- finalResultJson = line;
161
- }
162
- } catch {
163
- // Not JSON, skip
164
- }
165
123
  }
166
124
 
167
- function handleSilentJsonLines(lines, timestamp) {
168
- for (const line of lines) {
169
- if (!line.trim()) continue;
170
- maybeHandleFatalError(line, timestamp);
171
- if (captureStreamingError(line, timestamp)) {
172
- continue;
173
- }
174
- maybeCaptureStructuredOutput(line);
175
- }
176
- }
177
-
178
- function handleStreamingLines(lines, timestamp) {
179
- for (const line of lines) {
180
- maybeHandleFatalError(line, timestamp);
181
- if (captureStreamingError(line, timestamp)) {
182
- continue;
183
- }
184
- log(`[${timestamp}]${line}\n`);
185
- }
186
- }
187
-
188
- function flushOutputBuffer(timestamp) {
189
- if (!outputBuffer.trim()) {
190
- return;
191
- }
192
-
193
- if (!enableRecovery) {
194
- if (!silentJsonMode) {
195
- log(`[${timestamp}]${outputBuffer}\n`);
196
- }
197
- return;
198
- }
199
-
200
- maybeHandleFatalError(outputBuffer, timestamp);
201
- if (captureStreamingError(outputBuffer, timestamp)) {
202
- return;
203
- }
204
-
205
- if (silentJsonMode) {
206
- maybeCaptureStructuredOutput(outputBuffer);
207
- return;
208
- }
209
-
210
- log(`[${timestamp}]${outputBuffer}\n`);
211
- }
212
-
213
- function attemptRecovery(code, timestamp) {
214
- if (!(code !== 0 && streamingModeError?.sessionId)) {
215
- return null;
216
- }
217
-
218
- const recovered = recoverProviderStructuredOutput(providerName, streamingModeError.sessionId);
219
- if (recovered?.payload) {
220
- const recoveredLine = JSON.stringify(recovered.payload);
221
- if (silentJsonMode) {
222
- finalResultJson = recoveredLine;
223
- } else {
224
- log(`[${timestamp}]${recoveredLine}\n`);
225
- }
226
- } else if (streamingModeError.line) {
227
- if (silentJsonMode) {
228
- log(streamingModeError.line + '\n');
229
- } else {
230
- log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
231
- }
232
- }
233
-
234
- return recovered;
235
- }
236
-
237
- async function cleanupCommandSpec() {
238
- if (cleanupStarted) return;
239
- cleanupStarted = true;
240
- for (const file of commandSpecCleanup) {
241
- try {
242
- await unlink(file);
243
- } catch (error) {
244
- log(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
245
- }
246
- }
247
- }
248
-
249
- function cleanupCommandSpecSync() {
250
- if (cleanupStarted) return;
251
- cleanupStarted = true;
252
- for (const file of commandSpecCleanup) {
253
- try {
254
- unlinkSync(file);
255
- } catch (error) {
256
- emergencyLog(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
257
- }
258
- }
259
- }
260
-
261
- function writeCompletionFooter(code, signal) {
262
- if (config.outputFormat === 'json') {
263
- return;
264
- }
125
+ const outputRuntime = watcherOutputRuntime.createWatcherOutputRuntime({
126
+ config,
127
+ providerName,
128
+ log,
129
+ stopProvider: stopProviderAfterFatalOutput,
130
+ providerSessionCapture,
131
+ });
265
132
 
266
- log(`\n${'='.repeat(50)}\n`);
267
- log(`Finished: ${new Date().toISOString()}\n`);
268
- log(`Exit code: ${code}, Signal: ${signal}\n`);
133
+ if (
134
+ await watcherOutputRuntime.completePendingWatcherCancellation({
135
+ taskId,
136
+ getTask,
137
+ commandCleanup,
138
+ terminateProvider: () => true,
139
+ updateTask,
140
+ emergencyLog,
141
+ terminalUpdates: { socketPath: null },
142
+ })
143
+ ) {
144
+ process.exit(0);
269
145
  }
270
146
 
271
147
  server = new AttachServer({
@@ -280,48 +156,21 @@ server = new AttachServer({
280
156
  });
281
157
 
282
158
  server.on('output', (data) => {
283
- const chunk = data.toString();
284
- const timestamp = Date.now();
285
-
286
- const { lines, remaining } = splitBufferLines(outputBuffer, chunk);
287
- outputBuffer = remaining;
288
-
289
- if (silentJsonMode) {
290
- handleSilentJsonLines(lines, timestamp);
291
- } else {
292
- handleStreamingLines(lines, timestamp);
293
- }
159
+ outputBuffer = outputRuntime.consumeOutput(outputBuffer, data);
294
160
  });
295
161
 
296
162
  server.on('exit', async ({ exitCode, signal }) => {
297
- const timestamp = Date.now();
298
- const code = exitCode;
299
-
300
- flushOutputBuffer(timestamp);
301
-
302
- const recovered = attemptRecovery(code, timestamp);
303
-
304
- if (silentJsonMode && finalResultJson) {
305
- log(finalResultJson + '\n');
306
- }
307
-
308
- writeCompletionFooter(code, signal);
309
- await cleanupCommandSpec();
310
-
311
- const resolvedCode = fatalError ? 1 : recovered?.payload ? 0 : code;
312
- const status = resolvedCode === 0 ? 'completed' : 'failed';
313
- try {
314
- await updateTask(taskId, {
315
- status,
316
- pid: null,
317
- processGroupId: null,
318
- exitCode: resolvedCode,
319
- error: fatalError || (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
320
- socketPath: null,
321
- });
322
- } catch (updateError) {
323
- log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
324
- }
163
+ if (crashStarted) return;
164
+ const completion = outputRuntime.complete({ code: exitCode, signal, outputBuffer });
165
+ await watcherOutputRuntime.completeWatcherTask({
166
+ taskId,
167
+ completion,
168
+ commandCleanup,
169
+ terminateProvider: () => stopAttachableProvider(true),
170
+ updateTask,
171
+ emergencyLog,
172
+ terminalUpdates: { socketPath: null },
173
+ });
325
174
 
326
175
  setTimeout(() => {
327
176
  process.exit(0);
@@ -329,19 +178,7 @@ server.on('exit', async ({ exitCode, signal }) => {
329
178
  });
330
179
 
331
180
  server.on('error', async (err) => {
332
- log(`\nError: ${err.message}\n`);
333
- await cleanupCommandSpec();
334
- try {
335
- await updateTask(taskId, {
336
- status: 'failed',
337
- pid: null,
338
- processGroupId: null,
339
- error: err.message,
340
- });
341
- } catch (updateError) {
342
- log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
343
- }
344
- process.exit(1);
181
+ await failWatcher(err, 'attach server error');
345
182
  });
346
183
 
347
184
  server.on('clientAttach', ({ clientId }) => {
@@ -363,22 +200,33 @@ try {
363
200
  terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
364
201
  });
365
202
 
203
+ if (getTask(taskId)?.cancelRequested) {
204
+ crashStarted = true;
205
+ await watcherOutputRuntime.completePendingWatcherCancellation({
206
+ taskId,
207
+ getTask,
208
+ commandCleanup,
209
+ terminateProvider: stopAttachableProvider,
210
+ updateTask,
211
+ emergencyLog,
212
+ terminalUpdates: { socketPath: null },
213
+ });
214
+ process.exit(0);
215
+ }
216
+
366
217
  log(`[${Date.now()}][SYSTEM] Started with PTY (attachable)\n`);
367
218
  log(`[${Date.now()}][SYSTEM] Socket: ${socketPath}\n`);
368
219
  log(`[${Date.now()}][SYSTEM] PID: ${server.pid}\n`);
369
220
  } catch (err) {
370
- log(`\nFailed to start: ${err.message}\n`);
371
- await cleanupCommandSpec();
372
- updateTask(taskId, { status: 'failed', error: err.message });
373
- process.exit(1);
221
+ await failWatcher(err, 'attach server start');
374
222
  }
375
223
 
376
224
  process.on('SIGTERM', async () => {
377
225
  log(`[${Date.now()}][SYSTEM] Received SIGTERM, stopping...\n`);
378
- await server.stop('SIGTERM');
226
+ await stopAttachableProvider();
379
227
  });
380
228
 
381
229
  process.on('SIGINT', async () => {
382
230
  log(`[${Date.now()}][SYSTEM] Received SIGINT, stopping...\n`);
383
- await server.stop('SIGINT');
231
+ await stopAttachableProvider();
384
232
  });
@@ -0,0 +1,219 @@
1
+ import { lstat, realpath, rm, unlink } from 'fs/promises';
2
+ import { lstatSync, realpathSync, rmSync, unlinkSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { basename, dirname, isAbsolute, resolve } from 'path';
5
+ import { createRequire } from 'module';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const {
9
+ isCanonicalClaudeSettingsOverlayDirectory,
10
+ isClaudeSettingsOverlayDirectory,
11
+ } = require('../src/worktree-claude-config');
12
+
13
+ const CLEANUP_METADATA_KEYS = ['kind', 'path', 'provider', 'reason'];
14
+ const SCHEMA_DIRECTORY_PATTERN = /^zeroshot-schema-[A-Za-z0-9_-]+$/u;
15
+ const SCHEMA_FILE_PATTERN =
16
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.json$/u;
17
+
18
+ function assertClosedCleanupMetadata(cleanupPath, metadata) {
19
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
20
+ throw new Error(`Refusing cleanup without closed ownership metadata: ${cleanupPath}`);
21
+ }
22
+ const keys = Object.keys(metadata).sort();
23
+ if (
24
+ keys.length !== CLEANUP_METADATA_KEYS.length ||
25
+ keys.some((key, index) => key !== CLEANUP_METADATA_KEYS[index])
26
+ ) {
27
+ throw new Error(`Refusing cleanup with open ownership metadata: ${cleanupPath}`);
28
+ }
29
+ if (metadata.path !== cleanupPath) {
30
+ throw new Error(`Refusing cleanup with mismatched ownership path: ${cleanupPath}`);
31
+ }
32
+ }
33
+
34
+ function createCleanupPlan(commandSpec) {
35
+ if (!Array.isArray(commandSpec?.cleanup) || !Array.isArray(commandSpec?.cleanupMetadata)) {
36
+ throw new Error('Refusing cleanup with malformed cleanup collections');
37
+ }
38
+ if (commandSpec.cleanup.length !== commandSpec.cleanupMetadata.length) {
39
+ throw new Error('Refusing cleanup without one closed metadata receipt per path');
40
+ }
41
+ const uniquePaths = new Set(commandSpec.cleanup);
42
+ if (uniquePaths.size !== commandSpec.cleanup.length) {
43
+ throw new Error('Refusing cleanup with duplicate cleanup paths');
44
+ }
45
+
46
+ return commandSpec.cleanup.map((cleanupPath) => {
47
+ if (typeof cleanupPath !== 'string' || cleanupPath.length === 0) {
48
+ throw new Error('Refusing cleanup with a non-string or empty path');
49
+ }
50
+ const matches = commandSpec.cleanupMetadata.filter(
51
+ (metadata) => metadata?.path === cleanupPath
52
+ );
53
+ if (matches.length !== 1) {
54
+ throw new Error(`Refusing cleanup without exact ownership metadata: ${cleanupPath}`);
55
+ }
56
+ assertClosedCleanupMetadata(cleanupPath, matches[0]);
57
+ return { cleanupPath, metadata: matches[0] };
58
+ });
59
+ }
60
+
61
+ function assertOwnedTempDirectory(cleanupPath, metadata) {
62
+ if (
63
+ metadata.provider !== 'claude' ||
64
+ metadata.reason !== 'settings-overlay' ||
65
+ !isCanonicalClaudeSettingsOverlayDirectory(cleanupPath)
66
+ ) {
67
+ throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
68
+ }
69
+ if (isClaudeSettingsOverlayDirectory(cleanupPath)) {
70
+ return false;
71
+ }
72
+ try {
73
+ lstatSync(cleanupPath);
74
+ } catch (error) {
75
+ if (error?.code === 'ENOENT') return true;
76
+ throw error;
77
+ }
78
+ throw new Error(`Refusing unowned temporary directory cleanup: ${cleanupPath}`);
79
+ }
80
+
81
+ function assertCanonicalSchemaPath(cleanupPath, metadata) {
82
+ if (
83
+ metadata.kind !== 'temp-file' ||
84
+ metadata.provider !== 'codex' ||
85
+ metadata.reason !== 'output-schema' ||
86
+ !isAbsolute(cleanupPath) ||
87
+ resolve(cleanupPath) !== cleanupPath
88
+ ) {
89
+ throw new Error(`Refusing unowned output-schema cleanup: ${cleanupPath}`);
90
+ }
91
+ const schemaDirectory = dirname(cleanupPath);
92
+ if (
93
+ dirname(schemaDirectory) !== resolve(tmpdir()) ||
94
+ !SCHEMA_DIRECTORY_PATTERN.test(basename(schemaDirectory)) ||
95
+ !SCHEMA_FILE_PATTERN.test(basename(cleanupPath))
96
+ ) {
97
+ throw new Error(`Refusing non-canonical output-schema cleanup: ${cleanupPath}`);
98
+ }
99
+ }
100
+
101
+ async function assertOwnedSchemaFile(cleanupPath, metadata) {
102
+ assertCanonicalSchemaPath(cleanupPath, metadata);
103
+ const schemaDirectory = dirname(cleanupPath);
104
+ const [tempRoot, realSchemaDirectory, directoryStat, schemaStat] = await Promise.all([
105
+ realpath(tmpdir()),
106
+ realpath(schemaDirectory),
107
+ lstat(schemaDirectory),
108
+ lstat(cleanupPath),
109
+ ]);
110
+ if (
111
+ directoryStat.isSymbolicLink() ||
112
+ !directoryStat.isDirectory() ||
113
+ dirname(realSchemaDirectory) !== tempRoot ||
114
+ basename(realSchemaDirectory) !== basename(schemaDirectory) ||
115
+ schemaStat.isSymbolicLink() ||
116
+ !schemaStat.isFile()
117
+ ) {
118
+ throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
119
+ }
120
+ }
121
+
122
+ function assertOwnedSchemaFileSync(cleanupPath, metadata) {
123
+ assertCanonicalSchemaPath(cleanupPath, metadata);
124
+ const schemaDirectory = dirname(cleanupPath);
125
+ const tempRoot = realpathSync(tmpdir());
126
+ const realSchemaDirectory = realpathSync(schemaDirectory);
127
+ const directoryStat = lstatSync(schemaDirectory);
128
+ const schemaStat = lstatSync(cleanupPath);
129
+ if (
130
+ directoryStat.isSymbolicLink() ||
131
+ !directoryStat.isDirectory() ||
132
+ dirname(realSchemaDirectory) !== tempRoot ||
133
+ basename(realSchemaDirectory) !== basename(schemaDirectory) ||
134
+ schemaStat.isSymbolicLink() ||
135
+ !schemaStat.isFile()
136
+ ) {
137
+ throw new Error(`Refusing non-regular or escaped output-schema cleanup: ${cleanupPath}`);
138
+ }
139
+ }
140
+
141
+ async function removeCleanupPath(cleanupPath, metadata) {
142
+ if (metadata.kind === 'temp-directory') {
143
+ if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
144
+ await rm(cleanupPath, { recursive: true, force: true });
145
+ return;
146
+ }
147
+ await assertOwnedSchemaFile(cleanupPath, metadata);
148
+ await unlink(cleanupPath);
149
+ }
150
+
151
+ function removeCleanupPathSync(cleanupPath, metadata) {
152
+ if (metadata.kind === 'temp-directory') {
153
+ if (assertOwnedTempDirectory(cleanupPath, metadata)) return;
154
+ rmSync(cleanupPath, { recursive: true, force: true });
155
+ return;
156
+ }
157
+ assertOwnedSchemaFileSync(cleanupPath, metadata);
158
+ unlinkSync(cleanupPath);
159
+ }
160
+
161
+ /**
162
+ * Build one idempotent cleanup owner for a provider command. Callers may run it
163
+ * only after the persisted provider termination boundary is terminal.
164
+ */
165
+ export function createCommandSpecCleanup(commandSpec, logFailure) {
166
+ let started = false;
167
+ let result = true;
168
+
169
+ return {
170
+ async run() {
171
+ if (started) return result;
172
+ started = true;
173
+ let cleanupPlan;
174
+ try {
175
+ cleanupPlan = createCleanupPlan(commandSpec);
176
+ } catch (error) {
177
+ logFailure('<command-cleanup>', error);
178
+ result = false;
179
+ return result;
180
+ }
181
+ let succeeded = true;
182
+ for (const { cleanupPath, metadata } of cleanupPlan) {
183
+ try {
184
+ await removeCleanupPath(cleanupPath, metadata);
185
+ } catch (error) {
186
+ if (error?.code === 'ENOENT') continue;
187
+ succeeded = false;
188
+ logFailure(cleanupPath, error);
189
+ }
190
+ }
191
+ result = succeeded;
192
+ return result;
193
+ },
194
+ runSync() {
195
+ if (started) return result;
196
+ started = true;
197
+ let cleanupPlan;
198
+ try {
199
+ cleanupPlan = createCleanupPlan(commandSpec);
200
+ } catch (error) {
201
+ logFailure('<command-cleanup>', error);
202
+ result = false;
203
+ return result;
204
+ }
205
+ let succeeded = true;
206
+ for (const { cleanupPath, metadata } of cleanupPlan) {
207
+ try {
208
+ removeCleanupPathSync(cleanupPath, metadata);
209
+ } catch (error) {
210
+ if (error?.code === 'ENOENT') continue;
211
+ succeeded = false;
212
+ logFailure(cleanupPath, error);
213
+ }
214
+ }
215
+ result = succeeded;
216
+ return result;
217
+ },
218
+ };
219
+ }