@fitlab-ai/agent-infra 0.9.4 → 0.9.6
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -1,35 +1,90 @@
|
|
|
1
|
+
import { validateTaskCreateCandidate } from '../../task/create.js';
|
|
1
2
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
2
|
-
export const
|
|
3
|
+
export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
4
|
+
export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
5
|
+
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
6
|
+
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
7
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
|
|
8
|
+
export class SandboxControlProtocolError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
retryable;
|
|
11
|
+
constructor(code, message, retryable = false) {
|
|
12
|
+
super(`${code}: ${message}`);
|
|
13
|
+
this.name = 'SandboxControlProtocolError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.retryable = retryable;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function fail(code, message, retryable = false) {
|
|
19
|
+
throw new SandboxControlProtocolError(code, message, retryable);
|
|
20
|
+
}
|
|
3
21
|
export function isSandboxControlFamily(value) {
|
|
4
22
|
return SANDBOX_CONTROL_FAMILIES.includes(value);
|
|
5
23
|
}
|
|
6
|
-
export function validateSandboxControlRequest(value, manifest) {
|
|
24
|
+
export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
7
25
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
8
|
-
|
|
26
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
9
27
|
}
|
|
10
28
|
const request = value;
|
|
11
|
-
if (request.version !==
|
|
12
|
-
||
|
|
13
|
-
||
|
|
14
|
-
|| request.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|| !request.args.every((arg) => typeof arg === 'string')) {
|
|
19
|
-
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
29
|
+
if (request.version !== 2 || typeof request.id !== 'string'
|
|
30
|
+
|| !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
|
|
31
|
+
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
|
|
32
|
+
|| !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
|
|
33
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
34
|
+
if (request.generation !== manifest.generation) {
|
|
35
|
+
fail('SANDBOX_CONTROL_GENERATION_INVALID', 'request generation does not match the broker');
|
|
20
36
|
}
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
const issuedAt = request.issuedAt;
|
|
38
|
+
const expiresAt = request.expiresAt;
|
|
39
|
+
const now = options.now ?? Date.now();
|
|
40
|
+
if (expiresAt <= issuedAt || expiresAt - issuedAt > SANDBOX_CONTROL_ADMISSION_WINDOW_MS || issuedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
41
|
+
fail('SANDBOX_CONTROL_REQUEST_DEADLINE_INVALID', 'request admission deadline is invalid');
|
|
23
42
|
}
|
|
43
|
+
if (now > expiresAt)
|
|
44
|
+
fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
|
|
24
45
|
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
25
|
-
|
|
46
|
+
fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
|
|
47
|
+
}
|
|
48
|
+
if (request.family === 'task-create') {
|
|
49
|
+
const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
50
|
+
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
|
|
51
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
52
|
+
}
|
|
53
|
+
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
|
|
54
|
+
}
|
|
55
|
+
const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
56
|
+
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
57
|
+
|| !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
|
|
58
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
59
|
+
}
|
|
60
|
+
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
61
|
+
fail('SANDBOX_CONTROL_BRANCH_ONLY', "branch-only sandboxes cannot coordinate tasks; return to the host and run 'ai sandbox start --recreate <task-ref-or-correct-branch>'");
|
|
62
|
+
}
|
|
63
|
+
if (request.family === 'task-orchestration'
|
|
64
|
+
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
|
|
65
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
|
|
26
66
|
}
|
|
27
67
|
return request;
|
|
28
68
|
}
|
|
29
69
|
export function bindSandboxControlTask(request, taskId) {
|
|
30
|
-
if (request.
|
|
31
|
-
|
|
32
|
-
|
|
70
|
+
if (request.family === 'task-create')
|
|
71
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create requests do not bind a current task');
|
|
72
|
+
if (request.args.length === 0)
|
|
73
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
|
|
33
74
|
return [taskId, ...request.args.slice(1)];
|
|
34
75
|
}
|
|
76
|
+
export function controlError(error) {
|
|
77
|
+
if (error instanceof SandboxControlProtocolError) {
|
|
78
|
+
return { code: error.code, message: error.message, retryable: error.retryable };
|
|
79
|
+
}
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'SANDBOX_CONTROL_INTERNAL_ERROR';
|
|
82
|
+
const retryable = new Set([
|
|
83
|
+
'SANDBOX_CONTROL_BUSY',
|
|
84
|
+
'SANDBOX_CONTROL_HANDOFF_ACTIVE',
|
|
85
|
+
'SANDBOX_CONTROL_REQUEST_EXPIRED',
|
|
86
|
+
'SANDBOX_WORKTREE_BINDING_LOST'
|
|
87
|
+
]).has(code);
|
|
88
|
+
return { code, message, retryable };
|
|
89
|
+
}
|
|
35
90
|
//# sourceMappingURL=protocol.js.map
|
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (manifest.version !== 1
|
|
12
|
-
|| typeof manifest.repoRoot !== 'string'
|
|
13
|
-
|| typeof manifest.channelDir !== 'string'
|
|
14
|
-
|| typeof manifest.token !== 'string')
|
|
15
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
16
|
-
if (path.resolve(manifest.channelDir) !== path.join(path.dirname(path.resolve(manifestPath)), 'channel')) {
|
|
17
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
18
|
-
}
|
|
19
|
-
return manifest;
|
|
20
|
-
}
|
|
4
|
+
import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
|
|
5
|
+
import { controlError, SANDBOX_CONTROL_STATUS_INTERVAL_MS } from './protocol.js';
|
|
6
|
+
import { prepareSandboxControlExecution } from './executor.js';
|
|
7
|
+
import { appendSandboxControlAudit, atomicWriteJson, cleanupStaleSandboxControlLease, executionPath, readActiveLease, readExecution, terminateSandboxControlExecution, writeSandboxControlStatus } from './state.js';
|
|
8
|
+
import { validateSandboxControlRequest } from './protocol.js';
|
|
9
|
+
import { acquireSandboxControlBrokerStartup, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
|
|
21
10
|
function assertRealDirectory(directory, parent) {
|
|
22
11
|
const stat = fs.lstatSync(directory);
|
|
23
12
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
@@ -26,84 +15,250 @@ function assertRealDirectory(directory, parent) {
|
|
|
26
15
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
27
16
|
}
|
|
28
17
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
function responsePath(manifest, id) {
|
|
19
|
+
return path.join(manifest.channelDir, 'responses', `${id}.json`);
|
|
20
|
+
}
|
|
21
|
+
function writeResponse(manifest, response) {
|
|
22
|
+
atomicWriteJson(responsePath(manifest, response.id), response);
|
|
23
|
+
}
|
|
24
|
+
function rejected(id, error) {
|
|
25
|
+
const detail = controlError(error);
|
|
26
|
+
return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${detail.message}\n`, error: detail };
|
|
27
|
+
}
|
|
28
|
+
function unknown(id) {
|
|
29
|
+
const error = {
|
|
30
|
+
code: 'SANDBOX_CONTROL_RESULT_UNKNOWN',
|
|
31
|
+
message: 'SANDBOX_CONTROL_RESULT_UNKNOWN: accepted execution ended without a provable result; inspect domain state before retrying',
|
|
32
|
+
retryable: false
|
|
33
|
+
};
|
|
34
|
+
return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${error.message}\n`, error };
|
|
33
35
|
}
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
36
|
+
function notExecuted(id) {
|
|
37
|
+
const error = {
|
|
38
|
+
code: 'SANDBOX_CONTROL_NOT_EXECUTED',
|
|
39
|
+
message: 'SANDBOX_CONTROL_NOT_EXECUTED: request was claimed but never accepted; retry with a new request id',
|
|
40
|
+
retryable: true
|
|
41
|
+
};
|
|
42
|
+
return { version: 2, id, phase: 'rejected', exitCode: null, stdout: '', stderr: `${error.message}\n`, error };
|
|
43
|
+
}
|
|
44
|
+
function consumeRequest(directory, id) {
|
|
45
|
+
try {
|
|
46
|
+
fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code === 'EEXIST')
|
|
50
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function claimRequest(manifest, requestPath, id) {
|
|
55
|
+
const directory = path.join(manifest.processingDir, id);
|
|
56
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
57
|
+
const claimed = path.join(directory, 'request.json');
|
|
58
|
+
try {
|
|
59
|
+
fs.renameSync(requestPath, claimed);
|
|
60
|
+
return claimed;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function bindingReason(manifest) {
|
|
68
|
+
try {
|
|
69
|
+
assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return 'SANDBOX_WORKTREE_BINDING_LOST';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function recoverProcessing(manifest) {
|
|
77
|
+
for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
|
|
78
|
+
if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name))
|
|
79
|
+
continue;
|
|
80
|
+
const descriptor = executionPath(manifest, entry.name);
|
|
81
|
+
const existingResponse = responsePath(manifest, entry.name);
|
|
82
|
+
if (fs.existsSync(descriptor)) {
|
|
83
|
+
const execution = readExecution(descriptor);
|
|
84
|
+
if (!terminateSandboxControlExecution(execution)) {
|
|
85
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
|
|
86
|
+
}
|
|
87
|
+
appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
|
|
88
|
+
let terminal = false;
|
|
89
|
+
if (fs.existsSync(existingResponse)) {
|
|
90
|
+
try {
|
|
91
|
+
const response = JSON.parse(fs.readFileSync(existingResponse, 'utf8'));
|
|
92
|
+
terminal = response.version === 2 && response.id === entry.name
|
|
93
|
+
&& (response.phase === 'completed' || response.phase === 'rejected');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
terminal = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!terminal)
|
|
100
|
+
writeResponse(manifest, unknown(entry.name));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
writeResponse(manifest, notExecuted(entry.name));
|
|
104
|
+
}
|
|
105
|
+
fs.rmSync(path.join(manifest.processingDir, entry.name), { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function delay(ms) {
|
|
109
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
110
|
+
}
|
|
111
|
+
export function sandboxControlSafeEnv(env = process.env) {
|
|
112
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
|
|
113
|
+
}
|
|
114
|
+
export async function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
|
|
115
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
116
|
+
const root = path.dirname(manifestPath);
|
|
37
117
|
const startTime = getProcessStartTime(process.pid);
|
|
38
118
|
if (!startTime)
|
|
39
119
|
throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
|
|
40
|
-
|
|
41
|
-
version: 1,
|
|
42
|
-
pid: process.pid,
|
|
43
|
-
startTime,
|
|
44
|
-
token: manifest.token
|
|
45
|
-
})}\n`, { mode: 0o600, flag: 'wx' });
|
|
120
|
+
const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime });
|
|
46
121
|
const requestsDir = path.join(manifest.channelDir, 'requests');
|
|
47
122
|
const responsesDir = path.join(manifest.channelDir, 'responses');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return;
|
|
123
|
+
const consumedDir = path.join(root, 'consumed');
|
|
124
|
+
const broker = { pid: process.pid, startTime };
|
|
125
|
+
const brokerPath = path.join(root, 'broker.json');
|
|
126
|
+
const brokerRecord = `${JSON.stringify({ version: 2, ...broker, token: manifest.token, generation: manifest.generation })}\n`;
|
|
127
|
+
try {
|
|
128
|
+
for (const directory of [manifest.channelDir, requestsDir, responsesDir, consumedDir, manifest.publicStatusDir, manifest.processingDir]) {
|
|
129
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
130
|
+
}
|
|
131
|
+
assertRealDirectory(manifest.channelDir);
|
|
58
132
|
assertRealDirectory(requestsDir, manifest.channelDir);
|
|
59
133
|
assertRealDirectory(responsesDir, manifest.channelDir);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
134
|
+
assertRealDirectory(consumedDir, root);
|
|
135
|
+
assertRealDirectory(manifest.publicStatusDir, root);
|
|
136
|
+
assertRealDirectory(manifest.processingDir, root);
|
|
137
|
+
fs.writeFileSync(brokerPath, brokerRecord, { mode: 0o600, flag: 'wx' });
|
|
138
|
+
if (isSandboxControlRootQuiescing(root)) {
|
|
139
|
+
if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
|
|
140
|
+
fs.unlinkSync(brokerPath);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
releaseStartup();
|
|
146
|
+
}
|
|
147
|
+
let active = null;
|
|
148
|
+
let lastState = '';
|
|
149
|
+
let lastStatusAt = 0;
|
|
150
|
+
try {
|
|
151
|
+
writeSandboxControlStatus(manifest, broker, 'starting', null, null);
|
|
152
|
+
appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
|
|
153
|
+
recoverProcessing(manifest);
|
|
154
|
+
while (!signal.aborted) {
|
|
155
|
+
let current;
|
|
66
156
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
id = request.id;
|
|
72
|
-
if (consumed.has(id))
|
|
73
|
-
throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
|
|
74
|
-
consumed.add(id);
|
|
75
|
-
const result = spawnSync(process.execPath, ['--experimental-strip-types', '--no-warnings', process.argv[1], request.family, ...bindSandboxControlTask(request, manifest.taskId)], {
|
|
76
|
-
cwd: manifest.repoRoot,
|
|
77
|
-
encoding: 'utf8',
|
|
78
|
-
env: Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('AGENT_INFRA_CONTROL_')))
|
|
79
|
-
});
|
|
80
|
-
response = {
|
|
81
|
-
version: 1,
|
|
82
|
-
id,
|
|
83
|
-
exitCode: result.status ?? 1,
|
|
84
|
-
stdout: result.stdout ?? '',
|
|
85
|
-
stderr: result.stderr ?? ''
|
|
86
|
-
};
|
|
157
|
+
current = readSandboxControlManifest(manifestPath);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
break;
|
|
87
161
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
162
|
+
if (current.token !== manifest.token || current.generation !== manifest.generation)
|
|
163
|
+
break;
|
|
164
|
+
if (active?.settled) {
|
|
165
|
+
if (active.result) {
|
|
166
|
+
writeResponse(manifest, {
|
|
167
|
+
version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
|
|
168
|
+
stdout: active.result.stdout, stderr: active.result.stderr, error: null
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
writeResponse(manifest, unknown(active.request.id));
|
|
173
|
+
}
|
|
174
|
+
fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
|
|
175
|
+
active = null;
|
|
96
176
|
}
|
|
177
|
+
let reasonCode = null;
|
|
97
178
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
179
|
+
if (cleanupStaleSandboxControlLease(manifest)) {
|
|
180
|
+
appendSandboxControlAudit(manifest, 'lease-stale-cleanup');
|
|
181
|
+
}
|
|
182
|
+
if (readActiveLease(manifest))
|
|
183
|
+
reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
|
|
187
|
+
}
|
|
188
|
+
reasonCode ??= bindingReason(manifest);
|
|
189
|
+
const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
|
|
190
|
+
const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
if (stateKey !== lastState || now - lastStatusAt >= SANDBOX_CONTROL_STATUS_INTERVAL_MS) {
|
|
193
|
+
writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now);
|
|
194
|
+
lastStatusAt = now;
|
|
195
|
+
}
|
|
196
|
+
if (stateKey !== lastState) {
|
|
197
|
+
appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
|
|
198
|
+
lastState = stateKey;
|
|
100
199
|
}
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
200
|
+
for (const name of fs.readdirSync(requestsDir).sort()) {
|
|
201
|
+
if (!/^[a-f0-9-]{16,64}\.json$/.test(name))
|
|
202
|
+
continue;
|
|
203
|
+
const id = name.slice(0, -5);
|
|
204
|
+
const source = path.join(requestsDir, name);
|
|
205
|
+
let claimed = null;
|
|
206
|
+
try {
|
|
207
|
+
claimed = claimRequest(manifest, source, id);
|
|
208
|
+
consumeRequest(consumedDir, id);
|
|
209
|
+
if (reasonCode)
|
|
210
|
+
throw new Error(reasonCode);
|
|
211
|
+
if (active)
|
|
212
|
+
throw new Error('SANDBOX_CONTROL_BUSY');
|
|
213
|
+
const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
|
|
214
|
+
if (bindingReason(manifest))
|
|
215
|
+
throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
|
|
216
|
+
if (readActiveLease(manifest))
|
|
217
|
+
throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
218
|
+
const prepared = await prepareSandboxControlExecution({
|
|
219
|
+
manifest, manifestPath, request, requestPath: claimed, internalCliPath: process.argv[1]
|
|
220
|
+
});
|
|
221
|
+
const execution = { request, prepared, result: null, failure: null, settled: false };
|
|
222
|
+
active = execution;
|
|
223
|
+
prepared.completion.then((result) => { execution.result = result; execution.settled = true; }, (error) => { execution.failure = error; execution.settled = true; });
|
|
224
|
+
writeResponse(manifest, {
|
|
225
|
+
version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
prepared.start();
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
prepared.terminate();
|
|
232
|
+
writeResponse(manifest, unknown(id));
|
|
233
|
+
fs.rmSync(path.join(manifest.processingDir, id), { recursive: true, force: true });
|
|
234
|
+
active = null;
|
|
235
|
+
claimed = null;
|
|
236
|
+
appendSandboxControlAudit(manifest, 'executor-gate-failed', { requestId: id });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
writeResponse(manifest, rejected(id, error));
|
|
242
|
+
if (claimed)
|
|
243
|
+
fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
|
|
244
|
+
}
|
|
104
245
|
}
|
|
246
|
+
await delay(25);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
if (active) {
|
|
251
|
+
active.prepared.terminate();
|
|
252
|
+
writeResponse(manifest, unknown(active.request.id));
|
|
253
|
+
}
|
|
254
|
+
appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid });
|
|
255
|
+
try {
|
|
256
|
+
if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
|
|
257
|
+
fs.unlinkSync(brokerPath);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// A newer owner or recovery path owns the record.
|
|
105
261
|
}
|
|
106
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
107
262
|
}
|
|
108
263
|
}
|
|
109
264
|
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { buildProcessTreeStopCommand } from '../../server/process-control.js';
|
|
6
|
+
import { processIdentityMatches } from '../../server/process-state.js';
|
|
7
|
+
export const SANDBOX_CONTROL_AUDIT_MAX_BYTES = 1024 * 1024;
|
|
8
|
+
export const SANDBOX_CONTROL_EXECUTION_STOP_MS = 2_000;
|
|
9
|
+
function assertRegularFile(filePath) {
|
|
10
|
+
const stat = fs.lstatSync(filePath);
|
|
11
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
12
|
+
throw new Error('SANDBOX_CONTROL_STATE_INVALID');
|
|
13
|
+
}
|
|
14
|
+
export function atomicWriteJson(filePath, value, mode = 0o600, createParent = true) {
|
|
15
|
+
if (createParent)
|
|
16
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
17
|
+
const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
18
|
+
try {
|
|
19
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode, flag: 'wx' });
|
|
20
|
+
fs.renameSync(temporary, filePath);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
fs.rmSync(temporary, { force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function readJsonFile(filePath) {
|
|
27
|
+
assertRegularFile(filePath);
|
|
28
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
export function statusPath(manifest) {
|
|
31
|
+
return path.join(manifest.publicStatusDir, 'status.json');
|
|
32
|
+
}
|
|
33
|
+
export function writeSandboxControlStatus(manifest, broker, state, reasonCode, activeRequestId, now = Date.now()) {
|
|
34
|
+
const status = {
|
|
35
|
+
version: 1, generation: manifest.generation, broker, state, reasonCode, activeRequestId, updatedAt: now
|
|
36
|
+
};
|
|
37
|
+
atomicWriteJson(statusPath(manifest), status, 0o600, false);
|
|
38
|
+
return status;
|
|
39
|
+
}
|
|
40
|
+
export function parseSandboxControlStatus(value) {
|
|
41
|
+
const status = value;
|
|
42
|
+
if (!status || status.version !== 1 || typeof status.generation !== 'string'
|
|
43
|
+
|| !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== 'string'
|
|
44
|
+
|| !['starting', 'healthy', 'busy', 'parked'].includes(status.state ?? '')
|
|
45
|
+
|| (status.reasonCode !== null && typeof status.reasonCode !== 'string')
|
|
46
|
+
|| (status.activeRequestId !== null && typeof status.activeRequestId !== 'string')
|
|
47
|
+
|| !Number.isSafeInteger(status.updatedAt))
|
|
48
|
+
throw new Error('SANDBOX_CONTROL_STATUS_INVALID');
|
|
49
|
+
return status;
|
|
50
|
+
}
|
|
51
|
+
export function readSandboxControlStatus(directory) {
|
|
52
|
+
return parseSandboxControlStatus(readJsonFile(path.join(directory, 'status.json')));
|
|
53
|
+
}
|
|
54
|
+
export function readActiveLease(manifest, now = Date.now()) {
|
|
55
|
+
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
56
|
+
if (!fs.existsSync(filePath))
|
|
57
|
+
return null;
|
|
58
|
+
const lease = readJsonFile(filePath);
|
|
59
|
+
if (!lease || lease.version !== 1
|
|
60
|
+
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
61
|
+
|| typeof lease.owner.startTime !== 'string' || !Number.isSafeInteger(lease.issuedAt)
|
|
62
|
+
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
63
|
+
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
64
|
+
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
65
|
+
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
66
|
+
}
|
|
67
|
+
if (lease.generation !== manifest.generation || lease.expiresAt <= now
|
|
68
|
+
|| lease.taskId !== manifest.taskId || lease.branch !== manifest.branch)
|
|
69
|
+
return null;
|
|
70
|
+
return lease;
|
|
71
|
+
}
|
|
72
|
+
export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
|
|
73
|
+
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
74
|
+
if (!fs.existsSync(filePath))
|
|
75
|
+
return false;
|
|
76
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
77
|
+
const lease = JSON.parse(raw);
|
|
78
|
+
if (!lease || lease.version !== 1
|
|
79
|
+
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
80
|
+
|| typeof lease.owner.startTime !== 'string' || !Number.isSafeInteger(lease.issuedAt)
|
|
81
|
+
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
82
|
+
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
83
|
+
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
84
|
+
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
85
|
+
}
|
|
86
|
+
const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
|
|
87
|
+
|| lease.branch !== manifest.branch || lease.expiresAt <= now || !processIdentityMatches({
|
|
88
|
+
version: 1, pid: lease.owner.pid, startTime: lease.owner.startTime
|
|
89
|
+
});
|
|
90
|
+
if (!stale)
|
|
91
|
+
return false;
|
|
92
|
+
if (fs.readFileSync(filePath, 'utf8') !== raw)
|
|
93
|
+
throw new Error('SANDBOX_CONTROL_LEASE_OWNERSHIP_LOST');
|
|
94
|
+
fs.unlinkSync(filePath);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
export function appendSandboxControlAudit(manifest, event, fields = {}, now = Date.now()) {
|
|
98
|
+
const auditPath = path.join(path.dirname(manifest.publicStatusDir), 'audit.ndjson');
|
|
99
|
+
if (fs.existsSync(auditPath) && fs.statSync(auditPath).size >= SANDBOX_CONTROL_AUDIT_MAX_BYTES) {
|
|
100
|
+
fs.rmSync(`${auditPath}.1`, { force: true });
|
|
101
|
+
fs.renameSync(auditPath, `${auditPath}.1`);
|
|
102
|
+
}
|
|
103
|
+
fs.appendFileSync(auditPath, `${JSON.stringify({ version: 1, event, generation: manifest.generation, at: now, ...fields })}\n`, {
|
|
104
|
+
encoding: 'utf8', mode: 0o600
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export function executionPath(manifest, requestId) {
|
|
108
|
+
return path.join(manifest.processingDir, requestId, 'execution.json');
|
|
109
|
+
}
|
|
110
|
+
export function readExecution(filePath) {
|
|
111
|
+
const execution = readJsonFile(filePath);
|
|
112
|
+
if (!execution || execution.version !== 1 || typeof execution.generation !== 'string'
|
|
113
|
+
|| typeof execution.requestId !== 'string' || typeof execution.nonce !== 'string'
|
|
114
|
+
|| !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== 'string'
|
|
115
|
+
|| !['prepared', 'running', 'terminating'].includes(execution.phase ?? '')
|
|
116
|
+
|| !Number.isSafeInteger(execution.updatedAt))
|
|
117
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_INVALID');
|
|
118
|
+
return execution;
|
|
119
|
+
}
|
|
120
|
+
export function terminateSandboxControlExecution(execution, options = {}) {
|
|
121
|
+
const platform = options.platform ?? process.platform;
|
|
122
|
+
const processGroupId = execution.child.processGroupId;
|
|
123
|
+
const processGroupAlive = (groupId) => {
|
|
124
|
+
try {
|
|
125
|
+
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
126
|
+
return rows.split('\n').some((row) => {
|
|
127
|
+
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
128
|
+
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
try {
|
|
133
|
+
process.kill(-groupId, 0);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return error.code === 'EPERM';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const treeAlive = () => {
|
|
142
|
+
if (platform === 'win32' || !processGroupId) {
|
|
143
|
+
return processIdentityMatches({ version: 1, pid: execution.child.pid, startTime: execution.child.startTime });
|
|
144
|
+
}
|
|
145
|
+
return processGroupAlive(processGroupId);
|
|
146
|
+
};
|
|
147
|
+
if (!treeAlive())
|
|
148
|
+
return true;
|
|
149
|
+
const command = buildProcessTreeStopCommand(processGroupId ?? execution.child.pid, platform);
|
|
150
|
+
try {
|
|
151
|
+
if (command.kind === 'exec')
|
|
152
|
+
execFileSync(command.command, command.args, { stdio: 'ignore' });
|
|
153
|
+
else
|
|
154
|
+
process.kill(command.pid, command.signal);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// The identity check below distinguishes an already-exited tree from a failed termination.
|
|
158
|
+
}
|
|
159
|
+
const deadline = Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS);
|
|
160
|
+
while (Date.now() < deadline) {
|
|
161
|
+
if (!treeAlive())
|
|
162
|
+
return true;
|
|
163
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
164
|
+
}
|
|
165
|
+
if (platform !== 'win32') {
|
|
166
|
+
try {
|
|
167
|
+
process.kill(processGroupId ? -processGroupId : execution.child.pid, 'SIGKILL');
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// The final identity check handles an already-exited tree.
|
|
171
|
+
}
|
|
172
|
+
const killDeadline = Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS);
|
|
173
|
+
while (Date.now() < killDeadline) {
|
|
174
|
+
if (!treeAlive())
|
|
175
|
+
return true;
|
|
176
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return !treeAlive();
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -15,9 +15,9 @@ Commands:
|
|
|
15
15
|
rebuild [--quiet] [--refresh]
|
|
16
16
|
Rebuild the sandbox image (--refresh pulls base + tools)
|
|
17
17
|
refresh Sync host Claude Code credentials to all sandbox copies
|
|
18
|
-
rm <branch> | --
|
|
18
|
+
rm <branch> | --unbound | --purge
|
|
19
19
|
Remove one sandbox, all sandboxes not bound to an
|
|
20
|
-
active task (--
|
|
20
|
+
active task (--unbound), or tear down everything (--purge)
|
|
21
21
|
start [--recreate] <branch | TASK-id | N>
|
|
22
22
|
Verify or recover an existing sandbox container;
|
|
23
23
|
optionally replace only the container on failure
|