文件沙盒(Sandbox)
把 AI 编程 CLI 安全地开放给半可信用户的隔离机制——常配合 Oncall 模式 一起用。
开了沙盒后,话题里的人让机器人改代码、跑命令,所有写操作都被隔离:真实文件一个字节都不会被改动;机器人却照常读取真实的项目 / 配置 / 登录态、原生干活(它根本不知道自己在沙盒里)。改完之后由 owner 审阅 diff 卡片,确认无误再「应用到磁盘」落回真实仓库,或直接丢弃。
适合把 oncall / 值班机器人开放给不完全信任的群成员、外部协作者:既让他们用 AI 改东西,又不怕真实仓库被误改、搞乱。
它做什么 / 不做什么
一句话定位:这是「防误改 + 改动可审」的隔离,不是「防一切」的安全沙箱。它保证真实仓库不被沙盒里的写操作污染、且每一笔改动都要 owner 点头才落盘;它不阻止机器人读取本机文件或访问网络——敏感内容请用下面的屏蔽机制挡住。
开启方式
前置要求:Linux(依赖 bubblewrap + overlayfs)。root / 非 root 都支持——root 用更快的内核 overlayfs,非 root 自动回退到 fuse-overlayfs。依赖(bubblewrap / fuse-overlayfs)在开启沙盒时自动安装,无需提前装;环境缺自动安装权限时会提示一条手动安装命令。Mac(sandbox-exec)在路线图上、暂未支持。
方式一:bots.json
给某个 bot 加配置:
详见 bots.json 配置。
方式二:Dashboard
进 Dashboard 的「Bot 配置」页,打开「文件沙盒」开关、保存即可。
按会话固化:开 / 关沙盒只对新话题生效。已经在跑的老会话保持原样——重启 daemon 不会把历史会话无脑拖进沙盒模式。
落盘(/land)
机器人在沙盒里改完后,由 owner 在话题里发:
会收到一张「沙盒改动落盘」卡片:
- 改动统计:N 个文件(+x / −y),以及落盘目标仓库路径
- diff 预览:项目相对路径 + 真实增删行;过长自动截断
- 完整
.patch文件附件:可git apply,适合大改动离线 / 逐行审阅 - 「应用到磁盘」/「丢弃」按钮:仅 owner 可点
点「应用到磁盘」→ 把隔离层里的改动集 git apply 回真实仓库;点「丢弃」→ 扔掉这次改动。应用之前真实仓库不受任何影响。
实测过最硬的场景:让沙盒里的机器人改了 botmux 自己的运行源码并重新编译,线上正在跑的进程毫发无损——改动全在隔离层,
/land后才落回。
隐私屏蔽(sandboxHidePaths)
默认沙盒不限制读——机器人能看到本机所有文件。如果某些路径不想让半可信的 oncall 机器人读到(私钥、密钥配置、其它项目等),在 bots.json 里 per-bot 配置:
被列的路径在沙盒里会被空目录 / 空文件遮罩。没有默认值——不配就是全可读(包括 bots.json)。按你对群成员的信任级别自行决定要挡哪些。
额外只读路径(sandboxReadonlyPaths)
当机器人需要查看额外本地上下文、但不应该修改这些内容时,配置 sandboxReadonlyPaths:
每个已存在路径会以只读方式挂到沙盒里的同一路径(~ 会展开为家目录);不存在的项会被忽略。适合共享源码快照、参考仓库、生成文档、运行手册等不应进入 /land 改动集的输入。
两条护栏:sandboxHidePaths 遮罩永远优先——与遮罩路径重叠的只读项不会重新暴露被隐藏的内容;等于或包含家目录 / 会话工作目录的项会被忽略并告警(否则会整体顶掉写隔离 overlay),工作目录之下的子路径不受影响。
网络策略(sandboxNetwork)
默认沙盒保留网络访问,用于包安装、API 调用和 CLI 正常联网。配置:
会添加 --unshare-net,让 CLI 在无宿主网络的命名空间里运行。这个开关很硬:模型/API 访问、包管理器、git remote、代理都可能不可用,除非该 CLI 能完全依赖已经挂载好的本地输入工作。
分区目标 IP 网络策略(显式启用)
sandboxNetworkPolicy 将公网与内网分开管理。未配置时,sandboxNetwork 布尔值和历史会话行为保持不变;显式策略优先于该布尔值。新策略仅适用于 Linux x64/arm64、本地 PTY、oncall 文件沙箱。Docker 容器还必须允许创建用户/网络/PID 命名空间及命名空间内 nftables。macOS、scratch、持久终端、远程后端、adopt 和外部 App Server 均拒绝启动,不能给已有进程补装边界。这是策略支持范围,不等于部署运行器已验收;standalone Bun + 原生 Codex 必须先通过下述新建/恢复模型回合验收,不能只因网络测试通过就迁移现有 tmux 部署。
两个区域可独立组合。先按实际目标地址分区,再执行该区域规则;规则之间为“任一匹配”,没有最长前缀覆盖或后项反转。跨区域 CIDR(如 0.0.0.0/0)只在所属区域内匹配。只接受 IP 和 CIDR,主机位会规范化;IPv4 映射 IPv6 地址与对应 IPv4 共用规则。规则省略 protocol/ports 时匹配全部协议/端口;protocol 仅接受 tcp/udp,ports 为 1–65535 整数列表,必须同时指定协议。域名、URL、通配符、端口范围及其它协议选择器会报错。
“内网”采用保守的非公网分类:IPv4 的 RFC1918、回环、链路本地、CGNAT、未指定、文档/测试、保留、组播空间;IPv6 的 2000::/3 之外全部空间,以及 2001::/23、2001:db8::/32、2002::/16、3fff::/20。这包含 ULA、回环、链路本地、组播、IPv4 翻译/过渡地址。IPv4 的完整集合见 src/core/sandbox-network-policy.ts 的 PRIVATE_V4;此静态分类保守处理特殊用途,不实时推导路由或 DNS 后缀。
执行边界是任务实际发出的 IP 数据包目标地址。nftables 安装在新的网络命名空间中,随后才用通用 slirp4netns 接通网络;不依赖 HTTP_PROXY,也不修改宿主防火墙、Docker 或任何代理产品配置。直接 socket、改写/删除代理变量、IPv4 映射地址均受同一规则约束;DNS 结果变化、HTTP 重定向到另一个地址会重新受目标规则约束。模型 API 需要的目标 CIDR、端口和 DNS 必须获准。
该策略不解释加密流量或允许地址提供的应用功能。受限策略默认拒绝继承的 HTTP/HTTPS/ALL proxy 配置,环境变量不会被悄悄改写。可显式设置 proxyMode: "trusted-egress",将代理后的业务目标控制委托给获准出口;proxyMode: "reject" 则明确拒绝这些代理环境变量,包括公网和内网全部放行的策略。字段缺省时保留原行为(两区均 allow 不拒绝代理)。
信任出口不等于限制代理后的目标:内核仍过滤客户端实际连接的代理 IP/端口,未经授权的代理出口和直接内网连接继续被拒绝;但获准代理可以替客户端访问任何它能访问的地址,包括客户端策略禁止的内网。最终模型地址、域名、CONNECT/HTTP 请求及代理端 DNS 由部署层代理自身控制。本字段不创建代理、不填入环境变量、不修改具体代理产品配置,也不保证 CLI 会使用代理。NO_PROXY 和任务自行删改代理变量均不改变实际 IP 边界。
例如,代理环境变量由部署层提供,Botmux 仅信任已批准的内网出口:
示例只有代理端点可连接;如果代理域名需要客户端解析,应另外显式授权 DNS。代理必须是命名空间可路由的 IP,宿主 127.0.0.1、Unix socket 和 slirp 网关别名不能借此开放。要求最终业务目标也受约束时,必须先在部署层限制代理转发范围,或选择直接出口。
命名空间自己的 127.0.0.1/::1 为本地进程通信保留,与宿主回环不同。宿主回环映射、转发器网关别名及宿主 DNS 转发始终关闭;IPv6 邻居发现只开放必要的链路控制报文。任务不能创建宿主 Unix socket,不能调用 setns/unshare 或 io_uring 绕过过滤,也没有网络管理能力;socketpair 等进程内部 IPC 保留。宿主 MCP Gateway/Unix IPC 无法同时兑现该边界时,启动会明确拒绝;文件 outbox 中转仍保留。
dnsServers 是独立、显式的 DNS 能力:这些 IP 的 TCP/UDP 53 优先于区域规则放行,最多 8 个;不能使用回环或转发器别名。缺省为空,不委托宿主 DNS。DNS 服务收到查询本身属于获准访问;最终业务连接仍按解析出的实际地址过滤。阻断所有外部通信时不要配置此能力。
需自行提供 iproute2 的 ip、nft、带 --disable-host-loopback 和 --disable-dns 的 slirp4netns,以及支持 --disable-userns/--add-seccomp-fd 的 bwrap。不会自动安装新网络工具。依赖缺失、命名空间/nft/转发器初始化失败均拒绝任务;转发器退出会终止该任务生命周期。无未过滤的回退路径。
Dashboard 的安全页可保存 JSON 策略;状态表示“已配置,下个新会话采用”,不代表正在运行的会话已经切换。也可在宿主 CLI 使用:
set/clear 经目标在线 daemon 的认证配置接口原子写入;沙箱/会话内不得修改宿主策略。IM 的 /config sandboxNetworkPolicy <JSON> 使用同一校验/持久化入口。策略深拷贝到会话和 workflow 快照,fork/重启/恢复继续使用原策略;编辑机器人配置只影响新会话。清除后新会话回到原 sandboxNetwork 行为。
持久会话的兼容边界与迁移
现有 tmux 会话不能通过填入新策略直接收紧。TmuxBackend.spawn 创建 pane 时由 tmux server 启动进程;恢复时只 attach 存活 pane,不重跑 CLI 的启动参数。当前 namespace/link 监督器随本地 PTY 的 worker 生命周期运行;worker 消失会关闭链路并终止任务,这与持久 pane 跨 worker 重启存活的约定不同。仅放开后端校验或保存一个 policy JSON,无法证明存活进程真的处于原边界中。因此本 PR 保持 tmux/zellij/herdr/zmx、adopt 和已有外部 App Server 的显式拒绝,不静默切换后端或取消策略。
PTY 是当前新策略允许的后端,不代表 standalone Bun + 原生 Codex 的部署模型回合已经跑通;已有 tmux pane 也不能原地迁移。编辑 Bot 配置只作用于新会话,旧会话/fork/恢复仍使用冻结快照。CLI 自身持久化的历史恢复与 tmux 活进程恢复是两回事;启动新 CLI 恢复历史时仍须重新建立新边界。
旧 Codex 会话还需单独验收实际工具目录。当前 src/adapters/cli/codex.ts 为新建会话传 -C workingDir,resume/fork 则复用不含 -C 的基础参数,可能继续使用 Codex 自己保存的原 cwd。只更新 BotConfig 或冻结 Session 的 workingDir,不能证明模型工具根目录已迁移。网络目标过滤本身不验证 cwd;与 oncall 文件边界、工作区根及工具上下文组合时,必须核对它们是否一致。
迁移方报告曾以 Codex 0.159.3 原生 app-server thread/resume 的 cwd 参数完成一次性目录迁移:9 条旧 cwd 记录保留 transcript 前缀、模型、审批及 sandbox 参数,同一 Codex ID 冷恢复后的实际 Python 工具验证了新 cwd。这是外部实跑证据,本分支未执行该迁移,也不据此保证其它版本行为;本 PR 不修复 adapter、不自动改写 Codex 历史或在线部署。
部署前运行器验收(当前存在独立阻塞)
迁移方报告了网络策略尚未参与的运行器失败:共享镜像中 Botmux 3.20.0 原生 Bun standalone + Codex 0.159.3,在 sandbox: false、backendType: "pty" 的新 HTTP virtual turn 中,pty.spawn 返回 PID 后约 15 ms 收到 exitCode: 0 / signal: 1 (SIGHUP),输入尚未提交;相同参数经 Python PTY 启动 Codex 则保持存活。这是迁移方的实跑报告,本 PR 未在同一镜像/版本组合独立复现,也没有修复或归因该运行器问题。 短期保留原生 tmux,不以关闭网络策略或放宽权限冒充 PTY 兼容修复。
另一个独立报告是 HTTP virtual/noTransport tmux 在无 server 时产生 probe: unknown 而被原生门禁拒绝。当前代码可验证:worker.ts 的 policy-off 迁移分支对 noTransportSession && tmux 生效,unknown 经状态机拒绝,不能当作已证明缺失而跳过;普通 transport-enabled、策略关闭且无旧 provenance 的会话不受这条 noTransport 分支约束(其它显式隔离或旧 provenance 仍可能触发门禁)。这不是新网络策略导致的失败,也不能推广为全部 tmux 会话不可用;本 PR 不绕过原门禁。
现有网络 CI 的 PTY 用例用普通运行时管理真实 PTY,standalone 用例另行验证网络 runner 自重执行;两者组合不等于 standalone worker 的 Codex 回合测试。test/pty-native-smoke.test.ts / src/cli/pty-smoke.ts 还刻意不构建 tty.ReadStream 包装,不能代替生产 PtyBackend 的存活及提交验收。本 PR 的本地/内核绿不能解除上述部署阻塞。
启用网络策略前,目标镜像必须完成以下前置验收并保存结果:
- 固定镜像及 Botmux/Bun/Codex 实际版本,使用原生 standalone worker 和生产
PtyBackend,不以 Python PTY、自定义 runtime 或绕过 ReadStream 的 helper 代替。先在sandbox: false下验证原生运行器,再在 oncall + 网络策略下验证。 - 新建会话须保持存活直到 prompt ready,实际提交输入并得到完整模型回合及可回读结果;记录 PID、ready/submit/response 时间及退出 code/signal。spawn 返回 PID、短暂存活或退出 0 均不构成成功。
- 恢复已保存的 Codex 历史,由新 worker/CLI 建立新 PTY 并完成下一模型回合;验证恢复身份、提交与结果回读。要求模型实际工具在未先执行
cd、未覆盖工具 cwd 的情况下回读pwd或 Pythonos.getcwd(),保存工具执行结果,按规范化路径核对预期工作根、Session 快照及文件策略根;不能只检查 Botmux JSON、模型文字回答或提示词。目录迁移还须确认原 Codex ID、transcript 前缀、模型/审批/sandbox 参数保持预期;cwd 不符时验收失败,目录修复作为独立迁移处理。PTY 不承诺活进程跨 worker 重启存活,不把它写成 tmux reattach。 - HTTP virtual/noTransport 与正常 transport 形状分别用隔离测试装配验收;IM 发送调用用测试替身拒绝并计数为 0,不发真实消息。模型可先使用协议兼容的本地 fixture,但须标明 fixture 与实际模型验收的区别;DNS、部署层代理及最终目标 ACL 应另行验证。
- 单独验证 tmux 无 server/探测 unknown 的失败与已有 server 的路径,保留 tri-state 门禁语义;不得通过把 unknown 强制当 missing 或改成虚假 transport-enabled 来消除阻塞。未通过时继续保留原 tmux 部署,将运行器和 noTransport 问题作为独立修复需求。
后续持久后端需求应独立实现并验收:在启动 shell/CLI 前建立边界;为每代 pane/真实 CLI PID 绑定不可由任务改写的策略摘要、namespace 身份与监督器代际;daemon/worker 重启后核验这些证据及实际存活状态,缺失或不符时拒绝附着;由独立监督器管理 namespace/nft/link 的持久生命周期,转发器死亡必须终止关联任务;清理、pane 替换、并发恢复与 adopt 均不能复用过期证明。验收需真实 tmux、新建/重附着、worker/daemon 重启、转发器死亡、篡改/旧证明、跨会话与 IPv4/IPv6 回归,不能以存储字段或 session 名称代替边界证明。
隔离 Linux 验证
先 bun run build,然后在专用测试目录的外层网络命名空间中准备测试地址。网关测试在外层 loopback 的 10.0.2.3 及主机 loopback 地址监听服务,确保删除网关拒绝规则时能观察到实际连接。下面只修改新命名空间,不修改宿主网络:
测试默认跳过;显式启用后,依赖或内核能力缺失会失败,不能把跳过当作边界验证通过。设置 BOTMUX_NETWORK_TEST_BINARY 为 build:bun 产物的绝对路径后,还会验证单文件自重执行;未设置时仅该编译态用例跳过。Sandbox network boundary CI 在相关 PR 中构建真实二进制并执行全部边界用例。模型请求用隔离 HTTP 测试服务,不调用真实供应商。
注意事项
- 仅 Linux:依赖 bwrap + overlayfs(非 root 自动用 fuse-overlayfs,依赖开沙盒时自动装);Mac(sandbox-exec)暂未支持。
- 读不隔离:默认全可读,挡敏感凭证要主动配
sandboxHidePaths(见上)。 - 网络默认开放:只有确认机器人能接受无网络 / 无代理时,才设置
sandboxNetwork: false。 - build 产物会进改动集:如果机器人在沙盒里跑了
pnpm build/ 编译,产物(如dist/)也会出现在/land的改动集里。落盘前看清 diff,别把编译产物apply覆盖真实仓库。 - 机器人无感:它看到的是 overlay 合并视图,以为改的就是真文件、照常工作;「隔离」对它完全透明。
botmux send照常:沙盒里botmux send通过 daemon 中转正常发消息、附图、附件、自定义卡片 JSON(应用凭证不进沙盒环境)。
配合 Oncall 起飞
文件沙盒 + Oncall 模式 是绝配:oncall 把机器人开放给一群人随时 @,沙盒保证这群人的改动不会动到真实仓库、且每笔改动 owner 审阅后才落盘。半可信、多人、随时改的 oncall 场景的标准搭配。
