@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.ts';
2
+ import { isDeepStrictEqual } from 'node:util';
1
3
  import { completedReentryView } from './completed-reentry.ts';
2
4
  import fs from 'node:fs';
3
5
  import path from 'node:path';
@@ -16,7 +18,6 @@ import {
16
18
  } from './protocol.ts';
17
19
  import { prepareSandboxControlExecution, type PreparedSandboxControlExecution, type SandboxControlExecutionResult } from './executor.ts';
18
20
  import {
19
- appendSandboxControlAudit,
20
21
  atomicWriteJsonNoReplace,
21
22
  createSandboxControlPayload,
22
23
  cleanupStaleSandboxControlLease,
@@ -26,6 +27,8 @@ import {
26
27
  readExecution,
27
28
  readSandboxControlPayload,
28
29
  readSandboxControlResultEvidence,
30
+ readSandboxControlTerminalResult,
31
+ createSandboxControlTerminalResult,
29
32
  sandboxControlEncodedJsonBytes,
30
33
  sandboxControlGenerationUsage,
31
34
  sanitizeSandboxControlOutput,
@@ -34,11 +37,36 @@ import {
34
37
  writeSandboxControlPayload,
35
38
  writeSandboxControlReservation,
36
39
  writeSandboxControlResultEvidence,
40
+ writeSandboxControlTerminalResult,
41
+ appendSandboxControlAudit,
37
42
  terminateSandboxControlExecution,
38
43
  writeSandboxControlStatus,
39
44
  readSandboxControlStatus
40
45
  } from './state.ts';
41
46
  import { validateSandboxControlRequest } from './protocol.ts';
47
+ import {
48
+ requestAuditFields,
49
+ resultAuditFields,
50
+ appendCriticalAudit,
51
+ appendDiagnosticAudit,
52
+ createSandboxControlAuditContext,
53
+ writeSandboxControlTransition,
54
+ readSandboxControlTransition
55
+ } from './audit.ts';
56
+ import { parseTaskControlOperation } from '../../task/control-authority.ts';
57
+ import {
58
+ parseControlOutput,
59
+ classifySandboxControlRecovery,
60
+ findSandboxControlRecoveryOperation,
61
+ operationRecoveryBinding,
62
+ SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES
63
+ } from '../../task/control-recovery.ts';
64
+ import { readRun } from '../../task/orchestration.ts';
65
+ import { captureRepositorySnapshot } from '../../task/workspace-snapshot.ts';
66
+ import { parseTypedTaskFrontmatter } from '../../task/frontmatter.ts';
67
+ import { readLifecycleJournalEvidence } from '../../task/lifecycle.ts';
68
+ import { resolveTaskRef } from '../../task/resolve-ref.ts';
69
+ import { loadShortIdByTaskId } from '../../task/short-id.ts';
42
70
  import { inspectSandboxControlContainer, type ContainerObservation } from './container-identity.ts';
43
71
  import {
44
72
  acquireSandboxControlBrokerStartup,
@@ -49,7 +77,8 @@ import {
49
77
  import type { BrokerOwner } from './lifecycle.ts';
50
78
  import { nextSandboxControlBackoff } from './timing.ts';
51
79
  import { readTaskFinalizationReceipt } from '../../task/finalization.ts';
52
- import { resolveTaskRef } from '../../task/resolve-ref.ts';
80
+ import { readCodexControllerRegistration } from './controller-registration.ts';
81
+ import { validateSandboxControlIdentity } from './identity-sentinel.ts';
53
82
  import {
54
83
  mergeSandboxTaskView,
55
84
  taskViewAfterFinalization,
@@ -57,22 +86,68 @@ import {
57
86
  type SandboxTaskView
58
87
  } from './task-view.ts';
59
88
  import { taskCreateOutputUnavailableResult } from '../../task/create-service.ts';
89
+ import {
90
+ consumeLifecycleRecoveryAttestation,
91
+ issueLifecycleRecoveryAttestation,
92
+ recoverLifecycleRecoveryOperation,
93
+ type LifecycleAuthorityResponseV1
94
+ } from '../../task/control-authority.ts';
95
+ import { createCodexCapabilityStore } from '../../agent-clients/adapters/codex-lifecycle/capability-store.ts';
96
+ import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.ts';
60
97
 
61
98
  type ActiveExecution = {
62
99
  request: SandboxControlRequest;
63
100
  prepared: PreparedSandboxControlExecution;
101
+ lifecycleAuthority: LifecycleAuthorityResponseV1 | undefined;
64
102
  result: SandboxControlExecutionResult | null;
65
103
  resultEvidenceWritten: boolean;
66
104
  failure: unknown;
67
105
  settled: boolean;
68
106
  };
69
107
 
70
- function safeRealpath(value: string): string | null {
108
+ function consumeLifecycleAuthorityForResult(
109
+ manifest: SandboxControlManifest,
110
+ execution: ActiveExecution
111
+ ): boolean {
112
+ const authority = execution.lifecycleAuthority?.attestation;
113
+ if (!authority || execution.result?.exitCode !== 0) return true;
114
+ try {
115
+ consumeLifecycleRecoveryAttestation(authority);
116
+ return true;
117
+ } catch (error) {
118
+ appendBrokerAudit(manifest, 'lifecycle-authority-consume-retry', {
119
+ ...requestAuditFields(manifest, execution.request),
120
+ errorCode: error instanceof Error ? error.name : 'LIFECYCLE_AUTHORITY_CONSUME_FAILED'
121
+ });
122
+ return false;
123
+ }
124
+ }
125
+
126
+ function lifecycleAuthorityForRequest(
127
+ manifest: SandboxControlManifest,
128
+ manifestPath: string,
129
+ request: SandboxControlRequest
130
+ ): LifecycleAuthorityResponseV1 | undefined {
131
+ const selector = request.family === 'task-workflow' || request.family === 'task-orchestration'
132
+ ? request.authority
133
+ : undefined;
134
+ if (!selector) return undefined;
135
+ let controllerBinding: { instanceDigest: string; controlGeneration: string } | null = null;
71
136
  try {
72
- return fs.realpathSync.native(value);
137
+ const registration = readCodexControllerRegistration(manifestPath);
138
+ controllerBinding = {
139
+ instanceDigest: registration.controllerInstanceDigest,
140
+ controlGeneration: registration.controlGeneration
141
+ };
73
142
  } catch {
74
- return null;
143
+ // The issuer reports the stable unavailable result; the broker never trusts
144
+ // a controller binding supplied by the request.
75
145
  }
146
+ return issueLifecycleRecoveryAttestation(selector, {
147
+ controllerBinding,
148
+ capabilityStore: createCodexCapabilityStore(),
149
+ buildIdentity: computeLifecycleBuildIdentity(manifest.repoRoot)
150
+ });
76
151
  }
77
152
 
78
153
  function appendBrokerAudit(
@@ -80,53 +155,72 @@ function appendBrokerAudit(
80
155
  event: string,
81
156
  fields: Record<string, string | number | boolean | null> = {}
82
157
  ): void {
158
+ appendDiagnosticAudit(manifest, event, { source: 'broker', ...fields });
159
+ }
160
+
161
+ function operationKey(request: SandboxControlRequest, output?: string): string | null {
162
+ if (request.family === 'task-finalization') return request.operation;
163
+ if (request.family === 'task-create') return 'create';
164
+ if (request.family === 'codex-controller') return request.command;
165
+ if (request.family === 'task-workflow') return request.workflow.operation;
166
+ if (request.family !== 'task-lifecycle' && request.family !== 'task-orchestration') return null;
167
+ if (request.family === 'task-orchestration' && request.args[1] === 'route') {
168
+ if (!output) return 'route';
169
+ const result = parseControlOutput(output);
170
+ return result?.changed === true && result.status === 'completed' ? 'route.clean-completion' : 'route.read';
171
+ }
83
172
  try {
84
- appendSandboxControlAudit(manifest, event, { source: 'broker', ...fields });
173
+ const operation = parseTaskControlOperation(request.family, request.args);
174
+ return operation.family === 'task-orchestration' ? operation.intent : operation.request.intent;
85
175
  } catch {
86
- // Diagnostics must never change the control protocol.
176
+ return null;
177
+ }
178
+ }
179
+
180
+ function recoveryOperationKey(
181
+ request: SandboxControlRequest,
182
+ terminalResult: ReturnType<typeof readSandboxControlTerminalResult> | null,
183
+ output: string | undefined
184
+ ): string | null {
185
+ const operation = operationKey(request, output);
186
+ if (operation !== 'route') return operation;
187
+ const digest = terminalResult?.intentDigest;
188
+ for (const candidate of ['route.read', 'route.clean-completion'] as const) {
189
+ const expected = operationRecoveryBinding(request.id, request.generation, null, request.family, candidate).intentDigest;
190
+ if (digest === expected) return candidate;
87
191
  }
192
+ return null;
88
193
  }
89
194
 
90
- function requestAuditFields(
195
+ function terminalResultMatchesRequest(
196
+ request: SandboxControlRequest,
197
+ terminalResult: ReturnType<typeof readSandboxControlTerminalResult>
198
+ ): boolean {
199
+ const operation = operationKey(request);
200
+ const candidates = operation === 'route'
201
+ ? ['route.read', 'route.clean-completion']
202
+ : operation ? [operation] : [];
203
+ return candidates.some((candidate) => createHash('sha256').update(`${request.family}\0${candidate}`, 'utf8').digest('hex') === terminalResult.intentDigest);
204
+ }
205
+
206
+ function criticalRequestPhase(
91
207
  manifest: SandboxControlManifest,
92
- manifestPath: string,
93
- request: SandboxControlRequest
94
- ): Record<string, string | number | boolean | null> {
95
- const args = 'args' in request ? request.args : [];
96
- const encodedArgs = JSON.stringify(args);
97
- return {
208
+ request: SandboxControlRequest,
209
+ phase: Parameters<typeof createSandboxControlAuditContext>[1]['phase'],
210
+ outcome: Parameters<typeof createSandboxControlAuditContext>[1]['outcome'],
211
+ reference?: string | null
212
+ ): void {
213
+ const context = createSandboxControlAuditContext(manifest, {
98
214
  requestId: request.id,
99
- requestFamily: request.family,
100
- sandboxTaskId: manifest.taskId,
101
- requestGeneration: request.generation,
102
- requestIssuedAt: request.issuedAt,
103
- requestExpiresAt: request.expiresAt,
104
- requestArgCount: args.length,
105
- requestArgsSha256: createHash('sha256').update(encodedArgs, 'utf8').digest('hex'),
106
- requestTaskRef: args[0] ?? null,
107
- requestCommand: args[1] ?? null,
108
- controllerProofPresent: request.controllerProof !== null,
109
- hostCwd: process.cwd(),
110
- manifestPath,
111
- manifestPathRealpath: safeRealpath(manifestPath),
112
- repoRoot: manifest.repoRoot,
113
- repoRootRealpath: safeRealpath(manifest.repoRoot),
114
- worktreeRoot: manifest.worktreeRoot,
115
- worktreeRootRealpath: safeRealpath(manifest.worktreeRoot),
116
- runtimeDir: manifest.runtimeDir,
117
- runtimeDirRealpath: safeRealpath(manifest.runtimeDir)
118
- };
215
+ family: request.family,
216
+ operation: operationKey(request),
217
+ phase,
218
+ outcome
219
+ });
220
+ appendCriticalAudit(manifest, context, { reference: reference ?? null });
221
+ writeSandboxControlTransition(manifest, { requestId: request.id, phase, reference });
119
222
  }
120
223
 
121
- function resultAuditFields(result: SandboxControlExecutionResult): Record<string, string | number | boolean | null> {
122
- return {
123
- exitCode: result.exitCode,
124
- stdoutBytes: Buffer.byteLength(result.stdout, 'utf8'),
125
- stderrBytes: Buffer.byteLength(result.stderr, 'utf8'),
126
- stdoutSha256: createHash('sha256').update(result.stdout, 'utf8').digest('hex'),
127
- stderrSha256: createHash('sha256').update(result.stderr, 'utf8').digest('hex')
128
- };
129
- }
130
224
 
131
225
  function assertRealDirectory(directory: string, parent?: string): void {
132
226
  const stat = fs.lstatSync(directory);
@@ -212,33 +306,7 @@ function finalizationRecoveryResponse(
212
306
  } catch {
213
307
  return { status: 'deferred' };
214
308
  }
215
- const pendingSteps = [
216
- receipt.taskComment === 'pending' ? 'task-comment' : null,
217
- receipt.verification === 'pending' ? 'verification' : null
218
- ].filter((step): step is string => step !== null);
219
- const completedSteps = ['lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment', receipt.verification === 'pending' ? null : 'verification']
220
- .filter((step): step is string => step !== null);
221
- const warnings = receipt.warnings
222
- .filter((warning) => warning.status === 'open')
223
- .map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning);
224
- const result = {
225
- status: 'completed', changed: false, taskId: manifest.taskId,
226
- lifecycle: { status: 'no-op', changed: false, error: null },
227
- taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
228
- verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
229
- completedSteps, pendingSteps,
230
- result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
231
- ? 'completed_with_warnings' : 'completed',
232
- warnings, error: null
233
- };
234
- return {
235
- status: 'matched',
236
- response: {
237
- version: 2, id: requestId, phase: 'completed', exitCode: 0,
238
- stdout: `${JSON.stringify({ version: 1, status: 'completed', changed: false, accepted: true, result, error: null })}\n`,
239
- stderr: '', error: null
240
- }
241
- };
309
+ return { status: 'matched', response: finalizationTerminalResponse(manifest.taskId, requestId, receipt) };
242
310
  }
243
311
 
244
312
  function publishFinalizationTaskView(
@@ -322,29 +390,269 @@ function genericRecoveryResponse(
322
390
  };
323
391
  }
324
392
 
393
+ type RecoveryDomainEvidence = Readonly<{
394
+ domain: Readonly<Record<string, unknown>> | null;
395
+ journal?: ReturnType<typeof readLifecycleJournalEvidence>;
396
+ }>;
397
+
398
+ function taskCreateDomainEvidence(
399
+ manifest: SandboxControlManifest,
400
+ output: Record<string, unknown> | null
401
+ ): Readonly<Record<string, unknown>> {
402
+ if (!output || !['applied', 'no-op', 'degraded'].includes(String(output.status))) {
403
+ return { consistent: false };
404
+ }
405
+ const task = output.task && typeof output.task === 'object' && !Array.isArray(output.task)
406
+ ? output.task as Record<string, unknown> : null;
407
+ const taskId = typeof task?.id === 'string' ? task.id : null;
408
+ const shortId = typeof task?.shortId === 'string' ? task.shortId : null;
409
+ if (!taskId || !shortId) return { consistent: false };
410
+ try {
411
+ const resolved = resolveTaskRef(taskId, { repoRoot: manifest.repoRoot });
412
+ const shortIds = loadShortIdByTaskId(manifest.repoRoot);
413
+ return {
414
+ consistent: resolved.ok && resolved.taskId === taskId && resolved.state === 'active' && shortIds.get(taskId) === shortId,
415
+ taskId,
416
+ shortId
417
+ };
418
+ } catch {
419
+ return { consistent: false };
420
+ }
421
+ }
422
+
423
+ function controllerDomainEvidence(
424
+ manifest: SandboxControlManifest,
425
+ manifestPath: string,
426
+ request: SandboxControlRequest,
427
+ output: Record<string, unknown> | null
428
+ ): Readonly<Record<string, unknown>> {
429
+ if (!output || output.error !== null || typeof output.status !== 'string') return { consistent: false };
430
+ try {
431
+ const registration = readCodexControllerRegistration(manifestPath);
432
+ if (request.family !== 'codex-controller') return { consistent: false };
433
+ if (request.command === 'open') {
434
+ const lease = output.lease && typeof output.lease === 'object' && !Array.isArray(output.lease)
435
+ ? output.lease as Record<string, unknown> : null;
436
+ return {
437
+ consistent: output.status === 'opened' && output.changed === true
438
+ && lease?.taskId === registration.taskId
439
+ && lease.controlGeneration === registration.controlGeneration
440
+ && lease.controllerInstanceDigest === registration.controllerInstanceDigest
441
+ && registration.taskId === manifest.taskId
442
+ && registration.controlGeneration === manifest.generation
443
+ };
444
+ }
445
+ if (request.command === 'verify') {
446
+ const binding = output.binding && typeof output.binding === 'object' && !Array.isArray(output.binding)
447
+ ? output.binding as Record<string, unknown> : null;
448
+ return {
449
+ consistent: output.status === 'verified' && output.changed === false
450
+ && binding?.taskId === registration.taskId
451
+ && binding.controlGeneration === registration.controlGeneration
452
+ && binding.controllerInstanceDigest === registration.controllerInstanceDigest
453
+ };
454
+ }
455
+ return { consistent: false };
456
+ } catch (error) {
457
+ return { consistent: request.family === 'codex-controller' && request.command === 'close'
458
+ && (error as { code?: string }).code === 'CODEX_SANDBOX_CONTROLLER_REGISTRATION_MISSING'
459
+ && output?.status === 'closed' && typeof output.changed === 'boolean' };
460
+ }
461
+ }
462
+
463
+ function orchestrationDomainEvidence(
464
+ operation: ReturnType<typeof findSandboxControlRecoveryOperation>,
465
+ terminalResult: ReturnType<typeof readSandboxControlTerminalResult>,
466
+ output: Record<string, unknown> | null,
467
+ run: ReturnType<typeof readRun> | null
468
+ ): Readonly<Record<string, unknown>> {
469
+ if (!operation || !output) return { consistent: false };
470
+ const outputRun = output.run && typeof output.run === 'object' && !Array.isArray(output.run)
471
+ ? output.run as Record<string, unknown> : null;
472
+ const runMatches = outputRun === null ? run === null : run !== null && JSON.stringify(run) === JSON.stringify(outputRun);
473
+ if (operation.class === 'read-only') {
474
+ return {
475
+ consistent: output.status === terminalResult.status && output.changed === terminalResult.changed && runMatches,
476
+ snapshotValid: true,
477
+ status: run?.status ?? terminalResult.status,
478
+ pendingDelegation: run?.pendingDelegation ?? null
479
+ };
480
+ }
481
+ if (!run || !outputRun) return { consistent: false };
482
+ return {
483
+ consistent: output.status === terminalResult.status && output.changed === terminalResult.changed && runMatches,
484
+ status: run.status,
485
+ pendingDelegation: run.pendingDelegation
486
+ };
487
+ }
488
+
489
+ function readRecoveryDomain(
490
+ manifest: SandboxControlManifest,
491
+ manifestPath: string,
492
+ request: SandboxControlRequest,
493
+ operation: ReturnType<typeof findSandboxControlRecoveryOperation>,
494
+ terminalResult: ReturnType<typeof readSandboxControlTerminalResult>,
495
+ payloadOutput: string | null
496
+ ): RecoveryDomainEvidence {
497
+ if (!operation) return { domain: null };
498
+ const taskRef = request.family === 'task-finalization' || request.family === 'task-workflow'
499
+ ? manifest.taskId
500
+ : 'args' in request ? request.args[0] ?? null : manifest.taskId;
501
+ const output = parseControlOutput(payloadOutput);
502
+ if (!taskRef && operation.family !== 'task-create' && operation.family !== 'codex-controller') {
503
+ return { domain: null };
504
+ }
505
+
506
+ if (operation.family === 'task-lifecycle') {
507
+ if (!taskRef || !terminalResult.targetState) return { domain: null };
508
+ const journal = readLifecycleJournalEvidence(manifest.repoRoot, taskRef);
509
+ try {
510
+ const resolved = resolveTaskRef(taskRef, { repoRoot: manifest.repoRoot });
511
+ const shortIds = loadShortIdByTaskId(manifest.repoRoot);
512
+ const shortIdMatches = terminalResult.targetState === 'active'
513
+ ? shortIds.has(resolved.ok ? resolved.taskId : taskRef)
514
+ : !shortIds.has(resolved.ok ? resolved.taskId : taskRef);
515
+ return {
516
+ domain: { consistent: resolved.ok && resolved.state === terminalResult.targetState && shortIdMatches },
517
+ journal
518
+ };
519
+ } catch {
520
+ return { domain: { consistent: false }, journal };
521
+ }
522
+ }
523
+
524
+ if (operation.family === 'task-orchestration') {
525
+ if (!taskRef) return { domain: null };
526
+ try {
527
+ const resolved = resolveTaskRef(taskRef, { repoRoot: manifest.repoRoot });
528
+ if (!resolved.ok) return { domain: { consistent: false } };
529
+ const run = readRun(resolved.taskDir);
530
+ if (!run && operation.class !== 'read-only') return { domain: { consistent: false } };
531
+ if (operation.class === 'route.clean-completion') {
532
+ const snapshot = captureRepositorySnapshot(manifest.repoRoot);
533
+ const metadata = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
534
+ const completion = run?.completionEvidence ?? null;
535
+ const consistent = run !== null && completion !== null
536
+ && terminalResult.completionEvidence !== null
537
+ && isDeepStrictEqual(completion, terminalResult.completionEvidence);
538
+ return {
539
+ domain: {
540
+ consistent,
541
+ status: run?.status ?? null,
542
+ pendingDelegation: run?.pendingDelegation ?? null,
543
+ completionEvidence: completion,
544
+ snapshot,
545
+ lastReviewedCommit: metadata.last_reviewed_commit
546
+ }
547
+ };
548
+ }
549
+ return { domain: orchestrationDomainEvidence(operation, terminalResult, output, run) };
550
+ } catch {
551
+ return { domain: { consistent: false } };
552
+ }
553
+ }
554
+
555
+ if (operation.family === 'task-create') {
556
+ return { domain: taskCreateDomainEvidence(manifest, output) };
557
+ }
558
+ if (operation.family === 'codex-controller') {
559
+ return { domain: controllerDomainEvidence(manifest, manifestPath, request, output) };
560
+ }
561
+ if (operation.family === 'task-workflow') {
562
+ return {
563
+ domain: { consistent: output?.status === terminalResult.status && output.changed === terminalResult.changed, snapshotValid: true }
564
+ };
565
+ }
566
+ return { domain: null };
567
+ }
568
+
569
+ function readCommittedCriticalPhases(manifest: SandboxControlManifest, requestId: string): readonly string[] {
570
+ return SANDBOX_CONTROL_REQUIRED_COMPLETION_PHASES.filter((phase) => {
571
+ try {
572
+ readSandboxControlTransition(manifest, requestId, phase);
573
+ return true;
574
+ } catch {
575
+ return false;
576
+ }
577
+ });
578
+ }
579
+
325
580
  function recoveryResponse(
326
581
  manifest: SandboxControlManifest,
582
+ manifestPath: string,
327
583
  request: SandboxControlRequest,
328
584
  evidence: ReturnType<typeof readSandboxControlResultEvidence>,
329
- payload: ReturnType<typeof readSandboxControlPayload> | null
585
+ payload: ReturnType<typeof readSandboxControlPayload> | null,
586
+ terminalResult: ReturnType<typeof readSandboxControlTerminalResult>
330
587
  ): SandboxControlResponse | null {
588
+ const operationName = recoveryOperationKey(request, terminalResult, payload?.stdout);
589
+ const operation = operationName ? findSandboxControlRecoveryOperation(request.family, operationName) : null;
590
+ if (!operation) return null;
591
+ if (payload) {
592
+ const payloadTerminal = createSandboxControlTerminalResult(manifest, {
593
+ id: request.id, family: request.family, operation: operationName
594
+ }, payload.stdout);
595
+ if (JSON.stringify(payloadTerminal) !== JSON.stringify(terminalResult)) return unknown(request.id);
596
+ }
597
+ const finalization = request.family === 'task-finalization' ? finalizationRecoveryResponse(manifest, request.id, 0) : null;
598
+ if (evidence.exitCode === 0 && finalization?.status === 'deferred') return null;
599
+ if (evidence.exitCode === 0 && request.family === 'task-workflow' && request.authority?.phase === 'task-event.completed') {
600
+ try {
601
+ const registration = readCodexControllerRegistration(manifestPath);
602
+ const recovered = recoverLifecycleRecoveryOperation(request.authority, {
603
+ repoRoot: manifest.repoRoot,
604
+ capabilityStore: createCodexCapabilityStore(),
605
+ buildIdentity: computeLifecycleBuildIdentity(manifest.repoRoot),
606
+ controllerBinding: {
607
+ instanceDigest: registration.controllerInstanceDigest,
608
+ controlGeneration: registration.controlGeneration
609
+ }
610
+ });
611
+ if (recovered.status !== 'committed') return null;
612
+ } catch (error) {
613
+ appendBrokerAudit(manifest, 'lifecycle-authority-recovery-failed', {
614
+ ...requestAuditFields(manifest, request),
615
+ errorCode: error instanceof Error ? error.name : 'LIFECYCLE_AUTHORITY_RECOVERY_FAILED'
616
+ });
617
+ return null;
618
+ }
619
+ }
620
+ const recovery: RecoveryDomainEvidence = finalization
621
+ ? { domain: { consistent: finalization.status === 'matched' } }
622
+ : readRecoveryDomain(manifest, manifestPath, request, operation, terminalResult, payload?.stdout ?? null);
623
+ const binding = operationRecoveryBinding(request.id, manifest.generation, manifest.taskId, request.family, operationName!);
624
+ const decision = classifySandboxControlRecovery({
625
+ operation,
626
+ binding,
627
+ startedCommitted: true,
628
+ terminalResult,
629
+ criticalPhases: readCommittedCriticalPhases(manifest, request.id),
630
+ domain: recovery.domain,
631
+ journal: recovery.journal
632
+ });
633
+ if (decision.outcome === 'unknown' || decision.outcome === 'in-progress') return unknown(request.id);
634
+ if (decision.outcome === 'not-executed') return notExecuted(request.id);
635
+ if (decision.outcome !== 'success' && decision.outcome !== 'failure') return null;
331
636
  if (request.family === 'task-finalization' && evidence.exitCode === 0) {
332
- const finalization = finalizationRecoveryResponse(manifest, request.id, evidence.exitCode);
333
- return finalization.status === 'matched' ? finalization.response ?? null : null;
637
+ if (finalization?.status !== 'matched') return null;
638
+ return finalization.response ?? null;
334
639
  }
335
640
  return genericRecoveryResponse(request, evidence.exitCode, payload);
336
641
  }
337
642
 
338
643
  function terminalMatchesEvidence(
339
644
  manifest: SandboxControlManifest,
645
+ manifestPath: string,
340
646
  request: SandboxControlRequest,
341
647
  response: SandboxControlResponse,
342
648
  evidence: ReturnType<typeof readSandboxControlResultEvidence>,
343
649
  payload: ReturnType<typeof readSandboxControlPayload> | null,
344
- payloadInvalid: boolean
650
+ payloadInvalid: boolean,
651
+ terminalResult: ReturnType<typeof readSandboxControlTerminalResult> | null
345
652
  ): { valid: boolean; payloadReferenced: boolean } {
346
653
  if (request.family === 'task-finalization' && evidence.exitCode === 0) {
347
- const expected = recoveryResponse(manifest, request, evidence, payload);
654
+ if (!terminalResult) return { valid: false, payloadReferenced: false };
655
+ const expected = recoveryResponse(manifest, manifestPath, request, evidence, payload, terminalResult);
348
656
  return {
349
657
  valid: expected !== null && JSON.stringify(response) === JSON.stringify(expected),
350
658
  payloadReferenced: false
@@ -392,6 +700,16 @@ function publishExecutionResult(
392
700
  brokerOwns: () => boolean
393
701
  ): boolean {
394
702
  const normalized = sanitizeSandboxControlResult(manifest, result);
703
+ writeSandboxControlTerminalResult(manifest, {
704
+ id: request.id,
705
+ family: request.family,
706
+ operation: operationKey(request, normalized.stdout)
707
+ }, normalized.stdout);
708
+ if (fs.existsSync(path.join(manifest.processingDir, request.id, 'transitions'))) {
709
+ criticalRequestPhase(manifest, request, 'completed', normalized.exitCode === 0 ? 'success' : 'failure');
710
+ criticalRequestPhase(manifest, request, 'evidence-written', normalized.exitCode === 0 ? 'success' : 'failure');
711
+ criticalRequestPhase(manifest, request, 'publish-authorized', normalized.exitCode === 0 ? 'success' : 'failure');
712
+ }
395
713
  let terminal: SandboxControlResponse | null = null;
396
714
  if (request.family === 'task-finalization') {
397
715
  const finalization = finalizationRecoveryResponse(manifest, request.id, normalized.exitCode);
@@ -427,7 +745,11 @@ function publishExecutionResult(
427
745
  const view = publishFinalizationTaskView(manifest, broker, request.id, 'healthy', null, null);
428
746
  if (view.state === 'unknown') return false;
429
747
  }
430
- return writeSandboxControlResponse(manifest, terminal);
748
+ const committed = writeSandboxControlResponse(manifest, terminal);
749
+ if (committed && fs.existsSync(path.join(manifest.processingDir, request.id, 'transitions'))) {
750
+ criticalRequestPhase(manifest, request, 'published-committed', normalized.exitCode === 0 ? 'success' : 'failure');
751
+ }
752
+ return committed;
431
753
  }
432
754
 
433
755
  function writeAcceptedResponse(manifest: SandboxControlManifest, response: SandboxControlResponse): void {
@@ -492,21 +814,40 @@ function bindingReason(manifest: SandboxControlManifest): string | null {
492
814
  }
493
815
  }
494
816
 
495
- function recoverProcessing(manifest: SandboxControlManifest, broker: BrokerOwner, brokerOwns: () => boolean): boolean {
817
+ function assertCurrentSandboxControlIdentity(manifest: SandboxControlManifest, manifestPath: string): void {
818
+ const result = validateSandboxControlIdentity({
819
+ publicStatusDir: manifest.publicStatusDir,
820
+ root: path.dirname(path.resolve(manifestPath)),
821
+ mode: manifest.mode,
822
+ taskId: manifest.taskId,
823
+ generation: manifest.generation,
824
+ controlRootId: manifest.controlRootId
825
+ });
826
+ if (result.state !== 'valid') {
827
+ throw new Error(`SANDBOX_CONTROL_IDENTITY_${result.state.replaceAll('-', '_').toUpperCase()}`);
828
+ }
829
+ }
830
+
831
+ function recoverProcessing(manifest: SandboxControlManifest, manifestPath: string, broker: BrokerOwner, brokerOwns: () => boolean): boolean {
496
832
  for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
497
833
  if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name)) continue;
498
834
  if (!brokerOwns()) return false;
499
835
  const descriptor = executionPath(manifest, entry.name);
500
836
  const existingResponse = responsePath(manifest, entry.name);
501
837
  const processingDirectory = path.join(manifest.processingDir, entry.name);
838
+ const transitionsDirectory = path.join(processingDirectory, 'transitions');
839
+ const transitionProtocolActive = fs.existsSync(transitionsDirectory);
840
+ const startedCommitted = fs.existsSync(path.join(transitionsDirectory, 'started-committed.json'));
502
841
  let payloadReferenced = false;
503
842
  if (fs.existsSync(descriptor)) {
504
843
  const execution = readExecution(descriptor);
505
844
  let resultEvidence: ReturnType<typeof readSandboxControlResultEvidence> | null = null;
845
+ let terminalResult: ReturnType<typeof readSandboxControlTerminalResult> | null = null;
506
846
  let payload: ReturnType<typeof readSandboxControlPayload> | null = null;
507
847
  let payloadInvalid = false;
508
848
  let request: SandboxControlRequest | null = null;
509
849
  let existingTerminalResponse: SandboxControlResponse | null = null;
850
+ let terminalReconciled = false;
510
851
  try {
511
852
  const rawRequest = JSON.parse(fs.readFileSync(path.join(processingDirectory, 'request.json'), 'utf8')) as Record<string, unknown>;
512
853
  request = validateSandboxControlRequest(rawRequest, manifest, {
@@ -526,6 +867,18 @@ function recoverProcessing(manifest: SandboxControlManifest, broker: BrokerOwner
526
867
  resultEvidence = null;
527
868
  }
528
869
  }
870
+ const terminalResultPathValue = path.join(processingDirectory, 'terminal-result.json');
871
+ if (fs.existsSync(terminalResultPathValue)) {
872
+ try {
873
+ terminalResult = readSandboxControlTerminalResult(terminalResultPathValue);
874
+ if (terminalResult.requestId !== entry.name || terminalResult.generation !== manifest.generation
875
+ || !request || !terminalResultMatchesRequest(request, terminalResult)) {
876
+ terminalResult = null;
877
+ }
878
+ } catch {
879
+ terminalResult = null;
880
+ }
881
+ }
529
882
  const publishedPayloadPath = payloadPath(manifest, entry.name);
530
883
  if (fs.existsSync(publishedPayloadPath)) {
531
884
  try {
@@ -538,11 +891,21 @@ function recoverProcessing(manifest: SandboxControlManifest, broker: BrokerOwner
538
891
  payloadInvalid = true;
539
892
  }
540
893
  }
894
+ if (transitionProtocolActive && startedCommitted && !terminalResult) {
895
+ if (!brokerOwns()) return false;
896
+ if (!terminateSandboxControlExecution(execution)) {
897
+ throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
898
+ }
899
+ appendDiagnosticAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
900
+ writeSandboxControlResponse(manifest, unknown(entry.name));
901
+ continue;
902
+ }
903
+ if (transitionProtocolActive && !startedCommitted && terminalResult) continue;
541
904
  if (!terminateSandboxControlExecution(execution)) {
542
905
  throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
543
906
  }
544
907
  if (!brokerOwns()) return false;
545
- appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
908
+ appendDiagnosticAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
546
909
  let terminal = false;
547
910
  if (fs.existsSync(existingResponse)) {
548
911
  try {
@@ -571,28 +934,61 @@ function recoverProcessing(manifest: SandboxControlManifest, broker: BrokerOwner
571
934
  }
572
935
  if (terminal && (!request || !resultEvidence)) continue;
573
936
  if (terminal && request && resultEvidence) {
574
- const reconciliation = terminalMatchesEvidence(manifest, request, existingTerminalResponse!, resultEvidence, payload, payloadInvalid);
937
+ if (!terminalResult && request.family !== 'task-create') continue;
938
+ const reconciliation = terminalMatchesEvidence(
939
+ manifest,
940
+ manifestPath,
941
+ request,
942
+ existingTerminalResponse!,
943
+ resultEvidence,
944
+ payload,
945
+ payloadInvalid,
946
+ terminalResult
947
+ );
575
948
  if (!reconciliation.valid) continue;
949
+ terminalReconciled = true;
576
950
  payloadReferenced = reconciliation.payloadReferenced;
577
951
  if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
578
952
  const view = publishFinalizationTaskView(manifest, broker, request.id, 'starting', null, null);
579
953
  if (view.state === 'unknown') continue;
580
954
  }
581
955
  }
956
+ if (!terminalResult && !terminalReconciled) {
957
+ if (terminal) continue;
958
+ if (!brokerOwns()) return false;
959
+ if (!resultEvidence || !request || payloadInvalid) continue;
960
+ if (request.family === 'task-create' && resultEvidence.exitCode === 0 && payload === null) {
961
+ writeSandboxControlResponse(manifest, genericRecoveryResponse(request, resultEvidence.exitCode, null));
962
+ terminalReconciled = true;
963
+ terminal = true;
964
+ } else {
965
+ writeSandboxControlResponse(manifest, unknown(entry.name));
966
+ continue;
967
+ }
968
+ }
582
969
  if (!terminal) {
583
970
  if (!brokerOwns()) return false;
584
971
  if (!resultEvidence || !request || payloadInvalid) continue;
585
- const recovered = recoveryResponse(manifest, request, resultEvidence, payload);
972
+ const recovered = recoveryResponse(manifest, manifestPath, request, resultEvidence, payload, terminalResult!);
586
973
  if (!recovered) continue;
587
974
  if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
588
975
  const view = publishFinalizationTaskView(manifest, broker, request.id, 'starting', null, null);
589
976
  if (view.state === 'unknown') continue;
590
977
  }
591
978
  writeSandboxControlResponse(manifest, recovered);
979
+ const preserveRecoveryEvidence = recovered.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN';
980
+ if (transitionProtocolActive && !preserveRecoveryEvidence) {
981
+ writeSandboxControlTransition(manifest, { requestId: entry.name, phase: 'recovered' });
982
+ }
983
+ if (preserveRecoveryEvidence) continue;
592
984
  payloadReferenced = Boolean(payload) && request.family !== 'task-finalization';
593
985
  }
594
986
  } else {
595
987
  if (!brokerOwns()) return false;
988
+ if (transitionProtocolActive && startedCommitted) {
989
+ writeSandboxControlResponse(manifest, unknown(entry.name));
990
+ continue;
991
+ }
596
992
  writeSandboxControlResponse(manifest, notExecuted(entry.name));
597
993
  }
598
994
  if (!brokerOwns()) return false;
@@ -634,6 +1030,7 @@ export async function serveSandboxControl(
634
1030
  const prepareExecution = options.prepareExecution ?? prepareSandboxControlExecution;
635
1031
  const manifest = readSandboxControlManifest(manifestPath);
636
1032
  const root = path.dirname(manifestPath);
1033
+ assertCurrentSandboxControlIdentity(manifest, manifestPath);
637
1034
  const startTime = getProcessStartTime(process.pid);
638
1035
  if (!startTime) throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
639
1036
  const brokerId = randomUUID();
@@ -721,22 +1118,9 @@ export async function serveSandboxControl(
721
1118
  if (!brokerOwns()) return;
722
1119
  appendBrokerAudit(manifest, 'broker-start', {
723
1120
  pid: broker.pid,
724
- brokerId: broker.brokerId,
725
- hostCwd: process.cwd(),
726
- manifestPath,
727
- manifestPathRealpath: safeRealpath(manifestPath),
728
- repoRoot: manifest.repoRoot,
729
- repoRootRealpath: safeRealpath(manifest.repoRoot),
730
- worktreeRoot: manifest.worktreeRoot,
731
- worktreeRootRealpath: safeRealpath(manifest.worktreeRoot),
732
- runtimeDir: manifest.runtimeDir,
733
- runtimeDirRealpath: safeRealpath(manifest.runtimeDir),
734
- channelDir: manifest.channelDir,
735
- publicStatusDir: manifest.publicStatusDir,
736
- processingDir: manifest.processingDir,
737
- internalCliPath: options.internalCliPath ?? process.argv[1] ?? null
1121
+ brokerId: broker.brokerId
738
1122
  });
739
- if (!recoverProcessing(manifest, broker, brokerOwns)) return;
1123
+ if (!recoverProcessing(manifest, manifestPath, broker, brokerOwns)) return;
740
1124
  try {
741
1125
  const published = readSandboxControlStatus(manifest.publicStatusDir);
742
1126
  if (published.generation === manifest.generation) taskView = published.taskView;
@@ -749,6 +1133,7 @@ export async function serveSandboxControl(
749
1133
  let current: SandboxControlManifest;
750
1134
  try {
751
1135
  current = readSandboxControlManifest(manifestPath);
1136
+ assertCurrentSandboxControlIdentity(current, manifestPath);
752
1137
  } catch {
753
1138
  break;
754
1139
  }
@@ -779,7 +1164,7 @@ export async function serveSandboxControl(
779
1164
  nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
780
1165
  } else if (observation.state === 'unknown') {
781
1166
  containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_UNKNOWN';
782
- appendSandboxControlAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
1167
+ appendDiagnosticAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
783
1168
  const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
784
1169
  nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
785
1170
  containerBackoffMs = backoff.nextDelayMs;
@@ -795,7 +1180,7 @@ export async function serveSandboxControl(
795
1180
  } catch (error) {
796
1181
  if (!brokerOwns()) break;
797
1182
  containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_ABSENT_GC_FAILED';
798
- appendSandboxControlAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
1183
+ appendDiagnosticAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
799
1184
  const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
800
1185
  nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
801
1186
  containerBackoffMs = backoff.nextDelayMs;
@@ -858,6 +1243,10 @@ export async function serveSandboxControl(
858
1243
  if (!brokerOwns()) break;
859
1244
  let terminalCommitted = false;
860
1245
  if (settledExecution.result && settledExecution.resultEvidenceWritten) {
1246
+ if (!consumeLifecycleAuthorityForResult(manifest, settledExecution)) {
1247
+ active = settledExecution;
1248
+ continue;
1249
+ }
861
1250
  terminalCommitted = publishExecutionResult(manifest, settledExecution.request, settledExecution.result, broker, brokerOwns);
862
1251
  if (terminalCommitted && settledExecution.request.family === 'task-finalization') {
863
1252
  try {
@@ -872,7 +1261,7 @@ export async function serveSandboxControl(
872
1261
  }
873
1262
  if (!terminalCommitted) return;
874
1263
  appendBrokerAudit(manifest, 'executor-result-published', {
875
- ...requestAuditFields(manifest, manifestPath, settledExecution.request),
1264
+ ...requestAuditFields(manifest, settledExecution.request),
876
1265
  resultAvailable: settledExecution.result !== null,
877
1266
  resultEvidenceWritten: settledExecution.resultEvidenceWritten,
878
1267
  ...(settledExecution.result ? resultAuditFields(settledExecution.result) : {})
@@ -889,6 +1278,7 @@ export async function serveSandboxControl(
889
1278
  const source = path.join(requestsDir, name);
890
1279
  let claimed: string | null = null;
891
1280
  let validatedRequest: SandboxControlRequest | null = null;
1281
+ let prepared: PreparedSandboxControlExecution | null = null;
892
1282
  try {
893
1283
  if (!brokerOwns()) {
894
1284
  retiring = true;
@@ -914,69 +1304,84 @@ export async function serveSandboxControl(
914
1304
  if (active) throw new Error('SANDBOX_CONTROL_BUSY');
915
1305
  const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
916
1306
  validatedRequest = request;
1307
+ criticalRequestPhase(manifest, request, 'validated', 'in-progress');
917
1308
  appendBrokerAudit(manifest, 'request-validated', {
918
- ...requestAuditFields(manifest, manifestPath, request),
1309
+ ...requestAuditFields(manifest, request),
919
1310
  requestPath: claimed
920
1311
  });
921
1312
  if (bindingCheck(manifest)) throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
922
1313
  if (readActiveLease(manifest)) throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
1314
+ criticalRequestPhase(manifest, request, 'gated', 'in-progress');
923
1315
  appendBrokerAudit(manifest, 'request-gates-passed', {
924
- ...requestAuditFields(manifest, manifestPath, request),
1316
+ ...requestAuditFields(manifest, request),
925
1317
  requestPath: claimed,
926
1318
  bindingChecked: true,
927
1319
  handoffLeaseChecked: true
928
1320
  });
1321
+ criticalRequestPhase(manifest, request, 'reserved', 'in-progress');
929
1322
  writeSandboxControlReservation(manifest, request.id, sandboxControlGenerationUsage(manifest));
930
1323
  appendBrokerAudit(manifest, 'executor-reservation-written', {
931
- ...requestAuditFields(manifest, manifestPath, request),
1324
+ ...requestAuditFields(manifest, request),
932
1325
  requestPath: claimed,
933
1326
  executionPath: executionPath(manifest, request.id)
934
1327
  });
935
1328
  appendBrokerAudit(manifest, 'executor-prepare-start', {
936
- ...requestAuditFields(manifest, manifestPath, request),
1329
+ ...requestAuditFields(manifest, request),
937
1330
  requestPath: claimed,
938
1331
  executorCwd: manifest.repoRoot,
939
1332
  executorEntry: options.internalCliPath ?? process.argv[1] ?? null
940
1333
  });
941
- const prepared = await prepareExecution({
1334
+ const lifecycleAuthority = lifecycleAuthorityForRequest(manifest, manifestPath, request);
1335
+ if (lifecycleAuthority?.status === 'rejected') {
1336
+ throw Object.assign(
1337
+ new Error(lifecycleAuthority.error?.message ?? 'lifecycle authority was rejected'),
1338
+ { code: lifecycleAuthority.error?.code ?? 'LIFECYCLE_AUTHORITY_REJECTED' }
1339
+ );
1340
+ }
1341
+ prepared = await prepareExecution({
942
1342
  manifest, manifestPath, request, requestPath: claimed,
943
- internalCliPath: options.internalCliPath ?? process.argv[1]!
1343
+ internalCliPath: options.internalCliPath ?? process.argv[1]!,
1344
+ ...(lifecycleAuthority ? { lifecycleAuthority } : {})
944
1345
  });
1346
+ const preparedExecution = prepared;
1347
+ if (!preparedExecution) throw new Error('SANDBOX_CONTROL_EXECUTION_PREPARE_INVALID');
1348
+ criticalRequestPhase(manifest, request, 'prepared', 'in-progress', `${preparedExecution.execution.child.pid}`);
945
1349
  appendBrokerAudit(manifest, 'executor-prepared', {
946
- ...requestAuditFields(manifest, manifestPath, request),
1350
+ ...requestAuditFields(manifest, request),
947
1351
  requestPath: claimed,
948
1352
  executorCwd: manifest.repoRoot,
949
- childPid: prepared.execution.child.pid,
950
- childStartTime: prepared.execution.child.startTime,
951
- childProcessGroupId: prepared.execution.child.processGroupId,
1353
+ childPid: preparedExecution.execution.child.pid,
1354
+ childStartTime: preparedExecution.execution.child.startTime,
1355
+ childProcessGroupId: preparedExecution.execution.child.processGroupId,
952
1356
  executionPath: executionPath(manifest, request.id)
953
1357
  });
954
1358
  const execution: ActiveExecution = {
955
- request, prepared, result: null, resultEvidenceWritten: false, failure: null, settled: false
1359
+ request, prepared: preparedExecution, lifecycleAuthority, result: null,
1360
+ resultEvidenceWritten: false, failure: null, settled: false
956
1361
  };
957
1362
  active = execution;
958
- prepared.completion.then(
1363
+ preparedExecution.completion.then(
959
1364
  (result) => {
960
1365
  execution.result = sanitizeSandboxControlResult(manifest, result);
961
1366
  appendBrokerAudit(manifest, 'executor-completed', {
962
- ...requestAuditFields(manifest, manifestPath, request),
1367
+ ...requestAuditFields(manifest, request),
963
1368
  ...resultAuditFields(execution.result),
964
- childPid: prepared.execution.child.pid,
965
- childStartTime: prepared.execution.child.startTime
1369
+ childPid: preparedExecution.execution.child.pid,
1370
+ childStartTime: preparedExecution.execution.child.startTime
966
1371
  });
967
1372
  try {
968
1373
  writeSandboxControlResultEvidence(manifest, request.id, execution.result);
969
1374
  execution.resultEvidenceWritten = true;
970
1375
  execution.settled = true;
971
1376
  appendBrokerAudit(manifest, 'executor-result-evidence-written', {
972
- ...requestAuditFields(manifest, manifestPath, request),
1377
+ ...requestAuditFields(manifest, request),
973
1378
  resultPath: path.join(manifest.processingDir, request.id, 'result.json'),
974
1379
  ...resultAuditFields(execution.result)
975
1380
  });
976
1381
  } catch (error) {
977
1382
  execution.failure = error;
978
1383
  appendBrokerAudit(manifest, 'executor-result-evidence-failed', {
979
- ...requestAuditFields(manifest, manifestPath, request),
1384
+ ...requestAuditFields(manifest, request),
980
1385
  errorType: error instanceof Error ? error.name : typeof error,
981
1386
  });
982
1387
  }
@@ -985,9 +1390,9 @@ export async function serveSandboxControl(
985
1390
  execution.failure = error;
986
1391
  execution.settled = true;
987
1392
  appendBrokerAudit(manifest, 'executor-failed', {
988
- ...requestAuditFields(manifest, manifestPath, request),
989
- childPid: prepared.execution.child.pid,
990
- childStartTime: prepared.execution.child.startTime,
1393
+ ...requestAuditFields(manifest, request),
1394
+ childPid: preparedExecution.execution.child.pid,
1395
+ childStartTime: preparedExecution.execution.child.startTime,
991
1396
  errorType: error instanceof Error ? error.name : typeof error,
992
1397
  });
993
1398
  }
@@ -1004,11 +1409,13 @@ export async function serveSandboxControl(
1004
1409
  retiring = true;
1005
1410
  continue;
1006
1411
  }
1412
+ criticalRequestPhase(manifest, request, 'accepted-authorized', 'in-progress');
1007
1413
  writeAcceptedResponse(manifest, {
1008
1414
  version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
1009
1415
  });
1416
+ writeSandboxControlTransition(manifest, { requestId: request.id, phase: 'accepted-committed' });
1010
1417
  appendBrokerAudit(manifest, 'request-accepted', {
1011
- ...requestAuditFields(manifest, manifestPath, request),
1418
+ ...requestAuditFields(manifest, request),
1012
1419
  acceptedPath: acceptedResponsePath(manifest, id),
1013
1420
  executionPhase: 'prepared'
1014
1421
  });
@@ -1019,8 +1426,9 @@ export async function serveSandboxControl(
1019
1426
  break;
1020
1427
  }
1021
1428
  try {
1429
+ criticalRequestPhase(manifest, request, 'start-authorized', 'in-progress', `${prepared.execution.child.pid}`);
1022
1430
  appendBrokerAudit(manifest, 'executor-start', {
1023
- ...requestAuditFields(manifest, manifestPath, request),
1431
+ ...requestAuditFields(manifest, request),
1024
1432
  childPid: prepared.execution.child.pid,
1025
1433
  childStartTime: prepared.execution.child.startTime,
1026
1434
  executionPath: executionPath(manifest, request.id)
@@ -1028,7 +1436,7 @@ export async function serveSandboxControl(
1028
1436
  prepared.start(brokerOwns);
1029
1437
  } catch (error) {
1030
1438
  appendBrokerAudit(manifest, 'executor-start-failed', {
1031
- ...requestAuditFields(manifest, manifestPath, request),
1439
+ ...requestAuditFields(manifest, request),
1032
1440
  errorType: error instanceof Error ? error.name : typeof error,
1033
1441
  });
1034
1442
  const owned = brokerOwns();
@@ -1056,6 +1464,11 @@ export async function serveSandboxControl(
1056
1464
  continue;
1057
1465
  }
1058
1466
  } catch (error) {
1467
+ if (prepared) {
1468
+ try { prepared.terminate(brokerOwns()); } catch { /* cleanup is best effort after a failed admission */ }
1469
+ prepared = null;
1470
+ active = null;
1471
+ }
1059
1472
  if (!brokerOwns()) {
1060
1473
  retiring = true;
1061
1474
  break;
@@ -1075,6 +1488,7 @@ export async function serveSandboxControl(
1075
1488
  retiring = true;
1076
1489
  break;
1077
1490
  }
1491
+ removeAcceptedResponse(manifest, id);
1078
1492
  if (claimed) fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
1079
1493
  }
1080
1494
  }
@@ -1096,7 +1510,8 @@ export async function serveSandboxControl(
1096
1510
  }
1097
1511
  }
1098
1512
  if (owned && active.result && active.resultEvidenceWritten) {
1099
- if (publishExecutionResult(manifest, active.request, active.result, broker, brokerOwns)) {
1513
+ if (consumeLifecycleAuthorityForResult(manifest, active)
1514
+ && publishExecutionResult(manifest, active.request, active.result, broker, brokerOwns)) {
1100
1515
  if (brokerOwns()) {
1101
1516
  removeAcceptedResponse(manifest, active.request.id);
1102
1517
  fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
@@ -1126,7 +1541,7 @@ export async function serveSandboxControl(
1126
1541
  }
1127
1542
  }
1128
1543
  if (brokerOwns()) {
1129
- appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
1544
+ appendDiagnosticAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
1130
1545
  try {
1131
1546
  if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord) fs.unlinkSync(brokerPath);
1132
1547
  } catch {