@fitlab-ai/agent-infra 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -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 +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -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/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- 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/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- 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 +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -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/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- 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/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/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/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/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 +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -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
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -8,9 +8,9 @@ const USAGE = `Usage: ai sandbox start [--recreate] <branch | TASK-id | N>
|
|
|
8
8
|
|
|
9
9
|
Start an existing sandbox container that has stopped (for example after the
|
|
10
10
|
Docker daemon was restarted or replaced). The container must already exist:
|
|
11
|
-
if none is found, run 'ai sandbox create <branch>' first.
|
|
12
|
-
already running is checked without modifying healthy runtime
|
|
13
|
-
|
|
11
|
+
if none is found, run 'ai sandbox create <branch>' first. Without --recreate, a
|
|
12
|
+
container that is already running is checked without modifying healthy runtime
|
|
13
|
+
state. --recreate replaces the existing container even when it is healthy.`;
|
|
14
14
|
export function parseStartArgs(args) {
|
|
15
15
|
let recreate = false;
|
|
16
16
|
let help = false;
|
|
@@ -62,6 +62,7 @@ export async function start(args) {
|
|
|
62
62
|
workspace: target.workspace,
|
|
63
63
|
row: found,
|
|
64
64
|
allowRecreate: parsed.recreate,
|
|
65
|
+
forceRecreate: parsed.recreate,
|
|
65
66
|
recreate: async () => {
|
|
66
67
|
const { create } = await import('./create.js');
|
|
67
68
|
await create([target.requestedRef, '--no-refresh']);
|
|
@@ -1,15 +1,75 @@
|
|
|
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
|
}
|
|
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);
|
|
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
|
+
}
|
|
8
66
|
function exchangeSandboxControl(request, params) {
|
|
9
67
|
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
68
|
+
const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
|
|
69
|
+
preflight(statusDir, request.generation);
|
|
10
70
|
const encoded = `${JSON.stringify(request)}\n`;
|
|
11
71
|
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
12
|
-
|
|
72
|
+
clientError('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit', false);
|
|
13
73
|
}
|
|
14
74
|
const requestsDir = path.join(channelDir, 'requests');
|
|
15
75
|
const responsesDir = path.join(channelDir, 'responses');
|
|
@@ -19,39 +79,95 @@ function exchangeSandboxControl(request, params) {
|
|
|
19
79
|
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
20
80
|
fs.renameSync(temporary, requestPath);
|
|
21
81
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
82
|
+
let accepted = false;
|
|
83
|
+
let malformedResponseRaw = null;
|
|
84
|
+
let malformedResponseObservedAt = 0;
|
|
22
85
|
while (Date.now() < deadline) {
|
|
23
86
|
if (fs.existsSync(responsePath)) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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);
|
|
29
131
|
}
|
|
30
|
-
return response;
|
|
31
132
|
}
|
|
32
133
|
sleep(25);
|
|
33
134
|
}
|
|
34
|
-
|
|
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 };
|
|
35
149
|
}
|
|
36
150
|
export function requestSandboxControl(params) {
|
|
37
|
-
if (!isSandboxControlFamily(params.family))
|
|
38
|
-
|
|
39
|
-
}
|
|
151
|
+
if (!isSandboxControlFamily(params.family))
|
|
152
|
+
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
40
153
|
if (params.family === 'task-create')
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
};
|
|
47
162
|
return exchangeSandboxControl(request, params);
|
|
48
163
|
}
|
|
49
164
|
export function requestSandboxTaskCreate(params) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
165
|
+
const auth = authority(params);
|
|
166
|
+
const issuedAt = Date.now();
|
|
53
167
|
const request = {
|
|
54
|
-
version:
|
|
168
|
+
version: 2, id: randomUUID(), ...auth, issuedAt,
|
|
169
|
+
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
170
|
+
family: 'task-create', candidate: params.candidate
|
|
55
171
|
};
|
|
56
172
|
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
57
173
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { commandForEngine, runEngine, runProbe } from '../shell.js';
|
|
2
|
+
function isAuthoritativeNotFound(error) {
|
|
3
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4
|
+
return /(?:no such container|container .* not found|cannot find container)/i.test(message);
|
|
5
|
+
}
|
|
6
|
+
export function classifySandboxContainerInspection(expected, inspection) {
|
|
7
|
+
if ('notFound' in inspection)
|
|
8
|
+
return { state: 'absent', id: expected.id };
|
|
9
|
+
if ('error' in inspection) {
|
|
10
|
+
return isAuthoritativeNotFound(inspection.error)
|
|
11
|
+
? { state: 'absent', id: expected.id }
|
|
12
|
+
: { state: 'unknown', reason: `container inspection failed: ${inspection.error instanceof Error ? inspection.error.message : String(inspection.error)}` };
|
|
13
|
+
}
|
|
14
|
+
const response = inspection;
|
|
15
|
+
if (typeof response.id !== 'string' || response.id !== expected.id
|
|
16
|
+
|| typeof response.running !== 'boolean' || !response.labels
|
|
17
|
+
|| typeof response.labels !== 'object' || Array.isArray(response.labels)) {
|
|
18
|
+
return { state: 'unknown', reason: 'container inspection response is incomplete' };
|
|
19
|
+
}
|
|
20
|
+
const labels = response.labels;
|
|
21
|
+
for (const [key, value] of Object.entries(expected.labels)) {
|
|
22
|
+
if (labels[key] !== value)
|
|
23
|
+
return { state: 'unknown', reason: 'container labels do not match the manifest' };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
state: 'found',
|
|
27
|
+
id: response.id,
|
|
28
|
+
running: response.running,
|
|
29
|
+
labels: Object.fromEntries(Object.entries(labels).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : []))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export async function inspectSandboxControlContainer(manifest, options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const args = ['inspect', '--format', '{{json .}}', manifest.containerIdentity.id];
|
|
35
|
+
let raw;
|
|
36
|
+
if (options.run) {
|
|
37
|
+
raw = options.run(manifest.engine, 'docker', args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const command = commandForEngine(manifest.engine, 'docker', args);
|
|
41
|
+
const result = (options.probe ?? runProbe)(command.cmd, command.args, options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs });
|
|
42
|
+
if (result.status !== 0) {
|
|
43
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : result.stderr?.toString('utf8') ?? '';
|
|
44
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, {
|
|
45
|
+
error: new Error(stderr || `container inspection exited with ${result.status ?? 'unknown'}`)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
raw = typeof result.stdout === 'string' ? result.stdout : result.stdout?.toString('utf8') ?? '';
|
|
49
|
+
}
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
const value = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
|
|
52
|
+
if (!value || Array.isArray(value))
|
|
53
|
+
return { state: 'unknown', reason: 'container inspection response is incomplete' };
|
|
54
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, {
|
|
55
|
+
id: value.Id,
|
|
56
|
+
running: value.State?.Running,
|
|
57
|
+
labels: value.Config?.Labels
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return classifySandboxContainerInspection(manifest.containerIdentity, { error });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=container-identity.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
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 !== null)
|
|
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: 2,
|
|
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(canWrite = () => true) {
|
|
67
|
+
if (!child.connected)
|
|
68
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
|
|
69
|
+
if (!canWrite())
|
|
70
|
+
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
71
|
+
atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
72
|
+
if (!canWrite())
|
|
73
|
+
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
74
|
+
child.send({ version: 1, nonce });
|
|
75
|
+
},
|
|
76
|
+
completion,
|
|
77
|
+
terminate(updateState = true) {
|
|
78
|
+
if (updateState) {
|
|
79
|
+
atomicWriteJson(executionPath(params.manifest, params.request.id), {
|
|
80
|
+
...execution, phase: 'terminating', updatedAt: Date.now()
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return terminateSandboxControlExecution(execution);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function waitForGate(nonce, timeoutMs = 2_000) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
|
|
90
|
+
const onDisconnect = () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED'));
|
|
93
|
+
};
|
|
94
|
+
process.once('disconnect', onDisconnect);
|
|
95
|
+
process.once('message', (message) => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
process.off('disconnect', onDisconnect);
|
|
98
|
+
const value = message;
|
|
99
|
+
if (!value || value.version !== 1 || value.nonce !== nonce) {
|
|
100
|
+
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function executeRequest(manifest, request) {
|
|
108
|
+
if (request.family === 'task-create') {
|
|
109
|
+
const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
110
|
+
return {
|
|
111
|
+
exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
|
|
112
|
+
stdout: `${JSON.stringify(result)}\n`,
|
|
113
|
+
stderr: ''
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const boundArgs = bindSandboxControlTask(request, manifest.taskId);
|
|
117
|
+
if (request.family === 'task-orchestration')
|
|
118
|
+
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
119
|
+
const result = spawnSync(process.execPath, nodeEntryArgs(process.argv[1], [request.family, ...boundArgs]), { cwd: manifest.repoRoot, encoding: 'utf8', env: safeEnv(process.env) });
|
|
120
|
+
return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
|
|
121
|
+
}
|
|
122
|
+
export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
123
|
+
await waitForGate(nonce);
|
|
124
|
+
process.disconnect?.();
|
|
125
|
+
const root = fs.realpathSync.native(process.cwd());
|
|
126
|
+
const request = JSON.parse(fs.readFileSync(requestPath, 'utf8'));
|
|
127
|
+
const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
|
|
128
|
+
if (!manifestPath)
|
|
129
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
|
|
130
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
131
|
+
if (fs.realpathSync.native(manifest.repoRoot) !== root)
|
|
132
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_ROOT_INVALID');
|
|
133
|
+
const result = executeRequest(manifest, request);
|
|
134
|
+
process.stdout.write(result.stdout);
|
|
135
|
+
process.stderr.write(result.stderr);
|
|
136
|
+
process.exitCode = result.exitCode;
|
|
137
|
+
}
|
|
138
|
+
export function disconnectExecutor(child) {
|
|
139
|
+
if (child.connected)
|
|
140
|
+
child.disconnect();
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=executor.js.map
|