@fitlab-ai/agent-infra 0.11.2 → 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/manual-validation.js +22 -27
- package/dist/lib/internal/sandbox-control.js +1 -43
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +6 -2
- package/dist/lib/platform/issue-comments.js +2 -31
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -79
- package/dist/lib/sandbox/control/finalization-response.js +3 -5
- 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 +1 -2
- package/dist/lib/task/control-authority.js +34 -36
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +1 -1
- package/dist/lib/task/finalization.js +53 -126
- package/dist/lib/task/lifecycle.js +1 -1
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +12 -6
- 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/manual-validation.ts +22 -26
- package/lib/internal/sandbox-control.ts +0 -42
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +7 -4
- package/lib/platform/issue-comments.ts +1 -35
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -81
- package/lib/sandbox/control/finalization-response.ts +3 -5
- 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 +1 -2
- package/lib/task/control-authority.ts +38 -47
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +1 -1
- package/lib/task/finalization.ts +57 -121
- package/lib/task/lifecycle.ts +2 -2
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +30 -19
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +19 -5
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
- 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 +6 -7
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +6 -7
- 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 +9 -8
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -142
- 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 -21
- package/dist/lib/task/workflow-dispatch.js +0 -213
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -156
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -22
- package/lib/task/workflow-dispatch.ts +0 -214
|
@@ -22,7 +22,7 @@ export type TaskOperationEffect = TaskViewAccessEffect;
|
|
|
22
22
|
export type TaskRefSource = 'argv' | 'environment' | 'none' | 'delegated' | 'input';
|
|
23
23
|
|
|
24
24
|
export type SandboxControlTransportDecision = Readonly<{
|
|
25
|
-
kind: 'direct-host' | 'broker-client' | 'fail-closed';
|
|
25
|
+
kind: 'direct-host' | 'sandbox-local' | 'broker-client' | 'fail-closed';
|
|
26
26
|
reasonCode: string | null;
|
|
27
27
|
}>;
|
|
28
28
|
|
|
@@ -277,6 +277,7 @@ export function resolveTaskOperation(
|
|
|
277
277
|
export function resolveDelegatedTaskOperation(args: readonly string[]): TaskOperationDescriptor | null {
|
|
278
278
|
const [operation, ...rest] = args;
|
|
279
279
|
if (operation !== 'client' || !rest[0]) return null;
|
|
280
|
+
if (rest[0] === 'task-orchestration') return null;
|
|
280
281
|
return resolveTaskOperation('internal', rest[0], rest.slice(1));
|
|
281
282
|
}
|
|
282
283
|
|
|
@@ -340,7 +341,7 @@ function taskMarkerState(env: NodeJS.ProcessEnv): TaskMarkerState {
|
|
|
340
341
|
|
|
341
342
|
export function resolveSandboxControlTransport(
|
|
342
343
|
env: NodeJS.ProcessEnv = process.env,
|
|
343
|
-
options: Readonly<{ statusMountPath?: string }> = {}
|
|
344
|
+
options: Readonly<{ statusMountPath?: string; localWorkflow?: boolean }> = {}
|
|
344
345
|
): SandboxControlTransportDecision {
|
|
345
346
|
const hasAnyMarker = TASK_MARKER_KEYS.some((key) => Boolean(env[key]))
|
|
346
347
|
|| Boolean(env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING)
|
|
@@ -407,13 +408,15 @@ export function resolveSandboxControlTransport(
|
|
|
407
408
|
if (!hasCompleteConfig) {
|
|
408
409
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
409
410
|
}
|
|
410
|
-
return { kind: 'broker-client', reasonCode: null };
|
|
411
|
+
return { kind: options.localWorkflow ? 'sandbox-local' : 'broker-client', reasonCode: null };
|
|
411
412
|
}
|
|
412
413
|
if (!hasAnyMarker) return { kind: 'direct-host', reasonCode: null };
|
|
413
414
|
if (!hasCompleteConfig || (taskBound && !runtime) || (!taskBound && runtime)) {
|
|
414
415
|
return { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_CONFIGURATION_INCOMPLETE' };
|
|
415
416
|
}
|
|
416
|
-
return
|
|
417
|
+
return options.localWorkflow
|
|
418
|
+
? { kind: 'fail-closed', reasonCode: 'SANDBOX_CONTROL_IDENTITY_MISSING' }
|
|
419
|
+
: { kind: 'broker-client', reasonCode: null };
|
|
417
420
|
}
|
|
418
421
|
|
|
419
422
|
export function hasTaskBoundMarker(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
import { projectTaskComment } from './task-comment-projection.ts';
|
|
28
28
|
import {
|
|
29
29
|
canonicalizeCommentBody,
|
|
30
|
-
canonicalizeSummaryBody,
|
|
31
30
|
escapeHtmlText,
|
|
32
31
|
fenceRanges,
|
|
33
32
|
renderSafeCodeFence
|
|
@@ -52,7 +51,6 @@ type SyncOptions = {
|
|
|
52
51
|
client?: PlatformClient;
|
|
53
52
|
runtimeVersion?: string;
|
|
54
53
|
summaryAuthorization?: { sha256: string };
|
|
55
|
-
verifyOnly?: boolean;
|
|
56
54
|
};
|
|
57
55
|
|
|
58
56
|
function providerCommentId(id: string, provider: { identity?: { comment?: string } }): number | string {
|
|
@@ -76,8 +74,6 @@ const ARTIFACT_TITLES: Record<string, string> = {
|
|
|
76
74
|
'review-plan': '技术方案审查',
|
|
77
75
|
code: '实现报告',
|
|
78
76
|
'review-code': '代码审查',
|
|
79
|
-
'manual-validation': '人工验证报告',
|
|
80
|
-
'validation-run': '验证运行证据',
|
|
81
77
|
'pr-review': 'PR 审查报告'
|
|
82
78
|
};
|
|
83
79
|
|
|
@@ -154,7 +150,7 @@ function taskCommentLanguage(repoRoot: string): string {
|
|
|
154
150
|
|
|
155
151
|
function artifactIdentity(artifact: string): { stem: string; title: string } {
|
|
156
152
|
const stem = path.basename(artifact, '.md');
|
|
157
|
-
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|
|
|
153
|
+
const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|pr-review)(?:-r(\d+))?$/);
|
|
158
154
|
if (!match) throw new Error(`unsupported artifact '${artifact}'`);
|
|
159
155
|
const base = ARTIFACT_TITLES[match[1]!]!;
|
|
160
156
|
const round = match[2] ? Number(match[2]) : 1;
|
|
@@ -483,15 +479,6 @@ function summaryPosition(comments: RemoteComment[], taskId: string): { ok: boole
|
|
|
483
479
|
return { ok: managed.every((comment) => sequence >= comment.createdSequence!), known: true, summary: summary[0]! };
|
|
484
480
|
}
|
|
485
481
|
|
|
486
|
-
function summaryDigest(comment: RemoteComment): string {
|
|
487
|
-
const body = normalizeCommentContent(comment.body)
|
|
488
|
-
.replace(/^<!-- sync-issue:[^\n]+:summary -->\n## [^\n]+\n\n> [^\n]+\n\n/u, '')
|
|
489
|
-
.replace(/^<details><summary>恢复元数据<\/summary>[\s\S]*?<\/details>\n\n/u, '')
|
|
490
|
-
.replace(/\n---\n\*[^\n]*\*$/u, '');
|
|
491
|
-
const canonical = canonicalizeSummaryBody(body);
|
|
492
|
-
return canonical.ok ? createHash('sha256').update(canonical.value).digest('hex') : '';
|
|
493
|
-
}
|
|
494
|
-
|
|
495
482
|
async function listedComments(provider: any, loaded: any, parent: ReturnType<typeof taskIssueIdentity>): Promise<any> {
|
|
496
483
|
return provider.comments?.list
|
|
497
484
|
? provider.comments.list({ context: providerOperationContext(loaded), parent }).then((response: any) => response.ok
|
|
@@ -609,27 +596,6 @@ async function syncPlatformComment(taskRef: string, options: SyncOptions): Promi
|
|
|
609
596
|
}
|
|
610
597
|
}
|
|
611
598
|
|
|
612
|
-
if (options.verifyOnly) {
|
|
613
|
-
if (options.kind !== 'summary' || !options.summaryAuthorization) {
|
|
614
|
-
return platformResult('failed', {
|
|
615
|
-
...contextFields(context), resource: { kind: 'issue', number: issue },
|
|
616
|
-
error: { code: 'SUMMARY_VERIFICATION_INVALID', message: 'summary verification requires durable authorization', retryable: false }
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
const position = summaryPosition(listed.value, resolved.taskId);
|
|
620
|
-
if (!position.ok || !position.summary || summaryDigest(position.summary) !== options.summaryAuthorization.sha256) {
|
|
621
|
-
return platformResult('failed', {
|
|
622
|
-
...contextFields(context), resource: { kind: 'issue', number: issue },
|
|
623
|
-
error: { code: 'SUMMARY_VERIFICATION_FAILED', message: 'summary marker, body digest, or managed comment order is invalid', retryable: true }
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
return platformResult('no-op', {
|
|
627
|
-
...contextFields(context), changed: false, resource: { kind: 'issue', number: issue },
|
|
628
|
-
comment: { kind: options.kind, marker: desired[0]!.marker, ids: [position.summary.id], parts: 1 }, error: null
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
|
|
633
599
|
// Backfill only supplies missing artifact comments; valid existing marker sets stay untouched.
|
|
634
600
|
if (options.kind === 'artifact' && options.backfill && existing.length > 0) {
|
|
635
601
|
const operations = existing.map((comment): PlatformOperation => ({
|
|
@@ -35,7 +35,7 @@ import type {
|
|
|
35
35
|
MechanicalChangeReport,
|
|
36
36
|
PrChangeReport
|
|
37
37
|
} from './pr-change-report.ts';
|
|
38
|
-
import { manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.ts';
|
|
38
|
+
import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.ts';
|
|
39
39
|
import { readManualValidationCompletion } from '../task/manual-validation-completion.ts';
|
|
40
40
|
|
|
41
41
|
type SummaryComment = { id: number | string; body: string };
|
|
@@ -582,6 +582,7 @@ async function syncPullRequestSummary(
|
|
|
582
582
|
const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
583
583
|
if (!initial.ok) return fail(initial.status, context, initial.error);
|
|
584
584
|
const manual = options.manualValidation;
|
|
585
|
+
let manualEvidenceDigest: string | null = null;
|
|
585
586
|
const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
|
|
586
587
|
if (hasFinalManualValidation && (!manual || manual.phase !== 'final')) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
|
|
587
588
|
if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
|
|
@@ -599,6 +600,7 @@ async function syncPullRequestSummary(
|
|
|
599
600
|
});
|
|
600
601
|
if (!completion.ok) return fail('failed', context, platformError(completion.error), prNumber);
|
|
601
602
|
const { receipt, transaction } = completion.value;
|
|
603
|
+
manualEvidenceDigest = receipt.evidenceDigest;
|
|
602
604
|
if (transaction.phase !== 'final-promotion-in-progress' || !transaction.eventAppended) return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final summary requires a receipt-backed promotion intent', retryable: false }, prNumber);
|
|
603
605
|
if (!manualValidationFinalSummaryProjectionMatches(options.body, receipt)) return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PROJECTION_INVALID', message: 'final summary body does not match the canonical receipt projection', retryable: false }, prNumber);
|
|
604
606
|
}
|
|
@@ -635,7 +637,13 @@ async function syncPullRequestSummary(
|
|
|
635
637
|
}
|
|
636
638
|
const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${placeholder}${suffix.value}`, report.value);
|
|
637
639
|
if (!replaced.ok) return fail('failed', context, platformError(replaced.error));
|
|
638
|
-
const
|
|
640
|
+
const renderedBody = manual?.phase === 'final'
|
|
641
|
+
? replaced.value.replace(
|
|
642
|
+
MANUAL_VALIDATION_RECEIPT_PLACEHOLDER,
|
|
643
|
+
`<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`
|
|
644
|
+
)
|
|
645
|
+
: replaced.value;
|
|
646
|
+
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
|
|
639
647
|
if (!isSafeSummaryEnvelope(desired, resolved.taskId)) return fail('failed', context, {
|
|
640
648
|
code: 'PR_SUMMARY_RENDER_INVALID',
|
|
641
649
|
message: 'Summary contains an invalid or duplicated reserved control marker',
|
|
@@ -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,35 +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
|
-
const diagnosticLog: OrchestrationDiagnosticLogger = (event, fields): void => {
|
|
445
|
-
appendExecutorAudit(manifest, event.startsWith('orchestration-') ? event : `orchestration-${event}`, {
|
|
446
|
-
requestId: request.id,
|
|
447
|
-
requestFamily: request.family,
|
|
448
|
-
sandboxTaskId: manifest.taskId,
|
|
449
|
-
...fields
|
|
450
|
-
});
|
|
451
|
-
};
|
|
452
403
|
const context = createSandboxExecutorExecutionContext({
|
|
453
404
|
repoRoot: manifest.repoRoot,
|
|
454
405
|
worktreeRoot: manifest.worktreeRoot,
|
|
@@ -456,9 +407,7 @@ async function executeRequestInner(
|
|
|
456
407
|
taskId: manifest.taskId!,
|
|
457
408
|
generation: manifest.generation,
|
|
458
409
|
manifestPath,
|
|
459
|
-
requestId: request.id
|
|
460
|
-
diagnosticLog,
|
|
461
|
-
lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
|
|
410
|
+
requestId: request.id
|
|
462
411
|
});
|
|
463
412
|
const format = (value: unknown): SandboxControlExecutionResult => {
|
|
464
413
|
const result = value as { status?: string };
|
|
@@ -468,12 +417,8 @@ async function executeRequestInner(
|
|
|
468
417
|
stderr: ''
|
|
469
418
|
};
|
|
470
419
|
};
|
|
471
|
-
if (request.family === 'task-orchestration') {
|
|
472
|
-
if (operation.family !== 'task-orchestration') throw new Error('SANDBOX_CONTROL_ORCHESTRATION_OPERATION_INVALID');
|
|
473
|
-
return format(await dispatchTaskControlOperation(context, operation));
|
|
474
|
-
}
|
|
475
420
|
if (operation.family !== 'task-lifecycle') throw new Error('SANDBOX_CONTROL_LIFECYCLE_OPERATION_INVALID');
|
|
476
|
-
|
|
421
|
+
return format(await dispatchTaskControlOperation(context, operation));
|
|
477
422
|
}
|
|
478
423
|
|
|
479
424
|
export async function executeRequest(
|
|
@@ -535,12 +480,6 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
535
480
|
const raw = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as unknown;
|
|
536
481
|
const request = validateSandboxControlRequest(raw, manifest);
|
|
537
482
|
if (gate.requestId !== request.id) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_REQUEST_MISMATCH');
|
|
538
|
-
if (gate.authority !== null
|
|
539
|
-
&& (gate.authority.requestId !== request.id
|
|
540
|
-
|| gate.authority.operationId !== gate.operationId
|
|
541
|
-
|| gate.authority.phase !== gate.phase)) {
|
|
542
|
-
throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_AUTHORITY_MISMATCH');
|
|
543
|
-
}
|
|
544
483
|
appendExecutorAudit(manifest, 'executor-request-validated', requestAuditFields(manifest, request));
|
|
545
484
|
const requestDirectory = path.resolve(path.dirname(requestPath));
|
|
546
485
|
const processingDirectory = path.resolve(path.join(manifest.processingDir, request.id));
|
|
@@ -558,7 +497,7 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
558
497
|
let result: SandboxControlExecutionResult;
|
|
559
498
|
try {
|
|
560
499
|
assertSandboxControlExecutorAuthority(manifest, gateOwner);
|
|
561
|
-
const operation = request.family === 'task-lifecycle'
|
|
500
|
+
const operation = request.family === 'task-lifecycle'
|
|
562
501
|
? (() => {
|
|
563
502
|
try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
|
|
564
503
|
})()
|
|
@@ -566,10 +505,8 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
566
505
|
const context = createSandboxControlAuditContext(manifest, {
|
|
567
506
|
requestId: request.id,
|
|
568
507
|
family: request.family,
|
|
569
|
-
operation: operation?.family === 'task-
|
|
570
|
-
? operation.intent
|
|
571
|
-
: operation?.family === 'task-lifecycle'
|
|
572
|
-
? operation.request.intent
|
|
508
|
+
operation: operation?.family === 'task-lifecycle'
|
|
509
|
+
? operation.request.intent
|
|
573
510
|
: request.family === 'task-finalization' ? request.operation : null,
|
|
574
511
|
phase: 'started-committed',
|
|
575
512
|
outcome: 'in-progress'
|
|
@@ -578,9 +515,7 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
578
515
|
writeSandboxControlTransition(manifest, { requestId: request.id, phase: 'started-committed' });
|
|
579
516
|
const execution = readJsonExecution(executionPath(manifest, request.id));
|
|
580
517
|
atomicWriteJson(executionPath(manifest, request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
581
|
-
result = await executeRequest(manifest, manifestPath, request
|
|
582
|
-
lifecycleRecoveryAttestation: gate.authority
|
|
583
|
-
});
|
|
518
|
+
result = await executeRequest(manifest, manifestPath, request);
|
|
584
519
|
} catch (error) {
|
|
585
520
|
appendExecutorAudit(manifest, 'executor-authority-or-dispatch-failed', {
|
|
586
521
|
...requestAuditFields(manifest, request),
|
|
@@ -599,9 +534,7 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
599
534
|
}
|
|
600
535
|
: request.family === 'task-create'
|
|
601
536
|
? taskCreateExecutionFailure(request.id, code, detail)
|
|
602
|
-
:
|
|
603
|
-
? lifecycleFailure(code, detail)
|
|
604
|
-
: orchestrationFailure(code, detail);
|
|
537
|
+
: lifecycleFailure(code, detail);
|
|
605
538
|
}
|
|
606
539
|
process.stdout.write(result.stdout);
|
|
607
540
|
process.stderr.write(result.stderr);
|
|
@@ -5,13 +5,11 @@ export function finalizationTerminalResponse(taskId: string, requestId: string,
|
|
|
5
5
|
const pendingSteps = [
|
|
6
6
|
receipt.taskComment === 'pending' ? 'task-comment' : null,
|
|
7
7
|
receipt.verification === 'pending' ? 'verification' : null,
|
|
8
|
-
receipt.summary === 'pending' ? 'summary' : null
|
|
9
|
-
receipt.postSummaryVerification === 'pending' ? 'post-summary-verification' : null
|
|
8
|
+
receipt.summary === 'pending' ? 'summary' : null
|
|
10
9
|
].filter((step): step is string => step !== null);
|
|
11
10
|
const completedSteps = [
|
|
12
11
|
'lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment',
|
|
13
|
-
receipt.verification === 'pending' ? null : 'verification', receipt.summary === 'pending' ? null : 'summary'
|
|
14
|
-
receipt.postSummaryVerification === 'pending' ? null : 'post-summary-verification'
|
|
12
|
+
receipt.verification === 'pending' ? null : 'verification', receipt.summary === 'pending' ? null : 'summary'
|
|
15
13
|
]
|
|
16
14
|
.filter((step): step is string => step !== null);
|
|
17
15
|
const warnings = receipt.warnings
|
|
@@ -19,11 +17,11 @@ export function finalizationTerminalResponse(taskId: string, requestId: string,
|
|
|
19
17
|
.map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning);
|
|
20
18
|
const result = {
|
|
21
19
|
status: 'completed', changed: false, taskId,
|
|
20
|
+
backfill: { status: 'no-op', changed: false, error: null },
|
|
22
21
|
lifecycle: { status: 'no-op', changed: false, error: null },
|
|
23
22
|
taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
24
23
|
verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
25
24
|
summary: receipt.summary === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
26
|
-
postSummaryVerification: receipt.postSummaryVerification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
|
|
27
25
|
completedSteps, pendingSteps,
|
|
28
26
|
result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
|
|
29
27
|
? 'completed_with_warnings' : 'completed',
|