如果只看产品体验,沙箱像是一个开关:允许写工作区、禁止联网、必要时请求审批。 但源码阅读不能停在这个说法上。模型请求执行命令以后,真正保护本机的不是一句 prompt, 也不是 UI 上的标签,而是操作系统愿意承认的边界。

OpenAI 官方的 《为 Codex 构建 Windows 沙箱》 把问题说得很具体:macOS 可以借助 Seatbelt,Linux 可以借助 seccomp/bubblewrap, Windows 没有一个刚好匹配本地 coding agent 工作流的现成原语。用户又不应该在 “几乎每条命令都审批”和“完全放开本机访问”之间二选一。于是 Codex 需要自己把 runtime 权限策略落到 Windows 的安全模型里。

本篇的主线是:Windows sandbox 是权限策略的操作系统落地层。 Codex 先在 runtime 里决定这次工具调用能拿到什么权限,再通过 setup 准备 sandbox 用户、文件 ACL 和防火墙规则;真正执行命令时,command runner 以 sandbox 用户身份启动,再派生出受限 token 的子进程。

阅读契约。 这篇只追 Windows 本地执行链路:为什么现成 Windows 方案不够用, 未提权原型解决了什么、漏掉了什么,提权 setup 为什么出现,command runner 为什么要站到 sandbox 用户一侧,以及这些机制怎样接回第五篇的工具权限主线。

证据边界。 产品背景来自 OpenAI 官方工程文章;实现细节来自 openai/codex 公开源码。本文只讨论本地 Windows sandbox backend,不推断 OpenAI 私有服务端如何执行远程任务。

一、先接回第五篇:runtime 只决定“应该怎样执行”

第五篇里,我们把副作用拆成几道关口:permission hooks 可以提前观察或拒绝, approval policy 决定是否需要人类确认,ToolOrchestrator 把一次模型工具请求整理成 SandboxAttempt,最后 exec backend 负责真正启动命令。

到这里,Codex 已经知道“这条命令应该在什么权限下执行”。例如:只读、允许写工作区、 禁止网络、允许网络、或者需要提权重试。可是 Windows 内核不会理解 SandboxAttempt 这个 Rust 类型。它只认用户身份、访问控制列表、令牌权限、 防火墙规则和进程创建 API。

Codex runtime 层 Windows 必须看到的东西 源码落点
这次命令能不能写工作区。 文件系统 ACL 和受限 token。 workspace_acl.rstoken.rs
这次命令能不能联网。 可被防火墙规则识别的本地用户身份。 sandbox_users.rsfirewall.rs
这次命令怎样启动并把输出带回 Codex。 runner 进程、匿名管道、ConPTY 或 stdio。 runner_client.rscommand_runner/win.rs
失败时如何暴露给上层。 exit status、spawn result、streamed output。 exec.rsunified_exec

这张表很重要。它把“权限”从产品词汇拆成了 Windows 能执行的材料。 后面每个 Windows API 名字,都是为了解决其中一个材料问题,不需要提前背。

二、Windows 难点:现成隔离方案都差一点

官方文章先评估了三类方案。AppContainer 是 UWP 应用模型的一部分, 约束强,但把任意命令、用户工程目录和开发工具链塞进去并不自然。Windows Sandbox 更像一台临时 VM,隔离清楚,却很重,和 Codex 需要在用户当前工作区里快速读写的节奏不合。 Mandatory Integrity Control 可以降低完整性级别,能限制一部分写入,但网络控制和开发体验仍然不够完整。

候选方案 能提供什么 为什么没有成为主路径
AppContainer 应用容器级隔离。 更适合打包应用,不适合把任意 CLI、编译器和仓库命令原样放进去。
Windows Sandbox 临时 VM 边界。 隔离粒度太重,启动、文件映射和交互成本不适合每次工具调用。
MIC 低完整性进程,限制部分写入。 能帮上忙,但不能单独表达 Codex 对工作区写入和网络访问的组合策略。

Codex 需要的边界更贴近日常开发:命令要在原仓库里跑,能读依赖和源码, 但写入要收在允许的目录里;默认不能随便联网,必要时又要支持在线模式和代理例外。 这不是一个“把进程关进盒子里”的单点问题,而是一组要和开发工具链共存的约束。

三、未提权原型:文件写入先收住,网络还不够硬

官方文章里提到的第一版原型没有要求管理员权限。它的思路很直接: 给子进程一个受限 token,再用文件 ACL 控制哪些路径可写。源码里的 CreateRestrictedToken 调用也能看到这个方向:token 会被加上 restricting SID, 并带上 WRITE_RESTRICTED 这类标志。

CreateRestrictedToken(
    base_token,
    DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED,
    ...
)

文件这边比较容易落地:工作区可写,仓库里的敏感控制目录还能额外加拒绝写入。 例如 workspace_acl.rs 会保护 .codex.agents 子目录,避免 sandbox 子进程把 Codex 自己的控制面写坏。

难点出在网络。未提权原型可以通过环境变量把网络请求引到 proxy,也可以用替身可执行文件拦截某些常见命令。 但这类方案本质上依赖进程配合:程序如果直接开 socket,或者绕过约定的环境变量, Codex 就很难在不提权的情况下把它完全拦住。官方文章也把这点作为重新设计的关键原因。

未提权原型已经说明一件事:文件写入可以靠 ACL 收口,网络访问必须找到更硬的身份边界。 这就是后面两个 sandbox 用户和防火墙规则出现的根因。

四、提权 setup:把网络边界变成 Windows 用户身份

重新设计后的核心变化,是先用一个提权 setup 准备本机安全材料。源码里这两个用户名写得非常直白:

const OFFLINE_USERNAME: &str = "CodexSandboxOffline";
const ONLINE_USERNAME: &str = "CodexSandboxOnline";

CodexSandboxOffline 用于默认离线执行,CodexSandboxOnline 用于允许联网的执行。setup 会创建或刷新这些本地用户,生成随机密码,把 secret 通过 DPAPI 保护后写入 sandbox_users.json。这一步需要管理员权限, 因为创建本地用户、调整 ACL、写防火墙规则都属于系统级配置。

这个设计的好处在于:Windows 防火墙可以按用户身份建立规则。 offline 用户的出站网络默认被 block;如果 Codex 配置了本地代理,就只放行指向代理端口的 loopback 例外。online 用户则用于明确允许联网的场景。这样,网络权限不再靠子进程自觉读取环境变量, 而是落在 Windows 可以强制执行的 principal 上。

setup 产物 保护什么 为什么必须提前准备
CodexSandboxOffline 默认离线身份。 让防火墙可以按用户维度拦截出站网络。
CodexSandboxOnline 允许联网身份。 把“允许网络”从环境变量升级成明确身份选择。
工作区 ACL 可写根、只读根、拒绝写入根。 命令启动前,文件系统需要已经知道哪些 SID 可以写。
防火墙规则 offline 用户的出站网络和代理例外。 网络拦截必须在进程尝试连接前生效。

所以 setup 不是“安装一个沙箱程序”这么简单。它是在用户机器上铺好几条操作系统边界: 文件系统认 ACL,网络栈认用户身份,后续 runner 才能把一次工具调用放进正确边界里。

五、command runner:真正的子进程从 sandbox 用户一侧出生

有了 sandbox 用户,还剩一个实际问题:Codex 主进程通常以真实用户身份运行, 它怎样让最终命令在 sandbox 用户身份下启动,同时还能拿回 stdin/stdout、exit code 和 terminal 尺寸变化?

源码把这件事拆成两段。第一段在 runner_client.rs: Codex 创建管道,找到 codex-command-runner.exe,再用 CreateProcessWithLogonW 以 sandbox 用户身份启动 runner。 第二段在 runner 进程内部:runner 通过 IPC 读到 SpawnRequest, 再基于当前 sandbox 用户 token 派生更窄的 restricted token,最后用 ConPTY 或普通 stdio 启动真正的命令。

Codex 主进程
  -> CreateProcessWithLogonW(sandbox user, command runner)
  -> 通过 pipe 发送 SpawnRequest
  -> runner 创建 restricted token
  -> runner 启动真正命令
  -> stdout/stderr/status 流回 Codex

这一步看起来绕,但它解决了身份和交互两个问题。身份方面,最终命令出生在 CodexSandboxOfflineCodexSandboxOnline 的世界里, 防火墙和 ACL 可以生效。交互方面,Codex 仍然保留对命令生命周期的观察: 可以写 stdin、读输出、处理 resize、等待退出,也能把结果投影回 protocol event 和 rollout。

六、放回 Codex 主线:平台沙箱保护的到底是什么

现在回到第五篇的工具权限主线。模型没有直接获得“运行命令”的能力; 它只能提出工具请求。Codex runtime 先把请求变成可审查的工具调用, 再按配置、hook 和 approval policy 决定权限。Windows backend 接手时, 它的任务是把这份权限决定变成 OS 能强制执行的进程边界。

层级 负责的问题 失败时应该怎样暴露
approval / hooks 这次副作用是否被允许。 拒绝、请求审批、返回可解释事件。
SandboxAttempt 这次命令应在什么权限轮廓下运行。 选择更窄或更宽的尝试,必要时触发 retry。
Windows setup 本机是否已经具备可执行边界。 提示需要 setup、提权失败或环境不支持。
command runner 真正子进程是否按 sandbox 用户和 restricted token 启动。 spawn failure、pipe failure、exit status、captured output。
event / rollout 客户端和恢复路径能否看到事实。 结构化事件、工具输出、后续 retry 或诊断材料。

这样看,Windows sandbox 不是 Codex 系列里一块孤立的安全附录。 它是第五篇“权限与 sandbox”的平台实现篇:runtime 决定策略,setup 准备边界, runner 把命令放进边界,事件和 rollout 把结果带回主线。

七、读源码时容易误会的几件事

第一,沙箱不是 prompt 约束。prompt 可以要求模型谨慎,但模型之外的命令执行需要 OS 边界兜底。 第二,它也不是一台完整 VM。Codex 的目标是在当前工作区里跑开发命令,所以它更像一组贴着仓库和用户身份工作的本地边界。 第三,setup 成功不代表每次命令都一定成功;它只说明本机材料准备好了,后续还要看权限轮廓、路径、网络和 runner 状态。

最后要记住一个判断标准:当一条命令被模型提出时,Codex 希望能回答五个问题: 这条命令为什么被允许,在哪个用户身份下跑,能写哪里,能不能联网,失败时谁能看到证据。 Windows sandbox 这一层,就是把这些答案交给操作系统执行。

参考源码