Files
qiming/qimingclaw/docs/sandbox-plan.md

448 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 沙箱多平台测试方案(源码校准版)
> 更新日期2026-04-09
> 审查范围:`SandboxInvoker.ts`、`policy.ts`、`@shared/types/sandbox.ts`、`windows-sandbox-helper/*.rs`
> 审查目标:准确性、完整性、一致性、深度、可操作性、安全性
---
## 1. 以源码为准的当前实现
### 1.1 模式与后端(事实)
- 沙箱模式:`strict | compat | permissive`(默认 `compat`)。
- 后端:`auto | docker | macos-seatbelt | linux-bwrap | windows-sandbox`
- `auto` 映射:`darwin -> macos-seatbelt``linux -> linux-bwrap``win32 -> windows-sandbox`
- `docker``SandboxInvoker.buildInvocation()` 中仅返回未包装命令并记录 warning进程级未实现
### 1.2 macOSseatbelt
- 基线固定是 `(deny default)`
-`permissive`
- 允许 `file-read*`
- 允许 `process-exec` 的 regex`^/usr/bin/``^/bin/``^/usr/lib/`
- 允许 command 本身literal及其 `realpath`
- `compat` 才会额外并入 `startupExecAllowlist`
- `permissive``(allow file-write*)` + `(allow process-exec)` + `(allow signal)`
-`permissive` 的写权限仅来自 `writablePaths`(会同时尝试加入 `realpath`)。
- 额外固定写白名单:`/dev/null``/dev/dtracehelper``/dev/urandom`
### 1.3 Linuxbwrap
- `strict/compat` 都启用:
- `--unshare-user-try --unshare-pid --unshare-uts --unshare-cgroup-try`
- `networkEnabled=false` 时加 `--unshare-net`
- `--tmpfs /tmp`
- 仅绑定 `/dev/null``/dev/urandom``/dev/zero`
- `strict`:只读挂载最小面 + 命令相关目录:
- 固定目录:`/usr /bin /sbin /lib /lib64 /etc /opt /usr/local`
- 额外加入 `dirname(command)`,以及绝对参数对应目录(或目录本身)
- `compat``--ro-bind / /`
- `permissive``--bind / / --dev-bind /dev /dev --proc /proc`,且不做 namespace 隔离;`networkEnabled` 在此模式下不生效。
### 1.4 Windowsqiming-sandbox-helper
- 调用格式:`qiming-sandbox-helper run|serve --mode --cwd --policy-json -- <cmd>`
- `run` 默认 `write_restricted=true``permissive + run` 时会加 `--no-write-restricted`
- `serve` 模式固定 `write_restricted=false`(为允许孙进程)。
- `workspace-write` 且有 `writablePaths`
- `strict` 仅传第一个 root
- `compat/permissive` 传全部 roots。
- helper 内部策略关键点:
- `WorkspaceWrite` 才有 `writable_roots``network_access` 字段。
- `ReadOnly` 固定 `has_full_network_access=false`
- 网络“禁用”通过环境变量与 denybin stub`ssh/scp`)实现,不是内核级网络隔离。
- `compute_allow_paths()` 会在 `WorkspaceWrite` 下始终放行 `command_cwd``TEMP/TMP`
- `.git` deny 仅在被加入的 writable root 下且目录存在时生效。
- `workspace-write` 下 ACE 默认持久化(`persist_aces=true`),不在进程退出时回滚。
### 1.5 回退策略policy.ts
- `enabled=false` => 直接 `type=none`
- 后端不可用:
- `autoFallback=manual``SANDBOX_UNAVAILABLE`
- 否则统一降级 `none`,并 `app.emit("sandbox:unavailable", ...)`
- 当前实现里 `startup-only``session` 没有行为差异(都走统一降级逻辑)。
---
## 2. 发现的问题清单(含修改建议)
### 2.1 critical
1. **Windows 网络隔离被文档描述为“全阻断”,与实现不符**
- 问题helper 仅做环境变量与命令桩劫持,无法强制阻断所有二进制直连网络。
- 位置原文网络章节Windows
- 建议修改明确标注为“best-effort 网络抑制”,并新增绕过测试(自带 socket 客户端二进制)。
2. **Windows strict 写范围描述过度乐观,存在 `cwd` 放行面**
- 问题helper 在 `WorkspaceWrite` 下总是放行 `command_cwd`;若上游未约束 `cwd`,可扩大写面。
- 位置原文“strict 仅第一个 writablePath 可写”。
- 建议修改:文档加入前置条件“调用方必须保证 `cwd` 在工作区内”;新增负例测试。
### 2.2 major
3. **`autoFallback=session` 被描述为会话级差异,但源码无差异实现**
- 问题:`startup-only``session` 当前都降级为 `none`
- 建议修改:文档改为“预留枚举,当前行为一致”;补充后续实现 TODO。
4. **`compat` 的 startup allowlist 价值被夸大**
- 问题:当前两个调用点仅传入 command 本身,通常不增加额外可执行路径。
- 建议修改:文档改为“机制已支持,但当前调用链默认未传额外启动链”。
5. **Linux strict 挂载面文档遗漏 `/usr/local`**
- 问题:源码已包含 `/usr/local`
- 建议修改:更正目录清单,并统一所有章节。
6. **Linux permissive 网络行为描述前后冲突**
- 问题:原文一处写“由 networkEnabled 控制”,另一处写“无隔离”;实现是不会 `unshare-net`
- 建议修改统一为“permissive 下不做 net namespace 隔离”。
7. **Windows read-only 与 `network_access` 字段关系未说明**
- 问题:`ReadOnly` 变体不消费 `network_access`,实际固定无 full network。
- 建议修改在文档中写清“read-only 下网络默认受限,不依赖 policy_json 的 network_access 字段”。
8. **Windows ACL 持久化副作用未纳入测试计划**
- 问题:`workspace-write` 下 ACE 持久化可能造成长期 ACL 污染。
- 建议修改:新增“重复运行/卸载后 ACL 回收”测试与清理策略。
9. **现有 Linux 集成测试样例不可直接执行风险高**
- 问题:测试文件内 `await exec(...)` 用法与 Node API 不匹配,计划中的“可直接执行”不成立。
- 建议修改:文档明确当前仅 macOS/Linux 部分集成用例存在,且需先修复测试脚本后纳入门禁。
### 2.3 minor
10. **术语不一致strict/compact 拼写混用)**
- 建议:统一为 `strict/compat/permissive`
11. **macOS 设备写白名单漏写 `/dev/dtracehelper`**
- 建议:补全为 `/dev/null``/dev/dtracehelper``/dev/urandom`
12. **部分“绝对结论”缺少前置条件**
- 建议:在表格增加“前置条件/调用方约束”列(如 cwd 约束、路径存在性)。
---
## 3. 需要补充的内容
1. **Windows 真实集成测试**(当前仓库缺失)
- `run` / `serve` 两路径;`strict/compat/permissive` 三模式;`read-only/workspace-write` 两子模式。
2. **Windows 安全回归专项**
- `cwd` 越界写入、`TEMP/TMP` 放行面、`.git` deny 仅在存在时生效、world-writable 扫描上限200 子目录 + 1 秒)。
3. **回退可观测性测试**
- 校验 `app.emit("sandbox:unavailable")` 是否被 UI/日志消费;避免“以为开了沙箱,实际 none”。
4. **模式一致性断言**
- `startup-only``session` 当前等价,应有显式测试防止文档与实现再次漂移。
5. **调用方约束测试**
- ACP terminal/create 的 `cwd` 是否被限制在工作区(当前未在 `AcpTerminalManager` 内做路径校验)。
---
## 4. 修订后的测试矩阵(可执行)
### 4.1 已有且可运行(先行)
- 单元测试:
- `src/main/services/sandbox/SandboxInvoker.test.ts`
- `src/main/services/sandbox/sandboxProcessWrapper.test.ts`
- `src/main/services/sandbox/policy.test.ts`
- 集成测试(现有文件):
- `tests/sandbox-integration/macos-seatbelt.integration.test.ts`
- `tests/sandbox-integration/linux-bwrap.integration.test.ts`
执行命令:
```bash
cd crates/agent-electron-client
npm run test:run -- src/main/services/sandbox/SandboxInvoker.test.ts src/main/services/sandbox/sandboxProcessWrapper.test.ts src/main/services/sandbox/policy.test.ts
npm run test:run -- tests/sandbox-integration/macos-seatbelt.integration.test.ts
npm run test:run -- tests/sandbox-integration/linux-bwrap.integration.test.ts
```
### 4.2 必补Windows
| ID | 场景 | 预期 |
|----|------|------|
| WN-01 | `workspace-write + strict``cwd` 在 workspace 内写文件 | 成功 |
| WN-02 | `workspace-write + strict``cwd` 指向 workspace 外 | 必须失败(若成功则判定逃逸) |
| WN-03 | `read-only` 下写 workspace | 失败 |
| WN-04 | `run + permissive``--no-write-restricted`)创建子进程管道 | 成功 |
| WN-05 | `serve` 模式孙进程启动 | 成功 |
| WN-06 | `network_access=false` 下原生 socket 直连外网 | 应失败(若成功,标记为当前设计限制) |
| WN-07 | `.git` denyroot 存在 `.git` 时写入 `.git/index.lock` | 失败 |
| WN-08 | world-writable 目录审计超 200 子目录 | 仅扫描前 200日志可见 |
| WN-09 | `workspace-write` 退出后 ACL 残留检查 | 残留可观测并有清理方案 |
---
## 5. 安全逃逸路径与当前状态
1. **Windows 网络旁路critical**
- 状态:未彻底解决。
- 原因:仅 env/proxy/stub 抑制,无内核网络隔离。
2. **Windows `cwd` 扩写面critical**
- 状态:依赖调用方约束。
- 原因:`compute_allow_paths()` 默认允许 `command_cwd`
3. **Windows ACL 持久化污染major**
- 状态:已存在设计行为。
- 原因:`workspace-write` 设置 `persist_aces=true`
4. **Linux permissive 全盘可写known risk**
- 状态:设计如此,仅用于排障。
- 控制:禁止作为默认策略,需审计日志。
5. **回退到 `none` 的误感知风险major**
- 状态:已有 `sandbox:unavailable` 事件,但需 UI 与监控闭环。
---
## 6. CI 门禁建议(修订)
1. `PR required`:三份单元测试必须通过。
2. `platform required`macOS + Linux 集成测试通过。
3. `release required`Windows 集成测试通过后再允许发布。
4. `security required`
- 回退到 `none` 必须有告警事件;
- permissive 运行必须记录 warning
- Windows 网络“best-effort”限制必须在报告中明确声明。
---
## 7. 里程碑(更新)
| 周次 | 目标 |
|------|------|
| 第1周 | 修正文档与现有测试脚本可执行性(尤其 Linux 集成脚本) |
| 第2周 | 补齐 Windows helper 集成测试WN-01~WN-05 |
| 第3周 | 完成 Windows 安全回归WN-06~WN-09 |
| 第4周 | 打通 CI 门禁与降级告警可视化 |
---
## 8. 第二轮复审:第一轮修正核验结果(逐项对源码)
> 结论:第一轮 12 条修正中10 条“准确”2 条“部分准确(需补充限定)”。
| ID | 第一轮修正 | 复审结论 | 源码依据 |
|----|------------|----------|----------|
| R1 | Windows 网络并非“全阻断” | ✅ 准确 | `windows-sandbox-helper/src/env.rs``apply_no_network_to_env()` 仅设置代理/离线变量与 denybinssh/scp非内核级隔离 |
| R2 | strict 写范围受 `cwd` 影响 | ✅ 准确 | `acpTerminalManager.ts``cwd` 加入 `writablePaths``allow.rs``compute_allow_paths()` 总是放行 `command_cwd` |
| R3 | `autoFallback=session``startup-only` 当前无行为差异 | ✅ 准确 | `policy.ts` 中 backend 不可用时,非 `manual` 统一返回 `type=none,degraded=true` |
| R4 | compat 启动链 allowlist 被高估 | ⚠️ 部分准确 | 机制有效;`sandboxProcessWrapper.ts` 当前仅传 `[originalCommand]`,但未来可扩展,文档应写“当前调用链价值有限” |
| R5 | Linux strict 漏写 `/usr/local` | ✅ 准确 | `SandboxInvoker.ts` strict ro-bind 列表包含 `/usr/local` |
| R6 | Linux permissive 网络描述冲突 | ✅ 准确 | `SandboxInvoker.ts` permissive 分支不做 `--unshare-net``networkEnabled` 在此分支不生效 |
| R7 | Windows read-only 与 `network_access` 关系未说明 | ✅ 准确 | `policy.rs` `ReadOnly => has_full_network_access=false`,不消费 `network_access` 字段 |
| R8 | ACL 持久化副作用未纳入测试 | ✅ 准确 | `main.rs` `persist_aces = is_workspace_write`cleanup 仅在 `!persist_aces` 回滚 ACE |
| R9 | Linux 集成测试可执行性描述偏乐观 | ✅ 准确 | `linux-bwrap.integration.test.ts` 存在 `await exec("which bwrap")` 等 callback API 误用 |
| R10 | strict/compact 术语混用 | ✅ 准确 | 当前实现统一为 `strict/compat/permissive``@shared/types/sandbox.ts` |
| R11 | macOS 设备白名单漏写 `/dev/dtracehelper` | ✅ 准确 | `SandboxInvoker.ts` 固定写白名单含 `/dev/dtracehelper` |
| R12 | 缺少前置条件声明 | ⚠️ 部分准确 | 实现已隐含多项调用方前置约束(如 `cwd`、路径存在、MCP 进程 env文档需显式化 |
---
## 9. 第二轮新增发现(第一轮遗漏)
### 9.1 环境变量链路存在“分层过滤不一致”
- `AcpTerminalManager`Windows `terminal/create` 路径)确实使用 `safeKeys` 最小白名单构造 env可降低敏感变量泄露面
-`windows-sandbox-helper``SandboxContext::setup()``std::env::vars()` 作为基底,`env.rs` 内没有独立的 `safeKeys` 白名单逻辑。
- `sandboxed-bash-mcp.mjs` 执行 helper 时使用 `env = { ...process.env }`,仅删除 `ELECTRON_RUN_AS_NODE`,因此若 MCP 进程继承到敏感变量,可能继续传入 helper/子命令。
结论需要把“safeKeys 白名单”在文档中明确为“仅 terminal/create 路径有效,不是 helper 全局机制”,并新增泄露测试(见第 12 节)。
### 9.2 `sandboxed-bash-mcp` 强制 `--no-write-restricted`
- MCP 脚本构造 helper 参数时固定包含 `run --no-write-restricted`
- 这意味着该路径始终依赖 DACL ACE 控制写权限,不使用 WRITE_RESTRICTED token 的二次约束。
结论:文档应把该行为从“可选/模式相关”改为“当前实现固定开启”。
### 9.3 world-writable 审计是“尽力而为”,且不会 fail-closed
- `audit.rs` 只扫描 `cwd` 直接子目录,最多 200 个,最长 1 秒。
- 发现风险目录后仅尝试加 deny ACE失败仅记日志不阻断运行。
- 对工作区 root 下目录(`starts_with(workspace_roots)`)会跳过 deny。
结论:文档需明确这是补偿性审计,不是强制安全边界。
---
## 10. 深度审查补充(指定模块)
### 10.1 AcpTerminalManagerWindows `terminal/create` 路径)
- Windows + helper 可用时:`terminal/create` 通过 `SandboxInvoker.buildInvocation(...subcommand=run)``qiming-sandbox-helper run``parseJson=true`
- `writablePaths` 传参为 `[workspaceRoot, cwd]`strict 下 helper policy 仅保留首个 root但 helper 内仍会允许 `command_cwd`)。
- env 构建:
- 沙箱路径:仅取 `safeKeys`
- 非沙箱路径:透传宿主 env
- `params.env` 直接覆盖(无二次过滤)。
- 直接执行分支macOS/Linux 或未启用 helper在 Windows 下会 `shell: true`helper 分支 `shell: false`
### 10.2 sandboxed-bash-mcpWindows 专属 MCP 注入)
- 仅在 `engine=claude-code``windows-sandbox` 启用时注入,且通过 `_meta` 禁用内置 Bash。
- MCP 工具名仍为 `Bash`,实际命令固定走 helper
- `run --no-write-restricted --mode <...> --cwd <process.cwd()> --policy-json <...> -- <bash/powershell> -c/-Command <user command>`
- shell 优先级:
- 优先 Git Bash支持 bash 语法);
- 否则回退 PowerShell。
- 该路径的 env 继承策略与 `terminal/create` 不同(见 9.1)。
### 10.3 env.rs环境变量处理
- 当前职责是网络抑制、分页器设置、`/dev/null` 归一化,不负责敏感键白名单过滤。
- `apply_no_network_to_env()` 会注入代理与离线相关变量,并 prepend denybin`ssh/scp` stub
- 实际安全语义是“网络访问抑制 + 常见命令劫持”,不是“环境最小化”。
### 10.4 audit.rsworld-writable 扫描)
- 扫描范围:仅 `cwd` 下一级目录,忽略 symlink/非目录。
- 资源上限:`MAX_CWD_CHILDREN=200``AUDIT_TIME_LIMIT_SECS=1`
- 判定:检查 DACL 中是否存在 world SID 的 write allow ACE。
- 处置:尝试对 capability SID 添加 deny write ACE失败仅日志流程继续。
### 10.5 acl.rsDACL/ACE 细节)
- `add_allow_ace`:为 capability SID 授予 `FILE_GENERIC_READ|WRITE|EXECUTE`,继承到子对象。
- `add_deny_write_ace`deny mask 包含 `FILE_GENERIC_WRITE``FILE_WRITE_*` 细项。
- `revoke_ace`cleanup 阶段用 `REVOKE_ACCESS` 回收(仅 `persist_aces=false` 时执行)。
- `allow_null_device`:对 `\\.\NUL` 追加 allow ACE避免 null 设备访问异常。
### 10.6 token.rsRestricted Token
- 基础标志:`DISABLE_MAX_PRIVILEGE | LUA_TOKEN`
- `write_restricted=true` 时额外加 `WRITE_RESTRICTED`,并附加 3 个 restricting SIDs
- capability SID
- 当前 logon SID
- everyone SID
- `write_restricted=false`serve 或 run+`--no-write-restricted`)时不加 restricting SIDs主要依赖 DACL。
- 创建后仅重新启用 `SeChangeNotifyPrivilege`
---
## 11. 与既有文档发现的纳入情况
### 11.1 `sandbox/TEST-PLAN.md` 纳入状态
| 项 | 状态 | 备注 |
|----|------|------|
| Gap-1~Gap-4signal/exec/dev-bind/降级告警) | 已纳入 | 第 1/2/6 节已覆盖 |
| macOS/Linux 集成思路 | 部分纳入 | 已引用测试文件,但需补“当前脚本可执行性风险” |
| Windows 集成缺口 | 已纳入 | 第 4 节 WN-01~WN-09 |
| 并发/长稳/泄漏 | 未充分纳入 | 本轮第 12 节补全 |
### 11.2 `sandbox/CODE-REVIEW.md` 纳入状态
- 该文档多数问题属于 `DockerSandbox/PermissionManager/WorkspaceManager` 通用模块,不全是“多平台 sandbox-plan”范围。
- 与本计划直接相关且应跟踪的项:
- 降级可观测性(已纳入)
- 测试覆盖不足(已纳入)
- 安全检测可绕过类问题(应在独立 `PermissionManager` 安全计划跟进,不并入本计划的门禁结论)
结论:`CODE-REVIEW` 需在文档中标注“范围交集清单”,避免误解为“全部已在本计划闭环”。
---
## 12. 新增测试用例(第二轮补充)
### 12.1 环境变量泄露测试
| ID | 路径 | 步骤 | 预期 |
|----|------|------|------|
| ENV-01 | `terminal/create` | 预置宿主 `ANTHROPIC_API_KEY=leak_test`,在沙箱内执行 `env` | 输出中不应出现该键(除非通过 `params.env` 显式注入) |
| ENV-02 | `sandboxed-bash-mcp` | 同样预置敏感键,经 MCP Bash 执行 `set`/`env` | 若出现敏感键,标记高危并推动 MCP 路径加白名单过滤 |
| ENV-03 | helper `run` 直调 | 传入最小 env 与全量 env 对比 | 证明 helper 无内置 `safeKeys` 过滤(文档与实现一致) |
### 12.2 并发沙箱实例测试
| ID | 场景 | 预期 |
|----|------|------|
| CONC-01 | 50 个并发 `terminal/create`(上限) | 全部可创建并可回收 |
| CONC-02 | 第 51 个 `terminal/create` | 返回“Terminal limit reached”错误 |
| CONC-03 | 并发 `run` + `serve` 混合 | 无死锁;退出码与输出互不串扰 |
### 12.3 长时间运行稳定性
| ID | 场景 | 指标/阈值 |
|----|------|-----------|
| SOAK-01 | 单实例 `serve` 连续 24h | 无异常退出;内存增长斜率可控(例如 < 5%/h |
| SOAK-02 | 周期性 `run`(每分钟一次,持续 12h | 成功率 >= 99.9%,无句柄持续上涨 |
### 12.4 资源泄漏测试
| ID | 场景 | 预期 |
|----|------|------|
| LEAK-01 | 批量创建/释放 terminal 1000 次 | 无孤儿进程、无句柄持续累积 |
| LEAK-02 | `workspace-write` 退出后 ACL 检查 | 记录持久 ACE 残留;清理脚本可回收 |
| LEAK-03 | seatbelt profile 清理 | `sandboxProcessWrapper` cleanup 后临时 `.sb` 文件不存在 |
---
## 13. CI/CD 细化GitHub Actions 可落地模板)
### 13.1 Workflow 建议
1. `sandbox-unit.yml`
- 触发:`pull_request``push` 到主干
- 矩阵:`ubuntu-latest`, `macos-latest`, `windows-latest`
- 运行:`SandboxInvoker/policy/sandboxProcessWrapper` 单元测试
2. `sandbox-integration-unix.yml`
- 触发:`pull_request`
- 矩阵:`ubuntu-latest`, `macos-latest`
- 运行:`tests/sandbox-integration/*`(先修复 Linux 脚本 API 误用)
3. `sandbox-integration-windows.yml`
- 触发:`workflow_dispatch` + `release/*` 分支必跑
- 运行WN/ENV/CONC/SOAK/LEAK 中可自动化子集
4. `sandbox-security-report.yml`
- 汇总各 job 产物,生成单一 `sandbox-report.json` + Markdown 摘要并上传 artifact。
### 13.2 报告格式与指标
- 建议统一 JSON schema
- `platform`, `backend`, `mode`, `case_id`, `status`, `duration_ms`, `exit_code`, `notes`
- 关键指标:
- `pass_rate`(按平台/后端/模式分组)
- `degrade_to_none_count`
- `policy_violation_count`(如 strict 下越权写成功)
- `env_leak_count`
- `resource_leak_signals`handle/process/tempfile/acl
- PR 门禁阈值建议:
- `critical` 用例 100% 通过
- `major` 用例 >= 99%
- `env_leak_count == 0`
- `degrade_to_none_count` 必须附带告警记录与原因
---
## 14. 迁移与兼容性补充
### 14.1 旧版本策略迁移
- 已有兼容:`policy.ts` 支持 `windows.sandbox.mode -> windowsMode` 映射。
- 建议新增迁移规则:
1. 缺失 `autoFallback` 时默认补 `startup-only`
2. 缺失 `mode` 时默认补 `compat`
3. 旧值非法时回退到 `DEFAULT_SANDBOX_POLICY` 并记录一次迁移日志。
### 14.2 跨版本兼容要求
1. 新增字段必须向后兼容unknown 字段忽略,不影响旧客户端读取)。
2. 禁止静默改变默认安全语义:
- 例如 `windowsMode` 默认值变更必须伴随版本门禁与迁移提示。
3. `SandboxAutoFallback` 新枚举落地前,需先保证旧版本按“最安全可运行”路径降级(当前即 `none + degraded=true + 事件告警`)。
4. helper 与主程序版本协商:
- 建议在 helper 启动时输出 `version/capabilities`,主程序按能力选择参数,避免跨版本参数不兼容。
### 14.3 升级回滚策略
- 升级前备份 `sandbox_policy`SQLite
- 新版本启动首轮执行策略 normalize + migrate并写入 `policy_migration_audit` 日志。
- 回滚时保留可识别字段,删除新版本专有字段(或降级到默认策略),确保旧版本不崩溃。