Skip to content

Commit ade9201

Browse files
CodeCasterXclaude
andcommitted
refactor(docker): 沙箱支持容器启动后命令并拆分开发者文档
新增 postSetupCmds 通用机制,支持容器启动后自动执行 shell 命令。 Codex 利用此机制将项目 slash commands 符号链接到容器内,无需手动 安装。将 README 中的开发者内容(认证机制、注册表字段、添加新工具 指南)拆分到 DEVELOPMENT.md。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5791218 commit ade9201

4 files changed

Lines changed: 144 additions & 47 deletions

File tree

docker/sandbox/DEVELOPMENT.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# 沙箱开发者指南
2+
3+
> 本文档面向维护者和开发者,说明 AI 工具注册表的设计原理和扩展方法。使用指南请参阅 [README.md](README.md)
4+
5+
## 为什么每个沙箱使用独立的 AI 工具配置?
6+
7+
Claude Code、Codex、OpenCode、Gemini CLI 都在各自的配置目录(`~/.claude/``~/.codex/``~/.local/share/opencode/``~/.gemini/`)中存储会话历史、项目记忆、锁文件等状态数据。如果多个沙箱容器共享同一个配置目录,会导致:
8+
9+
1. **并发写入冲突** — 多个容器同时写入 `history.jsonl``session-env/` 等文件会导致数据竞争和文件损坏
10+
2. **会话/记忆交叉污染** — 不同分支的项目上下文和会话历史会互相干扰
11+
3. **清理困难**`sandbox rm` 无法从共享目录中安全地只删除某个沙箱的数据
12+
4. **宿主机风险** — 容器内的破坏性操作可能损坏宿主机的凭据和配置
13+
14+
因此每个沙箱拥有独立的配置目录(`~/.{tool}-sandboxes/{branch}/`),实现完全隔离。
15+
16+
## AI 工具认证机制
17+
18+
各 AI 工具在宿主机上使用不同的凭据存储方式,导致沙箱内的认证体验有所差异:
19+
20+
| | 宿主机凭据存储 | 沙箱认证方式 | 首次使用 |
21+
|---|---|---|---|
22+
| **Codex** | 文件(`~/.codex/auth.json`| 自动从宿主机预植入 `auth.json` | 无需登录,直接使用 |
23+
| **OpenCode** | 文件(`~/.local/share/opencode/auth.json`| 自动从宿主机预植入 `auth.json` | 无需登录,直接使用 |
24+
| **Gemini CLI** | 文件(`~/.gemini/oauth_creds.json`| 自动从宿主机预植入 `oauth_creds.json` + `settings.json` | 无需登录,直接使用 |
25+
| **Claude Code** | macOS Keychain(`Claude Code-credentials`| 容器内 OAuth 登录,凭据存入 `.credentials.json` | 需在容器内登录一次 |
26+
27+
### 为什么 Claude Code 不能预植入?
28+
29+
Claude Code 在 macOS 上将 OAuth token 存储在系统 Keychain 中,宿主机的 `~/.claude/` 目录内没有凭据文件。Docker 容器无法访问 macOS Keychain,因此 Claude Code 在容器内会回退到基于文件的凭据存储(`~/.claude/.credentials.json`),需要首次在容器内完成 OAuth 登录。
30+
31+
登录后凭据持久化在 `~/.claude-sandboxes/{branch}/` 中,后续使用**无需再次登录**
32+
33+
### Codex / OpenCode / Gemini CLI 为什么可以?
34+
35+
Codex、OpenCode 和 Gemini CLI 始终使用文件存储凭据(分别为 `~/.codex/auth.json``~/.local/share/opencode/auth.json``~/.gemini/oauth_creds.json`),`sandbox create` 时自动将宿主机的凭据文件复制到沙箱配置目录,容器内可直接使用。Gemini CLI 还会额外预植入 `settings.json``google_accounts.json`,确保容器内的模型选项和用户设置与宿主机一致。
36+
37+
## AI 工具注册表
38+
39+
AI 工具的安装与运行配置以 `src/tools.ts` 中的 `AI_TOOLS` 注册表为唯一来源:
40+
41+
- `sandbox create` / `sandbox rebuild` 自动把注册表中的 `npmPackage` 列表作为 `AI_TOOL_PACKAGES` 传给 Docker build
42+
- `sandbox create` 把注册表中的 `envVars` 作为 `docker run -e` 注入容器
43+
- `Dockerfile.runtime-only` 不需要硬编码工具包名,只消费 `AI_TOOL_PACKAGES`
44+
45+
### 字段参考
46+
47+
| 字段 | 类型 | 必填 | 说明 |
48+
|---|---|---|---|
49+
| `name` | `string` || 显示名称,如 `"Claude Code"` |
50+
| `npmPackage` | `string` || npm 包名,用于 `npm install -g` |
51+
| `sandboxBase` | `string` || 宿主机上的沙箱配置根目录,如 `~/.codex-sandboxes` |
52+
| `containerMount` | `string` || 容器内挂载路径(绝对路径),如 `/home/devuser/.codex` |
53+
| `versionCmd` | `string` || 验证安装的命令,通过 `bash -lc` 执行 |
54+
| `noAuthHint` | `string` || 未预植入认证时的提示信息 |
55+
| `hostAuthFile` | `string` || 宿主机认证文件路径,与 `authFileName` 成对使用 |
56+
| `authFileName` | `string` || 沙箱内认证文件名(相对于 `sandboxBase/{branch}/`|
57+
| `hostPreSeedFiles` | `Array<{hostPath, sandboxName}>` || 额外需要预植入的宿主机文件(如设置、账户信息) |
58+
| `postSetupCmds` | `string[]` || 容器启动后执行的 shell 命令(如创建符号链接) |
59+
| `envVars` | `Record<string, string>` || 注入容器的额外环境变量 |
60+
61+
**校验规则**`validateTools` 启动时检查):
62+
- `name` 不能重复
63+
- `npmPackage` 不能为空
64+
- `containerMount` 必须是绝对路径
65+
- `hostAuthFile``authFileName` 必须同时存在或同时缺省
66+
67+
### `sandbox create` 执行流程
68+
69+
```
70+
1. 创建沙箱配置目录 sandboxBase/{branch}/
71+
2. 预植入认证文件 hostAuthFile → sandboxBase/{branch}/authFileName
72+
3. 预植入额外配置文件 hostPreSeedFiles[].hostPath → sandboxBase/{branch}/sandboxName
73+
4. 挂载配置目录到容器 sandboxBase/{branch}/ → containerMount
74+
5. 注入环境变量 envVars → docker run -e
75+
6. 容器启动后执行命令 postSetupCmds → docker exec bash -lc
76+
7. 验证安装 versionCmd → docker exec bash -lc
77+
```
78+
79+
所有预植入操作遵循"仅首次"策略:宿主机文件存在且沙箱中不存在时才复制,不会覆盖已有配置。
80+
81+
## 添加新工具
82+
83+
以 Gemini CLI 为例,说明添加一个新工具的完整步骤:
84+
85+
### 步骤 1:在注册表中追加描述符
86+
87+
编辑 `src/tools.ts`,在 `AI_TOOLS` 数组末尾追加:
88+
89+
```typescript
90+
{
91+
name: 'Gemini CLI',
92+
npmPackage: '@google/gemini-cli',
93+
sandboxBase: path.join(HOME, '.gemini-sandboxes'),
94+
containerMount: '/home/devuser/.gemini',
95+
versionCmd: 'gemini --version',
96+
hostAuthFile: path.join(HOME, '.gemini', 'oauth_creds.json'),
97+
authFileName: 'oauth_creds.json',
98+
noAuthHint: '首次使用需在容器内运行 gemini 完成认证。',
99+
hostPreSeedFiles: [
100+
{ hostPath: path.join(HOME, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
101+
{ hostPath: path.join(HOME, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' },
102+
],
103+
},
104+
```
105+
106+
### 步骤 2:编译验证
107+
108+
```bash
109+
cd docker/sandbox
110+
npm run build # 确认无语法错误
111+
```
112+
113+
### 步骤 3:重建镜像 + 重建沙箱
114+
115+
```bash
116+
sandbox rebuild # 重建镜像(安装新工具的 npm 包)
117+
sandbox rm <branch> # 删除旧沙箱
118+
sandbox create <branch> # 重新创建(触发预植入和 postSetupCmds)
119+
```
120+
121+
### 步骤 4:验证
122+
123+
```bash
124+
sandbox exec <branch>
125+
gemini --version # 确认工具可用
126+
```
127+
128+
**通常只需修改 `src/tools.ts` 一个文件。** 只有当工具需要注册表尚未支持的新能力时,才需要扩展 `AiTool` 接口和 `create.ts`

docker/sandbox/README.md

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -203,53 +203,9 @@ sandbox vm start --cpu 6 --memory 8 # 自定义资源启动
203203
- 路径简短清晰
204204
- 不在项目目录内,天然被 `.gitignore` 排除
205205

206-
### 为什么每个沙箱使用独立的 AI 工具配置?
206+
每个沙箱拥有独立的 AI 工具配置目录(如 `~/.codex-sandboxes/{branch}/`),避免并发冲突和会话污染。Codex、OpenCode、Gemini CLI 创建沙箱时自动从宿主机预植入认证凭据;Claude Code 首次需在容器内 OAuth 登录一次。
207207

208-
Claude Code、Codex、OpenCode、Gemini CLI 都在各自的配置目录(`~/.claude/``~/.codex/``~/.local/share/opencode/``~/.gemini/`)中存储会话历史、项目记忆、锁文件等状态数据。如果多个沙箱容器共享同一个配置目录,会导致:
209-
210-
1. **并发写入冲突** — 多个容器同时写入 `history.jsonl``session-env/` 等文件会导致数据竞争和文件损坏
211-
2. **会话/记忆交叉污染** — 不同分支的项目上下文和会话历史会互相干扰
212-
3. **清理困难**`sandbox rm` 无法从共享目录中安全地只删除某个沙箱的数据
213-
4. **宿主机风险** — 容器内的破坏性操作可能损坏宿主机的凭据和配置
214-
215-
因此每个沙箱拥有独立的配置目录,实现完全隔离。
216-
217-
### AI 工具认证机制
218-
219-
各 AI 工具在宿主机上使用不同的凭据存储方式,导致沙箱内的认证体验有所差异:
220-
221-
| | 宿主机凭据存储 | 沙箱认证方式 | 首次使用 |
222-
|---|---|---|---|
223-
| **Codex** | 文件(`~/.codex/auth.json`| 自动从宿主机预植入 `auth.json` | 无需登录,直接使用 |
224-
| **OpenCode** | 文件(`~/.local/share/opencode/auth.json`| 自动从宿主机预植入 `auth.json` | 无需登录,直接使用 |
225-
| **Claude Code** | macOS Keychain(`Claude Code-credentials`| 容器内 OAuth 登录,凭据存入 `.credentials.json` | 需在容器内登录一次 |
226-
| **Gemini CLI** | 文件(`~/.gemini/oauth_creds.json`| 自动从宿主机预植入 `oauth_creds.json` + `settings.json` | 无需登录,直接使用 |
227-
228-
**为什么 Claude Code 不能预植入?**
229-
230-
Claude Code 在 macOS 上将 OAuth token 存储在系统 Keychain 中,宿主机的 `~/.claude/` 目录内没有凭据文件。Docker 容器无法访问 macOS Keychain,因此 Claude Code 在容器内会回退到基于文件的凭据存储(`~/.claude/.credentials.json`),需要首次在容器内完成 OAuth 登录。
231-
232-
登录后凭据持久化在 `~/.claude-sandboxes/{branch}/` 中,后续使用**无需再次登录**
233-
234-
**Codex / OpenCode / Gemini CLI 为什么可以?**
235-
236-
Codex、OpenCode 和 Gemini CLI 始终使用文件存储凭据(分别为 `~/.codex/auth.json``~/.local/share/opencode/auth.json``~/.gemini/oauth_creds.json`),`sandbox create` 时自动将宿主机的凭据文件复制到沙箱配置目录,容器内可直接使用。Gemini CLI 还会额外预植入 `settings.json``google_accounts.json`,确保容器内的模型选项和用户设置与宿主机一致。
237-
238-
### AI 工具维护(单一事实源)
239-
240-
AI 工具的安装与运行配置以 `src/tools.ts` 中的 `AI_TOOLS` 注册表为唯一来源:
241-
242-
- 每个工具在注册表中声明 `name``npmPackage``sandboxBase``containerMount``versionCmd` 等信息
243-
- `sandbox create` / `sandbox rebuild` 会自动把注册表中的 `npmPackage` 列表作为 `AI_TOOL_PACKAGES` 传给 Docker build
244-
- `sandbox create` 会把注册表中的 `envVars` 作为 `docker run -e` 注入容器
245-
- `Dockerfile.runtime-only` 不需要硬编码工具包名,只消费 `AI_TOOL_PACKAGES`
246-
247-
这意味着新增工具时,通常只需:
248-
249-
1.`src/tools.ts``AI_TOOLS` 追加新描述符
250-
2. 运行 `sandbox rebuild` 重建镜像
251-
252-
无需手工同步 Dockerfile 中的 `npm install -g` 包列表。
208+
> 详细的认证机制说明、注册表字段参考和添加新工具指南请参阅 [DEVELOPMENT.md](DEVELOPMENT.md)
253209
254210
## 高级配置
255211

@@ -302,7 +258,8 @@ EOF
302258

303259
```
304260
docker/sandbox/
305-
├── README.md # 本文件
261+
├── README.md # 用户指南(使用、管理、故障排查)
262+
├── DEVELOPMENT.md # 开发者指南(注册表、认证机制、添加新工具)
306263
├── sandbox.sh # CLI 入口(thin wrapper)
307264
├── Dockerfile.runtime-only # 镜像定义(运行时 + AI 工具)
308265
├── package.json # Node.js 依赖(commander + clack)

docker/sandbox/src/commands/create.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,13 @@ export async function create(branch: string, base: string | undefined, opts: Cre
172172
message('Syncing git config...');
173173
syncGitConfig(container);
174174

175+
// Run post-setup commands inside container
176+
for (const { tool } of resolvedTools) {
177+
for (const cmd of tool.postSetupCmds ?? []) {
178+
runSafe('docker', ['exec', container, 'bash', '-lc', cmd]);
179+
}
180+
}
181+
175182
return 'Container started';
176183
},
177184
},

docker/sandbox/src/tools.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface AiTool {
2828
noAuthHint: string;
2929
/** Additional host files to pre-seed into sandbox (e.g. settings, account info) */
3030
hostPreSeedFiles?: Array<{ hostPath: string; sandboxName: string }>;
31+
/** Shell commands to run inside the container after setup (e.g. symlink prompts) */
32+
postSetupCmds?: string[];
3133
/** Extra environment variables to pass to the container */
3234
envVars?: Record<string, string>;
3335
}
@@ -76,6 +78,9 @@ export const AI_TOOLS: readonly Readonly<AiTool>[] = [
7678
hostAuthFile: path.join(HOME, '.codex', 'auth.json'),
7779
authFileName: 'auth.json',
7880
noAuthHint: '首次使用需在容器内运行 codex,按 Esc 选择 Device Code 方式登录。',
81+
postSetupCmds: [
82+
'test -d /workspace/.codex/commands && ln -sfn /workspace/.codex/commands /home/devuser/.codex/prompts || true',
83+
],
7984
},
8085
{
8186
name: 'OpenCode',

0 commit comments

Comments
 (0)