@fitlab-ai/agent-infra 0.9.14 → 0.9.15
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/README.md +10 -0
- package/README.zh-CN.md +10 -0
- package/bin/internal-cli.sh +45 -0
- package/dist/bin/internal-cli.js +93 -30
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
- package/dist/lib/defaults.json +103 -103
- package/dist/lib/fs/durable-write.js +37 -0
- package/dist/lib/fs/file-lock.js +111 -0
- package/dist/lib/host-control/audit.js +19 -0
- package/dist/lib/host-control/client.js +142 -0
- package/dist/lib/host-control/command.js +49 -0
- package/dist/lib/host-control/path.js +160 -0
- package/dist/lib/host-control/secure-fs.js +140 -0
- package/dist/lib/host-control/server.js +159 -0
- package/dist/lib/host-control/service.js +112 -0
- package/dist/lib/init.js +11 -2
- package/dist/lib/internal/cli-route-inventory.js +3 -1
- package/dist/lib/internal/codex-lifecycle.js +6 -6
- package/dist/lib/internal/demo-promotion.js +322 -0
- package/dist/lib/internal/demo-transcript.js +454 -0
- package/dist/lib/internal/host-control.js +28 -0
- package/dist/lib/internal/manual-validation.js +322 -0
- package/dist/lib/internal/platform-pr.js +18 -4
- package/dist/lib/internal/release-workflow.js +63 -84
- package/dist/lib/internal/sandbox-control.js +149 -2
- package/dist/lib/internal/task-artifact.js +15 -262
- package/dist/lib/internal/task-create.js +6 -5
- package/dist/lib/internal/task-event.js +37 -37
- package/dist/lib/internal/task-operation-registry.js +101 -3
- package/dist/lib/internal/task-review.js +8 -53
- package/dist/lib/internal/task-validate.js +30 -10
- package/dist/lib/owned-path.js +33 -0
- package/dist/lib/platform/check-status.js +11 -0
- package/dist/lib/platform/comment-safety.js +4 -1
- package/dist/lib/platform/completion-backfill.js +1 -1
- package/dist/lib/platform/github-data.js +241 -0
- package/dist/lib/platform/github-provider.js +15 -20
- package/dist/lib/platform/issue-comments.js +2 -2
- package/dist/lib/platform/issues.js +1 -179
- package/dist/lib/platform/pr-checks.js +12 -65
- package/dist/lib/platform/pr-summary.js +55 -3
- package/dist/lib/platform/pull-requests.js +2 -105
- package/dist/lib/platform/snapshots.js +2 -0
- package/dist/lib/platform/verification-sync.js +127 -142
- package/dist/lib/release/workflow.js +1 -1
- package/dist/lib/sandbox/commands/create.js +7 -2
- package/dist/lib/sandbox/commands/rm.js +3 -1514
- package/dist/lib/sandbox/control/audit.js +203 -0
- package/dist/lib/sandbox/control/client.js +93 -54
- package/dist/lib/sandbox/control/executor.js +129 -65
- package/dist/lib/sandbox/control/finalization-response.js +27 -0
- package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
- package/dist/lib/sandbox/control/lifecycle.js +36 -2
- package/dist/lib/sandbox/control/native-file-lock.js +6 -98
- package/dist/lib/sandbox/control/protocol.js +55 -4
- package/dist/lib/sandbox/control/server.js +491 -120
- package/dist/lib/sandbox/control/state.js +58 -0
- package/dist/lib/sandbox/control/task-workflow.js +74 -0
- package/dist/lib/sandbox/control/workflow-executor.js +99 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/recovery.js +28 -4
- package/dist/lib/sandbox/removal.js +1717 -0
- package/dist/lib/sandbox/task-cleanup.js +217 -0
- package/dist/lib/sandbox/workspace-view.js +13 -14
- package/dist/lib/task/activity-intent.js +2 -1
- package/dist/lib/task/activity-log.js +30 -24
- package/dist/lib/task/artifact-command.js +101 -0
- package/dist/lib/task/artifact-lifecycle.js +30 -28
- package/dist/lib/task/artifact-name.js +36 -0
- package/dist/lib/task/artifact-operations.js +23 -69
- package/dist/lib/task/artifact-receipts.js +3 -15
- package/dist/lib/task/artifact-repair-intent.js +87 -16
- package/dist/lib/task/artifact-schema.js +6 -14
- package/dist/lib/task/capabilities.js +12 -19
- package/dist/lib/task/codex-orchestration.js +71 -25
- package/dist/lib/task/commit-identity.js +28 -0
- package/dist/lib/task/commit-operation.js +2 -16
- package/dist/lib/task/control-authority.js +371 -5
- package/dist/lib/task/control-recovery.js +128 -0
- package/dist/lib/task/create.js +62 -9
- package/dist/lib/task/decision-details.js +2 -46
- package/dist/lib/task/events.js +120 -9
- package/dist/lib/task/finalization-state.js +9 -0
- package/dist/lib/task/human-override.js +3 -6
- package/dist/lib/task/implementation-inputs.js +3 -3
- package/dist/lib/task/intermediate-cleanup.js +383 -0
- package/dist/lib/task/invalidation.js +18 -65
- package/dist/lib/task/ledger-intents.js +7 -22
- package/dist/lib/task/ledger.js +1 -3
- package/dist/lib/task/lifecycle.js +19 -1
- package/dist/lib/task/local-artifact-finalization.js +132 -157
- package/dist/lib/task/manual-validation-completion.js +26 -0
- package/dist/lib/task/manual-validation-evidence.js +130 -0
- package/dist/lib/task/manual-validation-receipt.js +101 -0
- package/dist/lib/task/manual-validation-shared.js +40 -0
- package/dist/lib/task/manual-validation-transaction.js +243 -0
- package/dist/lib/task/markdown.js +47 -0
- package/dist/lib/task/orchestration.js +14 -17
- package/dist/lib/task/qualification-audit.js +20 -34
- package/dist/lib/task/review-artifacts.js +2 -18
- package/dist/lib/task/review-command.js +25 -0
- package/dist/lib/task/review-finalization.js +125 -193
- package/dist/lib/task/review-fingerprint.js +2 -2
- package/dist/lib/task/sections.js +34 -56
- package/dist/lib/task/verification-engine.js +116 -323
- package/dist/lib/task/verification.js +1 -1
- package/dist/lib/task/workflow-command.js +17 -0
- package/dist/lib/task/workflow-warning-intents.js +0 -16
- package/dist/lib/task/workflow-warnings.js +28 -2
- package/dist/lib/update.js +2 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
- package/lib/fs/durable-write.ts +29 -0
- package/lib/fs/file-lock.ts +113 -0
- package/lib/host-control/audit.ts +22 -0
- package/lib/host-control/client.ts +172 -0
- package/lib/host-control/command.ts +60 -0
- package/lib/host-control/path.ts +167 -0
- package/lib/host-control/secure-fs.ts +135 -0
- package/lib/host-control/server.ts +180 -0
- package/lib/host-control/service.ts +133 -0
- package/lib/init.ts +15 -2
- package/lib/internal/cli-route-inventory.ts +3 -1
- package/lib/internal/codex-lifecycle.ts +6 -6
- package/lib/internal/demo-promotion.ts +354 -0
- package/lib/internal/demo-transcript.ts +419 -0
- package/lib/internal/host-control.ts +28 -0
- package/lib/internal/manual-validation.ts +306 -0
- package/lib/internal/platform-pr.ts +15 -4
- package/lib/internal/release-workflow.ts +74 -82
- package/lib/internal/sandbox-control.ts +162 -1
- package/lib/internal/task-artifact.ts +13 -216
- package/lib/internal/task-create.ts +5 -5
- package/lib/internal/task-event.ts +20 -11
- package/lib/internal/task-operation-registry.ts +111 -3
- package/lib/internal/task-review.ts +6 -51
- package/lib/internal/task-validate.ts +26 -10
- package/lib/owned-path.ts +26 -0
- package/lib/platform/check-status.ts +9 -0
- package/lib/platform/comment-safety.ts +5 -0
- package/lib/platform/completion-backfill.ts +1 -1
- package/lib/platform/github-data.ts +322 -0
- package/lib/platform/github-provider.ts +15 -20
- package/lib/platform/issue-comments.ts +3 -4
- package/lib/platform/issues.ts +4 -245
- package/lib/platform/pr-checks.ts +13 -77
- package/lib/platform/pr-summary.ts +61 -5
- package/lib/platform/pull-requests.ts +3 -170
- package/lib/platform/snapshots.ts +53 -0
- package/lib/platform/verification-sync.ts +145 -155
- package/lib/release/workflow.ts +3 -3
- package/lib/sandbox/commands/create.ts +7 -6
- package/lib/sandbox/commands/rm.ts +3 -1909
- package/lib/sandbox/control/audit.ts +253 -0
- package/lib/sandbox/control/client.ts +109 -66
- package/lib/sandbox/control/executor.ts +134 -74
- package/lib/sandbox/control/finalization-response.ts +29 -0
- package/lib/sandbox/control/identity-sentinel.ts +137 -0
- package/lib/sandbox/control/lifecycle.ts +45 -3
- package/lib/sandbox/control/native-file-lock.ts +6 -107
- package/lib/sandbox/control/protocol.ts +75 -6
- package/lib/sandbox/control/server.ts +538 -123
- package/lib/sandbox/control/state.ts +78 -0
- package/lib/sandbox/control/task-workflow.ts +95 -0
- package/lib/sandbox/control/workflow-executor.ts +102 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/recovery.ts +28 -5
- package/lib/sandbox/removal.ts +2144 -0
- package/lib/sandbox/task-cleanup.ts +209 -0
- package/lib/sandbox/workspace-view.ts +17 -15
- package/lib/task/activity-intent.ts +2 -2
- package/lib/task/activity-log.ts +27 -19
- package/lib/task/artifact-command.ts +104 -0
- package/lib/task/artifact-lifecycle.ts +31 -26
- package/lib/task/artifact-name.ts +55 -0
- package/lib/task/artifact-operations.ts +22 -68
- package/lib/task/artifact-receipts.ts +3 -14
- package/lib/task/artifact-repair-intent.ts +99 -18
- package/lib/task/artifact-schema.ts +5 -34
- package/lib/task/capabilities.ts +13 -22
- package/lib/task/codex-orchestration.ts +85 -24
- package/lib/task/commit-identity.ts +31 -0
- package/lib/task/commit-operation.ts +2 -16
- package/lib/task/control-authority.ts +502 -5
- package/lib/task/control-recovery.ts +174 -0
- package/lib/task/create.ts +63 -8
- package/lib/task/decision-details.ts +3 -71
- package/lib/task/events.ts +131 -10
- package/lib/task/finalization-state.ts +11 -0
- package/lib/task/human-override.ts +3 -8
- package/lib/task/implementation-inputs.ts +3 -3
- package/lib/task/intermediate-cleanup.ts +455 -0
- package/lib/task/invalidation.ts +18 -62
- package/lib/task/ledger-intents.ts +7 -24
- package/lib/task/ledger.ts +1 -3
- package/lib/task/lifecycle.ts +17 -1
- package/lib/task/local-artifact-finalization.ts +148 -182
- package/lib/task/manual-validation-completion.ts +40 -0
- package/lib/task/manual-validation-evidence.ts +209 -0
- package/lib/task/manual-validation-receipt.ts +152 -0
- package/lib/task/manual-validation-shared.ts +50 -0
- package/lib/task/manual-validation-transaction.ts +304 -0
- package/lib/task/markdown.ts +70 -0
- package/lib/task/orchestration.ts +19 -19
- package/lib/task/qualification-audit.ts +22 -33
- package/lib/task/review-artifacts.ts +1 -20
- package/lib/task/review-command.ts +23 -0
- package/lib/task/review-finalization.ts +131 -259
- package/lib/task/review-fingerprint.ts +2 -2
- package/lib/task/sections.ts +33 -54
- package/lib/task/verification-engine.ts +110 -366
- package/lib/task/verification-types.ts +17 -0
- package/lib/task/verification.ts +1 -1
- package/lib/task/workflow-command.ts +18 -0
- package/lib/task/workflow-warning-intents.ts +0 -13
- package/lib/task/workflow-warnings.ts +21 -2
- package/lib/update.ts +6 -0
- package/package.json +6 -6
- package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
- package/templates/.agents/rules/testing-discipline.en.md +2 -0
- package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
- package/templates/.agents/scripts/package.json +3 -0
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/release/SKILL.en.md +7 -3
- package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +17 -6
- package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/dist/lib/platform/adapters.js +0 -56
- package/lib/platform/adapters.ts +0 -197
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/workflow.js';
|
|
4
4
|
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
5
|
+
import { checkpointCommitMatches } from './commit-identity.js';
|
|
5
6
|
import { resolveTaskContext, resolveTaskRef } from './resolve-ref.js';
|
|
6
7
|
import { commitPushDecision } from './commit-policy.js';
|
|
7
8
|
import { mergeOperationWarnings } from './operation-outcome.js';
|
|
@@ -163,21 +164,6 @@ function syncTaskCommit(input, task, committedHead) {
|
|
|
163
164
|
return warning('TASK_STATUS_SYNC_FAILED', error instanceof Error ? error.message : String(error), true, 'task', task.taskId, 'ACTION_REQUIRED');
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
|
-
function checkpointChildMatches(task, intent, head) {
|
|
167
|
-
try {
|
|
168
|
-
const parent = gitText(task.repoRoot, ['rev-parse', `${head}^`]);
|
|
169
|
-
const tree = gitText(task.repoRoot, ['rev-parse', `${head}^{tree}`]);
|
|
170
|
-
const message = gitText(task.repoRoot, ['show', '-s', '--format=%s', head]);
|
|
171
|
-
const changed = gitText(task.repoRoot, ['diff-tree', '--no-commit-id', '--name-only', '-r', head]).split('\n').filter(Boolean).sort();
|
|
172
|
-
return parent === intent.expectedHead
|
|
173
|
-
&& tree === intent.expectedTree
|
|
174
|
-
&& message === intent.message
|
|
175
|
-
&& JSON.stringify(changed) === JSON.stringify([...intent.paths].sort());
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
167
|
function checkpointIdentity(input, task, delivery) {
|
|
182
168
|
if (delivery.mode !== 'local')
|
|
183
169
|
throw new Error('checkpoint identity requires local delivery');
|
|
@@ -256,7 +242,7 @@ function executeUnlocked(input, task, mode) {
|
|
|
256
242
|
operations: [{ name: 'commit', status: 'no-op' }], error: null
|
|
257
243
|
};
|
|
258
244
|
}
|
|
259
|
-
if (inspected.snapshot.head !== identity.expectedHead && !
|
|
245
|
+
if (inspected.snapshot.head !== identity.expectedHead && !checkpointCommitMatches(task.repoRoot, pendingIntent, inspected.snapshot.head))
|
|
260
246
|
return {
|
|
261
247
|
status: 'failed', changed: false, snapshot: inspected.snapshot, operations: [],
|
|
262
248
|
error: { code: 'COMMIT_INTENT_HEAD_CONFLICT', message: 'Current HEAD does not match the pending checkpoint intent' }
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
2
4
|
import { normalizeAgentToken } from '../agent-clients/tokens.js';
|
|
3
5
|
import { isAgentClientId } from '../agent-clients/types.js';
|
|
4
6
|
import { consumeHumanOverride, failureId, overrideDryRunConflict } from './human-override.js';
|
|
@@ -6,9 +8,357 @@ import { activateMatchingOrchestrationDelegation, activateOrchestrationDelegatio
|
|
|
6
8
|
import { prepareCodexOrchestrationDelegation } from './codex-orchestration.js';
|
|
7
9
|
import { applyTaskFinalization } from './finalization.js';
|
|
8
10
|
import { applyTaskLifecycle, taskLifecycleFailure } from './lifecycle.js';
|
|
11
|
+
import { locateActivityLog } from './activity-log.js';
|
|
9
12
|
import { resolveTaskRef, TASK_ID_RE } from './resolve-ref.js';
|
|
10
13
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
11
14
|
import { verifyTaskEvent } from './verification.js';
|
|
15
|
+
import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
|
|
16
|
+
import { findArtifactRepairIntentsByOperation, writeArtifactRepairIntent } from './artifact-repair-intent.js';
|
|
17
|
+
import { computeLifecycleBuildIdentity, verifyLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
18
|
+
const lifecycleAuthorityPhases = [
|
|
19
|
+
'orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'
|
|
20
|
+
];
|
|
21
|
+
const lifecycleAuthorityFamilies = ['analysis', 'plan', 'code'];
|
|
22
|
+
const lifecycleAuthorityArtifact = /^(?:analysis|plan|code)(?:-r[1-9]\d*)?\.md$/u;
|
|
23
|
+
const lifecycleAuthorityDigest = (value) => crypto.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
24
|
+
const lifecycleAuthorityBuildDigest = (value) => lifecycleAuthorityDigest(JSON.stringify(value));
|
|
25
|
+
export function lifecycleRecoveryAttestationDigest(value) {
|
|
26
|
+
return lifecycleAuthorityDigest(JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
function authorityExactKeys(value, keys) {
|
|
29
|
+
return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
|
|
30
|
+
}
|
|
31
|
+
export function validateLifecycleAuthorityRequest(value) {
|
|
32
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
33
|
+
throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
|
|
34
|
+
const request = value;
|
|
35
|
+
if (!authorityExactKeys(request, [
|
|
36
|
+
'artifact', 'authorityRef', 'expectedBuildIdentityDigest', 'expectedControlGeneration',
|
|
37
|
+
'expectedControllerInstanceDigest', 'expectedHookDefinitionHash', 'family', 'lifecycleRequestId',
|
|
38
|
+
'operationId', 'phase', 'requestId', 'round', 'taskId', 'version'
|
|
39
|
+
])
|
|
40
|
+
|| request.version !== 1
|
|
41
|
+
|| typeof request.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.requestId)
|
|
42
|
+
|| typeof request.operationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.operationId)
|
|
43
|
+
|| !lifecycleAuthorityPhases.includes(request.phase)
|
|
44
|
+
|| typeof request.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(request.taskId)
|
|
45
|
+
|| !lifecycleAuthorityFamilies.includes(request.family)
|
|
46
|
+
|| typeof request.artifact !== 'string' || !lifecycleAuthorityArtifact.test(request.artifact)
|
|
47
|
+
|| !Number.isSafeInteger(request.round) || request.round < 1
|
|
48
|
+
|| typeof request.lifecycleRequestId !== 'string' || !request.lifecycleRequestId || /[\r\n]/u.test(request.lifecycleRequestId)
|
|
49
|
+
|| typeof request.authorityRef !== 'string' || !request.authorityRef || /[\r\n]/u.test(request.authorityRef)
|
|
50
|
+
|| typeof request.expectedControlGeneration !== 'string' || !request.expectedControlGeneration
|
|
51
|
+
|| typeof request.expectedControllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedControllerInstanceDigest)
|
|
52
|
+
|| typeof request.expectedBuildIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedBuildIdentityDigest)
|
|
53
|
+
|| typeof request.expectedHookDefinitionHash !== 'string' || !/^[a-f0-9]{64}$/u.test(request.expectedHookDefinitionHash)) {
|
|
54
|
+
throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
|
|
55
|
+
}
|
|
56
|
+
const expectedArtifactFamily = request.artifact;
|
|
57
|
+
if (!expectedArtifactFamily.startsWith(request.family))
|
|
58
|
+
throw new Error('LIFECYCLE_AUTHORITY_REQUEST_INVALID');
|
|
59
|
+
return request;
|
|
60
|
+
}
|
|
61
|
+
export function validateLifecycleRecoveryAttestation(value, now = Date.now()) {
|
|
62
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
63
|
+
throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_INVALID');
|
|
64
|
+
const attestation = value;
|
|
65
|
+
if (!authorityExactKeys(attestation, [
|
|
66
|
+
'artifact', 'attestationId', 'authorityRefDigest', 'buildIdentityDigest', 'controlGeneration',
|
|
67
|
+
'controllerInstanceDigest', 'expiresAt', 'family', 'hookDefinitionHash', 'issuedAt',
|
|
68
|
+
'lifecycleRequestId', 'operationId', 'phase', 'requestId', 'round', 'sessionId',
|
|
69
|
+
'taskId', 'toolUseId', 'turnId', 'version'
|
|
70
|
+
])
|
|
71
|
+
|| attestation.version !== 1
|
|
72
|
+
|| typeof attestation.attestationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.attestationId)
|
|
73
|
+
|| typeof attestation.authorityRefDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.authorityRefDigest)
|
|
74
|
+
|| typeof attestation.operationId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.operationId)
|
|
75
|
+
|| !lifecycleAuthorityPhases.includes(attestation.phase)
|
|
76
|
+
|| typeof attestation.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(attestation.requestId)
|
|
77
|
+
|| typeof attestation.lifecycleRequestId !== 'string' || !attestation.lifecycleRequestId
|
|
78
|
+
|| typeof attestation.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(attestation.taskId)
|
|
79
|
+
|| !lifecycleAuthorityFamilies.includes(attestation.family)
|
|
80
|
+
|| typeof attestation.artifact !== 'string' || !lifecycleAuthorityArtifact.test(attestation.artifact)
|
|
81
|
+
|| !Number.isSafeInteger(attestation.round) || attestation.round < 1
|
|
82
|
+
|| typeof attestation.controlGeneration !== 'string' || !attestation.controlGeneration
|
|
83
|
+
|| typeof attestation.controllerInstanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.controllerInstanceDigest)
|
|
84
|
+
|| typeof attestation.sessionId !== 'string' || !attestation.sessionId
|
|
85
|
+
|| typeof attestation.turnId !== 'string' || !attestation.turnId
|
|
86
|
+
|| typeof attestation.toolUseId !== 'string' || !attestation.toolUseId
|
|
87
|
+
|| typeof attestation.buildIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.buildIdentityDigest)
|
|
88
|
+
|| typeof attestation.hookDefinitionHash !== 'string' || !/^[a-f0-9]{64}$/u.test(attestation.hookDefinitionHash)
|
|
89
|
+
|| !Number.isSafeInteger(attestation.issuedAt) || !Number.isSafeInteger(attestation.expiresAt)
|
|
90
|
+
|| attestation.expiresAt <= attestation.issuedAt
|
|
91
|
+
|| attestation.expiresAt <= now) {
|
|
92
|
+
throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_INVALID');
|
|
93
|
+
}
|
|
94
|
+
return attestation;
|
|
95
|
+
}
|
|
96
|
+
const authorityOperationMap = new WeakMap();
|
|
97
|
+
const authorityRecords = new Map();
|
|
98
|
+
const consumedAuthorityPhases = new Set();
|
|
99
|
+
export function issueLifecycleRecoveryAttestation(input, options = {}) {
|
|
100
|
+
let request;
|
|
101
|
+
try {
|
|
102
|
+
request = validateLifecycleAuthorityRequest(input);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return { version: 1, requestId: String(input?.requestId ?? ''), operationId: String(input?.operationId ?? ''), phase: input?.phase ?? 'artifact.finalize-local', status: 'rejected', attestation: null, error: { code: 'LIFECYCLE_AUTHORITY_REQUEST_INVALID', message: error instanceof Error ? error.message : String(error) } };
|
|
106
|
+
}
|
|
107
|
+
const binding = options.controllerBinding ?? null;
|
|
108
|
+
if (!binding)
|
|
109
|
+
return authorityRejected(request, 'LOCAL_EXECUTION_AUTHORITY_UNAVAILABLE', 'current controller authority is unavailable');
|
|
110
|
+
if (binding.controlGeneration !== request.expectedControlGeneration
|
|
111
|
+
|| binding.instanceDigest !== request.expectedControllerInstanceDigest) {
|
|
112
|
+
return authorityRejected(request, 'LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the request');
|
|
113
|
+
}
|
|
114
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(process.cwd());
|
|
115
|
+
if (lifecycleAuthorityBuildDigest(buildIdentity) !== request.expectedBuildIdentityDigest) {
|
|
116
|
+
return authorityRejected(request, 'LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the request');
|
|
117
|
+
}
|
|
118
|
+
const store = options.capabilityStore ?? createCodexCapabilityStore();
|
|
119
|
+
const registry = options.operationAttestations ?? authorityOperationMap.get(store) ?? new Map();
|
|
120
|
+
authorityOperationMap.set(store, registry);
|
|
121
|
+
const key = `${request.requestId}\0${request.operationId}\0${request.phase}`;
|
|
122
|
+
const prior = registry.get(key);
|
|
123
|
+
if (prior)
|
|
124
|
+
return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'already-completed', attestation: prior, error: null };
|
|
125
|
+
try {
|
|
126
|
+
const expected = {
|
|
127
|
+
taskId: request.taskId,
|
|
128
|
+
hookDefinitionHash: request.expectedHookDefinitionHash,
|
|
129
|
+
buildIdentity,
|
|
130
|
+
controller: binding
|
|
131
|
+
};
|
|
132
|
+
const record = store.reserveReference(request.authorityRef, request.operationId, expected);
|
|
133
|
+
if (!record.sessionId || !record.turnId || !record.toolUseId)
|
|
134
|
+
throw new Error('CODEX_CAPABILITY_IDENTITY_INVALID: capability hook attestation is incomplete');
|
|
135
|
+
const now = (options.now ?? Date.now)();
|
|
136
|
+
const attestation = validateLifecycleRecoveryAttestation({
|
|
137
|
+
version: 1,
|
|
138
|
+
attestationId: crypto.randomUUID(),
|
|
139
|
+
authorityRefDigest: lifecycleAuthorityDigest(request.authorityRef),
|
|
140
|
+
operationId: request.operationId,
|
|
141
|
+
phase: request.phase,
|
|
142
|
+
requestId: request.requestId,
|
|
143
|
+
lifecycleRequestId: request.lifecycleRequestId,
|
|
144
|
+
taskId: request.taskId,
|
|
145
|
+
family: request.family,
|
|
146
|
+
artifact: request.artifact,
|
|
147
|
+
round: request.round,
|
|
148
|
+
controlGeneration: binding.controlGeneration,
|
|
149
|
+
controllerInstanceDigest: binding.instanceDigest,
|
|
150
|
+
sessionId: record.sessionId,
|
|
151
|
+
turnId: record.turnId,
|
|
152
|
+
toolUseId: record.toolUseId,
|
|
153
|
+
buildIdentityDigest: lifecycleAuthorityBuildDigest(buildIdentity),
|
|
154
|
+
hookDefinitionHash: request.expectedHookDefinitionHash,
|
|
155
|
+
issuedAt: now,
|
|
156
|
+
expiresAt: Math.min(record.expiresAt, now + 30_000)
|
|
157
|
+
}, now);
|
|
158
|
+
store.reserveRecoveryPhase(request.authorityRef, request.operationId, request.phase, request.requestId, expected);
|
|
159
|
+
registry.set(key, attestation);
|
|
160
|
+
authorityRecords.set(attestation.attestationId, { store, capabilityRef: request.authorityRef, expected });
|
|
161
|
+
return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'issued', attestation, error: null };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return authorityRejected(request, error instanceof Error && error.name.startsWith('CODEX_') ? error.name : 'LIFECYCLE_AUTHORITY_REJECTED', error instanceof Error ? error.message : String(error));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function consumeLifecycleRecoveryAttestation(value, now = Date.now()) {
|
|
168
|
+
const attestation = validateLifecycleRecoveryAttestation(value, now);
|
|
169
|
+
if (consumedAuthorityPhases.has(attestation.attestationId))
|
|
170
|
+
return;
|
|
171
|
+
const record = authorityRecords.get(attestation.attestationId);
|
|
172
|
+
if (!record)
|
|
173
|
+
throw new Error('LIFECYCLE_AUTHORITY_ATTESTATION_UNKNOWN');
|
|
174
|
+
record.store.consumeRecoveryPhase(record.capabilityRef, attestation.operationId, attestation.phase, attestation.requestId, record.expected);
|
|
175
|
+
if (attestation.phase === 'task-event.completed') {
|
|
176
|
+
record.store.consumeReference(record.capabilityRef, attestation.operationId, record.expected);
|
|
177
|
+
}
|
|
178
|
+
consumedAuthorityPhases.add(attestation.attestationId);
|
|
179
|
+
}
|
|
180
|
+
export function queryLifecycleRecoveryOperation(operationId, options = {}) {
|
|
181
|
+
if (!/^[a-f0-9-]{16,64}$/u.test(operationId)) {
|
|
182
|
+
throw new Error('LIFECYCLE_RECOVERY_OPERATION_INVALID');
|
|
183
|
+
}
|
|
184
|
+
const store = options.capabilityStore ?? createCodexCapabilityStore();
|
|
185
|
+
const registry = authorityOperationMap.get(store);
|
|
186
|
+
const known = [...(registry?.values() ?? [])].filter((value) => value.operationId === operationId);
|
|
187
|
+
const intents = options.repoRoot ? findArtifactRepairIntentsByOperation(options.repoRoot, operationId) : [];
|
|
188
|
+
const capabilities = store.findByRecoveryOperation(operationId);
|
|
189
|
+
const phaseMap = new Map();
|
|
190
|
+
for (const capability of capabilities) {
|
|
191
|
+
for (const phase of capability.recoveryPhases) {
|
|
192
|
+
phaseMap.set(`${phase.phase}\0${phase.requestId}`, {
|
|
193
|
+
phase: phase.phase,
|
|
194
|
+
requestId: phase.requestId,
|
|
195
|
+
attestationId: null,
|
|
196
|
+
state: phase.state === 'consumed' ? 'consumed' : 'observed'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (const attestation of known) {
|
|
201
|
+
const key = `${attestation.phase}\0${attestation.requestId}`;
|
|
202
|
+
const durable = phaseMap.get(key);
|
|
203
|
+
phaseMap.set(key, {
|
|
204
|
+
phase: attestation.phase,
|
|
205
|
+
requestId: attestation.requestId,
|
|
206
|
+
attestationId: attestation.attestationId,
|
|
207
|
+
state: durable?.state === 'consumed' || consumedAuthorityPhases.has(attestation.attestationId)
|
|
208
|
+
? 'consumed' : 'issued'
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
for (const intent of intents) {
|
|
212
|
+
if (!intent.phase)
|
|
213
|
+
continue;
|
|
214
|
+
const key = `${intent.phase}\0${intent.requestId}`;
|
|
215
|
+
if (phaseMap.has(key))
|
|
216
|
+
continue;
|
|
217
|
+
phaseMap.set(key, {
|
|
218
|
+
phase: intent.phase,
|
|
219
|
+
requestId: intent.requestId,
|
|
220
|
+
attestationId: null,
|
|
221
|
+
state: intent.state === 'consumed' ? 'consumed' : 'observed'
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const capabilityState = capabilities.some((record) => record.recoveryState === 'consumed')
|
|
225
|
+
? 'consumed'
|
|
226
|
+
: capabilities.some((record) => record.recoveryState === 'reserved') ? 'reserved' : 'unknown';
|
|
227
|
+
const committed = capabilityState === 'consumed' || intents.some((intent) => intent.state === 'consumed');
|
|
228
|
+
return {
|
|
229
|
+
version: 1,
|
|
230
|
+
operationId,
|
|
231
|
+
status: committed ? 'committed' : capabilityState === 'reserved' || phaseMap.size > 0 ? 'in-progress' : 'unknown',
|
|
232
|
+
capabilityState,
|
|
233
|
+
phases: Object.freeze([...phaseMap.values()])
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function lifecycleRecoveryCompensationError(code, message) {
|
|
237
|
+
const error = new Error(`${code}: ${message}`);
|
|
238
|
+
error.name = code;
|
|
239
|
+
return error;
|
|
240
|
+
}
|
|
241
|
+
function lifecycleRecoveryEventCommitted(selector, repoRoot) {
|
|
242
|
+
const resolved = resolveTaskRef(selector.taskId, { repoRoot });
|
|
243
|
+
if (!resolved.ok || resolved.taskId !== selector.taskId)
|
|
244
|
+
return false;
|
|
245
|
+
let content;
|
|
246
|
+
try {
|
|
247
|
+
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const section = locateActivityLog(content);
|
|
253
|
+
if (!section)
|
|
254
|
+
return false;
|
|
255
|
+
const labels = { analysis: 'Analyze Task', plan: 'Plan Task', code: 'Code Task' };
|
|
256
|
+
const prefix = `${labels[selector.family]} (Round ${selector.round}`;
|
|
257
|
+
return section.entries.some((entry) => {
|
|
258
|
+
if (entry.step.endsWith(' [started]') || !entry.step.startsWith(prefix))
|
|
259
|
+
return false;
|
|
260
|
+
const delimiter = entry.step[prefix.length];
|
|
261
|
+
return (delimiter === ',' || delimiter === ')') && entry.note.endsWith(`→ ${selector.artifact}`);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function lifecycleRecoveryIntentMatches(intent, selector) {
|
|
265
|
+
return intent.taskId === selector.taskId
|
|
266
|
+
&& intent.family === selector.family
|
|
267
|
+
&& intent.artifact === selector.artifact
|
|
268
|
+
&& (intent.state === 'commit-started' || intent.state === 'consumed');
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Completes only the durable tail of a committed task event after a process
|
|
272
|
+
* restart. The task event itself is the commit boundary and is never replayed.
|
|
273
|
+
*/
|
|
274
|
+
export function recoverLifecycleRecoveryOperation(input, options) {
|
|
275
|
+
const selector = validateLifecycleAuthorityRequest(input);
|
|
276
|
+
if (selector.phase !== 'task-event.completed') {
|
|
277
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_UNSUPPORTED', 'only task-event.completed supports durable compensation');
|
|
278
|
+
}
|
|
279
|
+
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(options.repoRoot);
|
|
280
|
+
if (selector.expectedBuildIdentityDigest !== lifecycleAuthorityBuildDigest(buildIdentity)) {
|
|
281
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_BUILD_MISMATCH', 'current lifecycle build does not match the recovery request');
|
|
282
|
+
}
|
|
283
|
+
if (selector.expectedControlGeneration !== options.controllerBinding.controlGeneration
|
|
284
|
+
|| selector.expectedControllerInstanceDigest !== options.controllerBinding.instanceDigest) {
|
|
285
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_AUTHORITY_CONTROLLER_MISMATCH', 'current controller binding does not match the recovery request');
|
|
286
|
+
}
|
|
287
|
+
const store = options.capabilityStore ?? createCodexCapabilityStore();
|
|
288
|
+
const expected = {
|
|
289
|
+
taskId: selector.taskId,
|
|
290
|
+
hookDefinitionHash: selector.expectedHookDefinitionHash,
|
|
291
|
+
buildIdentity,
|
|
292
|
+
controller: options.controllerBinding
|
|
293
|
+
};
|
|
294
|
+
const capabilities = store.findByRecoveryOperation(selector.operationId);
|
|
295
|
+
if (capabilities.length !== 1 || capabilities[0].capabilityRefDigest !== lifecycleAuthorityDigest(selector.authorityRef)) {
|
|
296
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not identify this operation');
|
|
297
|
+
}
|
|
298
|
+
const capability = capabilities[0];
|
|
299
|
+
const build = verifyLifecycleBuildIdentity(capability.buildIdentity, buildIdentity);
|
|
300
|
+
const expiredReserved = capability.status === 'expired' && capability.recoveryState === 'reserved';
|
|
301
|
+
if (!build.ok
|
|
302
|
+
|| capability.taskId !== selector.taskId
|
|
303
|
+
|| capability.hookDefinitionHash !== selector.expectedHookDefinitionHash
|
|
304
|
+
|| capability.controller?.controlGeneration !== options.controllerBinding.controlGeneration
|
|
305
|
+
|| capability.controller?.instanceDigest !== options.controllerBinding.instanceDigest
|
|
306
|
+
|| capability.recoveryState === 'unreserved'
|
|
307
|
+
|| capability.recoveryOperationId !== selector.operationId
|
|
308
|
+
|| (!['attested', 'consumed'].includes(capability.status) && !expiredReserved)) {
|
|
309
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PROVENANCE_INVALID', 'durable capability provenance does not match the recovery request');
|
|
310
|
+
}
|
|
311
|
+
const phase = capability.recoveryPhases.find((entry) => entry.phase === selector.phase);
|
|
312
|
+
if (!phase) {
|
|
313
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_PHASE_MISSING', 'durable task completion phase is missing');
|
|
314
|
+
}
|
|
315
|
+
const intents = findArtifactRepairIntentsByOperation(options.repoRoot, selector.operationId)
|
|
316
|
+
.filter((intent) => intent.taskId === selector.taskId
|
|
317
|
+
&& intent.family === selector.family
|
|
318
|
+
&& intent.artifact === selector.artifact);
|
|
319
|
+
if (intents.length !== 1) {
|
|
320
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not identify this task event');
|
|
321
|
+
}
|
|
322
|
+
const intent = intents[0];
|
|
323
|
+
if (!lifecycleRecoveryIntentMatches(intent, selector)
|
|
324
|
+
|| intent.phase !== selector.phase
|
|
325
|
+
|| intent.requestId !== selector.lifecycleRequestId
|
|
326
|
+
|| intent.authorityDigest === null) {
|
|
327
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_INTENT_INVALID', 'durable artifact repair intent does not match the recovery request');
|
|
328
|
+
}
|
|
329
|
+
if (!lifecycleRecoveryEventCommitted(selector, options.repoRoot)) {
|
|
330
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_COMMIT_UNCONFIRMED', 'task completion is not durably present; compensation is not allowed');
|
|
331
|
+
}
|
|
332
|
+
if (capability.recoveryState === 'reserved') {
|
|
333
|
+
if (phase.state === 'issued') {
|
|
334
|
+
store.consumeRecoveryPhase(selector.authorityRef, selector.operationId, selector.phase, phase.requestId, expected, { allowExpiredReserved: true });
|
|
335
|
+
}
|
|
336
|
+
store.consumeReference(selector.authorityRef, selector.operationId, expected, { allowExpiredReserved: true });
|
|
337
|
+
}
|
|
338
|
+
else if (phase.state !== 'consumed') {
|
|
339
|
+
throw lifecycleRecoveryCompensationError('LIFECYCLE_RECOVERY_STATE_INVALID', 'consumed capability has an unconsumed completion phase');
|
|
340
|
+
}
|
|
341
|
+
if (intent.state !== 'consumed') {
|
|
342
|
+
writeArtifactRepairIntent(options.repoRoot, {
|
|
343
|
+
...intent,
|
|
344
|
+
state: 'consumed',
|
|
345
|
+
updatedAt: (options.now ?? Date.now)()
|
|
346
|
+
}, { expected: intent });
|
|
347
|
+
}
|
|
348
|
+
return queryLifecycleRecoveryOperation(selector.operationId, { repoRoot: options.repoRoot, capabilityStore: store });
|
|
349
|
+
}
|
|
350
|
+
function authorityRejected(request, code, message) {
|
|
351
|
+
return { version: 1, requestId: request.requestId, operationId: request.operationId, phase: request.phase, status: 'rejected', attestation: null, error: { code, message } };
|
|
352
|
+
}
|
|
353
|
+
export function taskControlOperationKey(operation) {
|
|
354
|
+
if (operation.family === 'task-finalization')
|
|
355
|
+
return 'complete';
|
|
356
|
+
if (operation.family === 'task-lifecycle')
|
|
357
|
+
return operation.request.intent;
|
|
358
|
+
return operation.intent === 'route'
|
|
359
|
+
? operation.input && operation.input.completed === true ? 'route.clean-completion' : 'route.read'
|
|
360
|
+
: operation.intent;
|
|
361
|
+
}
|
|
12
362
|
function invalidContext(message) {
|
|
13
363
|
throw new Error(`TASK_CONTROL_CONTEXT_INVALID: ${message}`);
|
|
14
364
|
}
|
|
@@ -31,7 +381,11 @@ export function createDirectHostExecutionContext(params) {
|
|
|
31
381
|
mode: 'direct-host',
|
|
32
382
|
repoRoot: absolute('repoRoot', params.repoRoot),
|
|
33
383
|
...(params.runtimeDir === undefined ? {} : { runtimeDir: absolute('runtimeDir', params.runtimeDir) }),
|
|
34
|
-
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
|
|
384
|
+
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
|
|
385
|
+
...(params.lifecycleRecoveryAttestation === undefined ? {} : {
|
|
386
|
+
lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
|
|
387
|
+
? null : validateLifecycleRecoveryAttestation(params.lifecycleRecoveryAttestation)
|
|
388
|
+
})
|
|
35
389
|
};
|
|
36
390
|
}
|
|
37
391
|
export function createSandboxExecutorExecutionContext(params) {
|
|
@@ -53,7 +407,11 @@ export function createSandboxExecutorExecutionContext(params) {
|
|
|
53
407
|
manifestPath: absolute('manifestPath', params.manifestPath),
|
|
54
408
|
requestId: params.requestId,
|
|
55
409
|
...(params.diagnosticLog === undefined ? {} : { diagnosticLog: params.diagnosticLog }),
|
|
56
|
-
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) })
|
|
410
|
+
...(params.controllerBinding === undefined ? {} : { controllerBinding: binding(params.controllerBinding) }),
|
|
411
|
+
...(params.lifecycleRecoveryAttestation === undefined ? {} : {
|
|
412
|
+
lifecycleRecoveryAttestation: params.lifecycleRecoveryAttestation === null
|
|
413
|
+
? null : validateLifecycleRecoveryAttestation(params.lifecycleRecoveryAttestation)
|
|
414
|
+
})
|
|
57
415
|
};
|
|
58
416
|
}
|
|
59
417
|
export function assertTaskControlExecutionContext(context) {
|
|
@@ -64,6 +422,8 @@ export function assertTaskControlExecutionContext(context) {
|
|
|
64
422
|
if (context.runtimeDir !== undefined)
|
|
65
423
|
absolute('runtimeDir', context.runtimeDir);
|
|
66
424
|
binding(context.controllerBinding);
|
|
425
|
+
if (context.lifecycleRecoveryAttestation)
|
|
426
|
+
validateLifecycleRecoveryAttestation(context.lifecycleRecoveryAttestation);
|
|
67
427
|
return;
|
|
68
428
|
}
|
|
69
429
|
if (context.mode !== 'task-bound-sandbox' || !/^TASK-\d{8}-\d{6}$/u.test(context.taskId)
|
|
@@ -78,6 +438,8 @@ export function assertTaskControlExecutionContext(context) {
|
|
|
78
438
|
invalidContext('runtimeDir is not bound to the manifest control root');
|
|
79
439
|
}
|
|
80
440
|
binding(context.controllerBinding);
|
|
441
|
+
if (context.lifecycleRecoveryAttestation)
|
|
442
|
+
validateLifecycleRecoveryAttestation(context.lifecycleRecoveryAttestation);
|
|
81
443
|
}
|
|
82
444
|
function operationTaskId(context, taskRef) {
|
|
83
445
|
if (context.source === 'sandbox-executor' && taskRef !== context.taskId) {
|
|
@@ -166,12 +528,16 @@ function orchestration(context, operation) {
|
|
|
166
528
|
client: input.client,
|
|
167
529
|
requestedModel: input.requestedModel,
|
|
168
530
|
requestedReasoningEffort: input.requestedReasoningEffort,
|
|
169
|
-
...(input.
|
|
531
|
+
...(input.capabilityRef === undefined ? {} : { capabilityRef: input.capabilityRef }),
|
|
532
|
+
...(context.lifecycleRecoveryAttestation === undefined
|
|
533
|
+
? {}
|
|
534
|
+
: { lifecycleRecoveryAttestation: context.lifecycleRecoveryAttestation })
|
|
170
535
|
};
|
|
171
536
|
if (prepareInput.client === 'codex') {
|
|
172
537
|
return prepareCodexOrchestrationDelegation(operation.taskRef, prepareInput, {
|
|
173
538
|
repoRoot: context.repoRoot,
|
|
174
539
|
orchestrationOptions: options,
|
|
540
|
+
deferLifecycleRecoveryConsumption: context.source === 'sandbox-executor',
|
|
175
541
|
...(context.controllerBinding ? { controllerBinding: context.controllerBinding } : {})
|
|
176
542
|
});
|
|
177
543
|
}
|
|
@@ -256,7 +622,7 @@ const FINALIZATION_FLAGS = new Set(['--agent']);
|
|
|
256
622
|
const ORCHESTRATION_FLAGS = new Set([
|
|
257
623
|
'--agent', '--max-steps', '--executor-model', '--executor-reasoning-effort', '--reviewer-model',
|
|
258
624
|
'--reviewer-reasoning-effort', '--client', '--requested-model', '--requested-reasoning-effort',
|
|
259
|
-
'--capability-
|
|
625
|
+
'--capability-ref', '--parent-id', '--before-fingerprint', '--stage', '--round', '--artifact', '--role',
|
|
260
626
|
'--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
|
|
261
627
|
'--model-fallback-reason', '--reasoning-effort-fallback-reason', '--exit-code', '--after-fingerprint',
|
|
262
628
|
'--changed-paths', '--code', '--message', '--recoverable', '--git-worktree-root'
|
|
@@ -372,7 +738,7 @@ export function parseTaskControlOperation(family, args) {
|
|
|
372
738
|
if (parsedIntent === 'prepare') {
|
|
373
739
|
input.requestedModel = value(values, '--requested-model');
|
|
374
740
|
input.requestedReasoningEffort = value(values, '--requested-reasoning-effort');
|
|
375
|
-
input.
|
|
741
|
+
input.capabilityRef = value(values, '--capability-ref');
|
|
376
742
|
}
|
|
377
743
|
if (parsedIntent === 'await-activation') {
|
|
378
744
|
required(values, ['--stage', '--round', '--artifact', '--role']);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isCompletionEvidence } from './orchestration.js';
|
|
3
|
+
import { TASK_WORKFLOW_OPERATIONS } from './workflow-command.js';
|
|
4
|
+
/** Normalize the command result once at the control transport boundary. */
|
|
5
|
+
export function parseControlOutput(output) {
|
|
6
|
+
if (output === null)
|
|
7
|
+
return null;
|
|
8
|
+
try {
|
|
9
|
+
const value = JSON.parse(output);
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
return null;
|
|
12
|
+
return value.result && typeof value.result === 'object' && !Array.isArray(value.result) ? value.result : value;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES = Object.freeze([
|
|
19
|
+
'completed', 'evidence-written', 'publish-authorized'
|
|
20
|
+
]);
|
|
21
|
+
const LIFECYCLE_INTENTS = ['block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'];
|
|
22
|
+
const ORCHESTRATION_INTENTS = [
|
|
23
|
+
'begin-or-resume', 'route.read', 'route.clean-completion', 'status', 'prepare', 'dispatch',
|
|
24
|
+
'await-activation', 'recover-prepared', 'hook-start', 'hook-stop', 'advance', 'pause'
|
|
25
|
+
];
|
|
26
|
+
export const SANDBOX_CONTROL_RECOVERY_OPERATIONS = Object.freeze([
|
|
27
|
+
...LIFECYCLE_INTENTS.map((intent) => ({ family: 'task-lifecycle', intent, class: 'lifecycle-mutation' })),
|
|
28
|
+
{ family: 'task-finalization', intent: 'complete', class: 'finalization' },
|
|
29
|
+
...ORCHESTRATION_INTENTS.map((intent) => ({
|
|
30
|
+
family: 'task-orchestration',
|
|
31
|
+
intent,
|
|
32
|
+
class: intent === 'route.clean-completion' ? 'route.clean-completion' : intent === 'route.read' || intent === 'status' ? 'read-only' : 'orchestration'
|
|
33
|
+
})),
|
|
34
|
+
{ family: 'task-create', intent: 'create', class: 'task-create' },
|
|
35
|
+
...TASK_WORKFLOW_OPERATIONS.map((intent) => ({ family: 'task-workflow', intent, class: intent === 'artifact-inspect' || intent === 'decision-next-id' ? 'read-only' : 'workflow' })),
|
|
36
|
+
...['open', 'close', 'verify'].map((intent) => ({ family: 'codex-controller', intent, class: 'codex-controller' }))
|
|
37
|
+
]);
|
|
38
|
+
export function digestControlRecoveryIntent(family, intent) {
|
|
39
|
+
return createHash('sha256').update(`${family}\0${intent}`, 'utf8').digest('hex');
|
|
40
|
+
}
|
|
41
|
+
export function findSandboxControlRecoveryOperation(family, intent) {
|
|
42
|
+
return SANDBOX_CONTROL_RECOVERY_OPERATIONS.find((operation) => operation.family === family && operation.intent === intent) ?? null;
|
|
43
|
+
}
|
|
44
|
+
function bindingMatchesResult(binding, result) {
|
|
45
|
+
return result.requestId === binding.requestId
|
|
46
|
+
&& result.generation === binding.generation
|
|
47
|
+
&& (result.taskId === undefined || result.taskId === binding.taskId)
|
|
48
|
+
&& (result.intentDigest === undefined || result.intentDigest === binding.intentDigest);
|
|
49
|
+
}
|
|
50
|
+
function domainEvidenceMatches(operation, result, domain) {
|
|
51
|
+
if (!domain)
|
|
52
|
+
return false;
|
|
53
|
+
if (domain.consistent !== true)
|
|
54
|
+
return false;
|
|
55
|
+
if (operation.class === 'route.clean-completion') {
|
|
56
|
+
const completion = domain.completionEvidence;
|
|
57
|
+
const snapshot = domain.snapshot;
|
|
58
|
+
const lastReviewedCommit = domain.lastReviewedCommit;
|
|
59
|
+
return result.status === 'completed'
|
|
60
|
+
&& domain.status === 'completed'
|
|
61
|
+
&& domain.pendingDelegation === null
|
|
62
|
+
&& isCompletionEvidence(completion)
|
|
63
|
+
&& snapshot?.head === completion.head
|
|
64
|
+
&& snapshot.headTree === completion.headTree
|
|
65
|
+
&& snapshot.worktreeTree === completion.worktreeTree
|
|
66
|
+
&& lastReviewedCommit === completion.lastReviewedCommit;
|
|
67
|
+
}
|
|
68
|
+
if (operation.class === 'read-only')
|
|
69
|
+
return result.changed === false && domain.snapshotValid === true;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
export function classifySandboxControlRecovery(input) {
|
|
73
|
+
const result = input.terminalResult ?? null;
|
|
74
|
+
if (input.evidenceConflict)
|
|
75
|
+
return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_EVIDENCE_CONFLICT' };
|
|
76
|
+
if (input.explicitRejection && !input.startedCommitted) {
|
|
77
|
+
return { outcome: 'not-executed', responseReconstructable: true, reasonCode: 'RECOVERY_EXPLICIT_REJECTION' };
|
|
78
|
+
}
|
|
79
|
+
if (!input.startedCommitted) {
|
|
80
|
+
return { outcome: 'not-executed', responseReconstructable: false, reasonCode: 'RECOVERY_NOT_STARTED' };
|
|
81
|
+
}
|
|
82
|
+
if (!result)
|
|
83
|
+
return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_TERMINAL_RESULT_MISSING' };
|
|
84
|
+
if (!bindingMatchesResult(input.binding, result)) {
|
|
85
|
+
return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_RESULT_BINDING_MISMATCH' };
|
|
86
|
+
}
|
|
87
|
+
if (!SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES.every((phase) => input.criticalPhases?.includes(phase))) {
|
|
88
|
+
return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_CRITICAL_AUDIT_INCOMPLETE' };
|
|
89
|
+
}
|
|
90
|
+
const status = result.status;
|
|
91
|
+
const failed = status === 'failed' || result.error !== null && result.error !== undefined;
|
|
92
|
+
if (failed) {
|
|
93
|
+
if (input.domain && input.domain.consistent === false) {
|
|
94
|
+
return { outcome: 'failure', responseReconstructable: true, reasonCode: 'RECOVERY_TERMINAL_FAILURE' };
|
|
95
|
+
}
|
|
96
|
+
return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_FAILURE_EVIDENCE_INCOMPLETE' };
|
|
97
|
+
}
|
|
98
|
+
if (result.changed === null) {
|
|
99
|
+
return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_RESULT_CHANGE_FLAG_MISSING' };
|
|
100
|
+
}
|
|
101
|
+
if (!domainEvidenceMatches(input.operation, result, input.domain)) {
|
|
102
|
+
if (input.journal?.exists && (input.journal.completedSteps?.length ?? 0) > 0) {
|
|
103
|
+
return { outcome: 'in-progress', responseReconstructable: false, reasonCode: 'RECOVERY_DOMAIN_EVIDENCE_INCOMPLETE' };
|
|
104
|
+
}
|
|
105
|
+
return { outcome: 'unknown', responseReconstructable: false, reasonCode: 'RECOVERY_DOMAIN_EVIDENCE_MISSING' };
|
|
106
|
+
}
|
|
107
|
+
if (input.operation.family === 'task-lifecycle' && input.journal?.exists) {
|
|
108
|
+
if (input.journal.failure) {
|
|
109
|
+
return { outcome: 'failure', responseReconstructable: true, reasonCode: 'RECOVERY_LIFECYCLE_JOURNAL_FAILURE' };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
outcome: input.journal.completedSteps && input.journal.completedSteps.length > 0 ? 'in-progress' : 'unknown',
|
|
113
|
+
responseReconstructable: false,
|
|
114
|
+
reasonCode: 'RECOVERY_LIFECYCLE_JOURNAL_PRESENT'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
outcome: 'success',
|
|
119
|
+
responseReconstructable: true,
|
|
120
|
+
reasonCode: input.operation.class === 'route.clean-completion' && result.changed === false
|
|
121
|
+
? 'RECOVERY_ROUTE_COMPLETION_NOOP'
|
|
122
|
+
: 'RECOVERY_TERMINAL_AND_DOMAIN_MATCH'
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function operationRecoveryBinding(requestId, generation, taskId, family, intent) {
|
|
126
|
+
return { requestId, generation, taskId, intentDigest: digestControlRecoveryIntent(family, intent) };
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=control-recovery.js.map
|