@fitlab-ai/agent-infra 0.9.6 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/task-orchestration.js +37 -5
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/sandbox/commands/create.js +103 -51
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +13 -7
- package/dist/lib/sandbox/control/lifecycle.js +238 -29
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +25 -15
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/mounts.js +5 -13
- package/dist/lib/sandbox/recovery.js +59 -42
- package/dist/lib/sandbox/workspace-view.js +57 -40
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +84 -2
- package/dist/lib/task/events.js +139 -2
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +235 -14
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +40 -8
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/task-orchestration.ts +31 -4
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/sandbox/commands/create.ts +56 -7
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +14 -10
- package/lib/sandbox/control/lifecycle.ts +264 -33
- package/lib/sandbox/control/protocol.ts +31 -5
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +34 -17
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/mounts.ts +5 -15
- package/lib/sandbox/recovery.ts +71 -42
- package/lib/sandbox/workspace-view.ts +61 -40
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +160 -8
- package/lib/task/events.ts +122 -1
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +356 -17
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +51 -16
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
4
5
|
import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
|
|
5
|
-
import { controlError,
|
|
6
|
+
import { controlError, DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
|
|
6
7
|
import { prepareSandboxControlExecution } from './executor.js';
|
|
7
8
|
import { appendSandboxControlAudit, atomicWriteJson, cleanupStaleSandboxControlLease, executionPath, readActiveLease, readExecution, terminateSandboxControlExecution, writeSandboxControlStatus } from './state.js';
|
|
8
9
|
import { validateSandboxControlRequest } from './protocol.js';
|
|
9
|
-
import {
|
|
10
|
+
import { inspectSandboxControlContainer } from './container-identity.js';
|
|
11
|
+
import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
|
|
12
|
+
import { nextSandboxControlBackoff } from './timing.js';
|
|
10
13
|
function assertRealDirectory(directory, parent) {
|
|
11
14
|
const stat = fs.lstatSync(directory);
|
|
12
15
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
@@ -73,10 +76,12 @@ function bindingReason(manifest) {
|
|
|
73
76
|
return 'SANDBOX_WORKTREE_BINDING_LOST';
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
|
-
function recoverProcessing(manifest) {
|
|
79
|
+
function recoverProcessing(manifest, brokerOwns) {
|
|
77
80
|
for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
|
|
78
81
|
if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name))
|
|
79
82
|
continue;
|
|
83
|
+
if (!brokerOwns())
|
|
84
|
+
return false;
|
|
80
85
|
const descriptor = executionPath(manifest, entry.name);
|
|
81
86
|
const existingResponse = responsePath(manifest, entry.name);
|
|
82
87
|
if (fs.existsSync(descriptor)) {
|
|
@@ -84,6 +89,8 @@ function recoverProcessing(manifest) {
|
|
|
84
89
|
if (!terminateSandboxControlExecution(execution)) {
|
|
85
90
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
|
|
86
91
|
}
|
|
92
|
+
if (!brokerOwns())
|
|
93
|
+
return false;
|
|
87
94
|
appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
|
|
88
95
|
let terminal = false;
|
|
89
96
|
if (fs.existsSync(existingResponse)) {
|
|
@@ -96,14 +103,22 @@ function recoverProcessing(manifest) {
|
|
|
96
103
|
terminal = false;
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
|
-
if (!terminal)
|
|
106
|
+
if (!terminal) {
|
|
107
|
+
if (!brokerOwns())
|
|
108
|
+
return false;
|
|
100
109
|
writeResponse(manifest, unknown(entry.name));
|
|
110
|
+
}
|
|
101
111
|
}
|
|
102
112
|
else {
|
|
113
|
+
if (!brokerOwns())
|
|
114
|
+
return false;
|
|
103
115
|
writeResponse(manifest, notExecuted(entry.name));
|
|
104
116
|
}
|
|
117
|
+
if (!brokerOwns())
|
|
118
|
+
return false;
|
|
105
119
|
fs.rmSync(path.join(manifest.processingDir, entry.name), { recursive: true, force: true });
|
|
106
120
|
}
|
|
121
|
+
return true;
|
|
107
122
|
}
|
|
108
123
|
function delay(ms) {
|
|
109
124
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -111,19 +126,38 @@ function delay(ms) {
|
|
|
111
126
|
export function sandboxControlSafeEnv(env = process.env) {
|
|
112
127
|
return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
|
|
113
128
|
}
|
|
114
|
-
export async function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
|
|
129
|
+
export async function serveSandboxControl(manifestPath, signal = new AbortController().signal, options = {}) {
|
|
130
|
+
const timing = options.timing ?? DEFAULT_SANDBOX_CONTROL_TIMING;
|
|
131
|
+
const inspectContainer = options.inspectContainer ?? ((value) => inspectSandboxControlContainer(value));
|
|
132
|
+
const bindingCheck = options.bindingCheck ?? bindingReason;
|
|
115
133
|
const manifest = readSandboxControlManifest(manifestPath);
|
|
116
134
|
const root = path.dirname(manifestPath);
|
|
117
135
|
const startTime = getProcessStartTime(process.pid);
|
|
118
136
|
if (!startTime)
|
|
119
137
|
throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
|
|
120
|
-
const
|
|
138
|
+
const brokerId = randomUUID();
|
|
139
|
+
const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime, brokerId });
|
|
121
140
|
const requestsDir = path.join(manifest.channelDir, 'requests');
|
|
122
141
|
const responsesDir = path.join(manifest.channelDir, 'responses');
|
|
123
142
|
const consumedDir = path.join(root, 'consumed');
|
|
124
|
-
const broker = {
|
|
143
|
+
const broker = {
|
|
144
|
+
version: 3,
|
|
145
|
+
pid: process.pid,
|
|
146
|
+
startTime,
|
|
147
|
+
brokerId,
|
|
148
|
+
token: manifest.token,
|
|
149
|
+
generation: manifest.generation
|
|
150
|
+
};
|
|
125
151
|
const brokerPath = path.join(root, 'broker.json');
|
|
126
|
-
const brokerRecord = `${JSON.stringify(
|
|
152
|
+
const brokerRecord = `${JSON.stringify(broker)}\n`;
|
|
153
|
+
const brokerOwns = () => {
|
|
154
|
+
try {
|
|
155
|
+
return fs.readFileSync(brokerPath, 'utf8') === brokerRecord;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
127
161
|
try {
|
|
128
162
|
for (const directory of [manifest.channelDir, requestsDir, responsesDir, consumedDir, manifest.publicStatusDir, manifest.processingDir]) {
|
|
129
163
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -147,11 +181,24 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
147
181
|
let active = null;
|
|
148
182
|
let lastState = '';
|
|
149
183
|
let lastStatusAt = 0;
|
|
184
|
+
let nextBindingCheckAt = 0;
|
|
185
|
+
let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
|
|
186
|
+
let containerReasonCode = null;
|
|
187
|
+
let containerBackoffMs = timing.parkedBindingInitialMs;
|
|
188
|
+
let bindingReasonCode = null;
|
|
189
|
+
let bindingBackoffMs = timing.parkedBindingInitialMs;
|
|
150
190
|
try {
|
|
191
|
+
if (!brokerOwns())
|
|
192
|
+
return;
|
|
151
193
|
writeSandboxControlStatus(manifest, broker, 'starting', null, null);
|
|
194
|
+
if (!brokerOwns())
|
|
195
|
+
return;
|
|
152
196
|
appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
|
|
153
|
-
recoverProcessing(manifest)
|
|
197
|
+
if (!recoverProcessing(manifest, brokerOwns))
|
|
198
|
+
return;
|
|
154
199
|
while (!signal.aborted) {
|
|
200
|
+
if (!brokerOwns())
|
|
201
|
+
break;
|
|
155
202
|
let current;
|
|
156
203
|
try {
|
|
157
204
|
current = readSandboxControlManifest(manifestPath);
|
|
@@ -161,22 +208,78 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
161
208
|
}
|
|
162
209
|
if (current.token !== manifest.token || current.generation !== manifest.generation)
|
|
163
210
|
break;
|
|
211
|
+
const heartbeatNow = Date.now();
|
|
212
|
+
if (heartbeatNow >= nextContainerHeartbeatAt) {
|
|
213
|
+
let observation;
|
|
214
|
+
try {
|
|
215
|
+
observation = await inspectContainer(manifest);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
observation = { state: 'unknown', reason: controlError(error).code };
|
|
219
|
+
}
|
|
220
|
+
if (!brokerOwns())
|
|
221
|
+
break;
|
|
222
|
+
if (observation.state === 'found') {
|
|
223
|
+
containerReasonCode = null;
|
|
224
|
+
containerBackoffMs = timing.parkedBindingInitialMs;
|
|
225
|
+
nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
|
|
226
|
+
}
|
|
227
|
+
else if (observation.state === 'unknown') {
|
|
228
|
+
containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_UNKNOWN';
|
|
229
|
+
appendSandboxControlAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
|
|
230
|
+
const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
|
|
231
|
+
nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
|
|
232
|
+
containerBackoffMs = backoff.nextDelayMs;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
try {
|
|
236
|
+
await garbageCollectSandboxControlRoot(root, {
|
|
237
|
+
timeoutMs: timing.quiesceDeadlineMs,
|
|
238
|
+
inspectContainer: (timeoutMs) => inspectContainer(manifest),
|
|
239
|
+
selfOwner: broker
|
|
240
|
+
});
|
|
241
|
+
active = null;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (!brokerOwns())
|
|
246
|
+
break;
|
|
247
|
+
containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_ABSENT_GC_FAILED';
|
|
248
|
+
appendSandboxControlAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
|
|
249
|
+
const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
|
|
250
|
+
nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
|
|
251
|
+
containerBackoffMs = backoff.nextDelayMs;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
164
255
|
if (active?.settled) {
|
|
256
|
+
if (!brokerOwns())
|
|
257
|
+
break;
|
|
165
258
|
if (active.result) {
|
|
259
|
+
if (!brokerOwns())
|
|
260
|
+
break;
|
|
166
261
|
writeResponse(manifest, {
|
|
167
262
|
version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
|
|
168
263
|
stdout: active.result.stdout, stderr: active.result.stderr, error: null
|
|
169
264
|
});
|
|
170
265
|
}
|
|
171
266
|
else {
|
|
267
|
+
if (!brokerOwns())
|
|
268
|
+
break;
|
|
172
269
|
writeResponse(manifest, unknown(active.request.id));
|
|
173
270
|
}
|
|
271
|
+
if (!brokerOwns())
|
|
272
|
+
break;
|
|
174
273
|
fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
|
|
175
274
|
active = null;
|
|
176
275
|
}
|
|
177
276
|
let reasonCode = null;
|
|
178
277
|
try {
|
|
278
|
+
if (!brokerOwns())
|
|
279
|
+
break;
|
|
179
280
|
if (cleanupStaleSandboxControlLease(manifest)) {
|
|
281
|
+
if (!brokerOwns())
|
|
282
|
+
break;
|
|
180
283
|
appendSandboxControlAudit(manifest, 'lease-stale-cleanup');
|
|
181
284
|
}
|
|
182
285
|
if (readActiveLease(manifest))
|
|
@@ -185,18 +288,32 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
185
288
|
catch {
|
|
186
289
|
reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
|
|
187
290
|
}
|
|
188
|
-
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
if (now >= nextBindingCheckAt) {
|
|
293
|
+
bindingReasonCode = bindingCheck(manifest);
|
|
294
|
+
if (bindingReasonCode) {
|
|
295
|
+
const backoff = nextSandboxControlBackoff(bindingBackoffMs, timing.slowCheckMs);
|
|
296
|
+
nextBindingCheckAt = now + backoff.delayMs;
|
|
297
|
+
bindingBackoffMs = backoff.nextDelayMs;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
nextBindingCheckAt = now + timing.slowCheckMs;
|
|
301
|
+
bindingBackoffMs = timing.parkedBindingInitialMs;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
reasonCode ??= containerReasonCode;
|
|
305
|
+
reasonCode ??= bindingReasonCode;
|
|
189
306
|
const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
|
|
190
307
|
const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
|
|
191
|
-
|
|
192
|
-
if (stateKey !== lastState || now - lastStatusAt >= SANDBOX_CONTROL_STATUS_INTERVAL_MS) {
|
|
308
|
+
if (brokerOwns() && (stateKey !== lastState || now - lastStatusAt >= timing.controlTickMs)) {
|
|
193
309
|
writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now);
|
|
194
310
|
lastStatusAt = now;
|
|
195
311
|
}
|
|
196
|
-
if (stateKey !== lastState) {
|
|
312
|
+
if (brokerOwns() && stateKey !== lastState) {
|
|
197
313
|
appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
|
|
198
314
|
lastState = stateKey;
|
|
199
315
|
}
|
|
316
|
+
let retiring = false;
|
|
200
317
|
for (const name of fs.readdirSync(requestsDir).sort()) {
|
|
201
318
|
if (!/^[a-f0-9-]{16,64}\.json$/.test(name))
|
|
202
319
|
continue;
|
|
@@ -204,14 +321,26 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
204
321
|
const source = path.join(requestsDir, name);
|
|
205
322
|
let claimed = null;
|
|
206
323
|
try {
|
|
324
|
+
if (!brokerOwns()) {
|
|
325
|
+
retiring = true;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
207
328
|
claimed = claimRequest(manifest, source, id);
|
|
329
|
+
if (!brokerOwns()) {
|
|
330
|
+
retiring = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
208
333
|
consumeRequest(consumedDir, id);
|
|
334
|
+
if (!brokerOwns()) {
|
|
335
|
+
retiring = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
209
338
|
if (reasonCode)
|
|
210
339
|
throw new Error(reasonCode);
|
|
211
340
|
if (active)
|
|
212
341
|
throw new Error('SANDBOX_CONTROL_BUSY');
|
|
213
342
|
const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
|
|
214
|
-
if (
|
|
343
|
+
if (bindingCheck(manifest))
|
|
215
344
|
throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
|
|
216
345
|
if (readActiveLease(manifest))
|
|
217
346
|
throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
@@ -221,43 +350,90 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
221
350
|
const execution = { request, prepared, result: null, failure: null, settled: false };
|
|
222
351
|
active = execution;
|
|
223
352
|
prepared.completion.then((result) => { execution.result = result; execution.settled = true; }, (error) => { execution.failure = error; execution.settled = true; });
|
|
353
|
+
if (!brokerOwns()) {
|
|
354
|
+
prepared.terminate(false);
|
|
355
|
+
active = null;
|
|
356
|
+
retiring = true;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!brokerOwns()) {
|
|
360
|
+
prepared.terminate(false);
|
|
361
|
+
active = null;
|
|
362
|
+
retiring = true;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
224
365
|
writeResponse(manifest, {
|
|
225
366
|
version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
|
|
226
367
|
});
|
|
368
|
+
if (!brokerOwns()) {
|
|
369
|
+
prepared.terminate(false);
|
|
370
|
+
active = null;
|
|
371
|
+
retiring = true;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
227
374
|
try {
|
|
228
|
-
prepared.start();
|
|
375
|
+
prepared.start(brokerOwns);
|
|
229
376
|
}
|
|
230
377
|
catch (error) {
|
|
231
|
-
|
|
378
|
+
const owned = brokerOwns();
|
|
379
|
+
prepared.terminate(owned);
|
|
380
|
+
if (!owned || !brokerOwns()) {
|
|
381
|
+
active = null;
|
|
382
|
+
retiring = true;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
232
385
|
writeResponse(manifest, unknown(id));
|
|
386
|
+
if (!brokerOwns()) {
|
|
387
|
+
active = null;
|
|
388
|
+
retiring = true;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
233
391
|
fs.rmSync(path.join(manifest.processingDir, id), { recursive: true, force: true });
|
|
234
392
|
active = null;
|
|
235
393
|
claimed = null;
|
|
394
|
+
if (!brokerOwns()) {
|
|
395
|
+
retiring = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
236
398
|
appendSandboxControlAudit(manifest, 'executor-gate-failed', { requestId: id });
|
|
237
399
|
continue;
|
|
238
400
|
}
|
|
239
401
|
}
|
|
240
402
|
catch (error) {
|
|
403
|
+
if (!brokerOwns()) {
|
|
404
|
+
retiring = true;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
241
407
|
writeResponse(manifest, rejected(id, error));
|
|
408
|
+
if (!brokerOwns()) {
|
|
409
|
+
retiring = true;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
242
412
|
if (claimed)
|
|
243
413
|
fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
|
|
244
414
|
}
|
|
245
415
|
}
|
|
246
|
-
|
|
416
|
+
if (retiring || !brokerOwns())
|
|
417
|
+
break;
|
|
418
|
+
await delay(timing.controlTickMs);
|
|
247
419
|
}
|
|
248
420
|
}
|
|
249
421
|
finally {
|
|
250
422
|
if (active) {
|
|
251
|
-
|
|
252
|
-
|
|
423
|
+
const owned = brokerOwns();
|
|
424
|
+
active.prepared.terminate(owned);
|
|
425
|
+
if (owned && brokerOwns())
|
|
426
|
+
writeResponse(manifest, unknown(active.request.id));
|
|
253
427
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
fs.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
428
|
+
if (brokerOwns()) {
|
|
429
|
+
appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
|
|
430
|
+
try {
|
|
431
|
+
if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
|
|
432
|
+
fs.unlinkSync(brokerPath);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// A newer owner or recovery path owns the record.
|
|
436
|
+
}
|
|
261
437
|
}
|
|
262
438
|
}
|
|
263
439
|
}
|
|
@@ -32,15 +32,17 @@ export function statusPath(manifest) {
|
|
|
32
32
|
}
|
|
33
33
|
export function writeSandboxControlStatus(manifest, broker, state, reasonCode, activeRequestId, now = Date.now()) {
|
|
34
34
|
const status = {
|
|
35
|
-
version:
|
|
35
|
+
version: 2, generation: manifest.generation, broker, state, reasonCode, activeRequestId, updatedAt: now
|
|
36
36
|
};
|
|
37
37
|
atomicWriteJson(statusPath(manifest), status, 0o600, false);
|
|
38
38
|
return status;
|
|
39
39
|
}
|
|
40
40
|
export function parseSandboxControlStatus(value) {
|
|
41
41
|
const status = value;
|
|
42
|
-
if (!status || status.version !==
|
|
43
|
-
|| !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== '
|
|
42
|
+
if (!status || status.version !== 2 || typeof status.generation !== 'string'
|
|
43
|
+
|| !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== 'number'
|
|
44
|
+
|| !Number.isSafeInteger(status.broker.startTime) || typeof status.broker.brokerId !== 'string'
|
|
45
|
+
|| status.broker.brokerId.length === 0
|
|
44
46
|
|| !['starting', 'healthy', 'busy', 'parked'].includes(status.state ?? '')
|
|
45
47
|
|| (status.reasonCode !== null && typeof status.reasonCode !== 'string')
|
|
46
48
|
|| (status.activeRequestId !== null && typeof status.activeRequestId !== 'string')
|
|
@@ -56,9 +58,10 @@ export function readActiveLease(manifest, now = Date.now()) {
|
|
|
56
58
|
if (!fs.existsSync(filePath))
|
|
57
59
|
return null;
|
|
58
60
|
const lease = readJsonFile(filePath);
|
|
59
|
-
if (!lease || lease.version !==
|
|
61
|
+
if (!lease || lease.version !== 2
|
|
60
62
|
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
61
|
-
|| typeof lease.owner.startTime !== '
|
|
63
|
+
|| typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
|
|
64
|
+
|| !Number.isSafeInteger(lease.issuedAt)
|
|
62
65
|
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
63
66
|
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
64
67
|
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
@@ -75,9 +78,10 @@ export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
|
|
|
75
78
|
return false;
|
|
76
79
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
77
80
|
const lease = JSON.parse(raw);
|
|
78
|
-
if (!lease || lease.version !==
|
|
81
|
+
if (!lease || lease.version !== 2
|
|
79
82
|
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
80
|
-
|| typeof lease.owner.startTime !== '
|
|
83
|
+
|| typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
|
|
84
|
+
|| !Number.isSafeInteger(lease.issuedAt)
|
|
81
85
|
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
82
86
|
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
83
87
|
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
@@ -85,7 +89,7 @@ export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
|
|
|
85
89
|
}
|
|
86
90
|
const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
|
|
87
91
|
|| lease.branch !== manifest.branch || lease.expiresAt <= now || !processIdentityMatches({
|
|
88
|
-
|
|
92
|
+
pid: lease.owner.pid, startTime: lease.owner.startTime
|
|
89
93
|
});
|
|
90
94
|
if (!stale)
|
|
91
95
|
return false;
|
|
@@ -109,9 +113,10 @@ export function executionPath(manifest, requestId) {
|
|
|
109
113
|
}
|
|
110
114
|
export function readExecution(filePath) {
|
|
111
115
|
const execution = readJsonFile(filePath);
|
|
112
|
-
if (!execution || execution.version !==
|
|
116
|
+
if (!execution || execution.version !== 2 || typeof execution.generation !== 'string'
|
|
113
117
|
|| typeof execution.requestId !== 'string' || typeof execution.nonce !== 'string'
|
|
114
|
-
|| !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== '
|
|
118
|
+
|| !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== 'number'
|
|
119
|
+
|| !Number.isSafeInteger(execution.child.startTime)
|
|
115
120
|
|| !['prepared', 'running', 'terminating'].includes(execution.phase ?? '')
|
|
116
121
|
|| !Number.isSafeInteger(execution.updatedAt))
|
|
117
122
|
throw new Error('SANDBOX_CONTROL_EXECUTION_INVALID');
|
|
@@ -140,7 +145,7 @@ export function terminateSandboxControlExecution(execution, options = {}) {
|
|
|
140
145
|
};
|
|
141
146
|
const treeAlive = () => {
|
|
142
147
|
if (platform === 'win32' || !processGroupId) {
|
|
143
|
-
return processIdentityMatches({
|
|
148
|
+
return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
|
|
144
149
|
}
|
|
145
150
|
return processGroupAlive(processGroupId);
|
|
146
151
|
};
|
|
@@ -156,20 +161,25 @@ export function terminateSandboxControlExecution(execution, options = {}) {
|
|
|
156
161
|
catch {
|
|
157
162
|
// The identity check below distinguishes an already-exited tree from a failed termination.
|
|
158
163
|
}
|
|
159
|
-
const deadline = Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS);
|
|
160
|
-
|
|
164
|
+
const deadline = options.deadlineAt ?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
|
|
165
|
+
const forceAt = options.forceAt ?? (options.deadlineAt === undefined
|
|
166
|
+
? deadline
|
|
167
|
+
: Math.min(deadline, Date.now() + Math.floor(Math.max(0, deadline - Date.now()) / 2)));
|
|
168
|
+
while (Date.now() < forceAt) {
|
|
161
169
|
if (!treeAlive())
|
|
162
170
|
return true;
|
|
163
171
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
164
172
|
}
|
|
165
|
-
if (platform !== 'win32'
|
|
173
|
+
if (platform !== 'win32' && options.allowForce !== false
|
|
174
|
+
&& (options.deadlineAt === undefined || Date.now() < deadline)) {
|
|
166
175
|
try {
|
|
167
176
|
process.kill(processGroupId ? -processGroupId : execution.child.pid, 'SIGKILL');
|
|
168
177
|
}
|
|
169
178
|
catch {
|
|
170
179
|
// The final identity check handles an already-exited tree.
|
|
171
180
|
}
|
|
172
|
-
const killDeadline =
|
|
181
|
+
const killDeadline = options.deadlineAt
|
|
182
|
+
?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
|
|
173
183
|
while (Date.now() < killDeadline) {
|
|
174
184
|
if (!treeAlive())
|
|
175
185
|
return true;
|
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
|
|
3
|
-
import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
|
|
4
3
|
export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
5
4
|
const taskBound = Boolean(overrides.taskSource && overrides.taskId);
|
|
6
|
-
const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(({ state, hostPath }) => taskBound && state === 'active'
|
|
7
|
-
? {
|
|
8
|
-
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
9
|
-
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
10
|
-
readOnly: true
|
|
11
|
-
}
|
|
12
|
-
: {
|
|
13
|
-
hostPaths: [hostPath],
|
|
14
|
-
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
15
|
-
readOnly: true
|
|
16
|
-
});
|
|
17
5
|
const mounts = [
|
|
18
6
|
{
|
|
19
7
|
hostPaths: overrides.worktree
|
|
@@ -22,7 +10,11 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
22
10
|
containerPath: '/workspace',
|
|
23
11
|
readOnly: false
|
|
24
12
|
},
|
|
25
|
-
|
|
13
|
+
{
|
|
14
|
+
hostPaths: [overrides.workspaceViewRoot],
|
|
15
|
+
containerPath: '/workspace/.agents/workspace',
|
|
16
|
+
readOnly: true
|
|
17
|
+
},
|
|
26
18
|
];
|
|
27
19
|
if (taskBound) {
|
|
28
20
|
mounts.push({
|