@fitlab-ai/agent-infra 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +63 -27
  2. package/README.zh-CN.md +61 -25
  3. package/bin/cli.ts +18 -6
  4. package/dist/bin/cli.js +20 -6
  5. package/dist/lib/cp.js +127 -0
  6. package/dist/lib/defaults.json +1 -0
  7. package/dist/lib/init.js +3 -0
  8. package/dist/lib/sandbox/clipboard/bridge.js +23 -4
  9. package/dist/lib/sandbox/clipboard/index.js +12 -3
  10. package/dist/lib/sandbox/commands/create.js +11 -2
  11. package/dist/lib/sandbox/commands/enter.js +29 -6
  12. package/dist/lib/sandbox/commands/list-running.js +108 -0
  13. package/dist/lib/sandbox/commands/ls.js +24 -45
  14. package/dist/lib/sandbox/commands/rebuild.js +15 -7
  15. package/dist/lib/sandbox/config.js +3 -0
  16. package/dist/lib/sandbox/index.js +6 -4
  17. package/dist/lib/sandbox/readme-scaffold.js +148 -0
  18. package/dist/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
  19. package/dist/lib/sandbox/runtimes/base.dockerfile +3 -3
  20. package/dist/lib/sandbox/tools.js +213 -8
  21. package/dist/lib/update.js +12 -1
  22. package/lib/cp.ts +177 -0
  23. package/lib/defaults.json +1 -0
  24. package/lib/init.ts +10 -0
  25. package/lib/sandbox/clipboard/bridge.ts +23 -4
  26. package/lib/sandbox/clipboard/index.ts +12 -3
  27. package/lib/sandbox/commands/create.ts +18 -2
  28. package/lib/sandbox/commands/enter.ts +48 -6
  29. package/lib/sandbox/commands/list-running.ts +135 -0
  30. package/lib/sandbox/commands/ls.ts +28 -49
  31. package/lib/sandbox/commands/rebuild.ts +24 -7
  32. package/lib/sandbox/config.ts +7 -0
  33. package/lib/sandbox/index.ts +6 -4
  34. package/lib/sandbox/readme-scaffold.ts +177 -0
  35. package/lib/sandbox/runtimes/ai-tools.dockerfile +12 -6
  36. package/lib/sandbox/runtimes/base.dockerfile +3 -3
  37. package/lib/sandbox/tools.ts +248 -9
  38. package/lib/update.ts +15 -1
  39. package/package.json +1 -1
  40. package/templates/.agents/QUICKSTART.en.md +1 -1
  41. package/templates/.agents/QUICKSTART.zh-CN.md +1 -1
  42. package/templates/.agents/README.en.md +79 -2
  43. package/templates/.agents/README.zh-CN.md +79 -2
  44. package/templates/.agents/rules/create-issue.en.md +1 -1
  45. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  46. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  47. package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
  48. package/templates/.agents/rules/issue-sync.github.en.md +6 -5
  49. package/templates/.agents/rules/issue-sync.github.zh-CN.md +6 -5
  50. package/templates/.agents/rules/milestone-inference.github.en.md +2 -2
  51. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +2 -2
  52. package/templates/.agents/rules/no-mid-flow-questions.en.md +57 -0
  53. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +57 -0
  54. package/templates/.agents/rules/pr-sync.github.en.md +4 -5
  55. package/templates/.agents/rules/pr-sync.github.zh-CN.md +4 -5
  56. package/templates/.agents/rules/task-management.en.md +9 -6
  57. package/templates/.agents/rules/task-management.zh-CN.md +9 -6
  58. package/templates/.agents/rules/testing-discipline.en.md +2 -2
  59. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -2
  60. package/templates/.agents/scripts/validate-artifact.js +1 -1
  61. package/templates/.agents/skills/analyze-task/SKILL.en.md +16 -4
  62. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +16 -4
  63. package/templates/.agents/skills/check-task/SKILL.en.md +43 -32
  64. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +42 -31
  65. package/templates/.agents/skills/code-task/SKILL.en.md +117 -0
  66. package/templates/.agents/skills/{implement-task → code-task}/SKILL.zh-CN.md +51 -24
  67. package/templates/.agents/skills/{implement-task → code-task}/config/verify.en.json +4 -4
  68. package/templates/.agents/skills/{implement-task → code-task}/config/verify.zh-CN.json +4 -4
  69. package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.zh-CN.md +2 -2
  70. package/templates/.agents/skills/{implement-task/reference/implementation-rules.en.md → code-task/reference/code-rules.en.md} +6 -6
  71. package/templates/.agents/skills/{implement-task/reference/implementation-rules.zh-CN.md → code-task/reference/code-rules.zh-CN.md} +3 -3
  72. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +69 -0
  73. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +69 -0
  74. package/templates/.agents/skills/{refine-task/reference/fix-workflow.en.md → code-task/reference/fix-mode.en.md} +12 -12
  75. package/templates/.agents/skills/{refine-task/reference/fix-workflow.zh-CN.md → code-task/reference/fix-mode.zh-CN.md} +8 -8
  76. package/templates/.agents/skills/code-task/reference/output-template.en.md +20 -0
  77. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +20 -0
  78. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.en.md +4 -4
  79. package/templates/.agents/skills/{implement-task → code-task}/reference/report-template.zh-CN.md +3 -3
  80. package/templates/.agents/skills/code-task/scripts/detect-mode.js +370 -0
  81. package/templates/.agents/skills/commit/SKILL.en.md +2 -2
  82. package/templates/.agents/skills/commit/SKILL.zh-CN.md +2 -2
  83. package/templates/.agents/skills/commit/reference/task-status-update.en.md +10 -6
  84. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +10 -6
  85. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -3
  86. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -3
  87. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -1
  88. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -1
  89. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  90. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  91. package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -2
  92. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +2 -2
  93. package/templates/.agents/skills/import-issue/SKILL.en.md +3 -3
  94. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +3 -3
  95. package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
  96. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
  97. package/templates/.agents/skills/restore-task/SKILL.en.md +4 -3
  98. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +4 -3
  99. package/templates/.agents/skills/review-analysis/SKILL.en.md +76 -0
  100. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +102 -0
  101. package/templates/.agents/skills/review-analysis/config/verify.en.json +51 -0
  102. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +51 -0
  103. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +87 -0
  104. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +87 -0
  105. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +90 -0
  106. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +91 -0
  107. package/templates/.agents/skills/review-analysis/reference/review-criteria.en.md +47 -0
  108. package/templates/.agents/skills/review-analysis/reference/review-criteria.zh-CN.md +47 -0
  109. package/templates/.agents/skills/{review-task → review-code}/SKILL.en.md +11 -9
  110. package/templates/.agents/skills/{review-task → review-code}/SKILL.zh-CN.md +15 -9
  111. package/templates/.agents/skills/{review-task → review-code}/config/verify.en.json +7 -5
  112. package/templates/.agents/skills/{review-task → review-code}/config/verify.zh-CN.json +6 -4
  113. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.en.md +21 -17
  114. package/templates/.agents/skills/{review-task → review-code}/reference/output-templates.zh-CN.md +19 -15
  115. package/templates/.agents/skills/{review-task → review-code}/reference/report-template.en.md +5 -6
  116. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +91 -0
  117. package/templates/.agents/skills/review-code/reference/review-criteria.en.md +48 -0
  118. package/templates/.agents/skills/{review-task → review-code}/reference/review-criteria.zh-CN.md +10 -4
  119. package/templates/.agents/skills/review-plan/SKILL.en.md +76 -0
  120. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +102 -0
  121. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.en.json +14 -10
  122. package/templates/.agents/skills/{refine-task → review-plan}/config/verify.zh-CN.json +14 -10
  123. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +87 -0
  124. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +87 -0
  125. package/templates/.agents/skills/review-plan/reference/report-template.en.md +90 -0
  126. package/templates/.agents/skills/{review-task → review-plan}/reference/report-template.zh-CN.md +3 -3
  127. package/templates/.agents/skills/review-plan/reference/review-criteria.en.md +47 -0
  128. package/templates/.agents/skills/review-plan/reference/review-criteria.zh-CN.md +47 -0
  129. package/templates/.agents/skills/test/SKILL.en.md +2 -2
  130. package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -31
  131. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +1 -0
  132. package/templates/.agents/templates/task.en.md +3 -3
  133. package/templates/.agents/templates/task.zh-CN.md +2 -2
  134. package/templates/.agents/workflows/bug-fix.en.yaml +126 -80
  135. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +90 -44
  136. package/templates/.agents/workflows/feature-development.en.yaml +115 -70
  137. package/templates/.agents/workflows/feature-development.zh-CN.yaml +92 -47
  138. package/templates/.agents/workflows/refactoring.en.yaml +123 -78
  139. package/templates/.agents/workflows/refactoring.zh-CN.yaml +89 -44
  140. package/templates/.claude/commands/code-task.en.md +8 -0
  141. package/templates/.claude/commands/code-task.zh-CN.md +8 -0
  142. package/templates/.claude/commands/review-analysis.en.md +8 -0
  143. package/templates/.claude/commands/review-analysis.zh-CN.md +8 -0
  144. package/templates/.claude/commands/review-code.en.md +8 -0
  145. package/templates/.claude/commands/review-code.zh-CN.md +8 -0
  146. package/templates/.claude/commands/review-plan.en.md +8 -0
  147. package/templates/.claude/commands/review-plan.zh-CN.md +8 -0
  148. package/templates/.gemini/commands/_project_/archive-tasks.zh-CN.toml +1 -1
  149. package/templates/.gemini/commands/_project_/code-task.en.toml +8 -0
  150. package/templates/.gemini/commands/_project_/code-task.zh-CN.toml +8 -0
  151. package/templates/.gemini/commands/_project_/init-labels.zh-CN.toml +1 -1
  152. package/templates/.gemini/commands/_project_/init-milestones.zh-CN.toml +1 -1
  153. package/templates/.gemini/commands/_project_/review-analysis.en.toml +8 -0
  154. package/templates/.gemini/commands/_project_/review-analysis.zh-CN.toml +8 -0
  155. package/templates/.gemini/commands/_project_/review-code.en.toml +8 -0
  156. package/templates/.gemini/commands/_project_/review-code.zh-CN.toml +8 -0
  157. package/templates/.gemini/commands/_project_/review-plan.en.toml +8 -0
  158. package/templates/.gemini/commands/_project_/review-plan.zh-CN.toml +8 -0
  159. package/templates/.opencode/commands/code-task.en.md +11 -0
  160. package/templates/.opencode/commands/code-task.zh-CN.md +11 -0
  161. package/templates/.opencode/commands/review-analysis.en.md +11 -0
  162. package/templates/.opencode/commands/review-analysis.zh-CN.md +11 -0
  163. package/templates/.opencode/commands/review-code.en.md +11 -0
  164. package/templates/.opencode/commands/review-code.zh-CN.md +11 -0
  165. package/templates/.opencode/commands/review-plan.en.md +11 -0
  166. package/templates/.opencode/commands/review-plan.zh-CN.md +11 -0
  167. package/templates/.agents/skills/implement-task/SKILL.en.md +0 -173
  168. package/templates/.agents/skills/implement-task/reference/output-template.en.md +0 -20
  169. package/templates/.agents/skills/implement-task/reference/output-template.zh-CN.md +0 -20
  170. package/templates/.agents/skills/refine-task/SKILL.en.md +0 -153
  171. package/templates/.agents/skills/refine-task/SKILL.zh-CN.md +0 -153
  172. package/templates/.agents/skills/refine-task/reference/report-template.en.md +0 -64
  173. package/templates/.agents/skills/refine-task/reference/report-template.zh-CN.md +0 -64
  174. package/templates/.agents/skills/review-task/reference/review-criteria.en.md +0 -42
  175. package/templates/.claude/commands/implement-task.en.md +0 -8
  176. package/templates/.claude/commands/implement-task.zh-CN.md +0 -8
  177. package/templates/.claude/commands/refine-task.en.md +0 -8
  178. package/templates/.claude/commands/refine-task.zh-CN.md +0 -8
  179. package/templates/.claude/commands/review-task.en.md +0 -8
  180. package/templates/.claude/commands/review-task.zh-CN.md +0 -8
  181. package/templates/.gemini/commands/_project_/implement-task.en.toml +0 -8
  182. package/templates/.gemini/commands/_project_/implement-task.zh-CN.toml +0 -8
  183. package/templates/.gemini/commands/_project_/refine-task.en.toml +0 -8
  184. package/templates/.gemini/commands/_project_/refine-task.zh-CN.toml +0 -8
  185. package/templates/.gemini/commands/_project_/review-task.en.toml +0 -8
  186. package/templates/.gemini/commands/_project_/review-task.zh-CN.toml +0 -8
  187. package/templates/.opencode/commands/implement-task.en.md +0 -11
  188. package/templates/.opencode/commands/implement-task.zh-CN.md +0 -11
  189. package/templates/.opencode/commands/refine-task.en.md +0 -11
  190. package/templates/.opencode/commands/refine-task.zh-CN.md +0 -11
  191. package/templates/.opencode/commands/review-task.en.md +0 -11
  192. package/templates/.opencode/commands/review-task.zh-CN.md +0 -11
  193. /package/templates/.agents/skills/{implement-task → code-task}/reference/branch-management.en.md +0 -0
@@ -2,13 +2,15 @@ const USAGE = `Usage: ai sandbox <command> [options]
2
2
 
3
3
  Commands:
4
4
  create <branch> [base] Create a sandbox (VM + image + worktree + container)
5
- exec <branch> [cmd...] Enter sandbox or run a command
6
- refresh Sync host Claude Code credentials to all sandbox copies
5
+ exec <branch | '#N'> [cmd...]
6
+ Enter sandbox or run a command (use leftmost '#' column from 'ls')
7
7
  ls List sandboxes for the current project
8
- rm <branch> [--all] Remove a sandbox or all sandboxes
9
8
  prune [--dry-run] Remove orphaned per-branch state dirs
9
+ rebuild [--quiet] [--refresh]
10
+ Rebuild the sandbox image (--refresh pulls base + tools)
11
+ refresh Sync host Claude Code credentials to all sandbox copies
12
+ rm <branch> [--all] Remove a sandbox or all sandboxes
10
13
  vm status|start|stop Manage the sandbox VM (macOS) or check the backend (Windows)
11
- rebuild [--quiet] Rebuild the sandbox image
12
14
 
13
15
  Run 'ai sandbox <command> --help' for details.`;
14
16
  export async function runSandbox(args) {
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { shareBranchDir, shareCommonDir } from "./constants.js";
4
+ const DOTFILES_README = `# User-level dotfiles channel
5
+
6
+ This directory is mounted **read-only** into every sandbox container at
7
+ \`/dotfiles\`. On entry, \`sandbox-dotfiles-link\` mirrors every file here as a
8
+ symlink under \`$HOME\` (e.g. \`.tmux.conf\` -> \`/home/devuser/.tmux.conf\`),
9
+ overriding image defaults so your editor, shell, and tool preferences follow
10
+ you across \`ai sandbox destroy + create\`.
11
+
12
+ See: https://github.com/fitlab-ai/agent-infra/blob/main/README.md#user-level-dotfiles-channel
13
+
14
+ Common usage - drop files or symlinks here:
15
+
16
+ \`\`\`sh
17
+ # Real files
18
+ echo "set -g mouse on" > ~/.agent-infra/dotfiles/.tmux.conf
19
+
20
+ # Symlinks to live host paths
21
+ ln -s ~/.tmux.conf ~/.agent-infra/dotfiles/.tmux.conf
22
+ ln -s ~/.config/lazygit ~/.agent-infra/dotfiles/.config/lazygit
23
+ \`\`\`
24
+
25
+ > Do **not** put secrets here. Use the dedicated SSH / credential mounts.
26
+
27
+ If you delete this file, the next \`ai sandbox create\` will re-create it
28
+ verbatim. To stop seeing it, edit or empty the file in place - the scaffold
29
+ only writes \`README.md\` when it is missing, never when it already exists.
30
+
31
+ ---
32
+
33
+ # 用户级 dotfiles 通道
34
+
35
+ 该目录被以**只读**方式挂载到每个 sandbox 容器的 \`/dotfiles\`。容器启动时,
36
+ \`sandbox-dotfiles-link\` 会把这里的每个文件 \`ln -sfn\` 到 \`$HOME\` 对应路径
37
+ (例如 \`.tmux.conf -> /home/devuser/.tmux.conf\`),覆盖镜像默认值,让你的编辑器、
38
+ shell、工具偏好跨 \`ai sandbox destroy + create\` 持久存在。
39
+
40
+ 参考:https://github.com/fitlab-ai/agent-infra/blob/main/README.zh-CN.md#用户级-dotfiles-通道
41
+
42
+ 常见用法:把文件或符号链接放进来:
43
+
44
+ \`\`\`sh
45
+ # 直接放文件
46
+ echo "set -g mouse on" > ~/.agent-infra/dotfiles/.tmux.conf
47
+
48
+ # 用符号链接指向 host 实际文件
49
+ ln -s ~/.tmux.conf ~/.agent-infra/dotfiles/.tmux.conf
50
+ ln -s ~/.config/lazygit ~/.agent-infra/dotfiles/.config/lazygit
51
+ \`\`\`
52
+
53
+ > **不要**在此放任何凭证。SSH / 凭证请使用专用挂载通道。
54
+
55
+ 如果你删除该文件,下一次 \`ai sandbox create\` 会原样重新生成。如果你不想再
56
+ 看到它,**就地编辑或清空内容**即可:scaffold 仅在 \`README.md\` **缺失**时
57
+ 写入,文件存在(哪怕被清空)就不会被重写。
58
+ `;
59
+ const SHARE_COMMON_README = `# /share/common - host <-> sandbox shared scratch (cross-branch)
60
+
61
+ This directory is mounted **read-write** into every sandbox container of this
62
+ project at \`/share/common\`, regardless of branch. Drop files here to share
63
+ between host and any sandbox without polluting the git worktree.
64
+
65
+ See: https://github.com/fitlab-ai/agent-infra/blob/main/README.md#host-sandbox-file-exchange
66
+
67
+ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
68
+
69
+ ---
70
+
71
+ # /share/common - 宿主 <-> sandbox 共享暂存(跨分支)
72
+
73
+ 该目录被以**读写**方式挂载到本项目所有 sandbox 容器的 \`/share/common\`,
74
+ 跨分支可见。可用来在宿主和任意 sandbox 之间传文件,无需弄脏 git worktree。
75
+
76
+ 参考:https://github.com/fitlab-ai/agent-infra/blob/main/README.zh-CN.md#宿主-沙箱文件交换
77
+
78
+ 该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
79
+ `;
80
+ const SHARE_BRANCH_README = `# /share/branch - host <-> sandbox shared scratch (branch-exclusive)
81
+
82
+ This directory is mounted **read-write** into the sandbox container of this
83
+ project's current branch at \`/share/branch\`. Files here are exclusive to this
84
+ branch's sandbox and do not leak across branches.
85
+
86
+ See: https://github.com/fitlab-ai/agent-infra/blob/main/README.md#host-sandbox-file-exchange
87
+
88
+ This file is safe to delete; the next \`ai sandbox create\` will re-create it.
89
+
90
+ ---
91
+
92
+ # /share/branch - 宿主 <-> sandbox 共享暂存(分支独占)
93
+
94
+ 该目录被以**读写**方式挂载到本项目当前分支 sandbox 容器的 \`/share/branch\`,
95
+ 仅当前分支可见,不会跨分支泄漏。
96
+
97
+ 参考:https://github.com/fitlab-ai/agent-infra/blob/main/README.zh-CN.md#宿主-沙箱文件交换
98
+
99
+ 该文件可以安全删除;下一次 \`ai sandbox create\` 会重新生成。
100
+ `;
101
+ function errorDetail(error) {
102
+ return error instanceof Error ? error.message : 'unknown error';
103
+ }
104
+ function errorCode(error) {
105
+ return typeof error === 'object' && error !== null && 'code' in error
106
+ ? String(error.code)
107
+ : '';
108
+ }
109
+ function ensureFile(target, content, options) {
110
+ const writeStderr = options.writeStderr ?? ((chunk) => process.stderr.write(chunk));
111
+ const fsModule = options.fsModule ?? fs;
112
+ const result = { created: false, path: target };
113
+ try {
114
+ fsModule.mkdirSync(path.dirname(target), { recursive: true });
115
+ }
116
+ catch (error) {
117
+ writeStderr(`sandbox-readme-scaffold: skipping ${target} (${errorDetail(error)})\n`);
118
+ return result;
119
+ }
120
+ try {
121
+ fsModule.writeFileSync(target, content, { encoding: 'utf8', flag: 'wx' });
122
+ result.created = true;
123
+ }
124
+ catch (error) {
125
+ if (errorCode(error) === 'EEXIST') {
126
+ return result;
127
+ }
128
+ writeStderr(`sandbox-readme-scaffold: skipping ${target} (${errorDetail(error)})\n`);
129
+ }
130
+ return result;
131
+ }
132
+ export function ensureDotfilesReadme(dotfilesDir, options = {}) {
133
+ return ensureFile(path.join(dotfilesDir, 'README.md'), DOTFILES_README, options);
134
+ }
135
+ export function ensureShareCommonReadme(config, options = {}) {
136
+ return ensureFile(path.join(shareCommonDir(config), 'README.md'), SHARE_COMMON_README, options);
137
+ }
138
+ export function ensureShareBranchReadme(config, branch, options = {}) {
139
+ return ensureFile(path.join(shareBranchDir(config, branch), 'README.md'), SHARE_BRANCH_README, options);
140
+ }
141
+ export function ensureSandboxDiscoveryReadmes(config, branch, options = {}) {
142
+ return [
143
+ ensureDotfilesReadme(config.dotfilesDir, options),
144
+ ensureShareCommonReadme(config, options),
145
+ ensureShareBranchReadme(config, branch, options)
146
+ ];
147
+ }
148
+ //# sourceMappingURL=readme-scaffold.js.map
@@ -1,17 +1,23 @@
1
1
  USER devuser
2
+ ENV DISABLE_UPDATES=1
3
+ ENV OPENCODE_DISABLE_AUTOUPDATE=1
2
4
  ENV NPM_CONFIG_PREFIX=/home/devuser/.npm-global
3
5
  ENV PATH="/home/devuser/.npm-global/bin:${PATH}"
4
6
 
5
- ARG AI_TOOL_PACKAGES
6
- RUN if [ -z "${AI_TOOL_PACKAGES}" ]; then \
7
- echo "AI_TOOL_PACKAGES build arg is required"; \
8
- exit 1; \
9
- fi && \
10
- set -e && \
7
+ ARG AI_TOOL_PACKAGES=
8
+ RUN set -e && \
11
9
  for pkg in ${AI_TOOL_PACKAGES}; do \
12
10
  npm install -g "$pkg"; \
13
11
  done
14
12
 
13
+ ARG AI_TOOLS_SHELL_INSTALL_B64=
14
+ RUN if [ -n "${AI_TOOLS_SHELL_INSTALL_B64}" ]; then \
15
+ set -e && \
16
+ echo "${AI_TOOLS_SHELL_INSTALL_B64}" | base64 -d > /tmp/ai-tools-install.sh && \
17
+ bash /tmp/ai-tools-install.sh && \
18
+ rm /tmp/ai-tools-install.sh; \
19
+ fi
20
+
15
21
  RUN npm install -g pyright
16
22
 
17
23
  RUN mkdir -p /home/devuser/.local/share /home/devuser/.local/state
@@ -1,4 +1,4 @@
1
- FROM ubuntu:22.04
1
+ FROM ubuntu:24.04
2
2
 
3
3
  LABEL description="AI coding sandbox"
4
4
 
@@ -28,7 +28,7 @@ RUN apt-get update && apt-get install -y \
28
28
  && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
29
29
  > /etc/apt/sources.list.d/github-cli.list \
30
30
  && apt-get update && apt-get install -y gh \
31
- && TMUX_VERSION=3.6a \
31
+ && TMUX_VERSION=3.6b \
32
32
  && wget -qO /tmp/tmux.tar.gz \
33
33
  "https://github.com/tmux/tmux/releases/download/${TMUX_VERSION}/tmux-${TMUX_VERSION}.tar.gz" \
34
34
  && tar xzf /tmp/tmux.tar.gz -C /tmp \
@@ -126,7 +126,7 @@ find . -type f -print | while IFS= read -r rel; do
126
126
  .config/opencode|.config/opencode/*|\
127
127
  .local/share/opencode|.local/share/opencode/*|\
128
128
  .host-shell-config|.host-shell-config/*|\
129
- .gitconfig|.gitignore_global|.stCommitMsg|.bash_aliases)
129
+ .gitconfig|.gitignore_global|.stCommitMsg|.bash_aliases|README.md)
130
130
  continue ;;
131
131
  esac
132
132
 
@@ -1,11 +1,12 @@
1
1
  import { safeNameCandidates, sanitizeBranchName } from "./constants.js";
2
2
  import { hostJoin } from "./engines/wsl2-paths.js";
3
+ const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
3
4
  function createBuiltinTools(home, project) {
4
5
  return {
5
6
  'claude-code': {
6
7
  id: 'claude-code',
7
8
  name: 'Claude Code',
8
- npmPackage: '@anthropic-ai/claude-code@stable',
9
+ install: { type: 'npm', cmd: '@anthropic-ai/claude-code@stable' },
9
10
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'claude-code'),
10
11
  containerMount: '/home/devuser/.claude',
11
12
  versionCmd: 'claude --version',
@@ -35,7 +36,7 @@ function createBuiltinTools(home, project) {
35
36
  codex: {
36
37
  id: 'codex',
37
38
  name: 'Codex',
38
- npmPackage: '@openai/codex',
39
+ install: { type: 'npm', cmd: '@openai/codex' },
39
40
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'codex'),
40
41
  containerMount: '/home/devuser/.codex',
41
42
  versionCmd: 'codex --version',
@@ -50,7 +51,7 @@ function createBuiltinTools(home, project) {
50
51
  opencode: {
51
52
  id: 'opencode',
52
53
  name: 'OpenCode',
53
- npmPackage: 'opencode-ai',
54
+ install: { type: 'npm', cmd: 'opencode-ai' },
54
55
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'opencode'),
55
56
  containerMount: '/home/devuser/.local/share/opencode',
56
57
  versionCmd: 'opencode version',
@@ -69,7 +70,7 @@ function createBuiltinTools(home, project) {
69
70
  'gemini-cli': {
70
71
  id: 'gemini-cli',
71
72
  name: 'Gemini CLI',
72
- npmPackage: '@google/gemini-cli',
73
+ install: { type: 'npm', cmd: '@google/gemini-cli' },
73
74
  sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'gemini-cli'),
74
75
  containerMount: '/home/devuser/.gemini',
75
76
  versionCmd: 'gemini --version',
@@ -84,15 +85,200 @@ function createBuiltinTools(home, project) {
84
85
  }
85
86
  };
86
87
  }
88
+ export function builtinToolIds() {
89
+ return Object.keys(createBuiltinTools('', ''));
90
+ }
87
91
  function validateTool(tool) {
88
- if (!tool.npmPackage || !tool.containerMount.startsWith('/')) {
89
- throw new Error(`Invalid sandbox tool descriptor: ${tool.id}`);
92
+ if (!tool.id || !TOOL_ID_PATTERN.test(tool.id)) {
93
+ throw new Error(`Invalid sandbox tool id: ${String(tool.id)}`);
94
+ }
95
+ if (!tool.install || (tool.install.type !== 'npm' && tool.install.type !== 'shell')) {
96
+ throw new Error(`Sandbox tool ${tool.id} has invalid install.type`);
97
+ }
98
+ if (!tool.install.cmd) {
99
+ throw new Error(`Sandbox tool ${tool.id} has empty install.cmd`);
100
+ }
101
+ if (!tool.containerMount || !tool.containerMount.startsWith('/')) {
102
+ throw new Error(`Sandbox tool ${tool.id} containerMount must be an absolute path`);
103
+ }
104
+ }
105
+ function isPlainObject(value) {
106
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
107
+ }
108
+ function asString(value, field, context) {
109
+ if (typeof value !== 'string') {
110
+ throw new Error(`${context}: field "${field}" must be a string`);
111
+ }
112
+ return value;
113
+ }
114
+ function asOptionalNonEmptyString(value, field, context) {
115
+ if (value === undefined) {
116
+ return undefined;
117
+ }
118
+ if (typeof value !== 'string') {
119
+ throw new Error(`${context}: field "${field}" must be a string when provided`);
120
+ }
121
+ if (value.length === 0) {
122
+ throw new Error(`${context}: field "${field}" must be non-empty when provided`);
123
+ }
124
+ return value;
125
+ }
126
+ function asStringRecord(value, field, context) {
127
+ if (value === undefined) {
128
+ return undefined;
129
+ }
130
+ if (!isPlainObject(value)) {
131
+ throw new Error(`${context}: field "${field}" must be an object when provided`);
132
+ }
133
+ const out = {};
134
+ for (const [key, val] of Object.entries(value)) {
135
+ if (typeof val !== 'string') {
136
+ throw new Error(`${context}: field "${field}.${key}" must be a string`);
137
+ }
138
+ out[key] = val;
139
+ }
140
+ return out;
141
+ }
142
+ function asStringArray(value, field, context) {
143
+ if (value === undefined) {
144
+ return undefined;
145
+ }
146
+ if (!Array.isArray(value)) {
147
+ throw new Error(`${context}: field "${field}" must be an array when provided`);
148
+ }
149
+ return value.map((item, index) => {
150
+ if (typeof item !== 'string') {
151
+ throw new Error(`${context}: field "${field}[${index}]" must be a string`);
152
+ }
153
+ return item;
154
+ });
155
+ }
156
+ function parseInstall(value, context) {
157
+ if (!isPlainObject(value)) {
158
+ throw new Error(`${context}: field "install" must be an object`);
159
+ }
160
+ const type = value.type;
161
+ if (type !== 'npm' && type !== 'shell') {
162
+ throw new Error(`${context}: field "install.type" must be "npm" or "shell"`);
163
+ }
164
+ const cmd = asString(value.cmd, 'install.cmd', context);
165
+ if (!cmd) {
166
+ throw new Error(`${context}: field "install.cmd" must be non-empty`);
167
+ }
168
+ return { type, cmd };
169
+ }
170
+ function parseHostPreSeedFiles(value, context) {
171
+ if (value === undefined) {
172
+ return undefined;
173
+ }
174
+ if (!Array.isArray(value)) {
175
+ throw new Error(`${context}: field "hostPreSeedFiles" must be an array when provided`);
90
176
  }
177
+ return value.map((item, index) => {
178
+ if (!isPlainObject(item)) {
179
+ throw new Error(`${context}: field "hostPreSeedFiles[${index}]" must be an object`);
180
+ }
181
+ return {
182
+ hostPath: asString(item.hostPath, `hostPreSeedFiles[${index}].hostPath`, context),
183
+ sandboxName: asString(item.sandboxName, `hostPreSeedFiles[${index}].sandboxName`, context)
184
+ };
185
+ });
186
+ }
187
+ function parseHostPreSeedDirs(value, context) {
188
+ if (value === undefined) {
189
+ return undefined;
190
+ }
191
+ if (!Array.isArray(value)) {
192
+ throw new Error(`${context}: field "hostPreSeedDirs" must be an array when provided`);
193
+ }
194
+ return value.map((item, index) => {
195
+ if (!isPlainObject(item)) {
196
+ throw new Error(`${context}: field "hostPreSeedDirs[${index}]" must be an object`);
197
+ }
198
+ return {
199
+ hostDir: asString(item.hostDir, `hostPreSeedDirs[${index}].hostDir`, context),
200
+ sandboxSubdir: asString(item.sandboxSubdir, `hostPreSeedDirs[${index}].sandboxSubdir`, context)
201
+ };
202
+ });
203
+ }
204
+ function parseHostLiveMounts(value, context) {
205
+ if (value === undefined) {
206
+ return undefined;
207
+ }
208
+ if (!Array.isArray(value)) {
209
+ throw new Error(`${context}: field "hostLiveMounts" must be an array when provided`);
210
+ }
211
+ return value.map((item, index) => {
212
+ if (!isPlainObject(item)) {
213
+ throw new Error(`${context}: field "hostLiveMounts[${index}]" must be an object`);
214
+ }
215
+ return {
216
+ hostPath: asString(item.hostPath, `hostLiveMounts[${index}].hostPath`, context),
217
+ containerSubpath: asString(item.containerSubpath, `hostLiveMounts[${index}].containerSubpath`, context)
218
+ };
219
+ });
220
+ }
221
+ export function parseCustomTool(entry, index, options) {
222
+ const context = `customTools[${index}]`;
223
+ if (!isPlainObject(entry)) {
224
+ throw new Error(`${context} must be an object`);
225
+ }
226
+ const id = asString(entry.id, 'id', context);
227
+ if (!TOOL_ID_PATTERN.test(id)) {
228
+ throw new Error(`${context}: field "id" must match ${TOOL_ID_PATTERN.source}`);
229
+ }
230
+ const containerMount = asOptionalNonEmptyString(entry.containerMount, 'containerMount', context)
231
+ ?? `/home/devuser/.${id}`;
232
+ if (!containerMount.startsWith('/')) {
233
+ throw new Error(`${context}: field "containerMount" must be an absolute path`);
234
+ }
235
+ const tool = {
236
+ id,
237
+ name: asOptionalNonEmptyString(entry.name, 'name', context) ?? id,
238
+ install: parseInstall(entry.install, context),
239
+ sandboxBase: hostJoin(options.home, '.agent-infra', 'sandboxes', id),
240
+ containerMount,
241
+ versionCmd: asOptionalNonEmptyString(entry.versionCmd, 'versionCmd', context) ?? `which ${id}`,
242
+ setupHint: asOptionalNonEmptyString(entry.setupHint, 'setupHint', context)
243
+ ?? `Run \`${id}\` inside the container to set up.`,
244
+ envVars: asStringRecord(entry.envVars, 'envVars', context),
245
+ hostPreSeedFiles: parseHostPreSeedFiles(entry.hostPreSeedFiles, context),
246
+ hostPreSeedDirs: parseHostPreSeedDirs(entry.hostPreSeedDirs, context),
247
+ pathRewriteFiles: asStringArray(entry.pathRewriteFiles, 'pathRewriteFiles', context),
248
+ hostLiveMounts: parseHostLiveMounts(entry.hostLiveMounts, context),
249
+ postSetupCmds: asStringArray(entry.postSetupCmds, 'postSetupCmds', context)
250
+ };
251
+ validateTool(tool);
252
+ return tool;
253
+ }
254
+ export function parseCustomTools(value, options) {
255
+ if (value === undefined || value === null) {
256
+ return [];
257
+ }
258
+ if (!Array.isArray(value)) {
259
+ throw new Error('sandbox: "customTools" must be an array');
260
+ }
261
+ return value.map((entry, index) => parseCustomTool(entry, index, options));
91
262
  }
92
263
  export function resolveTools(config) {
93
264
  const builtins = createBuiltinTools(config.home, config.project);
265
+ const customs = config.customTools ?? [];
266
+ const seen = new Set();
267
+ for (const tool of customs) {
268
+ if (builtins[tool.id]) {
269
+ throw new Error(`Custom sandbox tool id "${tool.id}" collides with a built-in tool`);
270
+ }
271
+ if (seen.has(tool.id)) {
272
+ throw new Error(`Duplicate sandbox tool id "${tool.id}" in customTools`);
273
+ }
274
+ seen.add(tool.id);
275
+ }
276
+ const merged = { ...builtins };
277
+ for (const tool of customs) {
278
+ merged[tool.id] = tool;
279
+ }
94
280
  return config.tools.map((id) => {
95
- const tool = builtins[id];
281
+ const tool = merged[id];
96
282
  if (!tool) {
97
283
  throw new Error(`Unknown sandbox tool: ${id}`);
98
284
  }
@@ -110,6 +296,25 @@ export function toolProjectDirCandidates(tool, project) {
110
296
  return [hostJoin(tool.sandboxBase, project)];
111
297
  }
112
298
  export function toolNpmPackagesArg(tools) {
113
- return tools.map((tool) => tool.npmPackage).join(' ');
299
+ return tools
300
+ .filter((tool) => tool.install.type === 'npm')
301
+ .map((tool) => tool.install.cmd)
302
+ .join(' ');
303
+ }
304
+ export function toolShellInstallScript(tools) {
305
+ const blocks = tools
306
+ .filter((tool) => tool.install.type === 'shell')
307
+ .map((tool) => `# install: ${tool.id}\n${tool.install.cmd}`);
308
+ if (blocks.length === 0) {
309
+ return '';
310
+ }
311
+ return ['#!/bin/bash', 'set -e', '', ...blocks, ''].join('\n');
312
+ }
313
+ export function toolShellInstallScriptBase64(tools) {
314
+ const script = toolShellInstallScript(tools);
315
+ return script ? Buffer.from(script, 'utf8').toString('base64') : '';
316
+ }
317
+ export function imageSignatureFields(tools) {
318
+ return tools.map((tool) => ({ id: tool.id, install: tool.install }));
114
319
  }
115
320
  //# sourceMappingURL=tools.js.map
@@ -121,6 +121,7 @@ async function cmdUpdate() {
121
121
  const platformAdded = !config.platform;
122
122
  const sandboxAdded = !config.sandbox;
123
123
  const labelsAdded = !config.labels;
124
+ const requiresPullRequestAdded = config.requiresPullRequest === undefined;
124
125
  let configChanged = changed;
125
126
  if (platformAdded) {
126
127
  config.platform = structuredClone(defaults.platform);
@@ -134,6 +135,10 @@ async function cmdUpdate() {
134
135
  config.labels = structuredClone(defaults.labels);
135
136
  configChanged = true;
136
137
  }
138
+ if (requiresPullRequestAdded) {
139
+ config.requiresPullRequest = defaults.requiresPullRequest;
140
+ configChanged = true;
141
+ }
137
142
  if (configChanged) {
138
143
  console.log('');
139
144
  if (hasNewEntries) {
@@ -145,7 +150,7 @@ async function cmdUpdate() {
145
150
  ok(` merged: ${entry}`);
146
151
  }
147
152
  }
148
- else if (platformAdded || sandboxAdded || labelsAdded) {
153
+ else if (platformAdded || sandboxAdded || labelsAdded || requiresPullRequestAdded) {
149
154
  if (platformAdded) {
150
155
  info(`Default platform config added to ${CONFIG_PATH}.`);
151
156
  }
@@ -155,6 +160,9 @@ async function cmdUpdate() {
155
160
  if (labelsAdded) {
156
161
  info(`Default labels.in config added to ${CONFIG_PATH}.`);
157
162
  }
163
+ if (requiresPullRequestAdded) {
164
+ info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
165
+ }
158
166
  }
159
167
  else {
160
168
  info(`File registry changed in ${CONFIG_PATH}.`);
@@ -168,6 +176,9 @@ async function cmdUpdate() {
168
176
  if (hasNewEntries && platformAdded) {
169
177
  info(`Default platform config added to ${CONFIG_PATH}.`);
170
178
  }
179
+ if (hasNewEntries && requiresPullRequestAdded) {
180
+ info(`Default requiresPullRequest=${defaults.requiresPullRequest} added to ${CONFIG_PATH}.`);
181
+ }
171
182
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
172
183
  ok(`Updated ${CONFIG_PATH}`);
173
184
  }