Appearance
功能规格说明:Bash 沙箱
规格文件:docs/specs/core/bash-sandbox.md创建日期:2026-10-21
用户场景与测试 (必填)
用户故事:文件系统隔离(优先级:P1)
作为使用者,我希望 agent 执行的 shell 命令只能在授权目录内读写,这样即使模型判断失误或被提示注入,也读不到 ~/.ssh、改不了 ~/.bashrc 与 settings.json。
优先级原因:读敏感文件与写 shell 配置是外部命令最主要的破坏面,逻辑层的路径规则拦不住 bash -c 里的任意动作。
独立测试:开启沙箱后在命令里写 /tmp 之外的文件、读 ~/.ssh/id_rsa,验证均失败且工具结果给出被拒路径。
验收场景:
- 假设 沙箱开启且可写范围只含工作区,当 执行
echo x > /tmp/outside.txt时,则 命令失败,工具结果包含文件系统拒绝信息。 - 假设 读拒绝清单包含
~/.ssh,当 执行cat ~/.ssh/id_rsa时,则 命令失败,结果标注被拒路径。 - 假设 命令在工作区内创建或修改文件,当 执行时,则 必须成功——默认可写范围必须包含工作区。
- 假设 命令试图写沙箱配置文件(
<workdir>/.wave/settings.json、settings.local.json、~/.wave/settings.json),当 执行时,则 必须被拒绝——写入配置即可让下一次命令逃出沙箱。 - 假设 命令在 git worktree 中执行
git操作,则 对主仓库.git的写入必须被允许(worktree 的锁与索引在主仓)。 - 假设 命令执行包管理器安装(
npm/pnpm的默认缓存目录),则 必须成功——否则沙箱一开安装类命令全线失败。
用户故事:网络域名白名单(优先级:P1)
作为使用者,我希望被沙箱的命令只能访问我列出的域名,这样命令既不能外传数据,也不会把结果传给未知主机。
优先级原因:只做文件系统隔离时,恶意命令仍可把读到的内容发出去;只做网络隔离时仍可读文件后落盘待发。两者缺一不成边界。
独立测试:白名单只含 api.github.com,分别 curl 该域与 example.com,验证前者成功、后者被拦且报出主机名。
验收场景:
- 假设 允许域名含
api.github.com,当 命令请求该域名时,则 请求成功。 - 假设 目标域名不在允许列表,当 命令请求时,则 失败,结果给出「被网络白名单拦截」与目标主机名。
- 假设 命令直连 IP、或使用非 HTTP(S) 协议,当 出网时,则 同样受域名裁决约束。
- 假设 允许域名为空,则 任何出网请求都必须被拒绝。
- 假设 命令清空代理相关环境变量后直连,则 仍必须被拦截——限制不能只靠环境变量。
- 假设 沙箱开启且允许域名为空,当 命令完全不出网(纯本地构建、跑测试)时,则 必须正常执行。
用户故事:违规回注给模型(优先级:P2)
作为 AI 代理,我希望在被拦时看到「哪条路径 / 哪个主机被拒」,以便换一条合规的命令继续,而不是把失败当成命令本身写错。
优先级原因:沙箱失败与普通失败在 stderr 上不易区分,不回注会让模型反复重试同一条命令。
独立测试:执行一条越界写命令,验证工具结果末尾含 <sandbox_violations> 块并列出该路径。
验收场景:
- 假设 一次调用发生一次或多次违规,当 返回结果时,则 必须附带
<sandbox_violations>块,逐条列出被拒操作。 - 假设 违规来自网络拒绝,则 回注内容必须只含主机与端口/路径,不得含 URL 的 query 片段(其中常带凭证)。
- 假设 违规匹配了忽略规则,则 该条不得回注,也不得出现在结果里。
用户故事:与权限系统联动(优先级:P1)
作为使用者,我希望「命令在沙箱里跑」这件事能换取更少的确认弹窗;但显式写下的拒绝规则必须仍然生效。
优先级原因:沙箱的价值一半在边界、一半在体验。若沙箱内仍逐条询问,用户只会感觉命令变容易失败了。
独立测试:开启沙箱与自动放行后执行一条原本会触发询问的命令,验证不再询问;再对同一命令加显式拒绝规则,验证被拒。
验收场景:
- 假设 沙箱开启且自动放行开关打开,当 命令确实会被沙箱执行时,则 跳过用户确认直接执行。
- 假设 存在针对该命令(或其任一子命令)的显式拒绝规则,当 命令会被沙箱执行时,则 必须仍然拒绝——沙箱不得把拒绝降级为放行。
- 假设 命令命中「排除命令」清单,当 执行时,则 不进入沙箱,回到既有权限判定流程(该询问仍询问)。
- 假设 模型显式请求不使用沙箱,且策略允许,当 执行时,则 命令不沙箱但仍须经过既有权限判定,不得静默放行。
- 假设 策略禁止不使用沙箱,则 模型的「不使用沙箱」请求必须被忽略,命令必须沙箱执行。
用户故事:平台可用性与降级(优先级:P1)
作为使用者,我希望在平台不支持或依赖缺失时被告知,而不是以为自己受保护。
优先级原因:安全开关「配了但没生效」比没配更危险——用户会按「已经有边界」的前提去授权。
独立测试:在缺少沙箱依赖的环境上开启沙箱,验证有明确警告且命令按降级语义执行。
验收场景:
- 假设 平台为 macOS 或 Linux(含 WSL2),当 依赖齐备时,则 沙箱可用。
- 假设 平台为 WSL1,则 判定为不支持。
- 假设 平台为 Windows,则 判定为不支持:不包装命令,命令照常走既有权限流程(与 Claude Code 保持一致)。
- 假设 沙箱已开启但依赖缺失(Linux 的
bwrap/socat/rg,或sandbox-exec不可用),当 命令执行时,则 降级为不沙箱执行,并给出一次明确警告说明原因。 - 假设 配置为「沙箱必须可用」,而平台不支持或依赖缺失,当 命令执行时,则 命令必须失败并说明原因——不得静默降级。
用户故事:同进程多会话的边界归属(优先级:P1)
作为使用者,我希望同时开着几个对话时,每条命令的边界都按它自己所属的会话算,不会因为另一个会话先跑过就换了个边界。
优先级原因:宿主的一个进程可以承载多个会话(wave --stdio 即如此),而沙箱运行时是进程级的单例;边界一旦按「先初始化者」定下来,后起的会话就会拿着别人的可写范围执行命令——这是静默越权,比命令失败严重得多。
独立测试:同一进程内先后创建两个工作目录不同的 Agent,让后建的那个在工作目录内写文件,验证写入成功,且越界写仍被拒。
验收场景:
- 假设 一个进程里有两个会话且工作目录不同,当 两者交替执行命令时,则 每条命令的边界只按它所属会话的工作目录与配置生成。
- 假设 会话 B 晚于会话 A 创建,当 B 在自身工作目录内写文件时,则 必须成功(不得继承 A 的可写范围)。
- 假设 会话 A 的配置在运行中变更(热重载),当 会话 B 随后执行命令时,则 B 仍按其自身配置执行,不继承 A 的变更。
- 假设 边界未变化,当 连续执行多条命令时,则 不重复下发配置,也不重启代理或重签证书。
边界情况
- 工作目录追踪:命令结束后读取新工作目录的临时文件必须位于可写范围内,否则 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 工具的子进程。