@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,12 +1,18 @@
|
|
|
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.ts';
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import {
|
|
7
|
+
getProcessIdentityState,
|
|
8
|
+
getProcessStartTime
|
|
9
|
+
} from '../../server/process-state.ts';
|
|
10
|
+
import type { ProcessIdentity, ProcessIdentityProbe, ProcessIdentityState } from '../../server/process-state.ts';
|
|
7
11
|
import type {
|
|
8
12
|
SandboxControlExecution,
|
|
13
|
+
SandboxControlManifestLike,
|
|
9
14
|
SandboxControlManifest,
|
|
15
|
+
SandboxControlLegacyManifest,
|
|
10
16
|
SandboxControlStatus,
|
|
11
17
|
SandboxControlTimingPolicy
|
|
12
18
|
} from './protocol.ts';
|
|
@@ -24,6 +30,8 @@ export type BrokerOwner = ProcessIdentity & Readonly<{ version: 3; brokerId: str
|
|
|
24
30
|
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
25
31
|
const QUIESCING_FILE = 'quiescing.json';
|
|
26
32
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
33
|
+
const REPLACEMENT_FILE = 'replacement.json';
|
|
34
|
+
const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
|
|
27
35
|
|
|
28
36
|
async function awaitWithDeadline<T>(
|
|
29
37
|
operation: () => Promise<T>,
|
|
@@ -90,13 +98,17 @@ function readStartupOwner(filePath: string): { raw: string; owner: OwnerIdentity
|
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
async function waitForStartupTransition(
|
|
101
|
+
async function waitForStartupTransition(
|
|
102
|
+
root: string,
|
|
103
|
+
timeoutMs: number,
|
|
104
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
105
|
+
): Promise<void> {
|
|
94
106
|
const transitionPath = path.join(root, BROKER_STARTING_FILE);
|
|
95
107
|
const deadline = Date.now() + timeoutMs;
|
|
96
108
|
while (Date.now() < deadline) {
|
|
97
109
|
const transition = readStartupOwner(transitionPath);
|
|
98
110
|
if (!fs.existsSync(transitionPath)) return;
|
|
99
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
111
|
+
if (transition && !ownerLive(transition.owner, identityProbe)) {
|
|
100
112
|
try {
|
|
101
113
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw) fs.unlinkSync(transitionPath);
|
|
102
114
|
} catch {
|
|
@@ -112,7 +124,8 @@ async function waitForStartupTransition(root: string, timeoutMs: number): Promis
|
|
|
112
124
|
export async function acquireSandboxControlBrokerStartup(
|
|
113
125
|
root: string,
|
|
114
126
|
owner: OwnerIdentity,
|
|
115
|
-
timeoutMs = 5_000
|
|
127
|
+
timeoutMs = 5_000,
|
|
128
|
+
options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
|
|
116
129
|
): Promise<() => void> {
|
|
117
130
|
const resolvedRoot = path.resolve(root);
|
|
118
131
|
const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
@@ -140,7 +153,7 @@ export async function acquireSandboxControlBrokerStartup(
|
|
|
140
153
|
} catch (error) {
|
|
141
154
|
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
142
155
|
const transition = readStartupOwner(transitionPath);
|
|
143
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
156
|
+
if (transition && !ownerLive(transition.owner, options.identityProbe)) {
|
|
144
157
|
try {
|
|
145
158
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw) fs.unlinkSync(transitionPath);
|
|
146
159
|
} catch {
|
|
@@ -154,37 +167,381 @@ export async function acquireSandboxControlBrokerStartup(
|
|
|
154
167
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
155
168
|
}
|
|
156
169
|
|
|
157
|
-
|
|
170
|
+
type ReplacementRecord = Readonly<{
|
|
171
|
+
version: 1;
|
|
172
|
+
transitionId: string;
|
|
173
|
+
pid: number;
|
|
174
|
+
startTime: number;
|
|
175
|
+
}>;
|
|
176
|
+
|
|
177
|
+
type ReplacementStateRecord = Readonly<{
|
|
178
|
+
version: 1;
|
|
179
|
+
phase: 'prepared' | 'committed';
|
|
180
|
+
root: string;
|
|
181
|
+
snapshotRoot: string;
|
|
182
|
+
transitionId: string;
|
|
183
|
+
}>;
|
|
184
|
+
|
|
185
|
+
export type SandboxControlReplacementCutover = Readonly<{
|
|
186
|
+
root: string;
|
|
187
|
+
snapshotRoot: string;
|
|
188
|
+
transitionId: string;
|
|
189
|
+
}>;
|
|
190
|
+
|
|
191
|
+
export type SandboxControlCutoverSnapshot = Readonly<{
|
|
192
|
+
root: string;
|
|
193
|
+
manifestRaw: string | null;
|
|
194
|
+
brokerRaw: string | null;
|
|
195
|
+
statusRaw: string | null;
|
|
196
|
+
processing: ReadonlyArray<Readonly<{ name: string; directory: boolean; executionRaw: string | null }>>;
|
|
197
|
+
}>;
|
|
198
|
+
|
|
199
|
+
export type SandboxControlReplacementLease = Readonly<{
|
|
200
|
+
root: string;
|
|
201
|
+
transitionId: string;
|
|
202
|
+
owner: ProcessIdentity;
|
|
203
|
+
assertOwned(): void;
|
|
204
|
+
clearQuiescing(): void;
|
|
205
|
+
release(): void;
|
|
206
|
+
}>;
|
|
207
|
+
|
|
208
|
+
function replacementPath(root: string): string {
|
|
209
|
+
return path.join(root, REPLACEMENT_FILE);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function replacementStatePath(root: string): string {
|
|
213
|
+
const resolvedRoot = path.resolve(root);
|
|
214
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}${REPLACEMENT_STATE_SUFFIX}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function replacementSnapshotPath(root: string, transitionId: string): string {
|
|
218
|
+
const resolvedRoot = path.resolve(root);
|
|
219
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}.replacement-${transitionId}.snapshot`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function writeAtomicText(filePath: string, content: string): void {
|
|
223
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
224
|
+
try {
|
|
225
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
226
|
+
fs.renameSync(temporary, filePath);
|
|
227
|
+
} finally {
|
|
228
|
+
fs.rmSync(temporary, { force: true });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function readReplacementState(root: string): ReplacementStateRecord | null {
|
|
233
|
+
const filePath = replacementStatePath(root);
|
|
234
|
+
if (!fs.existsSync(filePath)) return null;
|
|
235
|
+
if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
236
|
+
try {
|
|
237
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<ReplacementStateRecord>;
|
|
238
|
+
const resolvedRoot = path.resolve(root);
|
|
239
|
+
if (value.version !== 1 || (value.phase !== 'prepared' && value.phase !== 'committed')
|
|
240
|
+
|| value.root !== resolvedRoot || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
241
|
+
|| value.snapshotRoot !== replacementSnapshotPath(resolvedRoot, value.transitionId)) {
|
|
242
|
+
throw new Error('invalid replacement state');
|
|
243
|
+
}
|
|
244
|
+
return value as ReplacementStateRecord;
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function writeReplacementState(state: ReplacementStateRecord): void {
|
|
251
|
+
writeAtomicText(replacementStatePath(state.root), `${JSON.stringify(state)}\n`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function removeReplacementState(root: string): void {
|
|
255
|
+
fs.rmSync(replacementStatePath(root), { force: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function readReplacementRecord(filePath: string): { raw: string; record: ReplacementRecord } | null {
|
|
259
|
+
if (!fs.existsSync(filePath)) return null;
|
|
260
|
+
if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
261
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
262
|
+
try {
|
|
263
|
+
const value = JSON.parse(raw) as Partial<ReplacementRecord>;
|
|
264
|
+
if (value.version !== 1 || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
265
|
+
|| !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0
|
|
266
|
+
|| !Number.isSafeInteger(value.startTime)) throw new Error('invalid replacement');
|
|
267
|
+
return { raw, record: value as ReplacementRecord };
|
|
268
|
+
} catch {
|
|
269
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function acquireSandboxControlReplacement(
|
|
274
|
+
root: string,
|
|
275
|
+
options: Readonly<{
|
|
276
|
+
owner?: ProcessIdentity;
|
|
277
|
+
probeOwner?: (identity: ProcessIdentity) => ProcessIdentityState;
|
|
278
|
+
}> = {}
|
|
279
|
+
): SandboxControlReplacementLease {
|
|
280
|
+
const resolvedRoot = path.resolve(root);
|
|
281
|
+
if (fs.existsSync(resolvedRoot)) {
|
|
282
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
283
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
284
|
+
} else {
|
|
285
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
286
|
+
}
|
|
287
|
+
const startTime = options.owner?.startTime ?? getProcessStartTime(process.pid);
|
|
288
|
+
if (startTime === null || startTime === undefined) throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
289
|
+
const owner = options.owner ?? { pid: process.pid, startTime };
|
|
290
|
+
const transitionId = randomUUID();
|
|
291
|
+
const record = `${JSON.stringify({ version: 1, transitionId, ...owner })}\n`;
|
|
292
|
+
const filePath = replacementPath(resolvedRoot);
|
|
293
|
+
const existing = readReplacementRecord(filePath);
|
|
294
|
+
if (existing) {
|
|
295
|
+
const ownerState = (options.probeOwner ?? getProcessIdentityState)(existing.record);
|
|
296
|
+
if (ownerState === 'alive') {
|
|
297
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
298
|
+
}
|
|
299
|
+
if (ownerState === 'unknown') {
|
|
300
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
301
|
+
}
|
|
302
|
+
if (fs.readFileSync(filePath, 'utf8') !== existing.raw) {
|
|
303
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
304
|
+
}
|
|
305
|
+
fs.unlinkSync(filePath);
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
fs.writeFileSync(filePath, record, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
311
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
let released = false;
|
|
316
|
+
const verifyOwnership = (): void => {
|
|
317
|
+
if (released) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RELEASED');
|
|
318
|
+
const current = readReplacementRecord(filePath);
|
|
319
|
+
if (!current || current.raw !== record || current.record.transitionId !== transitionId) {
|
|
320
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
root: resolvedRoot,
|
|
325
|
+
transitionId,
|
|
326
|
+
owner,
|
|
327
|
+
assertOwned: verifyOwnership,
|
|
328
|
+
clearQuiescing(): void {
|
|
329
|
+
verifyOwnership();
|
|
330
|
+
const markerPath = path.join(resolvedRoot, QUIESCING_FILE);
|
|
331
|
+
if (fs.existsSync(markerPath)) {
|
|
332
|
+
if (!regularFile(markerPath)) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
333
|
+
fs.unlinkSync(markerPath);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
release(): void {
|
|
337
|
+
if (released) return;
|
|
338
|
+
verifyOwnership();
|
|
339
|
+
fs.unlinkSync(filePath);
|
|
340
|
+
released = true;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function readReplacementRaw(root: string): string {
|
|
346
|
+
const current = readReplacementRecord(replacementPath(root));
|
|
347
|
+
if (!current) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
348
|
+
return current.raw;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function beginSandboxControlReplacement(
|
|
352
|
+
root: string,
|
|
353
|
+
lease: Readonly<{ root: string; transitionId: string; assertOwned(): void }>
|
|
354
|
+
): SandboxControlReplacementCutover {
|
|
355
|
+
const resolvedRoot = path.resolve(root);
|
|
356
|
+
if (path.resolve(lease.root) !== resolvedRoot) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
357
|
+
lease.assertOwned();
|
|
358
|
+
if (!fs.existsSync(resolvedRoot)) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
359
|
+
const rootStat = fs.lstatSync(resolvedRoot);
|
|
360
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
361
|
+
if (readReplacementState(resolvedRoot)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_REQUIRED');
|
|
362
|
+
const snapshotRoot = replacementSnapshotPath(resolvedRoot, lease.transitionId);
|
|
363
|
+
if (fs.existsSync(snapshotRoot)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
364
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
365
|
+
const state: ReplacementStateRecord = {
|
|
366
|
+
version: 1,
|
|
367
|
+
phase: 'prepared',
|
|
368
|
+
root: resolvedRoot,
|
|
369
|
+
snapshotRoot,
|
|
370
|
+
transitionId: lease.transitionId
|
|
371
|
+
};
|
|
372
|
+
writeReplacementState(state);
|
|
373
|
+
let moved = false;
|
|
374
|
+
try {
|
|
375
|
+
fs.renameSync(resolvedRoot, snapshotRoot);
|
|
376
|
+
moved = true;
|
|
377
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
378
|
+
fs.writeFileSync(replacementPath(resolvedRoot), replacementRaw, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
379
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
380
|
+
return { root: resolvedRoot, snapshotRoot, transitionId: lease.transitionId };
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (moved) {
|
|
383
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
384
|
+
fs.renameSync(snapshotRoot, resolvedRoot);
|
|
385
|
+
}
|
|
386
|
+
removeReplacementState(resolvedRoot);
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isRecoverableCutoverManifestError(error: unknown): boolean {
|
|
392
|
+
return error instanceof Error
|
|
393
|
+
&& (error.message.startsWith('SANDBOX_CONTROL_MANIFEST_')
|
|
394
|
+
|| error.message === 'SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function recoverSandboxControlReplacement(
|
|
398
|
+
root: string,
|
|
399
|
+
lease: Readonly<{ root: string; assertOwned(): void }>
|
|
400
|
+
): Promise<'none' | 'restored' | 'committed'> {
|
|
401
|
+
const resolvedRoot = path.resolve(root);
|
|
402
|
+
if (path.resolve(lease.root) !== resolvedRoot) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
403
|
+
const state = readReplacementState(resolvedRoot);
|
|
404
|
+
if (!state) return 'none';
|
|
405
|
+
lease.assertOwned();
|
|
406
|
+
if (state.phase === 'committed') {
|
|
407
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
408
|
+
removeReplacementState(resolvedRoot);
|
|
409
|
+
return 'committed';
|
|
410
|
+
}
|
|
411
|
+
if (!fs.existsSync(state.snapshotRoot)) {
|
|
412
|
+
removeReplacementState(resolvedRoot);
|
|
413
|
+
return 'none';
|
|
414
|
+
}
|
|
415
|
+
if (fs.existsSync(resolvedRoot) && fs.existsSync(path.join(resolvedRoot, 'manifest.json'))) {
|
|
416
|
+
try {
|
|
417
|
+
await quiesceSandboxControlRoot(resolvedRoot);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (!isRecoverableCutoverManifestError(error)) throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
lease.assertOwned();
|
|
423
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
424
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
425
|
+
fs.renameSync(state.snapshotRoot, resolvedRoot);
|
|
426
|
+
writeAtomicText(replacementPath(resolvedRoot), replacementRaw);
|
|
427
|
+
removeReplacementState(resolvedRoot);
|
|
428
|
+
return 'restored';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function commitSandboxControlReplacement(
|
|
432
|
+
root: string,
|
|
433
|
+
lease: Readonly<{ root: string; assertOwned(): void }>
|
|
434
|
+
): void {
|
|
435
|
+
const resolvedRoot = path.resolve(root);
|
|
436
|
+
if (path.resolve(lease.root) !== resolvedRoot) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
437
|
+
const state = readReplacementState(resolvedRoot);
|
|
438
|
+
if (!state) return;
|
|
439
|
+
lease.assertOwned();
|
|
440
|
+
writeReplacementState({ ...state, phase: 'committed' });
|
|
441
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
442
|
+
removeReplacementState(resolvedRoot);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function readOptionalRaw(filePath: string): string | null {
|
|
446
|
+
if (!fs.existsSync(filePath)) return null;
|
|
447
|
+
if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
448
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function readProcessingSnapshot(root: string): SandboxControlCutoverSnapshot['processing'] {
|
|
452
|
+
const processingDir = path.join(root, 'processing');
|
|
453
|
+
if (!fs.existsSync(processingDir)) return [];
|
|
454
|
+
const stat = fs.lstatSync(processingDir);
|
|
455
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
456
|
+
return fs.readdirSync(processingDir, { withFileTypes: true })
|
|
457
|
+
.map((entry) => ({
|
|
458
|
+
name: entry.name,
|
|
459
|
+
directory: entry.isDirectory() && !entry.isSymbolicLink(),
|
|
460
|
+
executionRaw: entry.isDirectory() && !entry.isSymbolicLink()
|
|
461
|
+
? readOptionalRaw(path.join(processingDir, entry.name, 'execution.json'))
|
|
462
|
+
: null
|
|
463
|
+
}))
|
|
464
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function captureSandboxControlCutoverSnapshot(root: string): SandboxControlCutoverSnapshot {
|
|
468
|
+
const resolvedRoot = path.resolve(root);
|
|
469
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
470
|
+
if (fs.existsSync(manifestPath)) readSandboxControlManifestForTransition(manifestPath);
|
|
471
|
+
return {
|
|
472
|
+
root: resolvedRoot,
|
|
473
|
+
manifestRaw: readOptionalRaw(manifestPath),
|
|
474
|
+
brokerRaw: readOptionalRaw(path.join(resolvedRoot, 'broker.json')),
|
|
475
|
+
statusRaw: readOptionalRaw(path.join(resolvedRoot, 'public', 'status.json')),
|
|
476
|
+
processing: readProcessingSnapshot(resolvedRoot)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function assertSandboxControlCutoverSnapshot(snapshot: SandboxControlCutoverSnapshot): void {
|
|
481
|
+
const current = captureSandboxControlCutoverSnapshot(snapshot.root);
|
|
482
|
+
if (JSON.stringify(current) !== JSON.stringify(snapshot)) {
|
|
483
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function readSandboxControlManifestValue(
|
|
488
|
+
manifestPath: string,
|
|
489
|
+
allowLegacy: boolean
|
|
490
|
+
): SandboxControlManifestLike {
|
|
158
491
|
if (!regularFile(manifestPath)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
159
|
-
let manifest:
|
|
492
|
+
let manifest: Record<string, unknown>;
|
|
160
493
|
try {
|
|
161
|
-
|
|
494
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown;
|
|
495
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
496
|
+
throw new Error('invalid manifest');
|
|
497
|
+
}
|
|
498
|
+
manifest = parsed as Record<string, unknown>;
|
|
162
499
|
} catch {
|
|
163
500
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
164
501
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
||
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|| typeof
|
|
174
|
-
||
|
|
175
|
-
||
|
|
176
|
-
|| typeof
|
|
177
|
-
|| (
|
|
178
|
-
||
|
|
179
|
-
|| typeof
|
|
180
|
-
|| typeof
|
|
502
|
+
const candidate = manifest as Partial<SandboxControlManifest> & Partial<SandboxControlLegacyManifest>;
|
|
503
|
+
if (candidate.version !== 4 && candidate.version !== 5) {
|
|
504
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
505
|
+
}
|
|
506
|
+
if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
|
|
507
|
+
|| Array.isArray(candidate.containerIdentity)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
508
|
+
const containerIdentity = candidate.containerIdentity as Record<string, unknown>;
|
|
509
|
+
if (typeof candidate.engine !== 'string' || candidate.engine.length === 0
|
|
510
|
+
|| typeof candidate.repoRoot !== 'string' || typeof candidate.worktreeRoot !== 'string'
|
|
511
|
+
|| typeof candidate.project !== 'string' || typeof candidate.container !== 'string'
|
|
512
|
+
|| typeof containerIdentity.id !== 'string' || containerIdentity.id.length === 0
|
|
513
|
+
|| typeof containerIdentity.labels !== 'object' || containerIdentity.labels === null
|
|
514
|
+
|| Array.isArray(containerIdentity.labels)
|
|
515
|
+
|| Object.values(containerIdentity.labels).some((value) => typeof value !== 'string')
|
|
516
|
+
|| typeof candidate.branch !== 'string' || !['task-bound', 'branch-only'].includes(candidate.mode ?? '')
|
|
517
|
+
|| (candidate.taskId !== null && typeof candidate.taskId !== 'string')
|
|
518
|
+
|| typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
|
|
519
|
+
|| typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
|
|
520
|
+
|| typeof candidate.generation !== 'string') throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
181
521
|
const root = path.dirname(path.resolve(manifestPath));
|
|
182
|
-
if (path.resolve(
|
|
183
|
-
|| path.resolve(
|
|
184
|
-
|| path.resolve(
|
|
522
|
+
if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
|
|
523
|
+
|| path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
|
|
524
|
+
|| path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
|
|
185
525
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
186
526
|
}
|
|
187
|
-
|
|
527
|
+
if (candidate.version === 4) {
|
|
528
|
+
if (!allowLegacy) {
|
|
529
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
530
|
+
}
|
|
531
|
+
return candidate as unknown as SandboxControlLegacyManifest;
|
|
532
|
+
}
|
|
533
|
+
if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
|
|
534
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
535
|
+
}
|
|
536
|
+
return candidate as unknown as SandboxControlManifest;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function readSandboxControlManifest(manifestPath: string): SandboxControlManifest {
|
|
540
|
+
return readSandboxControlManifestValue(manifestPath, false) as SandboxControlManifest;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function readSandboxControlManifestForTransition(manifestPath: string): SandboxControlManifestLike {
|
|
544
|
+
return readSandboxControlManifestValue(manifestPath, true);
|
|
188
545
|
}
|
|
189
546
|
|
|
190
547
|
function readBrokerOwner(filePath: string): BrokerOwner | null {
|
|
@@ -218,11 +575,16 @@ function sameOwner(left: OwnerIdentity, right: OwnerIdentity): boolean {
|
|
|
218
575
|
&& (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
|
|
219
576
|
}
|
|
220
577
|
|
|
221
|
-
function ownerLive(
|
|
222
|
-
|
|
578
|
+
function ownerLive(
|
|
579
|
+
owner: OwnerIdentity,
|
|
580
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
581
|
+
): boolean {
|
|
582
|
+
const state = identityProbe({ pid: owner.pid, startTime: owner.startTime });
|
|
583
|
+
if (state === 'unknown') throw new Error('SANDBOX_CONTROL_OWNER_UNAVAILABLE');
|
|
584
|
+
return state === 'alive';
|
|
223
585
|
}
|
|
224
586
|
|
|
225
|
-
function readExecutions(manifest:
|
|
587
|
+
function readExecutions(manifest: SandboxControlManifestLike): SandboxControlExecution[] {
|
|
226
588
|
if (!fs.existsSync(manifest.processingDir)) return [];
|
|
227
589
|
const stat = fs.lstatSync(manifest.processingDir);
|
|
228
590
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
@@ -249,37 +611,50 @@ function readExecutions(manifest: SandboxControlManifest): SandboxControlExecuti
|
|
|
249
611
|
return executions;
|
|
250
612
|
}
|
|
251
613
|
|
|
252
|
-
function
|
|
614
|
+
function processGroupState(groupId: number): ProcessIdentityState {
|
|
253
615
|
try {
|
|
254
616
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
255
617
|
return rows.split('\n').some((row) => {
|
|
256
618
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
257
619
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
258
|
-
});
|
|
620
|
+
}) ? 'alive' : 'dead';
|
|
259
621
|
} catch {
|
|
260
622
|
try {
|
|
261
623
|
process.kill(-groupId, 0);
|
|
262
|
-
return
|
|
624
|
+
return 'alive';
|
|
263
625
|
} catch (error) {
|
|
264
|
-
|
|
626
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
627
|
+
if (code === 'ESRCH') return 'dead';
|
|
628
|
+
if (code === 'EPERM') return 'alive';
|
|
629
|
+
return 'unknown';
|
|
265
630
|
}
|
|
266
631
|
}
|
|
267
632
|
}
|
|
268
633
|
|
|
269
|
-
function executionAlive(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
634
|
+
function executionAlive(
|
|
635
|
+
execution: SandboxControlExecution,
|
|
636
|
+
platform: NodeJS.Platform,
|
|
637
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
638
|
+
): boolean {
|
|
639
|
+
const state = platform !== 'win32' && execution.child.processGroupId
|
|
640
|
+
? processGroupState(execution.child.processGroupId)
|
|
641
|
+
: identityProbe({ pid: execution.child.pid, startTime: execution.child.startTime });
|
|
642
|
+
if (state === 'unknown') throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
|
|
643
|
+
return state === 'alive';
|
|
274
644
|
}
|
|
275
645
|
|
|
276
|
-
async function waitForExit(
|
|
646
|
+
async function waitForExit(
|
|
647
|
+
owner: OwnerIdentity,
|
|
648
|
+
timeoutMs: number,
|
|
649
|
+
deadlineAt?: number,
|
|
650
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
651
|
+
): Promise<boolean> {
|
|
277
652
|
const deadline = deadlineAt ?? (Date.now() + timeoutMs);
|
|
278
653
|
while (Date.now() < deadline) {
|
|
279
|
-
if (!ownerLive(owner)) return true;
|
|
654
|
+
if (!ownerLive(owner, identityProbe)) return true;
|
|
280
655
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
281
656
|
}
|
|
282
|
-
return !ownerLive(owner);
|
|
657
|
+
return !ownerLive(owner, identityProbe);
|
|
283
658
|
}
|
|
284
659
|
|
|
285
660
|
function signalOwner(owner: OwnerIdentity, platform: NodeJS.Platform, force: boolean): void {
|
|
@@ -303,7 +678,12 @@ function mergeExecutions(...groups: readonly SandboxControlExecution[][]): Sandb
|
|
|
303
678
|
|
|
304
679
|
export async function quiesceSandboxControlRoot(
|
|
305
680
|
root: string,
|
|
306
|
-
options: {
|
|
681
|
+
options: {
|
|
682
|
+
platform?: NodeJS.Platform;
|
|
683
|
+
timeoutMs?: number;
|
|
684
|
+
timing?: SandboxControlTimingPolicy;
|
|
685
|
+
identityProbe?: ProcessIdentityProbe;
|
|
686
|
+
} = {}
|
|
307
687
|
): Promise<'missing' | 'stale' | 'stopped'> {
|
|
308
688
|
const resolvedRoot = path.resolve(root);
|
|
309
689
|
if (!fs.existsSync(resolvedRoot)) return 'missing';
|
|
@@ -317,18 +697,19 @@ export async function quiesceSandboxControlRoot(
|
|
|
317
697
|
}
|
|
318
698
|
|
|
319
699
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
700
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
320
701
|
const deadlineAt = Date.now() + timeoutMs;
|
|
321
702
|
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
322
703
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
323
|
-
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
704
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
324
705
|
|
|
325
706
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
326
|
-
const manifest = fs.existsSync(manifestPath) ?
|
|
707
|
+
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
|
|
327
708
|
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
328
709
|
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
329
710
|
const statusOwner = status ? status.broker : null;
|
|
330
|
-
const brokerLive = broker ? ownerLive(broker) : false;
|
|
331
|
-
const statusLive = statusOwner ? ownerLive(statusOwner) : false;
|
|
711
|
+
const brokerLive = broker ? ownerLive(broker, identityProbe) : false;
|
|
712
|
+
const statusLive = statusOwner ? ownerLive(statusOwner, identityProbe) : false;
|
|
332
713
|
|
|
333
714
|
if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
334
715
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
@@ -347,23 +728,23 @@ export async function quiesceSandboxControlRoot(
|
|
|
347
728
|
if (!owner) {
|
|
348
729
|
if (manifest && !broker && !status) throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
349
730
|
for (const execution of executions) {
|
|
350
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
731
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
351
732
|
}
|
|
352
733
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
353
734
|
while (Date.now() < softDeadlineAt) {
|
|
354
|
-
if (!executions.some((execution) => executionAlive(execution, platform))) break;
|
|
735
|
+
if (!executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
355
736
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
356
737
|
}
|
|
357
738
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
358
739
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
359
740
|
for (const execution of executions) {
|
|
360
741
|
if (!terminateSandboxControlExecution(execution, {
|
|
361
|
-
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now()
|
|
742
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
362
743
|
})) {
|
|
363
744
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
364
745
|
}
|
|
365
746
|
}
|
|
366
|
-
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
747
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
367
748
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
368
749
|
}
|
|
369
750
|
return broker || status ? 'stale' : 'missing';
|
|
@@ -372,28 +753,28 @@ export async function quiesceSandboxControlRoot(
|
|
|
372
753
|
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
373
754
|
signalOwner(owner, platform, false);
|
|
374
755
|
for (const execution of executions) {
|
|
375
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
756
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
376
757
|
}
|
|
377
758
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
378
759
|
while (Date.now() < softDeadlineAt) {
|
|
379
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) break;
|
|
760
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
380
761
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
381
762
|
}
|
|
382
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) {
|
|
763
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
383
764
|
return 'stopped';
|
|
384
765
|
}
|
|
385
766
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
386
767
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
387
768
|
for (const execution of executions) {
|
|
388
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now() });
|
|
769
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
|
|
389
770
|
}
|
|
390
771
|
signalOwner(owner, platform, true);
|
|
391
772
|
while (Date.now() < deadlineAt) {
|
|
392
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) return 'stopped';
|
|
773
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) return 'stopped';
|
|
393
774
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
394
775
|
}
|
|
395
|
-
if (ownerLive(owner)) throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
396
|
-
if (executions.some((execution) => executionAlive(execution, platform))) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
776
|
+
if (ownerLive(owner, identityProbe)) throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
777
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
397
778
|
return 'stopped';
|
|
398
779
|
}
|
|
399
780
|
|
|
@@ -401,6 +782,7 @@ export type RemoveSandboxControlOptions = Readonly<{
|
|
|
401
782
|
platform?: NodeJS.Platform;
|
|
402
783
|
timeoutMs?: number;
|
|
403
784
|
timing?: SandboxControlTimingPolicy;
|
|
785
|
+
identityProbe?: ProcessIdentityProbe;
|
|
404
786
|
inspectContainer?: (timeoutMs: number) => Promise<ContainerObservation>;
|
|
405
787
|
removeContainer: (timeoutMs: number) => Promise<void>;
|
|
406
788
|
requireAbsent?: boolean;
|
|
@@ -416,10 +798,11 @@ export async function removeSandboxControlRoot(
|
|
|
416
798
|
const stat = fs.lstatSync(resolvedRoot);
|
|
417
799
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
418
800
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
419
|
-
const manifest =
|
|
801
|
+
const manifest = readSandboxControlManifestForTransition(manifestPath);
|
|
420
802
|
const inspectContainer = options.inspectContainer
|
|
421
803
|
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
422
804
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
805
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
423
806
|
const deadlineAt = Date.now() + timeoutMs;
|
|
424
807
|
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
425
808
|
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
@@ -432,7 +815,7 @@ export async function removeSandboxControlRoot(
|
|
|
432
815
|
}
|
|
433
816
|
|
|
434
817
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
435
|
-
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
818
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
436
819
|
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
437
820
|
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
438
821
|
const broker = readBrokerOwner(brokerPath);
|
|
@@ -444,12 +827,12 @@ export async function removeSandboxControlRoot(
|
|
|
444
827
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
445
828
|
}
|
|
446
829
|
if (broker && status && !sameOwner(broker, status.broker)
|
|
447
|
-
&& (ownerLive(broker) || ownerLive(status.broker))) {
|
|
830
|
+
&& (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
|
|
448
831
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
449
832
|
}
|
|
450
833
|
const platform = options.platform ?? process.platform;
|
|
451
|
-
const owner = broker && ownerLive(broker) ? broker
|
|
452
|
-
: status && ownerLive(status.broker) ? status.broker : null;
|
|
834
|
+
const owner = broker && ownerLive(broker, identityProbe) ? broker
|
|
835
|
+
: status && ownerLive(status.broker, identityProbe) ? status.broker : null;
|
|
453
836
|
const selfOwner = options.selfOwner;
|
|
454
837
|
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
455
838
|
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
@@ -458,11 +841,12 @@ export async function removeSandboxControlRoot(
|
|
|
458
841
|
let executions = readExecutions(manifest);
|
|
459
842
|
if (owner && !selfOwned) signalOwner(owner, platform, false);
|
|
460
843
|
for (const execution of executions) {
|
|
461
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
844
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
462
845
|
}
|
|
463
846
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
464
847
|
while (Date.now() < softDeadlineAt) {
|
|
465
|
-
if ((!owner || selfOwned || !ownerLive(owner
|
|
848
|
+
if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
|
|
849
|
+
&& !executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
466
850
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
467
851
|
}
|
|
468
852
|
|
|
@@ -485,7 +869,7 @@ export async function removeSandboxControlRoot(
|
|
|
485
869
|
}
|
|
486
870
|
}
|
|
487
871
|
|
|
488
|
-
const currentManifest =
|
|
872
|
+
const currentManifest = readSandboxControlManifestForTransition(manifestPath);
|
|
489
873
|
if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
490
874
|
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
491
875
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
@@ -498,10 +882,10 @@ export async function removeSandboxControlRoot(
|
|
|
498
882
|
if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
|
|
499
883
|
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
500
884
|
}
|
|
501
|
-
if (!owner && currentBroker && ownerLive(currentBroker)) {
|
|
885
|
+
if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
|
|
502
886
|
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
503
887
|
}
|
|
504
|
-
if (owner && ownerLive(owner) && !currentBroker) {
|
|
888
|
+
if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
|
|
505
889
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
506
890
|
}
|
|
507
891
|
|
|
@@ -509,21 +893,21 @@ export async function removeSandboxControlRoot(
|
|
|
509
893
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
510
894
|
for (const execution of executions) {
|
|
511
895
|
if (!terminateSandboxControlExecution(execution, {
|
|
512
|
-
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now()
|
|
896
|
+
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
|
|
513
897
|
})) {
|
|
514
898
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
515
899
|
}
|
|
516
900
|
}
|
|
517
|
-
if (owner && currentBroker && ownerLive(currentBroker) && !selfOwned) {
|
|
901
|
+
if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
|
|
518
902
|
if (remaining() <= 0) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
519
903
|
signalOwner(currentBroker, platform, true);
|
|
520
|
-
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt)) {
|
|
904
|
+
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
|
|
521
905
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
522
906
|
}
|
|
523
907
|
}
|
|
524
908
|
|
|
525
909
|
const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
|
|
526
|
-
if (finalExecutions.some((execution) => executionAlive(execution, platform))) {
|
|
910
|
+
if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
527
911
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
528
912
|
}
|
|
529
913
|
|
|
@@ -533,7 +917,9 @@ export async function removeSandboxControlRoot(
|
|
|
533
917
|
if (!finalBroker) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
534
918
|
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
535
919
|
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
536
|
-
if (ownerLive(finalBroker) && !finalSelfOwner)
|
|
920
|
+
if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
|
|
921
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
922
|
+
}
|
|
537
923
|
if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
|
|
538
924
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
539
925
|
}
|
|
@@ -543,7 +929,8 @@ export async function removeSandboxControlRoot(
|
|
|
543
929
|
if (finalStatusRaw !== null) {
|
|
544
930
|
const finalStatus = readStatusOwner(statusPath);
|
|
545
931
|
if (!finalStatus) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
546
|
-
if (ownerLive(finalStatus.broker
|
|
932
|
+
if (ownerLive(finalStatus.broker, identityProbe)
|
|
933
|
+
&& !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
547
934
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
548
935
|
}
|
|
549
936
|
if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
|