---
url: /wave-agent/specs/core/bash-sandbox.md
description: >-
  `Bash` 命令的 OS 级沙箱：macOS Seatbelt / Linux bubblewrap
  的文件系统与网络隔离、域名白名单代理、违规回注，以及与权限系统的自动放行联动
---

# 功能规格说明：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 工具的子进程。
