@fitlab-ai/agent-infra 0.9.6 → 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 (178) hide show
  1. package/dist/bin/internal-cli.js +10 -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 +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -1,12 +1,38 @@
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';
7
+ import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
8
+ import { inspectSandboxControlContainer } from './container-identity.js';
6
9
  import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
7
- const DEFAULT_QUIESCE_TIMEOUT_MS = 7_000;
10
+ const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
8
11
  const QUIESCING_FILE = 'quiescing.json';
9
12
  const BROKER_STARTING_FILE = 'broker-starting.json';
13
+ const REPLACEMENT_FILE = 'replacement.json';
14
+ const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
15
+ async function awaitWithDeadline(operation, deadlineAt, errorCode) {
16
+ const remaining = deadlineAt - Date.now();
17
+ if (remaining <= 0)
18
+ throw new Error(errorCode);
19
+ let timer;
20
+ try {
21
+ return await Promise.race([
22
+ operation(),
23
+ new Promise((_, reject) => {
24
+ timer = setTimeout(() => reject(new Error(errorCode)), remaining);
25
+ })
26
+ ]);
27
+ }
28
+ finally {
29
+ if (timer)
30
+ clearTimeout(timer);
31
+ }
32
+ }
33
+ function forceDeadline(deadlineAt, timeoutMs) {
34
+ return Math.max(Date.now(), deadlineAt - Math.max(1, Math.floor(timeoutMs / 2)));
35
+ }
10
36
  function regularFile(filePath) {
11
37
  try {
12
38
  const stat = fs.lstatSync(filePath);
@@ -44,8 +70,8 @@ function readStartupOwner(filePath) {
44
70
  const raw = fs.readFileSync(filePath, 'utf8');
45
71
  try {
46
72
  const record = JSON.parse(raw);
47
- if (record.version !== 1 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
48
- || typeof record.startTime !== 'string')
73
+ if (record.version !== 2 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
74
+ || typeof record.startTime !== 'number' || !Number.isSafeInteger(record.startTime))
49
75
  return null;
50
76
  return { raw, owner: { pid: record.pid, startTime: record.startTime } };
51
77
  }
@@ -53,14 +79,14 @@ function readStartupOwner(filePath) {
53
79
  return null;
54
80
  }
55
81
  }
56
- async function waitForStartupTransition(root, timeoutMs) {
82
+ async function waitForStartupTransition(root, timeoutMs, identityProbe = getProcessIdentityState) {
57
83
  const transitionPath = path.join(root, BROKER_STARTING_FILE);
58
84
  const deadline = Date.now() + timeoutMs;
59
85
  while (Date.now() < deadline) {
60
86
  const transition = readStartupOwner(transitionPath);
61
87
  if (!fs.existsSync(transitionPath))
62
88
  return;
63
- if (transition && !ownerLive(transition.owner)) {
89
+ if (transition && !ownerLive(transition.owner, identityProbe)) {
64
90
  try {
65
91
  if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
66
92
  fs.unlinkSync(transitionPath);
@@ -74,10 +100,10 @@ async function waitForStartupTransition(root, timeoutMs) {
74
100
  }
75
101
  throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
76
102
  }
77
- export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000) {
103
+ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000, options = {}) {
78
104
  const resolvedRoot = path.resolve(root);
79
105
  const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
80
- const record = `${JSON.stringify({ version: 1, ...owner })}\n`;
106
+ const record = `${JSON.stringify({ version: 2, ...owner })}\n`;
81
107
  const deadline = Date.now() + timeoutMs;
82
108
  while (Date.now() < deadline) {
83
109
  if (isSandboxControlRootQuiescing(resolvedRoot))
@@ -108,7 +134,7 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
108
134
  if (error.code !== 'EEXIST')
109
135
  throw error;
110
136
  const transition = readStartupOwner(transitionPath);
111
- if (transition && !ownerLive(transition.owner)) {
137
+ if (transition && !ownerLive(transition.owner, options.identityProbe)) {
112
138
  try {
113
139
  if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
114
140
  fs.unlinkSync(transitionPath);
@@ -123,34 +149,342 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
123
149
  }
124
150
  throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
125
151
  }
126
- 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) {
127
432
  if (!regularFile(manifestPath))
128
433
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
129
434
  let manifest;
130
435
  try {
131
- 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;
132
441
  }
133
442
  catch {
134
443
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
135
444
  }
136
- if (manifest.version !== 3) {
137
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 3; container-only recreation is required');
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');
138
448
  }
139
- if (typeof manifest.repoRoot !== 'string' || typeof manifest.worktreeRoot !== 'string'
140
- || typeof manifest.project !== 'string' || typeof manifest.container !== 'string'
141
- || typeof manifest.branch !== 'string' || !['task-bound', 'branch-only'].includes(manifest.mode ?? '')
142
- || (manifest.taskId !== null && typeof manifest.taskId !== 'string')
143
- || typeof manifest.channelDir !== 'string' || typeof manifest.publicStatusDir !== 'string'
144
- || typeof manifest.processingDir !== 'string' || typeof manifest.token !== 'string'
145
- || typeof manifest.generation !== 'string')
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')
146
465
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
147
466
  const root = path.dirname(path.resolve(manifestPath));
148
- if (path.resolve(manifest.channelDir) !== path.join(root, 'channel')
149
- || path.resolve(manifest.publicStatusDir) !== path.join(root, 'public')
150
- || 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')) {
470
+ throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
471
+ }
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')) {
151
479
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
152
480
  }
153
- return manifest;
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);
154
488
  }
155
489
  function readBrokerOwner(filePath) {
156
490
  if (!fs.existsSync(filePath))
@@ -164,9 +498,10 @@ function readBrokerOwner(filePath) {
164
498
  catch {
165
499
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
166
500
  }
167
- if (!owner || owner.version !== 2 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
168
- || typeof owner.startTime !== 'string' || typeof owner.token !== 'string'
169
- || typeof owner.generation !== 'string')
501
+ if (!owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
502
+ || typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
503
+ || typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
504
+ || typeof owner.token !== 'string' || typeof owner.generation !== 'string')
170
505
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
171
506
  return owner;
172
507
  }
@@ -183,10 +518,14 @@ function readStatusOwner(filePath) {
183
518
  }
184
519
  }
185
520
  function sameOwner(left, right) {
186
- return left.pid === right.pid && left.startTime === right.startTime;
521
+ return left.pid === right.pid && left.startTime === right.startTime
522
+ && (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
187
523
  }
188
- function ownerLive(owner) {
189
- return processIdentityMatches({ version: 1, 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';
190
529
  }
191
530
  function readExecutions(manifest) {
192
531
  if (!fs.existsSync(manifest.processingDir))
@@ -219,38 +558,45 @@ function readExecutions(manifest) {
219
558
  }
220
559
  return executions;
221
560
  }
222
- function processGroupAlive(groupId) {
561
+ function processGroupState(groupId) {
223
562
  try {
224
563
  const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
225
564
  return rows.split('\n').some((row) => {
226
565
  const match = row.trim().match(/^(\d+)\s+(\S+)/);
227
566
  return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
228
- });
567
+ }) ? 'alive' : 'dead';
229
568
  }
230
569
  catch {
231
570
  try {
232
571
  process.kill(-groupId, 0);
233
- return true;
572
+ return 'alive';
234
573
  }
235
574
  catch (error) {
236
- 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';
237
581
  }
238
582
  }
239
583
  }
240
- function executionAlive(execution, platform) {
241
- if (platform !== 'win32' && execution.child.processGroupId) {
242
- return processGroupAlive(execution.child.processGroupId);
243
- }
244
- return processIdentityMatches({ version: 1, 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';
245
591
  }
246
- async function waitForExit(owner, timeoutMs) {
247
- const deadline = Date.now() + timeoutMs;
592
+ async function waitForExit(owner, timeoutMs, deadlineAt, identityProbe = getProcessIdentityState) {
593
+ const deadline = deadlineAt ?? (Date.now() + timeoutMs);
248
594
  while (Date.now() < deadline) {
249
- if (!ownerLive(owner))
595
+ if (!ownerLive(owner, identityProbe))
250
596
  return true;
251
597
  await new Promise((resolve) => setTimeout(resolve, 25));
252
598
  }
253
- return !ownerLive(owner);
599
+ return !ownerLive(owner, identityProbe);
254
600
  }
255
601
  function signalOwner(owner, platform, force) {
256
602
  try {
@@ -286,15 +632,19 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
286
632
  if (!stat.isDirectory() || stat.isSymbolicLink())
287
633
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
288
634
  }
635
+ const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
636
+ const identityProbe = options.identityProbe ?? getProcessIdentityState;
637
+ const deadlineAt = Date.now() + timeoutMs;
638
+ const forceAt = forceDeadline(deadlineAt, timeoutMs);
289
639
  markSandboxControlRootQuiescing(resolvedRoot);
290
- await waitForStartupTransition(resolvedRoot, options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS);
640
+ await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
291
641
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
292
- const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
642
+ const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
293
643
  const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
294
644
  const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
295
- const statusOwner = status ? { version: 1, ...status.broker } : null;
296
- const brokerLive = broker ? ownerLive(broker) : false;
297
- const statusLive = statusOwner ? ownerLive(statusOwner) : false;
645
+ const statusOwner = status ? status.broker : null;
646
+ const brokerLive = broker ? ownerLive(broker, identityProbe) : false;
647
+ const statusLive = statusOwner ? ownerLive(statusOwner, identityProbe) : false;
298
648
  if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
299
649
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
300
650
  }
@@ -307,42 +657,221 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
307
657
  if (!manifest && brokerLive)
308
658
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
309
659
  const platform = options.platform ?? process.platform;
310
- const timeoutMs = options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
311
660
  let executions = manifest ? readExecutions(manifest) : [];
312
661
  const owner = brokerLive ? broker : statusLive ? statusOwner : null;
313
662
  if (!owner) {
314
663
  if (manifest && !broker && !status)
315
664
  throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
316
665
  for (const execution of executions) {
317
- if (!terminateSandboxControlExecution(execution, { platform, timeoutMs })) {
666
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
667
+ }
668
+ const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
669
+ while (Date.now() < softDeadlineAt) {
670
+ if (!executions.some((execution) => executionAlive(execution, platform, identityProbe)))
671
+ break;
672
+ await new Promise((resolve) => setTimeout(resolve, 25));
673
+ }
674
+ if (manifest)
675
+ executions = mergeExecutions(executions, readExecutions(manifest));
676
+ if (Date.now() >= deadlineAt)
677
+ throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
678
+ for (const execution of executions) {
679
+ if (!terminateSandboxControlExecution(execution, {
680
+ platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
681
+ })) {
318
682
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
319
683
  }
320
684
  }
321
- if (executions.some((execution) => executionAlive(execution, platform))) {
685
+ if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
322
686
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
323
687
  }
324
688
  return broker || status ? 'stale' : 'missing';
325
689
  }
690
+ const remaining = () => Math.max(0, deadlineAt - Date.now());
326
691
  signalOwner(owner, platform, false);
327
- if (await waitForExit(owner, timeoutMs)) {
328
- if (executions.some((execution) => executionAlive(execution, platform))) {
329
- throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
330
- }
692
+ for (const execution of executions) {
693
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
694
+ }
695
+ const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
696
+ while (Date.now() < softDeadlineAt) {
697
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
698
+ break;
699
+ await new Promise((resolve) => setTimeout(resolve, 25));
700
+ }
701
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
331
702
  return 'stopped';
332
703
  }
333
704
  if (manifest)
334
705
  executions = mergeExecutions(executions, readExecutions(manifest));
706
+ if (Date.now() >= deadlineAt)
707
+ throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
335
708
  for (const execution of executions) {
336
- if (!terminateSandboxControlExecution(execution, { platform, timeoutMs })) {
337
- throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
338
- }
709
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
339
710
  }
340
711
  signalOwner(owner, platform, true);
341
- if (!await waitForExit(owner, timeoutMs))
712
+ while (Date.now() < deadlineAt) {
713
+ if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
714
+ return 'stopped';
715
+ await new Promise((resolve) => setTimeout(resolve, 25));
716
+ }
717
+ if (ownerLive(owner, identityProbe))
342
718
  throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
343
- if (executions.some((execution) => executionAlive(execution, platform))) {
719
+ if (executions.some((execution) => executionAlive(execution, platform, identityProbe)))
344
720
  throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
345
- }
346
721
  return 'stopped';
347
722
  }
723
+ export async function removeSandboxControlRoot(root, options) {
724
+ const resolvedRoot = path.resolve(root);
725
+ if (!fs.existsSync(resolvedRoot))
726
+ return;
727
+ const stat = fs.lstatSync(resolvedRoot);
728
+ if (!stat.isDirectory() || stat.isSymbolicLink())
729
+ throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
730
+ const manifestPath = path.join(resolvedRoot, 'manifest.json');
731
+ const manifest = readSandboxControlManifestForTransition(manifestPath);
732
+ const inspectContainer = options.inspectContainer
733
+ ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
734
+ const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
735
+ const identityProbe = options.identityProbe ?? getProcessIdentityState;
736
+ const deadlineAt = Date.now() + timeoutMs;
737
+ const forceAt = forceDeadline(deadlineAt, timeoutMs);
738
+ const remaining = () => Math.max(0, deadlineAt - Date.now());
739
+ const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
740
+ if (observation.state === 'unknown')
741
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
742
+ if (options.requireAbsent && observation.state !== 'absent') {
743
+ throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
744
+ }
745
+ markSandboxControlRootQuiescing(resolvedRoot);
746
+ await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
747
+ const brokerPath = path.join(resolvedRoot, 'broker.json');
748
+ const statusPath = path.join(resolvedRoot, 'public', 'status.json');
749
+ const broker = readBrokerOwner(brokerPath);
750
+ const status = readStatusOwner(statusPath);
751
+ if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
752
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
753
+ }
754
+ if (status && status.generation !== manifest.generation) {
755
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
756
+ }
757
+ if (broker && status && !sameOwner(broker, status.broker)
758
+ && (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
759
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
760
+ }
761
+ const platform = options.platform ?? process.platform;
762
+ const owner = broker && ownerLive(broker, identityProbe) ? broker
763
+ : status && ownerLive(status.broker, identityProbe) ? status.broker : null;
764
+ const selfOwner = options.selfOwner;
765
+ const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
766
+ if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
767
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
768
+ }
769
+ let executions = readExecutions(manifest);
770
+ if (owner && !selfOwned)
771
+ signalOwner(owner, platform, false);
772
+ for (const execution of executions) {
773
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
774
+ }
775
+ const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
776
+ while (Date.now() < softDeadlineAt) {
777
+ if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
778
+ && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
779
+ break;
780
+ await new Promise((resolve) => setTimeout(resolve, 25));
781
+ }
782
+ if (observation.state === 'found') {
783
+ if (Date.now() >= deadlineAt)
784
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
785
+ await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
786
+ const afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
787
+ if (afterRemoval.state !== 'absent') {
788
+ throw new Error(afterRemoval.state === 'unknown'
789
+ ? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
790
+ : 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
791
+ }
792
+ }
793
+ const currentManifest = readSandboxControlManifestForTransition(manifestPath);
794
+ if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
795
+ || !isSandboxControlRootQuiescing(resolvedRoot)) {
796
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
797
+ }
798
+ const currentBroker = readBrokerOwner(brokerPath);
799
+ if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
800
+ || currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
801
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
802
+ }
803
+ if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
804
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
805
+ }
806
+ if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
807
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
808
+ }
809
+ if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
810
+ throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
811
+ }
812
+ executions = mergeExecutions(executions, readExecutions(manifest));
813
+ if (Date.now() >= deadlineAt)
814
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
815
+ for (const execution of executions) {
816
+ if (!terminateSandboxControlExecution(execution, {
817
+ platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
818
+ })) {
819
+ throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
820
+ }
821
+ }
822
+ if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
823
+ if (remaining() <= 0)
824
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
825
+ signalOwner(currentBroker, platform, true);
826
+ if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
827
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
828
+ }
829
+ }
830
+ const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
831
+ if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
832
+ throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
833
+ }
834
+ const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
835
+ if (finalBrokerRaw !== null) {
836
+ const finalBroker = readBrokerOwner(brokerPath);
837
+ if (!finalBroker)
838
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
839
+ const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
840
+ && finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
841
+ if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
842
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
843
+ }
844
+ if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
845
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
846
+ }
847
+ fs.unlinkSync(brokerPath);
848
+ }
849
+ const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
850
+ if (finalStatusRaw !== null) {
851
+ const finalStatus = readStatusOwner(statusPath);
852
+ if (!finalStatus)
853
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
854
+ if (ownerLive(finalStatus.broker, identityProbe)
855
+ && !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
856
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
857
+ }
858
+ if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
859
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
860
+ }
861
+ fs.unlinkSync(statusPath);
862
+ }
863
+ if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
864
+ throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
865
+ }
866
+ fs.rmSync(resolvedRoot, { recursive: true, force: true });
867
+ }
868
+ export async function garbageCollectSandboxControlRoot(root, options = {}) {
869
+ await removeSandboxControlRoot(path.resolve(root), {
870
+ ...options,
871
+ requireAbsent: true,
872
+ removeContainer: async () => {
873
+ throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
874
+ }
875
+ });
876
+ }
348
877
  //# sourceMappingURL=lifecycle.js.map