23 KiB
沙箱白名单/黑名单矩阵与验证计划(跨平台 + 多模式)
更新日期:2026-04-09 上游审查依据:
sandbox-plan.md(两轮审查,12 条问题已确认)
1. 范围边界
1.1 In Scope
- 平台:macOS / Linux / Windows
- 后端:
macos-seatbelt/linux-bwrap/windows-sandbox/docker(当前未实现,显式标注unsupported) - 沙箱模式(
mode):strict/compat/permissive(全平台通用,含 Windows) - Windows 子模式(
windowsMode,独立维度):read-only/workspace-write,与mode正交组合 - 双层覆盖:
- 沙箱层:文件系统、网络、进程执行、设备访问的 allow/block/conditional 行为
- 命令权限层:
PermissionManager的 safeCommands 白名单与危险命令黑名单行为
1.2 Out of Scope
- GUI Agent 沙箱策略与测试
- 非沙箱业务功能
2. 核心设计决策
决策 1:矩阵是"规范文档"(normative),而非从实现反向生成
若矩阵从实现代码自动提取,则实现的 bug 会被误记为"正确行为",规范价值丧失。
确定方向:矩阵由人工审查后手动维护 → 测试验证实现是否符合矩阵 → CI 检查矩阵与生成文档是否同步。
手工维护 sandbox-matrix.spec.json ← 专家审查 + PR review
↓
测试驱动(从 spec 生成表驱动用例)
↓
验证实现是否符合矩阵
↓
CI: spec → 生成 Markdown,git diff 检查一致性
生成脚本(crates/agent-electron-client/scripts/generate-sandbox-matrix-doc.js)的职责是:spec.json → .md,方向单向,不反向提取。
决策 2:两层矩阵严格分离
| 层 | Spec 文件 | 内容 |
|---|---|---|
| 沙箱层 | docs/sandbox/sandbox-matrix.spec.json |
平台/后端/模式下各操作的 verdict |
| 命令权限层 | docs/sandbox/permission-matrix.spec.json |
safeCommands 白名单 + 危险命令黑名单行为 |
两层不混入同一文件,避免审查与测试范围混淆。
3. 产物定义
3.1 规范文件(手工维护,单一真源)
docs/sandbox/sandbox-matrix.spec.json — 沙箱层:
{
"cases": [
{
"id": "MAC-SEATBELT-COMPAT-workspace_write",
"platform": "darwin",
"backend": "macos-seatbelt",
"mode": "compat",
"operationId": "workspace_write",
"verdict": "allow",
"evidence": "SandboxInvoker.ts buildSeatbelt: writablePaths → allow file-write*"
},
{
"id": "WIN-SANDBOX-COMPAT-WW-network_external",
"platform": "win32",
"backend": "windows-sandbox",
"mode": "compat",
"windowsMode": "workspace-write",
"operationId": "network_external",
"verdict": "conditional",
"condition": "best-effort suppression via env vars + denybin stubs (ssh/scp); does not block raw socket connections",
"evidence": "windows-sandbox-helper/src/env.rs apply_no_network_to_env()"
},
{
"id": "ALL-DOCKER-ANY-any",
"platform": "*",
"backend": "docker",
"mode": "*",
"operationId": "*",
"verdict": "unsupported",
"reason": "process-level docker wrapping not implemented; SandboxInvoker returns unwrapped command with warning log"
}
]
}
操作枚举(operationId):
| ID | 说明 |
|---|---|
workspace_write |
工作区内写文件 |
outside_workspace_write |
工作区外写文件 |
system_path_write |
系统路径写(/etc, /usr, C:\Windows) |
file_read |
任意路径读 |
network_external |
外网 TCP/UDP |
network_loopback |
loopback(127.0.0.1)访问 |
process_exec_whitelist |
白名单路径可执行文件 |
process_exec_arbitrary |
任意路径可执行文件 |
privilege_escalation |
sudo/su/提权命令 |
device_write |
/dev 设备节点写 |
Verdict 语义:
| 值 | 含义 | 必填字段 |
|---|---|---|
allow |
操作被允许 | — |
block |
操作被阻断 | — |
conditional |
取决于前置条件或调用方约束 | condition |
unsupported |
该组合当前未实现 | reason |
所有条目必须有 evidence 字段,引用具体源码位置(如 SandboxInvoker.ts:L123)。
通配符语义:"*" 表示"适用于该维度的所有值"(如 "platform": "*" 表示全平台)。测试代码需显式处理:c.platform === process.platform || c.platform === "*"。
docs/sandbox/permission-matrix.spec.json — 命令权限层:
{
"safeCommands": {
"commands": ["node", "npm", "npx", "git", "ls", "cat", "..."],
"expectedBehavior": "auto-approve for command:execute without user confirmation",
"evidence": "PermissionManager.ts DEFAULT_PERMISSION_POLICY.safeCommands"
},
"dangerousCommands": {
"cases": [
{
"id": "PERM-SUDO",
"input": "sudo rm -rf /",
"matchedPattern": "sudo",
"expectedResult": "allowed: false",
"evidence": "PermissionManager.ts DANGEROUS_COMMANDS + checkDangerousOperation word-boundary regex"
},
{
"id": "PERM-CHMOD-777",
"input": "chmod 777 /etc",
"matchedPattern": "chmod 777",
"expectedResult": "allowed: false"
},
{
"id": "PERM-CHMOD-755-FP",
"input": "chmod 755 file",
"matchedPattern": null,
"expectedResult": "allowed: true",
"note": "word-boundary regression: must NOT match 'chmod 777' pattern"
}
]
},
"sensitivePaths": {
"cases": [
{
"id": "PERM-SSH",
"type": "file:write",
"target": "/home/user/.ssh/id_rsa",
"expectedResult": "allowed: false"
},
{
"id": "PERM-ETC-PASSWD",
"type": "file:write",
"target": "/etc/passwd",
"expectedResult": "allowed: false",
"note": "sensitiveEtcPaths 精确匹配;/etc/hosts 当前不在阻止列表中"
}
]
},
"denyList": {
"cases": [
{
"id": "PERM-SYS-PKG",
"type": "package:install:system",
"target": "curl",
"expectedResult": "allowed: false, no confirmation prompt"
}
]
}
}
3.2 已知 conditional 条目(需在 spec 中显式标注)
| 组合 | operationId | condition |
|---|---|---|
Windows / any / workspace_write strict |
outside_workspace_write |
helper 始终放行 command_cwd;调用方必须保证 cwd 在 workspace 内 |
Windows / windows-sandbox / any |
network_external |
best-effort env/stub 抑制,不是内核级隔离 |
All / autoFallback=session |
全部 | 当前与 startup-only 行为一致,预留枚举,TBD |
macOS seatbelt / sudo exec |
process_exec_whitelist |
/usr/bin/sudo 在 exec 白名单(^/usr/bin/),可被执行;但权限提升被 seatbelt deny,结果为 conditional |
Windows / sandboxed-bash-mcp |
全部 | MCP 路径固定传 --no-write-restricted,不使用 WRITE_RESTRICTED token;依赖 DACL ACE 控制写权限 |
Linux / linux-bwrap / permissive |
network_external, network_loopback |
permissive 走独立代码分支(SandboxInvoker.ts:228-239),不执行 --unshare-net;即使 networkEnabled=false 也不隔离网络 |
3.3 生成产物(CI 检查)
docs/sandbox/sandbox-matrix.generated.md— 由crates/agent-electron-client/scripts/generate-sandbox-matrix-doc.js从 spec 生成,提交至仓库sandbox-report.json— 测试执行结果汇总,上传 CI artifact
CI 一致性检查:每次 spec 变更后重新生成 .md;若生成结果与仓库已提交版本不一致,PR 门禁失败。
3.4 CI 报告 schema
{
"platform": "darwin|linux|win32",
"backend": "macos-seatbelt|linux-bwrap|windows-sandbox",
"mode": "strict|compat|permissive",
"windowsMode": "read-only|workspace-write|null",
"case_id": "MAC-SEATBELT-COMPAT-workspace_write",
"status": "pass|fail|skip",
"duration_ms": 123,
"exit_code": 0,
"notes": ""
}
关键指标:pass_rate(按平台/后端/模式分组)、degrade_to_none_count、env_leak_count、policy_violation_count。
4. 测试方案
4.1 沙箱层核心测试清单
注:Windows 列的行为取决于
mode×windowsMode组合,下表以compat+workspace-write(默认配置)为基准。
| 操作 | macOS seatbelt | Linux bwrap | Windows sandbox(compat + workspace-write) |
|---|---|---|---|
| workspace 内写 | allow | allow | allow |
| workspace 外写 | block | block | conditional(helper 放行 command_cwd,需调用方约束) |
| 系统路径写/删 | block | block | block |
| 外网访问 | 按 networkEnabled(true → (allow network*),适用所有模式) |
strict/compat:按 networkEnabled(false → --unshare-net);permissive:不隔离网络(走独立分支,--unshare-net 不执行) |
conditional(best-effort env/stub,非内核级) |
| loopback 访问 | 同外网,受 networkEnabled 控制(与 mode 无关) |
同外网(permissive 例外:始终可访问) | 需实测 |
sudo(/usr/bin/sudo) |
conditional:exec 被放行(路径在白名单 ^/usr/bin/),但权限提升被 seatbelt deny |
block(/usr/bin/ 不在 strict 挂载面 / bwrap 命名空间隔离) |
block(PermissionManager 层拦截) |
reboot/shutdown(/sbin/) |
block(/sbin/ 不在 exec 白名单) |
block | block |
workspace 内写(read-only 模式时) |
N/A | N/A | block(ReadOnly 策略无 writable_roots) |
4.2 命令权限层测试(PermissionManager)
新增测试文件:src/main/services/sandbox/PermissionManager.test.ts
关键断言:
注意:checkPermission 返回 Promise<PermissionResult>({ allowed: boolean; reason: string }),不抛出异常。危险操作返回 allowed: false,不进入用户确认队列。
// safeCommands 自动放行
it("should auto-approve safeCommands", async () => {
for (const cmd of DEFAULT_PERMISSION_POLICY.safeCommands) {
const r = await pm.checkPermission(sid, "command:execute", cmd);
expect(r.allowed).toBe(true);
}
});
// 危险命令拦截(直接返回 allowed:false,不抛出)
it("should block dangerous commands", async () => {
const sudo = await pm.checkPermission(sid, "command:execute", "sudo rm -rf /");
expect(sudo.allowed).toBe(false);
const nc = await pm.checkPermission(sid, "command:execute", "nc -l 8080");
expect(nc.allowed).toBe(false);
});
// word-boundary 回归:DANGEROUS_COMMANDS 包含 "chmod 777" 字面串
// 应 block "chmod 777 file",不应误杀 "chmod 755 file"
it("should not false-positive on chmod 755", async () => {
const block = await pm.checkPermission(sid, "command:execute", "chmod 777 /etc");
expect(block.allowed).toBe(false);
const safe = await pm.checkPermission(sid, "command:execute", "chmod 755 file");
expect(safe.allowed).toBe(true); // 755 不在危险命令黑名单
});
// 敏感路径拦截
// 注意:sensitiveEtcPaths 仅含 /etc/passwd, /etc/shadow, /etc/sudoers, /etc/group
// /etc/hosts 当前 **不在** 阻止列表中(待评估是否需要补入)
it("should block file:write to sensitive paths", async () => {
const passwd = await pm.checkPermission(sid, "file:write", "/etc/passwd");
expect(passwd.allowed).toBe(false);
const shadow = await pm.checkPermission(sid, "file:write", "/etc/shadow");
expect(shadow.allowed).toBe(false);
// .ssh 检测使用 .includes(".ssh"),存在子串误判风险
// "not.sshrc" 也会被拦截(已知限制,需评估是否改为路径段匹配)
const ssh = await pm.checkPermission(sid, "file:write", "/home/user/.ssh/id_rsa");
expect(ssh.allowed).toBe(false);
});
// workspaceOnly 注意:PermissionManager 中 workspaceOnly 检查为占位注释,
// 实际路径边界由 WorkspaceManager 在执行时校验。
// 此处仅测试 PermissionManager 不会越权放行 denyList/dangerousOp。
// system packages 在 denyList 中 → checkDangerousOperation 直接返回 allowed:false
it("should deny package:install:system without user confirmation", async () => {
const r = await pm.checkPermission(sid, "package:install:system", "curl");
expect(r.allowed).toBe(false);
// 验证:不应出现在待审批队列中(即未调用 requestPermission)
});
4.3 表驱动集成测试(复用 spec)
新增文件:tests/sandbox-integration/matrix-driven.test.ts
import spec from "../../docs/sandbox/sandbox-matrix.spec.json";
const runnable = spec.cases.filter(c =>
(c.platform === process.platform || c.platform === "*") &&
c.verdict !== "unsupported"
);
describe.each(runnable)("[$id] $platform/$backend/$mode $operationId", (tc) => {
it(`verdict: ${tc.verdict}`, async () => {
const result = await runSandboxedOperation(tc);
if (tc.verdict === "block") {
expect(result.exitCode).not.toBe(0);
} else if (tc.verdict === "allow") {
expect(result.exitCode).toBe(0);
}
// conditional: 由 tc.condition 描述约束,测试结果写入 notes 字段
});
});
runSandboxedOperation 职责:
- 根据
tc.backend构造SandboxInvoker实例 - 根据
tc.operationId映射到具体 shell 操作(如workspace_write→echo test > <workspaceDir>/test.txt,network_external→curl -s --max-time 2 https://example.com) - 调用
buildInvocation()包装命令,spawn执行,返回{ exitCode, stdout, stderr } - 需维护一个
operationId → shell command映射表,确保测试操作可复现
4.4 环境变量泄露测试
新增文件:tests/sandbox-integration/env-leak.test.ts
| ID | 路径 | 步骤 | 预期 |
|---|---|---|---|
| ENV-01 | terminal/create |
预置 ANTHROPIC_API_KEY=leak_test,沙箱内执行 env |
输出不含该键 |
| ENV-02 | sandboxed-bash-mcp |
同上,经 MCP Bash 路径 | 若出现则标记高危(当前已知风险:仅删 ELECTRON_RUN_AS_NODE) |
| ENV-03 | helper 直调 | 对比最小 env vs 全量 env | 证明 helper 无内置 safeKeys 过滤(文档与实现一致) |
4.5 降级可观测性测试
文件:tests/sandbox-integration/fallback-observability.test.ts
| ID | 场景 | 预期 |
|---|---|---|
| OBS-01 | 模拟 backend 不可用(autoFallback 非 manual),检查 app.emit("sandbox:unavailable") 是否触发 |
事件必须触发,并携带 reason |
| OBS-02 | 降级后实际 sandbox type 为 none,日志中必须有明确记录 |
日志可查,不静默 |
| OBS-03 | permissive 模式启动时,必须有 warning 日志 |
warning 可查 |
4.6 Windows 集成测试(WN 系列,来自 sandbox-plan.md §4.2)
| 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 直连外网 |
应失败(若成功,标记为当前设计限制并写入矩阵 conditional) |
| WN-07 | .git deny:root 存在 .git 时写入 .git/index.lock |
失败 |
| WN-08 | world-writable 目录审计超 200 子目录 | 仅扫描前 200,日志可见 |
| WN-09 | workspace-write 退出后 ACL 残留检查 |
残留可观测并有清理方案 |
5. CI 门禁(分层)
新增 workflow:.github/workflows/sandbox-gates.yml
5.1 PR 必跑
| Job | 说明 |
|---|---|
sandbox-unit |
matrix: ubuntu/macos/windows,运行 src/main/services/sandbox/ 单元测试 |
sandbox-matrix-consistency |
generate-sandbox-matrix-doc.js 生成后 git diff --exit-code,不一致则失败 |
sandbox-integration-unix |
matrix: ubuntu/macos,运行 tests/sandbox-integration/ |
5.2 Release 分支必跑
| Job | 说明 |
|---|---|
sandbox-integration-windows |
runs-on: windows-latest,运行 WN-01~WN-09 可自动化子集 |
sandbox-security-report |
汇总所有 job 产物,生成 sandbox-report.json artifact |
5.3 门禁阈值
system_path_write/outside_workspace_writecritical 用例:100% blockenv_leak_count == 0(ENV-01~ENV-03)degrade_to_none_count > 0时必须有对应sandbox:unavailable事件日志
6. 交付物一览
| 文件 | 类型 | 说明 |
|---|---|---|
docs/sandbox/sandbox-matrix.spec.json |
规范(手工维护) | 沙箱层单一真源 |
docs/sandbox/permission-matrix.spec.json |
规范(手工维护) | 命令权限层单一真源 |
docs/sandbox/sandbox-matrix.generated.md |
生成(CI 检查) | 从 spec 生成,人类可读 |
crates/agent-electron-client/scripts/generate-sandbox-matrix-doc.js |
工具 | spec.json → .md 单向转换 |
crates/agent-electron-client/scripts/generate-sandbox-report.js |
工具 | 测试结果 → sandbox-report.json |
src/main/services/sandbox/PermissionManager.test.ts |
测试(新增) | 命令权限层专项 |
tests/sandbox-integration/matrix-driven.test.ts |
测试(新增) | 从 spec 驱动的沙箱层集成测试 |
tests/sandbox-integration/env-leak.test.ts |
测试(新增) | 环境变量泄露测试 |
tests/sandbox-integration/fallback-observability.test.ts |
测试(新增) | 降级可观测性测试(OBS-01~03) |
.github/workflows/sandbox-gates.yml |
CI(新增) | 分层门禁 workflow |
7. 执行顺序:macOS → Windows → Linux
Phase 1:macOS(Week 1~2)
- Week 1:手工编写
sandbox-matrix.spec.json(macOS seatbelt 为第一批 verdict,含sudoconditional 条目) - Week 1:新增
PermissionManager.test.ts(allowed字段断言、word-boundary 回归;平台无关,最早可并行) - Week 1:新增
fallback-observability.test.ts(OBS-01~03;平台无关,最早可并行) - Week 2:新增
env-leak.test.tsENV-01~ENV-02(terminal/create + sandboxed-bash-mcp) - Week 2:实现
matrix-driven.test.ts(先只运行 macOS 部分);打通 macOS seatbelt 集成门禁
Phase 2:Windows(Week 3)
- 补写
sandbox-matrix.spec.jsonWindows 部分(conditional:cwd 约束、网络 best-effort、sandboxed-bash-mcp--no-write-restricted) - 实现 WN-01~WN-05(基础行为:workspace write、read-only、run/serve)
- ENV-03 + WN-06~WN-09(安全回归:网络旁路、ACL 持久化清理)
- 打通
sandbox-integration-windows.yml(release 分支门禁)
Phase 3:Linux(Week 4)
- 修复
linux-bwrap.integration.test.tscallback API 误用(改为promisify(exec)或execa) - 补写
sandbox-matrix.spec.jsonLinux bwrap 部分;扩展matrix-driven.test.ts - 打通全平台
sandbox-gates.yml;生成sandbox-report.json
8. 已锁定假设
- GUI Agent 沙箱不纳入本计划。
- 采用双层模型(沙箱层 + 命令权限层),两层矩阵严格分离,不混入同一 spec 文件。
- 交付形式固定为 JSON + Markdown + 自动化测试。
- CI 采用分层门禁:PR 强制单元测试 + 一致性检查;Windows 集成在 release 分支强制。
- 矩阵为规范(normative)文档,人工维护;生成脚本仅做 spec→md,不从实现反向提取。
docker全平台显式标注unsupported,不伪装为已支持。autoFallback=session与startup-only当前等价,矩阵注释标明"预留枚举,TBD"。- Windows 的
mode(strict/compat/permissive)与windowsMode(read-only/workspace-write)是正交组合,矩阵条目需覆盖两个维度。 - CONC(并发)、SOAK(长稳)、LEAK(资源泄漏)测试不纳入本计划,作为后续独立专项。
9. 与现有文档的关系
sandbox-plan.md:保留为"问题发现与决策记录"文档,本计划不替换它。本计划将其 §12 中的 ENV 测试用例和 §13(CI 建议)转化为可执行代码与 workflow。§12 中的 CONC(并发)、SOAK(长稳)、LEAK(资源泄漏)测试用例当前不纳入本计划,作为后续独立专项跟进。sandbox/TEST-PLAN.md、sandbox/CODE-REVIEW.md:已有内容仍有效,本计划以 spec + 表驱动测试为主干,与之互补。sandbox-matrix.spec.json:作为"当前正确行为"的规范真源,与sandbox-plan.md中的问题列表形成问题→修复→验证闭环。
10. Deep Research 发现(实施前需评估)
以下问题在源码审查中发现,需在 Phase 1 实施前决定处置方式(修复 or 标注为已知限制):
10.1 PermissionManager 层
| # | 问题 | 严重度 | 源码位置 | 建议 |
|---|---|---|---|---|
| DR-1 | /etc/hosts 不在 sensitiveEtcPaths 列表中,当前不会被拦截 |
中 | PermissionManager.ts:651-657 | 评估是否补入;若不补入则在矩阵中标注为 allow |
| DR-2 | .ssh 检测用 lowerTarget.includes(".ssh"),会误拦含 .ssh 子串的非 SSH 路径(如 not.sshrc) |
中 | PermissionManager.ts:642 | 改为路径段匹配(如 /.ssh/ 或 path.basename 检查) |
| DR-3 | workspaceOnly 在 PermissionManager 中为注释占位,实际由 WorkspaceManager 执行路径边界检查 |
高 | PermissionManager.ts:203-205 | 测试需跨模块验证(PermissionManager + WorkspaceManager 联合),不能仅测 PermissionManager |
10.2 沙箱层
| # | 问题 | 严重度 | 源码位置 | 建议 |
|---|---|---|---|---|
| DR-4 | Linux permissive 模式走独立代码分支,不执行 --unshare-net(即使 networkEnabled=false) |
中 | SandboxInvoker.ts:228-239 | 在矩阵中标注为 conditional;若需修复则在 permissive 分支末尾补入 --unshare-net 判断 |
| DR-5 | macOS strict/compat 下 signal 规则为 (allow signal (target self)),permissive 为无限制 (allow signal) |
低 | SandboxInvoker.ts:428,464 | 在矩阵中记录差异即可 |
| DR-6 | Windows serve 模式固定 write_restricted=false(main.rs:585),仅靠 DACL ACE 保护写权限 |
中 | windows-sandbox-helper/src/main.rs:585 | 已知设计决策,需在矩阵 conditional 中标注 |
| DR-7 | sandboxed-bash-mcp 使用 { ...process.env } 仅删除 ELECTRON_RUN_AS_NODE,所有其他环境变量透传至沙箱 |
高 | resources/sandboxed-bash-mcp/sandboxed-bash-mcp.mjs:197-202 | ENV-02 测试覆盖;长期应补入 safeKeys 白名单过滤 |
| DR-8 | Windows helper (env.rs) 无独立 safeKeys 白名单,以 std::env::vars() 全量继承再修改 |
中 | windows-sandbox-helper/src/main.rs:256, env.rs:100-135 | ENV-03 测试覆盖;与 DR-7 一起作为 env 安全专项跟进 |