@fitlab-ai/agent-infra 0.9.9 → 0.9.11

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 (309) hide show
  1. package/README.md +13 -3
  2. package/README.zh-CN.md +13 -3
  3. package/bin/cli.ts +17 -4
  4. package/dist/bin/cli.js +19 -4
  5. package/dist/bin/internal-cli.js +55 -4
  6. package/dist/lib/agent-client.js +0 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +17 -10
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +11 -31
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +15 -33
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
  13. package/dist/lib/agent-clients/adapters/traecli.js +65 -0
  14. package/dist/lib/agent-clients/config.js +14 -99
  15. package/dist/lib/agent-clients/reconcile.js +1 -11
  16. package/dist/lib/agent-clients/schema.js +5 -4
  17. package/dist/lib/agent-clients/tokens.js +7 -17
  18. package/dist/lib/agent-clients/types.js +2 -1
  19. package/dist/lib/decide.js +14 -30
  20. package/dist/lib/defaults.json +9 -0
  21. package/dist/lib/init.js +2 -2
  22. package/dist/lib/internal/codex-lifecycle.js +2 -2
  23. package/dist/lib/internal/codex-sandbox-controller.js +4 -3
  24. package/dist/lib/internal/release-workflow.js +0 -1
  25. package/dist/lib/internal/sandbox-control.js +2 -2
  26. package/dist/lib/internal/task-finalization.js +19 -9
  27. package/dist/lib/internal/task-ledger.js +8 -2
  28. package/dist/lib/internal/task-lifecycle.js +17 -93
  29. package/dist/lib/internal/task-orchestration.js +30 -216
  30. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +77 -43
  31. package/dist/lib/merge.js +205 -103
  32. package/dist/lib/process-data/normalize.js +2 -9
  33. package/dist/lib/run/host.js +46 -16
  34. package/dist/lib/run/tui.js +5 -2
  35. package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
  36. package/dist/lib/sandbox/commands/create.js +3 -3
  37. package/dist/lib/sandbox/commands/enter.js +18 -4
  38. package/dist/lib/sandbox/config.js +3 -12
  39. package/dist/lib/sandbox/control/controller-registration.js +4 -2
  40. package/dist/lib/sandbox/control/executor.js +135 -40
  41. package/dist/lib/sandbox/control/lifecycle.js +29 -20
  42. package/dist/lib/sandbox/control/server.js +18 -16
  43. package/dist/lib/sandbox/mounts.js +3 -2
  44. package/dist/lib/sandbox/recovery.js +123 -22
  45. package/dist/lib/sandbox/workspace-identity.js +14 -0
  46. package/dist/lib/sandbox/workspace-view.js +8 -9
  47. package/dist/lib/self-update.js +278 -0
  48. package/dist/lib/server/protocol.js +5 -5
  49. package/dist/lib/task/activity-log.js +4 -1
  50. package/dist/lib/task/artifact-lifecycle.js +4 -2
  51. package/dist/lib/task/codex-orchestration.js +32 -35
  52. package/dist/lib/task/command-options.js +1 -3
  53. package/dist/lib/task/commands/cat.js +0 -5
  54. package/dist/lib/task/commands/decisions.js +76 -92
  55. package/dist/lib/task/commands/files.js +6 -6
  56. package/dist/lib/task/commands/grep.js +25 -61
  57. package/dist/lib/task/commands/issue-body.js +6 -6
  58. package/dist/lib/task/commands/log.js +25 -10
  59. package/dist/lib/task/commands/show.js +5 -6
  60. package/dist/lib/task/commands/status.js +8 -8
  61. package/dist/lib/task/commit-operation.js +21 -25
  62. package/dist/lib/task/commit-policy.js +1 -1
  63. package/dist/lib/task/control-authority.js +431 -0
  64. package/dist/lib/task/create.js +6 -1
  65. package/dist/lib/task/current-contract.js +35 -0
  66. package/dist/lib/task/decision-details.js +263 -0
  67. package/dist/lib/task/decision-intents.js +5 -2
  68. package/dist/lib/task/delegation-receipts.js +8 -7
  69. package/dist/lib/task/events.js +11 -5
  70. package/dist/lib/task/finalization.js +72 -46
  71. package/dist/lib/task/index.js +19 -29
  72. package/dist/lib/task/ledger-intents.js +8 -9
  73. package/dist/lib/task/ledger.js +13 -6
  74. package/dist/lib/task/lifecycle.js +16 -0
  75. package/dist/lib/task/orchestration.js +51 -14
  76. package/dist/lib/task/review-artifacts.js +31 -12
  77. package/dist/lib/task/review-finalization.js +19 -6
  78. package/dist/lib/task/verification-engine.js +38 -18
  79. package/dist/lib/task/verification-review.js +1 -1
  80. package/dist/lib/task/workspace-snapshot.js +5 -11
  81. package/dist/lib/task/write.js +11 -1
  82. package/dist/lib/update.js +6 -37
  83. package/dist/lib/version.js +5 -1
  84. package/dist/lifecycle-build-manifest.json +3 -3
  85. package/lib/agent-client.ts +0 -1
  86. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +25 -10
  87. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +16 -33
  88. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +16 -35
  89. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  90. package/lib/agent-clients/adapters/index.ts +3 -1
  91. package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
  92. package/lib/agent-clients/adapters/traecli.ts +66 -0
  93. package/lib/agent-clients/config.ts +14 -144
  94. package/lib/agent-clients/reconcile.ts +1 -12
  95. package/lib/agent-clients/schema.ts +5 -4
  96. package/lib/agent-clients/tokens.ts +7 -20
  97. package/lib/agent-clients/types.ts +2 -1
  98. package/lib/decide.ts +12 -22
  99. package/lib/defaults.json +9 -0
  100. package/lib/init.ts +2 -2
  101. package/lib/internal/codex-lifecycle.ts +2 -2
  102. package/lib/internal/codex-sandbox-controller.ts +4 -3
  103. package/lib/internal/release-workflow.ts +0 -1
  104. package/lib/internal/sandbox-control.ts +2 -2
  105. package/lib/internal/task-finalization.ts +27 -13
  106. package/lib/internal/task-ledger.ts +8 -2
  107. package/lib/internal/task-lifecycle.ts +33 -76
  108. package/lib/internal/task-orchestration.ts +33 -190
  109. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +77 -41
  110. package/lib/merge.ts +236 -120
  111. package/lib/process-data/normalize.ts +2 -8
  112. package/lib/run/host.ts +63 -14
  113. package/lib/run/tui.ts +5 -3
  114. package/lib/sandbox/agent-client-reconciler.ts +5 -15
  115. package/lib/sandbox/commands/create.ts +4 -4
  116. package/lib/sandbox/commands/enter.ts +24 -4
  117. package/lib/sandbox/config.ts +2 -14
  118. package/lib/sandbox/control/container-identity.ts +2 -2
  119. package/lib/sandbox/control/controller-registration.ts +8 -3
  120. package/lib/sandbox/control/executor.ts +156 -53
  121. package/lib/sandbox/control/lifecycle.ts +34 -28
  122. package/lib/sandbox/control/protocol.ts +1 -3
  123. package/lib/sandbox/control/server.ts +15 -12
  124. package/lib/sandbox/control/state.ts +7 -7
  125. package/lib/sandbox/mounts.ts +4 -3
  126. package/lib/sandbox/recovery.ts +162 -34
  127. package/lib/sandbox/tools.ts +1 -1
  128. package/lib/sandbox/workspace-identity.ts +28 -0
  129. package/lib/sandbox/workspace-view.ts +14 -10
  130. package/lib/self-update.ts +341 -0
  131. package/lib/server/protocol.ts +5 -5
  132. package/lib/task/activity-log.ts +6 -1
  133. package/lib/task/artifact-lifecycle.ts +5 -3
  134. package/lib/task/codex-orchestration.ts +40 -45
  135. package/lib/task/command-options.ts +1 -2
  136. package/lib/task/commands/cat.ts +0 -4
  137. package/lib/task/commands/decisions.ts +80 -75
  138. package/lib/task/commands/files.ts +6 -6
  139. package/lib/task/commands/grep.ts +23 -51
  140. package/lib/task/commands/issue-body.ts +6 -6
  141. package/lib/task/commands/log.ts +22 -10
  142. package/lib/task/commands/show.ts +5 -6
  143. package/lib/task/commands/status.ts +8 -8
  144. package/lib/task/commit-operation.ts +25 -30
  145. package/lib/task/commit-policy.ts +1 -2
  146. package/lib/task/control-authority.ts +563 -0
  147. package/lib/task/create.ts +6 -1
  148. package/lib/task/current-contract.ts +49 -0
  149. package/lib/task/decision-details.ts +340 -0
  150. package/lib/task/decision-intents.ts +7 -2
  151. package/lib/task/delegation-receipts.ts +8 -6
  152. package/lib/task/events.ts +10 -5
  153. package/lib/task/finalization.ts +84 -42
  154. package/lib/task/index.ts +19 -28
  155. package/lib/task/ledger-intents.ts +6 -8
  156. package/lib/task/ledger.ts +17 -7
  157. package/lib/task/lifecycle.ts +20 -0
  158. package/lib/task/orchestration.ts +42 -16
  159. package/lib/task/review-artifacts.ts +53 -15
  160. package/lib/task/review-finalization.ts +33 -7
  161. package/lib/task/verification-engine.ts +33 -21
  162. package/lib/task/verification-review.ts +1 -1
  163. package/lib/task/workspace-snapshot.ts +4 -13
  164. package/lib/task/write.ts +20 -1
  165. package/lib/update.ts +6 -40
  166. package/lib/version.ts +6 -1
  167. package/package.json +1 -1
  168. package/templates/.agents/QUICKSTART.en.md +2 -3
  169. package/templates/.agents/QUICKSTART.zh-CN.md +2 -3
  170. package/templates/.agents/README.en.md +5 -3
  171. package/templates/.agents/README.zh-CN.md +5 -3
  172. package/templates/.agents/rules/README.en.md +1 -0
  173. package/templates/.agents/rules/README.zh-CN.md +1 -0
  174. package/templates/.agents/rules/compatibility-policy.en.md +40 -0
  175. package/templates/.agents/rules/compatibility-policy.zh-CN.md +40 -0
  176. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  177. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  178. package/templates/.agents/rules/human-decision-context.en.md +9 -7
  179. package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
  180. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  181. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  182. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  183. package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
  184. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
  185. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  186. package/templates/.agents/rules/issue-sync.github.en.md +1 -1
  187. package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
  188. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  189. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  190. package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
  191. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
  192. package/templates/.agents/rules/next-step-output.en.md +3 -3
  193. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
  194. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
  195. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
  196. package/templates/.agents/rules/pr-sync.en.md +2 -2
  197. package/templates/.agents/rules/pr-sync.github.en.md +3 -2
  198. package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -1
  199. package/templates/.agents/rules/pr-sync.zh-CN.md +2 -1
  200. package/templates/.agents/rules/release-commands.github.en.md +6 -0
  201. package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
  202. package/templates/.agents/rules/review-handshake.en.md +1 -1
  203. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  204. package/templates/.agents/rules/review-method.en.md +1 -0
  205. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  206. package/templates/.agents/rules/task-management.en.md +1 -2
  207. package/templates/.agents/rules/task-management.zh-CN.md +1 -2
  208. package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -3
  209. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -3
  210. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  211. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  212. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  213. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  214. package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
  215. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
  216. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  217. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  218. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  219. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  220. package/templates/.agents/skills/code-task/SKILL.en.md +4 -3
  221. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -3
  222. package/templates/.agents/skills/commit/SKILL.en.md +27 -7
  223. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  224. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
  226. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  227. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  228. package/templates/.agents/skills/create-pr/SKILL.en.md +15 -11
  229. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +15 -11
  230. package/templates/.agents/skills/create-pr/reference/change-report.en.md +46 -0
  231. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +46 -0
  232. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
  233. package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
  234. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
  235. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  236. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  237. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  238. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  239. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -4
  240. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -4
  241. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  242. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  243. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
  244. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
  245. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
  246. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
  247. package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
  248. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
  249. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  250. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  251. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +9 -6
  252. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +9 -6
  253. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
  254. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
  255. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
  256. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
  257. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +7 -3
  258. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +7 -3
  259. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  260. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  261. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  262. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  263. package/templates/.agents/skills/run-task/SKILL.en.md +6 -2
  264. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -2
  265. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  266. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  267. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  268. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  269. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +159 -104
  270. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  271. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  272. package/templates/.agents/templates/task.en.md +1 -1
  273. package/templates/.agents/templates/task.zh-CN.md +1 -1
  274. package/templates/.claude/commands/analyze-task.en.md +1 -1
  275. package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
  276. package/templates/.claude/commands/block-task.en.md +1 -1
  277. package/templates/.claude/commands/block-task.zh-CN.md +1 -1
  278. package/templates/.claude/commands/cancel-task.en.md +1 -1
  279. package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
  280. package/templates/.claude/commands/check-task.en.md +1 -1
  281. package/templates/.claude/commands/check-task.zh-CN.md +1 -1
  282. package/templates/.claude/commands/code-task.en.md +1 -1
  283. package/templates/.claude/commands/code-task.zh-CN.md +1 -1
  284. package/templates/.claude/commands/commit.en.md +1 -1
  285. package/templates/.claude/commands/commit.zh-CN.md +1 -1
  286. package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
  287. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
  288. package/templates/.claude/commands/complete-task.en.md +1 -1
  289. package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
  290. package/templates/.claude/commands/create-pr.en.md +1 -1
  291. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  292. package/templates/.claude/commands/plan-task.en.md +1 -1
  293. package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
  294. package/templates/.claude/commands/review-analysis.en.md +1 -1
  295. package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
  296. package/templates/.claude/commands/review-code.en.md +1 -1
  297. package/templates/.claude/commands/review-code.zh-CN.md +1 -1
  298. package/templates/.claude/commands/review-plan.en.md +1 -1
  299. package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
  300. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  301. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  302. package/templates/.claude/commands/run-task.en.md +1 -1
  303. package/templates/.claude/commands/run-task.zh-CN.md +1 -1
  304. package/templates/.claude/commands/watch-pr.en.md +1 -1
  305. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  306. package/templates/.traecli/commands/update-agent-infra.en.md +7 -0
  307. package/templates/.traecli/commands/update-agent-infra.zh-CN.md +7 -0
  308. package/dist/lib/builtin-tuis.js +0 -40
  309. package/lib/builtin-tuis.ts +0 -56
@@ -3,7 +3,9 @@ import {
3
3
  assertValidBranchName,
4
4
  containerNameCandidates,
5
5
  sandboxBranchLabel,
6
- sandboxLabel
6
+ sandboxLabel,
7
+ sandboxTaskIdLabel,
8
+ sandboxWorkspaceModeLabel
7
9
  } from '../constants.ts';
8
10
  import { detectEngine } from '../engine.ts';
9
11
  import { ensureSandboxReady } from '../recovery.ts';
@@ -21,7 +23,10 @@ import {
21
23
  fetchSandboxRows,
22
24
  selectSandboxContainer,
23
25
  } from './list-running.ts';
24
- import { resolveSandboxTarget } from '../workspace-identity.ts';
26
+ import {
27
+ resolveSandboxReentryContext,
28
+ resolveSandboxTarget
29
+ } from '../workspace-identity.ts';
25
30
 
26
31
  const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
27
32
 
@@ -128,7 +133,11 @@ export async function enter(args: string[]): Promise<number> {
128
133
  const { running, nonRunning } = fetchSandboxRows(
129
134
  engine,
130
135
  sandboxLabel(config),
131
- sandboxBranchLabel(config)
136
+ sandboxBranchLabel(config),
137
+ {
138
+ mode: sandboxWorkspaceModeLabel(config),
139
+ taskId: sandboxTaskIdLabel(config)
140
+ }
132
141
  );
133
142
  const found = selectSandboxContainer(
134
143
  [...running, ...nonRunning],
@@ -140,11 +149,22 @@ export async function enter(args: string[]): Promise<number> {
140
149
  `No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`
141
150
  );
142
151
  }
152
+ const containerWorkspace = found.workspaceMode === 'task-bound' && found.taskId
153
+ ? { mode: 'task-bound' as const, taskId: found.taskId }
154
+ : found.workspaceMode === 'branch-only'
155
+ ? { mode: 'branch-only' as const }
156
+ : { mode: 'legacy-invalid' as const };
157
+ const reentry = resolveSandboxReentryContext({
158
+ target,
159
+ containerWorkspace,
160
+ repoRoot: config.repoRoot
161
+ });
143
162
  const ready = await ensureSandboxReady({
144
163
  config,
145
164
  engine,
146
165
  branch,
147
- workspace: target.workspace,
166
+ workspace: reentry.workspace,
167
+ reentry: reentry.reentry,
148
168
  row: found,
149
169
  allowRecreate: parsed.recreate,
150
170
  recreate: async () => {
@@ -4,7 +4,6 @@ import { homedir, platform } from 'node:os';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import pc from 'picocolors';
6
6
  import { normalizeAgentClients } from '../agent-clients/config.ts';
7
- import { isAgentClientId } from '../agent-clients/types.ts';
8
7
  import type { AgentClientState } from '../agent-clients/types.ts';
9
8
  import { validateSandboxEngine } from './engine.ts';
10
9
  import type { SandboxEngineInput } from './engine.ts';
@@ -64,7 +63,6 @@ export type SandboxConfig = {
64
63
  tools: string[];
65
64
  customTools: SandboxTool[];
66
65
  agentClientState: AgentClientState;
67
- agentClientSource: 'canonical' | 'legacy';
68
66
  refreshIntervalDays: number;
69
67
  dockerfile: string | null;
70
68
  vm: SandboxVmConfig;
@@ -74,7 +72,6 @@ type AircConfig = {
74
72
  project?: unknown;
75
73
  org?: unknown;
76
74
  agentClients?: unknown;
77
- tuis?: unknown;
78
75
  sandbox?: SandboxConfigInput;
79
76
  };
80
77
 
@@ -110,17 +107,10 @@ function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes:
110
107
 
111
108
  function resolveSandboxToolIds(
112
109
  sandbox: SandboxConfigInput,
113
- agentClients: ReturnType<typeof normalizeAgentClients>,
114
110
  defaults: readonly string[]
115
111
  ): string[] {
116
112
  if (Array.isArray(sandbox.tools)) {
117
- const tools = sandbox.tools.filter((tool): tool is string => typeof tool === 'string');
118
- return agentClients.source === 'canonical'
119
- ? tools.filter((tool) => !isAgentClientId(tool))
120
- : tools;
121
- }
122
- if (agentClients.remainingSandboxTools !== undefined) {
123
- return [...agentClients.remainingSandboxTools];
113
+ return sandbox.tools.filter((tool): tool is string => typeof tool === 'string');
124
114
  }
125
115
  return [...defaults];
126
116
  }
@@ -142,7 +132,6 @@ export function loadConfig({
142
132
  }
143
133
 
144
134
  const airc = JSON.parse(fs.readFileSync(configPath, 'utf8')) as AircConfig;
145
- const sandboxInput = airc.sandbox;
146
135
  const agentClients = normalizeAgentClients(airc);
147
136
  const defaults = cloneDefaults();
148
137
  const sandbox = airc.sandbox ?? {};
@@ -178,7 +167,7 @@ export function loadConfig({
178
167
  }
179
168
 
180
169
  const customTools = parseCustomTools(sandbox.customTools, { home });
181
- const tools = resolveSandboxToolIds(sandbox, agentClients, defaults.tools);
170
+ const tools = resolveSandboxToolIds(sandbox, defaults.tools);
182
171
 
183
172
  return {
184
173
  repoRoot,
@@ -199,7 +188,6 @@ export function loadConfig({
199
188
  tools,
200
189
  customTools,
201
190
  agentClientState: agentClients.state,
202
- agentClientSource: agentClients.source,
203
191
  refreshIntervalDays: asNonNegativeIntegerOrDefault(
204
192
  sandbox.refreshIntervalDays,
205
193
  defaults.refreshIntervalDays
@@ -1,4 +1,4 @@
1
- import type { SandboxControlManifest, SandboxControlManifestLike } from './protocol.ts';
1
+ import type { SandboxControlManifest } from './protocol.ts';
2
2
  import { commandForEngine, runEngine, runProbe } from '../shell.ts';
3
3
 
4
4
  export type ContainerObservation =
@@ -48,7 +48,7 @@ export function classifySandboxContainerInspection(
48
48
  }
49
49
 
50
50
  export async function inspectSandboxControlContainer(
51
- manifest: SandboxControlManifestLike,
51
+ manifest: SandboxControlManifest,
52
52
  options: { run?: typeof runEngine; probe?: typeof runProbe; timeoutMs?: number } = {}
53
53
  ): Promise<ContainerObservation> {
54
54
  try {
@@ -188,10 +188,12 @@ function assertRegistrationBinding(
188
188
  ): void {
189
189
  if (registration.taskId !== manifest.taskId
190
190
  || registration.controlGeneration !== manifest.generation
191
- || registration.containerId !== manifest.containerIdentity.id
192
- || JSON.stringify(registration.buildIdentity) !== JSON.stringify(buildIdentity)) {
191
+ || registration.containerId !== manifest.containerIdentity.id) {
193
192
  fail('CODEX_SANDBOX_CONTROLLER_REGISTRATION_INVALID', 'controller registration binding is invalid');
194
193
  }
194
+ if (registration.buildIdentity.protocolVersion !== buildIdentity.protocolVersion) {
195
+ fail('CODEX_LIFECYCLE_PROTOCOL_MISMATCH', 'Codex lifecycle protocol version does not match');
196
+ }
195
197
  }
196
198
 
197
199
  function atomicWrite(file: string, value: CodexControllerRegistrationV1): void {
@@ -322,7 +324,10 @@ export function resolveCodexControllerBinding(params: Readonly<{
322
324
  buildIdentity: LifecycleBuildIdentity;
323
325
  now?: number;
324
326
  probeProcess?: (identity: ProcessIdentity) => ProcessIdentityState;
325
- }>): Readonly<{ instanceDigest: string; controlGeneration: string }> {
327
+ }>): Readonly<{
328
+ instanceDigest: string;
329
+ controlGeneration: string;
330
+ }> {
326
331
  if (params.manifest.mode !== 'task-bound' || !params.manifest.taskId) {
327
332
  fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot resolve a Codex controller registration');
328
333
  }
@@ -1,13 +1,19 @@
1
- import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { getProcessStartTime } from '../../server/process-state.ts';
6
6
  import { createTask } from '../../task/create-service.ts';
7
7
  import { applyTaskFinalization } from '../../task/finalization.ts';
8
- import { verifyTaskEvent } from '../../task/verification.ts';
9
- import { bindSandboxControlTask, type SandboxControlExecution, type SandboxControlManifest, type SandboxControlRequest } from './protocol.ts';
10
- import { atomicWriteJson, executionPath, terminateSandboxControlExecution } from './state.ts';
8
+ import { bindSandboxControlTask, validateSandboxControlRequest, type SandboxControlExecution, type SandboxControlManifest, type SandboxControlRequest } from './protocol.ts';
9
+ import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.ts';
10
+ import {
11
+ createSandboxExecutorExecutionContext,
12
+ dispatchTaskControlOperation,
13
+ parseTaskControlOperation,
14
+ type TaskControlOperation
15
+ } from '../../task/control-authority.ts';
16
+ import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
11
17
  import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.ts';
12
18
  import {
13
19
  closeCodexControllerRegistration,
@@ -29,6 +35,26 @@ export type PreparedSandboxControlExecution = {
29
35
  terminate(updateState?: boolean): boolean;
30
36
  };
31
37
 
38
+ function sameBrokerOwner(left: BrokerOwner, right: BrokerOwner): boolean {
39
+ return left.version === right.version
40
+ && left.pid === right.pid
41
+ && left.startTime === right.startTime
42
+ && left.brokerId === right.brokerId
43
+ && left.token === right.token
44
+ && left.generation === right.generation;
45
+ }
46
+
47
+ export function assertSandboxControlExecutorAuthority(
48
+ manifest: SandboxControlManifest,
49
+ gateOwner?: BrokerOwner
50
+ ): void {
51
+ const owner = assertSandboxControlBrokerOwner(manifest);
52
+ if (gateOwner && !sameBrokerOwner(owner, gateOwner)) {
53
+ throw new Error('SANDBOX_CONTROL_GATE_OWNER_MISMATCH');
54
+ }
55
+ if (readActiveLease(manifest)) throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
56
+ }
57
+
32
58
  function safeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
33
59
  return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
34
60
  }
@@ -104,9 +130,11 @@ export async function prepareSandboxControlExecution(params: {
104
130
  start(canWrite = () => true) {
105
131
  if (!child.connected) throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
106
132
  if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
133
+ const gateOwner = assertSandboxControlBrokerOwner(params.manifest);
134
+ if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
107
135
  atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
108
136
  if (!canWrite()) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
109
- child.send({ version: 1, nonce });
137
+ child.send({ version: 1, nonce, owner: gateOwner });
110
138
  },
111
139
  completion,
112
140
  terminate(updateState = true) {
@@ -120,7 +148,7 @@ export async function prepareSandboxControlExecution(params: {
120
148
  };
121
149
  }
122
150
 
123
- function waitForGate(nonce: string, timeoutMs = 2_000): Promise<void> {
151
+ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<BrokerOwner> {
124
152
  return new Promise((resolve, reject) => {
125
153
  const timer = setTimeout(() => reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_TIMEOUT')), timeoutMs);
126
154
  const onDisconnect = () => {
@@ -131,12 +159,21 @@ function waitForGate(nonce: string, timeoutMs = 2_000): Promise<void> {
131
159
  process.once('message', (message: unknown) => {
132
160
  clearTimeout(timer);
133
161
  process.off('disconnect', onDisconnect);
134
- const value = message as { version?: unknown; nonce?: unknown } | null;
135
- if (!value || value.version !== 1 || value.nonce !== nonce) {
162
+ const value = message as {
163
+ version?: unknown;
164
+ nonce?: unknown;
165
+ owner?: Partial<BrokerOwner> | null;
166
+ } | null;
167
+ const owner = value?.owner;
168
+ if (!value || value.version !== 1 || value.nonce !== nonce
169
+ || !owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
170
+ || typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
171
+ || typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
172
+ || typeof owner.token !== 'string' || typeof owner.generation !== 'string') {
136
173
  reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
137
174
  return;
138
175
  }
139
- resolve();
176
+ resolve(owner as BrokerOwner);
140
177
  });
141
178
  });
142
179
  }
@@ -146,12 +183,15 @@ function controllerFailure(error: unknown): SandboxControlExecutionResult {
146
183
  ? error.code
147
184
  : /^([A-Z][A-Z0-9_]+)/u.exec(error instanceof Error ? error.message : String(error))?.[1]
148
185
  ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
186
+ const message = error instanceof CodexControllerRegistrationError
187
+ ? error.message
188
+ : `${code}: controller operation failed; inspect the sandbox controller and rebuild the sandbox if needed`;
149
189
  const payload = {
150
190
  version: 1,
151
191
  status: 'failed',
152
192
  changed: false,
153
193
  lease: null,
154
- error: { code, message: `${code}: controller request failed`, retryable: false }
194
+ error: { code, message, retryable: false }
155
195
  };
156
196
  return { exitCode: 1, stdout: `${JSON.stringify(payload)}\n`, stderr: '' };
157
197
  }
@@ -167,6 +207,17 @@ function orchestrationFailure(code: string, message: string): SandboxControlExec
167
207
  };
168
208
  }
169
209
 
210
+ function lifecycleFailure(code: string, message: string): SandboxControlExecutionResult {
211
+ return {
212
+ exitCode: 1,
213
+ stdout: `${JSON.stringify({
214
+ status: 'failed', changed: false,
215
+ error: { code, message }
216
+ })}\n`,
217
+ stderr: ''
218
+ };
219
+ }
220
+
170
221
  function finalizationResult(result: ReturnType<typeof applyTaskFinalization>): SandboxControlExecutionResult {
171
222
  const payload = {
172
223
  version: 1,
@@ -183,28 +234,17 @@ function finalizationResult(result: ReturnType<typeof applyTaskFinalization>): S
183
234
  };
184
235
  }
185
236
 
186
- function isCodexPrepare(args: readonly string[]): boolean {
187
- if (args[1] !== 'prepare') return false;
188
- const values: string[] = [];
189
- for (let index = 2; index < args.length; index += 1) {
190
- if (args[index] === '--client') values.push(args[index + 1] ?? '');
191
- else if (args[index]?.startsWith('--client=')) values.push(args[index]!.slice('--client='.length));
192
- }
193
- return values.length === 1 && values[0] === 'codex';
194
- }
195
-
196
237
  type ExecuteRequestOptions = Readonly<{
197
238
  buildIdentity?: typeof computeLifecycleBuildIdentity;
198
239
  resolveControllerBinding?: typeof resolveCodexControllerBinding;
199
- spawnDomain?: typeof spawnSync;
200
240
  }>;
201
241
 
202
- export function executeRequest(
242
+ export async function executeRequest(
203
243
  manifest: SandboxControlManifest,
204
244
  manifestPath: string,
205
245
  request: SandboxControlRequest,
206
246
  options: ExecuteRequestOptions = {}
207
- ): SandboxControlExecutionResult {
247
+ ): Promise<SandboxControlExecutionResult> {
208
248
  if (request.family === 'task-create') {
209
249
  const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
210
250
  return {
@@ -257,17 +297,47 @@ export function executeRequest(
257
297
  }
258
298
  }
259
299
  if (request.family === 'task-finalization') {
260
- return finalizationResult(applyTaskFinalization(
261
- { taskRef: manifest.taskId!, intent: 'complete', agent: request.agent },
262
- { repoRoot: manifest.repoRoot, preflight: (input, options) => verifyTaskEvent(
263
- { ...input, event: 'complete-task.hard-preflight' }, options
264
- ) }
265
- ));
300
+ const operation = parseTaskControlOperation(
301
+ 'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
302
+ );
303
+ if (operation.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
304
+ const result = dispatchTaskControlOperation(
305
+ createSandboxExecutorExecutionContext({
306
+ repoRoot: manifest.repoRoot,
307
+ worktreeRoot: manifest.worktreeRoot,
308
+ runtimeDir: manifest.runtimeDir,
309
+ taskId: manifest.taskId!,
310
+ generation: manifest.generation,
311
+ manifestPath,
312
+ requestId: request.id
313
+ }),
314
+ operation
315
+ );
316
+ return finalizationResult(result);
266
317
  }
267
318
  const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
268
- let controllerBinding: Readonly<{ instanceDigest: string; controlGeneration: string }> | null = null;
269
319
  if (request.family === 'task-orchestration') {
270
- if (isCodexPrepare(request.args)) {
320
+ boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
321
+ }
322
+ let operation: TaskControlOperation;
323
+ try {
324
+ operation = parseTaskControlOperation(request.family, boundArgs);
325
+ } catch (error) {
326
+ const message = error instanceof Error ? error.message : String(error);
327
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_CONTROL_OPERATION_INVALID';
328
+ return request.family === 'task-lifecycle'
329
+ ? lifecycleFailure('LIFECYCLE_PAYLOAD_INVALID', message)
330
+ : orchestrationFailure(code, message);
331
+ }
332
+ let controllerBinding: Readonly<{
333
+ instanceDigest: string;
334
+ controlGeneration: string;
335
+ }> | null = null;
336
+ if (request.family === 'task-orchestration') {
337
+ const codexPrepare = operation.family === 'task-orchestration'
338
+ && operation.intent === 'prepare'
339
+ && operation.input.client === 'codex';
340
+ if (codexPrepare) {
271
341
  if (!request.controllerProof) {
272
342
  return orchestrationFailure(
273
343
  'CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED',
@@ -293,36 +363,69 @@ export function executeRequest(
293
363
  'Controller proof is only accepted for canonical Codex prepare'
294
364
  );
295
365
  }
296
- boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
297
366
  }
298
- const result = (options.spawnDomain ?? spawnSync)(
299
- process.execPath,
300
- nodeEntryArgs(process.argv[1]!, [request.family, ...boundArgs]),
301
- {
302
- cwd: manifest.repoRoot,
303
- encoding: 'utf8',
304
- env: {
305
- ...safeEnv(process.env),
306
- AGENT_INFRA_RUNTIME_DIR: manifest.runtimeDir,
307
- ...(controllerBinding
308
- ? { AGENT_INFRA_CONTROL_CONTROLLER_BINDING: JSON.stringify(controllerBinding) }
309
- : {})
310
- }
311
- }
312
- );
313
- return { exitCode: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
367
+ const context = createSandboxExecutorExecutionContext({
368
+ repoRoot: manifest.repoRoot,
369
+ worktreeRoot: manifest.worktreeRoot,
370
+ runtimeDir: manifest.runtimeDir,
371
+ taskId: manifest.taskId!,
372
+ generation: manifest.generation,
373
+ manifestPath,
374
+ requestId: request.id,
375
+ ...(controllerBinding ? { controllerBinding } : {})
376
+ });
377
+ const format = (value: unknown): SandboxControlExecutionResult => {
378
+ const result = value as { status?: string };
379
+ return {
380
+ exitCode: result.status === 'failed' ? 1 : 0,
381
+ stdout: `${JSON.stringify(value)}\n`,
382
+ stderr: ''
383
+ };
384
+ };
385
+ if (request.family === 'task-orchestration') {
386
+ if (operation.family !== 'task-orchestration') throw new Error('SANDBOX_CONTROL_ORCHESTRATION_OPERATION_INVALID');
387
+ return format(await dispatchTaskControlOperation(context, operation));
388
+ }
389
+ if (operation.family !== 'task-lifecycle') throw new Error('SANDBOX_CONTROL_LIFECYCLE_OPERATION_INVALID');
390
+ return format(dispatchTaskControlOperation(context, operation));
314
391
  }
315
392
 
316
393
  export async function runSandboxControlExecutor(requestPath: string, nonce: string): Promise<void> {
317
- await waitForGate(nonce);
394
+ const gateOwner = await waitForGate(nonce);
318
395
  process.disconnect?.();
319
- const root = fs.realpathSync.native(process.cwd());
320
- const request = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as SandboxControlRequest;
321
396
  const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
322
397
  if (!manifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
323
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as SandboxControlManifest;
398
+ const manifest = readSandboxControlManifest(manifestPath);
399
+ const root = fs.realpathSync.native(process.cwd());
324
400
  if (fs.realpathSync.native(manifest.repoRoot) !== root) throw new Error('SANDBOX_CONTROL_EXECUTOR_ROOT_INVALID');
325
- const result = executeRequest(manifest, manifestPath, request);
401
+ const raw = JSON.parse(fs.readFileSync(requestPath, 'utf8')) as unknown;
402
+ const request = validateSandboxControlRequest(raw, manifest);
403
+ const requestDirectory = path.resolve(path.dirname(requestPath));
404
+ const processingDirectory = path.resolve(path.join(manifest.processingDir, request.id));
405
+ const channelRequestDirectory = path.resolve(path.join(manifest.channelDir, 'requests'));
406
+ if (requestDirectory !== processingDirectory && requestDirectory !== channelRequestDirectory) {
407
+ throw new Error('SANDBOX_CONTROL_EXECUTOR_REQUEST_INVALID');
408
+ }
409
+ let result: SandboxControlExecutionResult;
410
+ try {
411
+ assertSandboxControlExecutorAuthority(manifest, gateOwner);
412
+ result = await executeRequest(manifest, manifestPath, request);
413
+ } catch (error) {
414
+ const detail = error instanceof Error ? error.message : String(error);
415
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(detail)?.[1] ?? 'SANDBOX_CONTROL_EXECUTOR_FAILED';
416
+ result = request.family === 'task-finalization'
417
+ ? {
418
+ exitCode: 1,
419
+ stdout: `${JSON.stringify({
420
+ version: 1, status: 'failed', changed: false, accepted: true, result: null,
421
+ error: { code, message: detail, retryable: false }
422
+ })}\n`,
423
+ stderr: ''
424
+ }
425
+ : request.family === 'task-lifecycle'
426
+ ? lifecycleFailure(code, detail)
427
+ : orchestrationFailure(code, detail);
428
+ }
326
429
  process.stdout.write(result.stdout);
327
430
  process.stderr.write(result.stderr);
328
431
  process.exitCode = result.exitCode;
@@ -10,9 +10,7 @@ import {
10
10
  import type { ProcessIdentity, ProcessIdentityProbe, ProcessIdentityState } from '../../server/process-state.ts';
11
11
  import type {
12
12
  SandboxControlExecution,
13
- SandboxControlManifestLike,
14
13
  SandboxControlManifest,
15
- SandboxControlLegacyManifest,
16
14
  SandboxControlStatus,
17
15
  SandboxControlTimingPolicy
18
16
  } from './protocol.ts';
@@ -468,7 +466,6 @@ function readProcessingSnapshot(root: string): SandboxControlCutoverSnapshot['pr
468
466
  export function captureSandboxControlCutoverSnapshot(root: string): SandboxControlCutoverSnapshot {
469
467
  const resolvedRoot = path.resolve(root);
470
468
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
471
- if (fs.existsSync(manifestPath)) readSandboxControlManifestForTransition(manifestPath);
472
469
  return {
473
470
  root: resolvedRoot,
474
471
  manifestRaw: readOptionalRaw(manifestPath),
@@ -485,10 +482,7 @@ export function assertSandboxControlCutoverSnapshot(snapshot: SandboxControlCuto
485
482
  }
486
483
  }
487
484
 
488
- function readSandboxControlManifestValue(
489
- manifestPath: string,
490
- allowLegacy: boolean
491
- ): SandboxControlManifestLike {
485
+ function readSandboxControlManifestValue(manifestPath: string): SandboxControlManifest {
492
486
  if (!regularFile(manifestPath)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
493
487
  let manifest: Record<string, unknown>;
494
488
  try {
@@ -500,9 +494,15 @@ function readSandboxControlManifestValue(
500
494
  } catch {
501
495
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
502
496
  }
503
- const candidate = manifest as Partial<SandboxControlManifest> & Partial<SandboxControlLegacyManifest>;
504
- if (candidate.version !== 4 && candidate.version !== 5) {
505
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
497
+ const candidate = manifest as Partial<SandboxControlManifest> & { version?: unknown };
498
+ const expectedKeys = [
499
+ 'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
500
+ 'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
501
+ 'taskId', 'token', 'worktreeRoot'
502
+ ];
503
+ if ('version' in candidate || typeof candidate.runtimeDir !== 'string'
504
+ || Object.keys(manifest).sort().join(',') !== expectedKeys.sort().join(',')) {
505
+ throw new Error('SANDBOX_CONTROL_MANIFEST_REBUILD_REQUIRED: container-only recreation/rebuild is required');
506
506
  }
507
507
  if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
508
508
  || Array.isArray(candidate.containerIdentity)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
@@ -518,31 +518,23 @@ function readSandboxControlManifestValue(
518
518
  || (candidate.taskId !== null && typeof candidate.taskId !== 'string')
519
519
  || typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
520
520
  || typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
521
- || typeof candidate.generation !== 'string') throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
521
+ || typeof candidate.generation !== 'string'
522
+ || (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
523
+ || (candidate.mode === 'branch-only' && candidate.taskId !== null)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
522
524
  const root = path.dirname(path.resolve(manifestPath));
523
525
  if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
524
526
  || path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
525
527
  || path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
526
528
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
527
529
  }
528
- if (candidate.version === 4) {
529
- if (!allowLegacy) {
530
- throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
531
- }
532
- return candidate as unknown as SandboxControlLegacyManifest;
533
- }
534
- if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
530
+ if (path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
535
531
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
536
532
  }
537
- return candidate as unknown as SandboxControlManifest;
533
+ return candidate as SandboxControlManifest;
538
534
  }
539
535
 
540
536
  export function readSandboxControlManifest(manifestPath: string): SandboxControlManifest {
541
- return readSandboxControlManifestValue(manifestPath, false) as SandboxControlManifest;
542
- }
543
-
544
- export function readSandboxControlManifestForTransition(manifestPath: string): SandboxControlManifestLike {
545
- return readSandboxControlManifestValue(manifestPath, true);
537
+ return readSandboxControlManifestValue(manifestPath);
546
538
  }
547
539
 
548
540
  function readBrokerOwner(filePath: string): BrokerOwner | null {
@@ -561,6 +553,20 @@ function readBrokerOwner(filePath: string): BrokerOwner | null {
561
553
  return owner as BrokerOwner;
562
554
  }
563
555
 
556
+ export function assertSandboxControlBrokerOwner(
557
+ manifest: SandboxControlManifest,
558
+ options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
559
+ ): BrokerOwner {
560
+ const root = path.dirname(path.resolve(manifest.publicStatusDir));
561
+ const owner = readBrokerOwner(path.join(root, 'broker.json'));
562
+ if (!owner) throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
563
+ if (owner.token !== manifest.token || owner.generation !== manifest.generation) {
564
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
565
+ }
566
+ if (!ownerLive(owner, options.identityProbe)) throw new Error('SANDBOX_CONTROL_OWNER_LOST');
567
+ return owner;
568
+ }
569
+
564
570
  function readStatusOwner(filePath: string): SandboxControlStatus | null {
565
571
  if (!fs.existsSync(filePath)) return null;
566
572
  if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
@@ -585,7 +591,7 @@ function ownerLive(
585
591
  return state === 'alive';
586
592
  }
587
593
 
588
- function readExecutions(manifest: SandboxControlManifestLike): SandboxControlExecution[] {
594
+ function readExecutions(manifest: SandboxControlManifest): SandboxControlExecution[] {
589
595
  if (!fs.existsSync(manifest.processingDir)) return [];
590
596
  const stat = fs.lstatSync(manifest.processingDir);
591
597
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
@@ -705,7 +711,7 @@ export async function quiesceSandboxControlRoot(
705
711
  await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
706
712
 
707
713
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
708
- const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
714
+ const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
709
715
  const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
710
716
  const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
711
717
  const statusOwner = status ? status.broker : null;
@@ -799,7 +805,7 @@ export async function removeSandboxControlRoot(
799
805
  const stat = fs.lstatSync(resolvedRoot);
800
806
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
801
807
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
802
- const manifest = readSandboxControlManifestForTransition(manifestPath);
808
+ const manifest = readSandboxControlManifest(manifestPath);
803
809
  const inspectContainer = options.inspectContainer
804
810
  ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
805
811
  const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
@@ -870,7 +876,7 @@ export async function removeSandboxControlRoot(
870
876
  }
871
877
  }
872
878
 
873
- const currentManifest = readSandboxControlManifestForTransition(manifestPath);
879
+ const currentManifest = readSandboxControlManifest(manifestPath);
874
880
  if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
875
881
  || !isSandboxControlRootQuiescing(resolvedRoot)) {
876
882
  throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
@@ -35,9 +35,7 @@ export type SandboxControlManifestBase = Readonly<{
35
35
  branch: string; mode: 'task-bound' | 'branch-only'; taskId: string | null; token: string;
36
36
  generation: string; channelDir: string; publicStatusDir: string; processingDir: string;
37
37
  }>;
38
- export type SandboxControlLegacyManifest = SandboxControlManifestBase & Readonly<{ version: 4 }>;
39
- export type SandboxControlManifest = SandboxControlManifestBase & Readonly<{ version: 5; runtimeDir: string }>;
40
- export type SandboxControlManifestLike = SandboxControlLegacyManifest | SandboxControlManifest;
38
+ export type SandboxControlManifest = SandboxControlManifestBase & Readonly<{ runtimeDir: string }>;
41
39
  export type SandboxControlBrokerOwner = ProcessIdentity & Readonly<{
42
40
  version: 3;
43
41
  brokerId: string;