@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.
Files changed (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -1,3 +1,5 @@
1
+ import { finalizationTerminalResponse } from './finalization-response.js';
2
+ import { isDeepStrictEqual } from 'node:util';
1
3
  import { completedReentryView } from './completed-reentry.js';
2
4
  import fs from 'node:fs';
3
5
  import path from 'node:path';
@@ -6,65 +8,125 @@ import { getProcessStartTime } from '../../server/process-state.js';
6
8
  import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
7
9
  import { controlError, DEFAULT_SANDBOX_CONTROL_TIMING, SANDBOX_CONTROL_MAX_RESPONSE_BYTES, SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES } from './protocol.js';
8
10
  import { prepareSandboxControlExecution } from './executor.js';
9
- import { appendSandboxControlAudit, atomicWriteJsonNoReplace, createSandboxControlPayload, cleanupStaleSandboxControlLease, executionPath, readJsonFile, readActiveLease, readExecution, readSandboxControlPayload, readSandboxControlResultEvidence, sandboxControlEncodedJsonBytes, sandboxControlGenerationUsage, sanitizeSandboxControlOutput, sanitizeSandboxControlResult, payloadPath, writeSandboxControlPayload, writeSandboxControlReservation, writeSandboxControlResultEvidence, terminateSandboxControlExecution, writeSandboxControlStatus, readSandboxControlStatus } from './state.js';
11
+ import { atomicWriteJsonNoReplace, createSandboxControlPayload, cleanupStaleSandboxControlLease, executionPath, readJsonFile, readActiveLease, readExecution, readSandboxControlPayload, readSandboxControlResultEvidence, readSandboxControlTerminalResult, createSandboxControlTerminalResult, sandboxControlEncodedJsonBytes, sandboxControlGenerationUsage, sanitizeSandboxControlOutput, sanitizeSandboxControlResult, payloadPath, writeSandboxControlPayload, writeSandboxControlReservation, writeSandboxControlResultEvidence, writeSandboxControlTerminalResult, appendSandboxControlAudit, terminateSandboxControlExecution, writeSandboxControlStatus, readSandboxControlStatus } from './state.js';
10
12
  import { validateSandboxControlRequest } from './protocol.js';
13
+ import { requestAuditFields, resultAuditFields, appendCriticalAudit, appendDiagnosticAudit, createSandboxControlAuditContext, writeSandboxControlTransition, readSandboxControlTransition } from './audit.js';
14
+ import { parseTaskControlOperation } from '../../task/control-authority.js';
15
+ import { parseControlOutput, classifySandboxControlRecovery, findSandboxControlRecoveryOperation, operationRecoveryBinding, SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES } from '../../task/control-recovery.js';
16
+ import { readRun } from '../../task/orchestration.js';
17
+ import { captureRepositorySnapshot } from '../../task/workspace-snapshot.js';
18
+ import { parseTypedTaskFrontmatter } from '../../task/frontmatter.js';
19
+ import { readLifecycleJournalEvidence } from '../../task/lifecycle.js';
20
+ import { resolveTaskRef } from '../../task/resolve-ref.js';
21
+ import { loadShortIdByTaskId } from '../../task/short-id.js';
11
22
  import { inspectSandboxControlContainer } from './container-identity.js';
12
23
  import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './lifecycle.js';
13
24
  import { nextSandboxControlBackoff } from './timing.js';
14
25
  import { readTaskFinalizationReceipt } from '../../task/finalization.js';
15
- import { resolveTaskRef } from '../../task/resolve-ref.js';
26
+ import { readCodexControllerRegistration } from './controller-registration.js';
27
+ import { validateSandboxControlIdentity } from './identity-sentinel.js';
16
28
  import { mergeSandboxTaskView, taskViewAfterFinalization, taskViewForManifest } from './task-view.js';
17
29
  import { taskCreateOutputUnavailableResult } from '../../task/create-service.js';
18
- function safeRealpath(value) {
30
+ import { consumeLifecycleRecoveryAttestation, issueLifecycleRecoveryAttestation, recoverLifecycleRecoveryOperation } from '../../task/control-authority.js';
31
+ import { createCodexCapabilityStore } from '../../agent-clients/adapters/codex-lifecycle/capability-store.js';
32
+ import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
33
+ function consumeLifecycleAuthorityForResult(manifest, execution) {
34
+ const authority = execution.lifecycleAuthority?.attestation;
35
+ if (!authority || execution.result?.exitCode !== 0)
36
+ return true;
19
37
  try {
20
- return fs.realpathSync.native(value);
38
+ consumeLifecycleRecoveryAttestation(authority);
39
+ return true;
40
+ }
41
+ catch (error) {
42
+ appendBrokerAudit(manifest, 'lifecycle-authority-consume-retry', {
43
+ ...requestAuditFields(manifest, execution.request),
44
+ errorCode: error instanceof Error ? error.name : 'LIFECYCLE_AUTHORITY_CONSUME_FAILED'
45
+ });
46
+ return false;
47
+ }
48
+ }
49
+ function lifecycleAuthorityForRequest(manifest, manifestPath, request) {
50
+ const selector = request.family === 'task-workflow' || request.family === 'task-orchestration'
51
+ ? request.authority
52
+ : undefined;
53
+ if (!selector)
54
+ return undefined;
55
+ let controllerBinding = null;
56
+ try {
57
+ const registration = readCodexControllerRegistration(manifestPath);
58
+ controllerBinding = {
59
+ instanceDigest: registration.controllerInstanceDigest,
60
+ controlGeneration: registration.controlGeneration
61
+ };
21
62
  }
22
63
  catch {
23
- return null;
64
+ // The issuer reports the stable unavailable result; the broker never trusts
65
+ // a controller binding supplied by the request.
24
66
  }
67
+ return issueLifecycleRecoveryAttestation(selector, {
68
+ controllerBinding,
69
+ capabilityStore: createCodexCapabilityStore(),
70
+ buildIdentity: computeLifecycleBuildIdentity(manifest.repoRoot)
71
+ });
25
72
  }
26
73
  function appendBrokerAudit(manifest, event, fields = {}) {
74
+ appendDiagnosticAudit(manifest, event, { source: 'broker', ...fields });
75
+ }
76
+ function operationKey(request, output) {
77
+ if (request.family === 'task-finalization')
78
+ return request.operation;
79
+ if (request.family === 'task-create')
80
+ return 'create';
81
+ if (request.family === 'codex-controller')
82
+ return request.command;
83
+ if (request.family === 'task-workflow')
84
+ return request.workflow.operation;
85
+ if (request.family !== 'task-lifecycle' && request.family !== 'task-orchestration')
86
+ return null;
87
+ if (request.family === 'task-orchestration' && request.args[1] === 'route') {
88
+ if (!output)
89
+ return 'route';
90
+ const result = parseControlOutput(output);
91
+ return result?.changed === true && result.status === 'completed' ? 'route.clean-completion' : 'route.read';
92
+ }
27
93
  try {
28
- appendSandboxControlAudit(manifest, event, { source: 'broker', ...fields });
94
+ const operation = parseTaskControlOperation(request.family, request.args);
95
+ return operation.family === 'task-orchestration' ? operation.intent : operation.request.intent;
29
96
  }
30
97
  catch {
31
- // Diagnostics must never change the control protocol.
98
+ return null;
32
99
  }
33
100
  }
34
- function requestAuditFields(manifest, manifestPath, request) {
35
- const args = 'args' in request ? request.args : [];
36
- const encodedArgs = JSON.stringify(args);
37
- return {
38
- requestId: request.id,
39
- requestFamily: request.family,
40
- sandboxTaskId: manifest.taskId,
41
- requestGeneration: request.generation,
42
- requestIssuedAt: request.issuedAt,
43
- requestExpiresAt: request.expiresAt,
44
- requestArgCount: args.length,
45
- requestArgsSha256: createHash('sha256').update(encodedArgs, 'utf8').digest('hex'),
46
- requestTaskRef: args[0] ?? null,
47
- requestCommand: args[1] ?? null,
48
- controllerProofPresent: request.controllerProof !== null,
49
- hostCwd: process.cwd(),
50
- manifestPath,
51
- manifestPathRealpath: safeRealpath(manifestPath),
52
- repoRoot: manifest.repoRoot,
53
- repoRootRealpath: safeRealpath(manifest.repoRoot),
54
- worktreeRoot: manifest.worktreeRoot,
55
- worktreeRootRealpath: safeRealpath(manifest.worktreeRoot),
56
- runtimeDir: manifest.runtimeDir,
57
- runtimeDirRealpath: safeRealpath(manifest.runtimeDir)
58
- };
101
+ function recoveryOperationKey(request, terminalResult, output) {
102
+ const operation = operationKey(request, output);
103
+ if (operation !== 'route')
104
+ return operation;
105
+ const digest = terminalResult?.intentDigest;
106
+ for (const candidate of ['route.read', 'route.clean-completion']) {
107
+ const expected = operationRecoveryBinding(request.id, request.generation, null, request.family, candidate).intentDigest;
108
+ if (digest === expected)
109
+ return candidate;
110
+ }
111
+ return null;
59
112
  }
60
- function resultAuditFields(result) {
61
- return {
62
- exitCode: result.exitCode,
63
- stdoutBytes: Buffer.byteLength(result.stdout, 'utf8'),
64
- stderrBytes: Buffer.byteLength(result.stderr, 'utf8'),
65
- stdoutSha256: createHash('sha256').update(result.stdout, 'utf8').digest('hex'),
66
- stderrSha256: createHash('sha256').update(result.stderr, 'utf8').digest('hex')
67
- };
113
+ function terminalResultMatchesRequest(request, terminalResult) {
114
+ const operation = operationKey(request);
115
+ const candidates = operation === 'route'
116
+ ? ['route.read', 'route.clean-completion']
117
+ : operation ? [operation] : [];
118
+ return candidates.some((candidate) => createHash('sha256').update(`${request.family}\0${candidate}`, 'utf8').digest('hex') === terminalResult.intentDigest);
119
+ }
120
+ function criticalRequestPhase(manifest, request, phase, outcome, reference) {
121
+ const context = createSandboxControlAuditContext(manifest, {
122
+ requestId: request.id,
123
+ family: request.family,
124
+ operation: operationKey(request),
125
+ phase,
126
+ outcome
127
+ });
128
+ appendCriticalAudit(manifest, context, { reference: reference ?? null });
129
+ writeSandboxControlTransition(manifest, { requestId: request.id, phase, reference });
68
130
  }
69
131
  function assertRealDirectory(directory, parent) {
70
132
  const stat = fs.lstatSync(directory);
@@ -146,33 +208,7 @@ function finalizationRecoveryResponse(manifest, requestId, exitCode) {
146
208
  catch {
147
209
  return { status: 'deferred' };
148
210
  }
149
- const pendingSteps = [
150
- receipt.taskComment === 'pending' ? 'task-comment' : null,
151
- receipt.verification === 'pending' ? 'verification' : null
152
- ].filter((step) => step !== null);
153
- const completedSteps = ['lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment', receipt.verification === 'pending' ? null : 'verification']
154
- .filter((step) => step !== null);
155
- const warnings = receipt.warnings
156
- .filter((warning) => warning.status === 'open')
157
- .map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning);
158
- const result = {
159
- status: 'completed', changed: false, taskId: manifest.taskId,
160
- lifecycle: { status: 'no-op', changed: false, error: null },
161
- taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
162
- verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
163
- completedSteps, pendingSteps,
164
- result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
165
- ? 'completed_with_warnings' : 'completed',
166
- warnings, error: null
167
- };
168
- return {
169
- status: 'matched',
170
- response: {
171
- version: 2, id: requestId, phase: 'completed', exitCode: 0,
172
- stdout: `${JSON.stringify({ version: 1, status: 'completed', changed: false, accepted: true, result, error: null })}\n`,
173
- stderr: '', error: null
174
- }
175
- };
211
+ return { status: 'matched', response: finalizationTerminalResponse(manifest.taskId, requestId, receipt) };
176
212
  }
177
213
  function publishFinalizationTaskView(manifest, broker, requestId, state, reasonCode, activeRequestId) {
178
214
  let view;
@@ -236,16 +272,244 @@ function genericRecoveryResponse(request, exitCode, payload, cause = 'recovery')
236
272
  payload: payload ? payloadReference(payload) : null
237
273
  };
238
274
  }
239
- function recoveryResponse(manifest, request, evidence, payload) {
275
+ function taskCreateDomainEvidence(manifest, output) {
276
+ if (!output || !['applied', 'no-op', 'degraded'].includes(String(output.status))) {
277
+ return { consistent: false };
278
+ }
279
+ const task = output.task && typeof output.task === 'object' && !Array.isArray(output.task)
280
+ ? output.task : null;
281
+ const taskId = typeof task?.id === 'string' ? task.id : null;
282
+ const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
283
+ if (!taskId || !shortId)
284
+ return { consistent: false };
285
+ try {
286
+ const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
287
+ const shortIds = loadShortIdByTaskId(manifest.repoRoot);
288
+ return {
289
+ consistent: resolved.ok && resolved.taskId === taskId && resolved.state === 'active' && shortIds.get(taskId) === shortId,
290
+ taskId,
291
+ shortId
292
+ };
293
+ }
294
+ catch {
295
+ return { consistent: false };
296
+ }
297
+ }
298
+ function controllerDomainEvidence(manifest, manifestPath, request, output) {
299
+ if (!output || output.error !== null || typeof output.status !== 'string')
300
+ return { consistent: false };
301
+ try {
302
+ const registration = readCodexControllerRegistration(manifestPath);
303
+ if (request.family !== 'codex-controller')
304
+ return { consistent: false };
305
+ if (request.command === 'open') {
306
+ const lease = output.lease && typeof output.lease === 'object' && !Array.isArray(output.lease)
307
+ ? output.lease : null;
308
+ return {
309
+ consistent: output.status === 'opened' && output.changed === true
310
+ && lease?.taskId === registration.taskId
311
+ && lease.controlGeneration === registration.controlGeneration
312
+ && lease.controllerInstanceDigest === registration.controllerInstanceDigest
313
+ && registration.taskId === manifest.taskId
314
+ && registration.controlGeneration === manifest.generation
315
+ };
316
+ }
317
+ if (request.command === 'verify') {
318
+ const binding = output.binding && typeof output.binding === 'object' && !Array.isArray(output.binding)
319
+ ? output.binding : null;
320
+ return {
321
+ consistent: output.status === 'verified' && output.changed === false
322
+ && binding?.taskId === registration.taskId
323
+ && binding.controlGeneration === registration.controlGeneration
324
+ && binding.controllerInstanceDigest === registration.controllerInstanceDigest
325
+ };
326
+ }
327
+ return { consistent: false };
328
+ }
329
+ catch (error) {
330
+ return { consistent: request.family === 'codex-controller' && request.command === 'close'
331
+ && error.code === 'CODEX_SANDBOX_CONTROLLER_REGISTRATION_MISSING'
332
+ && output?.status === 'closed' && typeof output.changed === 'boolean' };
333
+ }
334
+ }
335
+ function orchestrationDomainEvidence(operation, terminalResult, output, run) {
336
+ if (!operation || !output)
337
+ return { consistent: false };
338
+ const outputRun = output.run && typeof output.run === 'object' && !Array.isArray(output.run)
339
+ ? output.run : null;
340
+ const runMatches = outputRun === null ? run === null : run !== null && JSON.stringify(run) === JSON.stringify(outputRun);
341
+ if (operation.class === 'read-only') {
342
+ return {
343
+ consistent: output.status === terminalResult.status && output.changed === terminalResult.changed && runMatches,
344
+ snapshotValid: true,
345
+ status: run?.status ?? terminalResult.status,
346
+ pendingDelegation: run?.pendingDelegation ?? null
347
+ };
348
+ }
349
+ if (!run || !outputRun)
350
+ return { consistent: false };
351
+ return {
352
+ consistent: output.status === terminalResult.status && output.changed === terminalResult.changed && runMatches,
353
+ status: run.status,
354
+ pendingDelegation: run.pendingDelegation
355
+ };
356
+ }
357
+ function readRecoveryDomain(manifest, manifestPath, request, operation, terminalResult, payloadOutput) {
358
+ if (!operation)
359
+ return { domain: null };
360
+ const taskRef = request.family === 'task-finalization' || request.family === 'task-workflow'
361
+ ? manifest.taskId
362
+ : 'args' in request ? request.args[0] ?? null : manifest.taskId;
363
+ const output = parseControlOutput(payloadOutput);
364
+ if (!taskRef && operation.family !== 'task-create' && operation.family !== 'codex-controller') {
365
+ return { domain: null };
366
+ }
367
+ if (operation.family === 'task-lifecycle') {
368
+ if (!taskRef || !terminalResult.targetState)
369
+ return { domain: null };
370
+ const journal = readLifecycleJournalEvidence(manifest.repoRoot, taskRef);
371
+ try {
372
+ const resolved = resolveTaskRef(taskRef, { repoRoot: manifest.repoRoot });
373
+ const shortIds = loadShortIdByTaskId(manifest.repoRoot);
374
+ const shortIdMatches = terminalResult.targetState === 'active'
375
+ ? shortIds.has(resolved.ok ? resolved.taskId : taskRef)
376
+ : !shortIds.has(resolved.ok ? resolved.taskId : taskRef);
377
+ return {
378
+ domain: { consistent: resolved.ok && resolved.state === terminalResult.targetState && shortIdMatches },
379
+ journal
380
+ };
381
+ }
382
+ catch {
383
+ return { domain: { consistent: false }, journal };
384
+ }
385
+ }
386
+ if (operation.family === 'task-orchestration') {
387
+ if (!taskRef)
388
+ return { domain: null };
389
+ try {
390
+ const resolved = resolveTaskRef(taskRef, { repoRoot: manifest.repoRoot });
391
+ if (!resolved.ok)
392
+ return { domain: { consistent: false } };
393
+ const run = readRun(resolved.taskDir);
394
+ if (!run && operation.class !== 'read-only')
395
+ return { domain: { consistent: false } };
396
+ if (operation.class === 'route.clean-completion') {
397
+ const snapshot = captureRepositorySnapshot(manifest.repoRoot);
398
+ const metadata = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
399
+ const completion = run?.completionEvidence ?? null;
400
+ const consistent = run !== null && completion !== null
401
+ && terminalResult.completionEvidence !== null
402
+ && isDeepStrictEqual(completion, terminalResult.completionEvidence);
403
+ return {
404
+ domain: {
405
+ consistent,
406
+ status: run?.status ?? null,
407
+ pendingDelegation: run?.pendingDelegation ?? null,
408
+ completionEvidence: completion,
409
+ snapshot,
410
+ lastReviewedCommit: metadata.last_reviewed_commit
411
+ }
412
+ };
413
+ }
414
+ return { domain: orchestrationDomainEvidence(operation, terminalResult, output, run) };
415
+ }
416
+ catch {
417
+ return { domain: { consistent: false } };
418
+ }
419
+ }
420
+ if (operation.family === 'task-create') {
421
+ return { domain: taskCreateDomainEvidence(manifest, output) };
422
+ }
423
+ if (operation.family === 'codex-controller') {
424
+ return { domain: controllerDomainEvidence(manifest, manifestPath, request, output) };
425
+ }
426
+ if (operation.family === 'task-workflow') {
427
+ return {
428
+ domain: { consistent: output?.status === terminalResult.status && output.changed === terminalResult.changed, snapshotValid: true }
429
+ };
430
+ }
431
+ return { domain: null };
432
+ }
433
+ function readCommittedCriticalPhases(manifest, requestId) {
434
+ return SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES.filter((phase) => {
435
+ try {
436
+ readSandboxControlTransition(manifest, requestId, phase);
437
+ return true;
438
+ }
439
+ catch {
440
+ return false;
441
+ }
442
+ });
443
+ }
444
+ function recoveryResponse(manifest, manifestPath, request, evidence, payload, terminalResult) {
445
+ const operationName = recoveryOperationKey(request, terminalResult, payload?.stdout);
446
+ const operation = operationName ? findSandboxControlRecoveryOperation(request.family, operationName) : null;
447
+ if (!operation)
448
+ return null;
449
+ if (payload) {
450
+ const payloadTerminal = createSandboxControlTerminalResult(manifest, {
451
+ id: request.id, family: request.family, operation: operationName
452
+ }, payload.stdout);
453
+ if (JSON.stringify(payloadTerminal) !== JSON.stringify(terminalResult))
454
+ return unknown(request.id);
455
+ }
456
+ const finalization = request.family === 'task-finalization' ? finalizationRecoveryResponse(manifest, request.id, 0) : null;
457
+ if (evidence.exitCode === 0 && finalization?.status === 'deferred')
458
+ return null;
459
+ if (evidence.exitCode === 0 && request.family === 'task-workflow' && request.authority?.phase === 'task-event.completed') {
460
+ try {
461
+ const registration = readCodexControllerRegistration(manifestPath);
462
+ const recovered = recoverLifecycleRecoveryOperation(request.authority, {
463
+ repoRoot: manifest.repoRoot,
464
+ capabilityStore: createCodexCapabilityStore(),
465
+ buildIdentity: computeLifecycleBuildIdentity(manifest.repoRoot),
466
+ controllerBinding: {
467
+ instanceDigest: registration.controllerInstanceDigest,
468
+ controlGeneration: registration.controlGeneration
469
+ }
470
+ });
471
+ if (recovered.status !== 'committed')
472
+ return null;
473
+ }
474
+ catch (error) {
475
+ appendBrokerAudit(manifest, 'lifecycle-authority-recovery-failed', {
476
+ ...requestAuditFields(manifest, request),
477
+ errorCode: error instanceof Error ? error.name : 'LIFECYCLE_AUTHORITY_RECOVERY_FAILED'
478
+ });
479
+ return null;
480
+ }
481
+ }
482
+ const recovery = finalization
483
+ ? { domain: { consistent: finalization.status === 'matched' } }
484
+ : readRecoveryDomain(manifest, manifestPath, request, operation, terminalResult, payload?.stdout ?? null);
485
+ const binding = operationRecoveryBinding(request.id, manifest.generation, manifest.taskId, request.family, operationName);
486
+ const decision = classifySandboxControlRecovery({
487
+ operation,
488
+ binding,
489
+ startedCommitted: true,
490
+ terminalResult,
491
+ criticalPhases: readCommittedCriticalPhases(manifest, request.id),
492
+ domain: recovery.domain,
493
+ journal: recovery.journal
494
+ });
495
+ if (decision.outcome === 'unknown' || decision.outcome === 'in-progress')
496
+ return unknown(request.id);
497
+ if (decision.outcome === 'not-executed')
498
+ return notExecuted(request.id);
499
+ if (decision.outcome !== 'success' && decision.outcome !== 'failure')
500
+ return null;
240
501
  if (request.family === 'task-finalization' && evidence.exitCode === 0) {
241
- const finalization = finalizationRecoveryResponse(manifest, request.id, evidence.exitCode);
242
- return finalization.status === 'matched' ? finalization.response ?? null : null;
502
+ if (finalization?.status !== 'matched')
503
+ return null;
504
+ return finalization.response ?? null;
243
505
  }
244
506
  return genericRecoveryResponse(request, evidence.exitCode, payload);
245
507
  }
246
- function terminalMatchesEvidence(manifest, request, response, evidence, payload, payloadInvalid) {
508
+ function terminalMatchesEvidence(manifest, manifestPath, request, response, evidence, payload, payloadInvalid, terminalResult) {
247
509
  if (request.family === 'task-finalization' && evidence.exitCode === 0) {
248
- const expected = recoveryResponse(manifest, request, evidence, payload);
510
+ if (!terminalResult)
511
+ return { valid: false, payloadReferenced: false };
512
+ const expected = recoveryResponse(manifest, manifestPath, request, evidence, payload, terminalResult);
249
513
  return {
250
514
  valid: expected !== null && JSON.stringify(response) === JSON.stringify(expected),
251
515
  payloadReferenced: false
@@ -286,6 +550,16 @@ function terminalMatchesEvidence(manifest, request, response, evidence, payload,
286
550
  }
287
551
  function publishExecutionResult(manifest, request, result, broker, brokerOwns) {
288
552
  const normalized = sanitizeSandboxControlResult(manifest, result);
553
+ writeSandboxControlTerminalResult(manifest, {
554
+ id: request.id,
555
+ family: request.family,
556
+ operation: operationKey(request, normalized.stdout)
557
+ }, normalized.stdout);
558
+ if (fs.existsSync(path.join(manifest.processingDir, request.id, 'transitions'))) {
559
+ criticalRequestPhase(manifest, request, 'completed', normalized.exitCode === 0 ? 'success' : 'failure');
560
+ criticalRequestPhase(manifest, request, 'evidence-written', normalized.exitCode === 0 ? 'success' : 'failure');
561
+ criticalRequestPhase(manifest, request, 'publish-authorized', normalized.exitCode === 0 ? 'success' : 'failure');
562
+ }
289
563
  let terminal = null;
290
564
  if (request.family === 'task-finalization') {
291
565
  const finalization = finalizationRecoveryResponse(manifest, request.id, normalized.exitCode);
@@ -326,7 +600,11 @@ function publishExecutionResult(manifest, request, result, broker, brokerOwns) {
326
600
  if (view.state === 'unknown')
327
601
  return false;
328
602
  }
329
- return writeSandboxControlResponse(manifest, terminal);
603
+ const committed = writeSandboxControlResponse(manifest, terminal);
604
+ if (committed && fs.existsSync(path.join(manifest.processingDir, request.id, 'transitions'))) {
605
+ criticalRequestPhase(manifest, request, 'published-committed', normalized.exitCode === 0 ? 'success' : 'failure');
606
+ }
607
+ return committed;
330
608
  }
331
609
  function writeAcceptedResponse(manifest, response) {
332
610
  atomicWriteJsonNoReplace(acceptedResponsePath(manifest, response.id), response);
@@ -386,7 +664,20 @@ function bindingReason(manifest) {
386
664
  return 'SANDBOX_WORKTREE_BINDING_LOST';
387
665
  }
388
666
  }
389
- function recoverProcessing(manifest, broker, brokerOwns) {
667
+ function assertCurrentSandboxControlIdentity(manifest, manifestPath) {
668
+ const result = validateSandboxControlIdentity({
669
+ publicStatusDir: manifest.publicStatusDir,
670
+ root: path.dirname(path.resolve(manifestPath)),
671
+ mode: manifest.mode,
672
+ taskId: manifest.taskId,
673
+ generation: manifest.generation,
674
+ controlRootId: manifest.controlRootId
675
+ });
676
+ if (result.state !== 'valid') {
677
+ throw new Error(`SANDBOX_CONTROL_IDENTITY_${result.state.replaceAll('-', '_').toUpperCase()}`);
678
+ }
679
+ }
680
+ function recoverProcessing(manifest, manifestPath, broker, brokerOwns) {
390
681
  for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
391
682
  if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name))
392
683
  continue;
@@ -395,14 +686,19 @@ function recoverProcessing(manifest, broker, brokerOwns) {
395
686
  const descriptor = executionPath(manifest, entry.name);
396
687
  const existingResponse = responsePath(manifest, entry.name);
397
688
  const processingDirectory = path.join(manifest.processingDir, entry.name);
689
+ const transitionsDirectory = path.join(processingDirectory, 'transitions');
690
+ const transitionProtocolActive = fs.existsSync(transitionsDirectory);
691
+ const startedCommitted = fs.existsSync(path.join(transitionsDirectory, 'started-committed.json'));
398
692
  let payloadReferenced = false;
399
693
  if (fs.existsSync(descriptor)) {
400
694
  const execution = readExecution(descriptor);
401
695
  let resultEvidence = null;
696
+ let terminalResult = null;
402
697
  let payload = null;
403
698
  let payloadInvalid = false;
404
699
  let request = null;
405
700
  let existingTerminalResponse = null;
701
+ let terminalReconciled = false;
406
702
  try {
407
703
  const rawRequest = JSON.parse(fs.readFileSync(path.join(processingDirectory, 'request.json'), 'utf8'));
408
704
  request = validateSandboxControlRequest(rawRequest, manifest, {
@@ -424,6 +720,19 @@ function recoverProcessing(manifest, broker, brokerOwns) {
424
720
  resultEvidence = null;
425
721
  }
426
722
  }
723
+ const terminalResultPathValue = path.join(processingDirectory, 'terminal-result.json');
724
+ if (fs.existsSync(terminalResultPathValue)) {
725
+ try {
726
+ terminalResult = readSandboxControlTerminalResult(terminalResultPathValue);
727
+ if (terminalResult.requestId !== entry.name || terminalResult.generation !== manifest.generation
728
+ || !request || !terminalResultMatchesRequest(request, terminalResult)) {
729
+ terminalResult = null;
730
+ }
731
+ }
732
+ catch {
733
+ terminalResult = null;
734
+ }
735
+ }
427
736
  const publishedPayloadPath = payloadPath(manifest, entry.name);
428
737
  if (fs.existsSync(publishedPayloadPath)) {
429
738
  try {
@@ -437,12 +746,24 @@ function recoverProcessing(manifest, broker, brokerOwns) {
437
746
  payloadInvalid = true;
438
747
  }
439
748
  }
749
+ if (transitionProtocolActive && startedCommitted && !terminalResult) {
750
+ if (!brokerOwns())
751
+ return false;
752
+ if (!terminateSandboxControlExecution(execution)) {
753
+ throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
754
+ }
755
+ appendDiagnosticAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
756
+ writeSandboxControlResponse(manifest, unknown(entry.name));
757
+ continue;
758
+ }
759
+ if (transitionProtocolActive && !startedCommitted && terminalResult)
760
+ continue;
440
761
  if (!terminateSandboxControlExecution(execution)) {
441
762
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
442
763
  }
443
764
  if (!brokerOwns())
444
765
  return false;
445
- appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
766
+ appendDiagnosticAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
446
767
  let terminal = false;
447
768
  if (fs.existsSync(existingResponse)) {
448
769
  try {
@@ -476,9 +797,12 @@ function recoverProcessing(manifest, broker, brokerOwns) {
476
797
  if (terminal && (!request || !resultEvidence))
477
798
  continue;
478
799
  if (terminal && request && resultEvidence) {
479
- const reconciliation = terminalMatchesEvidence(manifest, request, existingTerminalResponse, resultEvidence, payload, payloadInvalid);
800
+ if (!terminalResult && request.family !== 'task-create')
801
+ continue;
802
+ const reconciliation = terminalMatchesEvidence(manifest, manifestPath, request, existingTerminalResponse, resultEvidence, payload, payloadInvalid, terminalResult);
480
803
  if (!reconciliation.valid)
481
804
  continue;
805
+ terminalReconciled = true;
482
806
  payloadReferenced = reconciliation.payloadReferenced;
483
807
  if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
484
808
  const view = publishFinalizationTaskView(manifest, broker, request.id, 'starting', null, null);
@@ -486,12 +810,29 @@ function recoverProcessing(manifest, broker, brokerOwns) {
486
810
  continue;
487
811
  }
488
812
  }
813
+ if (!terminalResult && !terminalReconciled) {
814
+ if (terminal)
815
+ continue;
816
+ if (!brokerOwns())
817
+ return false;
818
+ if (!resultEvidence || !request || payloadInvalid)
819
+ continue;
820
+ if (request.family === 'task-create' && resultEvidence.exitCode === 0 && payload === null) {
821
+ writeSandboxControlResponse(manifest, genericRecoveryResponse(request, resultEvidence.exitCode, null));
822
+ terminalReconciled = true;
823
+ terminal = true;
824
+ }
825
+ else {
826
+ writeSandboxControlResponse(manifest, unknown(entry.name));
827
+ continue;
828
+ }
829
+ }
489
830
  if (!terminal) {
490
831
  if (!brokerOwns())
491
832
  return false;
492
833
  if (!resultEvidence || !request || payloadInvalid)
493
834
  continue;
494
- const recovered = recoveryResponse(manifest, request, resultEvidence, payload);
835
+ const recovered = recoveryResponse(manifest, manifestPath, request, resultEvidence, payload, terminalResult);
495
836
  if (!recovered)
496
837
  continue;
497
838
  if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
@@ -500,12 +841,22 @@ function recoverProcessing(manifest, broker, brokerOwns) {
500
841
  continue;
501
842
  }
502
843
  writeSandboxControlResponse(manifest, recovered);
844
+ const preserveRecoveryEvidence = recovered.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN';
845
+ if (transitionProtocolActive && !preserveRecoveryEvidence) {
846
+ writeSandboxControlTransition(manifest, { requestId: entry.name, phase: 'recovered' });
847
+ }
848
+ if (preserveRecoveryEvidence)
849
+ continue;
503
850
  payloadReferenced = Boolean(payload) && request.family !== 'task-finalization';
504
851
  }
505
852
  }
506
853
  else {
507
854
  if (!brokerOwns())
508
855
  return false;
856
+ if (transitionProtocolActive && startedCommitted) {
857
+ writeSandboxControlResponse(manifest, unknown(entry.name));
858
+ continue;
859
+ }
509
860
  writeSandboxControlResponse(manifest, notExecuted(entry.name));
510
861
  }
511
862
  if (!brokerOwns())
@@ -536,6 +887,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
536
887
  const prepareExecution = options.prepareExecution ?? prepareSandboxControlExecution;
537
888
  const manifest = readSandboxControlManifest(manifestPath);
538
889
  const root = path.dirname(manifestPath);
890
+ assertCurrentSandboxControlIdentity(manifest, manifestPath);
539
891
  const startTime = getProcessStartTime(process.pid);
540
892
  if (!startTime)
541
893
  throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
@@ -634,22 +986,9 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
634
986
  return;
635
987
  appendBrokerAudit(manifest, 'broker-start', {
636
988
  pid: broker.pid,
637
- brokerId: broker.brokerId,
638
- hostCwd: process.cwd(),
639
- manifestPath,
640
- manifestPathRealpath: safeRealpath(manifestPath),
641
- repoRoot: manifest.repoRoot,
642
- repoRootRealpath: safeRealpath(manifest.repoRoot),
643
- worktreeRoot: manifest.worktreeRoot,
644
- worktreeRootRealpath: safeRealpath(manifest.worktreeRoot),
645
- runtimeDir: manifest.runtimeDir,
646
- runtimeDirRealpath: safeRealpath(manifest.runtimeDir),
647
- channelDir: manifest.channelDir,
648
- publicStatusDir: manifest.publicStatusDir,
649
- processingDir: manifest.processingDir,
650
- internalCliPath: options.internalCliPath ?? process.argv[1] ?? null
989
+ brokerId: broker.brokerId
651
990
  });
652
- if (!recoverProcessing(manifest, broker, brokerOwns))
991
+ if (!recoverProcessing(manifest, manifestPath, broker, brokerOwns))
653
992
  return;
654
993
  try {
655
994
  const published = readSandboxControlStatus(manifest.publicStatusDir);
@@ -666,6 +1005,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
666
1005
  let current;
667
1006
  try {
668
1007
  current = readSandboxControlManifest(manifestPath);
1008
+ assertCurrentSandboxControlIdentity(current, manifestPath);
669
1009
  }
670
1010
  catch {
671
1011
  break;
@@ -702,7 +1042,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
702
1042
  }
703
1043
  else if (observation.state === 'unknown') {
704
1044
  containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_UNKNOWN';
705
- appendSandboxControlAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
1045
+ appendDiagnosticAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
706
1046
  const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
707
1047
  nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
708
1048
  containerBackoffMs = backoff.nextDelayMs;
@@ -721,7 +1061,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
721
1061
  if (!brokerOwns())
722
1062
  break;
723
1063
  containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_ABSENT_GC_FAILED';
724
- appendSandboxControlAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
1064
+ appendDiagnosticAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
725
1065
  const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
726
1066
  nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
727
1067
  containerBackoffMs = backoff.nextDelayMs;
@@ -790,6 +1130,10 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
790
1130
  break;
791
1131
  let terminalCommitted = false;
792
1132
  if (settledExecution.result && settledExecution.resultEvidenceWritten) {
1133
+ if (!consumeLifecycleAuthorityForResult(manifest, settledExecution)) {
1134
+ active = settledExecution;
1135
+ continue;
1136
+ }
793
1137
  terminalCommitted = publishExecutionResult(manifest, settledExecution.request, settledExecution.result, broker, brokerOwns);
794
1138
  if (terminalCommitted && settledExecution.request.family === 'task-finalization') {
795
1139
  try {
@@ -808,7 +1152,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
808
1152
  if (!terminalCommitted)
809
1153
  return;
810
1154
  appendBrokerAudit(manifest, 'executor-result-published', {
811
- ...requestAuditFields(manifest, manifestPath, settledExecution.request),
1155
+ ...requestAuditFields(manifest, settledExecution.request),
812
1156
  resultAvailable: settledExecution.result !== null,
813
1157
  resultEvidenceWritten: settledExecution.resultEvidenceWritten,
814
1158
  ...(settledExecution.result ? resultAuditFields(settledExecution.result) : {})
@@ -826,6 +1170,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
826
1170
  const source = path.join(requestsDir, name);
827
1171
  let claimed = null;
828
1172
  let validatedRequest = null;
1173
+ let prepared = null;
829
1174
  try {
830
1175
  if (!brokerOwns()) {
831
1176
  retiring = true;
@@ -853,63 +1198,76 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
853
1198
  throw new Error('SANDBOX_CONTROL_BUSY');
854
1199
  const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
855
1200
  validatedRequest = request;
1201
+ criticalRequestPhase(manifest, request, 'validated', 'in-progress');
856
1202
  appendBrokerAudit(manifest, 'request-validated', {
857
- ...requestAuditFields(manifest, manifestPath, request),
1203
+ ...requestAuditFields(manifest, request),
858
1204
  requestPath: claimed
859
1205
  });
860
1206
  if (bindingCheck(manifest))
861
1207
  throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
862
1208
  if (readActiveLease(manifest))
863
1209
  throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
1210
+ criticalRequestPhase(manifest, request, 'gated', 'in-progress');
864
1211
  appendBrokerAudit(manifest, 'request-gates-passed', {
865
- ...requestAuditFields(manifest, manifestPath, request),
1212
+ ...requestAuditFields(manifest, request),
866
1213
  requestPath: claimed,
867
1214
  bindingChecked: true,
868
1215
  handoffLeaseChecked: true
869
1216
  });
1217
+ criticalRequestPhase(manifest, request, 'reserved', 'in-progress');
870
1218
  writeSandboxControlReservation(manifest, request.id, sandboxControlGenerationUsage(manifest));
871
1219
  appendBrokerAudit(manifest, 'executor-reservation-written', {
872
- ...requestAuditFields(manifest, manifestPath, request),
1220
+ ...requestAuditFields(manifest, request),
873
1221
  requestPath: claimed,
874
1222
  executionPath: executionPath(manifest, request.id)
875
1223
  });
876
1224
  appendBrokerAudit(manifest, 'executor-prepare-start', {
877
- ...requestAuditFields(manifest, manifestPath, request),
1225
+ ...requestAuditFields(manifest, request),
878
1226
  requestPath: claimed,
879
1227
  executorCwd: manifest.repoRoot,
880
1228
  executorEntry: options.internalCliPath ?? process.argv[1] ?? null
881
1229
  });
882
- const prepared = await prepareExecution({
1230
+ const lifecycleAuthority = lifecycleAuthorityForRequest(manifest, manifestPath, request);
1231
+ if (lifecycleAuthority?.status === 'rejected') {
1232
+ throw Object.assign(new Error(lifecycleAuthority.error?.message ?? 'lifecycle authority was rejected'), { code: lifecycleAuthority.error?.code ?? 'LIFECYCLE_AUTHORITY_REJECTED' });
1233
+ }
1234
+ prepared = await prepareExecution({
883
1235
  manifest, manifestPath, request, requestPath: claimed,
884
- internalCliPath: options.internalCliPath ?? process.argv[1]
1236
+ internalCliPath: options.internalCliPath ?? process.argv[1],
1237
+ ...(lifecycleAuthority ? { lifecycleAuthority } : {})
885
1238
  });
1239
+ const preparedExecution = prepared;
1240
+ if (!preparedExecution)
1241
+ throw new Error('SANDBOX_CONTROL_EXECUTION_PREPARE_INVALID');
1242
+ criticalRequestPhase(manifest, request, 'prepared', 'in-progress', `${preparedExecution.execution.child.pid}`);
886
1243
  appendBrokerAudit(manifest, 'executor-prepared', {
887
- ...requestAuditFields(manifest, manifestPath, request),
1244
+ ...requestAuditFields(manifest, request),
888
1245
  requestPath: claimed,
889
1246
  executorCwd: manifest.repoRoot,
890
- childPid: prepared.execution.child.pid,
891
- childStartTime: prepared.execution.child.startTime,
892
- childProcessGroupId: prepared.execution.child.processGroupId,
1247
+ childPid: preparedExecution.execution.child.pid,
1248
+ childStartTime: preparedExecution.execution.child.startTime,
1249
+ childProcessGroupId: preparedExecution.execution.child.processGroupId,
893
1250
  executionPath: executionPath(manifest, request.id)
894
1251
  });
895
1252
  const execution = {
896
- request, prepared, result: null, resultEvidenceWritten: false, failure: null, settled: false
1253
+ request, prepared: preparedExecution, lifecycleAuthority, result: null,
1254
+ resultEvidenceWritten: false, failure: null, settled: false
897
1255
  };
898
1256
  active = execution;
899
- prepared.completion.then((result) => {
1257
+ preparedExecution.completion.then((result) => {
900
1258
  execution.result = sanitizeSandboxControlResult(manifest, result);
901
1259
  appendBrokerAudit(manifest, 'executor-completed', {
902
- ...requestAuditFields(manifest, manifestPath, request),
1260
+ ...requestAuditFields(manifest, request),
903
1261
  ...resultAuditFields(execution.result),
904
- childPid: prepared.execution.child.pid,
905
- childStartTime: prepared.execution.child.startTime
1262
+ childPid: preparedExecution.execution.child.pid,
1263
+ childStartTime: preparedExecution.execution.child.startTime
906
1264
  });
907
1265
  try {
908
1266
  writeSandboxControlResultEvidence(manifest, request.id, execution.result);
909
1267
  execution.resultEvidenceWritten = true;
910
1268
  execution.settled = true;
911
1269
  appendBrokerAudit(manifest, 'executor-result-evidence-written', {
912
- ...requestAuditFields(manifest, manifestPath, request),
1270
+ ...requestAuditFields(manifest, request),
913
1271
  resultPath: path.join(manifest.processingDir, request.id, 'result.json'),
914
1272
  ...resultAuditFields(execution.result)
915
1273
  });
@@ -917,7 +1275,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
917
1275
  catch (error) {
918
1276
  execution.failure = error;
919
1277
  appendBrokerAudit(manifest, 'executor-result-evidence-failed', {
920
- ...requestAuditFields(manifest, manifestPath, request),
1278
+ ...requestAuditFields(manifest, request),
921
1279
  errorType: error instanceof Error ? error.name : typeof error,
922
1280
  });
923
1281
  }
@@ -925,9 +1283,9 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
925
1283
  execution.failure = error;
926
1284
  execution.settled = true;
927
1285
  appendBrokerAudit(manifest, 'executor-failed', {
928
- ...requestAuditFields(manifest, manifestPath, request),
929
- childPid: prepared.execution.child.pid,
930
- childStartTime: prepared.execution.child.startTime,
1286
+ ...requestAuditFields(manifest, request),
1287
+ childPid: preparedExecution.execution.child.pid,
1288
+ childStartTime: preparedExecution.execution.child.startTime,
931
1289
  errorType: error instanceof Error ? error.name : typeof error,
932
1290
  });
933
1291
  });
@@ -943,11 +1301,13 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
943
1301
  retiring = true;
944
1302
  continue;
945
1303
  }
1304
+ criticalRequestPhase(manifest, request, 'accepted-authorized', 'in-progress');
946
1305
  writeAcceptedResponse(manifest, {
947
1306
  version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
948
1307
  });
1308
+ writeSandboxControlTransition(manifest, { requestId: request.id, phase: 'accepted-committed' });
949
1309
  appendBrokerAudit(manifest, 'request-accepted', {
950
- ...requestAuditFields(manifest, manifestPath, request),
1310
+ ...requestAuditFields(manifest, request),
951
1311
  acceptedPath: acceptedResponsePath(manifest, id),
952
1312
  executionPhase: 'prepared'
953
1313
  });
@@ -958,8 +1318,9 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
958
1318
  break;
959
1319
  }
960
1320
  try {
1321
+ criticalRequestPhase(manifest, request, 'start-authorized', 'in-progress', `${prepared.execution.child.pid}`);
961
1322
  appendBrokerAudit(manifest, 'executor-start', {
962
- ...requestAuditFields(manifest, manifestPath, request),
1323
+ ...requestAuditFields(manifest, request),
963
1324
  childPid: prepared.execution.child.pid,
964
1325
  childStartTime: prepared.execution.child.startTime,
965
1326
  executionPath: executionPath(manifest, request.id)
@@ -968,7 +1329,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
968
1329
  }
969
1330
  catch (error) {
970
1331
  appendBrokerAudit(manifest, 'executor-start-failed', {
971
- ...requestAuditFields(manifest, manifestPath, request),
1332
+ ...requestAuditFields(manifest, request),
972
1333
  errorType: error instanceof Error ? error.name : typeof error,
973
1334
  });
974
1335
  const owned = brokerOwns();
@@ -997,6 +1358,14 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
997
1358
  }
998
1359
  }
999
1360
  catch (error) {
1361
+ if (prepared) {
1362
+ try {
1363
+ prepared.terminate(brokerOwns());
1364
+ }
1365
+ catch { /* cleanup is best effort after a failed admission */ }
1366
+ prepared = null;
1367
+ active = null;
1368
+ }
1000
1369
  if (!brokerOwns()) {
1001
1370
  retiring = true;
1002
1371
  break;
@@ -1016,6 +1385,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
1016
1385
  retiring = true;
1017
1386
  break;
1018
1387
  }
1388
+ removeAcceptedResponse(manifest, id);
1019
1389
  if (claimed)
1020
1390
  fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
1021
1391
  }
@@ -1041,7 +1411,8 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
1041
1411
  }
1042
1412
  }
1043
1413
  if (owned && active.result && active.resultEvidenceWritten) {
1044
- if (publishExecutionResult(manifest, active.request, active.result, broker, brokerOwns)) {
1414
+ if (consumeLifecycleAuthorityForResult(manifest, active)
1415
+ && publishExecutionResult(manifest, active.request, active.result, broker, brokerOwns)) {
1045
1416
  if (brokerOwns()) {
1046
1417
  removeAcceptedResponse(manifest, active.request.id);
1047
1418
  fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
@@ -1071,7 +1442,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
1071
1442
  }
1072
1443
  }
1073
1444
  if (brokerOwns()) {
1074
- appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
1445
+ appendDiagnosticAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
1075
1446
  try {
1076
1447
  if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord)
1077
1448
  fs.unlinkSync(brokerPath);