@fitlab-ai/agent-infra 0.9.7 → 0.9.8

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 (96) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  7. package/dist/lib/decide.js +68 -14
  8. package/dist/lib/init.js +1 -1
  9. package/dist/lib/internal/release-workflow.js +1 -3
  10. package/dist/lib/internal/task-activity.js +57 -7
  11. package/dist/lib/internal/task-event.js +57 -4
  12. package/dist/lib/internal/task-ledger.js +59 -5
  13. package/dist/lib/internal/task-lifecycle.js +59 -8
  14. package/dist/lib/internal/task-orchestration.js +11 -3
  15. package/dist/lib/internal/task-override.js +131 -0
  16. package/dist/lib/internal/task-review.js +64 -6
  17. package/dist/lib/internal/task-verify.js +65 -3
  18. package/dist/lib/internal/task-warning.js +57 -4
  19. package/dist/lib/platform/pr-summary.js +5 -3
  20. package/dist/lib/runtime/agent-runtime.js +56 -0
  21. package/dist/lib/sandbox/commands/create.js +389 -311
  22. package/dist/lib/sandbox/control/container-identity.js +1 -1
  23. package/dist/lib/sandbox/control/executor.js +10 -2
  24. package/dist/lib/sandbox/control/lifecycle.js +392 -72
  25. package/dist/lib/sandbox/control/state.js +27 -11
  26. package/dist/lib/sandbox/engine.js +14 -7
  27. package/dist/lib/sandbox/mounts.js +20 -5
  28. package/dist/lib/sandbox/recovery.js +78 -10
  29. package/dist/lib/sandbox/workspace-view.js +62 -40
  30. package/dist/lib/server/process-state.js +51 -1
  31. package/dist/lib/task/activity-intent.js +4 -1
  32. package/dist/lib/task/decision-intents.js +4 -2
  33. package/dist/lib/task/delegation-receipts.js +37 -16
  34. package/dist/lib/task/events.js +9 -5
  35. package/dist/lib/task/guard-override.js +31 -0
  36. package/dist/lib/task/human-override.js +1240 -0
  37. package/dist/lib/task/ledger-intents.js +6 -3
  38. package/dist/lib/task/lifecycle.js +69 -14
  39. package/dist/lib/task/orchestration.js +60 -6
  40. package/dist/lib/task/review-finalization.js +3 -2
  41. package/dist/lib/task/verification-engine.js +40 -14
  42. package/dist/lib/task/verification.js +13 -1
  43. package/dist/lib/task/workflow-warning-intents.js +4 -2
  44. package/dist/lib/task/write.js +2 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  47. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
  49. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
  50. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  51. package/lib/decide.ts +59 -14
  52. package/lib/init.ts +1 -1
  53. package/lib/internal/release-workflow.ts +1 -3
  54. package/lib/internal/task-activity.ts +46 -6
  55. package/lib/internal/task-event.ts +53 -4
  56. package/lib/internal/task-ledger.ts +48 -4
  57. package/lib/internal/task-lifecycle.ts +56 -12
  58. package/lib/internal/task-orchestration.ts +12 -4
  59. package/lib/internal/task-override.ts +138 -0
  60. package/lib/internal/task-review.ts +51 -6
  61. package/lib/internal/task-verify.ts +52 -3
  62. package/lib/internal/task-warning.ts +44 -3
  63. package/lib/platform/pr-summary.ts +10 -3
  64. package/lib/runtime/agent-runtime.ts +76 -0
  65. package/lib/sandbox/commands/create.ts +88 -4
  66. package/lib/sandbox/config.ts +2 -1
  67. package/lib/sandbox/control/container-identity.ts +3 -3
  68. package/lib/sandbox/control/executor.ts +10 -2
  69. package/lib/sandbox/control/lifecycle.ts +463 -76
  70. package/lib/sandbox/control/protocol.ts +5 -2
  71. package/lib/sandbox/control/state.ts +35 -18
  72. package/lib/sandbox/engine.ts +20 -9
  73. package/lib/sandbox/mounts.ts +23 -5
  74. package/lib/sandbox/recovery.ts +90 -9
  75. package/lib/sandbox/workspace-view.ts +70 -40
  76. package/lib/server/process-state.ts +48 -1
  77. package/lib/task/activity-intent.ts +4 -2
  78. package/lib/task/decision-intents.ts +4 -2
  79. package/lib/task/delegation-receipts.ts +46 -18
  80. package/lib/task/events.ts +10 -5
  81. package/lib/task/guard-override.ts +50 -0
  82. package/lib/task/human-override.ts +1503 -0
  83. package/lib/task/ledger-intents.ts +6 -3
  84. package/lib/task/lifecycle.ts +71 -14
  85. package/lib/task/orchestration.ts +89 -7
  86. package/lib/task/review-finalization.ts +6 -2
  87. package/lib/task/verification-engine.ts +42 -16
  88. package/lib/task/verification.ts +15 -1
  89. package/lib/task/workflow-warning-intents.ts +4 -2
  90. package/lib/task/write.ts +4 -1
  91. package/package.json +1 -1
  92. package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
  95. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  96. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
@@ -1,14 +1,17 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
4
5
  import { buildProcessTreeStopCommand } from '../../server/process-control.js';
5
- import { processIdentityMatches } from '../../server/process-state.js';
6
+ import { getProcessIdentityState, getProcessStartTime } from '../../server/process-state.js';
6
7
  import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
7
8
  import { inspectSandboxControlContainer } from './container-identity.js';
8
9
  import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
9
10
  const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
10
11
  const QUIESCING_FILE = 'quiescing.json';
11
12
  const BROKER_STARTING_FILE = 'broker-starting.json';
13
+ const REPLACEMENT_FILE = 'replacement.json';
14
+ const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
12
15
  async function awaitWithDeadline(operation, deadlineAt, errorCode) {
13
16
  const remaining = deadlineAt - Date.now();
14
17
  if (remaining <= 0)
@@ -76,14 +79,14 @@ function readStartupOwner(filePath) {
76
79
  return null;
77
80
  }
78
81
  }
79
- async function waitForStartupTransition(root, timeoutMs) {
82
+ async function waitForStartupTransition(root, timeoutMs, identityProbe = getProcessIdentityState) {
80
83
  const transitionPath = path.join(root, BROKER_STARTING_FILE);
81
84
  const deadline = Date.now() + timeoutMs;
82
85
  while (Date.now() < deadline) {
83
86
  const transition = readStartupOwner(transitionPath);
84
87
  if (!fs.existsSync(transitionPath))
85
88
  return;
86
- if (transition && !ownerLive(transition.owner)) {
89
+ if (transition && !ownerLive(transition.owner, identityProbe)) {
87
90
  try {
88
91
  if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
89
92
  fs.unlinkSync(transitionPath);
@@ -97,7 +100,7 @@ async function waitForStartupTransition(root, timeoutMs) {
97
100
  }
98
101
  throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
99
102
  }
100
- export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000) {
103
+ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000, options = {}) {
101
104
  const resolvedRoot = path.resolve(root);
102
105
  const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
103
106
  const record = `${JSON.stringify({ version: 2, ...owner })}\n`;
@@ -131,7 +134,7 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
131
134
  if (error.code !== 'EEXIST')
132
135
  throw error;
133
136
  const transition = readStartupOwner(transitionPath);
134
- if (transition && !ownerLive(transition.owner)) {
137
+ if (transition && !ownerLive(transition.owner, options.identityProbe)) {
135
138
  try {
136
139
  if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
137
140
  fs.unlinkSync(transitionPath);
@@ -146,40 +149,342 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
146
149
  }
147
150
  throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
148
151
  }
149
- export function readSandboxControlManifest(manifestPath) {
152
+ function replacementPath(root) {
153
+ return path.join(root, REPLACEMENT_FILE);
154
+ }
155
+ function replacementStatePath(root) {
156
+ const resolvedRoot = path.resolve(root);
157
+ return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}${REPLACEMENT_STATE_SUFFIX}`);
158
+ }
159
+ function replacementSnapshotPath(root, transitionId) {
160
+ const resolvedRoot = path.resolve(root);
161
+ return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}.replacement-${transitionId}.snapshot`);
162
+ }
163
+ function writeAtomicText(filePath, content) {
164
+ const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
165
+ try {
166
+ fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
167
+ fs.renameSync(temporary, filePath);
168
+ }
169
+ finally {
170
+ fs.rmSync(temporary, { force: true });
171
+ }
172
+ }
173
+ function readReplacementState(root) {
174
+ const filePath = replacementStatePath(root);
175
+ if (!fs.existsSync(filePath))
176
+ return null;
177
+ if (!regularFile(filePath))
178
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
179
+ try {
180
+ const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
181
+ const resolvedRoot = path.resolve(root);
182
+ if (value.version !== 1 || (value.phase !== 'prepared' && value.phase !== 'committed')
183
+ || value.root !== resolvedRoot || typeof value.transitionId !== 'string' || value.transitionId.length === 0
184
+ || value.snapshotRoot !== replacementSnapshotPath(resolvedRoot, value.transitionId)) {
185
+ throw new Error('invalid replacement state');
186
+ }
187
+ return value;
188
+ }
189
+ catch {
190
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
191
+ }
192
+ }
193
+ function writeReplacementState(state) {
194
+ writeAtomicText(replacementStatePath(state.root), `${JSON.stringify(state)}\n`);
195
+ }
196
+ function removeReplacementState(root) {
197
+ fs.rmSync(replacementStatePath(root), { force: true });
198
+ }
199
+ function readReplacementRecord(filePath) {
200
+ if (!fs.existsSync(filePath))
201
+ return null;
202
+ if (!regularFile(filePath))
203
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
204
+ const raw = fs.readFileSync(filePath, 'utf8');
205
+ try {
206
+ const value = JSON.parse(raw);
207
+ if (value.version !== 1 || typeof value.transitionId !== 'string' || value.transitionId.length === 0
208
+ || !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0
209
+ || !Number.isSafeInteger(value.startTime))
210
+ throw new Error('invalid replacement');
211
+ return { raw, record: value };
212
+ }
213
+ catch {
214
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
215
+ }
216
+ }
217
+ export function acquireSandboxControlReplacement(root, options = {}) {
218
+ const resolvedRoot = path.resolve(root);
219
+ if (fs.existsSync(resolvedRoot)) {
220
+ const stat = fs.lstatSync(resolvedRoot);
221
+ if (!stat.isDirectory() || stat.isSymbolicLink())
222
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
223
+ }
224
+ else {
225
+ fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
226
+ }
227
+ const startTime = options.owner?.startTime ?? getProcessStartTime(process.pid);
228
+ if (startTime === null || startTime === undefined)
229
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
230
+ const owner = options.owner ?? { pid: process.pid, startTime };
231
+ const transitionId = randomUUID();
232
+ const record = `${JSON.stringify({ version: 1, transitionId, ...owner })}\n`;
233
+ const filePath = replacementPath(resolvedRoot);
234
+ const existing = readReplacementRecord(filePath);
235
+ if (existing) {
236
+ const ownerState = (options.probeOwner ?? getProcessIdentityState)(existing.record);
237
+ if (ownerState === 'alive') {
238
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
239
+ }
240
+ if (ownerState === 'unknown') {
241
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
242
+ }
243
+ if (fs.readFileSync(filePath, 'utf8') !== existing.raw) {
244
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
245
+ }
246
+ fs.unlinkSync(filePath);
247
+ }
248
+ try {
249
+ fs.writeFileSync(filePath, record, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
250
+ }
251
+ catch (error) {
252
+ if (error.code === 'EEXIST') {
253
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
254
+ }
255
+ throw error;
256
+ }
257
+ let released = false;
258
+ const verifyOwnership = () => {
259
+ if (released)
260
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_RELEASED');
261
+ const current = readReplacementRecord(filePath);
262
+ if (!current || current.raw !== record || current.record.transitionId !== transitionId) {
263
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
264
+ }
265
+ };
266
+ return {
267
+ root: resolvedRoot,
268
+ transitionId,
269
+ owner,
270
+ assertOwned: verifyOwnership,
271
+ clearQuiescing() {
272
+ verifyOwnership();
273
+ const markerPath = path.join(resolvedRoot, QUIESCING_FILE);
274
+ if (fs.existsSync(markerPath)) {
275
+ if (!regularFile(markerPath))
276
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
277
+ fs.unlinkSync(markerPath);
278
+ }
279
+ },
280
+ release() {
281
+ if (released)
282
+ return;
283
+ verifyOwnership();
284
+ fs.unlinkSync(filePath);
285
+ released = true;
286
+ }
287
+ };
288
+ }
289
+ function readReplacementRaw(root) {
290
+ const current = readReplacementRecord(replacementPath(root));
291
+ if (!current)
292
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
293
+ return current.raw;
294
+ }
295
+ export function beginSandboxControlReplacement(root, lease) {
296
+ const resolvedRoot = path.resolve(root);
297
+ if (path.resolve(lease.root) !== resolvedRoot)
298
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
299
+ lease.assertOwned();
300
+ if (!fs.existsSync(resolvedRoot))
301
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
302
+ const rootStat = fs.lstatSync(resolvedRoot);
303
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
304
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
305
+ if (readReplacementState(resolvedRoot))
306
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_REQUIRED');
307
+ const snapshotRoot = replacementSnapshotPath(resolvedRoot, lease.transitionId);
308
+ if (fs.existsSync(snapshotRoot))
309
+ throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
310
+ const replacementRaw = readReplacementRaw(resolvedRoot);
311
+ const state = {
312
+ version: 1,
313
+ phase: 'prepared',
314
+ root: resolvedRoot,
315
+ snapshotRoot,
316
+ transitionId: lease.transitionId
317
+ };
318
+ writeReplacementState(state);
319
+ let moved = false;
320
+ try {
321
+ fs.renameSync(resolvedRoot, snapshotRoot);
322
+ moved = true;
323
+ fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
324
+ fs.writeFileSync(replacementPath(resolvedRoot), replacementRaw, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
325
+ markSandboxControlRootQuiescing(resolvedRoot);
326
+ return { root: resolvedRoot, snapshotRoot, transitionId: lease.transitionId };
327
+ }
328
+ catch (error) {
329
+ if (moved) {
330
+ fs.rmSync(resolvedRoot, { recursive: true, force: true });
331
+ fs.renameSync(snapshotRoot, resolvedRoot);
332
+ }
333
+ removeReplacementState(resolvedRoot);
334
+ throw error;
335
+ }
336
+ }
337
+ function isRecoverableCutoverManifestError(error) {
338
+ return error instanceof Error
339
+ && (error.message.startsWith('SANDBOX_CONTROL_MANIFEST_')
340
+ || error.message === 'SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
341
+ }
342
+ export async function recoverSandboxControlReplacement(root, lease) {
343
+ const resolvedRoot = path.resolve(root);
344
+ if (path.resolve(lease.root) !== resolvedRoot)
345
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
346
+ const state = readReplacementState(resolvedRoot);
347
+ if (!state)
348
+ return 'none';
349
+ lease.assertOwned();
350
+ if (state.phase === 'committed') {
351
+ fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
352
+ removeReplacementState(resolvedRoot);
353
+ return 'committed';
354
+ }
355
+ if (!fs.existsSync(state.snapshotRoot)) {
356
+ removeReplacementState(resolvedRoot);
357
+ return 'none';
358
+ }
359
+ if (fs.existsSync(resolvedRoot) && fs.existsSync(path.join(resolvedRoot, 'manifest.json'))) {
360
+ try {
361
+ await quiesceSandboxControlRoot(resolvedRoot);
362
+ }
363
+ catch (error) {
364
+ if (!isRecoverableCutoverManifestError(error))
365
+ throw error;
366
+ }
367
+ }
368
+ lease.assertOwned();
369
+ const replacementRaw = readReplacementRaw(resolvedRoot);
370
+ fs.rmSync(resolvedRoot, { recursive: true, force: true });
371
+ fs.renameSync(state.snapshotRoot, resolvedRoot);
372
+ writeAtomicText(replacementPath(resolvedRoot), replacementRaw);
373
+ removeReplacementState(resolvedRoot);
374
+ return 'restored';
375
+ }
376
+ export function commitSandboxControlReplacement(root, lease) {
377
+ const resolvedRoot = path.resolve(root);
378
+ if (path.resolve(lease.root) !== resolvedRoot)
379
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
380
+ const state = readReplacementState(resolvedRoot);
381
+ if (!state)
382
+ return;
383
+ lease.assertOwned();
384
+ writeReplacementState({ ...state, phase: 'committed' });
385
+ fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
386
+ removeReplacementState(resolvedRoot);
387
+ }
388
+ function readOptionalRaw(filePath) {
389
+ if (!fs.existsSync(filePath))
390
+ return null;
391
+ if (!regularFile(filePath))
392
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
393
+ return fs.readFileSync(filePath, 'utf8');
394
+ }
395
+ function readProcessingSnapshot(root) {
396
+ const processingDir = path.join(root, 'processing');
397
+ if (!fs.existsSync(processingDir))
398
+ return [];
399
+ const stat = fs.lstatSync(processingDir);
400
+ if (!stat.isDirectory() || stat.isSymbolicLink())
401
+ throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
402
+ return fs.readdirSync(processingDir, { withFileTypes: true })
403
+ .map((entry) => ({
404
+ name: entry.name,
405
+ directory: entry.isDirectory() && !entry.isSymbolicLink(),
406
+ executionRaw: entry.isDirectory() && !entry.isSymbolicLink()
407
+ ? readOptionalRaw(path.join(processingDir, entry.name, 'execution.json'))
408
+ : null
409
+ }))
410
+ .sort((left, right) => left.name.localeCompare(right.name));
411
+ }
412
+ export function captureSandboxControlCutoverSnapshot(root) {
413
+ const resolvedRoot = path.resolve(root);
414
+ const manifestPath = path.join(resolvedRoot, 'manifest.json');
415
+ if (fs.existsSync(manifestPath))
416
+ readSandboxControlManifestForTransition(manifestPath);
417
+ return {
418
+ root: resolvedRoot,
419
+ manifestRaw: readOptionalRaw(manifestPath),
420
+ brokerRaw: readOptionalRaw(path.join(resolvedRoot, 'broker.json')),
421
+ statusRaw: readOptionalRaw(path.join(resolvedRoot, 'public', 'status.json')),
422
+ processing: readProcessingSnapshot(resolvedRoot)
423
+ };
424
+ }
425
+ export function assertSandboxControlCutoverSnapshot(snapshot) {
426
+ const current = captureSandboxControlCutoverSnapshot(snapshot.root);
427
+ if (JSON.stringify(current) !== JSON.stringify(snapshot)) {
428
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
429
+ }
430
+ }
431
+ function readSandboxControlManifestValue(manifestPath, allowLegacy) {
150
432
  if (!regularFile(manifestPath))
151
433
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
152
434
  let manifest;
153
435
  try {
154
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
436
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
437
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
438
+ throw new Error('invalid manifest');
439
+ }
440
+ manifest = parsed;
155
441
  }
156
442
  catch {
157
443
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
158
444
  }
159
- if (manifest.version !== 4) {
160
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 4; container-only recreation is required');
161
- }
162
- if (typeof manifest.engine !== 'string' || manifest.engine.length === 0
163
- || typeof manifest.repoRoot !== 'string' || typeof manifest.worktreeRoot !== 'string'
164
- || typeof manifest.project !== 'string' || typeof manifest.container !== 'string'
165
- || !manifest.containerIdentity || typeof manifest.containerIdentity.id !== 'string'
166
- || manifest.containerIdentity.id.length === 0 || !manifest.containerIdentity.labels
167
- || typeof manifest.containerIdentity.labels !== 'object'
168
- || Array.isArray(manifest.containerIdentity.labels)
169
- || Object.values(manifest.containerIdentity.labels).some((value) => typeof value !== 'string')
170
- || typeof manifest.branch !== 'string' || !['task-bound', 'branch-only'].includes(manifest.mode ?? '')
171
- || (manifest.taskId !== null && typeof manifest.taskId !== 'string')
172
- || typeof manifest.channelDir !== 'string' || typeof manifest.publicStatusDir !== 'string'
173
- || typeof manifest.processingDir !== 'string' || typeof manifest.token !== 'string'
174
- || typeof manifest.generation !== 'string')
445
+ const candidate = manifest;
446
+ if (candidate.version !== 4 && candidate.version !== 5) {
447
+ throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
448
+ }
449
+ if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
450
+ || Array.isArray(candidate.containerIdentity))
451
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
452
+ const containerIdentity = candidate.containerIdentity;
453
+ if (typeof candidate.engine !== 'string' || candidate.engine.length === 0
454
+ || typeof candidate.repoRoot !== 'string' || typeof candidate.worktreeRoot !== 'string'
455
+ || typeof candidate.project !== 'string' || typeof candidate.container !== 'string'
456
+ || typeof containerIdentity.id !== 'string' || containerIdentity.id.length === 0
457
+ || typeof containerIdentity.labels !== 'object' || containerIdentity.labels === null
458
+ || Array.isArray(containerIdentity.labels)
459
+ || Object.values(containerIdentity.labels).some((value) => typeof value !== 'string')
460
+ || typeof candidate.branch !== 'string' || !['task-bound', 'branch-only'].includes(candidate.mode ?? '')
461
+ || (candidate.taskId !== null && typeof candidate.taskId !== 'string')
462
+ || typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
463
+ || typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
464
+ || typeof candidate.generation !== 'string')
175
465
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
176
466
  const root = path.dirname(path.resolve(manifestPath));
177
- if (path.resolve(manifest.channelDir) !== path.join(root, 'channel')
178
- || path.resolve(manifest.publicStatusDir) !== path.join(root, 'public')
179
- || path.resolve(manifest.processingDir) !== path.join(root, 'processing')) {
467
+ if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
468
+ || path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
469
+ || path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
180
470
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
181
471
  }
182
- return manifest;
472
+ if (candidate.version === 4) {
473
+ if (!allowLegacy) {
474
+ throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
475
+ }
476
+ return candidate;
477
+ }
478
+ if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
479
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
480
+ }
481
+ return candidate;
482
+ }
483
+ export function readSandboxControlManifest(manifestPath) {
484
+ return readSandboxControlManifestValue(manifestPath, false);
485
+ }
486
+ export function readSandboxControlManifestForTransition(manifestPath) {
487
+ return readSandboxControlManifestValue(manifestPath, true);
183
488
  }
184
489
  function readBrokerOwner(filePath) {
185
490
  if (!fs.existsSync(filePath))
@@ -216,8 +521,11 @@ function sameOwner(left, right) {
216
521
  return left.pid === right.pid && left.startTime === right.startTime
217
522
  && (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
218
523
  }
219
- function ownerLive(owner) {
220
- return processIdentityMatches({ pid: owner.pid, startTime: owner.startTime });
524
+ function ownerLive(owner, identityProbe = getProcessIdentityState) {
525
+ const state = identityProbe({ pid: owner.pid, startTime: owner.startTime });
526
+ if (state === 'unknown')
527
+ throw new Error('SANDBOX_CONTROL_OWNER_UNAVAILABLE');
528
+ return state === 'alive';
221
529
  }
222
530
  function readExecutions(manifest) {
223
531
  if (!fs.existsSync(manifest.processingDir))
@@ -250,38 +558,45 @@ function readExecutions(manifest) {
250
558
  }
251
559
  return executions;
252
560
  }
253
- function processGroupAlive(groupId) {
561
+ function processGroupState(groupId) {
254
562
  try {
255
563
  const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
256
564
  return rows.split('\n').some((row) => {
257
565
  const match = row.trim().match(/^(\d+)\s+(\S+)/);
258
566
  return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
259
- });
567
+ }) ? 'alive' : 'dead';
260
568
  }
261
569
  catch {
262
570
  try {
263
571
  process.kill(-groupId, 0);
264
- return true;
572
+ return 'alive';
265
573
  }
266
574
  catch (error) {
267
- return error.code === 'EPERM';
575
+ const code = error.code;
576
+ if (code === 'ESRCH')
577
+ return 'dead';
578
+ if (code === 'EPERM')
579
+ return 'alive';
580
+ return 'unknown';
268
581
  }
269
582
  }
270
583
  }
271
- function executionAlive(execution, platform) {
272
- if (platform !== 'win32' && execution.child.processGroupId) {
273
- return processGroupAlive(execution.child.processGroupId);
274
- }
275
- return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
584
+ function executionAlive(execution, platform, identityProbe = getProcessIdentityState) {
585
+ const state = platform !== 'win32' && execution.child.processGroupId
586
+ ? processGroupState(execution.child.processGroupId)
587
+ : identityProbe({ pid: execution.child.pid, startTime: execution.child.startTime });
588
+ if (state === 'unknown')
589
+ throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
590
+ return state === 'alive';
276
591
  }
277
- async function waitForExit(owner, timeoutMs, deadlineAt) {
592
+ async function waitForExit(owner, timeoutMs, deadlineAt, identityProbe = getProcessIdentityState) {
278
593
  const deadline = deadlineAt ?? (Date.now() + timeoutMs);
279
594
  while (Date.now() < deadline) {
280
- if (!ownerLive(owner))
595
+ if (!ownerLive(owner, identityProbe))
281
596
  return true;
282
597
  await new Promise((resolve) => setTimeout(resolve, 25));
283
598
  }
284
- return !ownerLive(owner);
599
+ return !ownerLive(owner, identityProbe);
285
600
  }
286
601
  function signalOwner(owner, platform, force) {
287
602
  try {
@@ -318,17 +633,18 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
318
633
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
319
634
  }
320
635
  const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
636
+ const identityProbe = options.identityProbe ?? getProcessIdentityState;
321
637
  const deadlineAt = Date.now() + timeoutMs;
322
638
  const forceAt = forceDeadline(deadlineAt, timeoutMs);
323
639
  markSandboxControlRootQuiescing(resolvedRoot);
324
- await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
640
+ await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
325
641
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
326
- const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
642
+ const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
327
643
  const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
328
644
  const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
329
645
  const statusOwner = status ? status.broker : null;
330
- const brokerLive = broker ? ownerLive(broker) : false;
331
- const statusLive = statusOwner ? ownerLive(statusOwner) : false;
646
+ const brokerLive = broker ? ownerLive(broker, identityProbe) : false;
647
+ const statusLive = statusOwner ? ownerLive(statusOwner, identityProbe) : false;
332
648
  if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
333
649
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
334
650
  }
@@ -347,11 +663,11 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
347
663
  if (manifest && !broker && !status)
348
664
  throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
349
665
  for (const execution of executions) {
350
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
666
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
351
667
  }
352
668
  const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
353
669
  while (Date.now() < softDeadlineAt) {
354
- if (!executions.some((execution) => executionAlive(execution, platform)))
670
+ if (!executions.some((execution) => executionAlive(execution, platform, identityProbe)))
355
671
  break;
356
672
  await new Promise((resolve) => setTimeout(resolve, 25));
357
673
  }
@@ -361,12 +677,12 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
361
677
  throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
362
678
  for (const execution of executions) {
363
679
  if (!terminateSandboxControlExecution(execution, {
364
- platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now()
680
+ platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
365
681
  })) {
366
682
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
367
683
  }
368
684
  }
369
- if (executions.some((execution) => executionAlive(execution, platform))) {
685
+ if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
370
686
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
371
687
  }
372
688
  return broker || status ? 'stale' : 'missing';
@@ -374,15 +690,15 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
374
690
  const remaining = () => Math.max(0, deadlineAt - Date.now());
375
691
  signalOwner(owner, platform, false);
376
692
  for (const execution of executions) {
377
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
693
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
378
694
  }
379
695
  const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
380
696
  while (Date.now() < softDeadlineAt) {
381
- if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
697
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
382
698
  break;
383
699
  await new Promise((resolve) => setTimeout(resolve, 25));
384
700
  }
385
- if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) {
701
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
386
702
  return 'stopped';
387
703
  }
388
704
  if (manifest)
@@ -390,17 +706,17 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
390
706
  if (Date.now() >= deadlineAt)
391
707
  throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
392
708
  for (const execution of executions) {
393
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now() });
709
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
394
710
  }
395
711
  signalOwner(owner, platform, true);
396
712
  while (Date.now() < deadlineAt) {
397
- if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
713
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
398
714
  return 'stopped';
399
715
  await new Promise((resolve) => setTimeout(resolve, 25));
400
716
  }
401
- if (ownerLive(owner))
717
+ if (ownerLive(owner, identityProbe))
402
718
  throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
403
- if (executions.some((execution) => executionAlive(execution, platform)))
719
+ if (executions.some((execution) => executionAlive(execution, platform, identityProbe)))
404
720
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
405
721
  return 'stopped';
406
722
  }
@@ -412,10 +728,11 @@ export async function removeSandboxControlRoot(root, options) {
412
728
  if (!stat.isDirectory() || stat.isSymbolicLink())
413
729
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
414
730
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
415
- const manifest = readSandboxControlManifest(manifestPath);
731
+ const manifest = readSandboxControlManifestForTransition(manifestPath);
416
732
  const inspectContainer = options.inspectContainer
417
733
  ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
418
734
  const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
735
+ const identityProbe = options.identityProbe ?? getProcessIdentityState;
419
736
  const deadlineAt = Date.now() + timeoutMs;
420
737
  const forceAt = forceDeadline(deadlineAt, timeoutMs);
421
738
  const remaining = () => Math.max(0, deadlineAt - Date.now());
@@ -426,7 +743,7 @@ export async function removeSandboxControlRoot(root, options) {
426
743
  throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
427
744
  }
428
745
  markSandboxControlRootQuiescing(resolvedRoot);
429
- await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
746
+ await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
430
747
  const brokerPath = path.join(resolvedRoot, 'broker.json');
431
748
  const statusPath = path.join(resolvedRoot, 'public', 'status.json');
432
749
  const broker = readBrokerOwner(brokerPath);
@@ -438,12 +755,12 @@ export async function removeSandboxControlRoot(root, options) {
438
755
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
439
756
  }
440
757
  if (broker && status && !sameOwner(broker, status.broker)
441
- && (ownerLive(broker) || ownerLive(status.broker))) {
758
+ && (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
442
759
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
443
760
  }
444
761
  const platform = options.platform ?? process.platform;
445
- const owner = broker && ownerLive(broker) ? broker
446
- : status && ownerLive(status.broker) ? status.broker : null;
762
+ const owner = broker && ownerLive(broker, identityProbe) ? broker
763
+ : status && ownerLive(status.broker, identityProbe) ? status.broker : null;
447
764
  const selfOwner = options.selfOwner;
448
765
  const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
449
766
  if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
@@ -453,11 +770,12 @@ export async function removeSandboxControlRoot(root, options) {
453
770
  if (owner && !selfOwned)
454
771
  signalOwner(owner, platform, false);
455
772
  for (const execution of executions) {
456
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
773
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
457
774
  }
458
775
  const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
459
776
  while (Date.now() < softDeadlineAt) {
460
- if ((!owner || selfOwned || !ownerLive(owner)) && !executions.some((execution) => executionAlive(execution, platform)))
777
+ if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
778
+ && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
461
779
  break;
462
780
  await new Promise((resolve) => setTimeout(resolve, 25));
463
781
  }
@@ -472,7 +790,7 @@ export async function removeSandboxControlRoot(root, options) {
472
790
  : 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
473
791
  }
474
792
  }
475
- const currentManifest = readSandboxControlManifest(manifestPath);
793
+ const currentManifest = readSandboxControlManifestForTransition(manifestPath);
476
794
  if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
477
795
  || !isSandboxControlRootQuiescing(resolvedRoot)) {
478
796
  throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
@@ -485,10 +803,10 @@ export async function removeSandboxControlRoot(root, options) {
485
803
  if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
486
804
  throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
487
805
  }
488
- if (!owner && currentBroker && ownerLive(currentBroker)) {
806
+ if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
489
807
  throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
490
808
  }
491
- if (owner && ownerLive(owner) && !currentBroker) {
809
+ if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
492
810
  throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
493
811
  }
494
812
  executions = mergeExecutions(executions, readExecutions(manifest));
@@ -496,21 +814,21 @@ export async function removeSandboxControlRoot(root, options) {
496
814
  throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
497
815
  for (const execution of executions) {
498
816
  if (!terminateSandboxControlExecution(execution, {
499
- platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now()
817
+ platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
500
818
  })) {
501
819
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
502
820
  }
503
821
  }
504
- if (owner && currentBroker && ownerLive(currentBroker) && !selfOwned) {
822
+ if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
505
823
  if (remaining() <= 0)
506
824
  throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
507
825
  signalOwner(currentBroker, platform, true);
508
- if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt)) {
826
+ if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
509
827
  throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
510
828
  }
511
829
  }
512
830
  const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
513
- if (finalExecutions.some((execution) => executionAlive(execution, platform))) {
831
+ if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
514
832
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
515
833
  }
516
834
  const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
@@ -520,8 +838,9 @@ export async function removeSandboxControlRoot(root, options) {
520
838
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
521
839
  const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
522
840
  && finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
523
- if (ownerLive(finalBroker) && !finalSelfOwner)
841
+ if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
524
842
  throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
843
+ }
525
844
  if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
526
845
  throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
527
846
  }
@@ -532,7 +851,8 @@ export async function removeSandboxControlRoot(root, options) {
532
851
  const finalStatus = readStatusOwner(statusPath);
533
852
  if (!finalStatus)
534
853
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
535
- if (ownerLive(finalStatus.broker) && !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
854
+ if (ownerLive(finalStatus.broker, identityProbe)
855
+ && !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
536
856
  throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
537
857
  }
538
858
  if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {