@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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