@fitlab-ai/agent-infra 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +10 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
- package/dist/lib/decide.js +68 -14
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- 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 +48 -8
- 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/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/pr-summary.js +5 -3
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +394 -264
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +23 -9
- package/dist/lib/sandbox/control/lifecycle.js +590 -61
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +51 -25
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +135 -50
- package/dist/lib/sandbox/workspace-view.js +59 -20
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +107 -20
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +120 -17
- package/dist/lib/task/events.js +148 -7
- 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/ledger.js +13 -1
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +293 -18
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +80 -22
- 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 +6 -0
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
- package/lib/decide.ts +59 -14
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- 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 +43 -8
- 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/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/pr-summary.ts +10 -3
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +139 -6
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +24 -12
- package/lib/sandbox/control/lifecycle.ts +685 -67
- package/lib/sandbox/control/protocol.ts +35 -6
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +67 -33
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +159 -49
- package/lib/sandbox/workspace-view.ts +73 -22
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +129 -27
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +205 -25
- package/lib/task/events.ts +132 -6
- 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/ledger.ts +16 -1
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +443 -22
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -32
- 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/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
- package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -1,25 +1,61 @@
|
|
|
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
|
+
import {
|
|
7
|
+
getProcessIdentityState,
|
|
8
|
+
getProcessStartTime
|
|
9
|
+
} from '../../server/process-state.ts';
|
|
10
|
+
import type { ProcessIdentity, ProcessIdentityProbe, ProcessIdentityState } from '../../server/process-state.ts';
|
|
6
11
|
import type {
|
|
7
12
|
SandboxControlExecution,
|
|
13
|
+
SandboxControlManifestLike,
|
|
8
14
|
SandboxControlManifest,
|
|
9
|
-
|
|
15
|
+
SandboxControlLegacyManifest,
|
|
16
|
+
SandboxControlStatus,
|
|
17
|
+
SandboxControlTimingPolicy
|
|
10
18
|
} from './protocol.ts';
|
|
19
|
+
import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.ts';
|
|
20
|
+
import { inspectSandboxControlContainer, type ContainerObservation } from './container-identity.ts';
|
|
11
21
|
import {
|
|
12
22
|
parseSandboxControlStatus,
|
|
13
23
|
readExecution,
|
|
14
24
|
terminateSandboxControlExecution
|
|
15
25
|
} from './state.ts';
|
|
16
26
|
|
|
17
|
-
type OwnerIdentity = Readonly<{
|
|
18
|
-
type BrokerOwner =
|
|
27
|
+
type OwnerIdentity = ProcessIdentity & Readonly<{ brokerId?: string }>;
|
|
28
|
+
export type BrokerOwner = ProcessIdentity & Readonly<{ version: 3; brokerId: string; token: string; generation: string }>;
|
|
19
29
|
|
|
20
|
-
const DEFAULT_QUIESCE_TIMEOUT_MS =
|
|
30
|
+
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
21
31
|
const QUIESCING_FILE = 'quiescing.json';
|
|
22
32
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
33
|
+
const REPLACEMENT_FILE = 'replacement.json';
|
|
34
|
+
const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
|
|
35
|
+
|
|
36
|
+
async function awaitWithDeadline<T>(
|
|
37
|
+
operation: () => Promise<T>,
|
|
38
|
+
deadlineAt: number,
|
|
39
|
+
errorCode: string
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
const remaining = deadlineAt - Date.now();
|
|
42
|
+
if (remaining <= 0) throw new Error(errorCode);
|
|
43
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
44
|
+
try {
|
|
45
|
+
return await Promise.race([
|
|
46
|
+
operation(),
|
|
47
|
+
new Promise<T>((_, reject) => {
|
|
48
|
+
timer = setTimeout(() => reject(new Error(errorCode)), remaining);
|
|
49
|
+
})
|
|
50
|
+
]);
|
|
51
|
+
} finally {
|
|
52
|
+
if (timer) clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function forceDeadline(deadlineAt: number, timeoutMs: number): number {
|
|
57
|
+
return Math.max(Date.now(), deadlineAt - Math.max(1, Math.floor(timeoutMs / 2)));
|
|
58
|
+
}
|
|
23
59
|
|
|
24
60
|
function regularFile(filePath: string): boolean {
|
|
25
61
|
try {
|
|
@@ -54,21 +90,25 @@ function readStartupOwner(filePath: string): { raw: string; owner: OwnerIdentity
|
|
|
54
90
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
55
91
|
try {
|
|
56
92
|
const record = JSON.parse(raw) as Partial<OwnerIdentity> & { version?: unknown };
|
|
57
|
-
if (record.version !==
|
|
58
|
-
|| typeof record.startTime !== '
|
|
93
|
+
if (record.version !== 2 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
|
|
94
|
+
|| typeof record.startTime !== 'number' || !Number.isSafeInteger(record.startTime)) return null;
|
|
59
95
|
return { raw, owner: { pid: record.pid!, startTime: record.startTime } };
|
|
60
96
|
} catch {
|
|
61
97
|
return null;
|
|
62
98
|
}
|
|
63
99
|
}
|
|
64
100
|
|
|
65
|
-
async function waitForStartupTransition(
|
|
101
|
+
async function waitForStartupTransition(
|
|
102
|
+
root: string,
|
|
103
|
+
timeoutMs: number,
|
|
104
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
105
|
+
): Promise<void> {
|
|
66
106
|
const transitionPath = path.join(root, BROKER_STARTING_FILE);
|
|
67
107
|
const deadline = Date.now() + timeoutMs;
|
|
68
108
|
while (Date.now() < deadline) {
|
|
69
109
|
const transition = readStartupOwner(transitionPath);
|
|
70
110
|
if (!fs.existsSync(transitionPath)) return;
|
|
71
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
111
|
+
if (transition && !ownerLive(transition.owner, identityProbe)) {
|
|
72
112
|
try {
|
|
73
113
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw) fs.unlinkSync(transitionPath);
|
|
74
114
|
} catch {
|
|
@@ -84,11 +124,12 @@ async function waitForStartupTransition(root: string, timeoutMs: number): Promis
|
|
|
84
124
|
export async function acquireSandboxControlBrokerStartup(
|
|
85
125
|
root: string,
|
|
86
126
|
owner: OwnerIdentity,
|
|
87
|
-
timeoutMs = 5_000
|
|
127
|
+
timeoutMs = 5_000,
|
|
128
|
+
options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
|
|
88
129
|
): Promise<() => void> {
|
|
89
130
|
const resolvedRoot = path.resolve(root);
|
|
90
131
|
const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
91
|
-
const record = `${JSON.stringify({ version:
|
|
132
|
+
const record = `${JSON.stringify({ version: 2, ...owner })}\n`;
|
|
92
133
|
const deadline = Date.now() + timeoutMs;
|
|
93
134
|
while (Date.now() < deadline) {
|
|
94
135
|
if (isSandboxControlRootQuiescing(resolvedRoot)) throw new Error('SANDBOX_CONTROL_QUIESCING');
|
|
@@ -112,7 +153,7 @@ export async function acquireSandboxControlBrokerStartup(
|
|
|
112
153
|
} catch (error) {
|
|
113
154
|
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
114
155
|
const transition = readStartupOwner(transitionPath);
|
|
115
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
156
|
+
if (transition && !ownerLive(transition.owner, options.identityProbe)) {
|
|
116
157
|
try {
|
|
117
158
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw) fs.unlinkSync(transitionPath);
|
|
118
159
|
} catch {
|
|
@@ -126,31 +167,381 @@ export async function acquireSandboxControlBrokerStartup(
|
|
|
126
167
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
127
168
|
}
|
|
128
169
|
|
|
129
|
-
|
|
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 {
|
|
130
491
|
if (!regularFile(manifestPath)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
131
|
-
let manifest:
|
|
492
|
+
let manifest: Record<string, unknown>;
|
|
132
493
|
try {
|
|
133
|
-
|
|
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>;
|
|
134
499
|
} catch {
|
|
135
500
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
136
501
|
}
|
|
137
|
-
|
|
138
|
-
|
|
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');
|
|
139
505
|
}
|
|
140
|
-
if (typeof
|
|
141
|
-
||
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|| typeof
|
|
145
|
-
|| typeof
|
|
146
|
-
|| typeof
|
|
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');
|
|
147
521
|
const root = path.dirname(path.resolve(manifestPath));
|
|
148
|
-
if (path.resolve(
|
|
149
|
-
|| path.resolve(
|
|
150
|
-
|| 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')) {
|
|
151
525
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
152
526
|
}
|
|
153
|
-
|
|
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);
|
|
154
545
|
}
|
|
155
546
|
|
|
156
547
|
function readBrokerOwner(filePath: string): BrokerOwner | null {
|
|
@@ -162,9 +553,10 @@ function readBrokerOwner(filePath: string): BrokerOwner | null {
|
|
|
162
553
|
} catch {
|
|
163
554
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
164
555
|
}
|
|
165
|
-
if (!owner || owner.version !==
|
|
166
|
-
|| typeof owner.startTime !== '
|
|
167
|
-
|| typeof owner.
|
|
556
|
+
if (!owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
|
|
557
|
+
|| typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
|
|
558
|
+
|| typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
|
|
559
|
+
|| typeof owner.token !== 'string' || typeof owner.generation !== 'string') throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
168
560
|
return owner as BrokerOwner;
|
|
169
561
|
}
|
|
170
562
|
|
|
@@ -179,14 +571,20 @@ function readStatusOwner(filePath: string): SandboxControlStatus | null {
|
|
|
179
571
|
}
|
|
180
572
|
|
|
181
573
|
function sameOwner(left: OwnerIdentity, right: OwnerIdentity): boolean {
|
|
182
|
-
return left.pid === right.pid && left.startTime === right.startTime
|
|
574
|
+
return left.pid === right.pid && left.startTime === right.startTime
|
|
575
|
+
&& (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
|
|
183
576
|
}
|
|
184
577
|
|
|
185
|
-
function ownerLive(
|
|
186
|
-
|
|
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';
|
|
187
585
|
}
|
|
188
586
|
|
|
189
|
-
function readExecutions(manifest:
|
|
587
|
+
function readExecutions(manifest: SandboxControlManifestLike): SandboxControlExecution[] {
|
|
190
588
|
if (!fs.existsSync(manifest.processingDir)) return [];
|
|
191
589
|
const stat = fs.lstatSync(manifest.processingDir);
|
|
192
590
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
@@ -213,37 +611,50 @@ function readExecutions(manifest: SandboxControlManifest): SandboxControlExecuti
|
|
|
213
611
|
return executions;
|
|
214
612
|
}
|
|
215
613
|
|
|
216
|
-
function
|
|
614
|
+
function processGroupState(groupId: number): ProcessIdentityState {
|
|
217
615
|
try {
|
|
218
616
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
219
617
|
return rows.split('\n').some((row) => {
|
|
220
618
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
221
619
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
222
|
-
});
|
|
620
|
+
}) ? 'alive' : 'dead';
|
|
223
621
|
} catch {
|
|
224
622
|
try {
|
|
225
623
|
process.kill(-groupId, 0);
|
|
226
|
-
return
|
|
624
|
+
return 'alive';
|
|
227
625
|
} catch (error) {
|
|
228
|
-
|
|
626
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
627
|
+
if (code === 'ESRCH') return 'dead';
|
|
628
|
+
if (code === 'EPERM') return 'alive';
|
|
629
|
+
return 'unknown';
|
|
229
630
|
}
|
|
230
631
|
}
|
|
231
632
|
}
|
|
232
633
|
|
|
233
|
-
function executionAlive(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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';
|
|
238
644
|
}
|
|
239
645
|
|
|
240
|
-
async function waitForExit(
|
|
241
|
-
|
|
646
|
+
async function waitForExit(
|
|
647
|
+
owner: OwnerIdentity,
|
|
648
|
+
timeoutMs: number,
|
|
649
|
+
deadlineAt?: number,
|
|
650
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
651
|
+
): Promise<boolean> {
|
|
652
|
+
const deadline = deadlineAt ?? (Date.now() + timeoutMs);
|
|
242
653
|
while (Date.now() < deadline) {
|
|
243
|
-
if (!ownerLive(owner)) return true;
|
|
654
|
+
if (!ownerLive(owner, identityProbe)) return true;
|
|
244
655
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
245
656
|
}
|
|
246
|
-
return !ownerLive(owner);
|
|
657
|
+
return !ownerLive(owner, identityProbe);
|
|
247
658
|
}
|
|
248
659
|
|
|
249
660
|
function signalOwner(owner: OwnerIdentity, platform: NodeJS.Platform, force: boolean): void {
|
|
@@ -267,7 +678,12 @@ function mergeExecutions(...groups: readonly SandboxControlExecution[][]): Sandb
|
|
|
267
678
|
|
|
268
679
|
export async function quiesceSandboxControlRoot(
|
|
269
680
|
root: string,
|
|
270
|
-
options: {
|
|
681
|
+
options: {
|
|
682
|
+
platform?: NodeJS.Platform;
|
|
683
|
+
timeoutMs?: number;
|
|
684
|
+
timing?: SandboxControlTimingPolicy;
|
|
685
|
+
identityProbe?: ProcessIdentityProbe;
|
|
686
|
+
} = {}
|
|
271
687
|
): Promise<'missing' | 'stale' | 'stopped'> {
|
|
272
688
|
const resolvedRoot = path.resolve(root);
|
|
273
689
|
if (!fs.existsSync(resolvedRoot)) return 'missing';
|
|
@@ -280,16 +696,20 @@ export async function quiesceSandboxControlRoot(
|
|
|
280
696
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
281
697
|
}
|
|
282
698
|
|
|
699
|
+
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
700
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
701
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
702
|
+
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
283
703
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
284
|
-
await waitForStartupTransition(resolvedRoot,
|
|
704
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
285
705
|
|
|
286
706
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
287
|
-
const manifest = fs.existsSync(manifestPath) ?
|
|
707
|
+
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
|
|
288
708
|
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
289
709
|
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
290
|
-
const statusOwner = status ?
|
|
291
|
-
const brokerLive = broker ? ownerLive(broker) : false;
|
|
292
|
-
const statusLive = statusOwner ? ownerLive(statusOwner) : false;
|
|
710
|
+
const statusOwner = status ? status.broker : null;
|
|
711
|
+
const brokerLive = broker ? ownerLive(broker, identityProbe) : false;
|
|
712
|
+
const statusLive = statusOwner ? ownerLive(statusOwner, identityProbe) : false;
|
|
293
713
|
|
|
294
714
|
if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
295
715
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
@@ -303,40 +723,238 @@ export async function quiesceSandboxControlRoot(
|
|
|
303
723
|
if (!manifest && brokerLive) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
304
724
|
|
|
305
725
|
const platform = options.platform ?? process.platform;
|
|
306
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
307
726
|
let executions = manifest ? readExecutions(manifest) : [];
|
|
308
727
|
const owner: OwnerIdentity | null = brokerLive ? broker : statusLive ? statusOwner : null;
|
|
309
728
|
if (!owner) {
|
|
310
729
|
if (manifest && !broker && !status) throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
311
730
|
for (const execution of executions) {
|
|
312
|
-
|
|
731
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
732
|
+
}
|
|
733
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
734
|
+
while (Date.now() < softDeadlineAt) {
|
|
735
|
+
if (!executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
736
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
737
|
+
}
|
|
738
|
+
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
739
|
+
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
740
|
+
for (const execution of executions) {
|
|
741
|
+
if (!terminateSandboxControlExecution(execution, {
|
|
742
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
743
|
+
})) {
|
|
313
744
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
314
745
|
}
|
|
315
746
|
}
|
|
316
|
-
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
747
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
317
748
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
318
749
|
}
|
|
319
750
|
return broker || status ? 'stale' : 'missing';
|
|
320
751
|
}
|
|
321
752
|
|
|
753
|
+
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
322
754
|
signalOwner(owner, platform, false);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
755
|
+
for (const execution of executions) {
|
|
756
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
757
|
+
}
|
|
758
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
759
|
+
while (Date.now() < softDeadlineAt) {
|
|
760
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
761
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
762
|
+
}
|
|
763
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
327
764
|
return 'stopped';
|
|
328
765
|
}
|
|
329
|
-
|
|
330
766
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
767
|
+
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
768
|
+
for (const execution of executions) {
|
|
769
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
|
|
770
|
+
}
|
|
771
|
+
signalOwner(owner, platform, true);
|
|
772
|
+
while (Date.now() < deadlineAt) {
|
|
773
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) return 'stopped';
|
|
774
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
775
|
+
}
|
|
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');
|
|
778
|
+
return 'stopped';
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export type RemoveSandboxControlOptions = Readonly<{
|
|
782
|
+
platform?: NodeJS.Platform;
|
|
783
|
+
timeoutMs?: number;
|
|
784
|
+
timing?: SandboxControlTimingPolicy;
|
|
785
|
+
identityProbe?: ProcessIdentityProbe;
|
|
786
|
+
inspectContainer?: (timeoutMs: number) => Promise<ContainerObservation>;
|
|
787
|
+
removeContainer: (timeoutMs: number) => Promise<void>;
|
|
788
|
+
requireAbsent?: boolean;
|
|
789
|
+
selfOwner?: BrokerOwner;
|
|
790
|
+
}>;
|
|
791
|
+
|
|
792
|
+
export async function removeSandboxControlRoot(
|
|
793
|
+
root: string,
|
|
794
|
+
options: RemoveSandboxControlOptions
|
|
795
|
+
): Promise<void> {
|
|
796
|
+
const resolvedRoot = path.resolve(root);
|
|
797
|
+
if (!fs.existsSync(resolvedRoot)) return;
|
|
798
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
799
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
800
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
801
|
+
const manifest = readSandboxControlManifestForTransition(manifestPath);
|
|
802
|
+
const inspectContainer = options.inspectContainer
|
|
803
|
+
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
804
|
+
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
805
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
806
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
807
|
+
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
808
|
+
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
809
|
+
const observation = await awaitWithDeadline(
|
|
810
|
+
() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
|
|
811
|
+
);
|
|
812
|
+
if (observation.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
|
|
813
|
+
if (options.requireAbsent && observation.state !== 'absent') {
|
|
814
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
818
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
819
|
+
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
820
|
+
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
821
|
+
const broker = readBrokerOwner(brokerPath);
|
|
822
|
+
const status = readStatusOwner(statusPath);
|
|
823
|
+
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
824
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
825
|
+
}
|
|
826
|
+
if (status && status.generation !== manifest.generation) {
|
|
827
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
828
|
+
}
|
|
829
|
+
if (broker && status && !sameOwner(broker, status.broker)
|
|
830
|
+
&& (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
|
|
831
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
832
|
+
}
|
|
833
|
+
const platform = options.platform ?? process.platform;
|
|
834
|
+
const owner = broker && ownerLive(broker, identityProbe) ? broker
|
|
835
|
+
: status && ownerLive(status.broker, identityProbe) ? status.broker : null;
|
|
836
|
+
const selfOwner = options.selfOwner;
|
|
837
|
+
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
838
|
+
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
839
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
840
|
+
}
|
|
841
|
+
let executions = readExecutions(manifest);
|
|
842
|
+
if (owner && !selfOwned) signalOwner(owner, platform, false);
|
|
843
|
+
for (const execution of executions) {
|
|
844
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
845
|
+
}
|
|
846
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
847
|
+
while (Date.now() < softDeadlineAt) {
|
|
848
|
+
if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
|
|
849
|
+
&& !executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
850
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (observation.state === 'found') {
|
|
854
|
+
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
855
|
+
await awaitWithDeadline(
|
|
856
|
+
() => options.removeContainer(remaining()),
|
|
857
|
+
deadlineAt,
|
|
858
|
+
'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
|
|
859
|
+
);
|
|
860
|
+
const afterRemoval = await awaitWithDeadline(
|
|
861
|
+
() => inspectContainer(remaining()),
|
|
862
|
+
deadlineAt,
|
|
863
|
+
'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
|
|
864
|
+
);
|
|
865
|
+
if (afterRemoval.state !== 'absent') {
|
|
866
|
+
throw new Error(afterRemoval.state === 'unknown'
|
|
867
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
|
|
868
|
+
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const currentManifest = readSandboxControlManifestForTransition(manifestPath);
|
|
873
|
+
if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
874
|
+
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
875
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
876
|
+
}
|
|
877
|
+
const currentBroker = readBrokerOwner(brokerPath);
|
|
878
|
+
if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
|
|
879
|
+
|| currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
|
|
880
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
881
|
+
}
|
|
882
|
+
if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
|
|
883
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
884
|
+
}
|
|
885
|
+
if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
|
|
886
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
887
|
+
}
|
|
888
|
+
if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
|
|
889
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
893
|
+
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
331
894
|
for (const execution of executions) {
|
|
332
|
-
if (!terminateSandboxControlExecution(execution, {
|
|
895
|
+
if (!terminateSandboxControlExecution(execution, {
|
|
896
|
+
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
|
|
897
|
+
})) {
|
|
333
898
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
334
899
|
}
|
|
335
900
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
901
|
+
if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
|
|
902
|
+
if (remaining() <= 0) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
903
|
+
signalOwner(currentBroker, platform, true);
|
|
904
|
+
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
|
|
905
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
|
|
910
|
+
if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
339
911
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
340
912
|
}
|
|
341
|
-
|
|
913
|
+
|
|
914
|
+
const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
|
|
915
|
+
if (finalBrokerRaw !== null) {
|
|
916
|
+
const finalBroker = readBrokerOwner(brokerPath);
|
|
917
|
+
if (!finalBroker) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
918
|
+
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
919
|
+
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
920
|
+
if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
|
|
921
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
922
|
+
}
|
|
923
|
+
if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
|
|
924
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
925
|
+
}
|
|
926
|
+
fs.unlinkSync(brokerPath);
|
|
927
|
+
}
|
|
928
|
+
const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
|
|
929
|
+
if (finalStatusRaw !== null) {
|
|
930
|
+
const finalStatus = readStatusOwner(statusPath);
|
|
931
|
+
if (!finalStatus) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
932
|
+
if (ownerLive(finalStatus.broker, identityProbe)
|
|
933
|
+
&& !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
934
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
935
|
+
}
|
|
936
|
+
if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
|
|
937
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
938
|
+
}
|
|
939
|
+
fs.unlinkSync(statusPath);
|
|
940
|
+
}
|
|
941
|
+
if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
|
|
942
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
|
|
943
|
+
}
|
|
944
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export type GarbageCollectSandboxControlOptions = Omit<RemoveSandboxControlOptions, 'removeContainer' | 'requireAbsent'>;
|
|
948
|
+
|
|
949
|
+
export async function garbageCollectSandboxControlRoot(
|
|
950
|
+
root: string,
|
|
951
|
+
options: GarbageCollectSandboxControlOptions = {}
|
|
952
|
+
): Promise<void> {
|
|
953
|
+
await removeSandboxControlRoot(path.resolve(root), {
|
|
954
|
+
...options,
|
|
955
|
+
requireAbsent: true,
|
|
956
|
+
removeContainer: async () => {
|
|
957
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
958
|
+
}
|
|
959
|
+
});
|
|
342
960
|
}
|