跳到主要内容

Codex 安全模型 — 审批策略 × OS 沙箱 × 失败再升级

这章讲什么: Codex 敢在你真机上跑命令、改文件,靠的是三层叠起来的护栏。本章讲清这三层各管什么、怎么协作:审批策略(哪些动作要问你)、沙箱策略(命令能动哪些资源)、以及把二者粘起来的 ToolOrchestrator(沙箱被拒时按策略再升级)。这是 Codex 区别于「玩具 agent」的地方。

1. 它要解决的小问题

你想让 agent 自主跑命令(跑测试、装依赖、改文件),但又怕它:删错文件、把密钥发到外网、改坏 .git/hooks 提权。

两难:

  • 管太松 → 危险,可能毁你机器或泄密;
  • 管太严(每条命令都问你) → 烦,失去自动化意义。

Codex 的答案是把控制拆成两个正交的旋钮,再加一层智能重试:

旋钮管什么枚举
审批策略 AskForApproval哪些动作需要你点头UnlessTrusted / OnRequest / Granular / Never
沙箱策略 SandboxPolicy命令实际能动哪些资源ReadOnly / WorkspaceWrite / DangerFullAccess / ExternalSandbox

二者正交:你可以「自动放行,但关进只读沙箱」,也可以「全权访问,但每步都问我」。

2. 旋钮一:审批策略(谁要问你)

AskForApproval 定义在 protocol/src/protocol.rs:901,四档:

档位含义
UnlessTrusted(序列化名 untrusted)只有「已知安全且只读」的命令自动放行,其余都问你
OnRequest(默认)由模型决定何时请你批准
Granular(...)细粒度开关:分别控制 shell 审批、规则提示、技能脚本、request_permissions、MCP elicitation 等是否允许
Never永不问你;命令失败直接把错误返回模型,不升级给你

直觉:UnlessTrusted 最谨慎(默认怀疑),Never 最放手,OnRequest 把判断权交给模型,Granular 给你逐类微调。

3. 旋钮二:沙箱策略(能动哪)

SandboxPolicy 定义在 protocol/src/protocol.rs:988,描述命令被允许触碰的资源边界:

策略文件网络
ReadOnly只读默认禁(可开 network_access)
WorkspaceWrite只读全盘 + 可写工作区(cwd、可加 writable_roots、默认含 /tmpTMPDIR)默认禁
DangerFullAccess无限制无限制
ExternalSandbox已在外部沙箱中,放开磁盘、按外部设置定网络由外部定

一个值得学的提权防护: WorkspaceWrite 即便把某个根设为可写,也会把根下一些敏感子路径设回只读——尤其是 .git/hooks.codex 这类「改了就能提权」的目录。看 WritableRoot 的文档注释:

// codex-rs/protocol/src/protocol.rs:1038 起(节选)
/// 用来确保:可写根下那些「一旦被改就能提升 agent 权限」的目录
/// (如 .codex、.git,尤其 .git/hooks)不会被 agent 修改。
pub struct WritableRoot {
pub root: AbsolutePathBuf,
pub read_only_subpaths: Vec<AbsolutePathBuf>, // 这些子路径保持只读
pub protected_metadata_names: Vec<String>, // 这些元数据名禁止新建/替换
}

直觉:「可写工作区」不等于「整个工作区都能改」——会改命运的元数据被单独锁住。这是个很实在的安全细节。

4. 沙箱靠 OS 真隔离,不是「自律」

Codex 的沙箱不是「让模型保证不乱来」,而是操作系统层面真的把进程关进笼子,跑命令的子进程在内核层面就动不了笼子外的东西。三套实现按平台分:

平台机制枚举值
macOSSeatbelt(sandbox-exec + .sbpl 策略)SandboxType::MacosSeatbelt
LinuxLandlock + seccompSandboxType::LinuxSeccomp
Windows受限令牌(restricted token)SandboxType::WindowsRestrictedToken
任意不沙箱SandboxType::None

枚举见 sandboxing/src/manager.rs:35SandboxManager 负责把一条命令「包」进对应沙箱(transform/select_initial,manager.rs:334 起)。

macOS 的策略是一份 .sbpl(Seatbelt Policy Language),精神是「默认全禁,再逐项开口子」——这是最稳的安全姿势:

; codex-rs/sandboxing/src/seatbelt_base_policy.sbpl(节选)
(version 1)
(deny default) ; 关键:默认全部拒绝
(allow process-exec) ; 再按需放开:允许执行子进程
(allow process-fork)
(allow file-write-data ; 只放开极少数白名单,如 /dev/null
(require-all (path "/dev/null") (vnode-type CHARACTER-DEVICE)))

记住一句:(deny default) 起手——任何没被显式 allow 的能力都被内核挡掉。可写区域、网络等再由 SandboxPolicy 翻译成具体的 allow 规则注入。

5. 把两个旋钮粘起来:ToolOrchestrator

光有两个旋钮还不够。真正聪明的地方是 ToolOrchestrator——每个会动副作用的工具(shell / apply_patch)都经它走一套固定流程。文件顶部注释把它讲得极清:

// codex-rs/core/src/tools/orchestrator.rs:1 起(节选)
// 审批 → 选沙箱 → 尝试 → 被拒则用「升级后的沙箱策略」重试
// (靠缓存,重试时不再重复要审批)

流程图:

工具调用进来

① 审批:按 AskForApproval 判断
│ ├─ Skip → 直接放行
│ ├─ Forbidden → 拒绝
│ └─ NeedsApproval → 问你 / 问 guardian,拒了就终止

② 选沙箱:按 SandboxPolicy 选 seatbelt/landlock/None

③ 第一次尝试(在沙箱里跑)

├─ 成功 → 返回输出
└─ 被沙箱拒(SandboxErr::Denied)


④ 升级判定:这个工具/策略允许「脱沙箱重试」吗?
├─ 允许 → (必要时再要一次审批) 脱沙箱重跑
└─ 不允许 → 把沙箱拒绝原样返回模型

怎么读这张图: 左边一路顺下来是「顺利放行」;右下角的分支是精华——沙箱拒绝不等于失败,而是触发一次「要不要升级权限重试」的决策。

真实锚点:

  • 编排主流程 ToolOrchestrator::run(orchestrator.rs:134),// 1) Approval(:151)、// 2) First attempt under the selected sandbox(:223)。
  • 审批要求的三态 ExecApprovalRequirement::{Skip, Forbidden, NeedsApproval}(orchestrator.rs:159-221)。
  • 第一次尝试被拒后的升级逻辑:匹配 SandboxErr::Denied(orchestrator.rs:297),据 escalate_on_failure()unsandboxed_execution_allowed(...)wants_no_sandbox_approval(...) 等决定是否脱沙箱重试(:321-:391)。
  • 升级重试默认不再重复要审批——靠「已批准」缓存 should_bypass_approval(approval_policy, already_approved)(:389-391),除非是严格自动审查(strict auto-review)需要 guardian 重新过一遍。

这套设计的巧妙:先用最严的沙箱试,大多数命令在沙箱里就跑通了,你完全无感;只有真撞墙(比如要写沙箱外的路径)才升级,而升级与否由你设的审批策略说了算。安全与省心由此兼得。

6. 命令是否「已知安全」:assess 系列

审批那一步要回答「这命令安不安全到能自动放行?」。这由 safety.rsassess_* 系列裁决,返回一个 SafetyCheck:

结果含义
AutoApprove { .. }直接放行(可能附带选好的沙箱)
AskUser升级给你点头
Reject { .. }直接拒绝

以补丁为例,assess_patch_safety(safety.rs:32)逻辑骨架:

  • 若审批策略是 UnlessTrusted → 直接 AskUser(默认怀疑,补丁一律问你);
  • 若是 Never 且沙箱不允许无沙箱执行 → 倾向 Reject;
  • 否则结合沙箱类型给 AutoApprove

SafetyCheck 枚举与各分支见 safety.rs:21:39-108AskForApproval::UnlessTrusted 那条所说的「known safe 只读命令」由 is_safe_command() 判定(注释 protocol.rs:902-904)。

7. 三层怎么协作(串起来看)

模型要跑一条命令


[审批策略] 这条要问你吗? ──问── 你/guardian 点头或拒绝
│ 不用问 / 已批准

[沙箱策略] 该把它关进哪种笼子?(ReadOnly/WorkspaceWrite/...)


[OS 沙箱] seatbelt / landlock 真隔离地跑

├─ 通过 → 输出回灌模型
└─ 被拒 → [Orchestrator] 按审批策略决定:升级重试,还是把拒绝告诉模型

三层各司其职、正交可调,正是 Codex 能「自动化但不失控」的底座。

8. 边界与局限

  • 沙箱依赖平台能力。 Linux 上若内核不支持 landlock、或缺 codex-linux-sandbox 可执行,会退化(SandboxTransformError::MissingLinuxSandboxExecutable 等,sandboxing/src/lib.rs:50 起)。DangerFullAccess 则等于不沙箱。
  • 网络默认禁、按需开。 ReadOnly/WorkspaceWrite 默认不放网络,需显式 network_access: true;放网络后还有单独的网络审批/代理路径(orchestrator 里的 network_approval 系列)。
  • 审批可被关到底。 Never + DangerFullAccess 等于完全放手,该组合把安全责任全交给用户,文档/UI 会警示。
  • 「升级」是双刃。 脱沙箱重试虽方便,但意味着权限抬升;Codex 用「是否已批准 / 是否 strict auto-review / 沙箱策略是否允许无沙箱执行」多个条件层层设防,改这块务必读全 orchestrator.rs:289-460 的分支。

9. 代码地图

主题文件符号
审批策略枚举codex-rs/protocol/src/protocol.rsAskForApprovalGranularApprovalConfig
沙箱策略枚举codex-rs/protocol/src/protocol.rsSandboxPolicyWritableRoot
安全裁决codex-rs/core/src/safety.rsassess_patch_safetySafetyCheck
工具编排(三层粘合)codex-rs/core/src/tools/orchestrator.rsToolOrchestrator::runrun_attemptExecApprovalRequirement
沙箱选择/变换codex-rs/sandboxing/src/manager.rsSandboxManagerSandboxTypeselect_initial
macOS 策略codex-rs/sandboxing/src/seatbelt_base_policy.sbpl(deny default)
Linux 沙箱codex-rs/sandboxing/src/landlock.rscodex-rs/linux-sandbox/src/landlockbwrap
沙箱被拒判定codex-rs/sandboxing/src/denial.rsis_likely_sandbox_denied