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