@fitlab-ai/agent-infra 0.9.13 → 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 (428) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/cli.ts +26 -9
  4. package/bin/internal-cli.sh +45 -0
  5. package/dist/bin/cli.js +173 -157
  6. package/dist/bin/internal-cli.js +126 -42
  7. package/dist/lib/agent-client.js +5 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  10. package/dist/lib/defaults.json +103 -103
  11. package/dist/lib/fs/durable-write.js +37 -0
  12. package/dist/lib/fs/file-lock.js +111 -0
  13. package/dist/lib/host-control/audit.js +19 -0
  14. package/dist/lib/host-control/client.js +142 -0
  15. package/dist/lib/host-control/command.js +49 -0
  16. package/dist/lib/host-control/path.js +160 -0
  17. package/dist/lib/host-control/secure-fs.js +140 -0
  18. package/dist/lib/host-control/server.js +159 -0
  19. package/dist/lib/host-control/service.js +112 -0
  20. package/dist/lib/init.js +11 -2
  21. package/dist/lib/internal/agent-client.js +5 -2
  22. package/dist/lib/internal/cli-route-inventory.js +154 -0
  23. package/dist/lib/internal/codex-lifecycle.js +15 -12
  24. package/dist/lib/internal/codex-sandbox-controller.js +8 -2
  25. package/dist/lib/internal/demo-promotion.js +322 -0
  26. package/dist/lib/internal/demo-transcript.js +454 -0
  27. package/dist/lib/internal/git-workflow.js +10 -6
  28. package/dist/lib/internal/host-control.js +28 -0
  29. package/dist/lib/internal/manual-validation.js +322 -0
  30. package/dist/lib/internal/platform-checks.js +16 -4
  31. package/dist/lib/internal/platform-comment.js +16 -4
  32. package/dist/lib/internal/platform-context.js +4 -1
  33. package/dist/lib/internal/platform-issue.js +12 -4
  34. package/dist/lib/internal/platform-metadata.js +88 -0
  35. package/dist/lib/internal/platform-pr-review.js +16 -3
  36. package/dist/lib/internal/platform-pr.js +42 -14
  37. package/dist/lib/internal/platform-release-notes.js +6 -3
  38. package/dist/lib/internal/platform-security.js +93 -0
  39. package/dist/lib/internal/pr-review-grade.js +14 -3
  40. package/dist/lib/internal/release-workflow.js +82 -89
  41. package/dist/lib/internal/sandbox-control.js +171 -7
  42. package/dist/lib/internal/task-activity.js +13 -4
  43. package/dist/lib/internal/task-artifact.js +19 -107
  44. package/dist/lib/internal/task-context.js +4 -1
  45. package/dist/lib/internal/task-create.js +48 -14
  46. package/dist/lib/internal/task-delivery.js +4 -1
  47. package/dist/lib/internal/task-event.js +38 -35
  48. package/dist/lib/internal/task-finalization.js +4 -1
  49. package/dist/lib/internal/task-invalidation.js +4 -1
  50. package/dist/lib/internal/task-ledger.js +12 -1
  51. package/dist/lib/internal/task-lifecycle.js +4 -1
  52. package/dist/lib/internal/task-operation-registry.js +519 -0
  53. package/dist/lib/internal/task-orchestration.js +12 -0
  54. package/dist/lib/internal/task-override.js +10 -3
  55. package/dist/lib/internal/task-qualification.js +90 -0
  56. package/dist/lib/internal/task-review.js +10 -52
  57. package/dist/lib/internal/task-short-id.js +11 -1
  58. package/dist/lib/internal/task-snapshot.js +4 -1
  59. package/dist/lib/internal/task-validate.js +37 -12
  60. package/dist/lib/internal/task-verify.js +5 -1
  61. package/dist/lib/internal/task-warning.js +8 -1
  62. package/dist/lib/owned-path.js +33 -0
  63. package/dist/lib/platform/check-status.js +11 -0
  64. package/dist/lib/platform/comment-safety.js +229 -0
  65. package/dist/lib/platform/completion-backfill.js +1 -1
  66. package/dist/lib/platform/github-data.js +241 -0
  67. package/dist/lib/platform/github-provider.js +248 -26
  68. package/dist/lib/platform/issue-comments.js +146 -34
  69. package/dist/lib/platform/issues.js +1 -179
  70. package/dist/lib/platform/pr-change-report.js +6 -12
  71. package/dist/lib/platform/pr-checks.js +12 -65
  72. package/dist/lib/platform/pr-summary.js +83 -10
  73. package/dist/lib/platform/provider-contract.d.ts +67 -1
  74. package/dist/lib/platform/provider-contract.js +6 -2
  75. package/dist/lib/platform/provider-validation.js +76 -3
  76. package/dist/lib/platform/pull-requests.js +117 -275
  77. package/dist/lib/platform/repository-metadata.js +377 -0
  78. package/dist/lib/platform/security-alerts.js +155 -0
  79. package/dist/lib/platform/snapshots.js +2 -0
  80. package/dist/lib/platform/verification-sync.js +154 -162
  81. package/dist/lib/process-data/index.js +7 -0
  82. package/dist/lib/release/workflow.js +1 -1
  83. package/dist/lib/run/index.js +5 -0
  84. package/dist/lib/sandbox/commands/control-status.js +22 -0
  85. package/dist/lib/sandbox/commands/create.js +39 -8
  86. package/dist/lib/sandbox/commands/ls.js +5 -0
  87. package/dist/lib/sandbox/commands/rm.js +3 -807
  88. package/dist/lib/sandbox/commands/show.js +16 -0
  89. package/dist/lib/sandbox/control/audit.js +203 -0
  90. package/dist/lib/sandbox/control/client.js +125 -12
  91. package/dist/lib/sandbox/control/completed-reentry.js +85 -0
  92. package/dist/lib/sandbox/control/container-identity.js +28 -31
  93. package/dist/lib/sandbox/control/executor.js +233 -34
  94. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  95. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  96. package/dist/lib/sandbox/control/lifecycle.js +656 -128
  97. package/dist/lib/sandbox/control/native-file-lock.js +85 -0
  98. package/dist/lib/sandbox/control/protocol.js +55 -4
  99. package/dist/lib/sandbox/control/server.js +720 -79
  100. package/dist/lib/sandbox/control/state.js +63 -3
  101. package/dist/lib/sandbox/control/task-view.js +183 -0
  102. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  103. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  104. package/dist/lib/sandbox/engines/authority.js +289 -0
  105. package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
  106. package/dist/lib/sandbox/index.js +7 -2
  107. package/dist/lib/sandbox/recovery.js +49 -5
  108. package/dist/lib/sandbox/removal.js +1717 -0
  109. package/dist/lib/sandbox/task-cleanup.js +217 -0
  110. package/dist/lib/sandbox/workspace-view.js +15 -15
  111. package/dist/lib/sandbox/worktree-safety.js +4 -1
  112. package/dist/lib/server/index.js +8 -0
  113. package/dist/lib/task/activity-intent.js +2 -1
  114. package/dist/lib/task/activity-log.js +30 -24
  115. package/dist/lib/task/artifact-command.js +101 -0
  116. package/dist/lib/task/artifact-lifecycle.js +71 -40
  117. package/dist/lib/task/artifact-name.js +36 -0
  118. package/dist/lib/task/artifact-operations.js +474 -0
  119. package/dist/lib/task/artifact-receipts.js +3 -15
  120. package/dist/lib/task/artifact-repair-intent.js +130 -0
  121. package/dist/lib/task/artifact-schema.js +127 -0
  122. package/dist/lib/task/capabilities.js +83 -19
  123. package/dist/lib/task/codex-orchestration.js +71 -25
  124. package/dist/lib/task/commit-identity.js +28 -0
  125. package/dist/lib/task/commit-operation.js +2 -16
  126. package/dist/lib/task/control-authority.js +375 -5
  127. package/dist/lib/task/control-recovery.js +128 -0
  128. package/dist/lib/task/create-service.js +103 -4
  129. package/dist/lib/task/create.js +80 -2
  130. package/dist/lib/task/decision-details.js +2 -46
  131. package/dist/lib/task/events.js +306 -76
  132. package/dist/lib/task/finalization-state.js +9 -0
  133. package/dist/lib/task/human-override.js +3 -6
  134. package/dist/lib/task/implementation-inputs.js +3 -3
  135. package/dist/lib/task/index.js +11 -3
  136. package/dist/lib/task/intermediate-cleanup.js +383 -0
  137. package/dist/lib/task/invalidation.js +18 -65
  138. package/dist/lib/task/ledger-intents.js +7 -22
  139. package/dist/lib/task/ledger.js +1 -3
  140. package/dist/lib/task/lifecycle.js +19 -1
  141. package/dist/lib/task/local-artifact-finalization.js +190 -290
  142. package/dist/lib/task/manual-validation-completion.js +26 -0
  143. package/dist/lib/task/manual-validation-evidence.js +130 -0
  144. package/dist/lib/task/manual-validation-receipt.js +101 -0
  145. package/dist/lib/task/manual-validation-shared.js +40 -0
  146. package/dist/lib/task/manual-validation-transaction.js +243 -0
  147. package/dist/lib/task/markdown.js +47 -0
  148. package/dist/lib/task/orchestration.js +206 -69
  149. package/dist/lib/task/qualification-audit.js +476 -0
  150. package/dist/lib/task/qualification-confirmation.js +109 -0
  151. package/dist/lib/task/qualification-intents.js +148 -0
  152. package/dist/lib/task/review-artifacts.js +23 -40
  153. package/dist/lib/task/review-command.js +25 -0
  154. package/dist/lib/task/review-finalization.js +146 -97
  155. package/dist/lib/task/review-fingerprint.js +2 -2
  156. package/dist/lib/task/sections.js +34 -54
  157. package/dist/lib/task/verification-config.js +11 -0
  158. package/dist/lib/task/verification-engine.js +170 -327
  159. package/dist/lib/task/verification.js +1 -1
  160. package/dist/lib/task/workflow-command.js +17 -0
  161. package/dist/lib/task/workflow-warning-intents.js +0 -16
  162. package/dist/lib/task/workflow-warnings.js +28 -2
  163. package/dist/lib/update.js +2 -0
  164. package/dist/lifecycle-build-manifest.json +3 -3
  165. package/lib/agent-client.ts +9 -0
  166. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  167. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  168. package/lib/fs/durable-write.ts +29 -0
  169. package/lib/fs/file-lock.ts +113 -0
  170. package/lib/host-control/audit.ts +22 -0
  171. package/lib/host-control/client.ts +172 -0
  172. package/lib/host-control/command.ts +60 -0
  173. package/lib/host-control/path.ts +167 -0
  174. package/lib/host-control/secure-fs.ts +135 -0
  175. package/lib/host-control/server.ts +180 -0
  176. package/lib/host-control/service.ts +133 -0
  177. package/lib/init.ts +15 -2
  178. package/lib/internal/agent-client.ts +4 -2
  179. package/lib/internal/cli-route-inventory.ts +142 -0
  180. package/lib/internal/codex-lifecycle.ts +14 -12
  181. package/lib/internal/codex-sandbox-controller.ts +7 -2
  182. package/lib/internal/demo-promotion.ts +354 -0
  183. package/lib/internal/demo-transcript.ts +419 -0
  184. package/lib/internal/git-workflow.ts +9 -6
  185. package/lib/internal/host-control.ts +28 -0
  186. package/lib/internal/manual-validation.ts +306 -0
  187. package/lib/internal/platform-checks.ts +12 -4
  188. package/lib/internal/platform-comment.ts +12 -4
  189. package/lib/internal/platform-context.ts +3 -1
  190. package/lib/internal/platform-issue.ts +11 -4
  191. package/lib/internal/platform-metadata.ts +70 -0
  192. package/lib/internal/platform-pr-review.ts +14 -3
  193. package/lib/internal/platform-pr.ts +38 -14
  194. package/lib/internal/platform-release-notes.ts +5 -3
  195. package/lib/internal/platform-security.ts +83 -0
  196. package/lib/internal/pr-review-grade.ts +10 -3
  197. package/lib/internal/release-workflow.ts +91 -87
  198. package/lib/internal/sandbox-control.ts +181 -6
  199. package/lib/internal/task-activity.ts +12 -4
  200. package/lib/internal/task-artifact.ts +15 -79
  201. package/lib/internal/task-context.ts +3 -1
  202. package/lib/internal/task-create.ts +76 -15
  203. package/lib/internal/task-delivery.ts +3 -1
  204. package/lib/internal/task-event.ts +21 -10
  205. package/lib/internal/task-finalization.ts +3 -1
  206. package/lib/internal/task-invalidation.ts +3 -1
  207. package/lib/internal/task-ledger.ts +11 -1
  208. package/lib/internal/task-lifecycle.ts +3 -1
  209. package/lib/internal/task-operation-registry.ts +590 -0
  210. package/lib/internal/task-orchestration.ts +12 -0
  211. package/lib/internal/task-override.ts +9 -3
  212. package/lib/internal/task-qualification.ts +61 -0
  213. package/lib/internal/task-review.ts +7 -50
  214. package/lib/internal/task-short-id.ts +10 -1
  215. package/lib/internal/task-snapshot.ts +3 -1
  216. package/lib/internal/task-validate.ts +32 -12
  217. package/lib/internal/task-verify.ts +4 -1
  218. package/lib/internal/task-warning.ts +7 -1
  219. package/lib/owned-path.ts +26 -0
  220. package/lib/platform/check-status.ts +9 -0
  221. package/lib/platform/comment-safety.ts +255 -0
  222. package/lib/platform/completion-backfill.ts +1 -1
  223. package/lib/platform/github-data.ts +322 -0
  224. package/lib/platform/github-provider.ts +244 -27
  225. package/lib/platform/issue-comments.ts +159 -35
  226. package/lib/platform/issues.ts +4 -245
  227. package/lib/platform/pr-change-report.ts +6 -12
  228. package/lib/platform/pr-checks.ts +13 -77
  229. package/lib/platform/pr-summary.ts +88 -11
  230. package/lib/platform/provider-contract.ts +78 -3
  231. package/lib/platform/provider-validation.ts +85 -3
  232. package/lib/platform/pull-requests.ts +129 -348
  233. package/lib/platform/repository-metadata.ts +453 -0
  234. package/lib/platform/security-alerts.ts +191 -0
  235. package/lib/platform/snapshots.ts +53 -0
  236. package/lib/platform/verification-sync.ts +171 -177
  237. package/lib/process-data/index.ts +7 -0
  238. package/lib/release/workflow.ts +3 -3
  239. package/lib/run/index.ts +5 -0
  240. package/lib/sandbox/commands/control-status.ts +26 -0
  241. package/lib/sandbox/commands/create.ts +53 -12
  242. package/lib/sandbox/commands/ls.ts +7 -0
  243. package/lib/sandbox/commands/rm.ts +3 -996
  244. package/lib/sandbox/commands/show.ts +16 -0
  245. package/lib/sandbox/control/audit.ts +253 -0
  246. package/lib/sandbox/control/client.ts +150 -13
  247. package/lib/sandbox/control/completed-reentry.ts +98 -0
  248. package/lib/sandbox/control/container-identity.ts +27 -43
  249. package/lib/sandbox/control/executor.ts +262 -37
  250. package/lib/sandbox/control/finalization-response.ts +29 -0
  251. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  252. package/lib/sandbox/control/lifecycle.ts +672 -18
  253. package/lib/sandbox/control/native-file-lock.ts +108 -0
  254. package/lib/sandbox/control/protocol.ts +80 -8
  255. package/lib/sandbox/control/server.ts +779 -82
  256. package/lib/sandbox/control/state.ts +84 -3
  257. package/lib/sandbox/control/task-view.ts +240 -0
  258. package/lib/sandbox/control/task-workflow.ts +95 -0
  259. package/lib/sandbox/control/workflow-executor.ts +102 -0
  260. package/lib/sandbox/engines/authority.ts +365 -0
  261. package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
  262. package/lib/sandbox/index.ts +9 -2
  263. package/lib/sandbox/recovery.ts +49 -6
  264. package/lib/sandbox/removal.ts +2144 -0
  265. package/lib/sandbox/task-cleanup.ts +209 -0
  266. package/lib/sandbox/workspace-view.ts +26 -18
  267. package/lib/sandbox/worktree-safety.ts +4 -1
  268. package/lib/server/index.ts +10 -0
  269. package/lib/task/activity-intent.ts +2 -2
  270. package/lib/task/activity-log.ts +27 -19
  271. package/lib/task/artifact-command.ts +104 -0
  272. package/lib/task/artifact-lifecycle.ts +66 -39
  273. package/lib/task/artifact-name.ts +55 -0
  274. package/lib/task/artifact-operations.ts +580 -0
  275. package/lib/task/artifact-receipts.ts +3 -14
  276. package/lib/task/artifact-repair-intent.ts +152 -0
  277. package/lib/task/artifact-schema.ts +163 -0
  278. package/lib/task/capabilities.ts +82 -22
  279. package/lib/task/codex-orchestration.ts +85 -24
  280. package/lib/task/commit-identity.ts +31 -0
  281. package/lib/task/commit-operation.ts +2 -16
  282. package/lib/task/control-authority.ts +513 -6
  283. package/lib/task/control-recovery.ts +174 -0
  284. package/lib/task/create-service.ts +131 -6
  285. package/lib/task/create.ts +83 -1
  286. package/lib/task/decision-details.ts +3 -71
  287. package/lib/task/events.ts +292 -75
  288. package/lib/task/finalization-state.ts +11 -0
  289. package/lib/task/human-override.ts +3 -8
  290. package/lib/task/implementation-inputs.ts +3 -3
  291. package/lib/task/index.ts +16 -3
  292. package/lib/task/intermediate-cleanup.ts +455 -0
  293. package/lib/task/invalidation.ts +18 -62
  294. package/lib/task/ledger-intents.ts +7 -24
  295. package/lib/task/ledger.ts +1 -3
  296. package/lib/task/lifecycle.ts +17 -1
  297. package/lib/task/local-artifact-finalization.ts +223 -388
  298. package/lib/task/manual-validation-completion.ts +40 -0
  299. package/lib/task/manual-validation-evidence.ts +209 -0
  300. package/lib/task/manual-validation-receipt.ts +152 -0
  301. package/lib/task/manual-validation-shared.ts +50 -0
  302. package/lib/task/manual-validation-transaction.ts +304 -0
  303. package/lib/task/markdown.ts +70 -0
  304. package/lib/task/orchestration.ts +191 -68
  305. package/lib/task/qualification-audit.ts +588 -0
  306. package/lib/task/qualification-confirmation.ts +117 -0
  307. package/lib/task/qualification-intents.ts +150 -0
  308. package/lib/task/review-artifacts.ts +21 -41
  309. package/lib/task/review-command.ts +23 -0
  310. package/lib/task/review-finalization.ts +154 -153
  311. package/lib/task/review-fingerprint.ts +2 -2
  312. package/lib/task/sections.ts +33 -52
  313. package/lib/task/verification-config.ts +12 -0
  314. package/lib/task/verification-engine.ts +165 -370
  315. package/lib/task/verification-types.ts +17 -0
  316. package/lib/task/verification.ts +1 -1
  317. package/lib/task/workflow-command.ts +18 -0
  318. package/lib/task/workflow-warning-intents.ts +0 -13
  319. package/lib/task/workflow-warnings.ts +21 -2
  320. package/lib/update.ts +6 -0
  321. package/package.json +7 -2
  322. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  323. package/templates/.agents/rules/README.en.md +2 -0
  324. package/templates/.agents/rules/README.zh-CN.md +2 -0
  325. package/templates/.agents/rules/decision-qualification.en.md +22 -0
  326. package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
  327. package/templates/.agents/rules/evidence-reporting.en.md +28 -0
  328. package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
  329. package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
  330. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
  331. package/templates/.agents/rules/local-artifact-repair.en.md +5 -1
  332. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +5 -1
  333. package/templates/.agents/rules/security-alerts.en.md +8 -8
  334. package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
  335. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  336. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  337. package/templates/.agents/scripts/package.json +3 -0
  338. package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -2
  339. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +19 -2
  340. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
  341. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
  342. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  343. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  344. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  345. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  346. package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
  347. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
  348. package/templates/.agents/skills/code-task/config/verify.en.json +5 -14
  349. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +5 -14
  350. package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
  351. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
  352. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  353. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  354. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
  355. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
  356. package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
  357. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
  358. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +37 -21
  359. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +37 -21
  360. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  361. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  362. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +4 -0
  363. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +4 -0
  364. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  365. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  366. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -23
  367. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -23
  368. package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
  369. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
  370. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  371. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  372. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  373. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  374. package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
  375. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
  376. package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
  377. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
  378. package/templates/.agents/skills/plan-task/SKILL.en.md +14 -1
  379. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +16 -1
  380. package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
  381. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
  382. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  383. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  384. package/templates/.agents/skills/review-analysis/SKILL.en.md +14 -1
  385. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -1
  386. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
  387. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
  388. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
  389. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
  390. package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
  391. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
  392. package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
  393. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
  394. package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
  395. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
  396. package/templates/.agents/skills/review-plan/SKILL.en.md +14 -1
  397. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -1
  398. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
  399. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
  400. package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
  401. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
  402. package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
  403. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
  404. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
  405. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
  406. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +25 -4
  407. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +25 -4
  408. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  409. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  410. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +5 -1
  411. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +5 -1
  412. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  413. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  414. package/templates/.agents/skills/test/SKILL.en.md +29 -7
  415. package/templates/.agents/skills/test/SKILL.zh-CN.md +29 -7
  416. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
  417. package/templates/.agents/templates/task.en.md +6 -0
  418. package/templates/.agents/templates/task.zh-CN.md +6 -0
  419. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  420. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  421. package/dist/lib/platform/adapters.js +0 -56
  422. package/lib/platform/adapters.ts +0 -197
  423. package/templates/.agents/scripts/security-alerts.github.sh +0 -151
  424. package/templates/.agents/scripts/security-alerts.sh +0 -31
  425. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
  426. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
  427. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
  428. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +0 -6
@@ -1,7 +1,8 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
- import { randomUUID } from 'node:crypto';
5
+ import { createHash, randomUUID } from 'node:crypto';
5
6
  import { buildProcessTreeStopCommand } from '../../server/process-control.ts';
6
7
  import {
7
8
  getProcessIdentityState,
@@ -16,6 +17,12 @@ import type {
16
17
  } from './protocol.ts';
17
18
  import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.ts';
18
19
  import { inspectSandboxControlContainer, type ContainerObservation } from './container-identity.ts';
20
+ import {
21
+ acquireSandboxResourceLock,
22
+ resolveSandboxLockNamespace,
23
+ type SandboxResourceLock
24
+ } from './native-file-lock.ts';
25
+ import { isSandboxAuthorityEvidence } from '../engines/authority.ts';
19
26
  import {
20
27
  parseSandboxControlStatus,
21
28
  readExecution,
@@ -30,6 +37,556 @@ const QUIESCING_FILE = 'quiescing.json';
30
37
  const BROKER_STARTING_FILE = 'broker-starting.json';
31
38
  const REPLACEMENT_FILE = 'replacement.json';
32
39
  const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
40
+ const REMOVAL_JOURNAL_ROOT = path.join('.agent-infra', 'sandbox-removal-journal');
41
+
42
+ export const SANDBOX_REMOVAL_JOURNAL_PHASES = [
43
+ 'prepared', 'target-committed', 'container-removal', 'container-absent',
44
+ 'carrier-finalizing', 'carrier-removed', 'workspace-finalizing', 'workspace-removed',
45
+ 'branch-finalizing', 'branch-removed', 'tool-finalizing', 'tool-removed',
46
+ 'shell-finalizing', 'shell-removed', 'share-finalizing', 'share-removed', 'completed'
47
+ ] as const;
48
+ export type SandboxRemovalJournalPhase = typeof SANDBOX_REMOVAL_JOURNAL_PHASES[number];
49
+ export type SandboxRemovalTargetCommit = Readonly<{
50
+ branch: string;
51
+ project: string;
52
+ controlRoot: string;
53
+ targetDigest: string;
54
+ permitDigest: string;
55
+ removeWorktree: boolean;
56
+ removeBranch: boolean;
57
+ removeShare: boolean;
58
+ worktreePaths: readonly string[];
59
+ workspaceViewPaths: readonly string[];
60
+ toolPaths: readonly string[];
61
+ shellPaths: readonly string[];
62
+ sharePath: string;
63
+ permits: readonly Readonly<{
64
+ path: string;
65
+ mode: 'clean' | 'discard';
66
+ snapshot: Readonly<{
67
+ worktree: string;
68
+ branch: string;
69
+ head: string;
70
+ changes: readonly Readonly<{
71
+ indexStatus: string;
72
+ worktreeStatus: string;
73
+ path: string;
74
+ originalPath?: string;
75
+ }>[];
76
+ identity: string;
77
+ source?: 'registered' | 'recovered';
78
+ recovery?: Readonly<{
79
+ repoRoot: string;
80
+ worktreeBase: string;
81
+ branch: string;
82
+ identitySource: 'branch-only' | 'task-bound';
83
+ taskId: string | null;
84
+ }>;
85
+ }>;
86
+ }>[];
87
+ }>;
88
+
89
+ export type SandboxRemovalJournal = Readonly<{
90
+ version: 2;
91
+ operation: 'sandbox-rm';
92
+ oldOperation: 'sandbox-rm';
93
+ newOperation: 'sandbox-rm';
94
+ handoffId: string;
95
+ transitionId: string;
96
+ engine: string;
97
+ containerId: string;
98
+ generation: string;
99
+ authorityFingerprint: string;
100
+ carrierIdentityDigest: string;
101
+ lockDomain: string;
102
+ owner: ProcessIdentity & Readonly<{ leaseNonce: string }>;
103
+ phase: SandboxRemovalJournalPhase;
104
+ expectedOldJournalRevision: number | null;
105
+ revision: number;
106
+ target: SandboxRemovalTargetCommit;
107
+ recordedAt: number;
108
+ }>;
109
+
110
+ function removalCarrierDigest(engine: string, containerId: string, generation: string): string {
111
+ return createHash('sha256').update(`${engine}\0${containerId}\0${generation}`).digest('hex');
112
+ }
113
+
114
+ function assertPrivateDirectory(directory: string): void {
115
+ const home = path.resolve(os.homedir());
116
+ const resolved = path.resolve(directory);
117
+ const relative = path.relative(home, resolved);
118
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
119
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
120
+ }
121
+ let current = home;
122
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
123
+ current = path.join(current, segment);
124
+ const stat = fs.lstatSync(current);
125
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
126
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
127
+ }
128
+ if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
129
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
130
+ }
131
+ }
132
+ }
133
+
134
+ function removalJournalDomain(lockDomain: string, create = true): string {
135
+ const namespace = resolveSandboxLockNamespace('sandbox-removal-journal', { lockDomain });
136
+ const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1)!);
137
+ if (create) fs.mkdirSync(root, { recursive: true, mode: 0o700 });
138
+ assertPrivateDirectory(root);
139
+ const domain = path.join(root, lockDomain);
140
+ if (create) fs.mkdirSync(domain, { recursive: true, mode: 0o700 });
141
+ assertPrivateDirectory(domain);
142
+ return domain;
143
+ }
144
+
145
+ function removalJournalPath(manifest: SandboxControlManifest): string {
146
+ return path.join(
147
+ removalJournalDomain(manifest.authorityEvidence.lockDomain),
148
+ `${removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation)}.json`
149
+ );
150
+ }
151
+
152
+ function removalJournalPathForRecord(record: SandboxRemovalJournal): string {
153
+ return path.join(
154
+ removalJournalDomain(record.lockDomain, false),
155
+ `${record.carrierIdentityDigest}.json`
156
+ );
157
+ }
158
+
159
+ function isRecord(value: unknown): value is Record<string, unknown> {
160
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
161
+ }
162
+
163
+ function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
164
+ const actual = Object.keys(value).sort();
165
+ return actual.length === keys.length && actual.every((key, index) => key === keys[index]);
166
+ }
167
+
168
+ function isRemovalPermitCommit(value: unknown): boolean {
169
+ if (!isRecord(value) || typeof value.path !== 'string' || !path.isAbsolute(value.path)
170
+ || (value.mode !== 'clean' && value.mode !== 'discard') || !isRecord(value.snapshot)
171
+ || typeof value.snapshot.worktree !== 'string' || !path.isAbsolute(value.snapshot.worktree)
172
+ || typeof value.snapshot.branch !== 'string' || typeof value.snapshot.head !== 'string'
173
+ || typeof value.snapshot.identity !== 'string' || !Array.isArray(value.snapshot.changes)
174
+ || value.snapshot.changes.some((change) => !isRecord(change)
175
+ || typeof change.indexStatus !== 'string' || typeof change.worktreeStatus !== 'string'
176
+ || typeof change.path !== 'string'
177
+ || (change.originalPath !== undefined && typeof change.originalPath !== 'string'))
178
+ || (value.snapshot.source !== undefined
179
+ && value.snapshot.source !== 'registered' && value.snapshot.source !== 'recovered')) return false;
180
+ if (value.snapshot.recovery === undefined) return true;
181
+ const recovery = value.snapshot.recovery;
182
+ return isRecord(recovery)
183
+ && typeof recovery.repoRoot === 'string' && path.isAbsolute(recovery.repoRoot)
184
+ && typeof recovery.worktreeBase === 'string' && path.isAbsolute(recovery.worktreeBase)
185
+ && typeof recovery.branch === 'string'
186
+ && (recovery.identitySource === 'branch-only' || recovery.identitySource === 'task-bound')
187
+ && (recovery.taskId === null || typeof recovery.taskId === 'string');
188
+ }
189
+
190
+ function parseRemovalJournal(raw: string): SandboxRemovalJournal {
191
+ let value: unknown;
192
+ try {
193
+ value = JSON.parse(raw);
194
+ } catch {
195
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
196
+ }
197
+ if (!isRecord(value) || !hasExactKeys(value, [
198
+ 'authorityFingerprint', 'carrierIdentityDigest', 'containerId', 'engine', 'expectedOldJournalRevision',
199
+ 'generation', 'handoffId', 'lockDomain', 'newOperation', 'oldOperation', 'operation', 'owner', 'phase',
200
+ 'recordedAt', 'revision', 'target', 'transitionId', 'version'
201
+ ]) || !isRecord(value.owner) || !hasExactKeys(value.owner, ['leaseNonce', 'pid', 'startTime'])
202
+ || !isRecord(value.target) || !hasExactKeys(value.target, [
203
+ 'branch', 'controlRoot', 'permitDigest', 'permits', 'project', 'removeBranch', 'removeShare',
204
+ 'removeWorktree', 'sharePath', 'shellPaths', 'targetDigest', 'toolPaths', 'workspaceViewPaths',
205
+ 'worktreePaths'
206
+ ]) || value.version !== 2 || value.operation !== 'sandbox-rm'
207
+ || value.oldOperation !== 'sandbox-rm' || value.newOperation !== 'sandbox-rm'
208
+ || typeof value.handoffId !== 'string' || value.handoffId.length === 0
209
+ || typeof value.transitionId !== 'string' || value.transitionId.length === 0
210
+ || typeof value.engine !== 'string' || value.engine.length === 0
211
+ || typeof value.containerId !== 'string' || value.containerId.length === 0
212
+ || typeof value.generation !== 'string' || value.generation.length === 0
213
+ || typeof value.authorityFingerprint !== 'string' || !/^[a-f0-9]{64}$/u.test(value.authorityFingerprint)
214
+ || typeof value.carrierIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.carrierIdentityDigest)
215
+ || typeof value.lockDomain !== 'string' || !/^[a-f0-9]{64}$/u.test(value.lockDomain)
216
+ || !Number.isSafeInteger(value.owner.pid) || (value.owner.pid as number) <= 0
217
+ || !Number.isSafeInteger(value.owner.startTime) || typeof value.owner.leaseNonce !== 'string'
218
+ || value.owner.leaseNonce.length === 0
219
+ || !SANDBOX_REMOVAL_JOURNAL_PHASES.includes(value.phase as SandboxRemovalJournalPhase)
220
+ || !(value.expectedOldJournalRevision === null
221
+ || (Number.isSafeInteger(value.expectedOldJournalRevision) && (value.expectedOldJournalRevision as number) >= 0))
222
+ || !Number.isSafeInteger(value.revision) || (value.revision as number) <= 0
223
+ || typeof value.target.branch !== 'string' || value.target.branch.length === 0
224
+ || typeof value.target.project !== 'string' || value.target.project.length === 0
225
+ || typeof value.target.controlRoot !== 'string' || path.isAbsolute(value.target.controlRoot) === false
226
+ || typeof value.target.targetDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.targetDigest)
227
+ || typeof value.target.permitDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.permitDigest)
228
+ || typeof value.target.removeWorktree !== 'boolean'
229
+ || typeof value.target.removeBranch !== 'boolean'
230
+ || typeof value.target.removeShare !== 'boolean'
231
+ || typeof value.target.sharePath !== 'string' || !path.isAbsolute(value.target.sharePath)
232
+ || !Array.isArray(value.target.worktreePaths) || value.target.worktreePaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
233
+ || !Array.isArray(value.target.workspaceViewPaths) || value.target.workspaceViewPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
234
+ || !Array.isArray(value.target.toolPaths) || value.target.toolPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
235
+ || !Array.isArray(value.target.shellPaths) || value.target.shellPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
236
+ || !Array.isArray(value.target.permits)
237
+ || value.target.permits.some((permit) => !isRemovalPermitCommit(permit))
238
+ || !Number.isSafeInteger(value.recordedAt)) {
239
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
240
+ }
241
+ const expected = value.expectedOldJournalRevision as number | null;
242
+ const revision = value.revision as number;
243
+ if ((expected === null && revision !== 1) || (expected !== null && revision !== expected + 1)) {
244
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
245
+ }
246
+ return {
247
+ version: 2,
248
+ operation: 'sandbox-rm',
249
+ oldOperation: 'sandbox-rm',
250
+ newOperation: 'sandbox-rm',
251
+ handoffId: value.handoffId as string,
252
+ transitionId: value.transitionId as string,
253
+ engine: value.engine as string,
254
+ containerId: value.containerId as string,
255
+ generation: value.generation as string,
256
+ authorityFingerprint: value.authorityFingerprint as string,
257
+ carrierIdentityDigest: value.carrierIdentityDigest as string,
258
+ lockDomain: value.lockDomain as string,
259
+ owner: {
260
+ pid: value.owner.pid as number,
261
+ startTime: value.owner.startTime as number,
262
+ leaseNonce: value.owner.leaseNonce as string
263
+ },
264
+ phase: value.phase as SandboxRemovalJournalPhase,
265
+ expectedOldJournalRevision: expected,
266
+ revision,
267
+ target: {
268
+ branch: value.target.branch as string,
269
+ project: value.target.project as string,
270
+ controlRoot: value.target.controlRoot as string,
271
+ targetDigest: value.target.targetDigest as string,
272
+ permitDigest: value.target.permitDigest as string,
273
+ removeWorktree: value.target.removeWorktree as boolean,
274
+ removeBranch: value.target.removeBranch as boolean,
275
+ removeShare: value.target.removeShare as boolean,
276
+ worktreePaths: value.target.worktreePaths as string[],
277
+ workspaceViewPaths: value.target.workspaceViewPaths as string[],
278
+ toolPaths: value.target.toolPaths as string[],
279
+ shellPaths: value.target.shellPaths as string[],
280
+ sharePath: value.target.sharePath as string,
281
+ permits: value.target.permits as SandboxRemovalTargetCommit['permits']
282
+ },
283
+ recordedAt: value.recordedAt as number
284
+ };
285
+ }
286
+
287
+ function readRemovalJournalAt(target: string): SandboxRemovalJournal | null {
288
+ if (!fs.existsSync(target)) return null;
289
+ const stat = fs.lstatSync(target);
290
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
291
+ return parseRemovalJournal(fs.readFileSync(target, 'utf8'));
292
+ }
293
+
294
+ export function readSandboxRemovalJournal(manifest: SandboxControlManifest): SandboxRemovalJournal | null {
295
+ return readRemovalJournalAt(removalJournalPath(manifest));
296
+ }
297
+
298
+ export function listSandboxRemovalJournals(
299
+ filter: Readonly<{ branch?: string; project?: string; targetDigest?: string }> = {}
300
+ ): SandboxRemovalJournal[] {
301
+ const namespace = resolveSandboxLockNamespace('sandbox-removal-journal');
302
+ const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1)!);
303
+ if (!fs.existsSync(root)) return [];
304
+ assertPrivateDirectory(root);
305
+ const journals: SandboxRemovalJournal[] = [];
306
+ for (const domainEntry of fs.readdirSync(root, { withFileTypes: true })) {
307
+ if (!domainEntry.isDirectory() || domainEntry.isSymbolicLink()) {
308
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
309
+ }
310
+ const domain = path.join(root, domainEntry.name);
311
+ assertPrivateDirectory(domain);
312
+ for (const entry of fs.readdirSync(domain, { withFileTypes: true })) {
313
+ if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith('.json')) {
314
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
315
+ }
316
+ const record = readRemovalJournalAt(path.join(domain, entry.name));
317
+ if (!record || record.lockDomain !== domainEntry.name
318
+ || `${record.carrierIdentityDigest}.json` !== entry.name) {
319
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
320
+ }
321
+ if ((!filter.branch || record.target.branch === filter.branch)
322
+ && (!filter.project || record.target.project === filter.project)
323
+ && (!filter.targetDigest || record.target.targetDigest === filter.targetDigest)) {
324
+ journals.push(record);
325
+ }
326
+ }
327
+ }
328
+ return journals;
329
+ }
330
+
331
+ export function clearSandboxRemovalJournalRecord(record: SandboxRemovalJournal): void {
332
+ const target = removalJournalPathForRecord(record);
333
+ if (fs.existsSync(target)) {
334
+ const current = readRemovalJournalAt(target);
335
+ if (!current || current.revision !== record.revision || current.handoffId !== record.handoffId) {
336
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
337
+ }
338
+ fs.rmSync(target, { force: true });
339
+ }
340
+ }
341
+
342
+ export function clearSandboxRemovalJournal(manifest: SandboxControlManifest): void {
343
+ const target = removalJournalPath(manifest);
344
+ if (fs.existsSync(target)) fs.rmSync(target, { force: true });
345
+ }
346
+
347
+ type RemovalJournalCursor = Readonly<{
348
+ record: SandboxRemovalJournal;
349
+ write(phase: SandboxRemovalJournalPhase): SandboxRemovalJournal;
350
+ }>;
351
+
352
+ function defaultRemovalTarget(manifest: Pick<SandboxControlManifest, 'project' | 'branch' | 'container' | 'channelDir'>): SandboxRemovalTargetCommit {
353
+ const targetDigest = createHash('sha256')
354
+ .update(`${manifest.project}\0${manifest.branch}\0${manifest.container}\0${manifest.channelDir}`)
355
+ .digest('hex');
356
+ return {
357
+ branch: manifest.branch,
358
+ project: manifest.project,
359
+ controlRoot: path.dirname(manifest.channelDir),
360
+ targetDigest,
361
+ permitDigest: createHash('sha256').update('none').digest('hex'),
362
+ removeWorktree: false,
363
+ removeBranch: false,
364
+ removeShare: false,
365
+ worktreePaths: [],
366
+ workspaceViewPaths: [],
367
+ toolPaths: [],
368
+ shellPaths: [],
369
+ sharePath: path.join(path.dirname(manifest.channelDir), 'share'),
370
+ permits: []
371
+ };
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
+
386
+ function sameRemovalTarget(left: SandboxRemovalTargetCommit, right: SandboxRemovalTargetCommit): boolean {
387
+ return JSON.stringify(left) === JSON.stringify(right);
388
+ }
389
+
390
+ function phaseIndex(phase: SandboxRemovalJournalPhase): number {
391
+ return SANDBOX_REMOVAL_JOURNAL_PHASES.indexOf(phase);
392
+ }
393
+
394
+ export function sandboxRemovalPhaseIndex(phase: SandboxRemovalJournalPhase): number {
395
+ return phaseIndex(phase);
396
+ }
397
+
398
+ function assertNextRemovalPhase(current: SandboxRemovalJournalPhase, next: SandboxRemovalJournalPhase): boolean {
399
+ const currentIndex = phaseIndex(current);
400
+ const nextIndex = phaseIndex(next);
401
+ if (nextIndex === currentIndex) return false;
402
+ if (current === 'target-committed' && next === 'container-absent') return true;
403
+ if (nextIndex !== currentIndex + 1) {
404
+ throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
405
+ }
406
+ return true;
407
+ }
408
+
409
+ function assertRemovalJournalLock(
410
+ record: SandboxRemovalJournal,
411
+ resourceLock: SandboxResourceLock
412
+ ): void {
413
+ const namespace = resolveSandboxLockNamespace(`${record.engine}:${record.containerId}`, {
414
+ lockDomain: record.lockDomain
415
+ });
416
+ if (resourceLock.lockDomain !== record.lockDomain || resourceLock.path !== namespace.lockPath) {
417
+ throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
418
+ }
419
+ }
420
+
421
+ function writeRemovalJournalAt(
422
+ target: string,
423
+ base: Omit<SandboxRemovalJournal, 'phase' | 'expectedOldJournalRevision' | 'revision' | 'recordedAt'>,
424
+ phase: SandboxRemovalJournalPhase,
425
+ expectedOldJournalRevision: number | null
426
+ ): SandboxRemovalJournal {
427
+ const existing = readRemovalJournalAt(target);
428
+ if (expectedOldJournalRevision === null
429
+ ? existing !== null
430
+ : existing?.revision !== expectedOldJournalRevision) {
431
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
432
+ }
433
+ const record: SandboxRemovalJournal = {
434
+ ...base,
435
+ phase,
436
+ expectedOldJournalRevision,
437
+ revision: expectedOldJournalRevision === null ? 1 : expectedOldJournalRevision + 1,
438
+ recordedAt: Date.now()
439
+ };
440
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
441
+ try {
442
+ fs.writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
443
+ if (expectedOldJournalRevision === null && fs.existsSync(target)) {
444
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
445
+ }
446
+ fs.renameSync(temporary, target);
447
+ } finally {
448
+ fs.rmSync(temporary, { force: true });
449
+ }
450
+ return record;
451
+ }
452
+
453
+ function writeRemovalJournal(
454
+ manifest: SandboxControlManifest,
455
+ base: Omit<SandboxRemovalJournal, 'phase' | 'expectedOldJournalRevision' | 'revision' | 'recordedAt'>,
456
+ phase: SandboxRemovalJournalPhase,
457
+ expectedOldJournalRevision: number | null
458
+ ): SandboxRemovalJournal {
459
+ return writeRemovalJournalAt(removalJournalPath(manifest), base, phase, expectedOldJournalRevision);
460
+ }
461
+
462
+ function startRemovalJournal(
463
+ manifest: SandboxControlManifest,
464
+ options: Readonly<{
465
+ target?: SandboxRemovalTargetCommit;
466
+ identityProbe: ProcessIdentityProbe;
467
+ }>
468
+ ): RemovalJournalCursor {
469
+ const target = removalJournalPath(manifest);
470
+ const existing = readRemovalJournalAt(target);
471
+ if (existing && (existing.engine !== manifest.engine || existing.containerId !== manifest.containerIdentity.id
472
+ || existing.generation !== manifest.generation
473
+ || existing.authorityFingerprint !== manifest.authorityEvidence.authorityFingerprint
474
+ || existing.lockDomain !== manifest.authorityEvidence.lockDomain)) {
475
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_IDENTITY_MISMATCH');
476
+ }
477
+ const targetCommit = options.target ?? existing?.target ?? defaultRemovalTarget(manifest);
478
+ if (existing && options.target && !sameRemovalTarget(existing.target, options.target)) {
479
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
480
+ }
481
+ if (existing) {
482
+ const state = options.identityProbe(existing.owner);
483
+ if (state === 'alive') throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
484
+ if (state === 'unknown') throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
485
+ }
486
+ const startTime = getProcessStartTime(process.pid);
487
+ if (startTime === null) throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
488
+ const base = {
489
+ version: 2 as const,
490
+ operation: 'sandbox-rm' as const,
491
+ oldOperation: 'sandbox-rm' as const,
492
+ newOperation: 'sandbox-rm' as const,
493
+ handoffId: randomUUID(),
494
+ transitionId: randomUUID(),
495
+ engine: manifest.engine,
496
+ containerId: manifest.containerIdentity.id,
497
+ generation: manifest.generation,
498
+ authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
499
+ carrierIdentityDigest: removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation),
500
+ lockDomain: manifest.authorityEvidence.lockDomain,
501
+ owner: { pid: process.pid, startTime, leaseNonce: randomUUID() },
502
+ target: targetCommit
503
+ };
504
+ const initialPhase = existing?.phase ?? 'prepared';
505
+ let record = writeRemovalJournal(manifest, base, initialPhase, existing?.revision ?? null);
506
+ return {
507
+ get record() { return record; },
508
+ write(phase) {
509
+ if (!assertNextRemovalPhase(record.phase, phase)) return record;
510
+ record = writeRemovalJournal(manifest, base, phase, record.revision);
511
+ return record;
512
+ }
513
+ };
514
+ }
515
+
516
+ export function claimSandboxRemovalJournal(
517
+ journal: SandboxRemovalJournal,
518
+ options: Readonly<{ identityProbe?: ProcessIdentityProbe; resourceLock?: SandboxResourceLock }> = {}
519
+ ): SandboxRemovalJournal {
520
+ const target = removalJournalPathForRecord(journal);
521
+ const resourceLock = options.resourceLock
522
+ ?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
523
+ const ownedLock = options.resourceLock ? null : resourceLock;
524
+ try {
525
+ assertRemovalJournalLock(journal, resourceLock);
526
+ const current = readRemovalJournalAt(target);
527
+ if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
528
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
529
+ }
530
+ const state = (options.identityProbe ?? getProcessIdentityState)(current.owner);
531
+ if (state === 'alive') throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
532
+ if (state === 'unknown') throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
533
+ const startTime = getProcessStartTime(process.pid);
534
+ if (startTime === null) throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
535
+ const base = {
536
+ ...current,
537
+ handoffId: randomUUID(),
538
+ transitionId: randomUUID(),
539
+ owner: { pid: process.pid, startTime, leaseNonce: randomUUID() }
540
+ };
541
+ return writeRemovalJournalAt(target, base, current.phase, current.revision);
542
+ } finally {
543
+ ownedLock?.release();
544
+ }
545
+ }
546
+
547
+ export function advanceSandboxRemovalJournalPhase(
548
+ journal: SandboxRemovalJournal,
549
+ phase: SandboxRemovalJournalPhase,
550
+ options: Readonly<{ resourceLock?: SandboxResourceLock }> = {}
551
+ ): SandboxRemovalJournal {
552
+ const target = removalJournalPathForRecord(journal);
553
+ const resourceLock = options.resourceLock
554
+ ?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
555
+ const ownedLock = options.resourceLock ? null : resourceLock;
556
+ try {
557
+ assertRemovalJournalLock(journal, resourceLock);
558
+ const current = readRemovalJournalAt(target);
559
+ if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
560
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
561
+ }
562
+ if (!assertNextRemovalPhase(current.phase, phase)) return current;
563
+ const {
564
+ phase: _phase,
565
+ expectedOldJournalRevision: _expected,
566
+ revision: _revision,
567
+ recordedAt: _recordedAt,
568
+ ...base
569
+ } = current;
570
+ return writeRemovalJournalAt(target, base, phase, current.revision);
571
+ } finally {
572
+ ownedLock?.release();
573
+ }
574
+ }
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
+ }
33
590
 
34
591
  async function awaitWithDeadline<T>(
35
592
  operation: () => Promise<T>,
@@ -82,6 +639,30 @@ function markSandboxControlRootQuiescing(root: string): void {
82
639
  }
83
640
  }
84
641
 
642
+ function recordSandboxRemovalPending(
643
+ root: string,
644
+ manifest: SandboxControlManifest,
645
+ phase: 'container-removal' | 'container-verification',
646
+ journal: RemovalJournalCursor
647
+ ): void {
648
+ const pendingPath = path.join(root, 'removal-pending.json');
649
+ const temporary = `${pendingPath}.${process.pid}.${randomUUID()}.tmp`;
650
+ try {
651
+ fs.writeFileSync(temporary, `${JSON.stringify({
652
+ version: 1,
653
+ phase,
654
+ containerId: manifest.containerIdentity.id,
655
+ generation: manifest.generation,
656
+ authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
657
+ recordedAt: Date.now()
658
+ })}\n`, { mode: 0o600, flag: 'wx' });
659
+ fs.renameSync(temporary, pendingPath);
660
+ } finally {
661
+ fs.rmSync(temporary, { force: true });
662
+ }
663
+ journal.write(phase === 'container-verification' ? 'container-absent' : 'container-removal');
664
+ }
665
+
85
666
  function readStartupOwner(filePath: string): { raw: string; owner: OwnerIdentity } | null {
86
667
  if (!fs.existsSync(filePath)) return null;
87
668
  if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
@@ -498,9 +1079,10 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
498
1079
  const expectedKeys = [
499
1080
  'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
500
1081
  'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
501
- 'taskId', 'token', 'worktreeRoot'
1082
+ 'taskId', 'token', 'worktreeRoot', 'authorityEvidence', 'controlRootId',
502
1083
  ];
503
- if (Object.keys(manifest).sort().join(',') !== expectedKeys.sort().join(',')) {
1084
+ const actualKeys = Object.keys(manifest).sort().join(',');
1085
+ if (actualKeys !== expectedKeys.sort().join(',')) {
504
1086
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
505
1087
  }
506
1088
  if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
@@ -521,8 +1103,10 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
521
1103
  || typeof candidate.processingDir !== 'string' || typeof candidate.runtimeDir !== 'string'
522
1104
  || typeof candidate.token !== 'string'
523
1105
  || typeof candidate.generation !== 'string'
1106
+ || typeof candidate.controlRootId !== 'string' || !/^[a-f0-9]{64,128}$/u.test(candidate.controlRootId)
524
1107
  || (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
525
- || (candidate.mode === 'branch-only' && candidate.taskId !== null)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
1108
+ || (candidate.mode === 'branch-only' && candidate.taskId !== null)
1109
+ || !isSandboxAuthorityEvidence(candidate.authorityEvidence)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
526
1110
  const root = path.dirname(path.resolve(manifestPath));
527
1111
  if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
528
1112
  || path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
@@ -542,15 +1126,17 @@ function readSandboxControlManifestValue(manifestPath: string): SandboxControlMa
542
1126
  id: containerIdentity.id as string,
543
1127
  labels: { ...(containerIdentity.labels as Record<string, string>) }
544
1128
  },
1129
+ authorityEvidence: candidate.authorityEvidence,
545
1130
  branch: candidate.branch,
546
1131
  mode: candidate.mode,
547
1132
  taskId: candidate.taskId,
548
1133
  token: candidate.token,
549
1134
  generation: candidate.generation,
1135
+ controlRootId: candidate.controlRootId,
550
1136
  channelDir: candidate.channelDir,
551
1137
  publicStatusDir: candidate.publicStatusDir,
552
1138
  processingDir: candidate.processingDir,
553
- runtimeDir: candidate.runtimeDir
1139
+ runtimeDir: candidate.runtimeDir,
554
1140
  };
555
1141
  return normalized;
556
1142
  }
@@ -816,18 +1402,42 @@ export type RemoveSandboxControlOptions = Readonly<{
816
1402
  removeContainer: (timeoutMs: number) => Promise<void>;
817
1403
  requireAbsent?: boolean;
818
1404
  selfOwner?: BrokerOwner;
1405
+ resourceLock?: SandboxResourceLock;
1406
+ retainRemovalJournal?: boolean;
1407
+ removalTarget?: SandboxRemovalTargetCommit;
819
1408
  }>;
820
1409
 
821
1410
  export async function removeSandboxControlRoot(
822
1411
  root: string,
823
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
824
1425
  ): Promise<void> {
825
1426
  const resolvedRoot = path.resolve(root);
826
1427
  if (!fs.existsSync(resolvedRoot)) return;
827
1428
  const stat = fs.lstatSync(resolvedRoot);
828
1429
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
829
1430
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
1431
+ const manifestRawBeforeLock = fs.readFileSync(manifestPath, 'utf8');
830
1432
  const manifest = readSandboxControlManifest(manifestPath);
1433
+ const resourceLock = options.resourceLock ?? acquireSandboxResourceLock(
1434
+ `${manifest.engine}:${manifest.containerIdentity.id}`,
1435
+ { lockDomain: manifest.authorityEvidence.lockDomain }
1436
+ );
1437
+ try {
1438
+ if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock) {
1439
+ throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
1440
+ }
831
1441
  const inspectContainer = options.inspectContainer
832
1442
  ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
833
1443
  const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
@@ -835,10 +1445,30 @@ export async function removeSandboxControlRoot(
835
1445
  const deadlineAt = Date.now() + timeoutMs;
836
1446
  const forceAt = forceDeadline(deadlineAt, timeoutMs);
837
1447
  const remaining = (): number => Math.max(0, deadlineAt - Date.now());
1448
+ const journal = startRemovalJournal(manifest, {
1449
+ target: options.removalTarget,
1450
+ identityProbe
1451
+ });
1452
+ if (journal.record.phase === 'carrier-finalizing') {
1453
+ if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock
1454
+ || !isSandboxControlRootQuiescing(resolvedRoot)) {
1455
+ throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
1456
+ }
1457
+ fs.rmSync(resolvedRoot, { recursive: true, force: true });
1458
+ if (fs.existsSync(resolvedRoot)) throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
1459
+ journal.write('carrier-removed');
1460
+ if (!options.retainRemovalJournal) clearSandboxRemovalJournal(manifest);
1461
+ return;
1462
+ }
1463
+ if (sandboxRemovalPhaseIndex(journal.record.phase) >= sandboxRemovalPhaseIndex('carrier-removed')) {
1464
+ throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
1465
+ }
1466
+ if (journal.record.phase === 'prepared') journal.write('target-committed');
838
1467
  const observation = await awaitWithDeadline(
839
1468
  () => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
840
1469
  );
841
1470
  if (observation.state === 'unknown') throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
1471
+ journal.write(observation.state === 'absent' ? 'container-absent' : 'container-removal');
842
1472
  if (options.requireAbsent && observation.state !== 'absent') {
843
1473
  throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
844
1474
  }
@@ -880,28 +1510,46 @@ export async function removeSandboxControlRoot(
880
1510
  }
881
1511
 
882
1512
  if (observation.state === 'found') {
883
- if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
884
- await awaitWithDeadline(
885
- () => options.removeContainer(remaining()),
886
- deadlineAt,
887
- 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
888
- );
889
- const afterRemoval = await awaitWithDeadline(
890
- () => inspectContainer(remaining()),
891
- deadlineAt,
892
- 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
893
- );
1513
+ if (Date.now() >= deadlineAt) {
1514
+ recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
1515
+ throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
1516
+ }
1517
+ let afterRemoval: ContainerObservation;
1518
+ try {
1519
+ await awaitWithDeadline(
1520
+ () => options.removeContainer(remaining()),
1521
+ deadlineAt,
1522
+ 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
1523
+ );
1524
+ afterRemoval = await awaitWithDeadline(
1525
+ () => inspectContainer(remaining()),
1526
+ deadlineAt,
1527
+ 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED'
1528
+ );
1529
+ } catch (error) {
1530
+ if (error instanceof Error && error.message === 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED') {
1531
+ recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
1532
+ throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
1533
+ }
1534
+ throw error;
1535
+ }
894
1536
  if (afterRemoval.state !== 'absent') {
895
1537
  throw new Error(afterRemoval.state === 'unknown'
896
1538
  ? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
897
1539
  : 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
898
1540
  }
1541
+ journal.write('container-absent');
899
1542
  }
900
1543
 
1544
+ const currentManifestRaw = fs.readFileSync(manifestPath, 'utf8');
901
1545
  const currentManifest = readSandboxControlManifest(manifestPath);
902
- if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
1546
+ if (currentManifestRaw !== manifestRawBeforeLock
1547
+ || currentManifest.engine !== manifest.engine
1548
+ || currentManifest.containerIdentity.id !== manifest.containerIdentity.id
1549
+ || currentManifest.authorityEvidence.lockDomain !== manifest.authorityEvidence.lockDomain
1550
+ || currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
903
1551
  || !isSandboxControlRootQuiescing(resolvedRoot)) {
904
- throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
1552
+ throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
905
1553
  }
906
1554
  const currentBroker = readBrokerOwner(brokerPath);
907
1555
  if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
@@ -970,7 +1618,13 @@ export async function removeSandboxControlRoot(
970
1618
  if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
971
1619
  throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
972
1620
  }
1621
+ journal.write('carrier-finalizing');
973
1622
  fs.rmSync(resolvedRoot, { recursive: true, force: true });
1623
+ journal.write('carrier-removed');
1624
+ if (!options.retainRemovalJournal) clearSandboxRemovalJournal(manifest);
1625
+ } finally {
1626
+ if (!options.resourceLock) resourceLock.release();
1627
+ }
974
1628
  }
975
1629
 
976
1630
  export type GarbageCollectSandboxControlOptions = Omit<RemoveSandboxControlOptions, 'removeContainer' | 'requireAbsent'>;