@fitlab-ai/agent-infra 0.9.14 → 0.10.0

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 (354) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +9 -4
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
  8. package/dist/lib/defaults.json +103 -103
  9. package/dist/lib/fs/durable-write.js +39 -0
  10. package/dist/lib/fs/file-lock.js +111 -0
  11. package/dist/lib/host-control/audit.js +19 -0
  12. package/dist/lib/host-control/client.js +142 -0
  13. package/dist/lib/host-control/command.js +49 -0
  14. package/dist/lib/host-control/path.js +160 -0
  15. package/dist/lib/host-control/secure-fs.js +140 -0
  16. package/dist/lib/host-control/server.js +159 -0
  17. package/dist/lib/host-control/service.js +112 -0
  18. package/dist/lib/init.js +11 -2
  19. package/dist/lib/internal/cli-route-inventory.js +4 -2
  20. package/dist/lib/internal/codex-lifecycle.js +6 -6
  21. package/dist/lib/internal/demo-promotion.js +322 -0
  22. package/dist/lib/internal/demo-transcript.js +454 -0
  23. package/dist/lib/internal/host-control.js +28 -0
  24. package/dist/lib/internal/manual-validation.js +331 -0
  25. package/dist/lib/internal/platform-pr.js +18 -4
  26. package/dist/lib/internal/release-workflow.js +63 -84
  27. package/dist/lib/internal/sandbox-control.js +149 -2
  28. package/dist/lib/internal/task-artifact.js +16 -263
  29. package/dist/lib/internal/task-create.js +6 -5
  30. package/dist/lib/internal/task-event.js +37 -37
  31. package/dist/lib/internal/task-lifecycle.js +2 -2
  32. package/dist/lib/internal/task-operation-registry.js +101 -4
  33. package/dist/lib/internal/task-review.js +11 -56
  34. package/dist/lib/internal/task-validate.js +30 -10
  35. package/dist/lib/owned-path.js +33 -0
  36. package/dist/lib/platform/check-status.js +11 -0
  37. package/dist/lib/platform/comment-safety.js +4 -1
  38. package/dist/lib/platform/completion-backfill.js +1 -1
  39. package/dist/lib/platform/github-data.js +241 -0
  40. package/dist/lib/platform/github-provider.js +15 -20
  41. package/dist/lib/platform/issue-comments.js +18 -12
  42. package/dist/lib/platform/issues.js +2 -180
  43. package/dist/lib/platform/pr-change-report.js +5 -4
  44. package/dist/lib/platform/pr-checks.js +12 -65
  45. package/dist/lib/platform/pr-summary.js +117 -33
  46. package/dist/lib/platform/pull-requests.js +5 -108
  47. package/dist/lib/platform/snapshots.js +2 -0
  48. package/dist/lib/platform/task-identities.js +2 -9
  49. package/dist/lib/platform/verification-sync.js +132 -143
  50. package/dist/lib/pr-review/evidence-grading.js +4 -8
  51. package/dist/lib/release/workflow.js +1 -1
  52. package/dist/lib/sandbox/commands/create.js +7 -2
  53. package/dist/lib/sandbox/commands/rm.js +19 -1511
  54. package/dist/lib/sandbox/control/audit.js +203 -0
  55. package/dist/lib/sandbox/control/client.js +93 -54
  56. package/dist/lib/sandbox/control/executor.js +129 -65
  57. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  58. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  59. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  60. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  61. package/dist/lib/sandbox/control/protocol.js +55 -4
  62. package/dist/lib/sandbox/control/server.js +518 -127
  63. package/dist/lib/sandbox/control/state.js +64 -0
  64. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  65. package/dist/lib/sandbox/control/workflow-executor.js +89 -0
  66. package/dist/lib/sandbox/index.js +1 -2
  67. package/dist/lib/sandbox/recovery.js +28 -4
  68. package/dist/lib/sandbox/removal.js +822 -0
  69. package/dist/lib/sandbox/task-cleanup.js +217 -0
  70. package/dist/lib/sandbox/workspace-identity.js +17 -1
  71. package/dist/lib/sandbox/workspace-view.js +13 -14
  72. package/dist/lib/task/activity-intent.js +2 -1
  73. package/dist/lib/task/activity-log.js +30 -24
  74. package/dist/lib/task/artifact-command.js +79 -0
  75. package/dist/lib/task/artifact-lifecycle.js +30 -28
  76. package/dist/lib/task/artifact-name.js +36 -0
  77. package/dist/lib/task/artifact-operations.js +29 -289
  78. package/dist/lib/task/artifact-receipts.js +3 -15
  79. package/dist/lib/task/artifact-recovery.js +430 -0
  80. package/dist/lib/task/artifact-repair-intent.js +134 -29
  81. package/dist/lib/task/artifact-schema.js +6 -14
  82. package/dist/lib/task/capabilities.js +12 -19
  83. package/dist/lib/task/codex-orchestration.js +71 -25
  84. package/dist/lib/task/commit-identity.js +28 -0
  85. package/dist/lib/task/commit-operation.js +33 -20
  86. package/dist/lib/task/control-authority.js +403 -6
  87. package/dist/lib/task/control-recovery.js +140 -0
  88. package/dist/lib/task/create-service.js +1 -8
  89. package/dist/lib/task/create.js +62 -9
  90. package/dist/lib/task/decision-details.js +2 -46
  91. package/dist/lib/task/delegation-receipts.js +64 -3
  92. package/dist/lib/task/events.js +275 -22
  93. package/dist/lib/task/finalization-state.js +9 -0
  94. package/dist/lib/task/human-override.js +8 -14
  95. package/dist/lib/task/implementation-inputs.js +3 -3
  96. package/dist/lib/task/intermediate-cleanup.js +441 -0
  97. package/dist/lib/task/invalidation.js +18 -65
  98. package/dist/lib/task/ledger-intents.js +7 -22
  99. package/dist/lib/task/ledger.js +1 -3
  100. package/dist/lib/task/lifecycle-recovery.js +549 -0
  101. package/dist/lib/task/lifecycle.js +31 -4
  102. package/dist/lib/task/local-artifact-finalization.js +197 -197
  103. package/dist/lib/task/manual-validation-completion.js +26 -0
  104. package/dist/lib/task/manual-validation-evidence.js +130 -0
  105. package/dist/lib/task/manual-validation-receipt.js +105 -0
  106. package/dist/lib/task/manual-validation-shared.js +40 -0
  107. package/dist/lib/task/manual-validation-transaction.js +243 -0
  108. package/dist/lib/task/markdown.js +47 -0
  109. package/dist/lib/task/orchestration.js +66 -19
  110. package/dist/lib/task/pr-delivery-fact.js +12 -56
  111. package/dist/lib/task/qualification-audit.js +20 -34
  112. package/dist/lib/task/recovery-warning.js +19 -0
  113. package/dist/lib/task/review-artifacts.js +2 -18
  114. package/dist/lib/task/review-command.js +26 -0
  115. package/dist/lib/task/review-finalization.js +175 -216
  116. package/dist/lib/task/review-fingerprint.js +2 -2
  117. package/dist/lib/task/sections.js +34 -56
  118. package/dist/lib/task/short-id.js +15 -7
  119. package/dist/lib/task/task-execution-lock.js +1 -1
  120. package/dist/lib/task/verification-engine.js +119 -337
  121. package/dist/lib/task/verification.js +1 -1
  122. package/dist/lib/task/workflow-command.js +16 -0
  123. package/dist/lib/task/workflow-warning-intents.js +0 -16
  124. package/dist/lib/task/workflow-warnings.js +28 -2
  125. package/dist/lib/update.js +2 -0
  126. package/dist/lib/version.js +1 -11
  127. package/dist/lifecycle-build-manifest.json +3 -3
  128. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  129. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +10 -4
  130. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
  131. package/lib/fs/durable-write.ts +30 -0
  132. package/lib/fs/file-lock.ts +113 -0
  133. package/lib/host-control/audit.ts +22 -0
  134. package/lib/host-control/client.ts +172 -0
  135. package/lib/host-control/command.ts +60 -0
  136. package/lib/host-control/path.ts +167 -0
  137. package/lib/host-control/secure-fs.ts +135 -0
  138. package/lib/host-control/server.ts +180 -0
  139. package/lib/host-control/service.ts +133 -0
  140. package/lib/init.ts +15 -2
  141. package/lib/internal/cli-route-inventory.ts +4 -2
  142. package/lib/internal/codex-lifecycle.ts +6 -6
  143. package/lib/internal/demo-promotion.ts +354 -0
  144. package/lib/internal/demo-transcript.ts +419 -0
  145. package/lib/internal/host-control.ts +28 -0
  146. package/lib/internal/manual-validation.ts +315 -0
  147. package/lib/internal/platform-pr.ts +15 -4
  148. package/lib/internal/release-workflow.ts +74 -82
  149. package/lib/internal/sandbox-control.ts +162 -1
  150. package/lib/internal/task-artifact.ts +14 -217
  151. package/lib/internal/task-create.ts +5 -5
  152. package/lib/internal/task-event.ts +20 -11
  153. package/lib/internal/task-lifecycle.ts +4 -3
  154. package/lib/internal/task-operation-registry.ts +111 -4
  155. package/lib/internal/task-review.ts +9 -54
  156. package/lib/internal/task-validate.ts +26 -10
  157. package/lib/owned-path.ts +26 -0
  158. package/lib/platform/check-status.ts +9 -0
  159. package/lib/platform/comment-safety.ts +5 -0
  160. package/lib/platform/completion-backfill.ts +1 -1
  161. package/lib/platform/github-data.ts +322 -0
  162. package/lib/platform/github-provider.ts +15 -20
  163. package/lib/platform/issue-comments.ts +21 -14
  164. package/lib/platform/issues.ts +5 -246
  165. package/lib/platform/pr-change-report.ts +7 -5
  166. package/lib/platform/pr-checks.ts +13 -77
  167. package/lib/platform/pr-summary.ts +123 -35
  168. package/lib/platform/pull-requests.ts +6 -173
  169. package/lib/platform/snapshots.ts +53 -0
  170. package/lib/platform/task-identities.ts +3 -14
  171. package/lib/platform/verification-sync.ts +150 -156
  172. package/lib/pr-review/evidence-grading.ts +4 -9
  173. package/lib/release/workflow.ts +3 -3
  174. package/lib/sandbox/commands/create.ts +7 -6
  175. package/lib/sandbox/commands/rm.ts +21 -1895
  176. package/lib/sandbox/control/audit.ts +253 -0
  177. package/lib/sandbox/control/client.ts +109 -66
  178. package/lib/sandbox/control/executor.ts +134 -74
  179. package/lib/sandbox/control/finalization-response.ts +29 -0
  180. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  181. package/lib/sandbox/control/lifecycle.ts +45 -3
  182. package/lib/sandbox/control/native-file-lock.ts +6 -107
  183. package/lib/sandbox/control/protocol.ts +80 -6
  184. package/lib/sandbox/control/server.ts +567 -132
  185. package/lib/sandbox/control/state.ts +86 -0
  186. package/lib/sandbox/control/task-workflow.ts +95 -0
  187. package/lib/sandbox/control/workflow-executor.ts +92 -0
  188. package/lib/sandbox/index.ts +1 -2
  189. package/lib/sandbox/recovery.ts +28 -5
  190. package/lib/sandbox/removal.ts +1142 -0
  191. package/lib/sandbox/task-cleanup.ts +209 -0
  192. package/lib/sandbox/workspace-identity.ts +19 -3
  193. package/lib/sandbox/workspace-view.ts +17 -15
  194. package/lib/task/activity-intent.ts +2 -2
  195. package/lib/task/activity-log.ts +27 -19
  196. package/lib/task/artifact-command.ts +85 -0
  197. package/lib/task/artifact-lifecycle.ts +31 -26
  198. package/lib/task/artifact-name.ts +55 -0
  199. package/lib/task/artifact-operations.ts +34 -310
  200. package/lib/task/artifact-receipts.ts +3 -14
  201. package/lib/task/artifact-recovery.ts +519 -0
  202. package/lib/task/artifact-repair-intent.ts +162 -33
  203. package/lib/task/artifact-schema.ts +5 -34
  204. package/lib/task/capabilities.ts +13 -22
  205. package/lib/task/codex-orchestration.ts +85 -24
  206. package/lib/task/commit-identity.ts +31 -0
  207. package/lib/task/commit-operation.ts +37 -20
  208. package/lib/task/control-authority.ts +540 -10
  209. package/lib/task/control-recovery.ts +186 -0
  210. package/lib/task/create-service.ts +1 -10
  211. package/lib/task/create.ts +63 -8
  212. package/lib/task/decision-details.ts +3 -71
  213. package/lib/task/delegation-receipts.ts +73 -2
  214. package/lib/task/events.ts +309 -24
  215. package/lib/task/finalization-state.ts +11 -0
  216. package/lib/task/human-override.ts +8 -16
  217. package/lib/task/implementation-inputs.ts +3 -3
  218. package/lib/task/intermediate-cleanup.ts +493 -0
  219. package/lib/task/invalidation.ts +18 -62
  220. package/lib/task/ledger-intents.ts +7 -24
  221. package/lib/task/ledger.ts +1 -3
  222. package/lib/task/lifecycle-recovery.ts +691 -0
  223. package/lib/task/lifecycle.ts +30 -5
  224. package/lib/task/local-artifact-finalization.ts +244 -234
  225. package/lib/task/manual-validation-completion.ts +40 -0
  226. package/lib/task/manual-validation-evidence.ts +209 -0
  227. package/lib/task/manual-validation-receipt.ts +156 -0
  228. package/lib/task/manual-validation-shared.ts +50 -0
  229. package/lib/task/manual-validation-transaction.ts +304 -0
  230. package/lib/task/markdown.ts +70 -0
  231. package/lib/task/orchestration.ts +84 -19
  232. package/lib/task/pr-delivery-fact.ts +15 -62
  233. package/lib/task/qualification-audit.ts +22 -33
  234. package/lib/task/recovery-warning.ts +21 -0
  235. package/lib/task/review-artifacts.ts +1 -20
  236. package/lib/task/review-command.ts +24 -0
  237. package/lib/task/review-finalization.ts +183 -292
  238. package/lib/task/review-fingerprint.ts +2 -2
  239. package/lib/task/sections.ts +33 -54
  240. package/lib/task/short-id.ts +27 -5
  241. package/lib/task/task-execution-lock.ts +1 -1
  242. package/lib/task/verification-engine.ts +113 -383
  243. package/lib/task/verification-types.ts +15 -0
  244. package/lib/task/verification.ts +1 -1
  245. package/lib/task/workflow-command.ts +17 -0
  246. package/lib/task/workflow-warning-intents.ts +0 -13
  247. package/lib/task/workflow-warnings.ts +21 -2
  248. package/lib/update.ts +6 -0
  249. package/lib/version.ts +1 -15
  250. package/package.json +6 -6
  251. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  252. package/templates/.agents/rules/create-issue.en.md +1 -1
  253. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  254. package/templates/.agents/rules/issue-sync.en.md +4 -2
  255. package/templates/.agents/rules/issue-sync.zh-CN.md +4 -3
  256. package/templates/.agents/rules/local-artifact-repair.en.md +46 -39
  257. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +43 -36
  258. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  259. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  260. package/templates/.agents/scripts/package.json +3 -0
  261. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  262. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  263. package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
  264. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
  265. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  266. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  267. package/templates/.agents/skills/block-task/config/verify.json +1 -1
  268. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  269. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  270. package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
  271. package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
  272. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +7 -7
  273. package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
  274. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
  275. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
  276. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
  277. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +33 -21
  278. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +33 -21
  279. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  280. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  281. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  282. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  283. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  284. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  285. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  286. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  287. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  288. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  289. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  290. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  291. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
  292. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
  293. package/templates/.agents/skills/create-pr/config/verify.json +1 -1
  294. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
  295. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
  296. package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
  297. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
  298. package/templates/.agents/skills/create-task/config/verify.json +1 -1
  299. package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
  300. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
  301. package/templates/.agents/skills/import-issue/config/verify.json +2 -2
  302. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  303. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  304. package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
  305. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
  306. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  307. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  308. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  309. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +2 -2
  310. package/templates/.agents/skills/restore-task/config/verify.json +1 -1
  311. package/templates/.agents/skills/review-analysis/SKILL.en.md +2 -2
  312. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  313. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
  314. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
  315. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
  316. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
  317. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  318. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  319. package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
  320. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
  321. package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
  322. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
  323. package/templates/.agents/skills/review-plan/SKILL.en.md +2 -2
  324. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  325. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
  326. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
  327. package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
  328. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
  329. package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
  330. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
  331. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
  332. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
  333. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  334. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  335. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
  336. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
  337. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  338. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  339. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  340. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  341. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  342. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  343. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  344. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  345. package/templates/.agents/templates/task.en.md +1 -1
  346. package/templates/.agents/templates/task.zh-CN.md +1 -1
  347. package/templates/.claude/commands/import-issue.en.md +1 -1
  348. package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
  349. package/templates/.claude/commands/restore-task.en.md +1 -1
  350. package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
  351. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  352. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  353. package/dist/lib/platform/adapters.js +0 -56
  354. package/lib/platform/adapters.ts +0 -197
@@ -43,11 +43,23 @@ type ActiveCodexLifecycleEvidenceQuery = Readonly<{
43
43
  const LOCK_RETRY_MS = 10;
44
44
  const LOCK_TIMEOUT_MS = 1_000;
45
45
  const LOCK_STALE_MS = 30_000;
46
+ const RECOVERY_CONSUMER_RE = /^lifecycle-recovery:([^:\r\n]+):([^:\r\n]+)$/u;
46
47
 
47
48
  function digest(value: string): string {
48
49
  return crypto.createHash('sha256').update(value).digest('hex');
49
50
  }
50
51
 
52
+ function isRecoveryConsumer(value: string): boolean {
53
+ return RECOVERY_CONSUMER_RE.test(value);
54
+ }
55
+
56
+ function recoveryConsumer(taskId: string, receiptId: string): string {
57
+ if (!/^[^:\r\n]+$/u.test(taskId) || !/^[^:\r\n]+$/u.test(receiptId)) {
58
+ throw new Error('Codex lifecycle recovery claim identity is invalid');
59
+ }
60
+ return `lifecycle-recovery:${taskId}:${receiptId}`;
61
+ }
62
+
51
63
  function readRecord(file: string): StoredCodexLifecycle {
52
64
  const value = JSON.parse(fs.readFileSync(file, 'utf8')) as StoredCodexLifecycle;
53
65
  if (
@@ -249,11 +261,12 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
249
261
 
250
262
  function read(childThreadId: string): StoredCodexLifecycle {
251
263
  const matches = findByChild(childThreadId);
252
- if (matches.length !== 1) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
264
+ if (matches.length === 0) throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
265
+ if (matches.length > 1) throw new Error(`Codex lifecycle child '${childThreadId}' is ambiguous`);
253
266
  return readRecord(matches[0]!);
254
267
  }
255
268
 
256
- function consume(
269
+ function consumeInternal(
257
270
  childThreadId: string,
258
271
  consumer: string,
259
272
  expectedHookDefinitionHash?: string
@@ -291,12 +304,51 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
291
304
  });
292
305
  }
293
306
 
307
+ function consume(
308
+ childThreadId: string,
309
+ consumer: string,
310
+ expectedHookDefinitionHash?: string
311
+ ): StoredCodexLifecycle {
312
+ if (!consumer.trim()) throw new Error('Codex lifecycle consumer is required');
313
+ if (consumer.startsWith('lifecycle-recovery:')) {
314
+ throw new Error('Codex lifecycle recovery claims require claimRecovery');
315
+ }
316
+ return consumeInternal(childThreadId, consumer, expectedHookDefinitionHash);
317
+ }
318
+
319
+ function claimRecovery(
320
+ childThreadId: string,
321
+ taskId: string,
322
+ receiptId: string,
323
+ expectedHookDefinitionHash?: string
324
+ ): StoredCodexLifecycle {
325
+ return consumeInternal(
326
+ childThreadId,
327
+ recoveryConsumer(taskId, receiptId),
328
+ expectedHookDefinitionHash
329
+ );
330
+ }
331
+
332
+ function releaseRecovery(childThreadId: string, consumer: string): boolean {
333
+ if (!/^lifecycle-recovery:[^:\r\n]+:[^:\r\n]+$/.test(consumer)) return false;
334
+ return withWriteLock(() => {
335
+ const matches = findByChild(childThreadId);
336
+ if (matches.length !== 1) return false;
337
+ const file = matches[0]!;
338
+ const current = readRecord(file);
339
+ if (current.state.status !== 'stop-ready' || current.consumer !== consumer || !current.consumedAt) return false;
340
+ fs.unlinkSync(file);
341
+ return true;
342
+ });
343
+ }
344
+
294
345
  function expireBefore(cutoff: string): number {
295
346
  return withWriteLock(() => {
296
347
  let changed = 0;
297
348
  for (const file of recordFiles(root)) {
298
349
  const current = readRecord(file);
299
350
  if (current.updatedAt >= cutoff) continue;
351
+ if (current.consumer && isRecoveryConsumer(current.consumer)) continue;
300
352
  if (current.consumer || ['invalid', 'expired', 'stop-ready'].includes(current.state.status)) {
301
353
  fs.unlinkSync(file);
302
354
  changed += 1;
@@ -316,7 +368,7 @@ function createCodexLifecycleStore(options: CodexLifecycleStoreOptions) {
316
368
  });
317
369
  }
318
370
 
319
- return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
371
+ return Object.freeze({ root, apply, applyToSpawn, consume, claimRecovery, releaseRecovery, expireBefore, findByParent, read });
320
372
  }
321
373
 
322
374
  export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ export function fsyncDirectory(directory: string): void {
6
+ if (process.platform === 'win32') return;
7
+ const descriptor = fs.openSync(directory, 'r');
8
+ try { fs.fsyncSync(descriptor); } finally { fs.closeSync(descriptor); }
9
+ }
10
+
11
+ /** The caller owns directory validation and the meaning of an existing target. */
12
+ export function writeDurableFile(
13
+ target: string,
14
+ content: string,
15
+ options: Readonly<{ mode: number; replace: boolean }>
16
+ ): void {
17
+ const directory = path.dirname(target);
18
+ const temporary = path.join(directory, `.${path.basename(target)}.${process.pid}.${randomUUID()}.tmp`);
19
+ try {
20
+ const descriptor = fs.openSync(temporary, 'wx', options.mode);
21
+ try {
22
+ fs.writeFileSync(descriptor, content, { encoding: 'utf8' });
23
+ fs.fchmodSync(descriptor, options.mode);
24
+ fs.fsyncSync(descriptor);
25
+ } finally { fs.closeSync(descriptor); }
26
+ if (options.replace) fs.renameSync(temporary, target);
27
+ else fs.linkSync(temporary, target);
28
+ fsyncDirectory(directory);
29
+ } finally { fs.rmSync(temporary, { force: true }); }
30
+ }
@@ -0,0 +1,113 @@
1
+ import { createRequire } from 'node:module';
2
+ import fs from 'node:fs';
3
+
4
+ type NativeFsExtensions = Readonly<{
5
+ tryLock: (fd: number) => boolean;
6
+ unlock: (fd: number) => void;
7
+ }>;
8
+
9
+ export type FileLockCapability = Readonly<{
10
+ supported: boolean;
11
+ primitive: 'flock' | 'F_OFD_SETLK' | 'LockFileEx' | 'unavailable';
12
+ reason?: string;
13
+ }>;
14
+
15
+ const require = createRequire(import.meta.url);
16
+ export const NATIVE_LOCK_PRIMITIVE = process.platform === 'win32' ? 'LockFileEx'
17
+ : process.platform === 'linux' ? 'F_OFD_SETLK' : 'flock';
18
+
19
+ function loadNative(): NativeFsExtensions {
20
+ try {
21
+ return require('fs-native-extensions') as NativeFsExtensions;
22
+ } catch (error) {
23
+ throw new Error(
24
+ `FILE_LOCK_UNSUPPORTED: native lock module is unavailable for Node ${process.version} (${process.platform}-${process.arch}). `
25
+ + 'Verify that fs-native-extensions includes a prebuilt binary for this platform and reinstall @fitlab-ai/agent-infra. '
26
+ + `Cause: ${error instanceof Error ? error.message : String(error)}`
27
+ );
28
+ }
29
+ }
30
+
31
+ export function probeNativeLockCapability(): FileLockCapability {
32
+ try {
33
+ loadNative();
34
+ return {
35
+ supported: true,
36
+ primitive: NATIVE_LOCK_PRIMITIVE
37
+ };
38
+ } catch (error) {
39
+ return {
40
+ supported: false,
41
+ primitive: 'unavailable',
42
+ reason: error instanceof Error ? error.message : String(error)
43
+ };
44
+ }
45
+ }
46
+
47
+ /** Hold a stable lock file; callers own its namespace and must never unlink it. */
48
+ export function acquireFileLock(filePath: string): Readonly<{ release(): void }> {
49
+ const native = loadNative();
50
+ if (fs.existsSync(filePath)) {
51
+ const existing = fs.lstatSync(filePath);
52
+ if (existing.isSymbolicLink() || !existing.isFile()) {
53
+ throw new Error('FILE_LOCK_UNSUPPORTED: lock object is not a regular file');
54
+ }
55
+ if (process.platform !== 'win32' && typeof process.getuid === 'function'
56
+ && existing.uid !== process.getuid()) {
57
+ throw new Error('FILE_LOCK_UNSUPPORTED: lock object owner mismatch');
58
+ }
59
+ }
60
+ let fd: number | undefined;
61
+ try {
62
+ const noFollow = process.platform !== 'win32' && typeof fs.constants.O_NOFOLLOW === 'number'
63
+ ? fs.constants.O_NOFOLLOW : 0;
64
+ fd = fs.openSync(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR | noFollow, 0o600);
65
+ const opened = fs.fstatSync(fd);
66
+ if (!opened.isFile()) throw new Error('lock object must be a regular file');
67
+ if (process.platform !== 'win32') {
68
+ if (typeof process.getuid === 'function' && opened.uid !== process.getuid()) {
69
+ throw new Error('lock object owner mismatch');
70
+ }
71
+ fs.fchmodSync(fd, 0o600);
72
+ }
73
+ if (!native.tryLock(fd)) {
74
+ fs.closeSync(fd);
75
+ fd = undefined;
76
+ } else {
77
+ const current = fs.lstatSync(filePath);
78
+ if (!current.isFile() || current.dev !== opened.dev || current.ino !== opened.ino) {
79
+ throw new Error('lock object changed during acquisition');
80
+ }
81
+ }
82
+ } catch (error) {
83
+ try {
84
+ if (fd !== undefined) fs.closeSync(fd);
85
+ } catch {
86
+ // Preserve the lock acquisition error.
87
+ }
88
+ throw new Error(`FILE_LOCK_UNSUPPORTED: ${error instanceof Error ? error.message : String(error)}`);
89
+ }
90
+ if (fd === undefined) throw new Error('FILE_LOCK_BUSY');
91
+
92
+ let released = false;
93
+ return {
94
+ release() {
95
+ if (released) return;
96
+ released = true;
97
+ let failure: unknown = null;
98
+ try {
99
+ if (fd === undefined) throw new Error('descriptor is missing');
100
+ native.unlock(fd);
101
+ } catch (error) {
102
+ failure = error;
103
+ } finally {
104
+ try {
105
+ if (fd !== undefined) fs.closeSync(fd);
106
+ } catch (error) {
107
+ failure ??= error;
108
+ }
109
+ }
110
+ if (failure) throw new Error(`FILE_LOCK_RELEASE_FAILED: ${failure instanceof Error ? failure.message : String(failure)}`);
111
+ }
112
+ };
113
+ }
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { HostControlAudit } from './server.ts';
5
+
6
+ export const HOST_CONTROL_AUDIT_FILE = 'audit.ndjson';
7
+
8
+ export function hostControlAuditPath(endpoint: string): string {
9
+ return path.join(path.dirname(endpoint), HOST_CONTROL_AUDIT_FILE);
10
+ }
11
+
12
+ export function appendHostControlAudit(endpoint: string, entry: HostControlAudit, at = Date.now()): void {
13
+ const filePath = hostControlAuditPath(endpoint);
14
+ const descriptor = fs.openSync(filePath, 'a', 0o600);
15
+ try {
16
+ fs.fchmodSync(descriptor, 0o600);
17
+ fs.writeFileSync(descriptor, `${JSON.stringify({ version: 1, at, ...entry })}\n`, 'utf8');
18
+ fs.fsyncSync(descriptor);
19
+ } finally {
20
+ fs.closeSync(descriptor);
21
+ }
22
+ }
@@ -0,0 +1,172 @@
1
+ import net from 'node:net';
2
+ import { randomUUID } from 'node:crypto';
3
+
4
+ import {
5
+ inspectHostControlEndpoint,
6
+ resolveHostControlEndpoint
7
+ } from './path.ts';
8
+
9
+ export const HOST_CONTROL_MAX_REQUEST_BYTES = 64 * 1024;
10
+
11
+ export const HOST_CONTROL_COMMANDS = Object.freeze([
12
+ 'task-lifecycle', 'task-orchestration', 'task-finalization',
13
+ 'task-artifact', 'task-review', 'task-event', 'task-ledger',
14
+ 'task-invalidation', 'task-warning'
15
+ ] as const);
16
+ export type HostControlCommand = typeof HOST_CONTROL_COMMANDS[number];
17
+
18
+ export type HostControlCommandPayload = Readonly<{
19
+ workingDirectory: string;
20
+ args: readonly string[];
21
+ environment?: Readonly<Record<string, string>>;
22
+ }>;
23
+
24
+ export const HOST_CONTROL_TEST_ENVIRONMENT_KEYS = Object.freeze([
25
+ 'TZ', 'AGENT_INFRA_GH_BIN', 'AGENT_INFRA_GH_ARGS_JSON', 'GH_FAKE_PR_PATH', 'GH_FAKE_ARGS_PATH'
26
+ ] as const);
27
+
28
+ export type HostControlRequest = Readonly<{
29
+ version: 1;
30
+ id: string;
31
+ taskId: string | null;
32
+ generation: string | null;
33
+ operation: HostControlCommand;
34
+ scope: 'host-command';
35
+ payload: HostControlCommandPayload;
36
+ }>;
37
+
38
+ export type HostControlResponse = Readonly<{
39
+ version: 1;
40
+ id: string;
41
+ status: 'completed' | 'rejected' | 'unknown';
42
+ exitCode: number;
43
+ stdout: string;
44
+ stderr: string;
45
+ error: Readonly<{ code: string; message: string }> | null;
46
+ }>;
47
+
48
+ export class HostControlClientError extends Error {
49
+ readonly code: string;
50
+ readonly retryable: boolean;
51
+ readonly changed: false | null;
52
+ constructor(code: string, message: string, retryable = false, submitted = false) {
53
+ super(`${code}: ${message}`);
54
+ this.name = 'HostControlClientError';
55
+ this.code = code;
56
+ this.retryable = retryable && !submitted;
57
+ this.changed = submitted ? null : false;
58
+ }
59
+ }
60
+
61
+ function reject(message: string): never {
62
+ throw new HostControlClientError('HOST_CONTROL_REQUEST_INVALID', message);
63
+ }
64
+
65
+ export function validateHostControlRequest(value: unknown): HostControlRequest {
66
+ if (!value || typeof value !== 'object' || Array.isArray(value)) reject('request must be an object');
67
+ const request = value as HostControlRequest;
68
+ if (Object.keys(request).sort().join(',') !== 'generation,id,operation,payload,scope,taskId,version'
69
+ || request.version !== 1 || typeof request.id !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(request.id)
70
+ || (request.taskId !== null && (typeof request.taskId !== 'string' || !/^TASK-\d{8}-\d{6}$/u.test(request.taskId)))
71
+ || (request.generation !== null && (typeof request.generation !== 'string' || !request.generation))
72
+ || request.scope !== 'host-command' || !HOST_CONTROL_COMMANDS.includes(request.operation as HostControlCommand)) reject('request schema is invalid');
73
+ const payload = request.payload;
74
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)
75
+ || !['args,environment,workingDirectory', 'args,workingDirectory'].includes(Object.keys(payload).sort().join(','))
76
+ || !Array.isArray(payload.args) || payload.args.length > 128
77
+ || !payload.args.every((arg) => typeof arg === 'string' && !/[\r\n]/u.test(arg))
78
+ || typeof payload.workingDirectory !== 'string' || !payload.workingDirectory.startsWith('/')) reject('host-command payload is invalid');
79
+ if (payload.environment !== undefined) {
80
+ if (!payload.environment || typeof payload.environment !== 'object' || Array.isArray(payload.environment)) reject('host-command environment is invalid');
81
+ for (const [key, value] of Object.entries(payload.environment)) {
82
+ if (!HOST_CONTROL_TEST_ENVIRONMENT_KEYS.includes(key as typeof HOST_CONTROL_TEST_ENVIRONMENT_KEYS[number])
83
+ || typeof value !== 'string' || /[\r\n]/u.test(value)) reject('host-command environment is invalid');
84
+ }
85
+ }
86
+ return request;
87
+ }
88
+
89
+ export function hostControlRequestForCommand(
90
+ command: HostControlCommand,
91
+ args: readonly string[],
92
+ workingDirectory: string,
93
+ environment: Readonly<Record<string, string>> = {}
94
+ ): HostControlRequest {
95
+ if (!HOST_CONTROL_COMMANDS.includes(command)) reject('host-command is unsupported');
96
+ const taskArg = args[0];
97
+ const taskId = taskArg && /^TASK-\d{8}-\d{6}$/u.test(taskArg) ? taskArg : null;
98
+ return validateHostControlRequest({
99
+ version: 1,
100
+ id: randomUUID(),
101
+ taskId,
102
+ generation: null,
103
+ operation: command,
104
+ scope: 'host-command',
105
+ payload: {
106
+ workingDirectory,
107
+ args: [...args],
108
+ ...(Object.keys(environment).length > 0 ? { environment } : {})
109
+ }
110
+ });
111
+ }
112
+
113
+ function parseResponse(value: unknown, id: string): HostControlResponse {
114
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new HostControlClientError('HOST_CONTROL_RESPONSE_INVALID', 'response must be an object');
115
+ const response = value as Record<string, unknown>;
116
+ if (Object.keys(response).sort().join(',') !== 'error,exitCode,id,status,stderr,stdout,version'
117
+ || response.version !== 1 || response.id !== id
118
+ || !['completed', 'rejected', 'unknown'].includes(response.status as string)
119
+ || !Number.isSafeInteger(response.exitCode)
120
+ || typeof response.stdout !== 'string' || typeof response.stderr !== 'string'
121
+ || (response.error !== null && (!response.error || typeof response.error !== 'object'))) {
122
+ throw new HostControlClientError('HOST_CONTROL_RESPONSE_INVALID', 'response schema is invalid');
123
+ }
124
+ return response as HostControlResponse;
125
+ }
126
+
127
+ export async function requestHostControl(params: Readonly<{
128
+ request: HostControlRequest;
129
+ endpoint?: string;
130
+ timeoutMs?: number;
131
+ }>): Promise<HostControlResponse> {
132
+ const request = validateHostControlRequest(params.request);
133
+ const endpoint = params.endpoint ?? resolveHostControlEndpoint();
134
+ const inspection = inspectHostControlEndpoint(endpoint, { uid: process.getuid?.() });
135
+ if (!inspection.ok) throw new HostControlClientError(inspection.code ?? 'HOST_CONTROL_UNAVAILABLE', 'fixed host-control endpoint is unavailable', true);
136
+ const encoded = `${JSON.stringify(request)}\n`;
137
+ if (Buffer.byteLength(encoded, 'utf8') > HOST_CONTROL_MAX_REQUEST_BYTES) throw new HostControlClientError('HOST_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
138
+ const timeoutMs = params.timeoutMs ?? 30_000;
139
+ return await new Promise<HostControlResponse>((resolve, rejectPromise) => {
140
+ const socket = net.createConnection(endpoint);
141
+ let output = '';
142
+ let settled = false;
143
+ let submitted = false;
144
+ const failure = (code: string, message: string, retryable = false) =>
145
+ new HostControlClientError(code, message, retryable, submitted);
146
+ const finish = (error?: Error, response?: HostControlResponse): void => {
147
+ if (settled) return;
148
+ settled = true;
149
+ clearTimeout(timer);
150
+ socket.destroy();
151
+ if (error) rejectPromise(error); else resolve(response!);
152
+ };
153
+ const timer = setTimeout(() => finish(failure('HOST_CONTROL_TIMEOUT', 'host-control did not respond; inspect task state before retrying', true)), timeoutMs);
154
+ socket.setEncoding('utf8');
155
+ socket.on('connect', () => { submitted = true; socket.end(encoded); });
156
+ socket.on('data', (chunk: string) => {
157
+ output += chunk;
158
+ if (Buffer.byteLength(output, 'utf8') > HOST_CONTROL_MAX_REQUEST_BYTES) {
159
+ finish(failure('HOST_CONTROL_RESPONSE_TOO_LARGE', 'host-control response exceeds the control limit'));
160
+ return;
161
+ }
162
+ const newline = output.indexOf('\n');
163
+ if (newline < 0) return;
164
+ try { finish(undefined, parseResponse(JSON.parse(output.slice(0, newline)), request.id)); }
165
+ catch (error) { finish(failure('HOST_CONTROL_RESPONSE_INVALID', String(error))); }
166
+ });
167
+ socket.on('error', (error) => finish(failure('HOST_CONTROL_UNAVAILABLE', error.message, true)));
168
+ socket.on('close', () => {
169
+ if (!settled) finish(failure('HOST_CONTROL_RESPONSE_INVALID', 'host-control closed without a response'));
170
+ });
171
+ });
172
+ }
@@ -0,0 +1,60 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import type { HostControlRequest } from './client.ts';
6
+ import { readHostControlWorkerToken, resolveHostControlEndpoint } from './path.ts';
7
+
8
+ type HostCommandResult = Readonly<{
9
+ stdout: string;
10
+ stderr: string;
11
+ exitCode: number;
12
+ }>;
13
+
14
+ export async function dispatchHostControlCommand(
15
+ request: Pick<HostControlRequest, 'operation' | 'payload'>
16
+ ): Promise<HostCommandResult> {
17
+ const { payload } = request;
18
+ const compiledEntry = fileURLToPath(new URL('../../bin/internal-cli.js', import.meta.url));
19
+ const sourceEntry = fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url));
20
+ const entry = fs.existsSync(compiledEntry) ? compiledEntry : sourceEntry;
21
+ const payloadEnvironment = payload.environment ?? {};
22
+ const env = { ...process.env, ...payloadEnvironment };
23
+ for (const key of Object.keys(env)) {
24
+ if (key.startsWith('AGENT_INFRA_CONTROL_')
25
+ || key === 'AGENT_INFRA_TASK_ID'
26
+ || key === 'AGENT_INFRA_RUNTIME_DIR'
27
+ || key === 'AGENT_INFRA_EXECUTOR_MANIFEST'
28
+ || key === 'AGENT_INFRA_HOST_CONTROL_SOCKET') {
29
+ delete env[key];
30
+ }
31
+ }
32
+ delete env.NODE_OPTIONS;
33
+ delete env.NODE_PATH;
34
+ delete env.AGENT_INFRA_HOST_CONTROL_WORKER;
35
+ delete env.AGENT_INFRA_HOST_CONTROL_WORKER_TOKEN;
36
+ env.AGENT_INFRA_HOST_CONTROL_WORKER = '1';
37
+ env.AGENT_INFRA_HOST_CONTROL_WORKER_TOKEN = readHostControlWorkerToken(
38
+ process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT ?? resolveHostControlEndpoint()
39
+ );
40
+
41
+ return await new Promise<HostCommandResult>((resolve, reject) => {
42
+ const runtimeArgs = entry.endsWith('.ts') ? ['--experimental-strip-types', '--no-warnings', entry] : [entry];
43
+ const child = spawn(process.execPath, [...runtimeArgs, request.operation, ...payload.args], {
44
+ cwd: payload.workingDirectory,
45
+ env,
46
+ stdio: ['ignore', 'pipe', 'pipe']
47
+ });
48
+ let stdout = '';
49
+ let stderr = '';
50
+ child.stdout.setEncoding('utf8');
51
+ child.stderr.setEncoding('utf8');
52
+ child.stdout.on('data', (chunk: string) => { stdout += chunk; });
53
+ child.stderr.on('data', (chunk: string) => { stderr += chunk; });
54
+ child.once('error', reject);
55
+ child.once('close', (code, signal) => {
56
+ if (signal) reject(new Error(`HOST_CONTROL_WORKER_TERMINATED: ${signal}`));
57
+ else resolve({ stdout, stderr, exitCode: code ?? 1 });
58
+ });
59
+ });
60
+ }
@@ -0,0 +1,167 @@
1
+ import fs from 'node:fs';
2
+ import { randomBytes } from 'node:crypto';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ export const HOST_CONTROL_DIRECTORY_MODE = 0o700;
7
+ export const HOST_CONTROL_SOCKET_MODE = 0o600;
8
+ export const HOST_CONTROL_WORKER_TOKEN_MODE = 0o600;
9
+
10
+ export type HostControlPlatform = 'linux' | 'darwin' | 'unsupported';
11
+
12
+ export type HostControlEndpointOptions = Readonly<{
13
+ platform?: HostControlPlatform;
14
+ uid?: number;
15
+ username?: string;
16
+ }>;
17
+
18
+ function currentPlatform(): HostControlPlatform {
19
+ if (process.platform === 'linux' || process.platform === 'darwin') return process.platform;
20
+ return 'unsupported';
21
+ }
22
+
23
+ function validUid(uid: number | undefined): number {
24
+ const resolved = uid ?? process.getuid?.();
25
+ if (!Number.isInteger(resolved) || resolved === undefined || resolved < 0) {
26
+ throw new Error('HOST_CONTROL_ENDPOINT_INVALID: uid is unavailable');
27
+ }
28
+ return resolved;
29
+ }
30
+
31
+ function validUsername(username: string | undefined): string {
32
+ const resolved = username ?? os.userInfo({ encoding: 'utf8' }).username;
33
+ if (!/^[A-Za-z0-9._-]+$/u.test(resolved)) {
34
+ throw new Error('HOST_CONTROL_ENDPOINT_INVALID: username is invalid');
35
+ }
36
+ return resolved;
37
+ }
38
+
39
+ export function resolveHostControlEndpoint(options: HostControlEndpointOptions = {}): string {
40
+ const platform = options.platform ?? currentPlatform();
41
+ if (platform === 'linux') return `/run/user/${validUid(options.uid)}/agent-infra/host-control.sock`;
42
+ if (platform === 'darwin') return path.posix.join(
43
+ '/Users',
44
+ validUsername(options.username),
45
+ 'Library',
46
+ 'Application Support',
47
+ 'agent-infra',
48
+ 'run',
49
+ 'host-control.sock'
50
+ );
51
+ throw new Error('HOST_CONTROL_UNSUPPORTED_PLATFORM');
52
+ }
53
+
54
+ export function hostControlWorkerTokenPath(endpoint: string): string {
55
+ return `${endpoint}.worker-token`;
56
+ }
57
+
58
+ function assertWorkerTokenFile(tokenPath: string, uid = process.getuid?.()): string {
59
+ const stat = fs.lstatSync(tokenPath);
60
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error('HOST_CONTROL_WORKER_TOKEN_INVALID');
61
+ if ((stat.mode & 0o777) !== HOST_CONTROL_WORKER_TOKEN_MODE) throw new Error('HOST_CONTROL_WORKER_TOKEN_MODE_INVALID');
62
+ if (uid !== undefined && stat.uid !== uid) throw new Error('HOST_CONTROL_WORKER_TOKEN_OWNER_INVALID');
63
+ const token = fs.readFileSync(tokenPath, 'utf8').trim();
64
+ if (!/^[a-f0-9]{64}$/u.test(token)) throw new Error('HOST_CONTROL_WORKER_TOKEN_INVALID');
65
+ return token;
66
+ }
67
+
68
+ export function ensureHostControlWorkerToken(endpoint: string, uid = process.getuid?.()): string {
69
+ const tokenPath = hostControlWorkerTokenPath(endpoint);
70
+ try {
71
+ return assertWorkerTokenFile(tokenPath, uid);
72
+ } catch (error) {
73
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
74
+ }
75
+ const token = randomBytes(32).toString('hex');
76
+ try {
77
+ const fd = fs.openSync(tokenPath, 'wx', HOST_CONTROL_WORKER_TOKEN_MODE);
78
+ try {
79
+ fs.writeFileSync(fd, `${token}\n`, 'utf8');
80
+ fs.fchmodSync(fd, HOST_CONTROL_WORKER_TOKEN_MODE);
81
+ if (uid !== undefined && process.getuid?.() === uid) fs.fchownSync(fd, uid, process.getgid?.() ?? -1);
82
+ } finally {
83
+ fs.closeSync(fd);
84
+ }
85
+ } catch (error) {
86
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
87
+ }
88
+ return assertWorkerTokenFile(tokenPath, uid);
89
+ }
90
+
91
+ export function readHostControlWorkerToken(endpoint: string, uid = process.getuid?.()): string {
92
+ return assertWorkerTokenFile(hostControlWorkerTokenPath(endpoint), uid);
93
+ }
94
+
95
+ export function removeHostControlWorkerToken(endpoint: string): void {
96
+ try {
97
+ fs.unlinkSync(hostControlWorkerTokenPath(endpoint));
98
+ } catch (error) {
99
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
100
+ }
101
+ }
102
+
103
+ export type HostControlEndpointInspection = Readonly<{
104
+ ok: boolean;
105
+ code: string | null;
106
+ ownerUid?: number;
107
+ mode?: number;
108
+ parentMode?: number;
109
+ }>;
110
+
111
+ export function inspectHostControlEndpoint(
112
+ endpoint: string,
113
+ expected: Readonly<{ uid?: number }> = {}
114
+ ): HostControlEndpointInspection {
115
+ let endpointStat: fs.Stats;
116
+ let parentStat: fs.Stats;
117
+ try {
118
+ endpointStat = fs.lstatSync(endpoint);
119
+ parentStat = fs.lstatSync(path.dirname(endpoint));
120
+ } catch {
121
+ return { ok: false, code: 'HOST_CONTROL_ENDPOINT_MISSING' };
122
+ }
123
+ if (endpointStat.isSymbolicLink()) return { ok: false, code: 'HOST_CONTROL_ENDPOINT_SYMLINK' };
124
+ if (!endpointStat.isSocket()) return { ok: false, code: 'HOST_CONTROL_ENDPOINT_NOT_SOCKET' };
125
+ if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) {
126
+ return { ok: false, code: 'HOST_CONTROL_PARENT_INVALID' };
127
+ }
128
+ let ancestor = path.dirname(endpoint);
129
+ while (true) {
130
+ let stat: fs.Stats;
131
+ try { stat = fs.lstatSync(ancestor); } catch { return { ok: false, code: 'HOST_CONTROL_PARENT_INVALID' }; }
132
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return { ok: false, code: 'HOST_CONTROL_PARENT_INVALID' };
133
+ const next = path.dirname(ancestor);
134
+ if (next === ancestor) break;
135
+ ancestor = next;
136
+ }
137
+ const mode = endpointStat.mode & 0o777;
138
+ const parentMode = parentStat.mode & 0o777;
139
+ if (mode !== HOST_CONTROL_SOCKET_MODE) return { ok: false, code: 'HOST_CONTROL_ENDPOINT_MODE_INVALID', ownerUid: endpointStat.uid, mode, parentMode };
140
+ if (parentMode !== HOST_CONTROL_DIRECTORY_MODE) return { ok: false, code: 'HOST_CONTROL_PARENT_MODE_INVALID', ownerUid: endpointStat.uid, mode, parentMode };
141
+ if (expected.uid !== undefined && endpointStat.uid !== expected.uid) {
142
+ return { ok: false, code: 'HOST_CONTROL_ENDPOINT_OWNER_INVALID', ownerUid: endpointStat.uid, mode, parentMode };
143
+ }
144
+ if (expected.uid !== undefined && parentStat.uid !== expected.uid) {
145
+ return { ok: false, code: 'HOST_CONTROL_PARENT_OWNER_INVALID', ownerUid: parentStat.uid, mode, parentMode };
146
+ }
147
+ return { ok: true, code: null, ownerUid: endpointStat.uid, mode, parentMode };
148
+ }
149
+
150
+ export function prepareHostControlDirectory(endpoint: string, uid = process.getuid?.()): string {
151
+ const directory = path.dirname(endpoint);
152
+ const segments = path.resolve(directory).split(path.sep);
153
+ let current = path.isAbsolute(directory) ? path.sep : '';
154
+ for (const segment of segments.filter(Boolean)) {
155
+ current = path.join(current, segment);
156
+ try {
157
+ const stat = fs.lstatSync(current);
158
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('HOST_CONTROL_PARENT_INVALID');
159
+ } catch (error) {
160
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
161
+ }
162
+ }
163
+ fs.mkdirSync(directory, { recursive: true, mode: HOST_CONTROL_DIRECTORY_MODE });
164
+ fs.chmodSync(directory, HOST_CONTROL_DIRECTORY_MODE);
165
+ if (uid !== undefined && process.getuid?.() === uid) fs.chownSync(directory, uid, process.getgid?.() ?? -1);
166
+ return directory;
167
+ }