@fitlab-ai/agent-infra 0.9.6 → 0.9.7
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/codex-lifecycle/app-server.js +45 -4
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/task-orchestration.js +37 -5
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- 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/sandbox/commands/create.js +103 -51
- 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 +13 -7
- package/dist/lib/sandbox/control/lifecycle.js +238 -29
- 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 +25 -15
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/mounts.js +5 -13
- package/dist/lib/sandbox/recovery.js +59 -42
- package/dist/lib/sandbox/workspace-view.js +57 -40
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +57 -20
- 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/delegation-receipts.js +84 -2
- package/dist/lib/task/events.js +139 -2
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +235 -14
- 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 +40 -8
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/task-orchestration.ts +31 -4
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -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/sandbox/commands/create.ts +56 -7
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +14 -10
- package/lib/sandbox/control/lifecycle.ts +264 -33
- package/lib/sandbox/control/protocol.ts +31 -5
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +34 -17
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/mounts.ts +5 -15
- package/lib/sandbox/recovery.ts +71 -42
- package/lib/sandbox/workspace-view.ts +61 -40
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +82 -27
- 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/delegation-receipts.ts +160 -8
- package/lib/task/events.ts +122 -1
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +356 -17
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +51 -16
- 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 +25 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- 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 +2 -0
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
- 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
|
@@ -11,7 +11,8 @@ import { assertManagedPath, removeManagedDir, removeWorktreeDir } from '../manag
|
|
|
11
11
|
import { runEngine, runOk, runOkEngine, runSafe, runSafeEngine } from '../shell.js';
|
|
12
12
|
import { resolveSandboxTarget } from '../workspace-identity.js';
|
|
13
13
|
import { sandboxControlPaths, sandboxWorkspaceViewPaths } from '../workspace-view.js';
|
|
14
|
-
import {
|
|
14
|
+
import { removeSandboxControlRoot, readSandboxControlManifest } from '../control/lifecycle.js';
|
|
15
|
+
import { inspectSandboxControlContainer } from '../control/container-identity.js';
|
|
15
16
|
import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.js';
|
|
16
17
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
|
|
17
18
|
import { fetchSandboxRows } from './list-running.js';
|
|
@@ -118,10 +119,39 @@ function removeEmptyManagedParent(base, directory) {
|
|
|
118
119
|
}
|
|
119
120
|
assertRemoved(parent, 'Empty sandbox container directory');
|
|
120
121
|
}
|
|
121
|
-
async function
|
|
122
|
+
async function removeExactSandboxContainer(engine, manifest, timeoutMs) {
|
|
123
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
124
|
+
const remaining = () => Math.max(1, deadlineAt - Date.now());
|
|
125
|
+
const inspect = () => inspectSandboxControlContainer(manifest, { timeoutMs: remaining() });
|
|
126
|
+
const before = await inspect();
|
|
127
|
+
if (before.state === 'unknown')
|
|
128
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${before.reason}`);
|
|
129
|
+
if (before.state === 'absent')
|
|
130
|
+
return;
|
|
131
|
+
if (before.running && !runOkEngine(engine, 'docker', ['stop', manifest.containerIdentity.id], { timeout: remaining() })) {
|
|
132
|
+
const afterStop = await inspect();
|
|
133
|
+
if (afterStop.state !== 'absent' && afterStop.state !== 'found') {
|
|
134
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
|
|
135
|
+
}
|
|
136
|
+
if (afterStop.state === 'found' && afterStop.running) {
|
|
137
|
+
throw new Error(`Failed to stop sandbox container: ${manifest.containerIdentity.id}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const afterStop = await inspect();
|
|
141
|
+
if (afterStop.state === 'unknown')
|
|
142
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterStop.reason}`);
|
|
143
|
+
if (afterStop.state === 'found' && !runOkEngine(engine, 'docker', ['rm', manifest.containerIdentity.id], { timeout: remaining() })) {
|
|
144
|
+
const afterRemove = await inspect();
|
|
145
|
+
if (afterRemove.state !== 'absent') {
|
|
146
|
+
throw new Error(`Failed to remove sandbox container: ${manifest.containerIdentity.id}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function removeProjectControlRoots(config, engine) {
|
|
151
|
+
const containers = new Set();
|
|
122
152
|
const projectRoot = path.join(config.controlBase, config.project);
|
|
123
153
|
if (!fs.existsSync(projectRoot))
|
|
124
|
-
return;
|
|
154
|
+
return containers;
|
|
125
155
|
assertManagedPath(config.controlBase, projectRoot);
|
|
126
156
|
const projectStat = fs.lstatSync(projectRoot);
|
|
127
157
|
if (!projectStat.isDirectory() || projectStat.isSymbolicLink())
|
|
@@ -133,9 +163,19 @@ async function quiesceProjectControlRoots(config) {
|
|
|
133
163
|
for (const identity of fs.readdirSync(containerRoot, { withFileTypes: true })) {
|
|
134
164
|
if (!identity.isDirectory() || identity.isSymbolicLink())
|
|
135
165
|
continue;
|
|
136
|
-
|
|
166
|
+
const root = path.join(containerRoot, identity.name);
|
|
167
|
+
const manifestPath = path.join(root, 'manifest.json');
|
|
168
|
+
if (!fs.existsSync(manifestPath))
|
|
169
|
+
throw new Error(`SANDBOX_CONTROL_TARGET_EVIDENCE_MISSING: ${root}`);
|
|
170
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
171
|
+
containers.add(manifest.container);
|
|
172
|
+
await removeSandboxControlRoot(root, {
|
|
173
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
|
|
174
|
+
removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
|
|
175
|
+
});
|
|
137
176
|
}
|
|
138
177
|
}
|
|
178
|
+
return containers;
|
|
139
179
|
}
|
|
140
180
|
function inspectionBlockers(inspections) {
|
|
141
181
|
return inspections.filter((inspection) => inspection.status !== 'clean');
|
|
@@ -211,25 +251,32 @@ async function rmOne(config, tools, branch, options = {}) {
|
|
|
211
251
|
p.outro('Cancelled');
|
|
212
252
|
return;
|
|
213
253
|
}
|
|
254
|
+
const coordinatedContainers = new Set();
|
|
214
255
|
for (const root of controlRoots.filter((candidate) => fs.existsSync(candidate))) {
|
|
215
256
|
assertLegacyCandidateEvidence(root, config.controlBase, config, effectiveBranch, matchedContainers);
|
|
216
257
|
assertControlRootMatchesTarget(root, effectiveBranch, workspace);
|
|
217
|
-
|
|
258
|
+
const manifest = readSandboxControlManifest(path.join(root, 'manifest.json'));
|
|
259
|
+
coordinatedContainers.add(manifest.container);
|
|
260
|
+
await removeSandboxControlRoot(root, {
|
|
261
|
+
inspectContainer: (timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }),
|
|
262
|
+
removeContainer: (timeoutMs) => removeExactSandboxContainer(engine, manifest, timeoutMs)
|
|
263
|
+
});
|
|
218
264
|
}
|
|
219
|
-
|
|
265
|
+
const uncoordinatedContainers = matchedContainers.filter((name) => !coordinatedContainers.has(name));
|
|
266
|
+
if (uncoordinatedContainers.length > 0) {
|
|
220
267
|
const spinner = p.spinner();
|
|
221
|
-
spinner.start(`Stopping container(s): ${
|
|
222
|
-
for (const name of
|
|
268
|
+
spinner.start(`Stopping container(s): ${uncoordinatedContainers.join(', ')}`);
|
|
269
|
+
for (const name of uncoordinatedContainers) {
|
|
223
270
|
if (!runOkEngine(engine, 'docker', ['stop', name]))
|
|
224
271
|
throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
225
272
|
if (!runOkEngine(engine, 'docker', ['rm', name]))
|
|
226
273
|
throw new Error(`Failed to remove sandbox container: ${name}`);
|
|
227
274
|
}
|
|
228
275
|
const remaining = runEngine(engine, 'docker', ['ps', '-a', '--format', '{{.Names}}']).split('\n').filter(Boolean);
|
|
229
|
-
const leftovers =
|
|
276
|
+
const leftovers = uncoordinatedContainers.filter((name) => remaining.includes(name));
|
|
230
277
|
if (leftovers.length > 0)
|
|
231
278
|
throw new Error(`Sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
|
|
232
|
-
spinner.stop(pc.green(`Removed container(s): ${
|
|
279
|
+
spinner.stop(pc.green(`Removed container(s): ${uncoordinatedContainers.join(', ')}`));
|
|
233
280
|
}
|
|
234
281
|
else {
|
|
235
282
|
p.log.warn(`No sandbox container found for '${branch}'`);
|
|
@@ -330,7 +377,7 @@ async function rmPurge(config, tools, prompt = {}) {
|
|
|
330
377
|
throw new Error(`Refusing to purge because worktree preflight found blocker(s):\n${blockerMessage(blockers)}`);
|
|
331
378
|
}
|
|
332
379
|
const permits = cleanPermits(inspections);
|
|
333
|
-
await
|
|
380
|
+
const coordinatedContainers = await removeProjectControlRoots(config, engine);
|
|
334
381
|
const containers = runEngine(engine, 'docker', [
|
|
335
382
|
'ps',
|
|
336
383
|
'-a',
|
|
@@ -339,10 +386,11 @@ async function rmPurge(config, tools, prompt = {}) {
|
|
|
339
386
|
'--format',
|
|
340
387
|
'{{.Names}}'
|
|
341
388
|
]);
|
|
342
|
-
|
|
389
|
+
const uncoordinatedContainers = containers.split('\n').filter((name) => name && !coordinatedContainers.has(name));
|
|
390
|
+
if (uncoordinatedContainers.length > 0) {
|
|
343
391
|
const spinner = p.spinner();
|
|
344
392
|
spinner.start('Stopping project sandbox containers...');
|
|
345
|
-
for (const name of
|
|
393
|
+
for (const name of uncoordinatedContainers) {
|
|
346
394
|
if (!runOkEngine(engine, 'docker', ['stop', name]))
|
|
347
395
|
throw new Error(`Failed to stop sandbox container: ${name}`);
|
|
348
396
|
if (!runOkEngine(engine, 'docker', ['rm', name]))
|
|
@@ -351,8 +399,9 @@ async function rmPurge(config, tools, prompt = {}) {
|
|
|
351
399
|
const remaining = runEngine(engine, 'docker', [
|
|
352
400
|
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
353
401
|
]);
|
|
354
|
-
|
|
355
|
-
|
|
402
|
+
const leftovers = remaining.split('\n').filter((name) => name && !coordinatedContainers.has(name));
|
|
403
|
+
if (leftovers.length > 0)
|
|
404
|
+
throw new Error(`Project sandbox container(s) still exist after removal: ${leftovers.join(', ')}`);
|
|
356
405
|
spinner.stop(pc.green('Project sandbox containers removed'));
|
|
357
406
|
}
|
|
358
407
|
else {
|
|
@@ -8,9 +8,9 @@ const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
|
|
|
8
8
|
|
|
9
9
|
Start an existing sandbox container that has stopped (for example after the
|
|
10
10
|
Docker daemon was restarted or replaced). The container must already exist:
|
|
11
|
-
if none is found, run 'ai sandbox create <branch>' first.
|
|
12
|
-
already running is checked without modifying healthy runtime
|
|
13
|
-
|
|
11
|
+
if none is found, run 'ai sandbox create <branch>' first. Without --recreate, a
|
|
12
|
+
container that is already running is checked without modifying healthy runtime
|
|
13
|
+
state. --recreate replaces the existing container even when it is healthy.`;
|
|
14
14
|
export function parseStartArgs(args) {
|
|
15
15
|
let recreate = false;
|
|
16
16
|
let help = false;
|
|
@@ -62,6 +62,7 @@ export async function start(args) {
|
|
|
62
62
|
workspace: target.workspace,
|
|
63
63
|
row: found,
|
|
64
64
|
allowRecreate: parsed.recreate,
|
|
65
|
+
forceRecreate: parsed.recreate,
|
|
65
66
|
recreate: async () => {
|
|
66
67
|
const { create } = await import('./create.js');
|
|
67
68
|
await create([target.requestedRef, '--no-refresh']);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { commandForEngine, runEngine, runProbe } from '../shell.js';
|
|
2
|
+
function isAuthoritativeNotFound(error) {
|
|
3
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4
|
+
return /(?:no such container|container .* not found|cannot find container)/i.test(message);
|
|
5
|
+
}
|
|
6
|
+
export function classifySandboxContainerInspection(expected, inspection) {
|
|
7
|
+
if ('notFound' in inspection)
|
|
8
|
+
return { state: 'absent', id: expected.id };
|
|
9
|
+
if ('error' in inspection) {
|
|
10
|
+
return isAuthoritativeNotFound(inspection.error)
|
|
11
|
+
? { state: 'absent', id: expected.id }
|
|
12
|
+
: { state: 'unknown', reason: `container inspection failed: ${inspection.error instanceof Error ? inspection.error.message : String(inspection.error)}` };
|
|
13
|
+
}
|
|
14
|
+
const response = inspection;
|
|
15
|
+
if (typeof response.id !== 'string' || response.id !== expected.id
|
|
16
|
+
|| typeof response.running !== 'boolean' || !response.labels
|
|
17
|
+
|| typeof response.labels !== 'object' || Array.isArray(response.labels)) {
|
|
18
|
+
return { state: 'unknown', reason: 'container inspection response is incomplete' };
|
|
19
|
+
}
|
|
20
|
+
const labels = response.labels;
|
|
21
|
+
for (const [key, value] of Object.entries(expected.labels)) {
|
|
22
|
+
if (labels[key] !== value)
|
|
23
|
+
return { state: 'unknown', reason: 'container labels do not match the manifest' };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
state: 'found',
|
|
27
|
+
id: response.id,
|
|
28
|
+
running: response.running,
|
|
29
|
+
labels: Object.fromEntries(Object.entries(labels).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : []))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function inspectSandboxControlContainer(manifest, options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const args = ['inspect', '--format', '{{json .}}', manifest.containerIdentity.id];
|
|
35
|
+
let raw;
|
|
36
|
+
if (options.run) {
|
|
37
|
+
raw = options.run(manifest.engine, 'docker', args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const command = commandForEngine(manifest.engine, 'docker', args);
|
|
41
|
+
const result = (options.probe ?? runProbe)(command.cmd, command.args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
|
|
42
|
+
if (result.status !== 0) {
|
|
43
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
|
|
44
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, {
|
|
45
|
+
error: new Error(stderr || `container inspection exited with ${result.status ?? 'unknown'}`)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
raw = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
|
|
49
|
+
}
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
const value = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
|
|
52
|
+
if (!value || Array.isArray(value))
|
|
53
|
+
return { state: 'unknown', reason: 'container inspection response is incomplete' };
|
|
54
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, {
|
|
55
|
+
id: value.Id,
|
|
56
|
+
running: value.State?.Running,
|
|
57
|
+
labels: value.Config?.Labels
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, { error });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=container-identity.js.map
|
|
@@ -18,7 +18,7 @@ async function waitForStartTime(pid, timeoutMs = 2_000) {
|
|
|
18
18
|
const deadline = Date.now() + timeoutMs;
|
|
19
19
|
while (Date.now() < deadline) {
|
|
20
20
|
const startTime = getProcessStartTime(pid);
|
|
21
|
-
if (startTime)
|
|
21
|
+
if (startTime !== null)
|
|
22
22
|
return startTime;
|
|
23
23
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
24
24
|
}
|
|
@@ -48,7 +48,7 @@ export async function prepareSandboxControlExecution(params) {
|
|
|
48
48
|
});
|
|
49
49
|
const startTime = await waitForStartTime(child.pid);
|
|
50
50
|
const execution = {
|
|
51
|
-
version:
|
|
51
|
+
version: 2,
|
|
52
52
|
generation: params.manifest.generation,
|
|
53
53
|
requestId: params.request.id,
|
|
54
54
|
nonce,
|
|
@@ -63,17 +63,23 @@ export async function prepareSandboxControlExecution(params) {
|
|
|
63
63
|
atomicWriteJson(executionPath(params.manifest, params.request.id), execution);
|
|
64
64
|
return {
|
|
65
65
|
execution,
|
|
66
|
-
start() {
|
|
66
|
+
start(canWrite = () => true) {
|
|
67
67
|
if (!child.connected)
|
|
68
68
|
throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
|
|
69
|
+
if (!canWrite())
|
|
70
|
+
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
69
71
|
atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
72
|
+
if (!canWrite())
|
|
73
|
+
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
70
74
|
child.send({ version: 1, nonce });
|
|
71
75
|
},
|
|
72
76
|
completion,
|
|
73
|
-
terminate() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
terminate(updateState = true) {
|
|
78
|
+
if (updateState) {
|
|
79
|
+
atomicWriteJson(executionPath(params.manifest, params.request.id), {
|
|
80
|
+
...execution, phase: 'terminating', updatedAt: Date.now()
|
|
81
|
+
});
|
|
82
|
+
}
|
|
77
83
|
return terminateSandboxControlExecution(execution);
|
|
78
84
|
}
|
|
79
85
|
};
|
|
@@ -3,10 +3,33 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { buildProcessTreeStopCommand } from '../../server/process-control.js';
|
|
5
5
|
import { processIdentityMatches } from '../../server/process-state.js';
|
|
6
|
+
import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
|
|
7
|
+
import { inspectSandboxControlContainer } from './container-identity.js';
|
|
6
8
|
import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
|
|
7
|
-
const DEFAULT_QUIESCE_TIMEOUT_MS =
|
|
9
|
+
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
8
10
|
const QUIESCING_FILE = 'quiescing.json';
|
|
9
11
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
12
|
+
async function awaitWithDeadline(operation, deadlineAt, errorCode) {
|
|
13
|
+
const remaining = deadlineAt - Date.now();
|
|
14
|
+
if (remaining <= 0)
|
|
15
|
+
throw new Error(errorCode);
|
|
16
|
+
let timer;
|
|
17
|
+
try {
|
|
18
|
+
return await Promise.race([
|
|
19
|
+
operation(),
|
|
20
|
+
new Promise((_, reject) => {
|
|
21
|
+
timer = setTimeout(() => reject(new Error(errorCode)), remaining);
|
|
22
|
+
})
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (timer)
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function forceDeadline(deadlineAt, timeoutMs) {
|
|
31
|
+
return Math.max(Date.now(), deadlineAt - Math.max(1, Math.floor(timeoutMs / 2)));
|
|
32
|
+
}
|
|
10
33
|
function regularFile(filePath) {
|
|
11
34
|
try {
|
|
12
35
|
const stat = fs.lstatSync(filePath);
|
|
@@ -44,8 +67,8 @@ function readStartupOwner(filePath) {
|
|
|
44
67
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
45
68
|
try {
|
|
46
69
|
const record = JSON.parse(raw);
|
|
47
|
-
if (record.version !==
|
|
48
|
-
|| typeof record.startTime !== '
|
|
70
|
+
if (record.version !== 2 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
|
|
71
|
+
|| typeof record.startTime !== 'number' || !Number.isSafeInteger(record.startTime))
|
|
49
72
|
return null;
|
|
50
73
|
return { raw, owner: { pid: record.pid, startTime: record.startTime } };
|
|
51
74
|
}
|
|
@@ -77,7 +100,7 @@ async function waitForStartupTransition(root, timeoutMs) {
|
|
|
77
100
|
export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000) {
|
|
78
101
|
const resolvedRoot = path.resolve(root);
|
|
79
102
|
const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
80
|
-
const record = `${JSON.stringify({ version:
|
|
103
|
+
const record = `${JSON.stringify({ version: 2, ...owner })}\n`;
|
|
81
104
|
const deadline = Date.now() + timeoutMs;
|
|
82
105
|
while (Date.now() < deadline) {
|
|
83
106
|
if (isSandboxControlRootQuiescing(resolvedRoot))
|
|
@@ -133,11 +156,17 @@ export function readSandboxControlManifest(manifestPath) {
|
|
|
133
156
|
catch {
|
|
134
157
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
135
158
|
}
|
|
136
|
-
if (manifest.version !==
|
|
137
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version
|
|
159
|
+
if (manifest.version !== 4) {
|
|
160
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 4; container-only recreation is required');
|
|
138
161
|
}
|
|
139
|
-
if (typeof manifest.
|
|
162
|
+
if (typeof manifest.engine !== 'string' || manifest.engine.length === 0
|
|
163
|
+
|| typeof manifest.repoRoot !== 'string' || typeof manifest.worktreeRoot !== 'string'
|
|
140
164
|
|| typeof manifest.project !== 'string' || typeof manifest.container !== 'string'
|
|
165
|
+
|| !manifest.containerIdentity || typeof manifest.containerIdentity.id !== 'string'
|
|
166
|
+
|| manifest.containerIdentity.id.length === 0 || !manifest.containerIdentity.labels
|
|
167
|
+
|| typeof manifest.containerIdentity.labels !== 'object'
|
|
168
|
+
|| Array.isArray(manifest.containerIdentity.labels)
|
|
169
|
+
|| Object.values(manifest.containerIdentity.labels).some((value) => typeof value !== 'string')
|
|
141
170
|
|| typeof manifest.branch !== 'string' || !['task-bound', 'branch-only'].includes(manifest.mode ?? '')
|
|
142
171
|
|| (manifest.taskId !== null && typeof manifest.taskId !== 'string')
|
|
143
172
|
|| typeof manifest.channelDir !== 'string' || typeof manifest.publicStatusDir !== 'string'
|
|
@@ -164,9 +193,10 @@ function readBrokerOwner(filePath) {
|
|
|
164
193
|
catch {
|
|
165
194
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
166
195
|
}
|
|
167
|
-
if (!owner || owner.version !==
|
|
168
|
-
|| typeof owner.startTime !== '
|
|
169
|
-
|| typeof owner.
|
|
196
|
+
if (!owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
|
|
197
|
+
|| typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
|
|
198
|
+
|| typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
|
|
199
|
+
|| typeof owner.token !== 'string' || typeof owner.generation !== 'string')
|
|
170
200
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
171
201
|
return owner;
|
|
172
202
|
}
|
|
@@ -183,10 +213,11 @@ function readStatusOwner(filePath) {
|
|
|
183
213
|
}
|
|
184
214
|
}
|
|
185
215
|
function sameOwner(left, right) {
|
|
186
|
-
return left.pid === right.pid && left.startTime === right.startTime
|
|
216
|
+
return left.pid === right.pid && left.startTime === right.startTime
|
|
217
|
+
&& (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
|
|
187
218
|
}
|
|
188
219
|
function ownerLive(owner) {
|
|
189
|
-
return processIdentityMatches({
|
|
220
|
+
return processIdentityMatches({ pid: owner.pid, startTime: owner.startTime });
|
|
190
221
|
}
|
|
191
222
|
function readExecutions(manifest) {
|
|
192
223
|
if (!fs.existsSync(manifest.processingDir))
|
|
@@ -241,10 +272,10 @@ function executionAlive(execution, platform) {
|
|
|
241
272
|
if (platform !== 'win32' && execution.child.processGroupId) {
|
|
242
273
|
return processGroupAlive(execution.child.processGroupId);
|
|
243
274
|
}
|
|
244
|
-
return processIdentityMatches({
|
|
275
|
+
return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
|
|
245
276
|
}
|
|
246
|
-
async function waitForExit(owner, timeoutMs) {
|
|
247
|
-
const deadline = Date.now() + timeoutMs;
|
|
277
|
+
async function waitForExit(owner, timeoutMs, deadlineAt) {
|
|
278
|
+
const deadline = deadlineAt ?? (Date.now() + timeoutMs);
|
|
248
279
|
while (Date.now() < deadline) {
|
|
249
280
|
if (!ownerLive(owner))
|
|
250
281
|
return true;
|
|
@@ -286,13 +317,16 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
286
317
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
287
318
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
288
319
|
}
|
|
320
|
+
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
321
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
322
|
+
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
289
323
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
290
|
-
await waitForStartupTransition(resolvedRoot,
|
|
324
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
291
325
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
292
326
|
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
|
|
293
327
|
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
294
328
|
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
295
|
-
const statusOwner = status ?
|
|
329
|
+
const statusOwner = status ? status.broker : null;
|
|
296
330
|
const brokerLive = broker ? ownerLive(broker) : false;
|
|
297
331
|
const statusLive = statusOwner ? ownerLive(statusOwner) : false;
|
|
298
332
|
if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
@@ -307,14 +341,28 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
307
341
|
if (!manifest && brokerLive)
|
|
308
342
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
309
343
|
const platform = options.platform ?? process.platform;
|
|
310
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
311
344
|
let executions = manifest ? readExecutions(manifest) : [];
|
|
312
345
|
const owner = brokerLive ? broker : statusLive ? statusOwner : null;
|
|
313
346
|
if (!owner) {
|
|
314
347
|
if (manifest && !broker && !status)
|
|
315
348
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
316
349
|
for (const execution of executions) {
|
|
317
|
-
|
|
350
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
351
|
+
}
|
|
352
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
353
|
+
while (Date.now() < softDeadlineAt) {
|
|
354
|
+
if (!executions.some((execution) => executionAlive(execution, platform)))
|
|
355
|
+
break;
|
|
356
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
357
|
+
}
|
|
358
|
+
if (manifest)
|
|
359
|
+
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
360
|
+
if (Date.now() >= deadlineAt)
|
|
361
|
+
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
362
|
+
for (const execution of executions) {
|
|
363
|
+
if (!terminateSandboxControlExecution(execution, {
|
|
364
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now()
|
|
365
|
+
})) {
|
|
318
366
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
319
367
|
}
|
|
320
368
|
}
|
|
@@ -323,26 +371,187 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
323
371
|
}
|
|
324
372
|
return broker || status ? 'stale' : 'missing';
|
|
325
373
|
}
|
|
374
|
+
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
326
375
|
signalOwner(owner, platform, false);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
376
|
+
for (const execution of executions) {
|
|
377
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
378
|
+
}
|
|
379
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
380
|
+
while (Date.now() < softDeadlineAt) {
|
|
381
|
+
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
|
|
382
|
+
break;
|
|
383
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
384
|
+
}
|
|
385
|
+
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) {
|
|
331
386
|
return 'stopped';
|
|
332
387
|
}
|
|
333
388
|
if (manifest)
|
|
334
389
|
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
390
|
+
if (Date.now() >= deadlineAt)
|
|
391
|
+
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
335
392
|
for (const execution of executions) {
|
|
336
|
-
|
|
337
|
-
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
338
|
-
}
|
|
393
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now() });
|
|
339
394
|
}
|
|
340
395
|
signalOwner(owner, platform, true);
|
|
341
|
-
|
|
396
|
+
while (Date.now() < deadlineAt) {
|
|
397
|
+
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
|
|
398
|
+
return 'stopped';
|
|
399
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
400
|
+
}
|
|
401
|
+
if (ownerLive(owner))
|
|
342
402
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
343
|
-
if (executions.some((execution) => executionAlive(execution, platform)))
|
|
403
|
+
if (executions.some((execution) => executionAlive(execution, platform)))
|
|
344
404
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
345
|
-
}
|
|
346
405
|
return 'stopped';
|
|
347
406
|
}
|
|
407
|
+
export async function removeSandboxControlRoot(root, options) {
|
|
408
|
+
const resolvedRoot = path.resolve(root);
|
|
409
|
+
if (!fs.existsSync(resolvedRoot))
|
|
410
|
+
return;
|
|
411
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
412
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
413
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
414
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
415
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
416
|
+
const inspectContainer = options.inspectContainer
|
|
417
|
+
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
418
|
+
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
419
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
420
|
+
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
421
|
+
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
422
|
+
const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
423
|
+
if (observation.state === 'unknown')
|
|
424
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
|
|
425
|
+
if (options.requireAbsent && observation.state !== 'absent') {
|
|
426
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
427
|
+
}
|
|
428
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
429
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
430
|
+
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
431
|
+
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
432
|
+
const broker = readBrokerOwner(brokerPath);
|
|
433
|
+
const status = readStatusOwner(statusPath);
|
|
434
|
+
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
435
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
436
|
+
}
|
|
437
|
+
if (status && status.generation !== manifest.generation) {
|
|
438
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
439
|
+
}
|
|
440
|
+
if (broker && status && !sameOwner(broker, status.broker)
|
|
441
|
+
&& (ownerLive(broker) || ownerLive(status.broker))) {
|
|
442
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
443
|
+
}
|
|
444
|
+
const platform = options.platform ?? process.platform;
|
|
445
|
+
const owner = broker && ownerLive(broker) ? broker
|
|
446
|
+
: status && ownerLive(status.broker) ? status.broker : null;
|
|
447
|
+
const selfOwner = options.selfOwner;
|
|
448
|
+
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
449
|
+
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
450
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
451
|
+
}
|
|
452
|
+
let executions = readExecutions(manifest);
|
|
453
|
+
if (owner && !selfOwned)
|
|
454
|
+
signalOwner(owner, platform, false);
|
|
455
|
+
for (const execution of executions) {
|
|
456
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
457
|
+
}
|
|
458
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
459
|
+
while (Date.now() < softDeadlineAt) {
|
|
460
|
+
if ((!owner || selfOwned || !ownerLive(owner)) && !executions.some((execution) => executionAlive(execution, platform)))
|
|
461
|
+
break;
|
|
462
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
463
|
+
}
|
|
464
|
+
if (observation.state === 'found') {
|
|
465
|
+
if (Date.now() >= deadlineAt)
|
|
466
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
467
|
+
await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
468
|
+
const afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
469
|
+
if (afterRemoval.state !== 'absent') {
|
|
470
|
+
throw new Error(afterRemoval.state === 'unknown'
|
|
471
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
|
|
472
|
+
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const currentManifest = readSandboxControlManifest(manifestPath);
|
|
476
|
+
if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
477
|
+
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
478
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
479
|
+
}
|
|
480
|
+
const currentBroker = readBrokerOwner(brokerPath);
|
|
481
|
+
if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
|
|
482
|
+
|| currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
|
|
483
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
484
|
+
}
|
|
485
|
+
if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
|
|
486
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
487
|
+
}
|
|
488
|
+
if (!owner && currentBroker && ownerLive(currentBroker)) {
|
|
489
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
490
|
+
}
|
|
491
|
+
if (owner && ownerLive(owner) && !currentBroker) {
|
|
492
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
493
|
+
}
|
|
494
|
+
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
495
|
+
if (Date.now() >= deadlineAt)
|
|
496
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
497
|
+
for (const execution of executions) {
|
|
498
|
+
if (!terminateSandboxControlExecution(execution, {
|
|
499
|
+
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now()
|
|
500
|
+
})) {
|
|
501
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (owner && currentBroker && ownerLive(currentBroker) && !selfOwned) {
|
|
505
|
+
if (remaining() <= 0)
|
|
506
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
507
|
+
signalOwner(currentBroker, platform, true);
|
|
508
|
+
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt)) {
|
|
509
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
|
|
513
|
+
if (finalExecutions.some((execution) => executionAlive(execution, platform))) {
|
|
514
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
515
|
+
}
|
|
516
|
+
const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
|
|
517
|
+
if (finalBrokerRaw !== null) {
|
|
518
|
+
const finalBroker = readBrokerOwner(brokerPath);
|
|
519
|
+
if (!finalBroker)
|
|
520
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
521
|
+
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
522
|
+
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
523
|
+
if (ownerLive(finalBroker) && !finalSelfOwner)
|
|
524
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
525
|
+
if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
|
|
526
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
527
|
+
}
|
|
528
|
+
fs.unlinkSync(brokerPath);
|
|
529
|
+
}
|
|
530
|
+
const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
|
|
531
|
+
if (finalStatusRaw !== null) {
|
|
532
|
+
const finalStatus = readStatusOwner(statusPath);
|
|
533
|
+
if (!finalStatus)
|
|
534
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
535
|
+
if (ownerLive(finalStatus.broker) && !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
536
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
537
|
+
}
|
|
538
|
+
if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
|
|
539
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
540
|
+
}
|
|
541
|
+
fs.unlinkSync(statusPath);
|
|
542
|
+
}
|
|
543
|
+
if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
|
|
544
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
|
|
545
|
+
}
|
|
546
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
547
|
+
}
|
|
548
|
+
export async function garbageCollectSandboxControlRoot(root, options = {}) {
|
|
549
|
+
await removeSandboxControlRoot(path.resolve(root), {
|
|
550
|
+
...options,
|
|
551
|
+
requireAbsent: true,
|
|
552
|
+
removeContainer: async () => {
|
|
553
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
348
557
|
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -5,6 +5,13 @@ export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
|
5
5
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
6
6
|
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
7
7
|
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
|
|
8
|
+
export const DEFAULT_SANDBOX_CONTROL_TIMING = Object.freeze({
|
|
9
|
+
controlTickMs: 250,
|
|
10
|
+
parkedBindingInitialMs: 1_000,
|
|
11
|
+
slowCheckMs: 5_000,
|
|
12
|
+
containerHeartbeatMs: 5_000,
|
|
13
|
+
quiesceDeadlineMs: 7_000
|
|
14
|
+
});
|
|
8
15
|
export class SandboxControlProtocolError extends Error {
|
|
9
16
|
code;
|
|
10
17
|
retryable;
|