@fitlab-ai/agent-infra 0.11.9 → 0.11.11
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 +0 -6
- package/dist/lib/agent-clients/adapter.js +20 -0
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
- package/dist/lib/agent-clients/adapters/codex.js +4 -0
- package/dist/lib/git/workflow.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/manual-validation.js +2 -2
- package/dist/lib/internal/platform-pr.js +4 -5
- package/dist/lib/internal/sandbox-control.js +30 -5
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-create.js +15 -1
- package/dist/lib/internal/task-finalization.js +14 -25
- package/dist/lib/internal/task-operation-registry.js +6 -141
- package/dist/lib/internal/task-platform-recovery.js +9 -4
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/context.js +4 -2
- package/dist/lib/platform/github-client.js +20 -17
- package/dist/lib/platform/github-provider.js +42 -22
- package/dist/lib/platform/github-release-notes.js +4 -2
- package/dist/lib/platform/in-label-sync.js +2 -1
- package/dist/lib/platform/issue-comments.js +13 -9
- package/dist/lib/platform/operation-coordinator.js +1 -1
- package/dist/lib/platform/pr-review.js +2 -1
- package/dist/lib/platform/pr-summary.js +45 -8
- package/dist/lib/platform/provider-bridge.js +4 -5
- package/dist/lib/platform/provider-contract.d.ts +3 -6
- package/dist/lib/platform/provider-contract.js +3 -15
- package/dist/lib/platform/provider-validation.js +9 -4
- package/dist/lib/platform/pull-requests.js +83 -16
- package/dist/lib/platform/verification-sync.js +17 -14
- package/dist/lib/sandbox/commands/create.js +18 -2
- package/dist/lib/sandbox/commands/enter.js +47 -33
- package/dist/lib/sandbox/control/client.js +99 -17
- package/dist/lib/sandbox/control/executor.js +12 -14
- package/dist/lib/sandbox/control/host-recovery.js +218 -0
- package/dist/lib/sandbox/control/protocol.js +2 -3
- package/dist/lib/sandbox/control/server.js +42 -2
- package/dist/lib/sandbox/environment.js +150 -0
- package/dist/lib/sandbox/recovery.js +8 -0
- package/dist/lib/sandbox/removal.js +0 -1
- package/dist/lib/sandbox/workspace-identity.js +0 -14
- package/dist/lib/sandbox/workspace-view.js +33 -6
- package/dist/lib/task/artifact-command.js +18 -3
- package/dist/lib/task/artifact-lifecycle.js +121 -63
- package/dist/lib/task/artifact-selection.js +1 -1
- package/dist/lib/task/capabilities.js +59 -22
- package/dist/lib/task/commands/status.js +0 -27
- package/dist/lib/task/commit-identity.js +22 -7
- package/dist/lib/task/commit-intent.js +67 -26
- package/dist/lib/task/commit-operation.js +19 -8
- package/dist/lib/task/completion-facts.js +32 -10
- package/dist/lib/task/control-authority.js +2 -6
- package/dist/lib/task/current-contract.js +1 -5
- package/dist/lib/task/events.js +9 -275
- package/dist/lib/task/execution-inputs.js +14 -0
- package/dist/lib/task/finalization-envelope.js +4 -0
- package/dist/lib/task/finalization.js +43 -86
- package/dist/lib/task/frontmatter.js +25 -2
- package/dist/lib/task/ledger-intents.js +6 -8
- package/dist/lib/task/orchestration.js +11 -10
- package/dist/lib/task/platform-operation-journal.js +29 -10
- package/dist/lib/task/platform-operation-recovery.js +109 -35
- package/dist/lib/task/resolve-ref.js +8 -3
- package/dist/lib/task/review-fingerprint.js +4 -26
- package/dist/lib/task/verification-engine.js +95 -6
- package/dist/lib/task/write.js +0 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +22 -0
- package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
- package/lib/agent-clients/adapters/codex.ts +4 -0
- package/lib/git/workflow.ts +1 -1
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/manual-validation.ts +2 -2
- package/lib/internal/platform-pr.ts +3 -4
- package/lib/internal/sandbox-control.ts +39 -6
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-create.ts +15 -2
- package/lib/internal/task-finalization.ts +16 -26
- package/lib/internal/task-operation-registry.ts +6 -153
- package/lib/internal/task-platform-recovery.ts +9 -4
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/context.ts +8 -4
- package/lib/platform/github-client.ts +24 -20
- package/lib/platform/github-provider.ts +47 -26
- package/lib/platform/github-release-notes.ts +5 -3
- package/lib/platform/in-label-sync.ts +2 -1
- package/lib/platform/issue-comments.ts +14 -9
- package/lib/platform/operation-coordinator.ts +1 -1
- package/lib/platform/pr-review.ts +2 -1
- package/lib/platform/pr-summary.ts +43 -7
- package/lib/platform/provider-bridge.ts +10 -9
- package/lib/platform/provider-contract.ts +6 -21
- package/lib/platform/provider-loader.ts +7 -6
- package/lib/platform/provider-validation.ts +30 -8
- package/lib/platform/pull-requests.ts +78 -15
- package/lib/platform/repository-metadata.ts +3 -2
- package/lib/platform/security-alerts.ts +1 -1
- package/lib/platform/types.ts +2 -0
- package/lib/platform/verification-sync.ts +18 -14
- package/lib/sandbox/commands/create.ts +24 -2
- package/lib/sandbox/commands/enter.ts +42 -39
- package/lib/sandbox/control/client.ts +107 -19
- package/lib/sandbox/control/executor.ts +13 -15
- package/lib/sandbox/control/host-recovery.ts +195 -0
- package/lib/sandbox/control/protocol.ts +4 -5
- package/lib/sandbox/control/server.ts +42 -1
- package/lib/sandbox/environment.ts +162 -0
- package/lib/sandbox/recovery.ts +10 -0
- package/lib/sandbox/removal.ts +2 -10
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/workspace-identity.ts +0 -26
- package/lib/sandbox/workspace-view.ts +33 -8
- package/lib/task/artifact-command.ts +19 -3
- package/lib/task/artifact-lifecycle.ts +102 -53
- package/lib/task/artifact-selection.ts +1 -1
- package/lib/task/capabilities.ts +52 -26
- package/lib/task/commands/status.ts +0 -31
- package/lib/task/commit-identity.ts +24 -7
- package/lib/task/commit-intent.ts +58 -24
- package/lib/task/commit-operation.ts +17 -8
- package/lib/task/completion-facts.ts +31 -9
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/current-contract.ts +1 -6
- package/lib/task/events.ts +9 -248
- package/lib/task/execution-inputs.ts +25 -0
- package/lib/task/finalization-envelope.ts +21 -0
- package/lib/task/finalization.ts +40 -82
- package/lib/task/frontmatter.ts +26 -2
- package/lib/task/ledger-intents.ts +6 -8
- package/lib/task/orchestration.ts +11 -17
- package/lib/task/platform-operation-journal.ts +47 -13
- package/lib/task/platform-operation-recovery.ts +102 -37
- package/lib/task/resolve-ref.ts +8 -3
- package/lib/task/review-fingerprint.ts +4 -26
- package/lib/task/verification-engine.ts +103 -9
- package/lib/task/write.ts +0 -19
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/commit-and-pr.en.md +5 -5
- package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
- package/templates/.agents/rules/decision-qualification.en.md +1 -1
- package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +1 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
- package/templates/.agents/rules/review-handshake.en.md +3 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
- package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +4 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +0 -12
- package/templates/.github/workflows/pr-label.yml +1 -2
- package/dist/lib/internal/task-invalidation.js +0 -63
- package/dist/lib/task/finalization-handoff.js +0 -93
- package/dist/lib/task/intermediate-cleanup.js +0 -327
- package/dist/lib/task/invalidation-command.js +0 -88
- package/dist/lib/task/invalidation.js +0 -242
- package/lib/internal/task-invalidation.ts +0 -41
- package/lib/task/finalization-handoff.ts +0 -105
- package/lib/task/intermediate-cleanup.ts +0 -393
- package/lib/task/invalidation-command.ts +0 -88
- package/lib/task/invalidation.ts +0 -313
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { hasTaskBoundMarker } from '../../internal/task-operation-registry.js';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
4
|
import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
|
|
@@ -8,7 +7,11 @@ import { readSandboxControlPayload, readSandboxControlStatus } from './state.js'
|
|
|
8
7
|
import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
|
|
9
8
|
import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
|
|
10
9
|
import { configuredShortIdLength, resolveShortIdReadOnly } from '../../task/short-id.js';
|
|
10
|
+
import { hasTaskBoundMarker, isSandbox } from '../environment.js';
|
|
11
|
+
import { loadConfig } from '../config.js';
|
|
12
|
+
import { recoverSandboxControlFromHost } from './host-recovery.js';
|
|
11
13
|
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
14
|
+
const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
|
|
12
15
|
export class SandboxControlClientError extends Error {
|
|
13
16
|
detail;
|
|
14
17
|
accepted;
|
|
@@ -79,8 +82,10 @@ function preflight(statusDir, generation, taskViewEffect = null, now = Date.now(
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
function taskViewEffectForRequest(request) {
|
|
82
|
-
if (request.family === 'task-lifecycle'
|
|
85
|
+
if (request.family === 'task-lifecycle')
|
|
83
86
|
return 'progress';
|
|
87
|
+
if (request.family === 'task-finalization')
|
|
88
|
+
return 'recovery';
|
|
84
89
|
return null;
|
|
85
90
|
}
|
|
86
91
|
function parseResponse(raw, id, accepted = false) {
|
|
@@ -95,7 +100,7 @@ function parseResponse(raw, id, accepted = false) {
|
|
|
95
100
|
}
|
|
96
101
|
return response;
|
|
97
102
|
}
|
|
98
|
-
function readPublishedResponse(raw, id, channelDir, accepted
|
|
103
|
+
function readPublishedResponse(raw, id, channelDir, accepted, generation) {
|
|
99
104
|
const response = parseResponse(raw, id, accepted);
|
|
100
105
|
if (response.outputState !== 'available')
|
|
101
106
|
return response;
|
|
@@ -109,6 +114,7 @@ function readPublishedResponse(raw, id, channelDir, accepted = false) {
|
|
|
109
114
|
}
|
|
110
115
|
if (!response.payload || response.payload.version !== payload.version
|
|
111
116
|
|| response.payload.id !== payload.id || response.payload.generation !== payload.generation
|
|
117
|
+
|| payload.generation !== generation
|
|
112
118
|
|| response.payload.stdoutBytes !== payload.stdoutBytes || response.payload.stderrBytes !== payload.stderrBytes
|
|
113
119
|
|| response.payload.stdoutSha256 !== payload.stdoutSha256 || response.payload.stderrSha256 !== payload.stderrSha256) {
|
|
114
120
|
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker payload reference is invalid', false, true, id);
|
|
@@ -213,7 +219,7 @@ function exchangeSandboxControl(request, params) {
|
|
|
213
219
|
try {
|
|
214
220
|
// The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
|
|
215
221
|
accepted = true;
|
|
216
|
-
response = readPublishedResponse(raw, request.id, channelDir, accepted);
|
|
222
|
+
response = readPublishedResponse(raw, request.id, channelDir, accepted, request.generation);
|
|
217
223
|
malformedResponseRaw = null;
|
|
218
224
|
}
|
|
219
225
|
catch (error) {
|
|
@@ -261,7 +267,40 @@ export function recoverSandboxControl(requestId, params = {}) {
|
|
|
261
267
|
if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
|
|
262
268
|
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
|
|
263
269
|
}
|
|
264
|
-
|
|
270
|
+
try {
|
|
271
|
+
if (!isSandbox()) {
|
|
272
|
+
const config = loadConfig();
|
|
273
|
+
const response = recoverSandboxControlFromHost(requestId, {
|
|
274
|
+
managedRoot: path.join(config.controlBase, config.project),
|
|
275
|
+
timeoutMs: params.timeoutMs
|
|
276
|
+
});
|
|
277
|
+
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
278
|
+
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'recovery evidence is unavailable; inspect domain state before retrying', false, true, requestId);
|
|
279
|
+
}
|
|
280
|
+
return response;
|
|
281
|
+
}
|
|
282
|
+
const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
|
|
283
|
+
const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
284
|
+
const identity = readSandboxControlIdentitySentinel(statusDir);
|
|
285
|
+
return recoverSandboxControlFromChannel(requestId, {
|
|
286
|
+
channelDir, generation: identity.generation, timeoutMs: params.timeoutMs
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (error instanceof SandboxControlClientError)
|
|
291
|
+
throw error;
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'SANDBOX_CONTROL_RECOVERY_INVALID';
|
|
294
|
+
clientError(code, 'sandbox control recovery evidence is invalid or unavailable', false, true, requestId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Low-level channel reader for control integration tests and internal callers
|
|
298
|
+
// that already hold a manifest-bound channel path.
|
|
299
|
+
export function recoverSandboxControlFromChannel(requestId, params) {
|
|
300
|
+
if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
|
|
301
|
+
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
|
|
302
|
+
}
|
|
303
|
+
const { channelDir } = params;
|
|
265
304
|
const responsePath = path.join(channelDir, 'responses', `${requestId}.json`);
|
|
266
305
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
267
306
|
let accepted = false;
|
|
@@ -284,7 +323,7 @@ export function recoverSandboxControl(requestId, params = {}) {
|
|
|
284
323
|
try {
|
|
285
324
|
// The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
|
|
286
325
|
accepted = true;
|
|
287
|
-
response = readPublishedResponse(raw, requestId, channelDir, accepted);
|
|
326
|
+
response = readPublishedResponse(raw, requestId, channelDir, accepted, params.generation);
|
|
288
327
|
malformedResponseRaw = null;
|
|
289
328
|
}
|
|
290
329
|
catch (error) {
|
|
@@ -298,15 +337,24 @@ export function recoverSandboxControl(requestId, params = {}) {
|
|
|
298
337
|
else if (now - malformedResponseObservedAt >= SANDBOX_CONTROL_RESPONSE_SETTLE_MS) {
|
|
299
338
|
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker response remained malformed', false, accepted, requestId);
|
|
300
339
|
}
|
|
301
|
-
|
|
340
|
+
const remainingMs = deadline - Date.now();
|
|
341
|
+
if (remainingMs > 0)
|
|
342
|
+
sleep(Math.min(25, Math.ceil(remainingMs)));
|
|
302
343
|
continue;
|
|
303
344
|
}
|
|
304
|
-
if (response.phase === 'accepted')
|
|
345
|
+
if (response.phase === 'accepted') {
|
|
305
346
|
accepted = true;
|
|
306
|
-
|
|
347
|
+
}
|
|
348
|
+
else if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
349
|
+
accepted = true;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
307
352
|
return response;
|
|
353
|
+
}
|
|
308
354
|
}
|
|
309
|
-
|
|
355
|
+
const remainingMs = deadline - Date.now();
|
|
356
|
+
if (remainingMs > 0)
|
|
357
|
+
sleep(Math.min(25, Math.ceil(remainingMs)));
|
|
310
358
|
}
|
|
311
359
|
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'request did not produce a final result; inspect domain state before retrying', false, accepted, requestId);
|
|
312
360
|
}
|
|
@@ -326,7 +374,7 @@ export function requestSandboxControl(params) {
|
|
|
326
374
|
const auth = authority(params);
|
|
327
375
|
const issuedAt = Date.now();
|
|
328
376
|
const request = {
|
|
329
|
-
version:
|
|
377
|
+
version: 4, id: randomUUID(), ...auth, issuedAt,
|
|
330
378
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
331
379
|
family: params.family, args: params.args,
|
|
332
380
|
controllerProcess: null,
|
|
@@ -341,9 +389,8 @@ export function requestSandboxTaskFinalization(params) {
|
|
|
341
389
|
const auth = authority(params);
|
|
342
390
|
const issuedAt = Date.now();
|
|
343
391
|
const id = randomUUID();
|
|
344
|
-
const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
|
|
345
392
|
const request = {
|
|
346
|
-
version:
|
|
393
|
+
version: 4,
|
|
347
394
|
id,
|
|
348
395
|
...auth,
|
|
349
396
|
issuedAt,
|
|
@@ -351,12 +398,47 @@ export function requestSandboxTaskFinalization(params) {
|
|
|
351
398
|
family: 'task-finalization',
|
|
352
399
|
operation: 'complete',
|
|
353
400
|
agent,
|
|
354
|
-
handoffSha256,
|
|
355
401
|
args: [],
|
|
356
402
|
controllerProcess: null,
|
|
357
403
|
controllerProof: null
|
|
358
404
|
};
|
|
359
|
-
|
|
405
|
+
let response;
|
|
406
|
+
try {
|
|
407
|
+
response = exchangeSandboxControl(request, params);
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
if (!(error instanceof SandboxControlClientError)
|
|
411
|
+
|| error.detail.code !== 'SANDBOX_CONTROL_RESULT_UNKNOWN'
|
|
412
|
+
|| !error.accepted || !error.requestId)
|
|
413
|
+
throw error;
|
|
414
|
+
return recoverAcceptedTaskFinalization(error.requestId, params);
|
|
415
|
+
}
|
|
416
|
+
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
417
|
+
return recoverAcceptedTaskFinalization(response.id, params);
|
|
418
|
+
}
|
|
419
|
+
return response;
|
|
420
|
+
}
|
|
421
|
+
export function recoverAcceptedTaskFinalization(requestId, params, recover = recoverSandboxControl) {
|
|
422
|
+
const deadline = Date.now() + (params.recoveryBudgetMs ?? SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS);
|
|
423
|
+
while (Date.now() < deadline) {
|
|
424
|
+
const timeoutMs = Math.min(30_000, deadline - Date.now());
|
|
425
|
+
if (timeoutMs <= 0)
|
|
426
|
+
break;
|
|
427
|
+
try {
|
|
428
|
+
const response = recover(requestId, { timeoutMs });
|
|
429
|
+
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
430
|
+
continue;
|
|
431
|
+
return response;
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
if (error instanceof SandboxControlClientError
|
|
435
|
+
&& error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN'
|
|
436
|
+
&& error.requestId === requestId)
|
|
437
|
+
continue;
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'accepted finalization request remained unknown after the automatic recovery budget; inspect the receipt and resume by request id', false, true, requestId);
|
|
360
442
|
}
|
|
361
443
|
function exactKeys(value, keys) {
|
|
362
444
|
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
@@ -456,7 +538,7 @@ function requestCodexController(params) {
|
|
|
456
538
|
const auth = authority(params);
|
|
457
539
|
const issuedAt = Date.now();
|
|
458
540
|
const request = {
|
|
459
|
-
version:
|
|
541
|
+
version: 4,
|
|
460
542
|
id: randomUUID(),
|
|
461
543
|
...auth,
|
|
462
544
|
issuedAt,
|
|
@@ -503,7 +585,7 @@ export function requestSandboxTaskCreate(params) {
|
|
|
503
585
|
const auth = authority(params);
|
|
504
586
|
const issuedAt = Date.now();
|
|
505
587
|
const request = {
|
|
506
|
-
version:
|
|
588
|
+
version: 4, id: randomUUID(), ...auth, issuedAt,
|
|
507
589
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
508
590
|
family: 'task-create', candidate: params.candidate,
|
|
509
591
|
controllerProcess: null,
|
|
@@ -2,10 +2,10 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
5
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
7
6
|
import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
|
|
8
7
|
import { applyTaskFinalization } from '../../task/finalization.js';
|
|
8
|
+
import { serializeTaskFinalizationEnvelope } from '../../task/finalization-envelope.js';
|
|
9
9
|
import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
|
|
10
10
|
import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.js';
|
|
11
11
|
import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.js';
|
|
@@ -14,10 +14,6 @@ import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, pa
|
|
|
14
14
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
|
|
15
15
|
import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
16
16
|
import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
|
|
17
|
-
import { shareBranchDir } from '../constants.js';
|
|
18
|
-
function handoffDirectory(manifest) {
|
|
19
|
-
return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
|
|
20
|
-
}
|
|
21
17
|
function sameBrokerOwner(left, right) {
|
|
22
18
|
return left.version === right.version
|
|
23
19
|
&& left.pid === right.pid
|
|
@@ -232,7 +228,7 @@ function finalizationResult(result) {
|
|
|
232
228
|
async function executeRequestInner(manifest, manifestPath, request, options = {}) {
|
|
233
229
|
if (request.family === 'task-create') {
|
|
234
230
|
try {
|
|
235
|
-
const result = await createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
231
|
+
const result = await (options.createTask ?? createTask)(request.candidate, { repoRoot: manifest.repoRoot });
|
|
236
232
|
return taskCreateExecutionResult(projectTaskCreateResult(result, {
|
|
237
233
|
requestId: request.id,
|
|
238
234
|
accepted: true,
|
|
@@ -303,7 +299,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
303
299
|
const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
|
|
304
300
|
if (parsed.family !== 'task-finalization')
|
|
305
301
|
throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
306
|
-
const operation =
|
|
302
|
+
const operation = parsed;
|
|
307
303
|
const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
|
|
308
304
|
repoRoot: manifest.repoRoot,
|
|
309
305
|
worktreeRoot: manifest.worktreeRoot,
|
|
@@ -311,8 +307,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
311
307
|
taskId: manifest.taskId,
|
|
312
308
|
generation: manifest.generation,
|
|
313
309
|
manifestPath,
|
|
314
|
-
requestId: request.id
|
|
315
|
-
handoffDirectory: handoffDirectory(manifest)
|
|
310
|
+
requestId: request.id
|
|
316
311
|
}), operation);
|
|
317
312
|
return finalizationResult(await result);
|
|
318
313
|
}
|
|
@@ -333,8 +328,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
333
328
|
taskId: manifest.taskId,
|
|
334
329
|
generation: manifest.generation,
|
|
335
330
|
manifestPath,
|
|
336
|
-
requestId: request.id
|
|
337
|
-
handoffDirectory: handoffDirectory(manifest)
|
|
331
|
+
requestId: request.id
|
|
338
332
|
});
|
|
339
333
|
const format = (value) => {
|
|
340
334
|
const result = value;
|
|
@@ -456,10 +450,14 @@ export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
|
456
450
|
result = request.family === 'task-finalization'
|
|
457
451
|
? {
|
|
458
452
|
exitCode: 1,
|
|
459
|
-
stdout:
|
|
460
|
-
|
|
453
|
+
stdout: serializeTaskFinalizationEnvelope({
|
|
454
|
+
status: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 'unknown' : 'failed',
|
|
455
|
+
changed: code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? null : false,
|
|
456
|
+
accepted: true,
|
|
457
|
+
requestId: request.id,
|
|
458
|
+
result: null,
|
|
461
459
|
error: { code, message: detail, retryable: false }
|
|
462
|
-
})
|
|
460
|
+
}),
|
|
463
461
|
stderr: ''
|
|
464
462
|
}
|
|
465
463
|
: request.family === 'task-create'
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readSandboxControlManifest } from './lifecycle.js';
|
|
5
|
+
import { identityDigest } from './identity-sentinel.js';
|
|
6
|
+
import { readSandboxControlPayload, readSandboxControlResultEvidence } from './state.js';
|
|
7
|
+
import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
|
|
8
|
+
const REQUEST_ID = /^[a-f0-9-]{16,64}$/u;
|
|
9
|
+
function unknown(requestId) {
|
|
10
|
+
return {
|
|
11
|
+
version: 2, id: requestId, phase: 'rejected', exitCode: null, stdout: '', stderr: '',
|
|
12
|
+
error: { code: 'SANDBOX_CONTROL_RESULT_UNKNOWN', message: 'SANDBOX_CONTROL_RESULT_UNKNOWN: recovery evidence is unavailable', retryable: false },
|
|
13
|
+
outputState: 'unavailable', payload: null
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function manifestDigest(manifest) {
|
|
17
|
+
return identityDigest({
|
|
18
|
+
version: 1, mode: manifest.mode, taskId: manifest.taskId,
|
|
19
|
+
generation: manifest.generation, controlRootId: manifest.controlRootId
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function managedManifestPaths(managedRoot) {
|
|
23
|
+
if (!fs.existsSync(managedRoot))
|
|
24
|
+
return [];
|
|
25
|
+
const rootStat = fs.lstatSync(managedRoot);
|
|
26
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
27
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
28
|
+
const result = [];
|
|
29
|
+
for (const container of fs.readdirSync(managedRoot, { withFileTypes: true })) {
|
|
30
|
+
if (!container.isDirectory() || container.isSymbolicLink())
|
|
31
|
+
continue;
|
|
32
|
+
const containerPath = path.join(managedRoot, container.name);
|
|
33
|
+
for (const identity of fs.readdirSync(containerPath, { withFileTypes: true })) {
|
|
34
|
+
if (!identity.isDirectory() || identity.isSymbolicLink() || !/^[a-f0-9]{16}$/u.test(identity.name))
|
|
35
|
+
continue;
|
|
36
|
+
result.push(path.join(containerPath, identity.name, 'manifest.json'));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function assertDirectory(directory) {
|
|
42
|
+
const stat = fs.lstatSync(directory);
|
|
43
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
44
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
45
|
+
}
|
|
46
|
+
function readAudit(manifest) {
|
|
47
|
+
const root = path.dirname(path.resolve(manifest.publicStatusDir));
|
|
48
|
+
const records = [];
|
|
49
|
+
for (const file of [path.join(root, 'audit.ndjson.1'), path.join(root, 'audit.ndjson')]) {
|
|
50
|
+
if (!fs.existsSync(file))
|
|
51
|
+
continue;
|
|
52
|
+
const stat = fs.lstatSync(file);
|
|
53
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
54
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
|
|
55
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
|
|
56
|
+
let value;
|
|
57
|
+
try {
|
|
58
|
+
value = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
|
|
62
|
+
}
|
|
63
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
64
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_INVALID');
|
|
65
|
+
records.push(value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return records;
|
|
69
|
+
}
|
|
70
|
+
function auditProof(manifest, requestId) {
|
|
71
|
+
const digest = manifestDigest(manifest);
|
|
72
|
+
const related = readAudit(manifest).filter((record) => record.requestId === requestId);
|
|
73
|
+
for (const record of related) {
|
|
74
|
+
if (record.generation !== manifest.generation || record.identityDigest !== digest) {
|
|
75
|
+
throw new Error('SANDBOX_CONTROL_AUDIT_IDENTITY_MISMATCH');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!related.some((record) => record.event === 'accepted-authorized'
|
|
79
|
+
&& record.version === 2 && record.phase === 'accepted-authorized'))
|
|
80
|
+
return [];
|
|
81
|
+
return related;
|
|
82
|
+
}
|
|
83
|
+
function publishedEvidence(records, generation) {
|
|
84
|
+
const publications = records.filter((record) => record.event === 'executor-result-published');
|
|
85
|
+
let selected = null;
|
|
86
|
+
let selectedSummary = null;
|
|
87
|
+
for (const record of publications) {
|
|
88
|
+
if (record.version !== 2 || record.requestGeneration !== generation
|
|
89
|
+
|| !Number.isSafeInteger(record.exitCode)
|
|
90
|
+
|| !Number.isSafeInteger(record.outputBytes) || !Number.isSafeInteger(record.errorBytes)
|
|
91
|
+
|| typeof record.outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.outputDigest)
|
|
92
|
+
|| typeof record.errorDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(record.errorDigest)) {
|
|
93
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
94
|
+
}
|
|
95
|
+
const summary = JSON.stringify([
|
|
96
|
+
record.requestFamily ?? null, record.requestGeneration, record.exitCode,
|
|
97
|
+
record.outputBytes, record.errorBytes, record.outputDigest, record.errorDigest
|
|
98
|
+
]);
|
|
99
|
+
if (selectedSummary !== null && summary !== selectedSummary) {
|
|
100
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_CONFLICT');
|
|
101
|
+
}
|
|
102
|
+
selected ??= record;
|
|
103
|
+
selectedSummary ??= summary;
|
|
104
|
+
}
|
|
105
|
+
return selected;
|
|
106
|
+
}
|
|
107
|
+
function verifiedResponse(manifest, requestId, records) {
|
|
108
|
+
assertDirectory(manifest.channelDir);
|
|
109
|
+
const responsesDir = path.join(manifest.channelDir, 'responses');
|
|
110
|
+
assertDirectory(responsesDir);
|
|
111
|
+
const responsePath = path.join(responsesDir, `${requestId}.json`);
|
|
112
|
+
if (!fs.existsSync(responsePath))
|
|
113
|
+
return null;
|
|
114
|
+
const responseStat = fs.lstatSync(responsePath);
|
|
115
|
+
if (!responseStat.isFile() || responseStat.isSymbolicLink())
|
|
116
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
117
|
+
const raw = fs.readFileSync(responsePath, 'utf8');
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
response = JSON.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
124
|
+
}
|
|
125
|
+
if (!response || response.version !== 2 || response.id !== requestId)
|
|
126
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
127
|
+
if (response.phase === 'accepted')
|
|
128
|
+
return null;
|
|
129
|
+
if (!['completed', 'rejected'].includes(response.phase))
|
|
130
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
131
|
+
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
132
|
+
return unknown(requestId);
|
|
133
|
+
const evidencePath = path.join(manifest.processingDir, requestId, 'result.json');
|
|
134
|
+
const published = publishedEvidence(records, manifest.generation);
|
|
135
|
+
if (!published)
|
|
136
|
+
return unknown(requestId);
|
|
137
|
+
let evidence = null;
|
|
138
|
+
if (fs.existsSync(evidencePath)) {
|
|
139
|
+
evidence = readSandboxControlResultEvidence(evidencePath);
|
|
140
|
+
if (evidence.id !== requestId || evidence.generation !== manifest.generation
|
|
141
|
+
|| evidence.exitCode !== published.exitCode || evidence.stdoutBytes !== published.outputBytes
|
|
142
|
+
|| evidence.stderrBytes !== published.errorBytes || evidence.stdoutSha256 !== published.outputDigest
|
|
143
|
+
|| evidence.stderrSha256 !== published.errorDigest)
|
|
144
|
+
throw new Error('SANDBOX_CONTROL_RESULT_EVIDENCE_INVALID');
|
|
145
|
+
}
|
|
146
|
+
if (response.phase !== 'completed' || response.exitCode !== published.exitCode || response.error !== null) {
|
|
147
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
148
|
+
}
|
|
149
|
+
if (response.outputState === undefined) {
|
|
150
|
+
const stdout = response.stdout;
|
|
151
|
+
const stderr = response.stderr;
|
|
152
|
+
if (response.payload !== undefined
|
|
153
|
+
|| Buffer.byteLength(stdout, 'utf8') !== published.outputBytes
|
|
154
|
+
|| Buffer.byteLength(stderr, 'utf8') !== published.errorBytes
|
|
155
|
+
|| createHash('sha256').update(stdout, 'utf8').digest('hex') !== published.outputDigest
|
|
156
|
+
|| createHash('sha256').update(stderr, 'utf8').digest('hex') !== published.errorDigest) {
|
|
157
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
158
|
+
}
|
|
159
|
+
return response;
|
|
160
|
+
}
|
|
161
|
+
if (response.outputState === 'available') {
|
|
162
|
+
const payload = readSandboxControlPayload(path.join(manifest.channelDir, 'responses', `${requestId}.payload.json`));
|
|
163
|
+
if (payload.id !== requestId || payload.generation !== manifest.generation
|
|
164
|
+
|| payload.stdoutBytes !== published.outputBytes || payload.stderrBytes !== published.errorBytes
|
|
165
|
+
|| payload.stdoutSha256 !== published.outputDigest || payload.stderrSha256 !== published.errorDigest
|
|
166
|
+
|| response.payload?.id !== payload.id || response.payload?.generation !== payload.generation
|
|
167
|
+
|| response.payload?.stdoutBytes !== payload.stdoutBytes || response.payload?.stderrBytes !== payload.stderrBytes
|
|
168
|
+
|| response.payload?.stdoutSha256 !== payload.stdoutSha256 || response.payload?.stderrSha256 !== payload.stderrSha256) {
|
|
169
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
170
|
+
}
|
|
171
|
+
return { ...response, stdout: payload.stdout, stderr: payload.stderr };
|
|
172
|
+
}
|
|
173
|
+
const expectedStdout = published.requestFamily === 'task-create'
|
|
174
|
+
? `${JSON.stringify(taskCreateOutputUnavailableResult(requestId))}\n`
|
|
175
|
+
: '';
|
|
176
|
+
if (response.outputState !== 'unavailable' || response.payload !== null
|
|
177
|
+
|| response.stdout !== expectedStdout || response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: broker restarted after executor completion\n'
|
|
178
|
+
&& response.stderr !== 'SANDBOX_CONTROL_OUTPUT_UNAVAILABLE: output payload was not retained\n') {
|
|
179
|
+
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID');
|
|
180
|
+
}
|
|
181
|
+
return response;
|
|
182
|
+
}
|
|
183
|
+
export function recoverSandboxControlFromHost(requestId, params) {
|
|
184
|
+
if (!REQUEST_ID.test(requestId))
|
|
185
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
|
|
186
|
+
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
187
|
+
let candidates = [];
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
candidates = [];
|
|
190
|
+
for (const manifestPath of managedManifestPaths(params.managedRoot)) {
|
|
191
|
+
let manifest;
|
|
192
|
+
try {
|
|
193
|
+
manifest = readSandboxControlManifest(manifestPath);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
197
|
+
}
|
|
198
|
+
if (manifest.project !== path.basename(path.resolve(params.managedRoot)) || !manifest.generation) {
|
|
199
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
200
|
+
}
|
|
201
|
+
const records = auditProof(manifest, requestId);
|
|
202
|
+
if (records.length)
|
|
203
|
+
candidates.push({ manifest, records });
|
|
204
|
+
}
|
|
205
|
+
if (candidates.length > 1)
|
|
206
|
+
throw new Error('SANDBOX_CONTROL_RECOVERY_AMBIGUOUS');
|
|
207
|
+
if (candidates.length === 1) {
|
|
208
|
+
const response = verifiedResponse(candidates[0].manifest, requestId, candidates[0].records);
|
|
209
|
+
if (response)
|
|
210
|
+
return response;
|
|
211
|
+
}
|
|
212
|
+
const remainingMs = deadline - Date.now();
|
|
213
|
+
if (remainingMs > 0)
|
|
214
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(25, remainingMs));
|
|
215
|
+
}
|
|
216
|
+
return unknown(requestId);
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=host-recovery.js.map
|
|
@@ -111,7 +111,7 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
111
111
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
112
112
|
}
|
|
113
113
|
const request = value;
|
|
114
|
-
if (request.version !==
|
|
114
|
+
if (request.version !== 4 || typeof request.id !== 'string'
|
|
115
115
|
|| !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
|
|
116
116
|
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
|
|
117
117
|
|| !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
|
|
@@ -158,12 +158,11 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
158
158
|
return request;
|
|
159
159
|
}
|
|
160
160
|
if (request.family === 'task-finalization') {
|
|
161
|
-
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', '
|
|
161
|
+
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
162
162
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
163
163
|
|| request.operation !== 'complete'
|
|
164
164
|
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
165
165
|
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
|
|
166
|
-
|| typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
|
|
167
166
|
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
168
167
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
169
168
|
}
|
|
@@ -20,8 +20,35 @@ import { nextSandboxControlBackoff } from './timing.js';
|
|
|
20
20
|
import { readTaskFinalizationReceipt } from '../../task/finalization.js';
|
|
21
21
|
import { readCodexControllerRegistration } from './controller-registration.js';
|
|
22
22
|
import { validateSandboxControlIdentity } from './identity-sentinel.js';
|
|
23
|
-
import { mergeSandboxTaskView, taskViewForManifest } from './task-view.js';
|
|
23
|
+
import { mergeSandboxTaskView, taskViewAfterFinalization, taskViewForManifest } from './task-view.js';
|
|
24
24
|
import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
|
|
25
|
+
async function taskViewForFinalizationResult(manifest, request, result) {
|
|
26
|
+
if (request.family !== 'task-finalization' || result?.exitCode !== 0 || !manifest.taskId)
|
|
27
|
+
return null;
|
|
28
|
+
let receipt = null;
|
|
29
|
+
for (let attempt = 0; attempt < 100 && !receipt; attempt += 1) {
|
|
30
|
+
try {
|
|
31
|
+
receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A missing or temporarily moving task directory is retried before publishing completion.
|
|
35
|
+
}
|
|
36
|
+
if (!receipt)
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
38
|
+
}
|
|
39
|
+
const finalized = taskViewAfterFinalization({
|
|
40
|
+
taskId: manifest.taskId,
|
|
41
|
+
generation: manifest.generation,
|
|
42
|
+
requestId: request.id,
|
|
43
|
+
receipt
|
|
44
|
+
});
|
|
45
|
+
if (finalized.state === 'unknown')
|
|
46
|
+
return finalized;
|
|
47
|
+
const canonical = taskViewForManifest({ ...manifest, receipt });
|
|
48
|
+
return canonical.state === 'current' && canonical.observedSource === 'completed'
|
|
49
|
+
? canonical
|
|
50
|
+
: finalized;
|
|
51
|
+
}
|
|
25
52
|
function appendBrokerAudit(manifest, event, fields = {}) {
|
|
26
53
|
appendDiagnosticAudit(manifest, event, { source: 'broker', ...fields });
|
|
27
54
|
}
|
|
@@ -722,6 +749,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
722
749
|
// A fresh control root has no previous task-view projection.
|
|
723
750
|
}
|
|
724
751
|
let lastState = '';
|
|
752
|
+
let lastTaskView = '';
|
|
725
753
|
let lastStatusAt = 0;
|
|
726
754
|
let nextBindingCheckAt = 0;
|
|
727
755
|
let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
|
|
@@ -838,6 +866,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
838
866
|
settledExecution = active;
|
|
839
867
|
active = null;
|
|
840
868
|
}
|
|
869
|
+
const finalizationView = settledExecution
|
|
870
|
+
? await taskViewForFinalizationResult(manifest, settledExecution.request, settledExecution.result)
|
|
871
|
+
: null;
|
|
872
|
+
if (finalizationView)
|
|
873
|
+
taskView = finalizationView;
|
|
841
874
|
let reasonCode = null;
|
|
842
875
|
try {
|
|
843
876
|
if (!brokerOwns())
|
|
@@ -881,9 +914,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
881
914
|
}
|
|
882
915
|
const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
|
|
883
916
|
const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
|
|
884
|
-
|
|
917
|
+
const taskViewKey = JSON.stringify(taskView);
|
|
918
|
+
if (brokerOwns() && (stateKey !== lastState || taskViewKey !== lastTaskView || now - lastStatusAt >= timing.controlTickMs)) {
|
|
885
919
|
writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now, taskView);
|
|
886
920
|
lastStatusAt = now;
|
|
921
|
+
lastTaskView = taskViewKey;
|
|
887
922
|
}
|
|
888
923
|
if (brokerOwns() && stateKey !== lastState) {
|
|
889
924
|
appendBrokerAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
|
|
@@ -1156,6 +1191,11 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
1156
1191
|
}
|
|
1157
1192
|
}
|
|
1158
1193
|
if (owned && active.result && active.resultEvidenceWritten) {
|
|
1194
|
+
const finalizationView = await taskViewForFinalizationResult(manifest, active.request, active.result);
|
|
1195
|
+
if (finalizationView) {
|
|
1196
|
+
taskView = finalizationView;
|
|
1197
|
+
writeSandboxControlStatus(manifest, broker, 'healthy', null, null, Date.now(), taskView);
|
|
1198
|
+
}
|
|
1159
1199
|
if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
|
|
1160
1200
|
if (brokerOwns()) {
|
|
1161
1201
|
removeAcceptedResponse(manifest, active.request.id);
|