@fitlab-ai/agent-infra 0.9.13 → 0.9.14

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 (280) hide show
  1. package/bin/cli.ts +26 -9
  2. package/dist/bin/cli.js +173 -157
  3. package/dist/bin/internal-cli.js +42 -21
  4. package/dist/lib/agent-client.js +5 -0
  5. package/dist/lib/internal/agent-client.js +5 -2
  6. package/dist/lib/internal/cli-route-inventory.js +152 -0
  7. package/dist/lib/internal/codex-lifecycle.js +9 -6
  8. package/dist/lib/internal/codex-sandbox-controller.js +8 -2
  9. package/dist/lib/internal/git-workflow.js +10 -6
  10. package/dist/lib/internal/platform-checks.js +16 -4
  11. package/dist/lib/internal/platform-comment.js +16 -4
  12. package/dist/lib/internal/platform-context.js +4 -1
  13. package/dist/lib/internal/platform-issue.js +12 -4
  14. package/dist/lib/internal/platform-metadata.js +88 -0
  15. package/dist/lib/internal/platform-pr-review.js +16 -3
  16. package/dist/lib/internal/platform-pr.js +24 -10
  17. package/dist/lib/internal/platform-release-notes.js +6 -3
  18. package/dist/lib/internal/platform-security.js +93 -0
  19. package/dist/lib/internal/pr-review-grade.js +14 -3
  20. package/dist/lib/internal/release-workflow.js +19 -5
  21. package/dist/lib/internal/sandbox-control.js +22 -5
  22. package/dist/lib/internal/task-activity.js +13 -4
  23. package/dist/lib/internal/task-artifact.js +186 -27
  24. package/dist/lib/internal/task-context.js +4 -1
  25. package/dist/lib/internal/task-create.js +48 -15
  26. package/dist/lib/internal/task-delivery.js +4 -1
  27. package/dist/lib/internal/task-event.js +4 -1
  28. package/dist/lib/internal/task-finalization.js +4 -1
  29. package/dist/lib/internal/task-invalidation.js +4 -1
  30. package/dist/lib/internal/task-ledger.js +12 -1
  31. package/dist/lib/internal/task-lifecycle.js +4 -1
  32. package/dist/lib/internal/task-operation-registry.js +421 -0
  33. package/dist/lib/internal/task-orchestration.js +12 -0
  34. package/dist/lib/internal/task-override.js +10 -3
  35. package/dist/lib/internal/task-qualification.js +90 -0
  36. package/dist/lib/internal/task-review.js +4 -1
  37. package/dist/lib/internal/task-short-id.js +11 -1
  38. package/dist/lib/internal/task-snapshot.js +4 -1
  39. package/dist/lib/internal/task-validate.js +7 -2
  40. package/dist/lib/internal/task-verify.js +5 -1
  41. package/dist/lib/internal/task-warning.js +8 -1
  42. package/dist/lib/platform/comment-safety.js +226 -0
  43. package/dist/lib/platform/github-provider.js +233 -6
  44. package/dist/lib/platform/issue-comments.js +146 -34
  45. package/dist/lib/platform/pr-change-report.js +6 -12
  46. package/dist/lib/platform/pr-summary.js +28 -7
  47. package/dist/lib/platform/provider-contract.d.ts +67 -1
  48. package/dist/lib/platform/provider-contract.js +6 -2
  49. package/dist/lib/platform/provider-validation.js +76 -3
  50. package/dist/lib/platform/pull-requests.js +115 -170
  51. package/dist/lib/platform/repository-metadata.js +377 -0
  52. package/dist/lib/platform/security-alerts.js +155 -0
  53. package/dist/lib/platform/verification-sync.js +33 -26
  54. package/dist/lib/process-data/index.js +7 -0
  55. package/dist/lib/run/index.js +5 -0
  56. package/dist/lib/sandbox/commands/control-status.js +22 -0
  57. package/dist/lib/sandbox/commands/create.js +32 -6
  58. package/dist/lib/sandbox/commands/ls.js +5 -0
  59. package/dist/lib/sandbox/commands/rm.js +827 -120
  60. package/dist/lib/sandbox/commands/show.js +16 -0
  61. package/dist/lib/sandbox/control/client.js +83 -9
  62. package/dist/lib/sandbox/control/completed-reentry.js +85 -0
  63. package/dist/lib/sandbox/control/container-identity.js +28 -31
  64. package/dist/lib/sandbox/control/executor.js +149 -14
  65. package/dist/lib/sandbox/control/lifecycle.js +621 -127
  66. package/dist/lib/sandbox/control/native-file-lock.js +177 -0
  67. package/dist/lib/sandbox/control/server.js +305 -35
  68. package/dist/lib/sandbox/control/state.js +5 -3
  69. package/dist/lib/sandbox/control/task-view.js +183 -0
  70. package/dist/lib/sandbox/engines/authority.js +289 -0
  71. package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
  72. package/dist/lib/sandbox/index.js +5 -0
  73. package/dist/lib/sandbox/recovery.js +21 -1
  74. package/dist/lib/sandbox/workspace-view.js +2 -1
  75. package/dist/lib/sandbox/worktree-safety.js +4 -1
  76. package/dist/lib/server/index.js +8 -0
  77. package/dist/lib/task/artifact-lifecycle.js +42 -13
  78. package/dist/lib/task/artifact-operations.js +520 -0
  79. package/dist/lib/task/artifact-repair-intent.js +59 -0
  80. package/dist/lib/task/artifact-schema.js +135 -0
  81. package/dist/lib/task/capabilities.js +72 -1
  82. package/dist/lib/task/control-authority.js +4 -0
  83. package/dist/lib/task/create-service.js +103 -4
  84. package/dist/lib/task/create.js +25 -0
  85. package/dist/lib/task/events.js +186 -67
  86. package/dist/lib/task/index.js +11 -3
  87. package/dist/lib/task/local-artifact-finalization.js +75 -150
  88. package/dist/lib/task/orchestration.js +192 -52
  89. package/dist/lib/task/qualification-audit.js +490 -0
  90. package/dist/lib/task/qualification-confirmation.js +109 -0
  91. package/dist/lib/task/qualification-intents.js +148 -0
  92. package/dist/lib/task/review-artifacts.js +22 -23
  93. package/dist/lib/task/review-finalization.js +121 -4
  94. package/dist/lib/task/sections.js +6 -4
  95. package/dist/lib/task/verification-config.js +11 -0
  96. package/dist/lib/task/verification-engine.js +54 -4
  97. package/dist/lifecycle-build-manifest.json +3 -3
  98. package/lib/agent-client.ts +9 -0
  99. package/lib/internal/agent-client.ts +4 -2
  100. package/lib/internal/cli-route-inventory.ts +140 -0
  101. package/lib/internal/codex-lifecycle.ts +8 -6
  102. package/lib/internal/codex-sandbox-controller.ts +7 -2
  103. package/lib/internal/git-workflow.ts +9 -6
  104. package/lib/internal/platform-checks.ts +12 -4
  105. package/lib/internal/platform-comment.ts +12 -4
  106. package/lib/internal/platform-context.ts +3 -1
  107. package/lib/internal/platform-issue.ts +11 -4
  108. package/lib/internal/platform-metadata.ts +70 -0
  109. package/lib/internal/platform-pr-review.ts +14 -3
  110. package/lib/internal/platform-pr.ts +23 -10
  111. package/lib/internal/platform-release-notes.ts +5 -3
  112. package/lib/internal/platform-security.ts +83 -0
  113. package/lib/internal/pr-review-grade.ts +10 -3
  114. package/lib/internal/release-workflow.ts +17 -5
  115. package/lib/internal/sandbox-control.ts +19 -5
  116. package/lib/internal/task-activity.ts +12 -4
  117. package/lib/internal/task-artifact.ts +164 -25
  118. package/lib/internal/task-context.ts +3 -1
  119. package/lib/internal/task-create.ts +76 -15
  120. package/lib/internal/task-delivery.ts +3 -1
  121. package/lib/internal/task-event.ts +3 -1
  122. package/lib/internal/task-finalization.ts +3 -1
  123. package/lib/internal/task-invalidation.ts +3 -1
  124. package/lib/internal/task-ledger.ts +11 -1
  125. package/lib/internal/task-lifecycle.ts +3 -1
  126. package/lib/internal/task-operation-registry.ts +482 -0
  127. package/lib/internal/task-orchestration.ts +12 -0
  128. package/lib/internal/task-override.ts +9 -3
  129. package/lib/internal/task-qualification.ts +61 -0
  130. package/lib/internal/task-review.ts +3 -1
  131. package/lib/internal/task-short-id.ts +10 -1
  132. package/lib/internal/task-snapshot.ts +3 -1
  133. package/lib/internal/task-validate.ts +6 -2
  134. package/lib/internal/task-verify.ts +4 -1
  135. package/lib/internal/task-warning.ts +7 -1
  136. package/lib/platform/comment-safety.ts +250 -0
  137. package/lib/platform/github-provider.ts +229 -7
  138. package/lib/platform/issue-comments.ts +160 -35
  139. package/lib/platform/pr-change-report.ts +6 -12
  140. package/lib/platform/pr-summary.ts +27 -6
  141. package/lib/platform/provider-contract.ts +78 -3
  142. package/lib/platform/provider-validation.ts +85 -3
  143. package/lib/platform/pull-requests.ts +126 -178
  144. package/lib/platform/repository-metadata.ts +453 -0
  145. package/lib/platform/security-alerts.ts +191 -0
  146. package/lib/platform/verification-sync.ts +32 -28
  147. package/lib/process-data/index.ts +7 -0
  148. package/lib/run/index.ts +5 -0
  149. package/lib/sandbox/commands/control-status.ts +26 -0
  150. package/lib/sandbox/commands/create.ts +46 -6
  151. package/lib/sandbox/commands/ls.ts +7 -0
  152. package/lib/sandbox/commands/rm.ts +1000 -87
  153. package/lib/sandbox/commands/show.ts +16 -0
  154. package/lib/sandbox/control/client.ts +103 -9
  155. package/lib/sandbox/control/completed-reentry.ts +98 -0
  156. package/lib/sandbox/control/container-identity.ts +27 -43
  157. package/lib/sandbox/control/executor.ts +177 -12
  158. package/lib/sandbox/control/lifecycle.ts +629 -17
  159. package/lib/sandbox/control/native-file-lock.ts +209 -0
  160. package/lib/sandbox/control/protocol.ts +5 -2
  161. package/lib/sandbox/control/server.ts +319 -37
  162. package/lib/sandbox/control/state.ts +6 -3
  163. package/lib/sandbox/control/task-view.ts +240 -0
  164. package/lib/sandbox/engines/authority.ts +365 -0
  165. package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
  166. package/lib/sandbox/index.ts +7 -0
  167. package/lib/sandbox/recovery.ts +21 -1
  168. package/lib/sandbox/workspace-view.ts +9 -3
  169. package/lib/sandbox/worktree-safety.ts +4 -1
  170. package/lib/server/index.ts +10 -0
  171. package/lib/task/artifact-lifecycle.ts +38 -16
  172. package/lib/task/artifact-operations.ts +626 -0
  173. package/lib/task/artifact-repair-intent.ts +71 -0
  174. package/lib/task/artifact-schema.ts +192 -0
  175. package/lib/task/capabilities.ts +70 -1
  176. package/lib/task/control-authority.ts +11 -1
  177. package/lib/task/create-service.ts +131 -6
  178. package/lib/task/create.ts +27 -0
  179. package/lib/task/events.ts +161 -65
  180. package/lib/task/index.ts +16 -3
  181. package/lib/task/local-artifact-finalization.ts +87 -218
  182. package/lib/task/orchestration.ts +172 -49
  183. package/lib/task/qualification-audit.ts +599 -0
  184. package/lib/task/qualification-confirmation.ts +117 -0
  185. package/lib/task/qualification-intents.ts +150 -0
  186. package/lib/task/review-artifacts.ts +21 -22
  187. package/lib/task/review-finalization.ts +133 -4
  188. package/lib/task/sections.ts +6 -4
  189. package/lib/task/verification-config.ts +12 -0
  190. package/lib/task/verification-engine.ts +55 -4
  191. package/package.json +6 -1
  192. package/templates/.agents/rules/README.en.md +2 -0
  193. package/templates/.agents/rules/README.zh-CN.md +2 -0
  194. package/templates/.agents/rules/decision-qualification.en.md +22 -0
  195. package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
  196. package/templates/.agents/rules/evidence-reporting.en.md +28 -0
  197. package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
  198. package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
  199. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
  200. package/templates/.agents/rules/local-artifact-repair.en.md +5 -1
  201. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +5 -1
  202. package/templates/.agents/rules/security-alerts.en.md +8 -8
  203. package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
  204. package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -2
  205. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +19 -2
  206. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
  207. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
  208. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  209. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  210. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  211. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  212. package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
  213. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
  214. package/templates/.agents/skills/code-task/config/verify.en.json +5 -14
  215. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +5 -14
  216. package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
  217. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
  218. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
  219. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
  220. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
  221. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
  222. package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
  223. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
  224. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -1
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -1
  226. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  227. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  228. package/templates/.agents/skills/complete-task/SKILL.en.md +9 -22
  229. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +9 -22
  230. package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
  231. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
  232. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  233. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  234. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  235. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  236. package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
  237. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
  238. package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
  239. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
  240. package/templates/.agents/skills/plan-task/SKILL.en.md +14 -1
  241. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +16 -1
  242. package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
  243. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
  244. package/templates/.agents/skills/review-analysis/SKILL.en.md +14 -1
  245. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -1
  246. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
  247. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
  248. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
  249. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
  250. package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
  251. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
  252. package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
  253. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
  254. package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
  255. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
  256. package/templates/.agents/skills/review-plan/SKILL.en.md +14 -1
  257. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -1
  258. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
  259. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
  260. package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
  261. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
  262. package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
  263. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
  264. package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
  265. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
  266. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -1
  267. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -1
  268. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +3 -1
  269. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +3 -1
  270. package/templates/.agents/skills/test/SKILL.en.md +13 -2
  271. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -2
  272. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
  273. package/templates/.agents/templates/task.en.md +6 -0
  274. package/templates/.agents/templates/task.zh-CN.md +6 -0
  275. package/templates/.agents/scripts/security-alerts.github.sh +0 -151
  276. package/templates/.agents/scripts/security-alerts.sh +0 -31
  277. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
  278. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
  279. package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
  280. package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +0 -6
@@ -1,17 +1,433 @@
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.js';
6
7
  import { getProcessIdentityState, getProcessStartTime } from '../../server/process-state.js';
7
8
  import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
8
9
  import { inspectSandboxControlContainer } from './container-identity.js';
10
+ import { acquireSandboxResourceLock, resolveSandboxLockNamespace } from './native-file-lock.js';
11
+ import { isSandboxAuthorityEvidence } from '../engines/authority.js';
9
12
  import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
10
13
  const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
11
14
  const QUIESCING_FILE = 'quiescing.json';
12
15
  const BROKER_STARTING_FILE = 'broker-starting.json';
13
16
  const REPLACEMENT_FILE = 'replacement.json';
14
17
  const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
18
+ const REMOVAL_JOURNAL_ROOT = path.join('.agent-infra', 'sandbox-removal-journal');
19
+ export const SANDBOX_REMOVAL_JOURNAL_PHASES = [
20
+ 'prepared', 'target-committed', 'container-removal', 'container-absent',
21
+ 'carrier-finalizing', 'carrier-removed', 'workspace-finalizing', 'workspace-removed',
22
+ 'branch-finalizing', 'branch-removed', 'tool-finalizing', 'tool-removed',
23
+ 'shell-finalizing', 'shell-removed', 'share-finalizing', 'share-removed', 'completed'
24
+ ];
25
+ function removalCarrierDigest(engine, containerId, generation) {
26
+ return createHash('sha256').update(`${engine}\0${containerId}\0${generation}`).digest('hex');
27
+ }
28
+ function assertPrivateDirectory(directory) {
29
+ const home = path.resolve(os.homedir());
30
+ const resolved = path.resolve(directory);
31
+ const relative = path.relative(home, resolved);
32
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
33
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
34
+ }
35
+ let current = home;
36
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
37
+ current = path.join(current, segment);
38
+ const stat = fs.lstatSync(current);
39
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
40
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
41
+ }
42
+ if (process.platform !== 'win32' && typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
43
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
44
+ }
45
+ }
46
+ }
47
+ function removalJournalDomain(lockDomain, create = true) {
48
+ const namespace = resolveSandboxLockNamespace('sandbox-removal-journal', { lockDomain });
49
+ const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1));
50
+ if (create)
51
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
52
+ assertPrivateDirectory(root);
53
+ const domain = path.join(root, lockDomain);
54
+ if (create)
55
+ fs.mkdirSync(domain, { recursive: true, mode: 0o700 });
56
+ assertPrivateDirectory(domain);
57
+ return domain;
58
+ }
59
+ function removalJournalPath(manifest) {
60
+ return path.join(removalJournalDomain(manifest.authorityEvidence.lockDomain), `${removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation)}.json`);
61
+ }
62
+ function removalJournalPathForRecord(record) {
63
+ return path.join(removalJournalDomain(record.lockDomain, false), `${record.carrierIdentityDigest}.json`);
64
+ }
65
+ function isRecord(value) {
66
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
67
+ }
68
+ function hasExactKeys(value, keys) {
69
+ const actual = Object.keys(value).sort();
70
+ return actual.length === keys.length && actual.every((key, index) => key === keys[index]);
71
+ }
72
+ function isRemovalPermitCommit(value) {
73
+ if (!isRecord(value) || typeof value.path !== 'string' || !path.isAbsolute(value.path)
74
+ || (value.mode !== 'clean' && value.mode !== 'discard') || !isRecord(value.snapshot)
75
+ || typeof value.snapshot.worktree !== 'string' || !path.isAbsolute(value.snapshot.worktree)
76
+ || typeof value.snapshot.branch !== 'string' || typeof value.snapshot.head !== 'string'
77
+ || typeof value.snapshot.identity !== 'string' || !Array.isArray(value.snapshot.changes)
78
+ || value.snapshot.changes.some((change) => !isRecord(change)
79
+ || typeof change.indexStatus !== 'string' || typeof change.worktreeStatus !== 'string'
80
+ || typeof change.path !== 'string'
81
+ || (change.originalPath !== undefined && typeof change.originalPath !== 'string'))
82
+ || (value.snapshot.source !== undefined
83
+ && value.snapshot.source !== 'registered' && value.snapshot.source !== 'recovered'))
84
+ return false;
85
+ if (value.snapshot.recovery === undefined)
86
+ return true;
87
+ const recovery = value.snapshot.recovery;
88
+ return isRecord(recovery)
89
+ && typeof recovery.repoRoot === 'string' && path.isAbsolute(recovery.repoRoot)
90
+ && typeof recovery.worktreeBase === 'string' && path.isAbsolute(recovery.worktreeBase)
91
+ && typeof recovery.branch === 'string'
92
+ && (recovery.identitySource === 'branch-only' || recovery.identitySource === 'task-bound')
93
+ && (recovery.taskId === null || typeof recovery.taskId === 'string');
94
+ }
95
+ function parseRemovalJournal(raw) {
96
+ let value;
97
+ try {
98
+ value = JSON.parse(raw);
99
+ }
100
+ catch {
101
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
102
+ }
103
+ if (!isRecord(value) || !hasExactKeys(value, [
104
+ 'authorityFingerprint', 'carrierIdentityDigest', 'containerId', 'engine', 'expectedOldJournalRevision',
105
+ 'generation', 'handoffId', 'lockDomain', 'newOperation', 'oldOperation', 'operation', 'owner', 'phase',
106
+ 'recordedAt', 'revision', 'target', 'transitionId', 'version'
107
+ ]) || !isRecord(value.owner) || !hasExactKeys(value.owner, ['leaseNonce', 'pid', 'startTime'])
108
+ || !isRecord(value.target) || !hasExactKeys(value.target, [
109
+ 'branch', 'controlRoot', 'permitDigest', 'permits', 'project', 'removeBranch', 'removeShare',
110
+ 'removeWorktree', 'sharePath', 'shellPaths', 'targetDigest', 'toolPaths', 'workspaceViewPaths',
111
+ 'worktreePaths'
112
+ ]) || value.version !== 2 || value.operation !== 'sandbox-rm'
113
+ || value.oldOperation !== 'sandbox-rm' || value.newOperation !== 'sandbox-rm'
114
+ || typeof value.handoffId !== 'string' || value.handoffId.length === 0
115
+ || typeof value.transitionId !== 'string' || value.transitionId.length === 0
116
+ || typeof value.engine !== 'string' || value.engine.length === 0
117
+ || typeof value.containerId !== 'string' || value.containerId.length === 0
118
+ || typeof value.generation !== 'string' || value.generation.length === 0
119
+ || typeof value.authorityFingerprint !== 'string' || !/^[a-f0-9]{64}$/u.test(value.authorityFingerprint)
120
+ || typeof value.carrierIdentityDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.carrierIdentityDigest)
121
+ || typeof value.lockDomain !== 'string' || !/^[a-f0-9]{64}$/u.test(value.lockDomain)
122
+ || !Number.isSafeInteger(value.owner.pid) || value.owner.pid <= 0
123
+ || !Number.isSafeInteger(value.owner.startTime) || typeof value.owner.leaseNonce !== 'string'
124
+ || value.owner.leaseNonce.length === 0
125
+ || !SANDBOX_REMOVAL_JOURNAL_PHASES.includes(value.phase)
126
+ || !(value.expectedOldJournalRevision === null
127
+ || (Number.isSafeInteger(value.expectedOldJournalRevision) && value.expectedOldJournalRevision >= 0))
128
+ || !Number.isSafeInteger(value.revision) || value.revision <= 0
129
+ || typeof value.target.branch !== 'string' || value.target.branch.length === 0
130
+ || typeof value.target.project !== 'string' || value.target.project.length === 0
131
+ || typeof value.target.controlRoot !== 'string' || path.isAbsolute(value.target.controlRoot) === false
132
+ || typeof value.target.targetDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.targetDigest)
133
+ || typeof value.target.permitDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.target.permitDigest)
134
+ || typeof value.target.removeWorktree !== 'boolean'
135
+ || typeof value.target.removeBranch !== 'boolean'
136
+ || typeof value.target.removeShare !== 'boolean'
137
+ || typeof value.target.sharePath !== 'string' || !path.isAbsolute(value.target.sharePath)
138
+ || !Array.isArray(value.target.worktreePaths) || value.target.worktreePaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
139
+ || !Array.isArray(value.target.workspaceViewPaths) || value.target.workspaceViewPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
140
+ || !Array.isArray(value.target.toolPaths) || value.target.toolPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
141
+ || !Array.isArray(value.target.shellPaths) || value.target.shellPaths.some((entry) => typeof entry !== 'string' || !path.isAbsolute(entry))
142
+ || !Array.isArray(value.target.permits)
143
+ || value.target.permits.some((permit) => !isRemovalPermitCommit(permit))
144
+ || !Number.isSafeInteger(value.recordedAt)) {
145
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
146
+ }
147
+ const expected = value.expectedOldJournalRevision;
148
+ const revision = value.revision;
149
+ if ((expected === null && revision !== 1) || (expected !== null && revision !== expected + 1)) {
150
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
151
+ }
152
+ return {
153
+ version: 2,
154
+ operation: 'sandbox-rm',
155
+ oldOperation: 'sandbox-rm',
156
+ newOperation: 'sandbox-rm',
157
+ handoffId: value.handoffId,
158
+ transitionId: value.transitionId,
159
+ engine: value.engine,
160
+ containerId: value.containerId,
161
+ generation: value.generation,
162
+ authorityFingerprint: value.authorityFingerprint,
163
+ carrierIdentityDigest: value.carrierIdentityDigest,
164
+ lockDomain: value.lockDomain,
165
+ owner: {
166
+ pid: value.owner.pid,
167
+ startTime: value.owner.startTime,
168
+ leaseNonce: value.owner.leaseNonce
169
+ },
170
+ phase: value.phase,
171
+ expectedOldJournalRevision: expected,
172
+ revision,
173
+ target: {
174
+ branch: value.target.branch,
175
+ project: value.target.project,
176
+ controlRoot: value.target.controlRoot,
177
+ targetDigest: value.target.targetDigest,
178
+ permitDigest: value.target.permitDigest,
179
+ removeWorktree: value.target.removeWorktree,
180
+ removeBranch: value.target.removeBranch,
181
+ removeShare: value.target.removeShare,
182
+ worktreePaths: value.target.worktreePaths,
183
+ workspaceViewPaths: value.target.workspaceViewPaths,
184
+ toolPaths: value.target.toolPaths,
185
+ shellPaths: value.target.shellPaths,
186
+ sharePath: value.target.sharePath,
187
+ permits: value.target.permits
188
+ },
189
+ recordedAt: value.recordedAt
190
+ };
191
+ }
192
+ function readRemovalJournalAt(target) {
193
+ if (!fs.existsSync(target))
194
+ return null;
195
+ const stat = fs.lstatSync(target);
196
+ if (!stat.isFile() || stat.isSymbolicLink())
197
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
198
+ return parseRemovalJournal(fs.readFileSync(target, 'utf8'));
199
+ }
200
+ export function readSandboxRemovalJournal(manifest) {
201
+ return readRemovalJournalAt(removalJournalPath(manifest));
202
+ }
203
+ export function listSandboxRemovalJournals(filter = {}) {
204
+ const namespace = resolveSandboxLockNamespace('sandbox-removal-journal');
205
+ const root = path.join(path.dirname(namespace.lockRoot), REMOVAL_JOURNAL_ROOT.split(path.sep).at(-1));
206
+ if (!fs.existsSync(root))
207
+ return [];
208
+ assertPrivateDirectory(root);
209
+ const journals = [];
210
+ for (const domainEntry of fs.readdirSync(root, { withFileTypes: true })) {
211
+ if (!domainEntry.isDirectory() || domainEntry.isSymbolicLink()) {
212
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
213
+ }
214
+ const domain = path.join(root, domainEntry.name);
215
+ assertPrivateDirectory(domain);
216
+ for (const entry of fs.readdirSync(domain, { withFileTypes: true })) {
217
+ if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith('.json')) {
218
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
219
+ }
220
+ const record = readRemovalJournalAt(path.join(domain, entry.name));
221
+ if (!record || record.lockDomain !== domainEntry.name
222
+ || `${record.carrierIdentityDigest}.json` !== entry.name) {
223
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_INVALID');
224
+ }
225
+ if ((!filter.branch || record.target.branch === filter.branch)
226
+ && (!filter.project || record.target.project === filter.project)
227
+ && (!filter.targetDigest || record.target.targetDigest === filter.targetDigest)) {
228
+ journals.push(record);
229
+ }
230
+ }
231
+ }
232
+ return journals;
233
+ }
234
+ export function clearSandboxRemovalJournalRecord(record) {
235
+ const target = removalJournalPathForRecord(record);
236
+ if (fs.existsSync(target)) {
237
+ const current = readRemovalJournalAt(target);
238
+ if (!current || current.revision !== record.revision || current.handoffId !== record.handoffId) {
239
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
240
+ }
241
+ fs.rmSync(target, { force: true });
242
+ }
243
+ }
244
+ export function clearSandboxRemovalJournal(manifest) {
245
+ const target = removalJournalPath(manifest);
246
+ if (fs.existsSync(target))
247
+ fs.rmSync(target, { force: true });
248
+ }
249
+ function defaultRemovalTarget(manifest) {
250
+ const targetDigest = createHash('sha256')
251
+ .update(`${manifest.project}\0${manifest.branch}\0${manifest.container}\0${manifest.channelDir}`)
252
+ .digest('hex');
253
+ return {
254
+ branch: manifest.branch,
255
+ project: manifest.project,
256
+ controlRoot: path.dirname(manifest.channelDir),
257
+ targetDigest,
258
+ permitDigest: createHash('sha256').update('none').digest('hex'),
259
+ removeWorktree: false,
260
+ removeBranch: false,
261
+ removeShare: false,
262
+ worktreePaths: [],
263
+ workspaceViewPaths: [],
264
+ toolPaths: [],
265
+ shellPaths: [],
266
+ sharePath: path.join(path.dirname(manifest.channelDir), 'share'),
267
+ permits: []
268
+ };
269
+ }
270
+ function sameRemovalTarget(left, right) {
271
+ return JSON.stringify(left) === JSON.stringify(right);
272
+ }
273
+ function phaseIndex(phase) {
274
+ return SANDBOX_REMOVAL_JOURNAL_PHASES.indexOf(phase);
275
+ }
276
+ export function sandboxRemovalPhaseIndex(phase) {
277
+ return phaseIndex(phase);
278
+ }
279
+ function assertNextRemovalPhase(current, next) {
280
+ const currentIndex = phaseIndex(current);
281
+ const nextIndex = phaseIndex(next);
282
+ if (nextIndex === currentIndex)
283
+ return false;
284
+ if (current === 'target-committed' && next === 'container-absent')
285
+ return true;
286
+ if (nextIndex !== currentIndex + 1) {
287
+ throw new Error('SANDBOX_CONTROL_REMOVAL_PHASE_TRANSITION_INVALID');
288
+ }
289
+ return true;
290
+ }
291
+ function assertRemovalJournalLock(record, resourceLock) {
292
+ const namespace = resolveSandboxLockNamespace(`${record.engine}:${record.containerId}`, {
293
+ lockDomain: record.lockDomain
294
+ });
295
+ if (resourceLock.lockDomain !== record.lockDomain || resourceLock.path !== namespace.lockPath) {
296
+ throw new Error('SANDBOX_CONTROL_REMOVAL_LOCK_MISMATCH');
297
+ }
298
+ }
299
+ function writeRemovalJournalAt(target, base, phase, expectedOldJournalRevision) {
300
+ const existing = readRemovalJournalAt(target);
301
+ if (expectedOldJournalRevision === null
302
+ ? existing !== null
303
+ : existing?.revision !== expectedOldJournalRevision) {
304
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
305
+ }
306
+ const record = {
307
+ ...base,
308
+ phase,
309
+ expectedOldJournalRevision,
310
+ revision: expectedOldJournalRevision === null ? 1 : expectedOldJournalRevision + 1,
311
+ recordedAt: Date.now()
312
+ };
313
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
314
+ try {
315
+ fs.writeFileSync(temporary, `${JSON.stringify(record)}\n`, { mode: 0o600, flag: 'wx' });
316
+ if (expectedOldJournalRevision === null && fs.existsSync(target)) {
317
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
318
+ }
319
+ fs.renameSync(temporary, target);
320
+ }
321
+ finally {
322
+ fs.rmSync(temporary, { force: true });
323
+ }
324
+ return record;
325
+ }
326
+ function writeRemovalJournal(manifest, base, phase, expectedOldJournalRevision) {
327
+ return writeRemovalJournalAt(removalJournalPath(manifest), base, phase, expectedOldJournalRevision);
328
+ }
329
+ function startRemovalJournal(manifest, options) {
330
+ const target = removalJournalPath(manifest);
331
+ const existing = readRemovalJournalAt(target);
332
+ if (existing && (existing.engine !== manifest.engine || existing.containerId !== manifest.containerIdentity.id
333
+ || existing.generation !== manifest.generation
334
+ || existing.authorityFingerprint !== manifest.authorityEvidence.authorityFingerprint
335
+ || existing.lockDomain !== manifest.authorityEvidence.lockDomain)) {
336
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_IDENTITY_MISMATCH');
337
+ }
338
+ const targetCommit = options.target ?? existing?.target ?? defaultRemovalTarget(manifest);
339
+ if (existing && options.target && !sameRemovalTarget(existing.target, options.target)) {
340
+ throw new Error('SANDBOX_CONTROL_REMOVAL_TARGET_MISMATCH');
341
+ }
342
+ if (existing) {
343
+ const state = options.identityProbe(existing.owner);
344
+ if (state === 'alive')
345
+ throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
346
+ if (state === 'unknown')
347
+ throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
348
+ }
349
+ const startTime = getProcessStartTime(process.pid);
350
+ if (startTime === null)
351
+ throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
352
+ const base = {
353
+ version: 2,
354
+ operation: 'sandbox-rm',
355
+ oldOperation: 'sandbox-rm',
356
+ newOperation: 'sandbox-rm',
357
+ handoffId: randomUUID(),
358
+ transitionId: randomUUID(),
359
+ engine: manifest.engine,
360
+ containerId: manifest.containerIdentity.id,
361
+ generation: manifest.generation,
362
+ authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
363
+ carrierIdentityDigest: removalCarrierDigest(manifest.engine, manifest.containerIdentity.id, manifest.generation),
364
+ lockDomain: manifest.authorityEvidence.lockDomain,
365
+ owner: { pid: process.pid, startTime, leaseNonce: randomUUID() },
366
+ target: targetCommit
367
+ };
368
+ const initialPhase = existing?.phase ?? 'prepared';
369
+ let record = writeRemovalJournal(manifest, base, initialPhase, existing?.revision ?? null);
370
+ return {
371
+ get record() { return record; },
372
+ write(phase) {
373
+ if (!assertNextRemovalPhase(record.phase, phase))
374
+ return record;
375
+ record = writeRemovalJournal(manifest, base, phase, record.revision);
376
+ return record;
377
+ }
378
+ };
379
+ }
380
+ export function claimSandboxRemovalJournal(journal, options = {}) {
381
+ const target = removalJournalPathForRecord(journal);
382
+ const resourceLock = options.resourceLock
383
+ ?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
384
+ const ownedLock = options.resourceLock ? null : resourceLock;
385
+ try {
386
+ assertRemovalJournalLock(journal, resourceLock);
387
+ const current = readRemovalJournalAt(target);
388
+ if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
389
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
390
+ }
391
+ const state = (options.identityProbe ?? getProcessIdentityState)(current.owner);
392
+ if (state === 'alive')
393
+ throw new Error('SANDBOX_CONTROL_REMOVE_RETRY_IN_PROGRESS');
394
+ if (state === 'unknown')
395
+ throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNKNOWN');
396
+ const startTime = getProcessStartTime(process.pid);
397
+ if (startTime === null)
398
+ throw new Error('SANDBOX_CONTROL_REMOVE_OWNER_UNAVAILABLE');
399
+ const base = {
400
+ ...current,
401
+ handoffId: randomUUID(),
402
+ transitionId: randomUUID(),
403
+ owner: { pid: process.pid, startTime, leaseNonce: randomUUID() }
404
+ };
405
+ return writeRemovalJournalAt(target, base, current.phase, current.revision);
406
+ }
407
+ finally {
408
+ ownedLock?.release();
409
+ }
410
+ }
411
+ export function advanceSandboxRemovalJournalPhase(journal, phase, options = {}) {
412
+ const target = removalJournalPathForRecord(journal);
413
+ const resourceLock = options.resourceLock
414
+ ?? acquireSandboxResourceLock(`${journal.engine}:${journal.containerId}`, { lockDomain: journal.lockDomain });
415
+ const ownedLock = options.resourceLock ? null : resourceLock;
416
+ try {
417
+ assertRemovalJournalLock(journal, resourceLock);
418
+ const current = readRemovalJournalAt(target);
419
+ if (!current || current.revision !== journal.revision || current.handoffId !== journal.handoffId) {
420
+ throw new Error('SANDBOX_CONTROL_REMOVAL_JOURNAL_REVISION_MISMATCH');
421
+ }
422
+ if (!assertNextRemovalPhase(current.phase, phase))
423
+ return current;
424
+ const { phase: _phase, expectedOldJournalRevision: _expected, revision: _revision, recordedAt: _recordedAt, ...base } = current;
425
+ return writeRemovalJournalAt(target, base, phase, current.revision);
426
+ }
427
+ finally {
428
+ ownedLock?.release();
429
+ }
430
+ }
15
431
  async function awaitWithDeadline(operation, deadlineAt, errorCode) {
16
432
  const remaining = deadlineAt - Date.now();
17
433
  if (remaining <= 0)
@@ -62,6 +478,25 @@ function markSandboxControlRootQuiescing(root) {
62
478
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
63
479
  }
64
480
  }
481
+ function recordSandboxRemovalPending(root, manifest, phase, journal) {
482
+ const pendingPath = path.join(root, 'removal-pending.json');
483
+ const temporary = `${pendingPath}.${process.pid}.${randomUUID()}.tmp`;
484
+ try {
485
+ fs.writeFileSync(temporary, `${JSON.stringify({
486
+ version: 1,
487
+ phase,
488
+ containerId: manifest.containerIdentity.id,
489
+ generation: manifest.generation,
490
+ authorityFingerprint: manifest.authorityEvidence.authorityFingerprint,
491
+ recordedAt: Date.now()
492
+ })}\n`, { mode: 0o600, flag: 'wx' });
493
+ fs.renameSync(temporary, pendingPath);
494
+ }
495
+ finally {
496
+ fs.rmSync(temporary, { force: true });
497
+ }
498
+ journal.write(phase === 'container-verification' ? 'container-absent' : 'container-removal');
499
+ }
65
500
  function readStartupOwner(filePath) {
66
501
  if (!fs.existsSync(filePath))
67
502
  return null;
@@ -446,9 +881,10 @@ function readSandboxControlManifestValue(manifestPath) {
446
881
  const expectedKeys = [
447
882
  'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
448
883
  'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
449
- 'taskId', 'token', 'worktreeRoot'
884
+ 'taskId', 'token', 'worktreeRoot', 'authorityEvidence'
450
885
  ];
451
- if (Object.keys(manifest).sort().join(',') !== expectedKeys.sort().join(',')) {
886
+ const actualKeys = Object.keys(manifest).sort().join(',');
887
+ if (actualKeys !== expectedKeys.sort().join(',')) {
452
888
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
453
889
  }
454
890
  if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
@@ -471,7 +907,8 @@ function readSandboxControlManifestValue(manifestPath) {
471
907
  || typeof candidate.token !== 'string'
472
908
  || typeof candidate.generation !== 'string'
473
909
  || (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
474
- || (candidate.mode === 'branch-only' && candidate.taskId !== null))
910
+ || (candidate.mode === 'branch-only' && candidate.taskId !== null)
911
+ || !isSandboxAuthorityEvidence(candidate.authorityEvidence))
475
912
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
476
913
  const root = path.dirname(path.resolve(manifestPath));
477
914
  if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
@@ -492,6 +929,7 @@ function readSandboxControlManifestValue(manifestPath) {
492
929
  id: containerIdentity.id,
493
930
  labels: { ...containerIdentity.labels }
494
931
  },
932
+ authorityEvidence: candidate.authorityEvidence,
495
933
  branch: candidate.branch,
496
934
  mode: candidate.mode,
497
935
  taskId: candidate.taskId,
@@ -761,142 +1199,198 @@ export async function removeSandboxControlRoot(root, options) {
761
1199
  if (!stat.isDirectory() || stat.isSymbolicLink())
762
1200
  throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
763
1201
  const manifestPath = path.join(resolvedRoot, 'manifest.json');
1202
+ const manifestRawBeforeLock = fs.readFileSync(manifestPath, 'utf8');
764
1203
  const manifest = readSandboxControlManifest(manifestPath);
765
- const inspectContainer = options.inspectContainer
766
- ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
767
- const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
768
- const identityProbe = options.identityProbe ?? getProcessIdentityState;
769
- const deadlineAt = Date.now() + timeoutMs;
770
- const forceAt = forceDeadline(deadlineAt, timeoutMs);
771
- const remaining = () => Math.max(0, deadlineAt - Date.now());
772
- const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
773
- if (observation.state === 'unknown')
774
- throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
775
- if (options.requireAbsent && observation.state !== 'absent') {
776
- throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
777
- }
778
- markSandboxControlRootQuiescing(resolvedRoot);
779
- await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
780
- const brokerPath = path.join(resolvedRoot, 'broker.json');
781
- const statusPath = path.join(resolvedRoot, 'public', 'status.json');
782
- const broker = readBrokerOwner(brokerPath);
783
- const status = readStatusOwner(statusPath);
784
- if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
785
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
786
- }
787
- if (status && status.generation !== manifest.generation) {
788
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
789
- }
790
- if (broker && status && !sameOwner(broker, status.broker)
791
- && (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
792
- throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
793
- }
794
- const platform = options.platform ?? process.platform;
795
- const owner = broker && ownerLive(broker, identityProbe) ? broker
796
- : status && ownerLive(status.broker, identityProbe) ? status.broker : null;
797
- const selfOwner = options.selfOwner;
798
- const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
799
- if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
800
- throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
801
- }
802
- let executions = readExecutions(manifest);
803
- if (owner && !selfOwned)
804
- signalOwner(owner, platform, false);
805
- for (const execution of executions) {
806
- terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
807
- }
808
- const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
809
- while (Date.now() < softDeadlineAt) {
810
- if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
811
- && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
812
- break;
813
- await new Promise((resolve) => setTimeout(resolve, 25));
814
- }
815
- if (observation.state === 'found') {
816
- if (Date.now() >= deadlineAt)
817
- throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
818
- await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
819
- const afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
820
- if (afterRemoval.state !== 'absent') {
821
- throw new Error(afterRemoval.state === 'unknown'
822
- ? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
823
- : 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
1204
+ const resourceLock = options.resourceLock ?? acquireSandboxResourceLock(`${manifest.engine}:${manifest.containerIdentity.id}`, { lockDomain: manifest.authorityEvidence.lockDomain });
1205
+ try {
1206
+ if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock) {
1207
+ throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
824
1208
  }
825
- }
826
- const currentManifest = readSandboxControlManifest(manifestPath);
827
- if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
828
- || !isSandboxControlRootQuiescing(resolvedRoot)) {
829
- throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
830
- }
831
- const currentBroker = readBrokerOwner(brokerPath);
832
- if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
833
- || currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
834
- throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
835
- }
836
- if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
837
- throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
838
- }
839
- if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
840
- throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
841
- }
842
- if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
843
- throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
844
- }
845
- executions = mergeExecutions(executions, readExecutions(manifest));
846
- if (Date.now() >= deadlineAt)
847
- throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
848
- for (const execution of executions) {
849
- if (!terminateSandboxControlExecution(execution, {
850
- platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
851
- })) {
852
- throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
1209
+ const inspectContainer = options.inspectContainer
1210
+ ?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
1211
+ const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
1212
+ const identityProbe = options.identityProbe ?? getProcessIdentityState;
1213
+ const deadlineAt = Date.now() + timeoutMs;
1214
+ const forceAt = forceDeadline(deadlineAt, timeoutMs);
1215
+ const remaining = () => Math.max(0, deadlineAt - Date.now());
1216
+ const journal = startRemovalJournal(manifest, {
1217
+ target: options.removalTarget,
1218
+ identityProbe
1219
+ });
1220
+ if (journal.record.phase === 'carrier-finalizing') {
1221
+ if (fs.readFileSync(manifestPath, 'utf8') !== manifestRawBeforeLock
1222
+ || !isSandboxControlRootQuiescing(resolvedRoot)) {
1223
+ throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
1224
+ }
1225
+ fs.rmSync(resolvedRoot, { recursive: true, force: true });
1226
+ if (fs.existsSync(resolvedRoot))
1227
+ throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
1228
+ journal.write('carrier-removed');
1229
+ if (!options.retainRemovalJournal)
1230
+ clearSandboxRemovalJournal(manifest);
1231
+ return;
853
1232
  }
854
- }
855
- if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
856
- if (remaining() <= 0)
857
- throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
858
- signalOwner(currentBroker, platform, true);
859
- if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
860
- throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
1233
+ if (sandboxRemovalPhaseIndex(journal.record.phase) >= sandboxRemovalPhaseIndex('carrier-removed')) {
1234
+ throw new Error('SANDBOX_CONTROL_TARGET_MISMATCH');
861
1235
  }
862
- }
863
- const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
864
- if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
865
- throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
866
- }
867
- const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
868
- if (finalBrokerRaw !== null) {
869
- const finalBroker = readBrokerOwner(brokerPath);
870
- if (!finalBroker)
1236
+ if (journal.record.phase === 'prepared')
1237
+ journal.write('target-committed');
1238
+ const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
1239
+ if (observation.state === 'unknown')
1240
+ throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
1241
+ journal.write(observation.state === 'absent' ? 'container-absent' : 'container-removal');
1242
+ if (options.requireAbsent && observation.state !== 'absent') {
1243
+ throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
1244
+ }
1245
+ markSandboxControlRootQuiescing(resolvedRoot);
1246
+ await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
1247
+ const brokerPath = path.join(resolvedRoot, 'broker.json');
1248
+ const statusPath = path.join(resolvedRoot, 'public', 'status.json');
1249
+ const broker = readBrokerOwner(brokerPath);
1250
+ const status = readStatusOwner(statusPath);
1251
+ if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
871
1252
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
872
- const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
873
- && finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
874
- if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
875
- throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
876
1253
  }
877
- if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
878
- throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
1254
+ if (status && status.generation !== manifest.generation) {
1255
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
879
1256
  }
880
- fs.unlinkSync(brokerPath);
881
- }
882
- const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
883
- if (finalStatusRaw !== null) {
884
- const finalStatus = readStatusOwner(statusPath);
885
- if (!finalStatus)
1257
+ if (broker && status && !sameOwner(broker, status.broker)
1258
+ && (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
886
1259
  throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
887
- if (ownerLive(finalStatus.broker, identityProbe)
888
- && !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
889
- throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
890
1260
  }
891
- if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
1261
+ const platform = options.platform ?? process.platform;
1262
+ const owner = broker && ownerLive(broker, identityProbe) ? broker
1263
+ : status && ownerLive(status.broker, identityProbe) ? status.broker : null;
1264
+ const selfOwner = options.selfOwner;
1265
+ const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
1266
+ if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
892
1267
  throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
893
1268
  }
894
- fs.unlinkSync(statusPath);
1269
+ let executions = readExecutions(manifest);
1270
+ if (owner && !selfOwned)
1271
+ signalOwner(owner, platform, false);
1272
+ for (const execution of executions) {
1273
+ terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
1274
+ }
1275
+ const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
1276
+ while (Date.now() < softDeadlineAt) {
1277
+ if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
1278
+ && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
1279
+ break;
1280
+ await new Promise((resolve) => setTimeout(resolve, 25));
1281
+ }
1282
+ if (observation.state === 'found') {
1283
+ if (Date.now() >= deadlineAt) {
1284
+ recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
1285
+ throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
1286
+ }
1287
+ let afterRemoval;
1288
+ try {
1289
+ await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
1290
+ afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
1291
+ }
1292
+ catch (error) {
1293
+ if (error instanceof Error && error.message === 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED') {
1294
+ recordSandboxRemovalPending(resolvedRoot, manifest, 'container-removal', journal);
1295
+ throw new Error('SANDBOX_CONTROL_REMOVE_PENDING');
1296
+ }
1297
+ throw error;
1298
+ }
1299
+ if (afterRemoval.state !== 'absent') {
1300
+ throw new Error(afterRemoval.state === 'unknown'
1301
+ ? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
1302
+ : 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
1303
+ }
1304
+ journal.write('container-absent');
1305
+ }
1306
+ const currentManifestRaw = fs.readFileSync(manifestPath, 'utf8');
1307
+ const currentManifest = readSandboxControlManifest(manifestPath);
1308
+ if (currentManifestRaw !== manifestRawBeforeLock
1309
+ || currentManifest.engine !== manifest.engine
1310
+ || currentManifest.containerIdentity.id !== manifest.containerIdentity.id
1311
+ || currentManifest.authorityEvidence.lockDomain !== manifest.authorityEvidence.lockDomain
1312
+ || currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
1313
+ || !isSandboxControlRootQuiescing(resolvedRoot)) {
1314
+ throw new Error('SANDBOX_CONTROL_MANIFEST_CHANGED');
1315
+ }
1316
+ const currentBroker = readBrokerOwner(brokerPath);
1317
+ if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
1318
+ || currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
1319
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
1320
+ }
1321
+ if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
1322
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
1323
+ }
1324
+ if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
1325
+ throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
1326
+ }
1327
+ if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
1328
+ throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
1329
+ }
1330
+ executions = mergeExecutions(executions, readExecutions(manifest));
1331
+ if (Date.now() >= deadlineAt)
1332
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
1333
+ for (const execution of executions) {
1334
+ if (!terminateSandboxControlExecution(execution, {
1335
+ platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
1336
+ })) {
1337
+ throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
1338
+ }
1339
+ }
1340
+ if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
1341
+ if (remaining() <= 0)
1342
+ throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
1343
+ signalOwner(currentBroker, platform, true);
1344
+ if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
1345
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
1346
+ }
1347
+ }
1348
+ const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
1349
+ if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
1350
+ throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
1351
+ }
1352
+ const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
1353
+ if (finalBrokerRaw !== null) {
1354
+ const finalBroker = readBrokerOwner(brokerPath);
1355
+ if (!finalBroker)
1356
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
1357
+ const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
1358
+ && finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
1359
+ if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
1360
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
1361
+ }
1362
+ if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
1363
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
1364
+ }
1365
+ fs.unlinkSync(brokerPath);
1366
+ }
1367
+ const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
1368
+ if (finalStatusRaw !== null) {
1369
+ const finalStatus = readStatusOwner(statusPath);
1370
+ if (!finalStatus)
1371
+ throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
1372
+ if (ownerLive(finalStatus.broker, identityProbe)
1373
+ && !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
1374
+ throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
1375
+ }
1376
+ if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
1377
+ throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
1378
+ }
1379
+ fs.unlinkSync(statusPath);
1380
+ }
1381
+ if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
1382
+ throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
1383
+ }
1384
+ journal.write('carrier-finalizing');
1385
+ fs.rmSync(resolvedRoot, { recursive: true, force: true });
1386
+ journal.write('carrier-removed');
1387
+ if (!options.retainRemovalJournal)
1388
+ clearSandboxRemovalJournal(manifest);
895
1389
  }
896
- if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
897
- throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
1390
+ finally {
1391
+ if (!options.resourceLock)
1392
+ resourceLock.release();
898
1393
  }
899
- fs.rmSync(resolvedRoot, { recursive: true, force: true });
900
1394
  }
901
1395
  export async function garbageCollectSandboxControlRoot(root, options = {}) {
902
1396
  await removeSandboxControlRoot(path.resolve(root), {