Skip to content

功能规格说明:Bash 沙箱 ​

规格文件:docs/specs/core/bash-sandbox.md创建日期:2026-10-21

用户场景与测试 (必填) ​

用户故事:文件系统隔离(优先级:P1) ​

作为使用者,我希望 agent 执行的 shell 命令只能在授权目录内读写,这样即使模型判断失误或被提示注入,也读不到 ~/.ssh、改不了 ~/.bashrc 与 settings.json。

优先级原因:读敏感文件与写 shell 配置是外部命令最主要的破坏面,逻辑层的路径规则拦不住 bash -c 里的任意动作。

独立测试:开启沙箱后在命令里写 /tmp 之外的文件、读 ~/.ssh/id_rsa,验证均失败且工具结果给出被拒路径。

验收场景:

  1. 假设 沙箱开启且可写范围只含工作区,当 执行 echo x > /tmp/outside.txt 时,则 命令失败,工具结果包含文件系统拒绝信息。
  2. 假设 读拒绝清单包含 ~/.ssh,当 执行 cat ~/.ssh/id_rsa 时,则 命令失败,结果标注被拒路径。
  3. 假设 命令在工作区内创建或修改文件,当 执行时,则 必须成功——默认可写范围必须包含工作区。
  4. 假设 命令试图写沙箱配置文件(<workdir>/.wave/settings.json、settings.local.json、~/.wave/settings.json),当 执行时,则 必须被拒绝——写入配置即可让下一次命令逃出沙箱。
  5. 假设 命令在 git worktree 中执行 git 操作,则 对主仓库 .git 的写入必须被允许(worktree 的锁与索引在主仓)。
  6. 假设 命令执行包管理器安装(npm / pnpm 的默认缓存目录),则 必须成功——否则沙箱一开安装类命令全线失败。

用户故事:网络域名白名单(优先级:P1) ​

作为使用者,我希望被沙箱的命令只能访问我列出的域名,这样命令既不能外传数据,也不会把结果传给未知主机。

优先级原因:只做文件系统隔离时,恶意命令仍可把读到的内容发出去;只做网络隔离时仍可读文件后落盘待发。两者缺一不成边界。

独立测试:白名单只含 api.github.com,分别 curl 该域与 example.com,验证前者成功、后者被拦且报出主机名。

验收场景:

  1. 假设 允许域名含 api.github.com,当 命令请求该域名时,则 请求成功。
  2. 假设 目标域名不在允许列表,当 命令请求时,则 失败,结果给出「被网络白名单拦截」与目标主机名。
  3. 假设 命令直连 IP、或使用非 HTTP(S) 协议,当 出网时,则 同样受域名裁决约束。
  4. 假设 允许域名为空,则 任何出网请求都必须被拒绝。
  5. 假设 命令清空代理相关环境变量后直连,则 仍必须被拦截——限制不能只靠环境变量。
  6. 假设 沙箱开启且允许域名为空,当 命令完全不出网(纯本地构建、跑测试)时,则 必须正常执行。

用户故事:违规回注给模型(优先级:P2) ​

作为 AI 代理,我希望在被拦时看到「哪条路径 / 哪个主机被拒」,以便换一条合规的命令继续,而不是把失败当成命令本身写错。

优先级原因:沙箱失败与普通失败在 stderr 上不易区分,不回注会让模型反复重试同一条命令。

独立测试:执行一条越界写命令,验证工具结果末尾含 <sandbox_violations> 块并列出该路径。

验收场景:

  1. 假设 一次调用发生一次或多次违规,当 返回结果时,则 必须附带 <sandbox_violations> 块,逐条列出被拒操作。
  2. 假设 违规来自网络拒绝,则 回注内容必须只含主机与端口/路径,不得含 URL 的 query 片段(其中常带凭证)。
  3. 假设 违规匹配了忽略规则,则 该条不得回注,也不得出现在结果里。

用户故事:与权限系统联动(优先级:P1) ​

作为使用者,我希望「命令在沙箱里跑」这件事能换取更少的确认弹窗;但显式写下的拒绝规则必须仍然生效。

优先级原因:沙箱的价值一半在边界、一半在体验。若沙箱内仍逐条询问,用户只会感觉命令变容易失败了。

独立测试:开启沙箱与自动放行后执行一条原本会触发询问的命令,验证不再询问;再对同一命令加显式拒绝规则,验证被拒。

验收场景:

  1. 假设 沙箱开启且自动放行开关打开,当 命令确实会被沙箱执行时,则 跳过用户确认直接执行。
  2. 假设 存在针对该命令(或其任一子命令)的显式拒绝规则,当 命令会被沙箱执行时,则 必须仍然拒绝——沙箱不得把拒绝降级为放行。
  3. 假设 命令命中「排除命令」清单,当 执行时,则 不进入沙箱,回到既有权限判定流程(该询问仍询问)。
  4. 假设 模型显式请求不使用沙箱,且策略允许,当 执行时,则 命令不沙箱但仍须经过既有权限判定,不得静默放行。
  5. 假设 策略禁止不使用沙箱,则 模型的「不使用沙箱」请求必须被忽略,命令必须沙箱执行。

用户故事:平台可用性与降级(优先级:P1) ​

作为使用者,我希望在平台不支持或依赖缺失时被告知,而不是以为自己受保护。

优先级原因:安全开关「配了但没生效」比没配更危险——用户会按「已经有边界」的前提去授权。

独立测试:在缺少沙箱依赖的环境上开启沙箱,验证有明确警告且命令按降级语义执行。

验收场景:

  1. 假设 平台为 macOS 或 Linux(含 WSL2),当 依赖齐备时,则 沙箱可用。
  2. 假设 平台为 WSL1,则 判定为不支持。
  3. 假设 平台为 Windows,则 判定为不支持:不包装命令,命令照常走既有权限流程(与 Claude Code 保持一致)。
  4. 假设 沙箱已开启但依赖缺失(Linux 的 bwrap / socat / rg,或 sandbox-exec 不可用),当 命令执行时,则 降级为不沙箱执行,并给出一次明确警告说明原因。
  5. 假设 配置为「沙箱必须可用」,而平台不支持或依赖缺失,当 命令执行时,则 命令必须失败并说明原因——不得静默降级。

用户故事:同进程多会话的边界归属(优先级:P1) ​

作为使用者,我希望同时开着几个对话时,每条命令的边界都按它自己所属的会话算,不会因为另一个会话先跑过就换了个边界。

优先级原因:宿主的一个进程可以承载多个会话(wave --stdio 即如此),而沙箱运行时是进程级的单例;边界一旦按「先初始化者」定下来,后起的会话就会拿着别人的可写范围执行命令——这是静默越权,比命令失败严重得多。

独立测试:同一进程内先后创建两个工作目录不同的 Agent,让后建的那个在工作目录内写文件,验证写入成功,且越界写仍被拒。

验收场景:

  1. 假设 一个进程里有两个会话且工作目录不同,当 两者交替执行命令时,则 每条命令的边界只按它所属会话的工作目录与配置生成。
  2. 假设 会话 B 晚于会话 A 创建,当 B 在自身工作目录内写文件时,则 必须成功(不得继承 A 的可写范围)。
  3. 假设 会话 A 的配置在运行中变更(热重载),当 会话 B 随后执行命令时,则 B 仍按其自身配置执行,不继承 A 的变更。
  4. 假设 边界未变化,当 连续执行多条命令时,则 不重复下发配置,也不重启代理或重签证书。

边界情况 ​

  • 工作目录追踪:命令结束后读取新工作目录的临时文件必须位于可写范围内,否则 cd 后目录追踪失效。
  • Linux 的路径通配:可写路径必须是具体路径,通配写法在 Linux 上不生效;配置了通配必须给出警告。
  • 复合命令与排除清单:a && b 形式中任一子命令命中排除清单即整条不沙箱,且此时必须走完整权限判定。
  • 沙箱启动失败:命令在沙箱里起不来(包装器报错、profile 编译失败)时,命令按失败处理并把原因回注给模型,不得静默改成不沙箱执行——权限闸可能已经因为「这条命令会进沙箱」而放行,丢掉边界是这里唯一不能出现的结果。「能不能用沙箱」这类判定(平台不支持、依赖缺失)走平台可用性故事的降级语义,两者不要混。
  • 后台命令:run_in_background 与超时自动转后台的命令必须与前台处于同一沙箱内。
  • 沙箱关闭时零差异:开关未开时,命令执行、权限判定、工具结果与现状完全一致。
  • cwd 被删除:沙箱内命令删除自身工作目录时,按既有「工作目录恢复」逻辑处理,不额外失败。

假设 ​

  • 执行层复用 @anthropic-ai/sandbox-runtime(Apache-2.0;macOS 用 sandbox-exec、Linux 用 bubblewrap),本仓不自研包装器。
  • 该运行时的配置是进程级的:initialize() 一句只认第一次(再调即幂等返回),所以多会话共进程时,边界不能靠「各自初始化一次」落实,而是每条命令前把本会话的配置经 updateConfig 推上去(不 reset、不重启代理、不重签证书),且与已装配置一致时不下发。这与 Claude Code 的处理方式相同——它也是模块级单例 + 设置变更时 updateConfig,只有 reset() 才推倒重来。
  • Linux 上沙箱要求「有能力的 user namespace」:Ubuntu 24.04 起默认开启的 kernel.apparmor_restrict_unprivileged_userns 会让 bubblewrap 与 seccomp 隔离层都建不出带能力的命名空间,命令以启动失败告终。上游的处置是让用户关掉该限制(或给相关二进制授 userns),不为此自动降级——本仓跟随同一处置,不在启动失败时改用更弱的一档保护。
  • 该运行时包(12MB,含各平台 vendor 二进制)不进宿主 bundle,按运行时依赖按需下载:只有 sandbox.enabled 为真时才下载,未启用的用户既不付体积也不付启动时间;下载失败按「不可用 + 可读原因」降级。
  • 配置放在 settings.json 顶层 sandbox 段,字段命名与 Claude Code 对齐,便于用户迁移既有配置。
  • 默认关闭:sandbox 段未配置或 enabled 未设时行为与今天完全一致。
  • 沙箱内自动放行开关一期实现但默认关闭(Claude Code 默认开启)——先上边界,弹窗体感单独再评估。
  • 一期不覆盖的 shell 出口与 Claude Code 一致:hook 命令、技能/自定义命令里注入的命令、用户在输入框里直接执行的 ! 命令、遥测 header 辅助命令。它们由用户显式配置或显式触发,不属于工具调用的执行面。
  • 一期不新增任何界面:只通过配置文件开启与配置,不做交互面板。

范围之外(一期明确不做) ​

  • Windows 沙箱后端(需独立账号 + 机器级网络过滤 + 管理员安装,另立工程)。
  • /sandbox 式交互面板与「一键把某命令排除出沙箱」的图形化入口。
  • 企业托管策略层对沙箱开关的锁定。
  • 用沙箱包裹 MCP server 或其他非 Bash 工具的子进程。