为 Windows 上的 Codex 构建安全有效的沙盒
原文: English original · Anthropic/OpenAI 官方
为 Windows 上的 Codex 构建安全有效的沙盒
发布于 2026 年 5 月 13 日
作者:David Wiesen,Member of Technical Staff
2025 年 9 月我加入 Codex 工程团队时,Windows 版 Codex 还没有沙盒实现。这意味着 Windows 用户在使用 OpenAI 的 coding agents 时,只能在两个都不理想的选项中二选一:
- 批准 coding agent 想运行的几乎每一条命令(甚至包括读取类命令),这既低效又烦人。使用 Codex 的一大好处,本来就是你不必亲自做所有繁琐工作。
- 启用 Full Access 模式:允许 Codex 在没有批准或限制的情况下运行所有命令,以牺牲监督为代价减少摩擦。
Codex 是我们的 coding agent,运行在开发者的笔记本电脑上,无论是通过 CLI、IDE 扩展,还是桌面应用。它负责管理键盘前的人与云端模型之间的对话,云端模型则负责处理推理。
默认情况下,Codex 以真实用户的权限运行,这意味着它能做用户能做的一切。这很强大,也有潜在危险。coding model 可能会要求 harness 在本地运行命令,从运行测试,到读取或编辑文件,再到创建 Git 分支。因此,Codex 的默认模式会尝试在有效性与安全性之间找到合适的平衡。
在这个默认模式下,Codex 几乎可以读取任何位置的文件,并能在你的 workspace(也就是你运行 Codex 的目录)内写入文件;除非你明确指定需要联网,否则它没有互联网访问权限。要把文件写入和网络访问自动限制在安全边界内,Codex 需要一个真正能够强制执行这些约束的沙盒环境。
沙盒是一种受约束的执行环境。当开发者使用 Codex 时,其电脑的操作系统会以降低后的权限启动命令,并且这些约束会沿着进程树向下传播。每一条 Codex 命令从一开始就处在沙盒中,每一个后代进程也都会留在同一边界内。
图片:展示 Codex 沙盒操作系统隔离边界的示意图。
Codex 需要借助由电脑操作系统强制执行的隔离能力,才能实现有效的沙盒。一些操作系统提供了能很好完成这件事的工具(例如 macOS 上的 Seatbelt,Linux 上的 seccomp 或 bubblewrap);不过,Windows 目前并没有开箱即用地提供这类能力。
为了让 Codex 在 Windows 上也能像在其他平台上一样安全、好用,我们需要实现自己的沙盒。
现有 Windows 工具的不足
Windows 提供了一些用于隔离的工具和原语。虽然没有一个完全符合我们的需求,但我们考察了若干潜在方案,主要包括 AppContainer、Windows Sandbox 和 Mandatory Integrity Control 标签。
AppContainer
- 是什么:AppContainer 是 Windows 原生沙盒,是一种基于 capability 的隔离模型,面向那些一开始就明确知道自己需要访问哪些内容的应用。
- 为什么考虑:它很有吸引力,因为它提供的是真正的 OS 边界,而不是尽力而为式的限制。
- 为什么不用:Codex 不是一个边界很窄的应用。它驱动的是开放式开发者 workflow:shell、Git、Python、包管理器、构建工具,以及 agent 认为自己需要的任何其他二进制文件。实践中,这让 AppContainer 与问题形态并不匹配。它是强隔离,但适用的是比“让 agent 像开发者一样操作”窄得多的一类 workload。
Windows Sandbox
- 是什么:Windows Sandbox 是 Microsoft 的一次性轻量级 VM。你会得到一个全新的 Windows 桌面和强隔离边界,并且会话结束后,里面做过的一切都会消失。
- 为什么考虑:理由很明显:它比 AppContainer 更兼容任意软件;从安全角度看,隔离边界也强得多。
- 为什么不用:Codex 需要直接作用在用户真实的 checkout、工具和环境上,而不是在一个需要额外设置以及主机/客户机桥接的独立一次性桌面里工作。它还有一个根本性的产品问题:Windows Sandbox 甚至不适用于 Windows Home SKU。
Mandatory Integrity Control(MIC)完整性标签
- 是什么:Windows 有一个叫“完整性级别”的概念,例如 low、medium 和 high,用于决定系统对对象和进程的信任程度。基本规则是,低完整性进程不能写入更高完整性级别的对象,即使普通 ACL 原本会允许它写入。例如,低完整性进程会被视为更不受信任,所以 Windows 会阻止它写入普通的中完整性对象,除非这些对象被明确重新标记为允许它写入。
- 为什么考虑:MIC 在纸面上看起来很优雅:以低完整性运行 Codex,把可写根目录重新标记为低完整性,然后让 Windows 强制执行“其他位置不可写”。这本可以给我们一条不需要管理员权限、背后又有真实 OS 机制支撑的路径。
- 为什么不用:和 ACL 一样,完整性标签会修改真实主机文件系统;而在这个方案中,语义变化尤其宽泛。把一个 workspace 标记为低完整性,并不只是意味着“Codex 可以在这里写入”。它意味着一般的低完整性进程都可以写入这里。在真实开发者机器上,这会把用户的实际 checkout 变成主机上的低完整性写入目标,比把 ACL 精准授予某一个沙盒设计要危险得多。
即使中完整性的开发工具仍能继续工作,workspace 底层的信任模型也已经以一种很难控制、更难证明合理的方式发生了变化。
评估完这些选项后,我们认为它们都不是可行起点,于是开始设计自己的方案,为 Windows 用户带来良好的 Codex 体验。
第一个原型:“非提权沙盒”
我们的第一个可运行原型结合了一组 Windows 概念和工具,以实现所需隔离。从一开始,我们的一个目标就是让它无需提权即可工作,也就是说,Codex 不应仅仅为了设置或运行沙盒,就要求用户授予管理员权限。这意味着我们要弄清楚如何合理限制两件事:文件写入和网络访问。
限制文件写入
如果完全不限制文件写入,就会带来安全问题。如果限制过度,沙盒又会损害用户生产力,需要不断请求批准。为解决这个问题,我们依赖了两个重要的 Windows 基础构件:SID 和 write-restricted token。
SID 让我们能给沙盒一个身份
SID,即 security identifier,是 Windows 绑定到权限上的身份。每个用户都有 SID,组也有 SID,甚至一次登录会话也有自己的 SID。例如,当前登录会话可能有一个类似 S-1-5-5-X-Y 的 SID。分配给本地管理员组的 SID 可能是 S-1-5-32-544。
Windows 还允许你创建合成 SID,这些 SID 不对应真实用户,但仍可出现在 ACL(access control list,访问控制列表)中,而 ACL 用于定义谁可以读取、写入或执行特定文件或目录。这让 SID 成为我们沙盒中的有用原语:我们可以创建专供 Codex 沙盒使用的 SID,而不会干扰机器上的其他内容。
Write-restricted token 限制 Codex 可以修改哪些文件
进程 token 是 Windows 中的安全对象,用于定义运行中进程的身份和权限。它们决定进程可以执行哪些操作。write-restricted token 是一种特殊的进程 token,会让 Windows 在写入操作上执行一次额外的访问检查。
要让一次写入成功,必须通过两项检查:
- 普通用户身份(token 的“owner”)必须被允许执行该操作
- token 的 restricted SID 列表中,至少有一个 SID 也必须被授予访问权限
图片:标题为“沙盒写入同时需要普通用户访问权限和 sandbox-write SID 访问权限”的示意图。
实践中,这些检查让我们可以使用 ACL 精确界定沙盒可以修改文件系统的哪些位置,从而获得写入操作所需的粒度。
借助 SID 和 write-restricted token,我们的非提权沙盒以如下方式工作:
- 沙盒设置过程会创建一个名为
sandbox-write的合成 SID。 sandbox-writeSID 会被授予对以下位置的写入、执行和删除权限:- 当前工作目录
config.toml中配置的任何额外writable_roots
- 沙盒设置过程会明确拒绝同一个 SID 对“可写区域内只读”位置的写入访问,例如:
<cwd>/.git<cwd>/.codex<cwd>/.agents
- Codex 会在 write-restricted token 下启动命令,该 token 的 restricted SID 列表包括
Everyone、当前登录会话 SID,以及sandbox-write合成 SID。
这个流程有效解决了限制文件写入的问题,看起来很有前景。接下来,我们需要一个限制沙盒网络访问的方案。
限制网络访问
限制网络访问是沙盒的重要组成部分;没有它,恶意代码就可能把机器上的数据外泄到互联网。因为我们想避免要求提权,所以可用于强力阻断网络流量的选项很有限。我们想使用的工具,例如 Windows Firewall,通常无法在没有管理员权限的情况下设置。
既然 Windows Firewall 不是选项,我们就尽量约束自己能控制的部分。我们试图让子环境对开发者实际使用的各类联网工具默认以失败收场,使 Git 命令、包安装器等在沙盒中失败,并要求用户批准任何面向互联网的操作。思路是封堵最明显的逃逸路径:把能识别代理的流量发送到一个不可用端点,让 Git 的 HTTP(S) 传输也这样做,并让通过 SSH 的 Git 立即失败。
在此之上,我们还把一个很小的 denybin 目录前置到 PATH,并重新排序 PATHEXT,使 stub SSH 和 SCP 脚本会先于真实二进制文件被解析到。
例如,下面是我们用于限制网络访问的一些具体环境覆盖:
HTTPS_PROXY=http://127.0.0.1:9ALL_PROXY=http://127.0.0.1:9GIT_HTTPS_PROXY=http://127.0.0.1:9NO_PROXY=localhost,127.0.0.1,::1GIT_SSH_COMMAND=cmd /c exit 1
图片:展示非提权沙盒网络环境覆盖的示意图。
这能拦住很多由普通工具驱动的流量,但它仍然只是建议性的。进程可以忽略环境变量、绕过 PATH,或者直接打开 socket,风险太高。
非提权方案伴随着取舍
和任何有意思的软件实现一样,第一个原型有优点也有缺点。它只用少数标准 Windows 能力就完成了任务,允许非常明确且细粒度的文件系统写入,并且无需提权运行,减少了用户接受过多提权提示或必须在本机拥有管理员权限的需求。但它也有一些实实在在的缺点,其中一部分让它无法成为我们的最终设计:
- 设置速度:应用 workspace ACL 的成本可能很高,具体取决于 workspace 目录的拓扑结构。
- 占用痕迹:我们会对开发者系统应用真实 ACL,尽管这个痕迹并不特别侵入,因为所有应用的 ACL 都只涉及一个自定义创建、仅供沙盒使用的合成 SID。
- 语义难以变更:依赖 ACL 做基于文件的限制,意味着改变沙盒语义既昂贵又复杂。相比之下,在 macOS 上,我们可以动态改变用于配置 Seatbelt 的
.sbpl文件的生成方式;而 Windows 沙盒可能需要一次缓慢且繁重的操作来调整 ACL。 - 网络保护很弱。如前所述,它是“建议性”的,肯定会被某些实现了自有网络栈的程序绕过,也不是为了抵御对抗性代码而设计的。
前三个问题是自定义沙盒实现固有的,前提是它要足够灵活,能够支持 agentic flows。网络抑制则是另一回事。
网络抑制太重要了
除了恶意 agent 可以轻松绕过基于环境变量的网络抑制之外,很多善意代码或二进制文件也会绕过它,只要它们不遵守环境中的代理变量,或者实现了自己的基于 socket 的网络代码。我们认为,这一点足以让我们投入一个更好的沙盒模式。
为了获得更好的网络抑制,我们想使用 Windows Firewall,因为它允许我们阻止某些用户或程序的出站网络流量。遗憾的是,出于几个原因,我们无法有效创建一条只适用于 Codex harness 生成的命令的可用防火墙规则:
- Windows 不允许把防火墙规则匹配到 restricted token 的非 principal 身份。这意味着我们无法把防火墙规则应用到“任何 restricted SID 列表中包含我们合成 SID 的 token”。
- 虽然我们可以创建一条匹配特定二进制文件的防火墙规则,但这只能限制
codex.exe自身的网络访问。它不会适用于 agent 代表用户生成的进程,例如 Git 或 Python 进程。 - 其他防火墙匹配维度也不合适。按用户限定的规则在非提权设计中仍然匹配真实 Windows 用户,而不只是受限子进程。按程序路径的规则又过于粗糙:它们可以整体阻止
codex.exe或python.exe,但无法阻止这一次处于沙盒中的python.exe调用。按端口或地址的规则也完全不是我们想要的策略。
比如,我们并不想阻止 443 端口;我们想阻止这个特定受限进程树的任意出站访问。
要把防火墙规则专门应用到我们的沙盒命令上,我们需要让它们作为一个独立 principal 运行,而不是作为“真实”用户运行。这个思路把我们带上了一条新路径,在这条路径上,我们放宽了“无需提权”的约束。
重新设计:“提权沙盒”
沙盒的下一次迭代,也就是我们当前的实现,在设置阶段需要提权的管理员权限。因此,我把它称为“提权沙盒”。在 Codex 于系统上生成命令的边界处,提权沙盒看起来很像非提权沙盒。
它仍然在 restricted token 下运行子进程,类似地使用 write_restricted token,并包含同样的 restricted SID 列表 [Everyone, Logon, Synthetic]。不过,这个 token 的 principal 不再是真实 Windows 用户,而是 Codex 自己创建的两个本地用户之一:
CodexSandboxOffline(防火墙规则针对的那个用户)CodexSandboxOnline(防火墙规则不针对的那个用户)
这个看似很小的细节,其实对沙盒、谁能使用它,以及设置和运行时执行的复杂性都有很大影响。
图片:展示带有防火墙规则和专用 Windows 用户的提权沙盒架构示意图。
它在视觉上类似非提权原型,但引入了防火墙规则和一个真正运行命令的专用 Windows 用户。(不过,引入这些新概念也意味着,在沙盒开始运行并保护命令之前,需要完成更多设置工作。)
我们现在需要一个一等设置步骤
非提权沙盒设计有一个简单的设置步骤,但工作量相对较小:
- 如有需要,创建合成 SID
- 为 sandbox-write 合成 SID 应用 ACL
而提权沙盒需要做更多事。
- 如果尚未创建,创建合成 SID
- 如果尚未创建,创建 online 和 offline 沙盒用户
- 在本地存储新创建用户的凭据,并使用 Windows Data Protection API(DPAPI)加密,且存放在沙盒用户实际上无法读取的位置
- 创建防火墙规则,以阻止
CodexSandboxOffline用户的所有出站网络访问;如果规则已经存在,则验证它们是否正确
设置阶段还有一个额外复杂点。Codex 的沙盒预期应拥有等同于真实 Windows 用户的读取访问权限。在非提权沙盒中,由于 restricted token 的 principal SID 就是 Windows 用户,这一点可以实现。但当 principal 变成新的 CodexSandbox 用户时,这并不是自动成立的。Windows 上许多相关目录会向“Authenticated Users”授予读取/执行权限。一个显著例子是用户的 profile 目录。
默认情况下,Windows 用户不能读取其他 Windows 用户的 profile 目录,因此在许多场景下,即使是简单的文件读取也会失败。
为解决这个问题,我们在沙盒设置流程中又增加了一层,用于在可能尚不存在相关 ACL 的位置,向沙盒用户授予读取 ACL。例如,针对一些常用 Windows 目录:
C:\Users\<real-user>C:\Windows\C:\Program Files\C:\Program Files (x86)\C:\ProgramData\
由于这个目录列表是尽力而为的,而且在每个目录上安装 ACL 的成本可能相当高,我们会异步运行这段逻辑,这样阻塞用户的沙盒设置步骤就不必等待它们全部完成。
我们把设置逻辑封装在自己的二进制文件中,部分原因是只在需要时跨越 UAC 边界。但更深层的原因是架构性的:沙盒设置与 codex.exe 的职责根本不同。
把沙盒设置逻辑放在专用二进制文件中,可以让 codex.exe 保持为普通的非提权 harness;避免 Windows 专属设置机制膨胀到其他平台上的 codex.exe 中;让较长时间运行的设置工作与主进程生命周期解耦;并给我们一个统一位置来处理沙盒所需的不同设置路径。
图片:展示一等提权沙盒设置步骤的示意图。
命令运行器是一个真正运行用户命令的新二进制文件
由于 Windows 用户和 token 登录边界的工作方式,我们无法继续像非提权沙盒那样创建 restricted token 并在其下生成进程。为了真正以另一个 Windows 用户身份生成命令,我们最初设想了以下流程:
codex.exe以真实 Windows 用户身份运行。随后,Codex 按顺序执行:- 为沙盒用户调用
LogonUserW(...)。 - 在该沙盒用户 token 上调用
CreateRestrictedToken(...)。 - 使用这个受限沙盒用户 token,调用
CreateProcessAsUserW(...)来启动最终子进程。
- 为沙盒用户调用
实践中,这个期望流程无法工作,因为在 CreateProcessAsUserW(...) 处遇到了权限墙。这意味着 codex.exe 可以为沙盒用户创建 restricted token,但无法从真实用户这一侧可靠地用该 token 启动子进程。我们需要一个已经以沙盒用户身份运行的进程,这样限制步骤和最终生成进程就可以发生在沙盒用户这一侧,而不是发生在真实用户这一侧。
这个需求催生了 codex-command-runner.exe,一个新二进制文件,其唯一职责就是创建 restricted token 并生成所请求的命令。我们不再要求 codex.exe 自己完成整个流程(真实用户 → 沙盒用户 → restricted token → 子进程),而是把流程拆成两部分:
Part 1
codex.exe调用CreateProcessWithLogonW(...),以沙盒用户身份启动codex-command-runner.exe,此时尚不使用 restricted token。
Part 2
- 在 runner 内部,
OpenProcessToken(GetCurrentProcess(), ...)会打开 runner 自己的 token,而该 token 已经属于沙盒用户。 - runner 调用
GetTokenInformation(...)来提取沙盒 logon SID,然后调用CreateRestrictedToken(...)构建最终的 restricted token。 - 仍然在 runner 内部,它调用
CreateProcessAsUserW(...),并使用这个 restricted token 来启动真正的子进程。
图片:展示命令运行器生成 restricted 命令流程的示意图。
全貌
Albert Einstein 曾说:“Everything should be made as simple as possible, but no simpler.” 本着这种精神,我们的设计充分解决了每个问题。最终架构包含前面讲过的四层:
codex.exe本身codex-windows-sandbox-setup.exe,用于处理所有与提权设置相关的工作codex-command-runner.exe,用于运行 restricted token 命令- 子进程
我刚开始做这个项目时,并没有很明确地知道它最终会走向哪里。我的做法是先在 Codex 与操作系统之间的边界上为沙盒能力添加 instrumentation。这个做法与 Codex 在 macOS 和 Linux 上实现沙盒的方式非常接近。
随着我进一步了解 Windows 提供的具体工具,并经历了数十个关于安全性和易用性平衡的决策,系统逐渐成长为当前形态:多个二进制文件、自定义用户、防火墙规则、提权设置步骤、异步进程,以及更多内容。
这不是一个特别简单的系统,但每一份复杂性都是出于必要而加入的,目的是构建一个既安全,又尽可能不打扰用户的沙盒。
图片:展示最终 Windows 沙盒架构的示意图。
在安全性与实际可用性之间取得平衡
为了给 Windows 上的 Codex 用户交付良好的用户体验,我们的目标是构建一个既安全又不牺牲可用性的系统。毕竟,使用 Codex 的全部意义,就是让 agents 能够在无需你持续关注的情况下完成工作。
这个项目给我的最大教训之一是,Windows 并没有直接交给我们一个能干净映射到“安全的自主 coding agent”的原语。我们组合了多个工具和概念,才构建出一个连贯系统。一些早期想法走进了死胡同。最终设计则是早期原型的混合体,而这些原型各自解决了问题的一部分。
另一个教训是,coding agent 的安全性不同于更经典的应用安全。Codex 必须服务于真实的开发者 workflow。工程工作的核心,是在兼容 agentic workloads 与实现真实强制约束之间取得平衡。这种张力塑造了最终设计中的取舍。
想看看 Codex 沙盒实际运行起来是什么样吗?试试看。