@fitlab-ai/agent-infra 0.9.0 → 0.9.2

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 (327) hide show
  1. package/README.md +22 -5
  2. package/README.zh-CN.md +21 -5
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +20 -0
  6. package/dist/lib/agent-client.js +163 -0
  7. package/dist/lib/agent-clients/adapter.js +294 -0
  8. package/dist/lib/agent-clients/adapters/claude-code.js +62 -0
  9. package/dist/lib/agent-clients/adapters/codex-sandbox.js +210 -0
  10. package/dist/lib/agent-clients/adapters/codex.js +54 -0
  11. package/dist/lib/agent-clients/adapters/gemini-cli.js +64 -0
  12. package/dist/lib/agent-clients/adapters/index.js +20 -0
  13. package/dist/lib/agent-clients/adapters/opencode.js +64 -0
  14. package/dist/lib/agent-clients/config.js +177 -0
  15. package/dist/lib/agent-clients/custom-tuis.js +99 -0
  16. package/dist/lib/agent-clients/invocation.js +9 -0
  17. package/dist/lib/agent-clients/next-steps.js +38 -0
  18. package/dist/lib/agent-clients/project-assets.js +45 -0
  19. package/dist/lib/agent-clients/reconcile.js +235 -0
  20. package/dist/lib/agent-clients/registry.js +72 -0
  21. package/dist/lib/agent-clients/schema.js +57 -0
  22. package/dist/lib/agent-clients/types.js +29 -0
  23. package/dist/lib/builtin-tuis.js +9 -14
  24. package/dist/lib/defaults.json +85 -73
  25. package/dist/lib/git/review-snapshot.js +25 -10
  26. package/dist/lib/git/reviewed-commit-equivalence.js +110 -0
  27. package/dist/lib/init.js +47 -91
  28. package/dist/lib/internal/agent-client.js +117 -0
  29. package/dist/lib/internal/git-workflow.js +1 -1
  30. package/dist/lib/internal/platform-release-notes.js +53 -0
  31. package/dist/lib/internal/release-workflow.js +175 -17
  32. package/dist/lib/internal/task-orchestration.js +151 -0
  33. package/dist/lib/internal/task-review.js +65 -0
  34. package/dist/lib/merge.js +2 -1
  35. package/dist/lib/platform/adapters.js +38 -1
  36. package/dist/lib/platform/change-request-git-evidence.js +113 -0
  37. package/dist/lib/platform/github-release-notes.js +96 -0
  38. package/dist/lib/platform/issues.js +4 -2
  39. package/dist/lib/platform/merged-pr-equivalence.js +84 -0
  40. package/dist/lib/platform/pr-checks.js +43 -15
  41. package/dist/lib/platform/pull-requests.js +113 -7
  42. package/dist/lib/platform/release-notes.js +143 -0
  43. package/dist/lib/platform/releases.js +1 -1
  44. package/dist/lib/platform/verification-required.js +31 -9
  45. package/dist/lib/prompt.js +15 -3
  46. package/dist/lib/release/channels.js +4 -1
  47. package/dist/lib/release/workflow.js +4 -2
  48. package/dist/lib/remove-dir.js +25 -0
  49. package/dist/lib/run/tui.js +8 -2
  50. package/dist/lib/sandbox/agent-client-reconciler.js +234 -0
  51. package/dist/lib/sandbox/build-proxy.js +63 -0
  52. package/dist/lib/sandbox/clipboard/linux.js +2 -1
  53. package/dist/lib/sandbox/commands/create.js +133 -189
  54. package/dist/lib/sandbox/commands/enter.js +17 -1
  55. package/dist/lib/sandbox/commands/prune.js +2 -2
  56. package/dist/lib/sandbox/commands/rebuild.js +24 -7
  57. package/dist/lib/sandbox/commands/rm.js +3 -2
  58. package/dist/lib/sandbox/config.js +21 -4
  59. package/dist/lib/sandbox/constants.js +3 -0
  60. package/dist/lib/sandbox/dockerfile.js +2 -1
  61. package/dist/lib/sandbox/image-build.js +2 -1
  62. package/dist/lib/sandbox/managed-fs.js +10 -5
  63. package/dist/lib/sandbox/recovery.js +52 -36
  64. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +24 -3
  65. package/dist/lib/sandbox/runtimes/base.dockerfile +8 -1
  66. package/dist/lib/sandbox/runtimes/java17.dockerfile +8 -1
  67. package/dist/lib/sandbox/runtimes/java21.dockerfile +8 -1
  68. package/dist/lib/sandbox/runtimes/node20.dockerfile +8 -1
  69. package/dist/lib/sandbox/runtimes/node22.dockerfile +8 -1
  70. package/dist/lib/sandbox/runtimes/python3.dockerfile +8 -1
  71. package/dist/lib/sandbox/shell.js +3 -2
  72. package/dist/lib/sandbox/tool-types.js +8 -0
  73. package/dist/lib/sandbox/tools.js +2 -126
  74. package/dist/lib/task/artifact-lifecycle.js +1 -49
  75. package/dist/lib/task/commands/decisions.js +50 -20
  76. package/dist/lib/task/commands/issue-body.js +10 -5
  77. package/dist/lib/task/delegation-receipts.js +107 -0
  78. package/dist/lib/task/events.js +101 -2
  79. package/dist/lib/task/issue-form.js +2 -0
  80. package/dist/lib/task/orchestration.js +420 -0
  81. package/dist/lib/task/review-artifacts.js +143 -59
  82. package/dist/lib/task/review-finalization.js +166 -0
  83. package/dist/lib/task/review-fingerprint.js +13 -0
  84. package/dist/lib/task/verification-checks.js +2 -1
  85. package/dist/lib/task/verification-engine.js +168 -23
  86. package/dist/lib/task/verification-review.js +1 -1
  87. package/dist/lib/task/verification.js +4 -2
  88. package/dist/lib/task/workspace-snapshot.js +34 -0
  89. package/dist/lib/update.js +42 -161
  90. package/lib/agent-client.ts +214 -0
  91. package/lib/agent-clients/adapter.ts +461 -0
  92. package/lib/agent-clients/adapters/claude-code.ts +70 -0
  93. package/lib/agent-clients/adapters/codex-sandbox.ts +245 -0
  94. package/lib/agent-clients/adapters/codex.ts +58 -0
  95. package/lib/agent-clients/adapters/gemini-cli.ts +65 -0
  96. package/lib/agent-clients/adapters/index.ts +27 -0
  97. package/lib/agent-clients/adapters/opencode.ts +66 -0
  98. package/lib/agent-clients/config.ts +278 -0
  99. package/lib/agent-clients/custom-tuis.ts +149 -0
  100. package/lib/agent-clients/invocation.ts +19 -0
  101. package/lib/agent-clients/next-steps.ts +72 -0
  102. package/lib/agent-clients/project-assets.ts +96 -0
  103. package/lib/agent-clients/reconcile.ts +324 -0
  104. package/lib/agent-clients/registry.ts +144 -0
  105. package/lib/agent-clients/schema.ts +62 -0
  106. package/lib/agent-clients/types.ts +78 -0
  107. package/lib/benchmark/schemas/v1.0.0/case-manifest.schema.json +120 -0
  108. package/lib/benchmark/schemas/v1.0.0/common.schema.json +109 -0
  109. package/lib/benchmark/schemas/v1.0.0/grader-result.schema.json +160 -0
  110. package/lib/benchmark/schemas/v1.0.0/run-manifest.schema.json +122 -0
  111. package/lib/benchmark/schemas/v1.0.0/subject.schema.json +227 -0
  112. package/lib/builtin-tuis.ts +16 -15
  113. package/lib/defaults.json +24 -12
  114. package/lib/git/review-snapshot.ts +39 -9
  115. package/lib/git/reviewed-commit-equivalence.ts +135 -0
  116. package/lib/init.ts +53 -121
  117. package/lib/internal/agent-client.ts +131 -0
  118. package/lib/internal/git-workflow.ts +1 -1
  119. package/lib/internal/platform-release-notes.ts +55 -0
  120. package/lib/internal/release-workflow.ts +172 -18
  121. package/lib/internal/task-orchestration.ts +132 -0
  122. package/lib/internal/task-review.ts +65 -0
  123. package/lib/merge.ts +2 -1
  124. package/lib/platform/adapters.ts +142 -3
  125. package/lib/platform/change-request-git-evidence.ts +159 -0
  126. package/lib/platform/github-release-notes.ts +132 -0
  127. package/lib/platform/issues.ts +6 -4
  128. package/lib/platform/merged-pr-equivalence.ts +107 -0
  129. package/lib/platform/pr-checks.ts +57 -28
  130. package/lib/platform/pull-requests.ts +140 -25
  131. package/lib/platform/release-notes.ts +167 -0
  132. package/lib/platform/releases.ts +1 -1
  133. package/lib/platform/verification-required.ts +37 -9
  134. package/lib/prompt.ts +16 -3
  135. package/lib/release/channels.ts +4 -1
  136. package/lib/release/workflow.ts +4 -3
  137. package/lib/remove-dir.ts +28 -0
  138. package/lib/run/tui.ts +9 -1
  139. package/lib/sandbox/agent-client-reconciler.ts +378 -0
  140. package/lib/sandbox/build-proxy.ts +94 -0
  141. package/lib/sandbox/clipboard/linux.ts +2 -1
  142. package/lib/sandbox/commands/create.ts +188 -203
  143. package/lib/sandbox/commands/enter.ts +23 -1
  144. package/lib/sandbox/commands/prune.ts +2 -2
  145. package/lib/sandbox/commands/rebuild.ts +29 -7
  146. package/lib/sandbox/commands/rm.ts +3 -2
  147. package/lib/sandbox/config.ts +31 -4
  148. package/lib/sandbox/constants.ts +6 -0
  149. package/lib/sandbox/dockerfile.ts +2 -1
  150. package/lib/sandbox/image-build.ts +4 -1
  151. package/lib/sandbox/managed-fs.ts +18 -5
  152. package/lib/sandbox/recovery.ts +98 -67
  153. package/lib/sandbox/runtimes/ai-tools.dockerfile +24 -3
  154. package/lib/sandbox/runtimes/base.dockerfile +8 -1
  155. package/lib/sandbox/runtimes/java17.dockerfile +8 -1
  156. package/lib/sandbox/runtimes/java21.dockerfile +8 -1
  157. package/lib/sandbox/runtimes/node20.dockerfile +8 -1
  158. package/lib/sandbox/runtimes/node22.dockerfile +8 -1
  159. package/lib/sandbox/runtimes/python3.dockerfile +8 -1
  160. package/lib/sandbox/shell.ts +4 -2
  161. package/lib/sandbox/tool-types.ts +143 -0
  162. package/lib/sandbox/tools.ts +5 -157
  163. package/lib/task/artifact-lifecycle.ts +1 -38
  164. package/lib/task/commands/decisions.ts +53 -20
  165. package/lib/task/commands/issue-body.ts +10 -5
  166. package/lib/task/delegation-receipts.ts +170 -0
  167. package/lib/task/events.ts +111 -3
  168. package/lib/task/issue-form.ts +4 -0
  169. package/lib/task/orchestration.ts +543 -0
  170. package/lib/task/review-artifacts.ts +201 -69
  171. package/lib/task/review-finalization.ts +275 -0
  172. package/lib/task/review-fingerprint.ts +15 -1
  173. package/lib/task/verification-checks.ts +2 -1
  174. package/lib/task/verification-engine.ts +190 -25
  175. package/lib/task/verification-review.ts +2 -0
  176. package/lib/task/verification.ts +6 -3
  177. package/lib/task/workspace-snapshot.ts +36 -0
  178. package/lib/update.ts +48 -184
  179. package/package.json +2 -2
  180. package/templates/.agents/README.en.md +21 -20
  181. package/templates/.agents/README.zh-CN.md +21 -20
  182. package/templates/.agents/hooks/lifecycle-delegation.js +64 -0
  183. package/templates/.agents/rules/README.en.md +2 -0
  184. package/templates/.agents/rules/README.zh-CN.md +2 -0
  185. package/templates/.agents/rules/commit-and-pr.en.md +1 -0
  186. package/templates/.agents/rules/commit-and-pr.zh-CN.md +1 -0
  187. package/templates/.agents/rules/human-decision-context.en.md +14 -12
  188. package/templates/.agents/rules/human-decision-context.zh-CN.md +14 -12
  189. package/templates/.agents/rules/lifecycle-orchestration.en.md +21 -0
  190. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +21 -0
  191. package/templates/.agents/rules/next-step-output.en.md +16 -0
  192. package/templates/.agents/rules/next-step-output.zh-CN.md +16 -0
  193. package/templates/.agents/rules/no-mid-flow-questions.en.md +2 -0
  194. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +2 -0
  195. package/templates/.agents/rules/release-commands.en.md +2 -2
  196. package/templates/.agents/rules/release-commands.github.en.md +15 -39
  197. package/templates/.agents/rules/release-commands.github.zh-CN.md +14 -38
  198. package/templates/.agents/rules/release-commands.zh-CN.md +2 -2
  199. package/templates/.agents/rules/review-handshake.en.md +4 -3
  200. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -3
  201. package/templates/.agents/rules/review-method.en.md +113 -0
  202. package/templates/.agents/rules/review-method.zh-CN.md +98 -0
  203. package/templates/.agents/rules/testing-discipline.en.md +20 -2
  204. package/templates/.agents/rules/testing-discipline.zh-CN.md +20 -2
  205. package/templates/.agents/skills/analyze-task/SKILL.en.md +9 -6
  206. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +9 -6
  207. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  208. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +4 -4
  209. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -4
  210. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -5
  211. package/templates/.agents/skills/check-task/SKILL.en.md +16 -16
  212. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -16
  213. package/templates/.agents/skills/close-codescan/SKILL.en.md +4 -4
  214. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +4 -4
  215. package/templates/.agents/skills/close-dependabot/SKILL.en.md +4 -4
  216. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +4 -4
  217. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  218. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  219. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +14 -16
  220. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +14 -16
  221. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  222. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  223. package/templates/.agents/skills/commit/SKILL.en.md +11 -1
  224. package/templates/.agents/skills/commit/SKILL.zh-CN.md +11 -1
  225. package/templates/.agents/skills/commit/reference/task-status-update.en.md +24 -28
  226. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +24 -28
  227. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  228. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  229. package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
  230. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
  231. package/templates/.agents/skills/create-pr/SKILL.en.md +10 -10
  232. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +10 -10
  233. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  234. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  235. package/templates/.agents/skills/create-release-note/SKILL.en.md +17 -38
  236. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +16 -37
  237. package/templates/.agents/skills/create-task/SKILL.en.md +15 -11
  238. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +15 -11
  239. package/templates/.agents/skills/create-task/reference/context-capture.en.md +29 -0
  240. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +29 -0
  241. package/templates/.agents/skills/import-codescan/SKILL.en.md +5 -5
  242. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +5 -5
  243. package/templates/.agents/skills/import-dependabot/SKILL.en.md +5 -5
  244. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +5 -5
  245. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  246. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  247. package/templates/.agents/skills/init-labels/SKILL.en.md +4 -4
  248. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +4 -4
  249. package/templates/.agents/skills/init-milestones/SKILL.en.md +4 -4
  250. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -4
  251. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  252. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  253. package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
  254. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
  255. package/templates/.agents/skills/release/SKILL.en.md +1 -1
  256. package/templates/.agents/skills/review-analysis/SKILL.en.md +13 -2
  257. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -3
  258. package/templates/.agents/skills/review-analysis/config/verify.en.json +6 -0
  259. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +6 -0
  260. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +13 -15
  261. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +13 -15
  262. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +51 -5
  263. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +52 -6
  264. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +25 -22
  265. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +25 -22
  266. package/templates/.agents/skills/review-code/SKILL.en.md +28 -8
  267. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +28 -8
  268. package/templates/.agents/skills/review-code/config/verify.en.json +7 -0
  269. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +7 -0
  270. package/templates/.agents/skills/review-code/reference/concurrency-risks.en.md +25 -0
  271. package/templates/.agents/skills/review-code/reference/concurrency-risks.zh-CN.md +25 -0
  272. package/templates/.agents/skills/review-code/reference/cross-platform-risks.en.md +27 -0
  273. package/templates/.agents/skills/review-code/reference/cross-platform-risks.zh-CN.md +27 -0
  274. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.en.md +13 -0
  275. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.zh-CN.md +13 -0
  276. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +25 -0
  277. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +25 -0
  278. package/templates/.agents/skills/review-code/reference/output-templates.en.md +22 -24
  279. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +22 -24
  280. package/templates/.agents/skills/review-code/reference/report-template.en.md +54 -7
  281. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +54 -7
  282. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +29 -24
  283. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +29 -24
  284. package/templates/.agents/skills/review-code/reference/security-risks.en.md +25 -0
  285. package/templates/.agents/skills/review-code/reference/security-risks.zh-CN.md +25 -0
  286. package/templates/.agents/skills/review-plan/SKILL.en.md +13 -2
  287. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -3
  288. package/templates/.agents/skills/review-plan/config/verify.en.json +6 -0
  289. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +6 -0
  290. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +13 -15
  291. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +13 -15
  292. package/templates/.agents/skills/review-plan/reference/report-template.en.md +61 -5
  293. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +61 -5
  294. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +21 -24
  295. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +21 -24
  296. package/templates/.agents/skills/run-task/SKILL.en.md +22 -0
  297. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +22 -0
  298. package/templates/.agents/skills/run-task/config/verify.en.json +9 -0
  299. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +9 -0
  300. package/templates/.agents/skills/test/SKILL.en.md +4 -4
  301. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -4
  302. package/templates/.agents/skills/test-integration/SKILL.en.md +4 -4
  303. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +4 -4
  304. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -5
  305. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -5
  306. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +647 -203
  307. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +4 -4
  308. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +4 -4
  309. package/templates/.agents/skills/watch-pr/SKILL.en.md +8 -8
  310. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +8 -8
  311. package/templates/.agents/templates/task.en.md +18 -0
  312. package/templates/.agents/templates/task.zh-CN.md +18 -0
  313. package/templates/.claude/agents/agent-infra-lifecycle-executor.en.md +6 -0
  314. package/templates/.claude/agents/agent-infra-lifecycle-executor.zh-CN.md +6 -0
  315. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.en.md +6 -0
  316. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.zh-CN.md +6 -0
  317. package/templates/.claude/commands/run-task.en.md +8 -0
  318. package/templates/.claude/commands/run-task.zh-CN.md +8 -0
  319. package/templates/.claude/settings.json +24 -0
  320. package/templates/.codex/agents/agent-infra-lifecycle-executor.en.toml +3 -0
  321. package/templates/.codex/agents/agent-infra-lifecycle-executor.zh-CN.toml +3 -0
  322. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.en.toml +3 -0
  323. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.zh-CN.toml +3 -0
  324. package/templates/.gemini/commands/_project_/run-task.en.toml +8 -0
  325. package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +8 -0
  326. package/templates/.opencode/commands/run-task.en.md +11 -0
  327. package/templates/.opencode/commands/run-task.zh-CN.md +11 -0
@@ -6,13 +6,15 @@ import { execFileSync } from 'node:child_process';
6
6
  import { parseArgs } from 'node:util';
7
7
  import * as p from '@clack/prompts';
8
8
  import pc from 'picocolors';
9
- import * as toml from 'smol-toml';
9
+ import { listAgentClientAdapters } from '../../agent-clients/registry.js';
10
10
  import { loadConfig } from '../config.js';
11
- import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from '../constants.js';
11
+ import { assertValidBranchName, containerName, containerNameCandidates, parsePositiveIntegerOption, sandboxBranchLabel, sandboxImageConfigLabel, sandboxImageRefreshLabel, sandboxLabel, sandboxRuntimeCapabilityLabel, shareBranchDir, shareCommonDir, shellConfigDir, worktreeDirCandidates } from '../constants.js';
12
12
  import { prepareDockerfile } from '../dockerfile.js';
13
+ import { assertBuildProxyCompatibility, buildProxyFailureHint, prepareBuildProxy, redactBuildProxyValues } from '../build-proxy.js';
13
14
  import { detectEngine, ensureDocker } from '../engine.js';
14
15
  import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSafe, runSafeEngine, runVerboseEngine } from '../shell.js';
15
16
  import { resolveTaskBranch } from '../task-resolver.js';
17
+ import { createSandboxCapabilityPlan, runSandboxHooks, runBoundedSandboxHookCommand } from '../agent-client-reconciler.js';
16
18
  import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirCandidates } from '../tools.js';
17
19
  import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts } from '../recovery.js';
18
20
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
@@ -21,35 +23,16 @@ import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.
21
23
  import { validateSelinuxDisableEnv } from '../engines/selinux.js';
22
24
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
23
25
  import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.js';
26
+ import { removeDirRecursive } from '../../remove-dir.js';
24
27
  import { prepareClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
25
28
  import { detectHostTimezone } from '../host-timezone.js';
26
29
  import { buildImageSignature, buildSandboxImageArgs, isRefreshDisabled, isRefreshDue, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
27
- const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
30
+ export { ensureCodexModelInheritance, ensureCodexWorkspaceTrust } from '../../agent-clients/adapters/codex-sandbox.js';
28
31
  const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
29
32
  const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
30
- const SANDBOX_ALIAS_NAMES = [
31
- 'claude-yolo',
32
- 'opencode-yolo',
33
- 'codex-yolo',
34
- 'gemini-yolo',
35
- 'cy',
36
- 'oy',
37
- 'xy',
38
- 'gy'
39
- ];
40
- const DEFAULT_SANDBOX_ALIASES = `alias claude-yolo='claude --dangerously-skip-permissions; tput ed'
41
- alias opencode-yolo='OPENCODE_PERMISSION='\\''${OPENCODE_YOLO_PERMISSION}'\\'' opencode; tput ed'
42
- alias codex-yolo='codex --yolo; tput ed'
43
- alias gemini-yolo='gemini --yolo; tput ed'
44
-
45
- alias cy='claude --dangerously-skip-permissions; tput ed'
46
- alias oy='OPENCODE_PERMISSION='\\''${OPENCODE_YOLO_PERMISSION}'\\'' opencode; tput ed'
47
- alias xy='codex --yolo; tput ed'
48
- alias gy='gemini --yolo; tput ed'
49
- `;
50
33
  const CONTAINER_HOME = '/home/devuser';
51
34
  const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
52
- const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh] [--inherit-proxy|-P]
35
+ const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh] [--inherit-proxy|-P] [--inherit-build-proxy|-B]
53
36
 
54
37
  Host aliases:
55
38
  ${'~'}/.agent-infra/aliases/sandbox.sh is auto-created on first run and exposed
@@ -59,7 +42,9 @@ Host aliases:
59
42
 
60
43
  Proxy:
61
44
  --inherit-proxy, -P Copy non-empty standard host proxy variables into the
62
- container environment through the private env file.`;
45
+ container environment through the private env file.
46
+ --inherit-build-proxy, -B Pass uppercase HTTP_PROXY, HTTPS_PROXY, and NO_PROXY
47
+ to managed image build steps for this invocation.`;
63
48
  const HOST_PROXY_ENV_KEYS = [
64
49
  'http_proxy',
65
50
  'HTTP_PROXY',
@@ -70,6 +55,15 @@ const HOST_PROXY_ENV_KEYS = [
70
55
  'no_proxy',
71
56
  'NO_PROXY'
72
57
  ];
58
+ const MANAGED_GITHUB_GIT_CONFIG = [
59
+ '[credential]',
60
+ '\thelper = !gh auth git-credential',
61
+ '[credential "https://github.com"]',
62
+ '\thelper =',
63
+ '\thelper = !gh auth git-credential',
64
+ '[url "https://github.com/"]',
65
+ '\tinsteadOf = git@github.com:'
66
+ ];
73
67
  function resolveToolDirs(config, tools, branch) {
74
68
  return tools.map((tool) => {
75
69
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -158,6 +152,14 @@ function normalizeContainerHomeSeparators(content) {
158
152
  const containerHomePattern = new RegExp(`${escapeRegExp(CONTAINER_HOME)}\\S*`, 'g');
159
153
  return content.replace(containerHomePattern, (value) => value.replaceAll('\\', '/'));
160
154
  }
155
+ function isGitHubCliCredentialHelper(line) {
156
+ const helperMatch = line.match(/^\s*helper\s*=\s*(.+?)\s*$/i);
157
+ if (!helperMatch) {
158
+ return false;
159
+ }
160
+ const helper = (helperMatch[1] ?? '').trim().replace(/^"(.*)"$/, '$1');
161
+ return /^!\s*(?:"[^"]*[\\/]+gh"|(?:\S*[\\/]+)?gh)\s+auth\s+git-credential(?:\s|$)/i.test(helper);
162
+ }
161
163
  export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot = '' } = {}) {
162
164
  const posixHome = home.replaceAll('\\', '/');
163
165
  const normalizedGitconfig = gitconfig
@@ -170,10 +172,12 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
170
172
  const sanitized = [];
171
173
  let inGpgSection = false;
172
174
  let currentSection = '';
175
+ let currentSectionName = '';
173
176
  for (const line of lines) {
174
177
  const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
175
178
  if (sectionMatch) {
176
179
  const sectionName = (sectionMatch[1] ?? '').trim();
180
+ currentSectionName = sectionName;
177
181
  currentSection = ((sectionName.match(/^([^\s"]+)/)?.[1]) ?? '').toLowerCase();
178
182
  inGpgSection = /^gpg(?:\s+"[^"]+")?$/i.test(sectionName);
179
183
  if (stripGpg && inGpgSection) {
@@ -199,9 +203,19 @@ export function sanitizeGitConfig(gitconfig, home, { stripGpg = false, repoRoot
199
203
  if (stripGpg && currentSection === 'user' && /^\s*signingKey\s*=.*$/i.test(line)) {
200
204
  continue;
201
205
  }
206
+ if (currentSection === 'credential' && isGitHubCliCredentialHelper(line)) {
207
+ continue;
208
+ }
209
+ if (/^url\s+"https:\/\/github\.com\/"$/i.test(currentSectionName)
210
+ && /^\s*insteadOf\s*=\s*git@github\.com:\s*$/i.test(line)) {
211
+ continue;
212
+ }
202
213
  sanitized.push(line);
203
214
  }
204
- return appendSafeDirectories(sanitized, repoRoot).join('\n');
215
+ return [
216
+ ...appendSafeDirectories(sanitized, repoRoot),
217
+ ...MANAGED_GITHUB_GIT_CONFIG
218
+ ].join('\n');
205
219
  }
206
220
  export function hostHasGpgKeys(home, execFn = execFileSync) {
207
221
  return currentKeyringFingerprint(home, execFn) !== null;
@@ -236,7 +250,7 @@ export function ensureShellConfigSymlinks(engine, container, execFn = execEngine
236
250
  }
237
251
  export function prepareHostShellConfig({ home, project, branch, repoRoot }) {
238
252
  const hostDir = hostShellConfigDir(home, project, branch);
239
- fs.rmSync(hostDir, { recursive: true, force: true });
253
+ removeDirRecursive(hostDir);
240
254
  fs.mkdirSync(hostDir, { recursive: true });
241
255
  writeSanitizedGitconfig({
242
256
  home,
@@ -274,11 +288,19 @@ function normalizeWorktreePath(worktreePath) {
274
288
  if (!worktreePath) {
275
289
  return '';
276
290
  }
291
+ const wslMount = process.platform === 'win32'
292
+ ? worktreePath.match(/^\/mnt\/([A-Za-z])(?:\/(.*))?$/)
293
+ : null;
294
+ const hostPath = wslMount
295
+ ? `${wslMount[1].toUpperCase()}:\\${(wslMount[2] ?? '').replace(/\//g, '\\')}`
296
+ : worktreePath;
277
297
  try {
278
- return fs.existsSync(worktreePath) ? fs.realpathSync(worktreePath) : path.resolve(worktreePath);
298
+ const normalized = fs.existsSync(hostPath) ? fs.realpathSync(hostPath) : path.resolve(hostPath);
299
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
279
300
  }
280
301
  catch {
281
- return path.resolve(worktreePath);
302
+ const normalized = path.resolve(hostPath);
303
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
282
304
  }
283
305
  }
284
306
  export function getGitSigningKey({ home, repoPath = null, execFn = execFileSync } = {}) {
@@ -449,7 +471,7 @@ export function collectHostProxyEntries(env) {
449
471
  });
450
472
  }
451
473
  export function buildContainerEnvFile(resolvedTools, engine, runSafeEngineFn = runSafeEngine, options = {}) {
452
- const { additionalEntries = [], mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = fs.rmSync, tmpDir = os.tmpdir(), runSafeFn = runSafe } = options;
474
+ const { additionalEntries = [], mkdtempFn = fs.mkdtempSync, writeFileFn = fs.writeFileSync, chmodFn = fs.chmodSync, rmFn = removeDirRecursive, tmpDir = os.tmpdir(), runSafeFn = runSafe } = options;
453
475
  const entries = resolvedTools.flatMap(({ tool }) => Object.entries(tool.envVars ?? {}));
454
476
  entries.push(...additionalEntries);
455
477
  let ghToken = runSafeEngineFn(engine, 'gh', ['auth', 'token']);
@@ -673,141 +695,6 @@ export function ensureClaudeSettings(toolDir, hostHomeDir) {
673
695
  fs.writeFileSync(settingsPath, JSON.stringify(data, null, 4), 'utf8');
674
696
  }
675
697
  }
676
- function resolveHostCatalogPath(value, hostHomeDir) {
677
- if (typeof value !== 'string' || value === '') {
678
- return null;
679
- }
680
- let resolved;
681
- if (value === '~' || value.startsWith('~/') || value.startsWith('~\\')) {
682
- resolved = path.join(hostHomeDir, value.slice(1).replace(/^[/\\]+/, ''));
683
- }
684
- else if (path.isAbsolute(value)) {
685
- resolved = value;
686
- }
687
- else {
688
- resolved = path.join(hostHomeDir, '.codex', value);
689
- }
690
- try {
691
- if (!fs.statSync(resolved).isFile()) {
692
- return null;
693
- }
694
- fs.accessSync(resolved, fs.constants.R_OK);
695
- return resolved;
696
- }
697
- catch {
698
- return null;
699
- }
700
- }
701
- const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'];
702
- function removeCodexMcpServers(sandboxParsed) {
703
- if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
704
- return false;
705
- }
706
- delete sandboxParsed.mcp_servers;
707
- return true;
708
- }
709
- function inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) {
710
- if (!isJsonObjectRecord(hostParsed.features)) {
711
- return false;
712
- }
713
- if (Object.hasOwn(sandboxParsed, 'features') && !isJsonObjectRecord(sandboxParsed.features)) {
714
- return false;
715
- }
716
- let sandboxFeatures = sandboxParsed.features;
717
- let changed = false;
718
- for (const key of CODEX_DISABLED_FEATURE_FLAGS) {
719
- if (hostParsed.features[key] !== false) {
720
- continue;
721
- }
722
- if (!sandboxFeatures) {
723
- sandboxFeatures = {};
724
- sandboxParsed.features = sandboxFeatures;
725
- }
726
- if (sandboxFeatures[key] !== false) {
727
- sandboxFeatures[key] = false;
728
- changed = true;
729
- }
730
- }
731
- return changed;
732
- }
733
- export function ensureCodexModelInheritance(toolDir, hostHomeDir, containerCodexDir = '/home/devuser/.codex') {
734
- if (!hostHomeDir) {
735
- return;
736
- }
737
- const hostConfigPath = path.join(hostHomeDir, '.codex', 'config.toml');
738
- if (!fs.existsSync(hostConfigPath)) {
739
- return;
740
- }
741
- let hostParsed;
742
- try {
743
- hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8'));
744
- }
745
- catch {
746
- return;
747
- }
748
- const sandboxConfigPath = path.join(toolDir, 'config.toml');
749
- // This rewrites sandbox-side TOML and drops comments; the host config stays untouched.
750
- let sandboxParsed = {};
751
- if (fs.existsSync(sandboxConfigPath)) {
752
- try {
753
- sandboxParsed = toml.parse(fs.readFileSync(sandboxConfigPath, 'utf8'));
754
- }
755
- catch {
756
- return;
757
- }
758
- }
759
- let changed = removeCodexMcpServers(sandboxParsed);
760
- changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
761
- const inheritSpecs = [
762
- ['model', 'string'],
763
- ['model_reasoning_effort', 'string'],
764
- ['model_auto_compact_token_limit', 'number']
765
- ];
766
- for (const [key, type] of inheritSpecs) {
767
- if (Object.hasOwn(sandboxParsed, key)) {
768
- continue;
769
- }
770
- const value = hostParsed[key];
771
- if (type === 'string' && (typeof value !== 'string' || value === '')) {
772
- continue;
773
- }
774
- if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
775
- continue;
776
- }
777
- sandboxParsed[key] = value;
778
- changed = true;
779
- }
780
- if (!Object.hasOwn(sandboxParsed, 'model_catalog_json')) {
781
- const hostCatalogPath = resolveHostCatalogPath(hostParsed['model_catalog_json'], hostHomeDir);
782
- if (hostCatalogPath) {
783
- try {
784
- const basename = path.basename(hostCatalogPath);
785
- const destDir = path.join(toolDir, 'model-catalogs');
786
- fs.mkdirSync(destDir, { recursive: true });
787
- fs.copyFileSync(hostCatalogPath, path.join(destDir, basename));
788
- sandboxParsed['model_catalog_json'] = path.posix.join(containerCodexDir, 'model-catalogs', basename);
789
- changed = true;
790
- }
791
- catch {
792
- // Copy failed (e.g. permissions): skip catalog, keep scalar inheritance intact.
793
- }
794
- }
795
- }
796
- if (changed) {
797
- fs.writeFileSync(sandboxConfigPath, `${toml.stringify(sandboxParsed)}\n`, 'utf8');
798
- }
799
- }
800
- export function ensureCodexWorkspaceTrust(toolDir) {
801
- const configPath = path.join(toolDir, 'config.toml');
802
- let content = '';
803
- if (fs.existsSync(configPath)) {
804
- content = fs.readFileSync(configPath, 'utf8');
805
- }
806
- if (!content.includes('[projects."/workspace"]')) {
807
- const entry = '\n[projects."/workspace"]\ntrust_level = "trusted"\n';
808
- fs.writeFileSync(configPath, content + entry, 'utf8');
809
- }
810
- }
811
698
  export function ensureOpenCodeModelInheritance(toolDir, hostHomeDir) {
812
699
  if (!hostHomeDir) {
813
700
  return;
@@ -868,7 +755,7 @@ function stripManagedSandboxAliasBlocks(content) {
868
755
  const blockPattern = new RegExp(`${escapeRegExp(SANDBOX_ALIAS_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(SANDBOX_ALIAS_BLOCK_END)}\\n?`, 'g');
869
756
  return content.replace(blockPattern, '').trimEnd();
870
757
  }
871
- function isLegacyManagedSandboxAliasFile(content) {
758
+ function isLegacyManagedSandboxAliasFile(content, aliases) {
872
759
  const lines = content
873
760
  .split(/\r?\n/)
874
761
  .map((line) => line.trim())
@@ -876,19 +763,25 @@ function isLegacyManagedSandboxAliasFile(content) {
876
763
  if (lines.length === 0) {
877
764
  return false;
878
765
  }
879
- const aliasPattern = new RegExp(`^alias (${SANDBOX_ALIAS_NAMES.map(escapeRegExp).join('|')})=`);
766
+ const aliasPattern = new RegExp(`^alias (${aliases.map((alias) => escapeRegExp(alias.name)).join('|')})=`);
880
767
  return lines.every((line) => aliasPattern.test(line));
881
768
  }
882
- export function ensureSandboxAliasesFile(home) {
769
+ function registrySandboxAliases() {
770
+ return listAgentClientAdapters().flatMap((adapter) => adapter.sandbox.aliases);
771
+ }
772
+ function renderSandboxAliases(aliases) {
773
+ return `${aliases.map((alias) => `alias ${alias.name}='${alias.command.replaceAll("'", "'\\''")}'`).join('\n')}\n`;
774
+ }
775
+ export function ensureSandboxAliasesFile(home, aliases = registrySandboxAliases()) {
883
776
  const aliasesPath = sandboxAliasesPath(home);
884
- const managedBlock = `${SANDBOX_ALIAS_BLOCK_BEGIN}\n${DEFAULT_SANDBOX_ALIASES}${SANDBOX_ALIAS_BLOCK_END}\n`;
777
+ const managedBlock = `${SANDBOX_ALIAS_BLOCK_BEGIN}\n${renderSandboxAliases(aliases)}${SANDBOX_ALIAS_BLOCK_END}\n`;
885
778
  fs.mkdirSync(path.dirname(aliasesPath), { recursive: true });
886
779
  const created = !fs.existsSync(aliasesPath);
887
780
  let existing = '';
888
781
  if (!created) {
889
782
  existing = fs.readFileSync(aliasesPath, 'utf8');
890
783
  }
891
- const userContent = isLegacyManagedSandboxAliasFile(existing)
784
+ const userContent = isLegacyManagedSandboxAliasFile(existing, aliases)
892
785
  ? ''
893
786
  : stripManagedSandboxAliasBlocks(existing);
894
787
  const nextContent = userContent
@@ -928,7 +821,7 @@ export function buildTmpfsRunArgs(containerMount, tmpfs) {
928
821
  const size = tmpfs.size ?? '512m';
929
822
  return ['--tmpfs', `${containerMount}:rw,size=${size}`];
930
823
  }
931
- export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env, refresh = false, lastRefresh } = {}) {
824
+ export function buildImage(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, runVerboseFn = runVerboseEngine, env = process.env, refresh = false, lastRefresh, buildProxyArgs = [], buildEnv } = {}) {
932
825
  const selectedEngine = engine ?? detectEngine({ engine: config.engine });
933
826
  runVerboseFn(selectedEngine, 'docker', buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
934
827
  engine: selectedEngine,
@@ -936,8 +829,9 @@ export function buildImage(config, tools, dockerfilePath, imageSignature, { engi
936
829
  runSafeFn,
937
830
  env,
938
831
  refresh,
939
- lastRefresh
940
- }), { cwd: config.repoRoot });
832
+ lastRefresh,
833
+ buildProxyArgs
834
+ }), { cwd: config.repoRoot, env: buildEnv });
941
835
  }
942
836
  function readImageLabels(config, engine) {
943
837
  return parseImageLabels(runSafeEngine(engine, 'docker', [
@@ -958,6 +852,7 @@ export async function create(args) {
958
852
  memory: { type: 'string' },
959
853
  'no-refresh': { type: 'boolean' },
960
854
  'inherit-proxy': { type: 'boolean', short: 'P' },
855
+ 'inherit-build-proxy': { type: 'boolean', short: 'B' },
961
856
  help: { type: 'boolean', short: 'h' }
962
857
  }
963
858
  });
@@ -984,15 +879,14 @@ export async function create(args) {
984
879
  };
985
880
  const worktreeCandidates = worktreeDirCandidates(effectiveConfig, branch);
986
881
  assertBranchAvailable(config.repoRoot, branch, { allowedWorktrees: worktreeCandidates });
987
- const tools = resolveTools(effectiveConfig);
882
+ const capabilityPlan = createSandboxCapabilityPlan(effectiveConfig);
883
+ const tools = [...capabilityPlan.tools];
988
884
  const resolvedTools = resolveToolDirs(effectiveConfig, tools, branch);
989
885
  // Fatal credential states still fail before filesystem/docker side effects.
990
- // A genuinely missing Claude Code credential only removes Claude Code's
991
- // sandbox config and credential mounts for this create run.
886
+ // Missing credentials leave the selected Claude Code capability installed
887
+ // and mounted; only the absent live credential file is omitted.
992
888
  const credentialOutcome = prepareClaudeCredentials(effectiveConfig.home, effectiveConfig.project, resolvedTools);
993
- const effectiveResolvedTools = credentialOutcome.status === 'SKIPPED'
994
- ? resolvedTools.filter(({ tool }) => tool.id !== 'claude-code')
995
- : resolvedTools;
889
+ const effectiveResolvedTools = resolvedTools;
996
890
  const container = containerName(effectiveConfig, branch);
997
891
  const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
998
892
  const shareCommon = shareCommonDir(effectiveConfig);
@@ -1011,6 +905,16 @@ export async function create(args) {
1011
905
  }
1012
906
  try {
1013
907
  p.log.step('Checking container engine...');
908
+ const prepareHookResults = await runSandboxHooks({
909
+ hooks: capabilityPlan.hooksByPhase.prepare,
910
+ phase: 'prepare',
911
+ context: { config: effectiveConfig, plan: capabilityPlan },
912
+ runCommand: runBoundedSandboxHookCommand
913
+ });
914
+ const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
915
+ if (prepareFailure) {
916
+ throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
917
+ }
1014
918
  await ensureDocker(effectiveConfig, (detail) => {
1015
919
  p.log.info(` ${detail}`);
1016
920
  });
@@ -1027,6 +931,12 @@ export async function create(args) {
1027
931
  && isRefreshDue(currentLastRefresh, now, effectiveConfig.refreshIntervalDays);
1028
932
  const needsImageBuild = signatureStale || refreshDue;
1029
933
  if (needsImageBuild) {
934
+ if (values['inherit-build-proxy'] && effectiveConfig.dockerfile) {
935
+ throw new Error('Build proxy inheritance is unavailable with a custom sandbox Dockerfile.');
936
+ }
937
+ const buildProxy = prepareBuildProxy(values['inherit-build-proxy'] ?? false, process.env, engine);
938
+ if (values['inherit-build-proxy'])
939
+ assertBuildProxyCompatibility(engine);
1030
940
  const buildRefresh = !imageExists || refreshDue;
1031
941
  const buildLastRefresh = buildRefresh ? now : currentLastRefresh || 0;
1032
942
  p.log.step(refreshDue
@@ -1035,7 +945,13 @@ export async function create(args) {
1035
945
  ? 'Rebuilding stale image...'
1036
946
  : 'Building image for first use...');
1037
947
  try {
1038
- buildImage(effectiveConfig, tools, preparedDockerfile.path, expectedImageSignature, { engine, refresh: buildRefresh, lastRefresh: buildLastRefresh });
948
+ buildImage(effectiveConfig, tools, preparedDockerfile.path, expectedImageSignature, {
949
+ engine,
950
+ refresh: buildRefresh,
951
+ lastRefresh: buildLastRefresh,
952
+ buildProxyArgs: buildProxy.args,
953
+ buildEnv: buildProxy.env
954
+ });
1039
955
  p.log.success(refreshDue
1040
956
  ? 'Image refreshed'
1041
957
  : imageExists
@@ -1045,10 +961,12 @@ export async function create(args) {
1045
961
  catch (error) {
1046
962
  if (refreshDue && !signatureStale && imageExists) {
1047
963
  p.log.warn('Scheduled sandbox image refresh failed; continuing with the existing image. ' +
1048
- commandErrorMessage(error));
964
+ redactBuildProxyValues(commandErrorMessage(error), buildProxy.redactionValues)
965
+ + (values['inherit-build-proxy'] ? ` ${buildProxyFailureHint(engine)}` : ''));
1049
966
  }
1050
967
  else {
1051
- throw error;
968
+ const hint = values['inherit-build-proxy'] ? `\n${buildProxyFailureHint(engine)}` : '';
969
+ throw new Error(`${redactBuildProxyValues(commandErrorMessage(error), buildProxy.redactionValues)}${hint}`);
1052
970
  }
1053
971
  }
1054
972
  }
@@ -1063,7 +981,7 @@ export async function create(args) {
1063
981
  if (fs.readdirSync(worktree).length > 0) {
1064
982
  return `Worktree exists at ${worktree}`;
1065
983
  }
1066
- fs.rmSync(worktree, { recursive: true, force: true });
984
+ removeDirRecursive(worktree);
1067
985
  }
1068
986
  const branchExists = runOk('git', [
1069
987
  '-C',
@@ -1152,7 +1070,7 @@ export async function create(args) {
1152
1070
  runSafeEngine(engine, 'docker', ['rm', name]);
1153
1071
  }
1154
1072
  }
1155
- const aliasesFile = ensureSandboxAliasesFile(effectiveConfig.home);
1073
+ const aliasesFile = ensureSandboxAliasesFile(effectiveConfig.home, capabilityPlan.aliases);
1156
1074
  if (aliasesFile.created) {
1157
1075
  message(`Created default sandbox aliases at ${aliasesFile.path}`);
1158
1076
  }
@@ -1181,6 +1099,24 @@ export async function create(args) {
1181
1099
  let hostShellConfig;
1182
1100
  let tmpfsSeedPlan = [];
1183
1101
  try {
1102
+ const beforeCreateResults = await runSandboxHooks({
1103
+ hooks: capabilityPlan.hooksByPhase['before-container-create'],
1104
+ phase: 'before-container-create',
1105
+ context: {
1106
+ config: effectiveConfig,
1107
+ plan: capabilityPlan,
1108
+ create: {
1109
+ hostHome: effectiveConfig.home,
1110
+ resolvedTools: effectiveResolvedTools
1111
+ }
1112
+ },
1113
+ runCommand: runBoundedSandboxHookCommand
1114
+ });
1115
+ const beforeCreateFailure = beforeCreateResults.find((result) => result.status === 'fatal');
1116
+ if (beforeCreateFailure) {
1117
+ throw new Error(beforeCreateFailure.message
1118
+ ?? `Sandbox hook '${beforeCreateFailure.hookId}' failed.`);
1119
+ }
1184
1120
  const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
1185
1121
  if (claudeCodeEntry) {
1186
1122
  ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
@@ -1189,11 +1125,6 @@ export async function create(args) {
1189
1125
  // provider/API settings are enough for Claude Code. If no usable
1190
1126
  // auth was present, the claude-code entry was removed above.
1191
1127
  }
1192
- const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
1193
- if (codexEntry) {
1194
- ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home, codexEntry.tool.containerMount);
1195
- ensureCodexWorkspaceTrust(codexEntry.dir);
1196
- }
1197
1128
  const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');
1198
1129
  if (geminiEntry) {
1199
1130
  ensureGeminiWorkspaceTrust(geminiEntry.dir);
@@ -1267,6 +1198,8 @@ export async function create(args) {
1267
1198
  sandboxLabel(effectiveConfig),
1268
1199
  '--label',
1269
1200
  `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1201
+ '--label',
1202
+ `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1270
1203
  ...coreVolumes,
1271
1204
  ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1272
1205
  '-v',
@@ -1344,6 +1277,17 @@ export async function create(args) {
1344
1277
  runSafeEngine(engine, 'docker', ['exec', container, 'bash', '-lc', command]);
1345
1278
  }
1346
1279
  }
1280
+ const afterStartResults = await runSandboxHooks({
1281
+ hooks: capabilityPlan.hooksByPhase['after-container-start'],
1282
+ phase: 'after-container-start',
1283
+ context: { config: effectiveConfig, plan: capabilityPlan },
1284
+ runCommand: runBoundedSandboxHookCommand
1285
+ });
1286
+ const afterStartFailure = afterStartResults.find((result) => result.status === 'fatal');
1287
+ if (afterStartFailure) {
1288
+ throw new Error(afterStartFailure.message
1289
+ ?? `Sandbox hook '${afterStartFailure.hookId}' failed.`);
1290
+ }
1347
1291
  return 'Container started';
1348
1292
  }
1349
1293
  }
@@ -2,6 +2,7 @@ import { loadConfig } from '../config.js';
2
2
  import { assertValidBranchName, containerNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
3
3
  import { detectEngine } from '../engine.js';
4
4
  import { ensureSandboxReady } from '../recovery.js';
5
+ import { createSandboxCapabilityPlan, runBoundedSandboxHookCommand, runSandboxHooks } from '../agent-client-reconciler.js';
5
6
  import { formatCredentialWarnings, formatRemaining, hasClaudeProviderAuth, reconcileClaudeCredentials, redactCommandError, validateClaudeCredentialsEnvOverride } from '../credentials.js';
6
7
  import { runInteractiveEngine } from '../shell.js';
7
8
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
@@ -123,7 +124,22 @@ export async function enter(args) {
123
124
  });
124
125
  const container = ready.container;
125
126
  const cmd = parsed.command;
126
- if (config.tools.includes('claude-code')) {
127
+ const capabilityPlan = createSandboxCapabilityPlan(config);
128
+ const enterHookResults = await runSandboxHooks({
129
+ hooks: capabilityPlan.hooksByPhase['before-enter'],
130
+ phase: 'before-enter',
131
+ context: { config, plan: capabilityPlan },
132
+ runCommand: runBoundedSandboxHookCommand
133
+ });
134
+ for (const result of enterHookResults) {
135
+ if (result.status === 'fatal') {
136
+ throw new Error(result.message ?? `Sandbox hook '${result.hookId}' failed.`);
137
+ }
138
+ if (result.status === 'warning') {
139
+ process.stderr.write(`Warning: ${result.message ?? `Sandbox hook '${result.hookId}' did not complete.`}\n`);
140
+ }
141
+ }
142
+ if (capabilityPlan.selectedAgentClients.some((adapter) => adapter.id === 'claude-code')) {
127
143
  try {
128
144
  // Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
129
145
  const providerAuthAvailable = hasClaudeProviderAuth(config.home);
@@ -10,7 +10,7 @@ import { hostJoin } from '../engines/wsl2-paths.js';
10
10
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
11
11
  import { parseLabels } from './ls.js';
12
12
  import { runEngine, runSafe } from '../shell.js';
13
- import { resolveTools } from '../tools.js';
13
+ import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
14
14
  const USAGE = `Usage: ai sandbox prune [--dry-run]`;
15
15
  function listChildDirs(base) {
16
16
  if (!fs.existsSync(base)) {
@@ -129,7 +129,7 @@ export async function prune(args) {
129
129
  return;
130
130
  }
131
131
  const config = loadConfig();
132
- const tools = resolveTools(config);
132
+ const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
133
133
  const engine = detectEngine(config);
134
134
  const psArgs = [
135
135
  'ps',
@@ -9,15 +9,17 @@ import { runEngine, runOkEngine, runSafeEngine, runVerboseEngine } from '../shel
9
9
  import { pruneSandboxDanglingImages } from '../image-prune.js';
10
10
  import { resolveTools } from '../tools.js';
11
11
  import { buildImageSignature, buildSandboxImageArgs, parseImageLabels, parseRefreshTimestamp } from '../image-build.js';
12
- const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh]`;
13
- export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh } = {}) {
12
+ import { assertBuildProxyCompatibility, buildProxyFailureHint, prepareBuildProxy, redactBuildProxyValues } from '../build-proxy.js';
13
+ const USAGE = `Usage: ai sandbox rebuild [--quiet] [--refresh] [--inherit-build-proxy|-B]`;
14
+ export function buildArgs(config, tools, dockerfilePath, imageSignature, { engine, runFn = runEngine, runSafeFn = runSafeEngine, env = process.env, refresh = false, lastRefresh, buildProxyArgs = [] } = {}) {
14
15
  return buildSandboxImageArgs(config, tools, dockerfilePath, imageSignature, {
15
16
  engine: engine ?? detectEngine(config),
16
17
  runFn,
17
18
  runSafeFn,
18
19
  env,
19
20
  refresh,
20
- lastRefresh
21
+ lastRefresh,
22
+ buildProxyArgs
21
23
  });
22
24
  }
23
25
  function readExistingLastRefresh(config, engine) {
@@ -41,6 +43,7 @@ export async function rebuild(args) {
41
43
  options: {
42
44
  refresh: { type: 'boolean' },
43
45
  quiet: { type: 'boolean', short: 'q' },
46
+ 'inherit-build-proxy': { type: 'boolean', short: 'B' },
44
47
  help: { type: 'boolean', short: 'h' }
45
48
  }
46
49
  });
@@ -56,6 +59,12 @@ export async function rebuild(args) {
56
59
  const refresh = values.refresh ?? false;
57
60
  const engine = detectEngine(config);
58
61
  await ensureDocker(config, undefined);
62
+ if (values['inherit-build-proxy'] && config.dockerfile) {
63
+ throw new Error('Build proxy inheritance is unavailable with a custom sandbox Dockerfile.');
64
+ }
65
+ const buildProxy = prepareBuildProxy(values['inherit-build-proxy'] ?? false, process.env, engine);
66
+ if (values['inherit-build-proxy'])
67
+ assertBuildProxyCompatibility(engine);
59
68
  const lastRefresh = refresh ? Date.now() : readExistingLastRefresh(config, engine);
60
69
  p.intro(pc.cyan('Rebuilding sandbox image'));
61
70
  try {
@@ -65,9 +74,11 @@ export async function rebuild(args) {
65
74
  runEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
66
75
  engine,
67
76
  refresh,
68
- lastRefresh
77
+ lastRefresh,
78
+ buildProxyArgs: buildProxy.args
69
79
  }), {
70
- cwd: config.repoRoot
80
+ cwd: config.repoRoot,
81
+ env: buildProxy.env
71
82
  });
72
83
  spinner.stop(pc.green('Sandbox image rebuilt'));
73
84
  }
@@ -76,12 +87,18 @@ export async function rebuild(args) {
76
87
  runVerboseEngine(engine, 'docker', buildArgs(config, tools, preparedDockerfile.path, imageSignature, {
77
88
  engine,
78
89
  refresh,
79
- lastRefresh
80
- }), { cwd: config.repoRoot });
90
+ lastRefresh,
91
+ buildProxyArgs: buildProxy.args
92
+ }), { cwd: config.repoRoot, env: buildProxy.env });
81
93
  p.log.success(pc.green('Sandbox image rebuilt'));
82
94
  }
83
95
  pruneSandboxDanglingImages(config, engine);
84
96
  }
97
+ catch (error) {
98
+ const message = redactBuildProxyValues(error instanceof Error ? error.message : String(error), buildProxy.redactionValues);
99
+ const hint = values['inherit-build-proxy'] ? `\n${buildProxyFailureHint(engine)}` : '';
100
+ throw new Error(`${message}${hint}`);
101
+ }
85
102
  finally {
86
103
  preparedDockerfile.cleanup();
87
104
  }