@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,7 +1,7 @@
1
1
  import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { createHash, randomUUID } from 'node:crypto';
4
+ import { randomUUID } from 'node:crypto';
5
5
  import { getProcessStartTime } from '../../server/process-state.ts';
6
6
  import {
7
7
  createTask,
@@ -11,20 +11,32 @@ import {
11
11
  type TaskCreateResult
12
12
  } from '../../task/create-service.ts';
13
13
  import { applyTaskFinalization } from '../../task/finalization.ts';
14
- import { bindSandboxControlTask, validateSandboxControlRequest, type SandboxControlExecution, type SandboxControlManifest, type SandboxControlRequest } from './protocol.ts';
15
14
  import {
16
- appendSandboxControlAudit,
15
+ bindSandboxControlTask,
16
+ validateSandboxControlRequest,
17
+ type SandboxControlExecution,
18
+ type SandboxControlExecutorGateV2,
19
+ type SandboxControlManifest,
20
+ type SandboxControlRequest
21
+ } from './protocol.ts';
22
+ import {
17
23
  atomicWriteJson,
18
24
  executionPath,
19
25
  readActiveLease,
20
26
  terminateSandboxControlExecution
21
27
  } from './state.ts';
28
+ import { requestAuditFields, resultAuditFields, appendDiagnosticAudit, createSandboxControlAuditContext, appendCriticalAudit, writeSandboxControlTransition } from './audit.ts';
29
+ import { validateSandboxControlIdentity } from './identity-sentinel.ts';
22
30
  import {
23
31
  createSandboxExecutorExecutionContext,
24
32
  dispatchTaskControlOperation,
25
33
  parseTaskControlOperation,
34
+ validateLifecycleRecoveryAttestation,
35
+ type LifecycleAuthorityResponseV1,
36
+ type LifecycleRecoveryAttestationV1,
26
37
  type TaskControlOperation
27
38
  } from '../../task/control-authority.ts';
39
+ import { executeTaskWorkflow } from './workflow-executor.ts';
28
40
  import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
29
41
  import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.ts';
30
42
  import {
@@ -86,52 +98,12 @@ function appendExecutorAudit(
86
98
  fields: Record<string, string | number | boolean | null> = {}
87
99
  ): void {
88
100
  try {
89
- appendSandboxControlAudit(manifest, event, { source: 'executor', ...fields });
101
+ appendDiagnosticAudit(manifest, event, { source: 'executor', ...fields });
90
102
  } catch {
91
103
  // Diagnostics must never change the control protocol.
92
104
  }
93
105
  }
94
106
 
95
- function requestAuditFields(
96
- manifest: SandboxControlManifest,
97
- manifestPath: string,
98
- request: SandboxControlRequest
99
- ): Record<string, string | number | boolean | null> {
100
- const args = 'args' in request ? request.args : [];
101
- const encodedArgs = JSON.stringify(args);
102
- return {
103
- requestId: request.id,
104
- requestFamily: request.family,
105
- sandboxTaskId: manifest.taskId,
106
- requestGeneration: request.generation,
107
- requestIssuedAt: request.issuedAt,
108
- requestExpiresAt: request.expiresAt,
109
- requestArgCount: args.length,
110
- requestArgsSha256: createHash('sha256').update(encodedArgs, 'utf8').digest('hex'),
111
- requestTaskRef: args[0] ?? null,
112
- requestCommand: args[1] ?? null,
113
- controllerProofPresent: request.controllerProof !== null,
114
- hostCwd: process.cwd(),
115
- manifestPath,
116
- manifestPathRealpath: safeRealpath(manifestPath),
117
- repoRoot: manifest.repoRoot,
118
- repoRootRealpath: safeRealpath(manifest.repoRoot),
119
- worktreeRoot: manifest.worktreeRoot,
120
- worktreeRootRealpath: safeRealpath(manifest.worktreeRoot),
121
- runtimeDir: manifest.runtimeDir,
122
- runtimeDirRealpath: safeRealpath(manifest.runtimeDir)
123
- };
124
- }
125
-
126
- function resultAuditFields(result: SandboxControlExecutionResult): Record<string, string | number | boolean | null> {
127
- return {
128
- exitCode: result.exitCode,
129
- stdoutBytes: Buffer.byteLength(result.stdout, 'utf8'),
130
- stderrBytes: Buffer.byteLength(result.stderr, 'utf8'),
131
- stdoutSha256: createHash('sha256').update(result.stdout, 'utf8').digest('hex'),
132
- stderrSha256: createHash('sha256').update(result.stderr, 'utf8').digest('hex')
133
- };
134
- }
135
107
 
136
108
  export function nodeEntryArgs(entry: string, args: string[]): string[] {
137
109
  return path.extname(entry) === '.ts'
@@ -155,6 +127,7 @@ export async function prepareSandboxControlExecution(params: {
155
127
  request: SandboxControlRequest;
156
128
  requestPath: string;
157
129
  internalCliPath: string;
130
+ lifecycleAuthority?: LifecycleAuthorityResponseV1;
158
131
  }): Promise<PreparedSandboxControlExecution> {
159
132
  const nonce = randomUUID();
160
133
  const child = spawn(
@@ -208,7 +181,16 @@ export async function prepareSandboxControlExecution(params: {
208
181
  if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
209
182
  atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
210
183
  if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
211
- child.send({ version: 1, nonce, owner: gateOwner });
184
+ const authority = params.lifecycleAuthority?.attestation ?? null;
185
+ child.send({
186
+ version: 2,
187
+ nonce,
188
+ owner: gateOwner,
189
+ requestId: params.request.id,
190
+ operationId: authority?.operationId ?? params.request.id,
191
+ phase: authority?.phase ?? (params.request.family === 'task-workflow' ? 'artifact.finalize-local' : 'orchestration.prepare'),
192
+ authority
193
+ } satisfies SandboxControlExecutorGateV2);
212
194
  },
213
195
  completion,
214
196
  terminate(updateState = true) {
@@ -222,7 +204,7 @@ export async function prepareSandboxControlExecution(params: {
222
204
  };
223
205
  }
224
206
 
225
- function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner> {
207
+ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<Readonly<{ owner: BrokerOwner; authority: LifecycleRecoveryAttestationV1 | null; requestId: string; operationId: string; phase: SandboxControlExecutorGateV2['phase'] }>> {
226
208
  return new Promise((resolve, reject) => {
227
209
  const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
228
210
  const onDisconnect = () => {
@@ -237,9 +219,14 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner> {
237
219
  version?: unknown;
238
220
  nonce?: unknown;
239
221
  owner?: Partial<BrokerOwner> | null;
222
+ requestId?: unknown;
223
+ operationId?: unknown;
224
+ phase?: unknown;
225
+ authority?: unknown;
240
226
  } | null;
241
227
  const owner = value?.owner;
242
- if (!value || value.version !== 1 || value.nonce !== nonce
228
+ if (!value || Object.keys(value).sort().join(',') !== 'authority,nonce,operationId,owner,phase,requestId,version'
229
+ || value.version !== 2 || value.nonce !== nonce
243
230
  || !owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
244
231
  || typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
245
232
  || typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
@@ -247,7 +234,21 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner> {
247
234
  reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
248
235
  return;
249
236
  }
250
- resolve(owner as BrokerOwner);
237
+ if (typeof value.requestId !== 'string' || typeof value.operationId !== 'string'
238
+ || !['orchestration.prepare', 'artifact.finalize-local', 'task-event.completed'].includes(value.phase as string)
239
+ || (value.authority !== null && value.authority !== undefined && (() => {
240
+ try { validateLifecycleRecoveryAttestation(value.authority); return false; } catch { return true; }
241
+ })())) {
242
+ reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
243
+ return;
244
+ }
245
+ resolve({
246
+ owner: owner as BrokerOwner,
247
+ authority: value.authority === null || value.authority === undefined ? null : value.authority as LifecycleRecoveryAttestationV1,
248
+ requestId: value.requestId,
249
+ operationId: value.operationId,
250
+ phase: value.phase as SandboxControlExecutorGateV2['phase']
251
+ });
251
252
  });
252
253
  });
253
254
  }
@@ -327,6 +328,7 @@ function finalizationResult(result: Awaited<ReturnType<typeof applyTaskFinalizat
327
328
  type ExecuteRequestOptions = Readonly<{
328
329
  buildIdentity?: typeof computeLifecycleBuildIdentity;
329
330
  resolveControllerBinding?: typeof resolveCodexControllerBinding;
331
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
330
332
  }>;
331
333
 
332
334
  async function executeRequestInner(
@@ -394,7 +396,7 @@ async function executeRequestInner(
394
396
  : /^([A-Z][A-Z0-9_]+)/u.exec(error instanceof Error ? error.message : String(error))?.[1]
395
397
  ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
396
398
  appendExecutorAudit(manifest, 'controller-operation-failed', {
397
- ...requestAuditFields(manifest, manifestPath, request),
399
+ ...requestAuditFields(manifest, request),
398
400
  controllerCommand: request.command,
399
401
  errorCode,
400
402
  errorType: error instanceof Error ? error.name : typeof error,
@@ -402,6 +404,7 @@ async function executeRequestInner(
402
404
  return controllerFailure(error);
403
405
  }
404
406
  }
407
+ if (request.family === 'task-workflow') return executeTaskWorkflow(manifest, request.workflow, options.lifecycleRecoveryAttestation ?? null);
405
408
  if (request.family === 'task-finalization') {
406
409
  const operation = parseTaskControlOperation(
407
410
  'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
@@ -415,7 +418,8 @@ async function executeRequestInner(
415
418
  taskId: manifest.taskId!,
416
419
  generation: manifest.generation,
417
420
  manifestPath,
418
- requestId: request.id
421
+ requestId: request.id,
422
+ lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
419
423
  }),
420
424
  operation
421
425
  );
@@ -445,23 +449,31 @@ async function executeRequestInner(
445
449
  && operation.input.client === 'codex';
446
450
  if (codexPrepare) {
447
451
  if (!request.controllerProof) {
448
- return orchestrationFailure(
449
- 'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
450
- 'Codex prepare requires a current controller lease proof'
451
- );
452
- }
453
- try {
454
- controllerBinding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
455
- manifest,
456
- manifestPath,
457
- proof: request.controllerProof,
458
- buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
459
- });
460
- } catch (error) {
461
- const code = error instanceof CodexControllerRegistrationError
462
- ? error.code
463
- : 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID';
464
- return orchestrationFailure(code, `${code}: Codex controller proof was rejected`);
452
+ const authority = options.lifecycleRecoveryAttestation;
453
+ if (!authority || authority.phase !== 'orchestration.prepare') {
454
+ return orchestrationFailure(
455
+ 'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
456
+ 'Codex prepare requires a current controller lease proof'
457
+ );
458
+ }
459
+ controllerBinding = {
460
+ instanceDigest: authority.controllerInstanceDigest,
461
+ controlGeneration: authority.controlGeneration
462
+ };
463
+ } else {
464
+ try {
465
+ controllerBinding = (options.resolveControllerBinding ?? resolveCodexControllerBinding)({
466
+ manifest,
467
+ manifestPath,
468
+ proof: request.controllerProof,
469
+ buildIdentity: (options.buildIdentity ?? computeLifecycleBuildIdentity)(manifest.repoRoot)
470
+ });
471
+ } catch (error) {
472
+ const code = error instanceof CodexControllerRegistrationError
473
+ ? error.code
474
+ : 'CODEX_SANDBOX_CONTROLLER_PROOF_INVALID';
475
+ return orchestrationFailure(code, `${code}: Codex controller proof was rejected`);
476
+ }
465
477
  }
466
478
  } else if (request.controllerProof !== null) {
467
479
  return orchestrationFailure(
@@ -487,7 +499,8 @@ async function executeRequestInner(
487
499
  manifestPath,
488
500
  requestId: request.id,
489
501
  diagnosticLog,
490
- ...(controllerBinding ? { controllerBinding } : {})
502
+ ...(controllerBinding ? { controllerBinding } : {}),
503
+ lifecycleRecoveryAttestation: options.lifecycleRecoveryAttestation ?? null
491
504
  });
492
505
  const format = (value: unknown): SandboxControlExecutionResult => {
493
506
  const result = value as { status?: string };
@@ -511,7 +524,7 @@ export async function executeRequest(
511
524
  request: SandboxControlRequest,
512
525
  options: ExecuteRequestOptions = {}
513
526
  ): Promise<SandboxControlExecutionResult> {
514
- const fields = requestAuditFields(manifest, manifestPath, request);
527
+ const fields = requestAuditFields(manifest, request);
515
528
  appendExecutorAudit(manifest, 'executor-request-start', fields);
516
529
  try {
517
530
  const result = await executeRequestInner(manifest, manifestPath, request, options);
@@ -534,11 +547,23 @@ export async function executeRequest(
534
547
  }
535
548
 
536
549
  export async function runSandboxControlExecutor(requestPath: string, nonce: string): Promise<void> {
537
- const gateOwner = await waitForGate(nonce);
550
+ const gate = await waitForGate(nonce);
551
+ const gateOwner = gate.owner;
538
552
  process.disconnect?.();
539
553
  const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
540
554
  if (!manifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
541
555
  const manifest = readSandboxControlManifest(manifestPath);
556
+ const identity = validateSandboxControlIdentity({
557
+ publicStatusDir: manifest.publicStatusDir,
558
+ root: path.dirname(path.resolve(manifestPath)),
559
+ mode: manifest.mode,
560
+ taskId: manifest.taskId,
561
+ generation: manifest.generation,
562
+ controlRootId: manifest.controlRootId
563
+ });
564
+ if (identity.state !== 'valid') {
565
+ throw new Error(`SANDBOX_CONTROL_IDENTITY_${identity.state.replaceAll('-', '_').toUpperCase()}`);
566
+ }
542
567
  const root = fs.realpathSync.native(process.cwd());
543
568
  const expectedRoot = safeRealpath(manifest.repoRoot);
544
569
  if (expectedRoot !== root) {
@@ -551,13 +576,20 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
551
576
  }
552
577
  const raw = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as unknown;
553
578
  const request = validateSandboxControlRequest(raw, manifest);
554
- appendExecutorAudit(manifest, 'executor-request-validated', requestAuditFields(manifest, manifestPath, request));
579
+ if (gate.requestId !== request.id) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_REQUEST_MISMATCH');
580
+ if (gate.authority !== null
581
+ && (gate.authority.requestId !== request.id
582
+ || gate.authority.operationId !== gate.operationId
583
+ || gate.authority.phase !== gate.phase)) {
584
+ throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_AUTHORITY_MISMATCH');
585
+ }
586
+ appendExecutorAudit(manifest, 'executor-request-validated', requestAuditFields(manifest, request));
555
587
  const requestDirectory = path.resolve(path.dirname(requestPath));
556
588
  const processingDirectory = path.resolve(path.join(manifest.processingDir, request.id));
557
589
  const channelRequestDirectory = path.resolve(path.join(manifest.channelDir, 'requests'));
558
590
  if (requestDirectory !== processingDirectory && requestDirectory !== channelRequestDirectory) {
559
591
  appendExecutorAudit(manifest, 'executor-request-path-validation-failed', {
560
- ...requestAuditFields(manifest, manifestPath, request),
592
+ ...requestAuditFields(manifest, request),
561
593
  requestPath,
562
594
  requestDirectory,
563
595
  processingDirectory,
@@ -568,10 +600,32 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
568
600
  let result: SandboxControlExecutionResult;
569
601
  try {
570
602
  assertSandboxControlExecutorAuthority(manifest, gateOwner);
571
- result = await executeRequest(manifest, manifestPath, request);
603
+ const operation = request.family === 'task-lifecycle' || request.family === 'task-orchestration'
604
+ ? (() => {
605
+ try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
606
+ })()
607
+ : null;
608
+ const context = createSandboxControlAuditContext(manifest, {
609
+ requestId: request.id,
610
+ family: request.family,
611
+ operation: operation?.family === 'task-orchestration'
612
+ ? operation.intent
613
+ : operation?.family === 'task-lifecycle'
614
+ ? operation.request.intent
615
+ : request.family === 'task-finalization' ? request.operation : null,
616
+ phase: 'started-committed',
617
+ outcome: 'in-progress'
618
+ });
619
+ appendCriticalAudit(manifest, context, { transition: 'started-committed' });
620
+ writeSandboxControlTransition(manifest, { requestId: request.id, phase: 'started-committed' });
621
+ const execution = readJsonExecution(executionPath(manifest, request.id));
622
+ atomicWriteJson(executionPath(manifest, request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
623
+ result = await executeRequest(manifest, manifestPath, request, {
624
+ lifecycleRecoveryAttestation: gate.authority
625
+ });
572
626
  } catch (error) {
573
627
  appendExecutorAudit(manifest, 'executor-authority-or-dispatch-failed', {
574
- ...requestAuditFields(manifest, manifestPath, request),
628
+ ...requestAuditFields(manifest, request),
575
629
  errorType: error instanceof Error ? error.name : typeof error,
576
630
  });
577
631
  const detail = error instanceof Error ? error.message : String(error);
@@ -596,6 +650,12 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
596
650
  process.exitCode = result.exitCode;
597
651
  }
598
652
 
653
+ function readJsonExecution(filePath: string): SandboxControlExecution {
654
+ const value = JSON.parse(fs.readFileSync(filePath, 'utf8')) as SandboxControlExecution;
655
+ if (!value || value.requestId.length === 0) throw new Error('SANDBOX_CONTROL_EXECUTION_INVALID');
656
+ return value;
657
+ }
658
+
599
659
  export function disconnectExecutor(child: ChildProcess): void {
600
660
  if (child.connected) child.disconnect();
601
661
  }
@@ -0,0 +1,29 @@
1
+ import type { TaskFinalizationReceipt } from '../../task/finalization.ts';
2
+ import type { SandboxControlResponse } from './protocol.ts';
3
+
4
+ export function finalizationTerminalResponse(taskId: string, requestId: string, receipt: TaskFinalizationReceipt): SandboxControlResponse {
5
+ const pendingSteps = [
6
+ receipt.taskComment === 'pending' ? 'task-comment' : null,
7
+ receipt.verification === 'pending' ? 'verification' : null
8
+ ].filter((step): step is string => step !== null);
9
+ const completedSteps = ['lifecycle', receipt.taskComment === 'pending' ? null : 'task-comment', receipt.verification === 'pending' ? null : 'verification']
10
+ .filter((step): step is string => step !== null);
11
+ const warnings = receipt.warnings
12
+ .filter((warning) => warning.status === 'open')
13
+ .map(({ status: _status, resolvedAt: _resolvedAt, ...warning }) => warning);
14
+ const result = {
15
+ status: 'completed', changed: false, taskId,
16
+ lifecycle: { status: 'no-op', changed: false, error: null },
17
+ taskComment: receipt.taskComment === 'pending' ? null : { status: 'no-op', changed: false, error: null },
18
+ verification: receipt.verification === 'pending' ? null : { status: 'no-op', changed: false, error: null },
19
+ completedSteps, pendingSteps,
20
+ result: pendingSteps.length > 0 || receipt.warningProjection === 'pending' || warnings.length > 0
21
+ ? 'completed_with_warnings' : 'completed',
22
+ warnings, error: null
23
+ };
24
+ return {
25
+ version: 2, id: requestId, phase: 'completed', exitCode: 0,
26
+ stdout: `${JSON.stringify({ version: 1, status: 'completed', changed: false, accepted: true, result, error: null })}\n`,
27
+ stderr: '', error: null
28
+ };
29
+ }
@@ -0,0 +1,137 @@
1
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { writeDurableFile } from '../../fs/durable-write.ts';
5
+
6
+ export const SANDBOX_CONTROL_IDENTITY_FILE = 'identity.json';
7
+
8
+ export type SandboxControlIdentitySentinel = Readonly<{
9
+ version: 1;
10
+ mode: 'task-bound' | 'branch-only';
11
+ taskId: string | null;
12
+ generation: string;
13
+ controlRootId: string;
14
+ }>;
15
+
16
+ export type SandboxControlIdentityValidation = Readonly<{
17
+ state: 'valid' | 'missing' | 'malformed' | 'generation-mismatch' | 'root-id-mismatch' | 'topology-mismatch';
18
+ sentinel: SandboxControlIdentitySentinel | null;
19
+ }>;
20
+
21
+ function identityError(code: SandboxControlIdentityValidation['state']): Error {
22
+ return new Error(`SANDBOX_CONTROL_IDENTITY_${code.replaceAll('-', '_').toUpperCase()}`);
23
+ }
24
+
25
+ function canonicalIdentity(value: SandboxControlIdentitySentinel): string {
26
+ return JSON.stringify({
27
+ version: value.version,
28
+ mode: value.mode,
29
+ taskId: value.taskId,
30
+ generation: value.generation,
31
+ controlRootId: value.controlRootId
32
+ });
33
+ }
34
+
35
+ export function identitySentinelPath(publicStatusDir: string): string {
36
+ return path.join(publicStatusDir, SANDBOX_CONTROL_IDENTITY_FILE);
37
+ }
38
+
39
+ export function createSandboxControlRootId(): string {
40
+ return `${randomBytes(32).toString('hex')}${randomUUID().replaceAll('-', '')}`;
41
+ }
42
+
43
+ export function createSandboxControlIdentitySentinel(params: Readonly<{
44
+ mode: 'task-bound' | 'branch-only';
45
+ taskId: string | null;
46
+ generation: string;
47
+ controlRootId?: string;
48
+ }>): SandboxControlIdentitySentinel {
49
+ const sentinel: SandboxControlIdentitySentinel = {
50
+ version: 1,
51
+ mode: params.mode,
52
+ taskId: params.mode === 'task-bound' ? params.taskId : null,
53
+ generation: params.generation,
54
+ controlRootId: params.controlRootId ?? createSandboxControlRootId()
55
+ };
56
+ parseSandboxControlIdentitySentinel(sentinel);
57
+ return sentinel;
58
+ }
59
+
60
+ export function identityDigest(value: SandboxControlIdentitySentinel): string {
61
+ const normalized = parseSandboxControlIdentitySentinel(value);
62
+ return createHash('sha256').update(canonicalIdentity(normalized), 'utf8').digest('hex');
63
+ }
64
+
65
+ export function parseSandboxControlIdentitySentinel(value: unknown): SandboxControlIdentitySentinel {
66
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw identityError('malformed');
67
+ const candidate = value as Partial<SandboxControlIdentitySentinel>;
68
+ if (Object.keys(candidate).sort().join(',') !== 'controlRootId,generation,mode,taskId,version'
69
+ || candidate.version !== 1
70
+ || (candidate.mode !== 'task-bound' && candidate.mode !== 'branch-only')
71
+ || (candidate.taskId !== null && typeof candidate.taskId !== 'string')
72
+ || (candidate.mode === 'task-bound' && !candidate.taskId)
73
+ || (candidate.mode === 'branch-only' && candidate.taskId !== null)
74
+ || typeof candidate.generation !== 'string' || candidate.generation.length === 0
75
+ || typeof candidate.controlRootId !== 'string' || !/^[a-f0-9]{64,128}$/u.test(candidate.controlRootId)) {
76
+ throw identityError('malformed');
77
+ }
78
+ return candidate as SandboxControlIdentitySentinel;
79
+ }
80
+
81
+ export function writeSandboxControlIdentitySentinel(
82
+ publicStatusDir: string,
83
+ value: SandboxControlIdentitySentinel
84
+ ): SandboxControlIdentitySentinel {
85
+ const sentinel = parseSandboxControlIdentitySentinel(value);
86
+ fs.mkdirSync(publicStatusDir, { recursive: true, mode: 0o700 });
87
+ const target = identitySentinelPath(publicStatusDir);
88
+ writeDurableFile(target, `${canonicalIdentity(sentinel)}\n`, { mode: 0o400, replace: true });
89
+ return sentinel;
90
+ }
91
+
92
+ export function readSandboxControlIdentitySentinel(publicStatusDir: string): SandboxControlIdentitySentinel {
93
+ const filePath = identitySentinelPath(publicStatusDir);
94
+ if (!fs.existsSync(filePath)) throw identityError('missing');
95
+ try {
96
+ const stat = fs.lstatSync(filePath);
97
+ if (!stat.isFile() || stat.isSymbolicLink()) throw identityError('malformed');
98
+ return parseSandboxControlIdentitySentinel(JSON.parse(fs.readFileSync(filePath, 'utf8')));
99
+ } catch (error) {
100
+ if (error instanceof Error && error.message.startsWith('SANDBOX_CONTROL_IDENTITY_')) throw error;
101
+ throw identityError('malformed');
102
+ }
103
+ }
104
+
105
+ export function validateSandboxControlIdentity(params: Readonly<{
106
+ publicStatusDir: string;
107
+ root: string;
108
+ manifestRoot?: string;
109
+ mode: 'task-bound' | 'branch-only';
110
+ taskId: string | null;
111
+ generation: string;
112
+ controlRootId: string;
113
+ }>): SandboxControlIdentityValidation {
114
+ let sentinel: SandboxControlIdentitySentinel;
115
+ try {
116
+ sentinel = readSandboxControlIdentitySentinel(params.publicStatusDir);
117
+ } catch (error) {
118
+ const code = error instanceof Error ? error.message : '';
119
+ if (code.endsWith('MISSING')) return { state: 'missing', sentinel: null };
120
+ return { state: 'malformed', sentinel: null };
121
+ }
122
+ if (sentinel.generation !== params.generation) return { state: 'generation-mismatch', sentinel };
123
+ if (sentinel.controlRootId !== params.controlRootId) return { state: 'root-id-mismatch', sentinel };
124
+ if (sentinel.mode !== params.mode || sentinel.taskId !== params.taskId) return { state: 'topology-mismatch', sentinel };
125
+ if (params.manifestRoot && path.resolve(params.manifestRoot) !== path.resolve(params.root)) {
126
+ return { state: 'topology-mismatch', sentinel };
127
+ }
128
+ return { state: 'valid', sentinel };
129
+ }
130
+
131
+ export function assertSandboxControlIdentity(params: Parameters<typeof validateSandboxControlIdentity>[0]): SandboxControlIdentitySentinel {
132
+ const result = validateSandboxControlIdentity(params);
133
+ if (result.state !== 'valid' || !result.sentinel) {
134
+ throw new Error(`SANDBOX_CONTROL_IDENTITY_${result.state.replaceAll('-', '_').toUpperCase()}`);
135
+ }
136
+ return result.sentinel;
137
+ }
@@ -85,6 +85,7 @@ export type SandboxRemovalTargetCommit = Readonly<{
85
85
  }>;
86
86
  }>[];
87
87
  }>;
88
+
88
89
  export type SandboxRemovalJournal = Readonly<{
89
90
  version: 2;
90
91
  operation: 'sandbox-rm';
@@ -348,7 +349,7 @@ type RemovalJournalCursor = Readonly<{
348
349
  write(phase: SandboxRemovalJournalPhase): SandboxRemovalJournal;
349
350
  }>;
350
351
 
351
- function defaultRemovalTarget(manifest: SandboxControlManifest): SandboxRemovalTargetCommit {
352
+ function defaultRemovalTarget(manifest: Pick<SandboxControlManifest, 'project' | 'branch' | 'container' | 'channelDir'>): SandboxRemovalTargetCommit {
352
353
  const targetDigest = createHash('sha256')
353
354
  .update(`${manifest.project}\0${manifest.branch}\0${manifest.container}\0${manifest.channelDir}`)
354
355
  .digest('hex');
@@ -370,6 +371,18 @@ function defaultRemovalTarget(manifest: SandboxControlManifest): SandboxRemovalT
370
371
  };
371
372
  }
372
373
 
374
+ export function isDefaultSandboxRemovalJournal(journal: SandboxRemovalJournal): boolean {
375
+ if (journal.phase !== 'carrier-removed' && journal.phase !== 'completed') return false;
376
+ const { target } = journal;
377
+ const expected = defaultRemovalTarget({
378
+ project: target.project, branch: target.branch,
379
+ container: path.basename(path.dirname(target.controlRoot)),
380
+ channelDir: path.join(target.controlRoot, 'channel')
381
+ });
382
+ return Object.entries(expected).every(([key, value]) =>
383
+ JSON.stringify(target[key as keyof SandboxRemovalTargetCommit]) === JSON.stringify(value));
384
+ }
385
+
373
386
  function sameRemovalTarget(left: SandboxRemovalTargetCommit, right: SandboxRemovalTargetCommit): boolean {
374
387
  return JSON.stringify(left) === JSON.stringify(right);
375
388
  }
@@ -560,6 +573,21 @@ export function advanceSandboxRemovalJournalPhase(
560
573
  }
561
574
  }
562
575
 
576
+ export function advanceSandboxRemovalJournalToPhase(
577
+ journal: SandboxRemovalJournal,
578
+ phase: SandboxRemovalJournalPhase,
579
+ resourceLock: SandboxResourceLock
580
+ ): SandboxRemovalJournal {
581
+ let current = journal;
582
+ while (phaseIndex(current.phase) < phaseIndex(phase)) {
583
+ const next = current.phase === 'target-committed' && phase === 'container-absent'
584
+ ? phase : SANDBOX_REMOVAL_JOURNAL_PHASES[phaseIndex(current.phase) + 1];
585
+ if (!next) throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
586
+ current = advanceSandboxRemovalJournalPhase(current, next, { resourceLock });
587
+ }
588
+ return current;
589
+ }
590
+
563
591
  async function awaitWithDeadline<T>(
564
592
  operation: () => Promise<T>,
565
593
  deadlineAt: number,
@@ -1051,7 +1079,7 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
1051
1079
  const expectedKeys = [
1052
1080
  'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
1053
1081
  'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
1054
- 'taskId', 'token', 'worktreeRoot', 'authorityEvidence'
1082
+ 'taskId', 'token', 'worktreeRoot', 'authorityEvidence', 'controlRootId',
1055
1083
  ];
1056
1084
  const actualKeys = Object.keys(manifest).sort().join(',');
1057
1085
  if (actualKeys !== expectedKeys.sort().join(',')) {
@@ -1075,6 +1103,7 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
1075
1103
  || typeof candidate.processingDir !== 'string' || typeof candidate.runtimeDir !== 'string'
1076
1104
  || typeof candidate.token !== 'string'
1077
1105
  || typeof candidate.generation !== 'string'
1106
+ || typeof candidate.controlRootId !== 'string' || !/^[a-f0-9]{64,128}$/u.test(candidate.controlRootId)
1078
1107
  || (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
1079
1108
  || (candidate.mode === 'branch-only' && candidate.taskId !== null)
1080
1109
  || !isSandboxAuthorityEvidence(candidate.authorityEvidence)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
@@ -1103,10 +1132,11 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
1103
1132
  taskId: candidate.taskId,
1104
1133
  token: candidate.token,
1105
1134
  generation: candidate.generation,
1135
+ controlRootId: candidate.controlRootId,
1106
1136
  channelDir: candidate.channelDir,
1107
1137
  publicStatusDir: candidate.publicStatusDir,
1108
1138
  processingDir: candidate.processingDir,
1109
- runtimeDir: candidate.runtimeDir
1139
+ runtimeDir: candidate.runtimeDir,
1110
1140
  };
1111
1141
  return normalized;
1112
1142
  }
@@ -1380,6 +1410,18 @@ export type RemoveSandboxControlOptions = Readonly<{
1380
1410
  export async function removeSandboxControlRoot(
1381
1411
  root: string,
1382
1412
  options: RemoveSandboxControlOptions
1413
+ ): Promise<void> {
1414
+ try {
1415
+ await removeSandboxControlRootAttempt(root, options);
1416
+ } catch (error) {
1417
+ if (!(error instanceof Error) || error.message !== 'SANDBOX_CONTROL_OWNER_UNAVAILABLE') throw error;
1418
+ await removeSandboxControlRootAttempt(root, options);
1419
+ }
1420
+ }
1421
+
1422
+ async function removeSandboxControlRootAttempt(
1423
+ root: string,
1424
+ options: RemoveSandboxControlOptions
1383
1425
  ): Promise<void> {
1384
1426
  const resolvedRoot = path.resolve(root);
1385
1427
  if (!fs.existsSync(resolvedRoot)) return;