@fitlab-ai/agent-infra 0.9.5 → 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/dist/bin/internal-cli.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +126 -16
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +48 -4
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/internal/codex-lifecycle.js +100 -2
- 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 +12 -4
- package/dist/lib/internal/task-orchestration.js +5 -4
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +15 -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 +138 -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 +62 -26
- package/dist/lib/sandbox/control/server.js +222 -102
- 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 +73 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- 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 +36 -9
- 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 +133 -4
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +142 -15
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +51 -3
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/git/workflow.ts +0 -1
- package/lib/internal/codex-lifecycle.ts +107 -3
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +4 -4
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +17 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +96 -69
- package/lib/sandbox/control/server.ts +239 -110
- 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 +81 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- 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 +52 -9
- 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 +199 -4
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +48 -12
- 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/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 +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -1,54 +1,90 @@
|
|
|
1
1
|
import { validateTaskCreateCandidate } from '../../task/create.js';
|
|
2
2
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
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;
|
|
3
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
|
+
}
|
|
4
21
|
export function isSandboxControlFamily(value) {
|
|
5
22
|
return SANDBOX_CONTROL_FAMILIES.includes(value);
|
|
6
23
|
}
|
|
7
|
-
export function validateSandboxControlRequest(value, manifest) {
|
|
24
|
+
export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
8
25
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
9
|
-
|
|
26
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
10
27
|
}
|
|
11
28
|
const request = value;
|
|
12
|
-
if (request.version !==
|
|
13
|
-
||
|
|
14
|
-
||
|
|
15
|
-
|| request.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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');
|
|
19
36
|
}
|
|
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');
|
|
42
|
+
}
|
|
43
|
+
if (now > expiresAt)
|
|
44
|
+
fail('SANDBOX_CONTROL_REQUEST_EXPIRED', 'request admission deadline has passed', true);
|
|
20
45
|
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
21
|
-
|
|
46
|
+
fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
|
|
22
47
|
}
|
|
23
48
|
if (request.family === 'task-create') {
|
|
24
|
-
const
|
|
25
|
-
if (keys
|
|
26
|
-
|
|
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');
|
|
27
52
|
}
|
|
28
53
|
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
|
|
29
54
|
}
|
|
30
|
-
const
|
|
31
|
-
if (keys
|
|
32
|
-
|| !Array.isArray(request.args)
|
|
33
|
-
|
|
34
|
-
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
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');
|
|
35
59
|
}
|
|
36
60
|
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
37
|
-
|
|
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>'");
|
|
38
62
|
}
|
|
39
63
|
if (request.family === 'task-orchestration'
|
|
40
64
|
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
|
|
41
|
-
|
|
65
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
|
|
42
66
|
}
|
|
43
67
|
return request;
|
|
44
68
|
}
|
|
45
69
|
export function bindSandboxControlTask(request, taskId) {
|
|
46
|
-
if (request.family === 'task-create')
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
|
|
51
|
-
}
|
|
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');
|
|
52
74
|
return [taskId, ...request.args.slice(1)];
|
|
53
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
|
+
}
|
|
54
90
|
//# sourceMappingURL=protocol.js.map
|
|
@@ -1,28 +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 { createTask } from '../../task/create-service.js';
|
|
6
4
|
import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
13
|
-
if (manifest.version !== 2) {
|
|
14
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 2; recreate the sandbox');
|
|
15
|
-
}
|
|
16
|
-
if (typeof manifest.repoRoot !== 'string'
|
|
17
|
-
|| typeof manifest.worktreeRoot !== 'string'
|
|
18
|
-
|| typeof manifest.channelDir !== 'string'
|
|
19
|
-
|| typeof manifest.token !== 'string')
|
|
20
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
21
|
-
if (path.resolve(manifest.channelDir) !== path.join(path.dirname(path.resolve(manifestPath)), 'channel')) {
|
|
22
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
23
|
-
}
|
|
24
|
-
return manifest;
|
|
25
|
-
}
|
|
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';
|
|
26
10
|
function assertRealDirectory(directory, parent) {
|
|
27
11
|
const stat = fs.lstatSync(directory);
|
|
28
12
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
@@ -31,114 +15,250 @@ function assertRealDirectory(directory, parent) {
|
|
|
31
15
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
32
16
|
}
|
|
33
17
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
fs.writeFileSync(temporary, `${JSON.stringify(response)}\n`, { mode: 0o600, flag: 'wx' });
|
|
37
|
-
fs.renameSync(temporary, path.join(directory, `${response.id}.json`));
|
|
18
|
+
function responsePath(manifest, id) {
|
|
19
|
+
return path.join(manifest.channelDir, 'responses', `${id}.json`);
|
|
38
20
|
}
|
|
39
|
-
|
|
40
|
-
|
|
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 };
|
|
35
|
+
}
|
|
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 };
|
|
41
43
|
}
|
|
42
44
|
function consumeRequest(directory, id) {
|
|
43
45
|
try {
|
|
44
46
|
fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
|
|
45
47
|
}
|
|
46
48
|
catch (error) {
|
|
47
|
-
if (error.code === 'EEXIST')
|
|
49
|
+
if (error.code === 'EEXIST')
|
|
48
50
|
throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
|
|
49
|
-
}
|
|
50
51
|
throw error;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
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);
|
|
57
117
|
const startTime = getProcessStartTime(process.pid);
|
|
58
118
|
if (!startTime)
|
|
59
119
|
throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
|
|
60
|
-
|
|
61
|
-
version: 1,
|
|
62
|
-
pid: process.pid,
|
|
63
|
-
startTime,
|
|
64
|
-
token: manifest.token
|
|
65
|
-
})}\n`, { mode: 0o600, flag: 'wx' });
|
|
120
|
+
const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime });
|
|
66
121
|
const requestsDir = path.join(manifest.channelDir, 'requests');
|
|
67
122
|
const responsesDir = path.join(manifest.channelDir, 'responses');
|
|
68
|
-
const consumedDir = path.join(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const current = readManifest(manifestPath);
|
|
78
|
-
if (current.token !== manifest.token)
|
|
79
|
-
return;
|
|
80
|
-
assertGitWorktreeBinding(current.repoRoot, current.worktreeRoot, current.branch);
|
|
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);
|
|
81
132
|
assertRealDirectory(requestsDir, manifest.channelDir);
|
|
82
133
|
assertRealDirectory(responsesDir, manifest.channelDir);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
156
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
stdout:
|
|
102
|
-
};
|
|
157
|
+
current = readSandboxControlManifest(manifestPath);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
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
|
+
});
|
|
103
170
|
}
|
|
104
171
|
else {
|
|
105
|
-
|
|
106
|
-
if (request.family === 'task-orchestration') {
|
|
107
|
-
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
108
|
-
}
|
|
109
|
-
const result = spawnSync(process.execPath, ['--experimental-strip-types', '--no-warnings', process.argv[1], request.family, ...boundArgs], {
|
|
110
|
-
cwd: manifest.repoRoot,
|
|
111
|
-
encoding: 'utf8',
|
|
112
|
-
env: sandboxControlSafeEnv()
|
|
113
|
-
});
|
|
114
|
-
response = {
|
|
115
|
-
version: 1,
|
|
116
|
-
id,
|
|
117
|
-
exitCode: result.status ?? 1,
|
|
118
|
-
stdout: result.stdout ?? '',
|
|
119
|
-
stderr: result.stderr ?? ''
|
|
120
|
-
};
|
|
172
|
+
writeResponse(manifest, unknown(active.request.id));
|
|
121
173
|
}
|
|
174
|
+
fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
|
|
175
|
+
active = null;
|
|
122
176
|
}
|
|
123
|
-
|
|
124
|
-
response = {
|
|
125
|
-
version: 1,
|
|
126
|
-
id,
|
|
127
|
-
exitCode: 1,
|
|
128
|
-
stdout: '',
|
|
129
|
-
stderr: `${error instanceof Error ? error.message : String(error)}\n`
|
|
130
|
-
};
|
|
131
|
-
}
|
|
177
|
+
let reasonCode = null;
|
|
132
178
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
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;
|
|
135
199
|
}
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
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
|
+
}
|
|
139
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.
|
|
140
261
|
}
|
|
141
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
142
262
|
}
|
|
143
263
|
}
|
|
144
264
|
//# sourceMappingURL=server.js.map
|