@fitlab-ai/agent-infra 0.9.1 → 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 (283) hide show
  1. package/README.md +16 -4
  2. package/README.zh-CN.md +16 -4
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +15 -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/release-workflow.js +71 -16
  31. package/dist/lib/internal/task-orchestration.js +151 -0
  32. package/dist/lib/internal/task-review.js +65 -0
  33. package/dist/lib/merge.js +2 -1
  34. package/dist/lib/platform/adapters.js +38 -1
  35. package/dist/lib/platform/change-request-git-evidence.js +113 -0
  36. package/dist/lib/platform/merged-pr-equivalence.js +41 -11
  37. package/dist/lib/platform/pr-checks.js +38 -10
  38. package/dist/lib/platform/pull-requests.js +110 -6
  39. package/dist/lib/platform/releases.js +1 -1
  40. package/dist/lib/platform/verification-required.js +8 -10
  41. package/dist/lib/prompt.js +15 -3
  42. package/dist/lib/release/channels.js +4 -1
  43. package/dist/lib/release/workflow.js +4 -2
  44. package/dist/lib/remove-dir.js +25 -0
  45. package/dist/lib/run/tui.js +8 -2
  46. package/dist/lib/sandbox/agent-client-reconciler.js +234 -0
  47. package/dist/lib/sandbox/clipboard/linux.js +2 -1
  48. package/dist/lib/sandbox/commands/create.js +76 -180
  49. package/dist/lib/sandbox/commands/enter.js +17 -1
  50. package/dist/lib/sandbox/commands/prune.js +2 -2
  51. package/dist/lib/sandbox/commands/rm.js +3 -2
  52. package/dist/lib/sandbox/config.js +21 -4
  53. package/dist/lib/sandbox/constants.js +3 -0
  54. package/dist/lib/sandbox/dockerfile.js +2 -1
  55. package/dist/lib/sandbox/managed-fs.js +10 -5
  56. package/dist/lib/sandbox/recovery.js +52 -36
  57. package/dist/lib/sandbox/shell.js +1 -1
  58. package/dist/lib/sandbox/tool-types.js +8 -0
  59. package/dist/lib/sandbox/tools.js +2 -126
  60. package/dist/lib/task/artifact-lifecycle.js +1 -49
  61. package/dist/lib/task/commands/decisions.js +50 -20
  62. package/dist/lib/task/delegation-receipts.js +107 -0
  63. package/dist/lib/task/events.js +100 -1
  64. package/dist/lib/task/orchestration.js +420 -0
  65. package/dist/lib/task/review-artifacts.js +143 -59
  66. package/dist/lib/task/review-finalization.js +166 -0
  67. package/dist/lib/task/review-fingerprint.js +4 -0
  68. package/dist/lib/task/verification-checks.js +2 -1
  69. package/dist/lib/task/verification-engine.js +156 -28
  70. package/dist/lib/task/verification-review.js +1 -1
  71. package/dist/lib/task/verification.js +3 -1
  72. package/dist/lib/task/workspace-snapshot.js +34 -0
  73. package/dist/lib/update.js +42 -161
  74. package/lib/agent-client.ts +214 -0
  75. package/lib/agent-clients/adapter.ts +461 -0
  76. package/lib/agent-clients/adapters/claude-code.ts +70 -0
  77. package/lib/agent-clients/adapters/codex-sandbox.ts +245 -0
  78. package/lib/agent-clients/adapters/codex.ts +58 -0
  79. package/lib/agent-clients/adapters/gemini-cli.ts +65 -0
  80. package/lib/agent-clients/adapters/index.ts +27 -0
  81. package/lib/agent-clients/adapters/opencode.ts +66 -0
  82. package/lib/agent-clients/config.ts +278 -0
  83. package/lib/agent-clients/custom-tuis.ts +149 -0
  84. package/lib/agent-clients/invocation.ts +19 -0
  85. package/lib/agent-clients/next-steps.ts +72 -0
  86. package/lib/agent-clients/project-assets.ts +96 -0
  87. package/lib/agent-clients/reconcile.ts +324 -0
  88. package/lib/agent-clients/registry.ts +144 -0
  89. package/lib/agent-clients/schema.ts +62 -0
  90. package/lib/agent-clients/types.ts +78 -0
  91. package/lib/benchmark/schemas/v1.0.0/case-manifest.schema.json +120 -0
  92. package/lib/benchmark/schemas/v1.0.0/common.schema.json +109 -0
  93. package/lib/benchmark/schemas/v1.0.0/grader-result.schema.json +160 -0
  94. package/lib/benchmark/schemas/v1.0.0/run-manifest.schema.json +122 -0
  95. package/lib/benchmark/schemas/v1.0.0/subject.schema.json +227 -0
  96. package/lib/builtin-tuis.ts +16 -15
  97. package/lib/defaults.json +24 -12
  98. package/lib/git/review-snapshot.ts +39 -9
  99. package/lib/git/reviewed-commit-equivalence.ts +135 -0
  100. package/lib/init.ts +53 -121
  101. package/lib/internal/agent-client.ts +131 -0
  102. package/lib/internal/git-workflow.ts +1 -1
  103. package/lib/internal/release-workflow.ts +68 -16
  104. package/lib/internal/task-orchestration.ts +132 -0
  105. package/lib/internal/task-review.ts +65 -0
  106. package/lib/merge.ts +2 -1
  107. package/lib/platform/adapters.ts +142 -3
  108. package/lib/platform/change-request-git-evidence.ts +159 -0
  109. package/lib/platform/merged-pr-equivalence.ts +51 -14
  110. package/lib/platform/pr-checks.ts +50 -22
  111. package/lib/platform/pull-requests.ts +134 -25
  112. package/lib/platform/releases.ts +1 -1
  113. package/lib/platform/verification-required.ts +12 -10
  114. package/lib/prompt.ts +16 -3
  115. package/lib/release/channels.ts +4 -1
  116. package/lib/release/workflow.ts +4 -3
  117. package/lib/remove-dir.ts +28 -0
  118. package/lib/run/tui.ts +9 -1
  119. package/lib/sandbox/agent-client-reconciler.ts +378 -0
  120. package/lib/sandbox/clipboard/linux.ts +2 -1
  121. package/lib/sandbox/commands/create.ts +111 -192
  122. package/lib/sandbox/commands/enter.ts +23 -1
  123. package/lib/sandbox/commands/prune.ts +2 -2
  124. package/lib/sandbox/commands/rm.ts +3 -2
  125. package/lib/sandbox/config.ts +31 -4
  126. package/lib/sandbox/constants.ts +6 -0
  127. package/lib/sandbox/dockerfile.ts +2 -1
  128. package/lib/sandbox/managed-fs.ts +18 -5
  129. package/lib/sandbox/recovery.ts +98 -67
  130. package/lib/sandbox/shell.ts +1 -1
  131. package/lib/sandbox/tool-types.ts +143 -0
  132. package/lib/sandbox/tools.ts +5 -157
  133. package/lib/task/artifact-lifecycle.ts +1 -38
  134. package/lib/task/commands/decisions.ts +53 -20
  135. package/lib/task/delegation-receipts.ts +170 -0
  136. package/lib/task/events.ts +110 -2
  137. package/lib/task/orchestration.ts +543 -0
  138. package/lib/task/review-artifacts.ts +201 -69
  139. package/lib/task/review-finalization.ts +275 -0
  140. package/lib/task/review-fingerprint.ts +5 -0
  141. package/lib/task/verification-checks.ts +2 -1
  142. package/lib/task/verification-engine.ts +177 -28
  143. package/lib/task/verification-review.ts +1 -0
  144. package/lib/task/verification.ts +5 -2
  145. package/lib/task/workspace-snapshot.ts +36 -0
  146. package/lib/update.ts +48 -184
  147. package/package.json +2 -2
  148. package/templates/.agents/README.en.md +21 -20
  149. package/templates/.agents/README.zh-CN.md +21 -20
  150. package/templates/.agents/hooks/lifecycle-delegation.js +64 -0
  151. package/templates/.agents/rules/README.en.md +2 -0
  152. package/templates/.agents/rules/README.zh-CN.md +2 -0
  153. package/templates/.agents/rules/commit-and-pr.en.md +1 -0
  154. package/templates/.agents/rules/commit-and-pr.zh-CN.md +1 -0
  155. package/templates/.agents/rules/human-decision-context.en.md +14 -12
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +14 -12
  157. package/templates/.agents/rules/lifecycle-orchestration.en.md +21 -0
  158. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +21 -0
  159. package/templates/.agents/rules/next-step-output.en.md +16 -0
  160. package/templates/.agents/rules/next-step-output.zh-CN.md +16 -0
  161. package/templates/.agents/rules/no-mid-flow-questions.en.md +2 -0
  162. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +2 -0
  163. package/templates/.agents/rules/release-commands.github.en.md +11 -5
  164. package/templates/.agents/rules/release-commands.github.zh-CN.md +11 -5
  165. package/templates/.agents/rules/review-handshake.en.md +4 -4
  166. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  167. package/templates/.agents/rules/review-method.en.md +113 -0
  168. package/templates/.agents/rules/review-method.zh-CN.md +98 -0
  169. package/templates/.agents/skills/analyze-task/SKILL.en.md +5 -5
  170. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +5 -5
  171. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  172. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +4 -4
  173. package/templates/.agents/skills/cancel-task/SKILL.en.md +4 -4
  174. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +5 -5
  175. package/templates/.agents/skills/check-task/SKILL.en.md +16 -16
  176. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -16
  177. package/templates/.agents/skills/close-codescan/SKILL.en.md +4 -4
  178. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +4 -4
  179. package/templates/.agents/skills/close-dependabot/SKILL.en.md +4 -4
  180. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +4 -4
  181. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  182. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  183. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +14 -16
  184. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +14 -16
  185. package/templates/.agents/skills/code-task/reference/output-template.en.md +3 -3
  186. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +3 -3
  187. package/templates/.agents/skills/commit/SKILL.en.md +11 -1
  188. package/templates/.agents/skills/commit/SKILL.zh-CN.md +11 -1
  189. package/templates/.agents/skills/commit/reference/task-status-update.en.md +24 -28
  190. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +24 -28
  191. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  192. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  193. package/templates/.agents/skills/create-pr/SKILL.en.md +10 -10
  194. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +10 -10
  195. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +3 -3
  196. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +3 -3
  197. package/templates/.agents/skills/create-task/SKILL.en.md +11 -11
  198. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +11 -11
  199. package/templates/.agents/skills/import-codescan/SKILL.en.md +5 -5
  200. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +5 -5
  201. package/templates/.agents/skills/import-dependabot/SKILL.en.md +5 -5
  202. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +5 -5
  203. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  204. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  205. package/templates/.agents/skills/init-labels/SKILL.en.md +4 -4
  206. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +4 -4
  207. package/templates/.agents/skills/init-milestones/SKILL.en.md +4 -4
  208. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +4 -4
  209. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  210. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  211. package/templates/.agents/skills/refine-title/SKILL.en.md +1 -1
  212. package/templates/.agents/skills/refine-title/SKILL.zh-CN.md +1 -1
  213. package/templates/.agents/skills/release/SKILL.en.md +1 -1
  214. package/templates/.agents/skills/review-analysis/SKILL.en.md +13 -2
  215. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -3
  216. package/templates/.agents/skills/review-analysis/config/verify.en.json +6 -0
  217. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +6 -0
  218. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +13 -15
  219. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +13 -15
  220. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +51 -5
  221. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +52 -6
  222. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +25 -22
  223. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +25 -22
  224. package/templates/.agents/skills/review-code/SKILL.en.md +28 -8
  225. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +28 -8
  226. package/templates/.agents/skills/review-code/config/verify.en.json +7 -0
  227. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +7 -0
  228. package/templates/.agents/skills/review-code/reference/concurrency-risks.en.md +25 -0
  229. package/templates/.agents/skills/review-code/reference/concurrency-risks.zh-CN.md +25 -0
  230. package/templates/.agents/skills/review-code/reference/cross-platform-risks.en.md +27 -0
  231. package/templates/.agents/skills/review-code/reference/cross-platform-risks.zh-CN.md +27 -0
  232. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.en.md +13 -0
  233. package/templates/.agents/skills/review-code/reference/documentation-antipatterns.zh-CN.md +13 -0
  234. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +25 -0
  235. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +25 -0
  236. package/templates/.agents/skills/review-code/reference/output-templates.en.md +22 -24
  237. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +22 -24
  238. package/templates/.agents/skills/review-code/reference/report-template.en.md +54 -7
  239. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +54 -7
  240. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +29 -24
  241. package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +29 -24
  242. package/templates/.agents/skills/review-code/reference/security-risks.en.md +25 -0
  243. package/templates/.agents/skills/review-code/reference/security-risks.zh-CN.md +25 -0
  244. package/templates/.agents/skills/review-plan/SKILL.en.md +13 -2
  245. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -3
  246. package/templates/.agents/skills/review-plan/config/verify.en.json +6 -0
  247. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +6 -0
  248. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +13 -15
  249. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +13 -15
  250. package/templates/.agents/skills/review-plan/reference/report-template.en.md +61 -5
  251. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +61 -5
  252. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +21 -24
  253. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +21 -24
  254. package/templates/.agents/skills/run-task/SKILL.en.md +22 -0
  255. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +22 -0
  256. package/templates/.agents/skills/run-task/config/verify.en.json +9 -0
  257. package/templates/.agents/skills/run-task/config/verify.zh-CN.json +9 -0
  258. package/templates/.agents/skills/test/SKILL.en.md +4 -4
  259. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -4
  260. package/templates/.agents/skills/test-integration/SKILL.en.md +4 -4
  261. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +4 -4
  262. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -5
  263. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -5
  264. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +647 -203
  265. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +4 -4
  266. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +4 -4
  267. package/templates/.agents/skills/watch-pr/SKILL.en.md +8 -8
  268. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +8 -8
  269. package/templates/.claude/agents/agent-infra-lifecycle-executor.en.md +6 -0
  270. package/templates/.claude/agents/agent-infra-lifecycle-executor.zh-CN.md +6 -0
  271. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.en.md +6 -0
  272. package/templates/.claude/agents/agent-infra-lifecycle-reviewer.zh-CN.md +6 -0
  273. package/templates/.claude/commands/run-task.en.md +8 -0
  274. package/templates/.claude/commands/run-task.zh-CN.md +8 -0
  275. package/templates/.claude/settings.json +24 -0
  276. package/templates/.codex/agents/agent-infra-lifecycle-executor.en.toml +3 -0
  277. package/templates/.codex/agents/agent-infra-lifecycle-executor.zh-CN.toml +3 -0
  278. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.en.toml +3 -0
  279. package/templates/.codex/agents/agent-infra-lifecycle-reviewer.zh-CN.toml +3 -0
  280. package/templates/.gemini/commands/_project_/run-task.en.toml +8 -0
  281. package/templates/.gemini/commands/_project_/run-task.zh-CN.toml +8 -0
  282. package/templates/.opencode/commands/run-task.en.md +11 -0
  283. package/templates/.opencode/commands/run-task.zh-CN.md +11 -0
@@ -7,7 +7,8 @@ import type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
7
7
  import { parseArgs } from 'node:util';
8
8
  import * as p from '@clack/prompts';
9
9
  import pc from 'picocolors';
10
- import * as toml from 'smol-toml';
10
+ import { listAgentClientAdapters } from '../../agent-clients/registry.ts';
11
+ import type { SandboxAlias } from '../tool-types.ts';
11
12
  import { loadConfig } from '../config.ts';
12
13
  import {
13
14
  assertValidBranchName,
@@ -18,6 +19,7 @@ import {
18
19
  sandboxImageConfigLabel,
19
20
  sandboxImageRefreshLabel,
20
21
  sandboxLabel,
22
+ sandboxRuntimeCapabilityLabel,
21
23
  shareBranchDir,
22
24
  shareCommonDir,
23
25
  shellConfigDir,
@@ -43,6 +45,11 @@ import {
43
45
  runVerboseEngine
44
46
  } from '../shell.ts';
45
47
  import { resolveTaskBranch } from '../task-resolver.ts';
48
+ import {
49
+ createSandboxCapabilityPlan,
50
+ runSandboxHooks,
51
+ runBoundedSandboxHookCommand
52
+ } from '../agent-client-reconciler.ts';
46
53
  import {
47
54
  resolveTools,
48
55
  tmpfsSeedStagingPath,
@@ -61,6 +68,7 @@ import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.
61
68
  import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
62
69
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.ts';
63
70
  import { ensureSandboxDiscoveryReadmes } from '../readme-scaffold.ts';
71
+ import { removeDirRecursive } from '../../remove-dir.ts';
64
72
  import {
65
73
  prepareClaudeCredentials,
66
74
  redactCommandError,
@@ -76,29 +84,13 @@ import {
76
84
  parseRefreshTimestamp
77
85
  } from '../image-build.ts';
78
86
 
79
- const OPENCODE_YOLO_PERMISSION = '{"*":"allow","read":"allow","bash":"allow","edit":"allow","webfetch":"allow","external_directory":"allow","doom_loop":"allow"}';
87
+ export {
88
+ ensureCodexModelInheritance,
89
+ ensureCodexWorkspaceTrust
90
+ } from '../../agent-clients/adapters/codex-sandbox.ts';
91
+
80
92
  const SANDBOX_ALIAS_BLOCK_BEGIN = '# >>> agent-infra managed aliases >>>';
81
93
  const SANDBOX_ALIAS_BLOCK_END = '# <<< agent-infra managed aliases <<<';
82
- const SANDBOX_ALIAS_NAMES = [
83
- 'claude-yolo',
84
- 'opencode-yolo',
85
- 'codex-yolo',
86
- 'gemini-yolo',
87
- 'cy',
88
- 'oy',
89
- 'xy',
90
- 'gy'
91
- ];
92
- const DEFAULT_SANDBOX_ALIASES = `alias claude-yolo='claude --dangerously-skip-permissions; tput ed'
93
- alias opencode-yolo='OPENCODE_PERMISSION='\\''${OPENCODE_YOLO_PERMISSION}'\\'' opencode; tput ed'
94
- alias codex-yolo='codex --yolo; tput ed'
95
- alias gemini-yolo='gemini --yolo; tput ed'
96
-
97
- alias cy='claude --dangerously-skip-permissions; tput ed'
98
- alias oy='OPENCODE_PERMISSION='\\''${OPENCODE_YOLO_PERMISSION}'\\'' opencode; tput ed'
99
- alias xy='codex --yolo; tput ed'
100
- alias gy='gemini --yolo; tput ed'
101
- `;
102
94
  const CONTAINER_HOME = '/home/devuser';
103
95
  const CONTAINER_SHELL_CONFIG_MOUNT = `${CONTAINER_HOME}/.host-shell-config`;
104
96
  const USAGE = `Usage: ai sandbox create <branch> [base] [--cpu <n>] [--memory <n>] [--no-refresh] [--inherit-proxy|-P] [--inherit-build-proxy|-B]
@@ -410,7 +402,7 @@ export function prepareHostShellConfig({
410
402
  repoRoot: string;
411
403
  }): HostShellConfig {
412
404
  const hostDir = hostShellConfigDir(home, project, branch);
413
- fs.rmSync(hostDir, { recursive: true, force: true });
405
+ removeDirRecursive(hostDir);
414
406
  fs.mkdirSync(hostDir, { recursive: true });
415
407
 
416
408
  writeSanitizedGitconfig({
@@ -459,10 +451,19 @@ function normalizeWorktreePath(worktreePath: string): string {
459
451
  return '';
460
452
  }
461
453
 
454
+ const wslMount = process.platform === 'win32'
455
+ ? worktreePath.match(/^\/mnt\/([A-Za-z])(?:\/(.*))?$/)
456
+ : null;
457
+ const hostPath = wslMount
458
+ ? `${wslMount[1]!.toUpperCase()}:\\${(wslMount[2] ?? '').replace(/\//g, '\\')}`
459
+ : worktreePath;
460
+
462
461
  try {
463
- return fs.existsSync(worktreePath) ? fs.realpathSync(worktreePath) : path.resolve(worktreePath);
462
+ const normalized = fs.existsSync(hostPath) ? fs.realpathSync(hostPath) : path.resolve(hostPath);
463
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
464
464
  } catch {
465
- return path.resolve(worktreePath);
465
+ const normalized = path.resolve(hostPath);
466
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
466
467
  }
467
468
  }
468
469
 
@@ -714,7 +715,7 @@ export function buildContainerEnvFile(
714
715
  mkdtempFn = fs.mkdtempSync,
715
716
  writeFileFn = fs.writeFileSync,
716
717
  chmodFn = fs.chmodSync,
717
- rmFn = fs.rmSync,
718
+ rmFn = removeDirRecursive as typeof fs.rmSync,
718
719
  tmpDir = os.tmpdir(),
719
720
  runSafeFn = runSafe
720
721
  } = options;
@@ -971,154 +972,6 @@ export function ensureClaudeSettings(toolDir: string, hostHomeDir?: string): voi
971
972
  }
972
973
  }
973
974
 
974
- function resolveHostCatalogPath(value: unknown, hostHomeDir: string): string | null {
975
- if (typeof value !== 'string' || value === '') {
976
- return null;
977
- }
978
- let resolved: string;
979
- if (value === '~' || value.startsWith('~/') || value.startsWith('~\\')) {
980
- resolved = path.join(hostHomeDir, value.slice(1).replace(/^[/\\]+/, ''));
981
- } else if (path.isAbsolute(value)) {
982
- resolved = value;
983
- } else {
984
- resolved = path.join(hostHomeDir, '.codex', value);
985
- }
986
- try {
987
- if (!fs.statSync(resolved).isFile()) {
988
- return null;
989
- }
990
- fs.accessSync(resolved, fs.constants.R_OK);
991
- return resolved;
992
- } catch {
993
- return null;
994
- }
995
- }
996
-
997
- const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'] as const;
998
-
999
- function removeCodexMcpServers(sandboxParsed: JsonObject): boolean {
1000
- if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
1001
- return false;
1002
- }
1003
- delete sandboxParsed.mcp_servers;
1004
- return true;
1005
- }
1006
-
1007
- function inheritDisabledCodexFeatureFlags(sandboxParsed: JsonObject, hostParsed: JsonObject): boolean {
1008
- if (!isJsonObjectRecord(hostParsed.features)) {
1009
- return false;
1010
- }
1011
- if (Object.hasOwn(sandboxParsed, 'features') && !isJsonObjectRecord(sandboxParsed.features)) {
1012
- return false;
1013
- }
1014
-
1015
- let sandboxFeatures = sandboxParsed.features as JsonObject | undefined;
1016
- let changed = false;
1017
- for (const key of CODEX_DISABLED_FEATURE_FLAGS) {
1018
- if (hostParsed.features[key] !== false) {
1019
- continue;
1020
- }
1021
- if (!sandboxFeatures) {
1022
- sandboxFeatures = {};
1023
- sandboxParsed.features = sandboxFeatures;
1024
- }
1025
- if (sandboxFeatures[key] !== false) {
1026
- sandboxFeatures[key] = false;
1027
- changed = true;
1028
- }
1029
- }
1030
- return changed;
1031
- }
1032
-
1033
- export function ensureCodexModelInheritance(
1034
- toolDir: string,
1035
- hostHomeDir?: string,
1036
- containerCodexDir: string = '/home/devuser/.codex'
1037
- ): void {
1038
- if (!hostHomeDir) {
1039
- return;
1040
- }
1041
-
1042
- const hostConfigPath = path.join(hostHomeDir, '.codex', 'config.toml');
1043
- if (!fs.existsSync(hostConfigPath)) {
1044
- return;
1045
- }
1046
-
1047
- let hostParsed: JsonObject;
1048
- try {
1049
- hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8')) as JsonObject;
1050
- } catch {
1051
- return;
1052
- }
1053
-
1054
- const sandboxConfigPath = path.join(toolDir, 'config.toml');
1055
- // This rewrites sandbox-side TOML and drops comments; the host config stays untouched.
1056
- let sandboxParsed: JsonObject = {};
1057
- if (fs.existsSync(sandboxConfigPath)) {
1058
- try {
1059
- sandboxParsed = toml.parse(fs.readFileSync(sandboxConfigPath, 'utf8')) as JsonObject;
1060
- } catch {
1061
- return;
1062
- }
1063
- }
1064
-
1065
- let changed = removeCodexMcpServers(sandboxParsed);
1066
- changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
1067
-
1068
- const inheritSpecs: Array<readonly [string, 'string' | 'number']> = [
1069
- ['model', 'string'],
1070
- ['model_reasoning_effort', 'string'],
1071
- ['model_auto_compact_token_limit', 'number']
1072
- ];
1073
-
1074
- for (const [key, type] of inheritSpecs) {
1075
- if (Object.hasOwn(sandboxParsed, key)) {
1076
- continue;
1077
- }
1078
- const value = hostParsed[key];
1079
- if (type === 'string' && (typeof value !== 'string' || value === '')) {
1080
- continue;
1081
- }
1082
- if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value) || value <= 0)) {
1083
- continue;
1084
- }
1085
- sandboxParsed[key] = value;
1086
- changed = true;
1087
- }
1088
-
1089
- if (!Object.hasOwn(sandboxParsed, 'model_catalog_json')) {
1090
- const hostCatalogPath = resolveHostCatalogPath(hostParsed['model_catalog_json'], hostHomeDir);
1091
- if (hostCatalogPath) {
1092
- try {
1093
- const basename = path.basename(hostCatalogPath);
1094
- const destDir = path.join(toolDir, 'model-catalogs');
1095
- fs.mkdirSync(destDir, { recursive: true });
1096
- fs.copyFileSync(hostCatalogPath, path.join(destDir, basename));
1097
- sandboxParsed['model_catalog_json'] = path.posix.join(containerCodexDir, 'model-catalogs', basename);
1098
- changed = true;
1099
- } catch {
1100
- // Copy failed (e.g. permissions): skip catalog, keep scalar inheritance intact.
1101
- }
1102
- }
1103
- }
1104
-
1105
- if (changed) {
1106
- fs.writeFileSync(sandboxConfigPath, `${toml.stringify(sandboxParsed)}\n`, 'utf8');
1107
- }
1108
- }
1109
-
1110
- export function ensureCodexWorkspaceTrust(toolDir: string): void {
1111
- const configPath = path.join(toolDir, 'config.toml');
1112
- let content = '';
1113
- if (fs.existsSync(configPath)) {
1114
- content = fs.readFileSync(configPath, 'utf8');
1115
- }
1116
- if (!content.includes('[projects."/workspace"]')) {
1117
- const entry = '\n[projects."/workspace"]\ntrust_level = "trusted"\n';
1118
- fs.writeFileSync(configPath, content + entry, 'utf8');
1119
- }
1120
- }
1121
-
1122
975
  export function ensureOpenCodeModelInheritance(toolDir: string, hostHomeDir?: string): void {
1123
976
  if (!hostHomeDir) {
1124
977
  return;
@@ -1189,7 +1042,10 @@ function stripManagedSandboxAliasBlocks(content: string): string {
1189
1042
  return content.replace(blockPattern, '').trimEnd();
1190
1043
  }
1191
1044
 
1192
- function isLegacyManagedSandboxAliasFile(content: string): boolean {
1045
+ function isLegacyManagedSandboxAliasFile(
1046
+ content: string,
1047
+ aliases: readonly SandboxAlias[]
1048
+ ): boolean {
1193
1049
  const lines = content
1194
1050
  .split(/\r?\n/)
1195
1051
  .map((line) => line.trim())
@@ -1199,13 +1055,29 @@ function isLegacyManagedSandboxAliasFile(content: string): boolean {
1199
1055
  return false;
1200
1056
  }
1201
1057
 
1202
- const aliasPattern = new RegExp(`^alias (${SANDBOX_ALIAS_NAMES.map(escapeRegExp).join('|')})=`);
1058
+ const aliasPattern = new RegExp(
1059
+ `^alias (${aliases.map((alias) => escapeRegExp(alias.name)).join('|')})=`
1060
+ );
1203
1061
  return lines.every((line) => aliasPattern.test(line));
1204
1062
  }
1205
1063
 
1206
- export function ensureSandboxAliasesFile(home: string): { created: boolean; path: string } {
1064
+ function registrySandboxAliases(): readonly SandboxAlias[] {
1065
+ return listAgentClientAdapters().flatMap((adapter) => adapter.sandbox.aliases);
1066
+ }
1067
+
1068
+ function renderSandboxAliases(aliases: readonly SandboxAlias[]): string {
1069
+ return `${aliases.map((alias) =>
1070
+ `alias ${alias.name}='${alias.command.replaceAll("'", "'\\''")}'`
1071
+ ).join('\n')}\n`;
1072
+ }
1073
+
1074
+ export function ensureSandboxAliasesFile(
1075
+ home: string,
1076
+ aliases: readonly SandboxAlias[] = registrySandboxAliases()
1077
+ ): { created: boolean; path: string } {
1207
1078
  const aliasesPath = sandboxAliasesPath(home);
1208
- const managedBlock = `${SANDBOX_ALIAS_BLOCK_BEGIN}\n${DEFAULT_SANDBOX_ALIASES}${SANDBOX_ALIAS_BLOCK_END}\n`;
1079
+ const managedBlock =
1080
+ `${SANDBOX_ALIAS_BLOCK_BEGIN}\n${renderSandboxAliases(aliases)}${SANDBOX_ALIAS_BLOCK_END}\n`;
1209
1081
  fs.mkdirSync(path.dirname(aliasesPath), { recursive: true });
1210
1082
  const created = !fs.existsSync(aliasesPath);
1211
1083
  let existing = '';
@@ -1214,7 +1086,7 @@ export function ensureSandboxAliasesFile(home: string): { created: boolean; path
1214
1086
  existing = fs.readFileSync(aliasesPath, 'utf8');
1215
1087
  }
1216
1088
 
1217
- const userContent = isLegacyManagedSandboxAliasFile(existing)
1089
+ const userContent = isLegacyManagedSandboxAliasFile(existing, aliases)
1218
1090
  ? ''
1219
1091
  : stripManagedSandboxAliasBlocks(existing);
1220
1092
  const nextContent = userContent
@@ -1355,19 +1227,18 @@ export async function create(args: string[]): Promise<void> {
1355
1227
  };
1356
1228
  const worktreeCandidates = worktreeDirCandidates(effectiveConfig, branch);
1357
1229
  assertBranchAvailable(config.repoRoot, branch, { allowedWorktrees: worktreeCandidates });
1358
- const tools = resolveTools(effectiveConfig);
1230
+ const capabilityPlan = createSandboxCapabilityPlan(effectiveConfig);
1231
+ const tools = [...capabilityPlan.tools];
1359
1232
  const resolvedTools = resolveToolDirs(effectiveConfig, tools, branch);
1360
1233
  // Fatal credential states still fail before filesystem/docker side effects.
1361
- // A genuinely missing Claude Code credential only removes Claude Code's
1362
- // sandbox config and credential mounts for this create run.
1234
+ // Missing credentials leave the selected Claude Code capability installed
1235
+ // and mounted; only the absent live credential file is omitted.
1363
1236
  const credentialOutcome = prepareClaudeCredentials(
1364
1237
  effectiveConfig.home,
1365
1238
  effectiveConfig.project,
1366
1239
  resolvedTools
1367
1240
  );
1368
- const effectiveResolvedTools = credentialOutcome.status === 'SKIPPED'
1369
- ? resolvedTools.filter(({ tool }) => tool.id !== 'claude-code')
1370
- : resolvedTools;
1241
+ const effectiveResolvedTools = resolvedTools;
1371
1242
  const container = containerName(effectiveConfig, branch);
1372
1243
  const worktree = worktreeCandidates.find((candidate) => fs.existsSync(candidate)) ?? worktreeCandidates[0] ?? '';
1373
1244
  const shareCommon = shareCommonDir(effectiveConfig);
@@ -1392,6 +1263,16 @@ export async function create(args: string[]): Promise<void> {
1392
1263
 
1393
1264
  try {
1394
1265
  p.log.step('Checking container engine...');
1266
+ const prepareHookResults = await runSandboxHooks({
1267
+ hooks: capabilityPlan.hooksByPhase.prepare,
1268
+ phase: 'prepare',
1269
+ context: { config: effectiveConfig, plan: capabilityPlan },
1270
+ runCommand: runBoundedSandboxHookCommand
1271
+ });
1272
+ const prepareFailure = prepareHookResults.find((result) => result.status === 'fatal');
1273
+ if (prepareFailure) {
1274
+ throw new Error(prepareFailure.message ?? `Sandbox hook '${prepareFailure.hookId}' failed.`);
1275
+ }
1395
1276
  await ensureDocker(effectiveConfig, (detail: string) => {
1396
1277
  p.log.info(` ${detail}`);
1397
1278
  });
@@ -1476,7 +1357,7 @@ export async function create(args: string[]): Promise<void> {
1476
1357
  if (fs.readdirSync(worktree).length > 0) {
1477
1358
  return `Worktree exists at ${worktree}`;
1478
1359
  }
1479
- fs.rmSync(worktree, { recursive: true, force: true });
1360
+ removeDirRecursive(worktree);
1480
1361
  }
1481
1362
 
1482
1363
  const branchExists = runOk('git', [
@@ -1573,7 +1454,10 @@ export async function create(args: string[]): Promise<void> {
1573
1454
  }
1574
1455
  }
1575
1456
 
1576
- const aliasesFile = ensureSandboxAliasesFile(effectiveConfig.home);
1457
+ const aliasesFile = ensureSandboxAliasesFile(
1458
+ effectiveConfig.home,
1459
+ capabilityPlan.aliases
1460
+ );
1577
1461
  if (aliasesFile.created) {
1578
1462
  message(`Created default sandbox aliases at ${aliasesFile.path}`);
1579
1463
  }
@@ -1609,6 +1493,28 @@ export async function create(args: string[]): Promise<void> {
1609
1493
  let hostShellConfig: HostShellConfig;
1610
1494
  let tmpfsSeedPlan: TmpfsSeedPlanEntry[] = [];
1611
1495
  try {
1496
+ const beforeCreateResults = await runSandboxHooks({
1497
+ hooks: capabilityPlan.hooksByPhase['before-container-create'],
1498
+ phase: 'before-container-create',
1499
+ context: {
1500
+ config: effectiveConfig,
1501
+ plan: capabilityPlan,
1502
+ create: {
1503
+ hostHome: effectiveConfig.home,
1504
+ resolvedTools: effectiveResolvedTools
1505
+ }
1506
+ },
1507
+ runCommand: runBoundedSandboxHookCommand
1508
+ });
1509
+ const beforeCreateFailure = beforeCreateResults.find(
1510
+ (result) => result.status === 'fatal'
1511
+ );
1512
+ if (beforeCreateFailure) {
1513
+ throw new Error(
1514
+ beforeCreateFailure.message
1515
+ ?? `Sandbox hook '${beforeCreateFailure.hookId}' failed.`
1516
+ );
1517
+ }
1612
1518
  const claudeCodeEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'claude-code');
1613
1519
  if (claudeCodeEntry) {
1614
1520
  ensureClaudeOnboarding(claudeCodeEntry.dir, effectiveConfig.home);
@@ -1617,11 +1523,6 @@ export async function create(args: string[]): Promise<void> {
1617
1523
  // provider/API settings are enough for Claude Code. If no usable
1618
1524
  // auth was present, the claude-code entry was removed above.
1619
1525
  }
1620
- const codexEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'codex');
1621
- if (codexEntry) {
1622
- ensureCodexModelInheritance(codexEntry.dir, effectiveConfig.home, codexEntry.tool.containerMount);
1623
- ensureCodexWorkspaceTrust(codexEntry.dir);
1624
- }
1625
1526
  const geminiEntry = effectiveResolvedTools.find(({ tool }) => tool.id === 'gemini-cli');
1626
1527
  if (geminiEntry) {
1627
1528
  ensureGeminiWorkspaceTrust(geminiEntry.dir);
@@ -1709,6 +1610,8 @@ export async function create(args: string[]): Promise<void> {
1709
1610
  sandboxLabel(effectiveConfig),
1710
1611
  '--label',
1711
1612
  `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1613
+ '--label',
1614
+ `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1712
1615
  ...coreVolumes,
1713
1616
  ...buildClipboardVolumeArgs(engine, effectiveConfig.home),
1714
1617
  '-v',
@@ -1805,6 +1708,22 @@ export async function create(args: string[]): Promise<void> {
1805
1708
  }
1806
1709
  }
1807
1710
 
1711
+ const afterStartResults = await runSandboxHooks({
1712
+ hooks: capabilityPlan.hooksByPhase['after-container-start'],
1713
+ phase: 'after-container-start',
1714
+ context: { config: effectiveConfig, plan: capabilityPlan },
1715
+ runCommand: runBoundedSandboxHookCommand
1716
+ });
1717
+ const afterStartFailure = afterStartResults.find(
1718
+ (result) => result.status === 'fatal'
1719
+ );
1720
+ if (afterStartFailure) {
1721
+ throw new Error(
1722
+ afterStartFailure.message
1723
+ ?? `Sandbox hook '${afterStartFailure.hookId}' failed.`
1724
+ );
1725
+ }
1726
+
1808
1727
  return 'Container started';
1809
1728
  }
1810
1729
  }
@@ -7,6 +7,11 @@ import {
7
7
  } from '../constants.ts';
8
8
  import { detectEngine } from '../engine.ts';
9
9
  import { ensureSandboxReady } from '../recovery.ts';
10
+ import {
11
+ createSandboxCapabilityPlan,
12
+ runBoundedSandboxHookCommand,
13
+ runSandboxHooks
14
+ } from '../agent-client-reconciler.ts';
10
15
  import {
11
16
  formatCredentialWarnings,
12
17
  formatRemaining,
@@ -186,8 +191,25 @@ export async function enter(args: string[]): Promise<number> {
186
191
  });
187
192
  const container = ready.container;
188
193
  const cmd = parsed.command;
194
+ const capabilityPlan = createSandboxCapabilityPlan(config);
195
+ const enterHookResults = await runSandboxHooks({
196
+ hooks: capabilityPlan.hooksByPhase['before-enter'],
197
+ phase: 'before-enter',
198
+ context: { config, plan: capabilityPlan },
199
+ runCommand: runBoundedSandboxHookCommand
200
+ });
201
+ for (const result of enterHookResults) {
202
+ if (result.status === 'fatal') {
203
+ throw new Error(result.message ?? `Sandbox hook '${result.hookId}' failed.`);
204
+ }
205
+ if (result.status === 'warning') {
206
+ process.stderr.write(
207
+ `Warning: ${result.message ?? `Sandbox hook '${result.hookId}' did not complete.`}\n`
208
+ );
209
+ }
210
+ }
189
211
 
190
- if (config.tools.includes('claude-code')) {
212
+ if (capabilityPlan.selectedAgentClients.some((adapter) => adapter.id === 'claude-code')) {
191
213
  try {
192
214
  // Scan all projects so a refresh from a neighbouring sandbox can still flow back to the host.
193
215
  const providerAuthAvailable = hasClaudeProviderAuth(config.home);
@@ -11,7 +11,7 @@ import { hostJoin } from '../engines/wsl2-paths.ts';
11
11
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
12
12
  import { parseLabels } from './ls.ts';
13
13
  import { runEngine, runSafe } from '../shell.ts';
14
- import { resolveTools } from '../tools.ts';
14
+ import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
15
15
  import type { SandboxTool } from '../tools.ts';
16
16
 
17
17
  const USAGE = `Usage: ai sandbox prune [--dry-run]`;
@@ -158,7 +158,7 @@ export async function prune(args: string[]): Promise<void> {
158
158
  }
159
159
 
160
160
  const config = loadConfig();
161
- const tools = resolveTools(config);
161
+ const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
162
162
  const engine = detectEngine(config);
163
163
  const psArgs = [
164
164
  'ps',
@@ -19,7 +19,8 @@ import { pruneSandboxDanglingImages } from '../image-prune.ts';
19
19
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
20
20
  import { runOk, runSafe, runSafeEngine } from '../shell.ts';
21
21
  import { resolveTaskBranch } from '../task-resolver.ts';
22
- import { resolveTools, toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
22
+ import { toolConfigDirCandidates, toolProjectDirCandidates } from '../tools.ts';
23
+ import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
23
24
  import type { SandboxTool } from '../tools.ts';
24
25
  import { fetchSandboxRows } from './list-running.ts';
25
26
  import { lookupShortIdByBranch } from '../../task/short-id.ts';
@@ -353,7 +354,7 @@ export async function rm(args: string[]): Promise<void> {
353
354
  }
354
355
 
355
356
  const config = loadConfig();
356
- const tools = resolveTools(config);
357
+ const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
357
358
 
358
359
  if (values.purge) {
359
360
  await rmPurge(config, tools);
@@ -3,6 +3,9 @@ import path from 'node:path';
3
3
  import { homedir, platform } from 'node:os';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import pc from 'picocolors';
6
+ import { normalizeAgentClients } from '../agent-clients/config.ts';
7
+ import { isAgentClientId } from '../agent-clients/types.ts';
8
+ import type { AgentClientState } from '../agent-clients/types.ts';
6
9
  import { validateSandboxEngine } from './engine.ts';
7
10
  import { hostJoin } from './engines/wsl2-paths.ts';
8
11
  import { findRuntimeEngineMismatches } from './runtime-engines.ts';
@@ -12,7 +15,7 @@ import type { SandboxTool } from './tools.ts';
12
15
  const DEFAULTS = Object.freeze({
13
16
  engine: null,
14
17
  runtimes: ['node22'],
15
- tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
18
+ tools: ['agent-infra'],
16
19
  refreshIntervalDays: 7,
17
20
  dockerfile: null,
18
21
  vm: {
@@ -57,6 +60,8 @@ export type SandboxConfig = {
57
60
  runtimes: string[];
58
61
  tools: string[];
59
62
  customTools: SandboxTool[];
63
+ agentClientState: AgentClientState;
64
+ agentClientSource: 'canonical' | 'legacy';
60
65
  refreshIntervalDays: number;
61
66
  dockerfile: string | null;
62
67
  vm: SandboxVmConfig;
@@ -65,6 +70,8 @@ export type SandboxConfig = {
65
70
  type AircConfig = {
66
71
  project?: unknown;
67
72
  org?: unknown;
73
+ agentClients?: unknown;
74
+ tuis?: unknown;
68
75
  sandbox?: SandboxConfigInput;
69
76
  };
70
77
 
@@ -98,6 +105,23 @@ function cloneDefaults(): SandboxConfigInput & { vm: SandboxVmConfig; runtimes:
98
105
  };
99
106
  }
100
107
 
108
+ function resolveSandboxToolIds(
109
+ sandbox: SandboxConfigInput,
110
+ agentClients: ReturnType<typeof normalizeAgentClients>,
111
+ defaults: readonly string[]
112
+ ): string[] {
113
+ if (Array.isArray(sandbox.tools)) {
114
+ const tools = sandbox.tools.filter((tool): tool is string => typeof tool === 'string');
115
+ return agentClients.source === 'canonical'
116
+ ? tools.filter((tool) => !isAgentClientId(tool))
117
+ : tools;
118
+ }
119
+ if (agentClients.remainingSandboxTools !== undefined) {
120
+ return [...agentClients.remainingSandboxTools];
121
+ }
122
+ return [...defaults];
123
+ }
124
+
101
125
  export function loadConfig({
102
126
  platformFn = platform,
103
127
  writeStderr = (chunk) => process.stderr.write(chunk)
@@ -115,6 +139,8 @@ export function loadConfig({
115
139
  }
116
140
 
117
141
  const airc = JSON.parse(fs.readFileSync(configPath, 'utf8')) as AircConfig;
142
+ const sandboxInput = airc.sandbox;
143
+ const agentClients = normalizeAgentClients(airc);
118
144
  const defaults = cloneDefaults();
119
145
  const sandbox = airc.sandbox ?? {};
120
146
  const engine = validateSandboxEngine(sandbox.engine ?? defaults.engine, { platformFn });
@@ -149,6 +175,7 @@ export function loadConfig({
149
175
  }
150
176
 
151
177
  const customTools = parseCustomTools(sandbox.customTools, { home });
178
+ const tools = resolveSandboxToolIds(sandbox, agentClients, defaults.tools);
152
179
 
153
180
  return {
154
181
  repoRoot,
@@ -164,10 +191,10 @@ export function loadConfig({
164
191
  dotfilesDir: hostJoin(home, '.agent-infra', 'dotfiles'),
165
192
  engine,
166
193
  runtimes,
167
- tools: Array.isArray(sandbox.tools) && sandbox.tools.length > 0
168
- ? [...sandbox.tools]
169
- : defaults.tools,
194
+ tools,
170
195
  customTools,
196
+ agentClientState: agentClients.state,
197
+ agentClientSource: agentClients.source,
171
198
  refreshIntervalDays: asNonNegativeIntegerOrDefault(
172
199
  sandbox.refreshIntervalDays,
173
200
  defaults.refreshIntervalDays
@@ -111,6 +111,12 @@ export function sandboxImageRefreshLabel(config: Pick<SandboxPathConfig, 'projec
111
111
  return `${sandboxLabel(config)}.last-refresh`;
112
112
  }
113
113
 
114
+ export function sandboxRuntimeCapabilityLabel(
115
+ config: Pick<SandboxPathConfig, 'project'>
116
+ ): string {
117
+ return `agent-infra.sandbox.runtime-capability.${config.project}`;
118
+ }
119
+
114
120
  export function parsePositiveIntegerOption(value: unknown, optionName: string): number | undefined {
115
121
  if (value === undefined || value === null) {
116
122
  return undefined;
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
5
  import { fileURLToPath } from 'node:url';
6
+ import { removeDirRecursive } from '../remove-dir.ts';
6
7
 
7
8
  const RUNTIMES_DIR = path.join(
8
9
  path.dirname(fileURLToPath(import.meta.url)),
@@ -88,7 +89,7 @@ export function prepareDockerfile(config: DockerfileConfig): { path: string; sig
88
89
  path: tempPath,
89
90
  signature: dockerfileSignature(config),
90
91
  cleanup() {
91
- fs.rmSync(tempDir, { recursive: true, force: true });
92
+ removeDirRecursive(tempDir);
92
93
  }
93
94
  };
94
95
  }