@fitlab-ai/agent-infra 0.11.1 → 0.11.3
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 +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/platform-comment.js +59 -0
- package/dist/lib/internal/sandbox-control.js +2 -171
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/internal/task-review.js +5 -5
- package/dist/lib/internal/task-validate.js +0 -6
- package/dist/lib/platform/comment-safety.js +7 -1
- package/dist/lib/platform/github-provider.js +13 -3
- package/dist/lib/platform/issue-comments.js +93 -15
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/platform/provider-contract.d.ts +2 -0
- package/dist/lib/platform/provider-validation.js +7 -2
- package/dist/lib/platform/verification-sync.js +19 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -117
- package/dist/lib/sandbox/control/finalization-response.js +8 -2
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +29 -206
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +2 -1
- package/dist/lib/task/artifact-command.js +6 -6
- package/dist/lib/task/codex-orchestration.js +10 -208
- package/dist/lib/task/commit-operation.js +3 -18
- package/dist/lib/task/control-authority.js +35 -44
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +16 -78
- package/dist/lib/task/events.js +89 -237
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +2 -1
- package/dist/lib/task/finalization.js +208 -57
- package/dist/lib/task/lifecycle-execution.js +2 -11
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/local-artifact-finalization.js +25 -242
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -24
- package/dist/lib/task/review-command.js +0 -1
- package/dist/lib/task/review-finalization.js +46 -211
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/platform-comment.ts +38 -0
- package/lib/internal/sandbox-control.ts +1 -185
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/internal/task-review.ts +5 -5
- package/lib/internal/task-validate.ts +0 -5
- package/lib/platform/comment-safety.ts +8 -0
- package/lib/platform/github-provider.ts +11 -2
- package/lib/platform/issue-comments.ts +102 -17
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/platform/provider-contract.ts +2 -0
- package/lib/platform/provider-validation.ts +6 -2
- package/lib/platform/verification-sync.ts +19 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -125
- package/lib/sandbox/control/finalization-response.ts +8 -2
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +30 -220
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +2 -1
- package/lib/task/artifact-command.ts +6 -7
- package/lib/task/codex-orchestration.ts +8 -241
- package/lib/task/commit-operation.ts +3 -23
- package/lib/task/control-authority.ts +39 -55
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +14 -81
- package/lib/task/events.ts +106 -255
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +2 -1
- package/lib/task/finalization.ts +221 -65
- package/lib/task/lifecycle-execution.ts +10 -21
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/local-artifact-finalization.ts +23 -281
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -53
- package/lib/task/review-command.ts +0 -1
- package/lib/task/review-finalization.ts +45 -193
- package/package.json +2 -2
- package/templates/.agents/rules/lifecycle-orchestration.en.md +23 -9
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +23 -9
- package/templates/.agents/rules/local-artifact-repair.en.md +14 -60
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +13 -60
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +10 -9
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +10 -9
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -157
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -18
- package/dist/lib/task/workflow-dispatch.js +0 -161
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -183
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -19
- package/lib/task/workflow-dispatch.ts +0 -171
|
@@ -13,7 +13,7 @@ import { planInLabelUpdate, validateInLabelMapping } from "./in-label-sync.ts";
|
|
|
13
13
|
import { readPrDeliveryFact } from "../task/pr-delivery-fact.ts";
|
|
14
14
|
import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from "./provider-bridge.ts";
|
|
15
15
|
import { taskIssueIdentity } from "./task-identities.ts";
|
|
16
|
-
import { renderTaskCommentResult } from "./issue-comments.ts";
|
|
16
|
+
import { MARKERS, renderTaskCommentResult } from "./issue-comments.ts";
|
|
17
17
|
import {
|
|
18
18
|
CONTROL_MARKER_PATTERN,
|
|
19
19
|
canonicalizeCommentBody,
|
|
@@ -279,7 +279,9 @@ async function fetchRemoteData(context: any, shared: VerificationShared): Promis
|
|
|
279
279
|
}
|
|
280
280
|
return {
|
|
281
281
|
issue,
|
|
282
|
-
comments: facts.value.comments.map((comment: { id: string; body: string }) => ({
|
|
282
|
+
comments: facts.value.comments.map((comment: { id: string; body: string; createdSequence: number | null }) => ({
|
|
283
|
+
id: comment.id, body: comment.body, createdSequence: comment.createdSequence
|
|
284
|
+
})),
|
|
283
285
|
prComments,
|
|
284
286
|
prLabels: changeRequest?.labels || null,
|
|
285
287
|
issueType: issueSnapshot
|
|
@@ -361,6 +363,21 @@ function checkCommentMarker(context: any, remoteData: any, shared: VerificationS
|
|
|
361
363
|
|
|
362
364
|
const comment = findCommentByMarker(remoteData.comments, context.marker);
|
|
363
365
|
if (comment) {
|
|
366
|
+
if (context.marker === MARKERS.summary(context.task.metadata.id)) {
|
|
367
|
+
const managed = remoteData.comments.filter((candidate: any) => {
|
|
368
|
+
const first = String(candidate.body || '').replace(/\r\n/g, '\n').split('\n', 1)[0] || '';
|
|
369
|
+
return first === MARKERS.task(context.task.metadata.id)
|
|
370
|
+
|| first === MARKERS.summary(context.task.metadata.id)
|
|
371
|
+
|| first.startsWith(`<!-- sync-issue:${context.task.metadata.id}:`);
|
|
372
|
+
});
|
|
373
|
+
if (managed.some((candidate: any) => !Number.isSafeInteger(candidate.createdSequence) || candidate.createdSequence < 1)) {
|
|
374
|
+
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment ordering is not provable`, "check_failed");
|
|
375
|
+
}
|
|
376
|
+
const summarySequence = comment.createdSequence;
|
|
377
|
+
if (!managed.every((candidate: any) => summarySequence >= candidate.createdSequence)) {
|
|
378
|
+
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} summary comment is not last among task-managed comments`, "check_failed");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
364
381
|
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} has expected comment marker`);
|
|
365
382
|
}
|
|
366
383
|
|
|
@@ -11,10 +11,8 @@ import {
|
|
|
11
11
|
type SandboxControlRequest,
|
|
12
12
|
type SandboxControlResponse,
|
|
13
13
|
type SandboxTaskCreateRequest,
|
|
14
|
-
type SandboxTaskCommandRequest,
|
|
15
14
|
type SandboxTaskFinalizationRequest,
|
|
16
|
-
type SandboxCodexControllerRequest
|
|
17
|
-
type SandboxTaskWorkflowRequest
|
|
15
|
+
type SandboxCodexControllerRequest
|
|
18
16
|
} from './protocol.ts';
|
|
19
17
|
import type {
|
|
20
18
|
CodexControllerLeaseProofV1,
|
|
@@ -26,8 +24,6 @@ import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts'
|
|
|
26
24
|
import type { TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
27
25
|
import { accessSandboxTaskView, taskViewFromStatus, type TaskViewAccessEffect } from './task-view.ts';
|
|
28
26
|
import { readSandboxControlIdentitySentinel } from './identity-sentinel.ts';
|
|
29
|
-
import type { TaskWorkflowRequest } from './task-workflow.ts';
|
|
30
|
-
import type { LifecycleAuthorityRequestV1 } from '../../task/control-authority.ts';
|
|
31
27
|
import { configuredShortIdLength, resolveShortIdReadOnly } from '../../task/short-id.ts';
|
|
32
28
|
|
|
33
29
|
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
@@ -119,8 +115,6 @@ function preflight(
|
|
|
119
115
|
|
|
120
116
|
function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
|
|
121
117
|
if (request.family === 'task-lifecycle' || request.family === 'task-finalization') return 'progress';
|
|
122
|
-
if (request.family === 'task-orchestration') return request.args[1] === 'status' ? 'diagnostic' : 'progress';
|
|
123
|
-
if (request.family === 'task-workflow') return request.workflow.operation === 'artifact-inspect' || request.workflow.operation === 'decision-next-id' ? 'diagnostic' : 'progress';
|
|
124
118
|
return null;
|
|
125
119
|
}
|
|
126
120
|
|
|
@@ -194,9 +188,7 @@ export function resolveVisibleActiveShortId(ref: string): string | null {
|
|
|
194
188
|
// may be a documented short id. Compare canonical ids so a bound sandbox keeps
|
|
195
189
|
// refusing other tasks without rejecting its own task's short id.
|
|
196
190
|
function canonicalRequestTask(request: SandboxControlRequest): CanonicalRequestTask {
|
|
197
|
-
const ref = request.
|
|
198
|
-
? request.workflow.taskId
|
|
199
|
-
: 'args' in request ? request.args[0] ?? null : null;
|
|
191
|
+
const ref = 'args' in request ? request.args[0] ?? null : null;
|
|
200
192
|
if (!ref) return { kind: 'none' };
|
|
201
193
|
if (CONTROL_TASK_ID_RE.test(ref)) return { kind: 'task', taskId: ref };
|
|
202
194
|
if (!CONTROL_SHORT_ID_RE.test(ref)) return { kind: 'none' };
|
|
@@ -223,7 +215,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
223
215
|
|| (requestTask.kind === 'task' && requestTask.taskId !== identity.taskId))) {
|
|
224
216
|
clientError('SANDBOX_CONTROL_IDENTITY_TOPOLOGY_MISMATCH', 'request task does not match the sandbox identity', false);
|
|
225
217
|
}
|
|
226
|
-
if (identity.mode === 'branch-only' &&
|
|
218
|
+
if (identity.mode === 'branch-only' && request.family === 'task-finalization') {
|
|
227
219
|
clientError('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks', false);
|
|
228
220
|
}
|
|
229
221
|
}
|
|
@@ -384,7 +376,7 @@ export function requestSandboxControl(params: Readonly<{
|
|
|
384
376
|
token?: string; generation?: string; timeoutMs?: number;
|
|
385
377
|
}>): SandboxControlResponse {
|
|
386
378
|
if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
387
|
-
if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization'
|
|
379
|
+
if (params.family === 'task-create' || params.family === 'codex-controller' || params.family === 'task-finalization') {
|
|
388
380
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
|
|
389
381
|
}
|
|
390
382
|
const auth = authority(params);
|
|
@@ -392,41 +384,13 @@ export function requestSandboxControl(params: Readonly<{
|
|
|
392
384
|
const request: SandboxControlRequest = {
|
|
393
385
|
version: 3, id: randomUUID(), ...auth, issuedAt,
|
|
394
386
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
395
|
-
family: params.family as 'task-lifecycle'
|
|
387
|
+
family: params.family as 'task-lifecycle', args: params.args,
|
|
396
388
|
controllerProcess: null,
|
|
397
389
|
controllerProof: null
|
|
398
390
|
};
|
|
399
391
|
return exchangeSandboxControl(request, params);
|
|
400
392
|
}
|
|
401
393
|
|
|
402
|
-
export function requestSandboxTaskControl(params: Readonly<{
|
|
403
|
-
family: 'task-lifecycle' | 'task-orchestration';
|
|
404
|
-
args: string[];
|
|
405
|
-
controllerProof: CodexControllerLeaseProofV1 | null;
|
|
406
|
-
authority?: LifecycleAuthorityRequestV1;
|
|
407
|
-
channelDir?: string;
|
|
408
|
-
statusDir?: string;
|
|
409
|
-
token?: string;
|
|
410
|
-
generation?: string;
|
|
411
|
-
timeoutMs?: number;
|
|
412
|
-
}>): SandboxControlResponse {
|
|
413
|
-
const auth = authority(params);
|
|
414
|
-
const issuedAt = Date.now();
|
|
415
|
-
const request: SandboxTaskCommandRequest = {
|
|
416
|
-
version: 3,
|
|
417
|
-
id: params.authority?.requestId ?? randomUUID(),
|
|
418
|
-
...auth,
|
|
419
|
-
issuedAt,
|
|
420
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
421
|
-
family: params.family,
|
|
422
|
-
args: params.args,
|
|
423
|
-
controllerProcess: null,
|
|
424
|
-
controllerProof: params.authority ? null : params.controllerProof,
|
|
425
|
-
...(params.authority ? { authority: params.authority } : {})
|
|
426
|
-
};
|
|
427
|
-
return exchangeSandboxControl(request, params);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
394
|
export function requestSandboxTaskFinalization(params: Readonly<{
|
|
431
395
|
agent: string;
|
|
432
396
|
channelDir?: string;
|
|
@@ -455,33 +419,6 @@ export function requestSandboxTaskFinalization(params: Readonly<{
|
|
|
455
419
|
return exchangeSandboxControl(request, params);
|
|
456
420
|
}
|
|
457
421
|
|
|
458
|
-
export function requestSandboxTaskWorkflow(params: Readonly<{
|
|
459
|
-
workflow: TaskWorkflowRequest;
|
|
460
|
-
channelDir?: string;
|
|
461
|
-
statusDir?: string;
|
|
462
|
-
token?: string;
|
|
463
|
-
generation?: string;
|
|
464
|
-
timeoutMs?: number;
|
|
465
|
-
authority?: LifecycleAuthorityRequestV1;
|
|
466
|
-
}>): SandboxControlResponse {
|
|
467
|
-
const auth = authority(params);
|
|
468
|
-
const issuedAt = Date.now();
|
|
469
|
-
const request: SandboxTaskWorkflowRequest = {
|
|
470
|
-
version: 3,
|
|
471
|
-
id: params.workflow.id,
|
|
472
|
-
...auth,
|
|
473
|
-
issuedAt,
|
|
474
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
475
|
-
family: 'task-workflow',
|
|
476
|
-
args: [],
|
|
477
|
-
controllerProcess: null,
|
|
478
|
-
controllerProof: null,
|
|
479
|
-
workflow: params.workflow,
|
|
480
|
-
...(params.authority ? { authority: params.authority } : {})
|
|
481
|
-
};
|
|
482
|
-
return exchangeSandboxControl(request, params);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
422
|
type CodexControllerClosed = Readonly<{
|
|
486
423
|
version: 1;
|
|
487
424
|
status: 'closed';
|
|
@@ -31,12 +31,8 @@ import {
|
|
|
31
31
|
createSandboxExecutorExecutionContext,
|
|
32
32
|
dispatchTaskControlOperation,
|
|
33
33
|
parseTaskControlOperation,
|
|
34
|
-
validateLifecycleRecoveryAttestation,
|
|
35
|
-
type LifecycleAuthorityResponseV1,
|
|
36
|
-
type LifecycleRecoveryAttestationV1,
|
|
37
34
|
type TaskControlOperation
|
|
38
35
|
} from '../../task/control-authority.ts';
|
|
39
|
-
import { executeTaskWorkflow } from './workflow-executor.ts';
|
|
40
36
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
|
|
41
37
|
import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
42
38
|
import {
|
|
@@ -45,7 +41,6 @@ import {
|
|
|
45
41
|
openCodexControllerRegistration,
|
|
46
42
|
resolveCodexControllerBinding
|
|
47
43
|
} from './controller-registration.ts';
|
|
48
|
-
import type { OrchestrationDiagnosticLogger } from '../../task/orchestration.ts';
|
|
49
44
|
|
|
50
45
|
export type SandboxControlExecutionResult = {
|
|
51
46
|
exitCode: number;
|
|
@@ -127,7 +122,6 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
127
122
|
request: SandboxControlRequest;
|
|
128
123
|
requestPath: string;
|
|
129
124
|
internalCliPath: string;
|
|
130
|
-
lifecycleAuthority?: LifecycleAuthorityResponseV1;
|
|
131
125
|
}): Promise<PreparedSandboxControlExecution> {
|
|
132
126
|
const nonce = randomUUID();
|
|
133
127
|
const child = spawn(
|
|
@@ -181,17 +175,11 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
181
175
|
if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
182
176
|
atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
183
177
|
if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
184
|
-
const authority = params.lifecycleAuthority?.attestation ?? null;
|
|
185
178
|
child.send({
|
|
186
179
|
version: 2,
|
|
187
180
|
nonce,
|
|
188
181
|
owner: gateOwner,
|
|
189
|
-
requestId: params.request.id
|
|
190
|
-
operationId: authority?.operationId ?? params.request.id,
|
|
191
|
-
phase: authority?.phase ?? (params.request.family === 'task-workflow'
|
|
192
|
-
&& params.request.workflow?.operation === 'artifact-preflight' ? 'artifact.preflight'
|
|
193
|
-
: params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
|
|
194
|
-
authority
|
|
182
|
+
requestId: params.request.id
|
|
195
183
|
} satisfies SandboxControlExecutorGateV2);
|
|
196
184
|
},
|
|
197
185
|
completion,
|
|
@@ -206,7 +194,7 @@ export async function prepareSandboxControlExecution(params: {
|
|
|
206
194
|
};
|
|
207
195
|
}
|
|
208
196
|
|
|
209
|
-
function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner: BrokerOwner;
|
|
197
|
+
function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner: BrokerOwner; requestId: string }>> {
|
|
210
198
|
return new Promise((resolve, reject) => {
|
|
211
199
|
const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
|
|
212
200
|
const onDisconnect = () => {
|
|
@@ -222,12 +210,9 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner
|
|
|
222
210
|
nonce?: unknown;
|
|
223
211
|
owner?: Partial<BrokerOwner> | null;
|
|
224
212
|
requestId?: unknown;
|
|
225
|
-
operationId?: unknown;
|
|
226
|
-
phase?: unknown;
|
|
227
|
-
authority?: unknown;
|
|
228
213
|
} | null;
|
|
229
214
|
const owner = value?.owner;
|
|
230
|
-
if (!value || Object.keys(value).sort().join(',') !== '
|
|
215
|
+
if (!value || Object.keys(value).sort().join(',') !== 'nonce,owner,requestId,version'
|
|
231
216
|
|| value.version !== 2 || value.nonce !== nonce
|
|
232
217
|
|| !owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
|
|
233
218
|
|| typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
|
|
@@ -236,20 +221,13 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner
|
|
|
236
221
|
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
|
|
237
222
|
return;
|
|
238
223
|
}
|
|
239
|
-
if (typeof value.requestId !== 'string'
|
|
240
|
-
|| !['orchestration.prepare', 'artifact.preflight', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
|
|
241
|
-
|| (value.authority !== null && value.authority !== undefined && (() => {
|
|
242
|
-
try { validateLifecycleRecoveryAttestation(value.authority); return false; } catch { return true; }
|
|
243
|
-
})())) {
|
|
224
|
+
if (typeof value.requestId !== 'string') {
|
|
244
225
|
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
|
|
245
226
|
return;
|
|
246
227
|
}
|
|
247
228
|
resolve({
|
|
248
229
|
owner: owner as BrokerOwner,
|
|
249
|
-
|
|
250
|
-
requestId: value.requestId,
|
|
251
|
-
operationId: value.operationId,
|
|
252
|
-
phase: value.phase as SandboxControlExecutorGateV2['phase']
|
|
230
|
+
requestId: value.requestId
|
|
253
231
|
});
|
|
254
232
|
});
|
|
255
233
|
});
|
|
@@ -273,17 +251,6 @@ function controllerFailure(error: unknown): SandboxControlExecutionResult {
|
|
|
273
251
|
return { exitCode: 1, stdout: `${JSON.stringify(payload)}\n`, stderr: '' };
|
|
274
252
|
}
|
|
275
253
|
|
|
276
|
-
function orchestrationFailure(code: string, message: string): SandboxControlExecutionResult {
|
|
277
|
-
return {
|
|
278
|
-
exitCode: 1,
|
|
279
|
-
stdout: `${JSON.stringify({
|
|
280
|
-
status: 'failed', changed: false, taskId: null, run: null, next: null,
|
|
281
|
-
error: { code, message }
|
|
282
|
-
})}\n`,
|
|
283
|
-
stderr: ''
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
254
|
function lifecycleFailure(code: string, message: string): SandboxControlExecutionResult {
|
|
288
255
|
return {
|
|
289
256
|
exitCode: 1,
|
|
@@ -330,7 +297,6 @@ function finalizationResult(result: Awaited<ReturnType<typeof applyTaskFinalizat
|
|
|
330
297
|
type ExecuteRequestOptions = Readonly<{
|
|
331
298
|
buildIdentity?: typeof computeLifecycleBuildIdentity;
|
|
332
299
|
resolveControllerBinding?: typeof resolveCodexControllerBinding;
|
|
333
|
-
lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
|
|
334
300
|
}>;
|
|
335
301
|
|
|
336
302
|
async function executeRequestInner(
|
|
@@ -406,7 +372,6 @@ async function executeRequestInner(
|
|
|
406
372
|
return controllerFailure(error);
|
|
407
373
|
}
|
|
408
374
|
}
|
|
409
|
-
if (request.family === 'task-workflow') return executeTaskWorkflow(manifest, request.workflow, options.lifecycleRecoveryAttestation ?? null);
|
|
410
375
|
if (request.family === 'task-finalization') {
|
|
411
376
|
const operation = parseTaskControlOperation(
|
|
412
377
|
'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
|
|
@@ -420,78 +385,21 @@ async function executeRequestInner(
|
|
|
420
385
|
taskId: manifest.taskId!,
|
|
421
386
|
generation: manifest.generation,
|
|
422
387
|
manifestPath,
|
|
423
|
-
requestId: request.id
|
|
424
|
-
lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
|
|
388
|
+
requestId: request.id
|
|
425
389
|
}),
|
|
426
390
|
operation
|
|
427
391
|
);
|
|
428
392
|
return finalizationResult(await result);
|
|
429
393
|
}
|
|
430
394
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
|
|
431
|
-
if (request.family === 'task-orchestration') {
|
|
432
|
-
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
433
|
-
}
|
|
434
395
|
let operation: TaskControlOperation;
|
|
435
396
|
try {
|
|
436
397
|
operation = parseTaskControlOperation(request.family, boundArgs);
|
|
437
398
|
} catch (error) {
|
|
438
399
|
const message = error instanceof Error ? error.message : String(error);
|
|
439
400
|
const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_CONTROL_OPERATION_INVALID';
|
|
440
|
-
return
|
|
441
|
-
? lifecycleFailure('LIFECYCLE_PAYLOAD_INVALID', message)
|
|
442
|
-
: orchestrationFailure(code, message);
|
|
401
|
+
return lifecycleFailure('LIFECYCLE_PAYLOAD_INVALID', `${code}: ${message}`);
|
|
443
402
|
}
|
|
444
|
-
let controllerBinding: Readonly<{
|
|
445
|
-
instanceDigest: string;
|
|
446
|
-
controlGeneration: string;
|
|
447
|
-
}> | null = null;
|
|
448
|
-
if (request.family === 'task-orchestration') {
|
|
449
|
-
const codexPrepare = operation.family === 'task-orchestration'
|
|
450
|
-
&& operation.intent === 'prepare'
|
|
451
|
-
&& operation.input.client === 'codex';
|
|
452
|
-
if (codexPrepare) {
|
|
453
|
-
if (!request.controllerProof) {
|
|
454
|
-
const authority = options.lifecycleRecoveryAttestation;
|
|
455
|
-
if (!authority || authority.phase !== 'orchestration.prepare') {
|
|
456
|
-
return orchestrationFailure(
|
|
457
|
-
'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
|
|
458
|
-
'Codex prepare requires a current controller lease proof'
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
controllerBinding = {
|
|
462
|
-
instanceDigest: authority.controllerInstanceDigest,
|
|
463
|
-
controlGeneration: authority.controlGeneration
|
|
464
|
-
};
|
|
465
|
-
} else {
|
|
466
|
-
try {
|
|
467
|
-
controllerBinding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
|
|
468
|
-
manifest,
|
|
469
|
-
manifestPath,
|
|
470
|
-
proof: request.controllerProof,
|
|
471
|
-
buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
|
|
472
|
-
});
|
|
473
|
-
} catch (error) {
|
|
474
|
-
const code = error instanceof CodexControllerRegistrationError
|
|
475
|
-
? error.code
|
|
476
|
-
: 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID';
|
|
477
|
-
return orchestrationFailure(code, `${code}: Codex controller proof was rejected`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
} else if (request.controllerProof !== null) {
|
|
481
|
-
return orchestrationFailure(
|
|
482
|
-
'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID',
|
|
483
|
-
'Controller proof is only accepted for canonical Codex prepare'
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
const diagnosticLog: OrchestrationDiagnosticLogger = (event, fields): void => {
|
|
488
|
-
appendExecutorAudit(manifest, event.startsWith('orchestration-') ? event : `orchestration-${event}`, {
|
|
489
|
-
requestId: request.id,
|
|
490
|
-
requestFamily: request.family,
|
|
491
|
-
sandboxTaskId: manifest.taskId,
|
|
492
|
-
...fields
|
|
493
|
-
});
|
|
494
|
-
};
|
|
495
403
|
const context = createSandboxExecutorExecutionContext({
|
|
496
404
|
repoRoot: manifest.repoRoot,
|
|
497
405
|
worktreeRoot: manifest.worktreeRoot,
|
|
@@ -499,10 +407,7 @@ async function executeRequestInner(
|
|
|
499
407
|
taskId: manifest.taskId!,
|
|
500
408
|
generation: manifest.generation,
|
|
501
409
|
manifestPath,
|
|
502
|
-
requestId: request.id
|
|
503
|
-
diagnosticLog,
|
|
504
|
-
...(controllerBinding ? { controllerBinding } : {}),
|
|
505
|
-
lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
|
|
410
|
+
requestId: request.id
|
|
506
411
|
});
|
|
507
412
|
const format = (value: unknown): SandboxControlExecutionResult => {
|
|
508
413
|
const result = value as { status?: string };
|
|
@@ -512,12 +417,8 @@ async function executeRequestInner(
|
|
|
512
417
|
stderr: ''
|
|
513
418
|
};
|
|
514
419
|
};
|
|
515
|
-
if (request.family === 'task-orchestration') {
|
|
516
|
-
if (operation.family !== 'task-orchestration') throw new Error('SANDBOX_CONTROL_ORCHESTRATION_OPERATION_INVALID');
|
|
517
|
-
return format(await dispatchTaskControlOperation(context, operation));
|
|
518
|
-
}
|
|
519
420
|
if (operation.family !== 'task-lifecycle') throw new Error('SANDBOX_CONTROL_LIFECYCLE_OPERATION_INVALID');
|
|
520
|
-
|
|
421
|
+
return format(await dispatchTaskControlOperation(context, operation));
|
|
521
422
|
}
|
|
522
423
|
|
|
523
424
|
export async function executeRequest(
|
|
@@ -579,12 +480,6 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
579
480
|
const raw = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as unknown;
|
|
580
481
|
const request = validateSandboxControlRequest(raw, manifest);
|
|
581
482
|
if (gate.requestId !== request.id) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_REQUEST_MISMATCH');
|
|
582
|
-
if (gate.authority !== null
|
|
583
|
-
&& (gate.authority.requestId !== request.id
|
|
584
|
-
|| gate.authority.operationId !== gate.operationId
|
|
585
|
-
|| gate.authority.phase !== gate.phase)) {
|
|
586
|
-
throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_AUTHORITY_MISMATCH');
|
|
587
|
-
}
|
|
588
483
|
appendExecutorAudit(manifest, 'executor-request-validated', requestAuditFields(manifest, request));
|
|
589
484
|
const requestDirectory = path.resolve(path.dirname(requestPath));
|
|
590
485
|
const processingDirectory = path.resolve(path.join(manifest.processingDir, request.id));
|
|
@@ -602,7 +497,7 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
602
497
|
let result: SandboxControlExecutionResult;
|
|
603
498
|
try {
|
|
604
499
|
assertSandboxControlExecutorAuthority(manifest, gateOwner);
|
|
605
|
-
const operation = request.family === 'task-lifecycle'
|
|
500
|
+
const operation = request.family === 'task-lifecycle'
|
|
606
501
|
? (() => {
|
|
607
502
|
try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
|
|
608
503
|
})()
|
|
@@ -610,10 +505,8 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
610
505
|
const context = createSandboxControlAuditContext(manifest, {
|
|
611
506
|
requestId: request.id,
|
|
612
507
|
family: request.family,
|
|
613
|
-
operation: operation?.family === 'task-
|
|
614
|
-
? operation.intent
|
|
615
|
-
: operation?.family === 'task-lifecycle'
|
|
616
|
-
? operation.request.intent
|
|
508
|
+
operation: operation?.family === 'task-lifecycle'
|
|
509
|
+
? operation.request.intent
|
|
617
510
|
: request.family === 'task-finalization' ? request.operation : null,
|
|
618
511
|
phase: 'started-committed',
|
|
619
512
|
outcome: 'in-progress'
|
|
@@ -622,9 +515,7 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
622
515
|
writeSandboxControlTransition(manifest, { requestId: request.id, phase: 'started-committed' });
|
|
623
516
|
const execution = readJsonExecution(executionPath(manifest, request.id));
|
|
624
517
|
atomicWriteJson(executionPath(manifest, request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
625
|
-
result = await executeRequest(manifest, manifestPath, request
|
|
626
|
-
lifecycleRecoveryAttestation: gate.authority
|
|
627
|
-
});
|
|
518
|
+
result = await executeRequest(manifest, manifestPath, request);
|
|
628
519
|
} catch (error) {
|
|
629
520
|
appendExecutorAudit(manifest, 'executor-authority-or-dispatch-failed', {
|
|
630
521
|
...requestAuditFields(manifest, request),
|
|
@@ -643,9 +534,7 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
643
534
|
}
|
|
644
535
|
: request.family === 'task-create'
|
|
645
536
|
? taskCreateExecutionFailure(request.id, code, detail)
|
|
646
|
-
:
|
|
647
|
-
? lifecycleFailure(code, detail)
|
|
648
|
-
: orchestrationFailure(code, detail);
|
|
537
|
+
: lifecycleFailure(code, detail);
|
|
649
538
|
}
|
|
650
539
|
process.stdout.write(result.stdout);
|
|
651
540
|
process.stderr.write(result.stderr);
|
|
@@ -4,18 +4,24 @@ import type { SandboxControlResponse } from './protocol.ts';
|
|
|
4
4
|
export function finalizationTerminalResponse(taskId: string, requestId: string, receipt: TaskFinalizationReceipt): SandboxControlResponse {
|
|
5
5
|
const pendingSteps = [
|
|
6
6
|
receipt.taskComment === 'pending' ? 'task-comment' : null,
|
|
7
|
-
receipt.verification === 'pending' ? 'verification' : null
|
|
7
|
+
receipt.verification === 'pending' ? 'verification' : null,
|
|
8
|
+
receipt.summary === 'pending' ? 'summary' : null
|
|
8
9
|
].filter((step): step is string => step !== null);
|
|
9
|
-
const completedSteps = [
|
|
10
|
+
const completedSteps = [
|
|
11
|
+
'lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment',
|
|
12
|
+
receipt.verification === 'pending' ? null : 'verification', receipt.summary === 'pending' ? null : 'summary'
|
|
13
|
+
]
|
|
10
14
|
.filter((step): step is string => step !== null);
|
|
11
15
|
const warnings = receipt.warnings
|
|
12
16
|
.filter((warning) => warning.status === 'open')
|
|
13
17
|
.map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning);
|
|
14
18
|
const result = {
|
|
15
19
|
status: 'completed', changed: false, taskId,
|
|
20
|
+
backfill: { status: 'no-op', changed: false, error: null },
|
|
16
21
|
lifecycle: { status: 'no-op', changed: false, error: null },
|
|
17
22
|
taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
18
23
|
verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
24
|
+
summary: receipt.summary === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
19
25
|
completedSteps, pendingSteps,
|
|
20
26
|
result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
|
|
21
27
|
? 'completed_with_warnings' : 'completed',
|
|
@@ -5,12 +5,6 @@ import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
|
5
5
|
import type { CodexControllerLeaseProofV1 } from './controller-registration.ts';
|
|
6
6
|
import type { SandboxAuthorityEvidenceV1 } from '../engines/authority.ts';
|
|
7
7
|
import type { SandboxTaskView } from './task-view.ts';
|
|
8
|
-
import { validateTaskWorkflowRequest, type TaskWorkflowRequest } from './task-workflow.ts';
|
|
9
|
-
import {
|
|
10
|
-
validateLifecycleAuthorityRequest,
|
|
11
|
-
type LifecycleAuthorityRequestV1,
|
|
12
|
-
type LifecycleRecoveryAttestationV1
|
|
13
|
-
} from '../../task/control-authority.ts';
|
|
14
8
|
|
|
15
9
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
16
10
|
export const SANDBOX_CONTROL_MAX_LOGICAL_RECORDS = 1024;
|
|
@@ -21,7 +15,7 @@ export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
|
21
15
|
export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
22
16
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
23
17
|
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
24
|
-
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-
|
|
18
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-finalization', 'task-create', 'codex-controller'] as const;
|
|
25
19
|
export type SandboxControlTimingPolicy = Readonly<{
|
|
26
20
|
controlTickMs: number;
|
|
27
21
|
parkedBindingInitialMs: number;
|
|
@@ -61,9 +55,6 @@ export type SandboxControlExecutorGateV2 = Readonly<{
|
|
|
61
55
|
nonce: string;
|
|
62
56
|
owner: SandboxControlBrokerOwner;
|
|
63
57
|
requestId: string;
|
|
64
|
-
operationId: string;
|
|
65
|
-
phase: 'orchestration.prepare' | 'artifact.preflight' | 'artifact.finalize-local' | 'task-event.completed';
|
|
66
|
-
authority: LifecycleRecoveryAttestationV1 | null;
|
|
67
58
|
}>;
|
|
68
59
|
type RequestBase = Readonly<{
|
|
69
60
|
version: 3; id: string; token: string; generation: string; issuedAt: number; expiresAt: number;
|
|
@@ -71,8 +62,7 @@ type RequestBase = Readonly<{
|
|
|
71
62
|
controllerProof: CodexControllerLeaseProofV1 | null;
|
|
72
63
|
}>;
|
|
73
64
|
export type SandboxTaskCommandRequest = RequestBase & Readonly<{
|
|
74
|
-
family: 'task-lifecycle'
|
|
75
|
-
authority?: LifecycleAuthorityRequestV1;
|
|
65
|
+
family: 'task-lifecycle'; args: string[];
|
|
76
66
|
}>;
|
|
77
67
|
export type SandboxTaskFinalizationRequest = RequestBase & Readonly<{
|
|
78
68
|
family: 'task-finalization'; operation: 'complete'; agent: string; args: [];
|
|
@@ -85,13 +75,7 @@ export type SandboxCodexControllerRequest = RequestBase & Readonly<{
|
|
|
85
75
|
command: 'open' | 'close' | 'verify';
|
|
86
76
|
args: [];
|
|
87
77
|
}>;
|
|
88
|
-
export type
|
|
89
|
-
family: 'task-workflow';
|
|
90
|
-
args: [];
|
|
91
|
-
workflow: TaskWorkflowRequest;
|
|
92
|
-
authority?: LifecycleAuthorityRequestV1;
|
|
93
|
-
}>;
|
|
94
|
-
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskFinalizationRequest | SandboxTaskCreateRequest | SandboxCodexControllerRequest | SandboxTaskWorkflowRequest;
|
|
78
|
+
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskFinalizationRequest | SandboxTaskCreateRequest | SandboxCodexControllerRequest;
|
|
95
79
|
export type SandboxControlError = Readonly<{ code: string; message: string; retryable: boolean }>;
|
|
96
80
|
export type SandboxControlResultEvidence = Readonly<{
|
|
97
81
|
version: 1;
|
|
@@ -131,11 +115,6 @@ export type SandboxControlResponse = Readonly<{
|
|
|
131
115
|
outputState?: 'available' | 'unavailable';
|
|
132
116
|
payload?: SandboxControlPayloadReference | null;
|
|
133
117
|
}>;
|
|
134
|
-
export type SandboxControlRecoveryWarning = Readonly<{
|
|
135
|
-
code: string;
|
|
136
|
-
message: string;
|
|
137
|
-
action: string;
|
|
138
|
-
}>;
|
|
139
118
|
export type SandboxControlStatus = Readonly<{
|
|
140
119
|
version: 3; generation: string; broker: ProcessIdentity & { brokerId: string };
|
|
141
120
|
state: 'starting' | 'healthy' | 'busy' | 'parked'; reasonCode: string | null;
|
|
@@ -313,25 +292,8 @@ export function validateSandboxControlRequest(
|
|
|
313
292
|
}
|
|
314
293
|
return request as SandboxTaskFinalizationRequest;
|
|
315
294
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const baseExpected = expected.filter((key) => key !== 'authority');
|
|
319
|
-
if (![baseExpected, expected].some((keys) => Object.keys(request).sort().join(',') === keys.sort().join(','))
|
|
320
|
-
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
321
|
-
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
322
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-workflow request schema is invalid');
|
|
323
|
-
}
|
|
324
|
-
if (manifest.mode !== 'task-bound' || !manifest.taskId) fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot use task-workflow');
|
|
325
|
-
const workflow = validateTaskWorkflowRequest(request.workflow);
|
|
326
|
-
if (workflow.taskId !== manifest.taskId || workflow.generation !== manifest.generation || workflow.id !== request.id) {
|
|
327
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-workflow binding does not match the sandbox manifest');
|
|
328
|
-
}
|
|
329
|
-
if (request.authority !== undefined) validateAuthoritySelector(request.authority, request, manifest);
|
|
330
|
-
return { ...request, workflow } as SandboxTaskWorkflowRequest;
|
|
331
|
-
}
|
|
332
|
-
const expected = ['args', 'authority', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
333
|
-
const baseExpected = expected.filter((key) => key !== 'authority');
|
|
334
|
-
if (![baseExpected, expected].some((keys) => Object.keys(request).sort().join(',') === keys.sort().join(','))
|
|
295
|
+
const expected = ['args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
296
|
+
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
335
297
|
|| !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
|
|
336
298
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
337
299
|
}
|
|
@@ -341,52 +303,14 @@ export function validateSandboxControlRequest(
|
|
|
341
303
|
"branch-only sandboxes cannot coordinate tasks; return to the host and run 'ai sandbox start --recreate <task-ref-or-correct-branch>'"
|
|
342
304
|
);
|
|
343
305
|
}
|
|
344
|
-
if (request.
|
|
345
|
-
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
|
|
346
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
|
|
347
|
-
}
|
|
348
|
-
if (request.controllerProcess !== null
|
|
349
|
-
|| (request.controllerProof !== null && !validControllerProof(request.controllerProof))) {
|
|
306
|
+
if (request.controllerProcess !== null || request.controllerProof !== null) {
|
|
350
307
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller authority is invalid');
|
|
351
308
|
}
|
|
352
|
-
if (request.family !== 'task-orchestration' && request.controllerProof !== null) {
|
|
353
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller proof is not allowed for this family');
|
|
354
|
-
}
|
|
355
|
-
if (request.authority !== undefined) validateAuthoritySelector(request.authority, request, manifest);
|
|
356
309
|
return request as SandboxTaskCommandRequest;
|
|
357
310
|
}
|
|
358
311
|
|
|
359
|
-
function validateAuthoritySelector(
|
|
360
|
-
value: unknown,
|
|
361
|
-
request: Record<string, unknown>,
|
|
362
|
-
manifest: SandboxControlManifest
|
|
363
|
-
): asserts value is LifecycleAuthorityRequestV1 {
|
|
364
|
-
let authority: LifecycleAuthorityRequestV1;
|
|
365
|
-
try { authority = validateLifecycleAuthorityRequest(value); }
|
|
366
|
-
catch { fail('SANDBOX_CONTROL_AUTHORITY_INVALID', 'lifecycle authority selector is invalid'); }
|
|
367
|
-
if (authority.requestId !== request.id
|
|
368
|
-
|| authority.taskId !== manifest.taskId
|
|
369
|
-
|| authority.expectedControlGeneration !== manifest.generation) {
|
|
370
|
-
fail('SANDBOX_CONTROL_AUTHORITY_INVALID', 'lifecycle authority selector is not bound to this request');
|
|
371
|
-
}
|
|
372
|
-
const args = Array.isArray(request.args) ? request.args : [];
|
|
373
|
-
if (request.family === 'task-orchestration'
|
|
374
|
-
&& (args[1] !== 'prepare' || authority.phase !== 'orchestration.prepare')) {
|
|
375
|
-
fail('SANDBOX_CONTROL_AUTHORITY_INVALID', 'orchestration authority is only valid for prepare');
|
|
376
|
-
}
|
|
377
|
-
const workflow = request.workflow as TaskWorkflowRequest | undefined;
|
|
378
|
-
if (request.family === 'task-workflow') {
|
|
379
|
-
const expectedPhase = workflow?.operation === 'artifact-finalize-local'
|
|
380
|
-
? 'artifact.finalize-local'
|
|
381
|
-
: workflow?.operation === 'event' ? 'task-event.completed' : null;
|
|
382
|
-
if (authority.phase !== expectedPhase) {
|
|
383
|
-
fail('SANDBOX_CONTROL_AUTHORITY_INVALID', 'workflow authority phase does not match the workflow operation');
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
312
|
export function bindSandboxControlTask(request: SandboxControlRequest, taskId: string): string[] {
|
|
389
|
-
if (request.family === 'task-create' || request.family === 'codex-controller' || request.family === 'task-finalization'
|
|
313
|
+
if (request.family === 'task-create' || request.family === 'codex-controller' || request.family === 'task-finalization') {
|
|
390
314
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
|
|
391
315
|
}
|
|
392
316
|
if (request.args.length === 0) fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
|