ai官小西

Codex Windows 沙箱深度解析 — 为一个 AI 编程 Agent 构建操作系统级隔离

Codex Windows 沙箱架构:V1 原型 vs V2 生产设计对比

“2025 年 9 月我加入 Codex 工程团队时,Codex for Windows 还没有沙箱实现。Windows 用户被迫在两个糟糕的选项之间选择:要么批准 agent 想运行的几乎每个命令,要么启用全权限模式,信任模型不会作恶。两种体验都很差,而且完全可以避免。”

这段话是 David Wiesen 说的,他花了几个月时间设计 Codex 的 Windows 沙箱。他那篇详细的工程博客完整记录了整个过程——这也是目前关于“如何在一个没有现成方案的操作系统上为 agent 构建沙箱”这个问题的最佳记录之一。

本文不是翻译。本文是对工程决策的结构化分析:为什么在每个关口选这条路而不是那条,哪些模式在 Windows 之外依然成立,以及如果你自己在构建沙箱化 agent 运行时该偷什么。

威胁模型

在讨论实现之前,有必要把威胁模型说清楚。Codex 运行在开发者笔记本电脑上,拥有登录用户的权限。Agent 默认可以读文件、写文件、创建进程、访问网络。模型在云端运行并发来命令,harness 在本地执行。

沙箱需要约束两件事:

  1. 文件写入 — agent 只能在工作区目录内(以及显式配置的可写根目录)写文件。其他地方只读或不可访问。
  2. 网络访问 — agent 不应能发起任意出站连接。没有这个约束,一个被攻陷或产生幻觉的模型就能外泄源码、环境变量或凭证。

这两个约束不是可选项。它们是让自主编程 agent 可以安全运行的最低限度。而且必须由操作系统强制执行,不能只靠 harness——advisory 级别的限制在进程决定无视它的那一刻就崩溃了。

为什么 Windows 这么难

在 macOS 上,Codex 使用 Seatbelt(macOS 沙箱)。在 Linux 上,seccomp 和 bubblewrap 提供内核级系统调用过滤。两者都是久经考验的原语,可以配置成:“这个进程树可以写这里、连那里,其他都不行。”

Windows 没有等价物。它有强大的隔离原语,但没有一个能干净地映射到“让自主 agent 像开发者一样操作”。

团队评估了三个候选方案:

AppContainer

AppContainer 是 Windows 原生应用沙箱,基于能力(capability)模型。你需要事先声明应用需要什么——文件路径、网络能力、设备访问——Windows 强制执行这些边界。

问题在于:Codex 不是一个职责单一的应用。它驱动的是开放式的开发者工作流:shell、Git、Python、包管理器、构建工具,以及 agent 决定需要的任何其他二进制。为一组未知的工具预先声明能力是不可能的。AppContainer 提供了强隔离,但只适用于比“让 agent 像开发者一样操作”狭窄得多的工作负载。

Windows Sandbox

Windows Sandbox 是微软的可丢弃轻量虚拟机。你得到一个全新的 Windows 桌面,有强隔离边界,会话结束时一切消失。

问题既是技术层的也是产品层的。技术层:Codex 需要在用户的实际 checkout、工具和环境中操作,不是在一个需要 host/guest 桥接的独立桌面内。产品层:Windows Sandbox 在 Windows Home SKU 上根本不可用。单这一点就足以排除。

强制完整性控制(MIC)

MIC 使用完整性级别——低、中、高——来控制哪些进程能写哪些对象。低完整性进程不能写中等完整性对象,即使 ACL 允许也不行。

这个想法很优雅:以低完整性运行 Codex,把可写根目录标记为低完整性,让 Windows 强制执行对其他地方不可写。不需要提权。

致命缺陷:完整性标签是全局的,不是按沙箱边界隔离的。把一个工作区标记为低完整性不只是意味着“Codex 可以写这里”。它意味着该机器上任何低完整性进程都可以写这里。在真实的开发者机器上,这会把用户的实际 checkout 变成一个供整个系统使用的低完整性水池,比给单个沙箱设计授予精准 ACL 危险得多。

这里值得停下来思考:MIC 的失败模式正是“试图将全局系统机制重新用于单沙箱场景”时会发生的。完整性的概念是正确的,但作用域是错的。

V1:非提权原型

三个标准方案都被排除了,团队开始构建自定义解决方案。第一个约束是不提权——Codex 应该不需要弹管理员权限提示就能工作。

文件写入控制:SID 与写入受限令牌

SID(安全标识符)是 Windows 的身份原语。用户有 SID,组有 SID,登录会话有 SID。你还可以创建不与任何真实用户对应但仍能出现在 ACL 中的合成 SID。

沙箱设置创建了一个名为 sandbox-write 的合成 SID,在工作区目录和任何配置的可写根目录上授予写权限,并在 .git.codex.agents 子目录上显式拒绝。

Codex 随后在写入受限令牌下启动命令——这种令牌要求任何写操作必须通过两重检查:正常用户身份必须被允许,并且受限 SID 列表中至少有一个 SID 也被授权。受限列表包含 Everyone、登录会话 SID 和合成 sandbox-write SID。

这给了团队粒度精准的写控制能力,仅使用标准 Windows 原语,无需提权。

网络抑制:Advisory 环境变量操控

没有 Windows Firewall(需要提权),团队尝试让子环境做到 fail-closed。他们污染了环境变量:

  • HTTPS_PROXY=http://127.0.0.1:9
  • ALL_PROXY=http://127.0.0.1:9
  • GIT_SSH_COMMAND=cmd /c exit 1

他们在 PATH 前面插入了 denybin 目录(包含存根 SSH/SCP 脚本),重新排序 PATHEXT 让存根在真实二进制之前解析。

这捕获了大多数正常工具流量。但它是 advisory 的——进程可以忽略环境变量,绕过 PATH,或直接打开原始套接字。

V1 为什么被否决

V1 有真正的优点:不提权、粒度精准的文件写入控制、干净地使用标准 Windows 原语。但网络抑制从根本上来说是坏的。Advisory 限制对抗不了恶意代码,即使是善意的但不遵循代理环境变量的二进制也会泄漏流量。

自定义方案有三个固有问题:ACL 在大工作区上可能很慢,ACL 足迹必须留在真实文件系统上,且修改沙箱语义需要重新应用 ACL。但网络问题是决定性的否定票。

V2:提权沙箱

为了获得强网络抑制,团队需要 Windows Firewall。而要做到把防火墙规则精准指向沙箱化进程树——不是笼统地指向 codex.exe,也不是所有调用的 python.exe——他们需要以不同的 Windows principal 身份运行子进程。

这意味着放弃“不提权”约束。

两个沙箱用户

提权沙箱创建两个本地 Windows 用户:

  • CodexSandboxOffline — 被防火墙规则锁定,阻止所有出站网络访问
  • CodexSandboxOnline — 不受防火墙规则影响(用户显式启用网络访问时使用)

防火墙规则简单且有效:阻止 CodexSandboxOffline 的所有出站流量。不需要猜测代理变量,不需要拦截 socket 调用,不需要路径操控。操作系统在网络栈层面强制执行。

命令运行器:跨边界架构

以不同用户身份运行子进程引入了一道权限墙。codex.exe 以真实 Windows 用户身份运行。但 CreateProcessAsUserW——以不同用户身份启动进程的 API——要求调用进程已经在该用户的令牌下运行。

解决方案是 codex-command-runner.exe,一个专门的二进制,唯一的工作就是铸造受限令牌并启动所请求的命令。流程分成两部分:

第一部分codex.exe 调用 CreateProcessWithLogonW 以沙箱用户身份启动 codex-command-runner.exe(此时尚未使用受限令牌)。

第二部分 — 在运行器内部,它打开自己的令牌(属于沙箱用户),从中构建受限令牌,然后调用 CreateProcessAsUserW 启动真正的子进程(gitpython 等等)。

这个分界是整个架构中最关键的洞见。codex.exe 无法直接启动受限子进程,因为它在用户边界的错误一侧。运行器是一个 trampoline:它位于沙箱用户一侧,在那里令牌限制和子进程启动都是被允许的。

读访问问题

当子进程以 CodexSandboxOffline 而非真实用户身份运行时,它会失去对那些只对真实用户做了 ACL 的文件的读访问权限。例如用户的 profile 目录默认不可被其他用户读取。

沙箱设置在常用目录上授予沙箱用户读 ACL:用户的 profile、C:\WindowsC:\Program FilesC:\Program Files (x86)C:\ProgramData。这个操作是异步执行的,因为在大目录树上应用 ACL 很昂贵且不应阻塞用户。

四层架构

最终设计分为四层:

  1. codex.exe — 普通用户态 harness,永不提权
  2. codex-windows-sandbox-setup.exe — 运行一次(或配置变更时),处理 UAC 提权,完成用户创建、ACL 授予和防火墙规则
  3. codex-command-runner.exe — 每个命令启动一次,跨越用户边界,铸造受限令牌,启动子进程
  4. 子进程 — agent 要求运行的实际命令,现在在沙箱内执行

每个二进制有且只有一个任务。Setup 二进制跨越 UAC 边界,所以 codex.exe 永远不需要。运行器跨越用户边界,所以 codex.exe 永远不需要。架构之所以干净,正是因为每个组件的职责定义得足够锐利。

值得借鉴的设计原则

研究完这个架构后,几个模式浮现出来,它们的适用性远超 Windows:

1. 将 Setup 与 Runtime 分离

沙箱设置(用户创建、ACL 授权、防火墙规则)和启动沙箱化进程是根本不同的工作。分离的二进制让你只在需要的地方跨越提权边界,将平台特定代码保持在主 harness 之外,并将耗时较长的设置工作与主进程生命周期解耦。

2. 用操作系统,不用 Harness

V1 失败是因为基于环境的网络抑制是 advisory 的。V2 成功是因为 Windows Firewall 在内核级别强制执行。当你需要一个安全边界时,使用操作系统。Harness 级别的检查是便利功能,不是安全。

3. 创建合成身份来实现细粒度策略

合成 SID 模式——创建一个只属于沙箱的身份,授予它定向访问权限,然后将进程限制到该身份——干净且可移植。在 Linux 上映射到自定义组或 SELinux 类别。在 macOS 上映射到带自定义 entitlements 的 Seatbelt 扩展。

4. 使用 Trampoline 模式进行跨边界执行

当你需要以不同身份运行代码且当前进程不能直接跨越边界时,插入一个 trampoline。运行器除了切换上下文并交接之外什么都不做。这种模式到处可见:Linux 上的 setuid 二进制、macOS 上的 XPC 服务,以及这里的 codex-command-runner.exe

5. 对非关键工作使用懒加载/异步设置

读 ACL 授予是异步运行的。如果慢,不会阻塞用户。如果失败,沙箱优雅降级(某些读取失败,用户看到错误,可以重试)。不是每个设置步骤都需要阻塞关键路径。

6. 在线/离线执行分离

双用户模式(Offline vs Online)很聪明:不是按命令动态添加和删除防火墙规则,而是创建两个带有永久规则的身份,在启动时选择合适的那个。这比规则操作更简单、更快、更容易避免出错。

反模式:不该做什么

不要将全局机制用于单沙箱场景

MIC 标签失败是因为完整性级别是全局的,不是按边界隔离的。“工作区对 Codex 是低完整性”变成了“工作区对一切都是低完整性”。当你重新利用系统级机制时,副作用也是系统级的。

不要把 Advisory 控制当作安全

基于环境的网络抑制捕获了正常流量,在测试中看起来不错。但任何依赖进程选择合作的“安全模型”都不是安全模型。它是一个贴了误导标签的便利功能。

不要假设平台功能普遍可用

Windows Sandbox 被排除的部分原因是它在 Windows Home 上不可用。如果你的产品需要在消费者 SKU 上工作,就不要在没有备选方案的情况下依赖仅企业版可用的功能。

不要把 Setup 逻辑塞进主二进制

codex-windows-sandbox-setup.execodex.exe 分开是一个明确的架构决策,而不是实现上的捷径。Setup 有不同的权限要求、不同的失败模式、不同的生命周期。把它们合在一起会把平台特定的 UAC 逻辑污染到主 harness 中。

安全-可用性-性能三角

这个项目是竞争性约束之间张力的案例研究:

维度 做出的取舍 理由
安全 提权设置(UAC 提示) 防火墙集成需要管理员权限;advisory 网络控制不是安全
可用性 异步读 ACL 授予 对大目录树做阻塞式设置不可接受
性能 大工作区上 ACL 应用成本 接受为一次性设置成本;通过异步执行缓解
兼容性 专用沙箱用户而非复用真实用户 启用防火墙定向,但需要显式读 ACL 授予
复杂度 四层架构而非单体设计 每层职责清晰;复杂度局部化而非分散

最有趣的取舍是提权要求。团队通过整个原型周期都在抵抗它。只有当明确看到强网络抑制需要一个只有提权才能提供的机制时,他们才接受了。这是正确的决策:先把更简单的路径走到底,然后在确实需要时才接受复杂度。

构建 Agent 沙箱的落地检查清单

如果你正在为 AI agent 构建沙箱化执行环境,以下是需要思考的事项:

  • 明确定义威胁模型。Agent 不加约束时能做什么?沙箱必须阻止什么?写下来。
  • 清点平台上的隔离原语。Linux:namespaces、seccomp、cgroups、SELinux/AppArmor。macOS:Seatbelt、sandbox-exec。Windows:tokens、SID、AppContainer、firewall。
  • 先试最简单的路径。非提权原型花了几周,不是几个月。它证明了什么可行(文件写入)和什么不可行(网络)。这比预先设计“完美架构”便宜得多。
  • 如果你的网络抑制是基于环境的,它就不是抑制。找到一个内核级机制,或者显式接受风险。
  • 分离设置与运行时,提权与正常操作。Harness 永远不应该提权。Setup 二进制只跨越一次 UAC 边界。
  • 使用合成身份实现定向策略。不要给真实用户授权然后试图减去权限。创建一个新身份,只授予必要的权限。
  • 考虑 trampoline 模式用于跨边界执行。如果不能直接启动,插入一个可以启动的进程。
  • 在所有目标 SKU 上测试。Windows Home、Pro、Enterprise。消费者和企业环境。不是所有变体上都能用的功能就不是你能依赖的功能。
  • 异步授予读权限。它慢且容忍失败。不要让用户等它。
  • 验证限制在 execfork 和进程树中持续存在。一个只对第一个进程生效的沙箱是坏的。
  • 为沙箱语义变更做好计划。基于 ACL 的方案修改起来昂贵。动态配置方案(如 macOS Seatbelt 的 .sbpl 文件)演进成本更低。

局限性与适用范围

这个沙箱是为特定威胁模型设计的:一个运行在开发者笔记本电脑上的 AI 编程 agent,主要风险是意外文件修改和数据外泄。它不是为了防御以下情形而设计的:

  • 内核漏洞利用或提权攻击
  • 硬件级侧信道
  • 以更高权限运行的被攻陷系统二进制

设计也假设用户信任 Codex harness 本身。如果 codex.exe 被攻陷,沙箱不提供任何保护——harness 在边界之外。

这些不是批评。每个沙箱都有威胁模型。Codex 团队的模型正确地限定了运行 AI 编程 agent 的实际风险范围,实现也精确匹配了这个范围。

资料来源


本文是对 OpenAI 工程博客文章的技术深度解读。架构图为根据原文中 V1 原型与 V2 生产设计描述的原创重构。