Skip to content

Commit 08376ba

Browse files
committed
Merge branch '3.6.x'
2 parents 9a02e4f + a1c6cd2 commit 08376ba

4 files changed

Lines changed: 243 additions & 26 deletions

File tree

docker/sandbox/DEVELOPMENT.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ Claude Code、Codex、OpenCode、Gemini CLI 都在各自的配置目录(`~/.cl
1919

2020
| | 宿主机凭据存储 | 沙箱认证方式 | 首次使用 |
2121
|---|---|---|---|
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` | 无需登录,直接使用 |
22+
| **Codex** | 文件(`~/.codex/auth.json`| 实时挂载(live mount)宿主机 `auth.json` | 无需登录,宿主机刷新后自动生效 |
23+
| **OpenCode** | 文件(`~/.local/share/opencode/auth.json`| 实时挂载(live mount)宿主机 `auth.json` | 无需登录,宿主机刷新后自动生效 |
24+
| **Gemini CLI** | 文件(`~/.gemini/oauth_creds.json`| 实时挂载 `oauth_creds.json` + 预植入 `settings.json` | 无需登录,宿主机刷新后自动生效 |
2525
| **Claude Code** | macOS Keychain(`Claude Code-credentials`| 容器内 OAuth 登录,凭据存入 `.credentials.json` | 需在容器内登录一次 |
2626

2727
### 为什么 Claude Code 不能预植入?
@@ -32,7 +32,11 @@ Claude Code 在 macOS 上将 OAuth token 存储在系统 Keychain 中,宿主
3232

3333
### Codex / OpenCode / Gemini CLI 为什么可以?
3434

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`,确保容器内的模型选项和用户设置与宿主机一致。
35+
Codex、OpenCode 和 Gemini CLI 始终使用文件存储凭据(分别为 `~/.codex/auth.json``~/.local/share/opencode/auth.json``~/.gemini/oauth_creds.json`)。这些认证文件通过 Docker bind mount(`hostLiveMounts`)直接从宿主机实时挂载到容器内,宿主机刷新 token 后容器自动生效,无需重建沙箱。
36+
37+
> **为什么使用实时挂载而非复制?** OAuth token 通常有过期时间(如 OpenAI token 有效期约 7 天),一次性复制的 token 过期后需要手动重新同步。实时挂载使宿主机和容器始终共享同一份文件,彻底消除 token 过期问题。
38+
39+
Gemini CLI 还会额外预植入(一次性复制)`settings.json``google_accounts.json`,确保容器内的模型选项和用户设置与宿主机一致。这些配置文件不含过期 token,无需实时同步。
3640

3741
## AI 工具注册表
3842

@@ -52,9 +56,12 @@ AI 工具的安装与运行配置以 `src/tools.ts` 中的 `AI_TOOLS` 注册表
5256
| `containerMount` | `string` || 容器内挂载路径(绝对路径),如 `/home/devuser/.codex` |
5357
| `versionCmd` | `string` || 验证安装的命令,通过 `bash -lc` 执行 |
5458
| `noAuthHint` | `string` || 未预植入认证时的提示信息 |
55-
| `hostAuthFile` | `string` || 宿主机认证文件路径,与 `authFileName` 成对使用 |
59+
| `hostAuthFile` | `string` || 宿主机认证文件路径,与 `authFileName` 成对使用(一次性复制) |
5660
| `authFileName` | `string` || 沙箱内认证文件名(相对于 `sandboxBase/{branch}/`|
5761
| `hostPreSeedFiles` | `Array<{hostPath, sandboxName}>` || 额外需要预植入的宿主机文件(如设置、账户信息) |
62+
| `hostPreSeedDirs` | `Array<{hostDir, sandboxSubdir}>` || 递归复制宿主机目录到沙箱(如插件目录) |
63+
| `pathRewriteFiles` | `string[]` || 预植入后需要路径重写的文件(宿主机路径 → 容器路径) |
64+
| `hostLiveMounts` | `Array<{hostPath, containerSubpath}>` || 实时挂载宿主机文件到容器(双向同步,用于认证 token) |
5865
| `postSetupCmds` | `string[]` || 容器启动后执行的 shell 命令(如创建符号链接) |
5966
| `envVars` | `Record<string, string>` || 注入容器的额外环境变量 |
6067

@@ -68,15 +75,19 @@ AI 工具的安装与运行配置以 `src/tools.ts` 中的 `AI_TOOLS` 注册表
6875

6976
```
7077
1. 创建沙箱配置目录 sandboxBase/{branch}/
71-
2. 预植入认证文件 hostAuthFile → sandboxBase/{branch}/authFileName
78+
2. 预植入认证文件(一次性) hostAuthFile → sandboxBase/{branch}/authFileName
7279
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
80+
4. 递归复制宿主机目录 hostPreSeedDirs[].hostDir → sandboxBase/{branch}/sandboxSubdir
81+
5. 路径重写 pathRewriteFiles[] 中的宿主机路径 → 容器路径
82+
6. 挂载配置目录到容器 sandboxBase/{branch}/ → containerMount
83+
7. 实时挂载认证文件 hostLiveMounts[].hostPath → containerMount/containerSubpath
84+
8. 注入环境变量 envVars → docker run -e
85+
9. 容器启动后执行命令 postSetupCmds → docker exec bash -lc
86+
10. 验证安装 versionCmd → docker exec bash -lc
7787
```
7888

79-
所有预植入操作遵循"仅首次"策略:宿主机文件存在且沙箱中不存在时才复制,不会覆盖已有配置。
89+
- **一次性操作**(步骤 2–5)遵循"仅首次"策略:宿主机文件存在且沙箱中不存在时才复制,不会覆盖已有配置。
90+
- **实时挂载**(步骤 7)通过 Docker bind mount 将宿主机文件直接映射到容器内,文件始终保持同步,无需重建沙箱。适用于会过期的认证 token。
8091

8192
## 添加新工具
8293

@@ -93,9 +104,12 @@ AI 工具的安装与运行配置以 `src/tools.ts` 中的 `AI_TOOLS` 注册表
93104
sandboxBase: path.join(HOME, '.gemini-sandboxes'),
94105
containerMount: '/home/devuser/.gemini',
95106
versionCmd: 'gemini --version',
96-
hostAuthFile: path.join(HOME, '.gemini', 'oauth_creds.json'),
97-
authFileName: 'oauth_creds.json',
98107
noAuthHint: '首次使用需在容器内运行 gemini 完成认证。',
108+
// 认证文件实时挂载(token 会过期,需与宿主机保持同步)
109+
hostLiveMounts: [
110+
{ hostPath: path.join(HOME, '.gemini', 'oauth_creds.json'), containerSubpath: 'oauth_creds.json' },
111+
],
112+
// 配置文件一次性预植入(不含过期 token,无需实时同步)
99113
hostPreSeedFiles: [
100114
{ hostPath: path.join(HOME, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
101115
{ hostPath: path.join(HOME, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' },

docker/sandbox/README.md

Lines changed: 187 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,16 @@ sandbox exec feat-xxx
9191
```bash
9292
# 进入容器后,直接使用
9393
claude # Claude Code(首次需容器内 OAuth 登录)
94-
codex # OpenAI Codex(自动预植入宿主机凭据
95-
opencode # OpenCode(自动预植入宿主机凭据
96-
gemini # Gemini CLI(自动预植入宿主机凭据
94+
codex # OpenAI Codex(实时同步宿主机凭据
95+
opencode # OpenCode(实时同步宿主机凭据
96+
gemini # Gemini CLI(实时同步宿主机凭据
9797

9898
# 也可以直接开发
9999
mvn clean install
100100
python3 script.py
101101
```
102102

103-
> **认证差异**:Codex、OpenCode、Gemini CLI 创建沙箱时会自动从宿主机预植入认证凭据,可直接使用;Claude Code 首次需要在容器内完成一次 OAuth 登录(之后免登录)。详见[认证机制说明](#ai-工具认证机制)
103+
> **认证差异**:Codex、OpenCode、Gemini CLI 的认证文件通过实时挂载(live mount)与宿主机双向同步,宿主机刷新 token 后容器自动生效;Claude Code 首次需要在容器内完成一次 OAuth 登录(之后免登录)。详见[认证机制说明](#ai-工具认证机制)
104104
105105
## 多容器并发工作流
106106

@@ -142,6 +142,188 @@ exit
142142
docker stop fit-dev-feat-xxx
143143
```
144144

145+
## 命令行模式(非交互式)
146+
147+
除了打开 TUI 界面,每个 AI 工具都支持直接在命令行传入 prompt 并获取输出,适合脚本自动化、快速提问和管道组合。每个工具还支持会话管理,可以在命令行实现多轮对话。
148+
149+
### Claude Code
150+
151+
```bash
152+
# 单次提问(-p = print mode,不打开 TUI)
153+
claude -p "解释一下这个项目的架构"
154+
155+
# 管道输入
156+
cat src/main/java/App.java | claude -p "审查这段代码的安全性"
157+
158+
# 指定输出格式(text / json / stream-json)
159+
claude -p "列出所有 TODO" --output-format json
160+
161+
# 指定模型
162+
claude -p --model opus "设计一个缓存方案"
163+
164+
# 限制轮次和预算
165+
claude -p --max-turns 3 --max-budget-usd 1.00 "重构数据库模块"
166+
167+
# 跳过所有权限确认(CI/脚本场景)
168+
claude -p --dangerously-skip-permissions "运行所有测试并汇报结果"
169+
170+
# ── 多轮对话(会话管理)──
171+
172+
# 预先生成 session ID,从第 1 轮起就使用(推荐)
173+
SID=$(uuidgen)
174+
175+
# 第 1 轮:指定 session ID 开启对话
176+
claude -p --session-id "$SID" "分析认证模块的架构"
177+
178+
# 第 2 轮:恢复同一个会话继续对话
179+
claude -p --resume "$SID" "重构登录函数"
180+
181+
# 第 3 轮:继续同一个会话
182+
claude -p --resume "$SID" "为重构后的代码补充单元测试"
183+
184+
# 快捷方式:继续当前目录下最近一次对话(无需 session ID)
185+
claude -p -c "为刚才的修改补充单元测试"
186+
187+
# 从已有会话分叉出新会话(不影响原会话)
188+
claude -p --resume "$SID" --fork-session "尝试另一种实现方案"
189+
```
190+
191+
### Codex
192+
193+
```bash
194+
# 单次提问(exec 子命令 = 非交互模式)
195+
codex exec "为 User 类生成单元测试"
196+
197+
# 管道输入
198+
echo "解释这个报错信息" | codex exec -
199+
200+
# JSON 事件流输出
201+
codex exec --json "列出所有 API 端点"
202+
203+
# 指定模型
204+
codex exec -m o4-mini "优化这个排序算法"
205+
206+
# 全自动模式(无需确认,可写工作区)
207+
codex exec --full-auto "给所有 API 路由添加错误处理"
208+
209+
# 将最终结果写入文件
210+
codex exec -o result.txt "分析项目依赖关系"
211+
212+
# ── 多轮对话(会话管理)──
213+
# Codex 不支持预指定 thread ID,需从首轮 JSON 输出获取
214+
215+
# 第 1 轮:启动会话,通过 JSON 事件流获取 thread_id
216+
TID=$(codex exec --json "分析数据库模块" 2>/dev/null \
217+
| jq -r 'select(.type=="thread.started") | .thread_id')
218+
219+
# 第 2 轮:通过 thread_id 恢复会话(注意 resume 是 exec 的子命令)
220+
codex exec resume "$TID" "添加连接池"
221+
222+
# 第 3 轮:继续同一个会话
223+
codex exec resume "$TID" "补充单元测试"
224+
225+
# 快捷方式:恢复最近一次会话
226+
codex exec resume --last "继续上次的任务"
227+
```
228+
229+
### OpenCode
230+
231+
```bash
232+
# 单次提问(run 子命令 = 非交互模式)
233+
opencode run "解释 JavaScript 闭包的工作原理"
234+
235+
# 指定模型(格式:provider/model-name)
236+
opencode run -m anthropic/claude-sonnet-4-20250514 "审查这段代码"
237+
238+
# 指定 Agent
239+
opencode run --agent plan "分析项目结构并给出改进建议"
240+
241+
# JSON 输出
242+
opencode run --format json "列出所有 API 端点"
243+
244+
# 附加文件到 prompt
245+
opencode run --file src/auth.ts "审查这个文件的安全性"
246+
247+
# ── 多轮对话(会话管理)──
248+
# OpenCode 不支持预指定 session ID,需从首轮 JSON 输出获取
249+
250+
# 第 1 轮:启动会话,通过 JSON 输出获取 sessionID
251+
SID=$(opencode run --format json "设计认证模块" | jq -r '.sessionID')
252+
253+
# 第 2 轮:通过 session ID 恢复会话
254+
opencode run -s "$SID" "添加 JWT 校验"
255+
256+
# 第 3 轮:继续同一个会话
257+
opencode run -s "$SID" "补充单元测试"
258+
259+
# 快捷方式:继续最近一次对话(无需 session ID)
260+
opencode run -c "为刚才的修改补充单元测试"
261+
262+
# 查看历史会话列表
263+
opencode session list
264+
opencode session list -n 10 # 只显示最近 10 个
265+
266+
# 从已有会话分叉
267+
opencode run -s "$SID" --fork "尝试另一种实现方案"
268+
```
269+
270+
### Gemini CLI
271+
272+
```bash
273+
# 单次提问(-p = prompt 模式,不打开 TUI)
274+
gemini -p "解释一下 Docker 的工作原理"
275+
276+
# 管道输入
277+
cat src/auth.py | gemini -p "审查这段代码的安全性"
278+
279+
# 指定输出格式(text / json / stream-json)
280+
gemini -p "列出所有 TODO" --output-format json
281+
282+
# 指定模型
283+
gemini -p -m gemini-2.5-flash "快速解释这段代码"
284+
285+
# 自动确认所有工具调用
286+
gemini -p --yolo "运行测试并汇报结果"
287+
288+
# 将项目所有文件纳入上下文
289+
gemini -p --all-files "分析这个项目的架构"
290+
291+
# ── 多轮对话(会话管理)──
292+
# Gemini CLI 不支持预指定 session ID,需从首轮 JSON 输出中提取。
293+
294+
# 第 1 轮:启动会话,通过 JSON 输出获取 session_id
295+
SID=$(gemini --output-format json -p "分析项目的整体架构" | jq -r '.session_id')
296+
297+
# 第 2 轮:通过 session_id 恢复会话
298+
gemini --resume "$SID" "针对刚才的分析,重构认证模块"
299+
300+
# 第 3 轮:继续同一个会话
301+
gemini --resume "$SID" "补充单元测试"
302+
303+
# 快捷方式:恢复最近一次会话(--resume 不带参数,仅适合非并发场景)
304+
gemini --resume "继续上次的对话"
305+
306+
# 查看当前项目所有会话
307+
gemini --list-sessions
308+
```
309+
310+
> **注意**:Gemini CLI 的会话按项目(工作目录)隔离,切换目录后 `--list-sessions` 显示的是不同项目的会话。`-s``--sandbox` 的缩写(安全沙箱),不是会话管理,会话管理使用 `-r` / `--resume`
311+
312+
### 快速对比
313+
314+
| 功能 | Claude Code | Codex | OpenCode | Gemini CLI |
315+
|------|------------|-------|----------|------------|
316+
| 非交互标志 | `-p` | `exec` 子命令 | `run` 子命令 | `-p` |
317+
| 管道输入 | `cat f \| claude -p` | `echo x \| codex exec -` | `--file` 附加文件 | `cat f \| gemini -p` |
318+
| JSON 输出 | `--output-format json` | `--json` | `--format json` | `--output-format json` |
319+
| 指定模型 | `--model opus` | `-m o4-mini` | `-m provider/model` | `-m gemini-2.5-flash` |
320+
| 跳过确认 | `--dangerously-skip-permissions` | `--full-auto` || `--yolo` |
321+
| 预指定会话 ID | `--session-id <UUID>` ||||
322+
| 恢复指定会话 | `--resume <ID>` | `exec resume <ID>` | `-s <ID>` | `--resume <ID>` |
323+
| 继续最近会话 | `-c` | `exec resume --last` | `-c` | `--resume`(无参数) |
324+
| 查看会话列表 ||| `session list` | `--list-sessions` |
325+
| 分叉会话 | `--fork-session` || `--fork` ||
326+
145327
## 沙箱管理
146328

147329
```bash
@@ -203,7 +385,7 @@ sandbox vm start --cpu 6 --memory 8 # 自定义资源启动
203385
- 路径简短清晰
204386
- 不在项目目录内,天然被 `.gitignore` 排除
205387

206-
每个沙箱拥有独立的 AI 工具配置目录(如 `~/.codex-sandboxes/{branch}/`),避免并发冲突和会话污染。Codex、OpenCode、Gemini CLI 创建沙箱时自动从宿主机预植入认证凭据;Claude Code 首次需在容器内 OAuth 登录一次。
388+
每个沙箱拥有独立的 AI 工具配置目录(如 `~/.codex-sandboxes/{branch}/`),避免并发冲突和会话污染。Codex、OpenCode、Gemini CLI 的认证文件通过实时挂载(live mount)与宿主机双向同步,宿主机刷新 token 后容器自动生效;Claude Code 首次需在容器内 OAuth 登录一次。
207389

208390
> 详细的认证机制说明、注册表字段参考和添加新工具指南请参阅 [DEVELOPMENT.md](DEVELOPMENT.md)
209391

docker/sandbox/src/commands/create.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ export async function create(branch: string, base: string | undefined, opts: Cre
174174
const toolVolumes = resolvedTools.flatMap(({ tool, dir }) =>
175175
['-v', `${dir}:${tool.containerMount}`]
176176
);
177+
// Live-mount auth files directly from host (bidirectional sync, always fresh)
178+
const liveMountVolumes = resolvedTools.flatMap(({ tool }) =>
179+
(tool.hostLiveMounts ?? [])
180+
.filter(({ hostPath }) => fs.existsSync(hostPath))
181+
.flatMap(({ hostPath, containerSubpath }) =>
182+
['-v', `${hostPath}:${path.join(tool.containerMount, containerSubpath)}`])
183+
);
177184

178185
run('docker', [
179186
'run', '-d',
@@ -185,6 +192,7 @@ export async function create(branch: string, base: string | undefined, opts: Cre
185192
'-v', `${MAIN_REPO}/.git:${MAIN_REPO}/.git`,
186193
'-v', `${process.env.HOME}/.ssh:/home/devuser/.ssh:ro`,
187194
...toolVolumes,
195+
...liveMountVolumes,
188196
...envArgs,
189197
'-w', '/workspace',
190198
IMAGE_NAME,
@@ -242,8 +250,12 @@ export async function create(branch: string, base: string | undefined, opts: Cre
242250

243251
// Tool credential hints
244252
const toolHints = resolvedTools.map(({ tool, dir }) => {
245-
const hasAuth = tool.authFileName && fs.existsSync(path.join(dir, tool.authFileName));
246-
const hint = hasAuth ? '已从宿主机预植入认证凭据,可直接使用。' : tool.noAuthHint;
253+
const hasLiveAuth = (tool.hostLiveMounts ?? []).some(({ hostPath }) => fs.existsSync(hostPath));
254+
const hasCopiedAuth = tool.authFileName && fs.existsSync(path.join(dir, tool.authFileName));
255+
const hasAuth = hasLiveAuth || hasCopiedAuth;
256+
const hint = hasAuth
257+
? (hasLiveAuth ? '已与宿主机认证凭据实时同步,宿主机刷新后容器自动生效。' : '已从宿主机预植入认证凭据,可直接使用。')
258+
: tool.noAuthHint;
247259
return `${pc.cyan(`${tool.name}:`)}\n ${hint}\n 凭据持久化:${dir}/`;
248260
}).join('\n\n');
249261

docker/sandbox/src/tools.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export interface AiTool {
3939
* Rewrites: HOST_HOME → container home, HOST_PROJECT → /workspace.
4040
*/
4141
pathRewriteFiles?: string[];
42+
/**
43+
* Host files to live-mount into container (bidirectional, always in sync with host).
44+
* Use for auth tokens that expire and need continuous refresh.
45+
* Overlays the sandbox directory mount — file-level bind takes precedence.
46+
*/
47+
hostLiveMounts?: Array<{ hostPath: string; containerSubpath: string }>;
4248
}
4349

4450
/** Validate descriptor consistency at startup — fail fast on misconfiguration. */
@@ -89,9 +95,10 @@ export const AI_TOOLS: readonly Readonly<AiTool>[] = [
8995
sandboxBase: path.join(HOME, '.codex-sandboxes'),
9096
containerMount: '/home/devuser/.codex',
9197
versionCmd: 'codex --version',
92-
hostAuthFile: path.join(HOME, '.codex', 'auth.json'),
93-
authFileName: 'auth.json',
9498
noAuthHint: '首次使用需在容器内运行 codex,按 Esc 选择 Device Code 方式登录。',
99+
hostLiveMounts: [
100+
{ hostPath: path.join(HOME, '.codex', 'auth.json'), containerSubpath: 'auth.json' },
101+
],
95102
postSetupCmds: [
96103
'test -d /workspace/.codex/commands && ln -sfn /workspace/.codex/commands /home/devuser/.codex/prompts || true',
97104
],
@@ -102,19 +109,21 @@ export const AI_TOOLS: readonly Readonly<AiTool>[] = [
102109
sandboxBase: path.join(HOME, '.opencode-sandboxes'),
103110
containerMount: '/home/devuser/.local/share/opencode',
104111
versionCmd: 'opencode version',
105-
hostAuthFile: path.join(HOME, '.local', 'share', 'opencode', 'auth.json'),
106-
authFileName: 'auth.json',
107112
noAuthHint: '首次使用需在容器内配置认证凭据。',
113+
hostLiveMounts: [
114+
{ hostPath: path.join(HOME, '.local', 'share', 'opencode', 'auth.json'), containerSubpath: 'auth.json' },
115+
],
108116
},
109117
{
110118
name: 'Gemini CLI',
111119
npmPackage: '@google/gemini-cli',
112120
sandboxBase: path.join(HOME, '.gemini-sandboxes'),
113121
containerMount: '/home/devuser/.gemini',
114122
versionCmd: 'gemini --version',
115-
hostAuthFile: path.join(HOME, '.gemini', 'oauth_creds.json'),
116-
authFileName: 'oauth_creds.json',
117123
noAuthHint: '首次使用需在容器内运行 gemini 完成认证(支持 Google 登录、API Key、Vertex AI)。',
124+
hostLiveMounts: [
125+
{ hostPath: path.join(HOME, '.gemini', 'oauth_creds.json'), containerSubpath: 'oauth_creds.json' },
126+
],
118127
hostPreSeedFiles: [
119128
{ hostPath: path.join(HOME, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
120129
{ hostPath: path.join(HOME, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' },

0 commit comments

Comments
 (0)