@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,44 +1,174 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { SANDBOX_CONTROL_MAX_BYTES, isSandboxControlFamily } from './protocol.js';
|
|
4
|
+
import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
|
|
5
|
+
import { readSandboxControlStatus } from './state.js';
|
|
6
|
+
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
7
|
+
export class SandboxControlClientError extends Error {
|
|
8
|
+
detail;
|
|
9
|
+
accepted;
|
|
10
|
+
constructor(detail, accepted = false) {
|
|
11
|
+
super(detail.message);
|
|
12
|
+
this.name = 'SandboxControlClientError';
|
|
13
|
+
this.detail = detail;
|
|
14
|
+
this.accepted = accepted;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
5
17
|
function sleep(milliseconds) {
|
|
6
18
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
7
19
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
function clientError(code, message, retryable, accepted = false) {
|
|
21
|
+
throw new SandboxControlClientError({ code, message: `${code}: ${message}`, retryable }, accepted);
|
|
22
|
+
}
|
|
23
|
+
function preflight(statusDir, generation, now = Date.now()) {
|
|
24
|
+
let status;
|
|
25
|
+
try {
|
|
26
|
+
status = readSandboxControlStatus(statusDir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
|
|
11
30
|
}
|
|
31
|
+
if (status.generation !== generation) {
|
|
32
|
+
clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
|
|
33
|
+
}
|
|
34
|
+
if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
|
|
35
|
+
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
|
|
36
|
+
}
|
|
37
|
+
if (status.state === 'busy')
|
|
38
|
+
clientError('SANDBOX_CONTROL_BUSY', 'broker is executing another request', true);
|
|
39
|
+
if (status.state === 'parked') {
|
|
40
|
+
const code = status.reasonCode ?? 'SANDBOX_CONTROL_BROKER_UNAVAILABLE';
|
|
41
|
+
clientError(code, 'broker is parked until the host restores a valid binding', true);
|
|
42
|
+
}
|
|
43
|
+
if (status.state !== 'healthy')
|
|
44
|
+
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker is not ready', true);
|
|
45
|
+
}
|
|
46
|
+
function parseResponse(raw, id) {
|
|
47
|
+
const response = JSON.parse(raw);
|
|
48
|
+
if (response.version !== 2 || response.id !== id
|
|
49
|
+
|| !['accepted', 'completed', 'rejected'].includes(response.phase)
|
|
50
|
+
|| (response.phase === 'completed' && !Number.isInteger(response.exitCode))) {
|
|
51
|
+
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response is invalid', false);
|
|
52
|
+
}
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
function cancelPendingRequest(requestPath) {
|
|
56
|
+
try {
|
|
57
|
+
fs.unlinkSync(requestPath);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === 'ENOENT')
|
|
62
|
+
return false;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function exchangeSandboxControl(request, params) {
|
|
12
67
|
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
16
|
-
const requestsDir = path.join(channelDir, 'requests');
|
|
17
|
-
const responsesDir = path.join(channelDir, 'responses');
|
|
18
|
-
const id = randomUUID();
|
|
19
|
-
const request = { version: 1, id, token, family: params.family, args: params.args };
|
|
68
|
+
const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
|
|
69
|
+
preflight(statusDir, request.generation);
|
|
20
70
|
const encoded = `${JSON.stringify(request)}\n`;
|
|
21
71
|
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
22
|
-
|
|
72
|
+
clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false);
|
|
23
73
|
}
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
74
|
+
const requestsDir = path.join(channelDir, 'requests');
|
|
75
|
+
const responsesDir = path.join(channelDir, 'responses');
|
|
76
|
+
const temporary = path.join(requestsDir, `.${request.id}.tmp`);
|
|
77
|
+
const requestPath = path.join(requestsDir, `${request.id}.json`);
|
|
78
|
+
const responsePath = path.join(responsesDir, `${request.id}.json`);
|
|
27
79
|
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
28
80
|
fs.renameSync(temporary, requestPath);
|
|
29
81
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
82
|
+
let accepted = false;
|
|
83
|
+
let malformedResponseRaw = null;
|
|
84
|
+
let malformedResponseObservedAt = 0;
|
|
30
85
|
while (Date.now() < deadline) {
|
|
31
86
|
if (fs.existsSync(responsePath)) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
let raw;
|
|
88
|
+
try {
|
|
89
|
+
raw = fs.readFileSync(responsePath, 'utf8');
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error.code === 'ENOENT')
|
|
93
|
+
continue;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
let response;
|
|
97
|
+
try {
|
|
98
|
+
response = parseResponse(raw, request.id);
|
|
99
|
+
malformedResponseRaw = null;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!(error instanceof SyntaxError))
|
|
103
|
+
throw error;
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
if (malformedResponseRaw !== raw) {
|
|
106
|
+
malformedResponseRaw = raw;
|
|
107
|
+
malformedResponseObservedAt = now;
|
|
108
|
+
}
|
|
109
|
+
else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
|
|
110
|
+
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted);
|
|
111
|
+
}
|
|
112
|
+
sleep(25);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (response.phase === 'accepted') {
|
|
116
|
+
accepted = true;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
fs.rmSync(responsePath, { force: true });
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!accepted) {
|
|
124
|
+
try {
|
|
125
|
+
preflight(statusDir, request.generation);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (cancelPendingRequest(requestPath))
|
|
129
|
+
throw error;
|
|
130
|
+
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'request was claimed before broker availability was lost; inspect domain state before retrying', false, true);
|
|
37
131
|
}
|
|
38
|
-
return response;
|
|
39
132
|
}
|
|
40
133
|
sleep(25);
|
|
41
134
|
}
|
|
42
|
-
|
|
135
|
+
if (accepted) {
|
|
136
|
+
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'accepted request did not produce a final result; inspect domain state before retrying', false, true);
|
|
137
|
+
}
|
|
138
|
+
if (!cancelPendingRequest(requestPath)) {
|
|
139
|
+
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'request was claimed but no acceptance result was observed; inspect domain state before retrying', false, true);
|
|
140
|
+
}
|
|
141
|
+
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', `broker did not accept the request within ${params.timeoutMs ?? 30_000}ms`, true);
|
|
142
|
+
}
|
|
143
|
+
function authority(params) {
|
|
144
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
145
|
+
const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
146
|
+
if (!token || !generation)
|
|
147
|
+
clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
|
|
148
|
+
return { token, generation };
|
|
149
|
+
}
|
|
150
|
+
export function requestSandboxControl(params) {
|
|
151
|
+
if (!isSandboxControlFamily(params.family))
|
|
152
|
+
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
153
|
+
if (params.family === 'task-create')
|
|
154
|
+
clientError('SANDBOX_CONTROL_COMMAND_DENIED', "'task-create' requires a typed candidate", false);
|
|
155
|
+
const auth = authority(params);
|
|
156
|
+
const issuedAt = Date.now();
|
|
157
|
+
const request = {
|
|
158
|
+
version: 2, id: randomUUID(), ...auth, issuedAt,
|
|
159
|
+
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
160
|
+
family: params.family, args: params.args
|
|
161
|
+
};
|
|
162
|
+
return exchangeSandboxControl(request, params);
|
|
163
|
+
}
|
|
164
|
+
export function requestSandboxTaskCreate(params) {
|
|
165
|
+
const auth = authority(params);
|
|
166
|
+
const issuedAt = Date.now();
|
|
167
|
+
const request = {
|
|
168
|
+
version: 2, id: randomUUID(), ...auth, issuedAt,
|
|
169
|
+
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
170
|
+
family: 'task-create', candidate: params.candidate
|
|
171
|
+
};
|
|
172
|
+
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
43
173
|
}
|
|
44
174
|
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { getProcessStartTime } from '../../server/process-state.js';
|
|
6
|
+
import { createTask } from '../../task/create-service.js';
|
|
7
|
+
import { bindSandboxControlTask } from './protocol.js';
|
|
8
|
+
import { atomicWriteJson, executionPath, terminateSandboxControlExecution } from './state.js';
|
|
9
|
+
function safeEnv(env) {
|
|
10
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
|
|
11
|
+
}
|
|
12
|
+
export function nodeEntryArgs(entry, args) {
|
|
13
|
+
return path.extname(entry) === '.ts'
|
|
14
|
+
? ['--experimental-strip-types', '--no-warnings', entry, ...args]
|
|
15
|
+
: [entry, ...args];
|
|
16
|
+
}
|
|
17
|
+
async function waitForStartTime(pid, timeoutMs = 2_000) {
|
|
18
|
+
const deadline = Date.now() + timeoutMs;
|
|
19
|
+
while (Date.now() < deadline) {
|
|
20
|
+
const startTime = getProcessStartTime(pid);
|
|
21
|
+
if (startTime)
|
|
22
|
+
return startTime;
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
24
|
+
}
|
|
25
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_IDENTITY_UNAVAILABLE');
|
|
26
|
+
}
|
|
27
|
+
export async function prepareSandboxControlExecution(params) {
|
|
28
|
+
const nonce = randomUUID();
|
|
29
|
+
const child = spawn(process.execPath, nodeEntryArgs(params.internalCliPath, [
|
|
30
|
+
'sandbox-control', 'execute', '--request', params.requestPath, '--nonce', nonce
|
|
31
|
+
]), {
|
|
32
|
+
cwd: params.manifest.repoRoot,
|
|
33
|
+
detached: process.platform !== 'win32',
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
35
|
+
env: { ...safeEnv(process.env), AGENT_INFRA_EXECUTOR_MANIFEST: params.manifestPath }
|
|
36
|
+
});
|
|
37
|
+
if (typeof child.pid !== 'number')
|
|
38
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_SPAWN_FAILED');
|
|
39
|
+
let stdout = '';
|
|
40
|
+
let stderr = '';
|
|
41
|
+
child.stdout?.setEncoding('utf8');
|
|
42
|
+
child.stderr?.setEncoding('utf8');
|
|
43
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk; });
|
|
44
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk; });
|
|
45
|
+
const completion = new Promise((resolve, reject) => {
|
|
46
|
+
child.once('error', reject);
|
|
47
|
+
child.once('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
48
|
+
});
|
|
49
|
+
const startTime = await waitForStartTime(child.pid);
|
|
50
|
+
const execution = {
|
|
51
|
+
version: 1,
|
|
52
|
+
generation: params.manifest.generation,
|
|
53
|
+
requestId: params.request.id,
|
|
54
|
+
nonce,
|
|
55
|
+
child: {
|
|
56
|
+
pid: child.pid,
|
|
57
|
+
startTime,
|
|
58
|
+
processGroupId: process.platform === 'win32' ? null : child.pid
|
|
59
|
+
},
|
|
60
|
+
phase: 'prepared',
|
|
61
|
+
updatedAt: Date.now()
|
|
62
|
+
};
|
|
63
|
+
atomicWriteJson(executionPath(params.manifest, params.request.id), execution);
|
|
64
|
+
return {
|
|
65
|
+
execution,
|
|
66
|
+
start() {
|
|
67
|
+
if (!child.connected)
|
|
68
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
|
|
69
|
+
atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
70
|
+
child.send({ version: 1, nonce });
|
|
71
|
+
},
|
|
72
|
+
completion,
|
|
73
|
+
terminate() {
|
|
74
|
+
atomicWriteJson(executionPath(params.manifest, params.request.id), {
|
|
75
|
+
...execution, phase: 'terminating', updatedAt: Date.now()
|
|
76
|
+
});
|
|
77
|
+
return terminateSandboxControlExecution(execution);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function waitForGate(nonce, timeoutMs = 2_000) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
|
|
84
|
+
const onDisconnect = () => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED'));
|
|
87
|
+
};
|
|
88
|
+
process.once('disconnect', onDisconnect);
|
|
89
|
+
process.once('message', (message) => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
process.off('disconnect', onDisconnect);
|
|
92
|
+
const value = message;
|
|
93
|
+
if (!value || value.version !== 1 || value.nonce !== nonce) {
|
|
94
|
+
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function executeRequest(manifest, request) {
|
|
102
|
+
if (request.family === 'task-create') {
|
|
103
|
+
const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
104
|
+
return {
|
|
105
|
+
exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
|
|
106
|
+
stdout: `${JSON.stringify(result)}\n`,
|
|
107
|
+
stderr: ''
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const boundArgs = bindSandboxControlTask(request, manifest.taskId);
|
|
111
|
+
if (request.family === 'task-orchestration')
|
|
112
|
+
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
113
|
+
const result = spawnSync(process.execPath, nodeEntryArgs(process.argv[1], [request.family, ...boundArgs]), { cwd: manifest.repoRoot, encoding: 'utf8', env: safeEnv(process.env) });
|
|
114
|
+
return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
115
|
+
}
|
|
116
|
+
export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
117
|
+
await waitForGate(nonce);
|
|
118
|
+
process.disconnect?.();
|
|
119
|
+
const root = fs.realpathSync.native(process.cwd());
|
|
120
|
+
const request = JSON.parse(fs.readFileSync(requestPath, 'utf8'));
|
|
121
|
+
const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
|
|
122
|
+
if (!manifestPath)
|
|
123
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
|
|
124
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
125
|
+
if (fs.realpathSync.native(manifest.repoRoot) !== root)
|
|
126
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_ROOT_INVALID');
|
|
127
|
+
const result = executeRequest(manifest, request);
|
|
128
|
+
process.stdout.write(result.stdout);
|
|
129
|
+
process.stderr.write(result.stderr);
|
|
130
|
+
process.exitCode = result.exitCode;
|
|
131
|
+
}
|
|
132
|
+
export function disconnectExecutor(child) {
|
|
133
|
+
if (child.connected)
|
|
134
|
+
child.disconnect();
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=executor.js.map
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { buildProcessTreeStopCommand } from '../../server/process-control.js';
|
|
5
|
+
import { processIdentityMatches } from '../../server/process-state.js';
|
|
6
|
+
import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
|
|
7
|
+
const DEFAULT_QUIESCE_TIMEOUT_MS = 7_000;
|
|
8
|
+
const QUIESCING_FILE = 'quiescing.json';
|
|
9
|
+
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
10
|
+
function regularFile(filePath) {
|
|
11
|
+
try {
|
|
12
|
+
const stat = fs.lstatSync(filePath);
|
|
13
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT')
|
|
17
|
+
return false;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isSandboxControlRootQuiescing(root) {
|
|
22
|
+
return fs.existsSync(path.join(path.resolve(root), QUIESCING_FILE));
|
|
23
|
+
}
|
|
24
|
+
function markSandboxControlRootQuiescing(root) {
|
|
25
|
+
const markerPath = path.join(root, QUIESCING_FILE);
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(markerPath, `${JSON.stringify({ version: 1, startedAt: Date.now() })}\n`, {
|
|
28
|
+
mode: 0o600,
|
|
29
|
+
flag: 'wx'
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== 'EEXIST')
|
|
34
|
+
throw error;
|
|
35
|
+
if (!regularFile(markerPath))
|
|
36
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readStartupOwner(filePath) {
|
|
40
|
+
if (!fs.existsSync(filePath))
|
|
41
|
+
return null;
|
|
42
|
+
if (!regularFile(filePath))
|
|
43
|
+
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
44
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
try {
|
|
46
|
+
const record = JSON.parse(raw);
|
|
47
|
+
if (record.version !== 1 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
|
|
48
|
+
|| typeof record.startTime !== 'string')
|
|
49
|
+
return null;
|
|
50
|
+
return { raw, owner: { pid: record.pid, startTime: record.startTime } };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function waitForStartupTransition(root, timeoutMs) {
|
|
57
|
+
const transitionPath = path.join(root, BROKER_STARTING_FILE);
|
|
58
|
+
const deadline = Date.now() + timeoutMs;
|
|
59
|
+
while (Date.now() < deadline) {
|
|
60
|
+
const transition = readStartupOwner(transitionPath);
|
|
61
|
+
if (!fs.existsSync(transitionPath))
|
|
62
|
+
return;
|
|
63
|
+
if (transition && !ownerLive(transition.owner)) {
|
|
64
|
+
try {
|
|
65
|
+
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
|
|
66
|
+
fs.unlinkSync(transitionPath);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// A concurrent startup transition changed or removed the record.
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
74
|
+
}
|
|
75
|
+
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
76
|
+
}
|
|
77
|
+
export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000) {
|
|
78
|
+
const resolvedRoot = path.resolve(root);
|
|
79
|
+
const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
80
|
+
const record = `${JSON.stringify({ version: 1, ...owner })}\n`;
|
|
81
|
+
const deadline = Date.now() + timeoutMs;
|
|
82
|
+
while (Date.now() < deadline) {
|
|
83
|
+
if (isSandboxControlRootQuiescing(resolvedRoot))
|
|
84
|
+
throw new Error('SANDBOX_CONTROL_QUIESCING');
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(transitionPath, record, { mode: 0o600, flag: 'wx' });
|
|
87
|
+
if (isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
88
|
+
try {
|
|
89
|
+
if (fs.readFileSync(transitionPath, 'utf8') === record)
|
|
90
|
+
fs.unlinkSync(transitionPath);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// The deletion side owns the transition now.
|
|
94
|
+
}
|
|
95
|
+
throw new Error('SANDBOX_CONTROL_QUIESCING');
|
|
96
|
+
}
|
|
97
|
+
return () => {
|
|
98
|
+
try {
|
|
99
|
+
if (fs.readFileSync(transitionPath, 'utf8') === record)
|
|
100
|
+
fs.unlinkSync(transitionPath);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// A newer transition or deletion owns the path.
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code !== 'EEXIST')
|
|
109
|
+
throw error;
|
|
110
|
+
const transition = readStartupOwner(transitionPath);
|
|
111
|
+
if (transition && !ownerLive(transition.owner)) {
|
|
112
|
+
try {
|
|
113
|
+
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
|
|
114
|
+
fs.unlinkSync(transitionPath);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// A concurrent transition changed the record.
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
125
|
+
}
|
|
126
|
+
export function readSandboxControlManifest(manifestPath) {
|
|
127
|
+
if (!regularFile(manifestPath))
|
|
128
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
129
|
+
let manifest;
|
|
130
|
+
try {
|
|
131
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
135
|
+
}
|
|
136
|
+
if (manifest.version !== 3) {
|
|
137
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 3; container-only recreation is required');
|
|
138
|
+
}
|
|
139
|
+
if (typeof manifest.repoRoot !== 'string' || typeof manifest.worktreeRoot !== 'string'
|
|
140
|
+
|| typeof manifest.project !== 'string' || typeof manifest.container !== 'string'
|
|
141
|
+
|| typeof manifest.branch !== 'string' || !['task-bound', 'branch-only'].includes(manifest.mode ?? '')
|
|
142
|
+
|| (manifest.taskId !== null && typeof manifest.taskId !== 'string')
|
|
143
|
+
|| typeof manifest.channelDir !== 'string' || typeof manifest.publicStatusDir !== 'string'
|
|
144
|
+
|| typeof manifest.processingDir !== 'string' || typeof manifest.token !== 'string'
|
|
145
|
+
|| typeof manifest.generation !== 'string')
|
|
146
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
147
|
+
const root = path.dirname(path.resolve(manifestPath));
|
|
148
|
+
if (path.resolve(manifest.channelDir) !== path.join(root, 'channel')
|
|
149
|
+
|| path.resolve(manifest.publicStatusDir) !== path.join(root, 'public')
|
|
150
|
+
|| path.resolve(manifest.processingDir) !== path.join(root, 'processing')) {
|
|
151
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
152
|
+
}
|
|
153
|
+
return manifest;
|
|
154
|
+
}
|
|
155
|
+
function readBrokerOwner(filePath) {
|
|
156
|
+
if (!fs.existsSync(filePath))
|
|
157
|
+
return null;
|
|
158
|
+
if (!regularFile(filePath))
|
|
159
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
160
|
+
let owner;
|
|
161
|
+
try {
|
|
162
|
+
owner = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
166
|
+
}
|
|
167
|
+
if (!owner || owner.version !== 2 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
|
|
168
|
+
|| typeof owner.startTime !== 'string' || typeof owner.token !== 'string'
|
|
169
|
+
|| typeof owner.generation !== 'string')
|
|
170
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
171
|
+
return owner;
|
|
172
|
+
}
|
|
173
|
+
function readStatusOwner(filePath) {
|
|
174
|
+
if (!fs.existsSync(filePath))
|
|
175
|
+
return null;
|
|
176
|
+
if (!regularFile(filePath))
|
|
177
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
178
|
+
try {
|
|
179
|
+
return parseSandboxControlStatus(JSON.parse(fs.readFileSync(filePath, 'utf8')));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function sameOwner(left, right) {
|
|
186
|
+
return left.pid === right.pid && left.startTime === right.startTime;
|
|
187
|
+
}
|
|
188
|
+
function ownerLive(owner) {
|
|
189
|
+
return processIdentityMatches({ version: 1, pid: owner.pid, startTime: owner.startTime });
|
|
190
|
+
}
|
|
191
|
+
function readExecutions(manifest) {
|
|
192
|
+
if (!fs.existsSync(manifest.processingDir))
|
|
193
|
+
return [];
|
|
194
|
+
const stat = fs.lstatSync(manifest.processingDir);
|
|
195
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
196
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
197
|
+
const executions = [];
|
|
198
|
+
for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
|
|
199
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
200
|
+
continue;
|
|
201
|
+
const descriptor = path.join(manifest.processingDir, entry.name, 'execution.json');
|
|
202
|
+
if (!fs.existsSync(descriptor))
|
|
203
|
+
continue;
|
|
204
|
+
let execution;
|
|
205
|
+
try {
|
|
206
|
+
execution = readExecution(descriptor);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
|
|
210
|
+
}
|
|
211
|
+
if (execution.generation !== manifest.generation || execution.requestId !== entry.name) {
|
|
212
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
|
|
213
|
+
}
|
|
214
|
+
if (execution.child.pid <= 0 || (execution.child.processGroupId !== null
|
|
215
|
+
&& (!Number.isSafeInteger(execution.child.processGroupId) || execution.child.processGroupId <= 0))) {
|
|
216
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
|
|
217
|
+
}
|
|
218
|
+
executions.push(execution);
|
|
219
|
+
}
|
|
220
|
+
return executions;
|
|
221
|
+
}
|
|
222
|
+
function processGroupAlive(groupId) {
|
|
223
|
+
try {
|
|
224
|
+
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
225
|
+
return rows.split('\n').some((row) => {
|
|
226
|
+
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
227
|
+
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
try {
|
|
232
|
+
process.kill(-groupId, 0);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
return error.code === 'EPERM';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function executionAlive(execution, platform) {
|
|
241
|
+
if (platform !== 'win32' && execution.child.processGroupId) {
|
|
242
|
+
return processGroupAlive(execution.child.processGroupId);
|
|
243
|
+
}
|
|
244
|
+
return processIdentityMatches({ version: 1, pid: execution.child.pid, startTime: execution.child.startTime });
|
|
245
|
+
}
|
|
246
|
+
async function waitForExit(owner, timeoutMs) {
|
|
247
|
+
const deadline = Date.now() + timeoutMs;
|
|
248
|
+
while (Date.now() < deadline) {
|
|
249
|
+
if (!ownerLive(owner))
|
|
250
|
+
return true;
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
252
|
+
}
|
|
253
|
+
return !ownerLive(owner);
|
|
254
|
+
}
|
|
255
|
+
function signalOwner(owner, platform, force) {
|
|
256
|
+
try {
|
|
257
|
+
if (platform === 'win32') {
|
|
258
|
+
const command = buildProcessTreeStopCommand(owner.pid, platform);
|
|
259
|
+
if (command.kind === 'exec')
|
|
260
|
+
execFileSync(command.command, command.args, { stdio: 'ignore' });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
process.kill(owner.pid, force ? 'SIGKILL' : 'SIGTERM');
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// The identity checks decide whether the owner actually exited.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function mergeExecutions(...groups) {
|
|
270
|
+
const merged = new Map();
|
|
271
|
+
for (const execution of groups.flat())
|
|
272
|
+
merged.set(`${execution.requestId}:${execution.nonce}`, execution);
|
|
273
|
+
return [...merged.values()];
|
|
274
|
+
}
|
|
275
|
+
export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
276
|
+
const resolvedRoot = path.resolve(root);
|
|
277
|
+
if (!fs.existsSync(resolvedRoot))
|
|
278
|
+
return 'missing';
|
|
279
|
+
const rootStat = fs.lstatSync(resolvedRoot);
|
|
280
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
281
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
282
|
+
for (const directory of [path.join(resolvedRoot, 'public'), path.join(resolvedRoot, 'processing')]) {
|
|
283
|
+
if (!fs.existsSync(directory))
|
|
284
|
+
continue;
|
|
285
|
+
const stat = fs.lstatSync(directory);
|
|
286
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
287
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
288
|
+
}
|
|
289
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
290
|
+
await waitForStartupTransition(resolvedRoot, options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS);
|
|
291
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
292
|
+
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
|
|
293
|
+
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
294
|
+
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
295
|
+
const statusOwner = status ? { version: 1, ...status.broker } : null;
|
|
296
|
+
const brokerLive = broker ? ownerLive(broker) : false;
|
|
297
|
+
const statusLive = statusOwner ? ownerLive(statusOwner) : false;
|
|
298
|
+
if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
299
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
300
|
+
}
|
|
301
|
+
if (manifest && status && status.generation !== manifest.generation) {
|
|
302
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
303
|
+
}
|
|
304
|
+
if (broker && statusOwner && !sameOwner(broker, statusOwner) && (brokerLive || statusLive)) {
|
|
305
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
306
|
+
}
|
|
307
|
+
if (!manifest && brokerLive)
|
|
308
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
309
|
+
const platform = options.platform ?? process.platform;
|
|
310
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
311
|
+
let executions = manifest ? readExecutions(manifest) : [];
|
|
312
|
+
const owner = brokerLive ? broker : statusLive ? statusOwner : null;
|
|
313
|
+
if (!owner) {
|
|
314
|
+
if (manifest && !broker && !status)
|
|
315
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
316
|
+
for (const execution of executions) {
|
|
317
|
+
if (!terminateSandboxControlExecution(execution, { platform, timeoutMs })) {
|
|
318
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
322
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
323
|
+
}
|
|
324
|
+
return broker || status ? 'stale' : 'missing';
|
|
325
|
+
}
|
|
326
|
+
signalOwner(owner, platform, false);
|
|
327
|
+
if (await waitForExit(owner, timeoutMs)) {
|
|
328
|
+
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
329
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
330
|
+
}
|
|
331
|
+
return 'stopped';
|
|
332
|
+
}
|
|
333
|
+
if (manifest)
|
|
334
|
+
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
335
|
+
for (const execution of executions) {
|
|
336
|
+
if (!terminateSandboxControlExecution(execution, { platform, timeoutMs })) {
|
|
337
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
signalOwner(owner, platform, true);
|
|
341
|
+
if (!await waitForExit(owner, timeoutMs))
|
|
342
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
343
|
+
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
344
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
345
|
+
}
|
|
346
|
+
return 'stopped';
|
|
347
|
+
}
|
|
348
|
+
//# sourceMappingURL=lifecycle.js.map
|