@the-open-engine/zeroshot 6.10.0 → 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 (31) hide show
  1. package/cli/index.js +16 -0
  2. package/cluster-hooks/block-ask-user-question.py +2 -3
  3. package/cluster-hooks/block-dangerous-git.py +2 -3
  4. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  5. package/lib/agent-cli-provider/adapters/claude.js +42 -1
  6. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  7. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/contract-options.js +2 -0
  9. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  10. package/lib/agent-cli-provider/types.d.ts +6 -2
  11. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/types.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/agent/agent-lifecycle.js +13 -6
  15. package/src/agent/agent-task-executor.js +526 -313
  16. package/src/agent-cli-provider/adapters/claude.ts +46 -1
  17. package/src/agent-cli-provider/contract-options.ts +6 -0
  18. package/src/agent-cli-provider/types.ts +9 -6
  19. package/src/claude-task-runner.js +153 -44
  20. package/src/task-spawn-cleanup-ownership.js +82 -0
  21. package/src/worktree-claude-config.js +193 -85
  22. package/task-lib/attachable-watcher.js +93 -267
  23. package/task-lib/command-spec-cleanup.js +219 -0
  24. package/task-lib/commands/clean.js +35 -5
  25. package/task-lib/commands/get-task-id-by-spawn-token.js +10 -0
  26. package/task-lib/commands/kill.js +117 -4
  27. package/task-lib/commands/status.js +1 -0
  28. package/task-lib/runner.js +61 -4
  29. package/task-lib/store.js +68 -6
  30. package/task-lib/watcher-output-runtime.js +284 -0
  31. package/task-lib/watcher.js +124 -257
@@ -5,16 +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';
8
+ import { appendFileSync } from 'fs';
10
9
  import { getTask, updateTask } from './store.js';
11
- import {
12
- detectProviderFatalError,
13
- detectProviderStreamingModeError,
14
- recoverProviderStructuredOutput,
15
- supportsProviderStructuredOutputRecovery,
16
- } from './provider-helper-runtime.js';
10
+ import { createCommandSpecCleanup } from './command-spec-cleanup.js';
17
11
  import { createProviderSessionCapture } from './provider-session-capture.js';
12
+ import * as watcherOutputRuntime from './watcher-output-runtime.js';
18
13
  import { createRequire } from 'module';
19
14
 
20
15
  // ═══════════════════════════════════════════════════════════════════════════
@@ -23,8 +18,9 @@ import { createRequire } from 'module';
23
18
  // ═══════════════════════════════════════════════════════════════════════════
24
19
 
25
20
  const [, , taskIdArg, cwdArg, logFileArg, argsJsonArg, configJsonArg] = process.argv;
26
- let commandSpecCleanup = [];
27
- let cleanupStarted = false;
21
+ let commandCleanup = watcherOutputRuntime.COMMAND_CLEANUP_UNINITIALIZED;
22
+ let crashStarted = false;
23
+ let server = null;
28
24
 
29
25
  function emergencyLog(msg) {
30
26
  if (logFileArg) {
@@ -38,37 +34,47 @@ function emergencyLog(msg) {
38
34
  }
39
35
  }
40
36
 
41
- function crashWithError(error, source) {
42
- const timestamp = Date.now();
43
- const errorMsg = error instanceof Error ? error.stack || error.message : String(error);
44
-
45
- emergencyLog(`\n[${timestamp}][CRASH] ${source}: ${errorMsg}\n`);
46
- emergencyLog(`[${timestamp}][CRASH] Process terminating due to unhandled error\n`);
47
- cleanupCommandSpecSync();
37
+ function stopAttachableProvider(exitObserved = false) {
38
+ return watcherOutputRuntime.terminateWatcherProvider(server, { exitObserved });
39
+ }
48
40
 
41
+ async function failWatcher(error, source) {
42
+ if (crashStarted) return;
43
+ crashStarted = true;
49
44
  if (taskIdArg) {
50
- try {
51
- updateTask(taskIdArg, {
52
- status: 'failed',
53
- error: `${source}: ${errorMsg}`,
54
- socketPath: null,
55
- });
56
- } catch (updateError) {
57
- emergencyLog(`[${timestamp}][CRASH] Failed to update task status: ${updateError.message}\n`);
58
- }
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
+ });
59
55
  }
60
56
 
61
57
  process.exit(1);
62
58
  }
63
59
 
64
60
  process.on('uncaughtException', (error) => {
65
- crashWithError(error, 'uncaughtException');
61
+ void failWatcher(error, 'uncaughtException');
66
62
  });
67
63
 
68
64
  process.on('unhandledRejection', (reason) => {
69
- crashWithError(reason, 'unhandledRejection');
65
+ void failWatcher(reason, 'unhandledRejection');
70
66
  });
71
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
+
72
78
  const require = createRequire(import.meta.url);
73
79
  const { AttachServer } = require('../src/attach');
74
80
  const { getTaskSocketPath } = require('../src/attach/socket-paths');
@@ -85,202 +91,57 @@ const commandSpec = config.commandSpec || {
85
91
  env: config.env || {},
86
92
  cleanup: [],
87
93
  };
88
- commandSpecCleanup = commandSpec.cleanup || [];
89
- let server = null;
90
-
91
94
  const socketPath = getTaskSocketPath(taskId);
92
95
 
93
96
  function log(msg) {
94
97
  appendFileSync(logFile, msg);
95
98
  }
96
99
 
97
- const providerName = normalizeProviderName(config.provider || 'claude');
98
- const enableRecovery = supportsProviderStructuredOutputRecovery(providerName);
99
- const storedTask = getTask(taskId);
100
+ const { providerName, env, command, finalArgs } = watcherOutputRuntime.resolveWatcherCommand(
101
+ config,
102
+ commandSpec,
103
+ args,
104
+ normalizeProviderName
105
+ );
100
106
  const providerSessionCapture = createProviderSessionCapture({
101
107
  providerName,
102
108
  taskId,
103
109
  updateTask,
104
110
  log,
105
- requestedSessionId: storedTask?.requestedResumeSessionId || null,
106
- initialSessionId: storedTask?.sessionId || null,
107
- initialSessionIdConflict: storedTask?.sessionIdConflict === true,
111
+ requestedSessionId: persistedTask?.requestedResumeSessionId || null,
112
+ initialSessionId: persistedTask?.sessionId || null,
113
+ initialSessionIdConflict: persistedTask?.sessionIdConflict === true,
108
114
  });
109
- const maybeCaptureProviderSession = providerSessionCapture.captureLine;
110
-
111
- const env = { ...process.env, ...(commandSpec.env || {}) };
112
- const command = commandSpec.binary;
113
- const finalArgs = [...(commandSpec.args || args)];
114
-
115
- const silentJsonMode =
116
- config.outputFormat === 'json' &&
117
- config.jsonSchema &&
118
- config.silentJsonOutput &&
119
- supportsProviderStructuredOutputRecovery(providerName);
120
-
121
- let finalResultJson = null;
122
115
  let outputBuffer = '';
123
- let streamingModeError = null;
124
- let fatalError = null;
125
-
126
- function splitBufferLines(buffer, chunk) {
127
- const nextBuffer = buffer + chunk;
128
- const lines = nextBuffer.split('\n');
129
- const remaining = lines.pop() || '';
130
- return { lines, remaining };
131
- }
132
-
133
- function maybeHandleFatalError(line, timestamp) {
134
- if (fatalError) {
135
- return false;
136
- }
137
-
138
- const detected = detectProviderFatalError(providerName, line);
139
- if (!detected) {
140
- return false;
141
- }
142
-
143
- fatalError = detected;
144
-
145
- if (silentJsonMode) {
146
- log(`[${timestamp}]${line}\n`);
147
- }
148
- log(`[${timestamp}][FATAL] ${detected}\n`);
149
116
 
117
+ function stopProviderAfterFatalOutput(timestamp) {
150
118
  if (server) {
151
119
  server.stop('SIGTERM').catch((error) => {
152
120
  log(`[${timestamp}][FATAL] Attach server stop failed: ${error.message}\n`);
153
121
  });
154
122
  }
155
- return true;
156
123
  }
157
124
 
158
- function captureStreamingError(line, timestamp) {
159
- const detectedError = detectProviderStreamingModeError(providerName, line);
160
- if (!detectedError) {
161
- return false;
162
- }
163
-
164
- streamingModeError = { ...detectedError, timestamp };
165
- return true;
166
- }
167
-
168
- function maybeCaptureStructuredOutput(line) {
169
- try {
170
- const json = JSON.parse(line);
171
- if (json.structured_output) {
172
- finalResultJson = line;
173
- }
174
- } catch {
175
- // Not JSON, skip
176
- }
177
- }
178
-
179
- function handleSilentJsonLines(lines, timestamp) {
180
- for (const line of lines) {
181
- if (!line.trim()) continue;
182
- maybeCaptureProviderSession(line);
183
- maybeHandleFatalError(line, timestamp);
184
- if (captureStreamingError(line, timestamp)) {
185
- continue;
186
- }
187
- maybeCaptureStructuredOutput(line);
188
- }
189
- }
190
-
191
- function handleStreamingLines(lines, timestamp) {
192
- for (const line of lines) {
193
- maybeCaptureProviderSession(line);
194
- maybeHandleFatalError(line, timestamp);
195
- if (captureStreamingError(line, timestamp)) {
196
- continue;
197
- }
198
- log(`[${timestamp}]${line}\n`);
199
- }
200
- }
201
-
202
- function flushOutputBuffer(timestamp) {
203
- if (!outputBuffer.trim()) {
204
- return;
205
- }
206
-
207
- maybeCaptureProviderSession(outputBuffer);
208
- if (!enableRecovery) {
209
- if (!silentJsonMode) {
210
- log(`[${timestamp}]${outputBuffer}\n`);
211
- }
212
- return;
213
- }
214
-
215
- maybeHandleFatalError(outputBuffer, timestamp);
216
- if (captureStreamingError(outputBuffer, timestamp)) {
217
- return;
218
- }
219
-
220
- if (silentJsonMode) {
221
- maybeCaptureStructuredOutput(outputBuffer);
222
- return;
223
- }
224
-
225
- log(`[${timestamp}]${outputBuffer}\n`);
226
- }
227
-
228
- function attemptRecovery(code, timestamp) {
229
- if (!(code !== 0 && streamingModeError?.sessionId)) {
230
- return null;
231
- }
232
-
233
- const recovered = recoverProviderStructuredOutput(providerName, streamingModeError.sessionId);
234
- if (recovered?.payload) {
235
- const recoveredLine = JSON.stringify(recovered.payload);
236
- if (silentJsonMode) {
237
- finalResultJson = recoveredLine;
238
- } else {
239
- log(`[${timestamp}]${recoveredLine}\n`);
240
- }
241
- } else if (streamingModeError.line) {
242
- if (silentJsonMode) {
243
- log(streamingModeError.line + '\n');
244
- } else {
245
- log(`[${streamingModeError.timestamp}]${streamingModeError.line}\n`);
246
- }
247
- }
248
-
249
- return recovered;
250
- }
251
-
252
- async function cleanupCommandSpec() {
253
- if (cleanupStarted) return;
254
- cleanupStarted = true;
255
- for (const file of commandSpecCleanup) {
256
- try {
257
- await unlink(file);
258
- } catch (error) {
259
- log(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
260
- }
261
- }
262
- }
263
-
264
- function cleanupCommandSpecSync() {
265
- if (cleanupStarted) return;
266
- cleanupStarted = true;
267
- for (const file of commandSpecCleanup) {
268
- try {
269
- unlinkSync(file);
270
- } catch (error) {
271
- emergencyLog(`[${Date.now()}][CLEANUP] Failed to delete ${file}: ${error.message}\n`);
272
- }
273
- }
274
- }
275
-
276
- function writeCompletionFooter(code, signal) {
277
- if (config.outputFormat === 'json') {
278
- return;
279
- }
125
+ const outputRuntime = watcherOutputRuntime.createWatcherOutputRuntime({
126
+ config,
127
+ providerName,
128
+ log,
129
+ stopProvider: stopProviderAfterFatalOutput,
130
+ providerSessionCapture,
131
+ });
280
132
 
281
- log(`\n${'='.repeat(50)}\n`);
282
- log(`Finished: ${new Date().toISOString()}\n`);
283
- 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);
284
145
  }
285
146
 
286
147
  server = new AttachServer({
@@ -295,55 +156,21 @@ server = new AttachServer({
295
156
  });
296
157
 
297
158
  server.on('output', (data) => {
298
- const chunk = data.toString();
299
- const timestamp = Date.now();
300
-
301
- const { lines, remaining } = splitBufferLines(outputBuffer, chunk);
302
- outputBuffer = remaining;
303
-
304
- if (silentJsonMode) {
305
- handleSilentJsonLines(lines, timestamp);
306
- } else {
307
- handleStreamingLines(lines, timestamp);
308
- }
159
+ outputBuffer = outputRuntime.consumeOutput(outputBuffer, data);
309
160
  });
310
161
 
311
162
  server.on('exit', async ({ exitCode, signal }) => {
312
- const timestamp = Date.now();
313
- const code = exitCode;
314
-
315
- flushOutputBuffer(timestamp);
316
-
317
- const recovered = attemptRecovery(code, timestamp);
318
-
319
- if (silentJsonMode && finalResultJson) {
320
- log(finalResultJson + '\n');
321
- }
322
-
323
- const sessionIdentityError = providerSessionCapture.getCompletionError();
324
- const resolvedCode =
325
- fatalError || sessionIdentityError ? 1 : recovered?.payload ? 0 : code;
326
- const status = resolvedCode === 0 ? 'completed' : 'failed';
327
-
328
- writeCompletionFooter(resolvedCode, signal);
329
- await cleanupCommandSpec();
330
-
331
- try {
332
- await updateTask(taskId, {
333
- status,
334
- pid: null,
335
- processGroupId: null,
336
- exitCode: resolvedCode,
337
- ...providerSessionCapture.getCompletionUpdate(resolvedCode),
338
- error:
339
- fatalError ||
340
- sessionIdentityError ||
341
- (resolvedCode !== 0 && signal ? `Killed by ${signal}` : null),
342
- socketPath: null,
343
- });
344
- } catch (updateError) {
345
- log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
346
- }
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
+ });
347
174
 
348
175
  setTimeout(() => {
349
176
  process.exit(0);
@@ -351,19 +178,7 @@ server.on('exit', async ({ exitCode, signal }) => {
351
178
  });
352
179
 
353
180
  server.on('error', async (err) => {
354
- log(`\nError: ${err.message}\n`);
355
- await cleanupCommandSpec();
356
- try {
357
- await updateTask(taskId, {
358
- status: 'failed',
359
- pid: null,
360
- processGroupId: null,
361
- error: err.message,
362
- });
363
- } catch (updateError) {
364
- log(`[${Date.now()}][ERROR] Failed to update task status: ${updateError.message}\n`);
365
- }
366
- process.exit(1);
181
+ await failWatcher(err, 'attach server error');
367
182
  });
368
183
 
369
184
  server.on('clientAttach', ({ clientId }) => {
@@ -385,22 +200,33 @@ try {
385
200
  terminationStrategy: process.platform === 'win32' ? 'process-tree' : 'process-group',
386
201
  });
387
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
+
388
217
  log(`[${Date.now()}][SYSTEM] Started with PTY (attachable)\n`);
389
218
  log(`[${Date.now()}][SYSTEM] Socket: ${socketPath}\n`);
390
219
  log(`[${Date.now()}][SYSTEM] PID: ${server.pid}\n`);
391
220
  } catch (err) {
392
- log(`\nFailed to start: ${err.message}\n`);
393
- await cleanupCommandSpec();
394
- updateTask(taskId, { status: 'failed', error: err.message });
395
- process.exit(1);
221
+ await failWatcher(err, 'attach server start');
396
222
  }
397
223
 
398
224
  process.on('SIGTERM', async () => {
399
225
  log(`[${Date.now()}][SYSTEM] Received SIGTERM, stopping...\n`);
400
- await server.stop('SIGTERM');
226
+ await stopAttachableProvider();
401
227
  });
402
228
 
403
229
  process.on('SIGINT', async () => {
404
230
  log(`[${Date.now()}][SYSTEM] Received SIGINT, stopping...\n`);
405
- await server.stop('SIGINT');
231
+ await stopAttachableProvider();
406
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
+ }