主题
Codex CLI Sandbox 深度调研:设计思想与执行原理
视角:操作系统安全 + Agent 安全模型 + 上下文工程 范围:OpenAI Codex(CLI / TUI / IDE Extension)官方公开实现 + Rust 源码 + 社区分析 代码锚点:本地
trpc-codex/codex-rs/(早期 0.x 形态) + 上游openai/codex0.115+~v0.2.0-alpha.2时代的稳定形态 时间锚点:2026-04
0. TL;DR:一句话理解 Codex Sandbox
Codex 不沙盒它自己,它沙盒它给 LLM 调用的"那只手"——
shell/apply_patch子进程。设计核心是 "双轴模型 + 平台原生隔离 + 失败回退":
- 轴 1:SandboxPolicy——技术上能做什么(OS 强制的文件/网络边界);
- 轴 2:AskForApproval——什么时候要问你(流程上的人机闸口);
- 平台原生:macOS 用 Seatbelt /
sandbox-exec,Linux 用 Landlock + seccomp(新版叠加 bubblewrap),Windows 用 Restricted Token;- 失败回退:被沙盒拒绝时不是直接报错,而是 "升级问用户:要不要不带沙盒重跑?"——这条路径是 Codex 与 Claude Code 等竞品最显著的差异。
主进程本身不进沙盒:维护 LLM 会话、读 ~/.codex/、写日志、调用 OpenAI API。只有 LLM 自己生成的那条命令进沙盒。这是一个清醒的取舍——把"模型这个不可信源"严格关在 OS 级别的 jail 里,而不是把整个工具关在容器里。
1. 为什么需要 Sandbox:威胁模型与设计哲学
1.1 Codex 面对的三类威胁
| 威胁类别 | 具体场景 | Sandbox 的对策 |
|---|---|---|
| Prompt Injection | README/issue/网页里藏着"删除 ~/.ssh"指令,模型被诱导执行 | OS 级写保护:~/.ssh 在 writable_roots 之外,syscall 直接被拒 |
| 模型幻觉/失误 | 模型本意修补 bug,实际 rm -rf .git | .git/ 在 writable_roots 内但被强制 read-only 子路径剔除 |
| 第三方 MCP / 脚本污染 | Skill 脚本或 MCP 工具被篡改后偷偷外发数据 | 默认无网络(network_access=false),即便文件被改也跑不出去 |
1.2 设计哲学:把不可信源关进 OS 级 jail
OpenAI 在 Codex 文档中明确表态:
By default, the agent runs with network access turned off. Locally, Codex uses an OS-enforced sandbox that limits what it can touch (typically to the current workspace), plus an approval policy that controls when it must stop and ask you before acting.
三条原则可以提炼为:
- Default-deny:拒绝默认。macOS 的
seatbelt_readonly_policy.sbpl第一行就是(deny default),再选择性allow,这是 Chrome 沙盒同款思路。 - Least Privilege:最小权限。即便是
workspace-write也不是 "整个 workspace 全开",.git//.codex//.agents/这些"自指目录"被强制只读,避免模型篡改沙盒配置或 git hooks 自我提权。 - Fail-closed with Escalation:失败关闭、可升级。沙盒拒绝命令时不静默吞掉,而是上抛一个
ExecApprovalRequest,让用户决策"要不要脱掉沙盒重试"——既保留生产力,又把决策权显式交给人。
1.3 Codex 沙盒 vs 容器化方案
很多人会问"为什么不直接用 Docker 把 Codex 关起来?"答案藏在工程现实里:
| 维度 | 容器化(Docker / VM) | Codex 的进程级沙盒 |
|---|---|---|
| 启动开销 | 秒级 | 微秒级(fork + apply policy) |
| 粒度 | 整个 Codex 进程,包括 IDE 通信 | 仅 LLM 生成的子命令 |
| 用户体验 | 每个 shell 调用都跨容器边界 | 与本地开发体感一致(同一 cwd、同一 git state) |
| 逃逸风险 | 内核漏洞 + Docker socket | 直接落到 LSM/Seatbelt 内核机制 |
| 兼容性 | 需要 Docker daemon | 零依赖(系统自带) |
Codex 选了第二条路:进程级沙盒 + LLM 调用粒度。这意味着每次 LLM 调一次 shell,Codex 才 fork 出一个被 Seatbelt/Landlock 包裹的子进程,子进程结束沙盒立刻消亡。
2. 概念分层:Sandbox + Approval 双轴模型
理解 Codex 安全模型的关键是分清两个正交的概念:
2.1 二维矩阵:常见预设组合
来自 Codex 官方文档:
| 意图 | CLI flag | 等价于 | 适用场景 |
|---|---|---|---|
Auto / --full-auto | 无 flag 或 --full-auto | --sandbox workspace-write --ask-for-approval on-request | 日常开发(默认推荐) |
| 只读浏览 | --sandbox read-only --ask-for-approval on-request | — | 代码审查 / 计划讨论 |
| CI 只读 | --sandbox read-only -a never | — | 流水线自动化分析 |
| 写 + 慎执行 | --sandbox workspace-write -a untrusted | — | 边写边谨慎运行 |
| 🔥 YOLO 模式 | --dangerously-bypass-approvals-and-sandbox(别名 --yolo) | — | 极不推荐,仅用于一次性脚本 |
2.2 启动时智能选择
Codex 启动时会根据 cwd 状态做"动态默认":
- 是 git repo →
Auto(workspace-write + on-request) - 不是 git repo / 用户未明确 trust →
read-only,等待用户用/permissions显式信任
这条逻辑的潜台词是:git 仓库本身就是回滚保险——即便 LLM 把 workspace 写花了,git reset --hard 一键还原。没有 git 时,这条保险不在,所以默认更保守。
3. SandboxPolicy 的演进:从扁平枚举到结构化策略
3.1 早期版本(trpc-codex / 0.x 形态):4 项扁平枚举
来自本地代码 codex-rs/core/src/protocol.rs:
trpc-codex/codex-rs/core/src/protocol.rsL98–L108
rust
pub enum SandboxPolicy {
/// Network syscalls will be blocked
NetworkRestricted,
/// Filesystem writes will be restricted
FileWriteRestricted,
/// Network and filesystem writes will be restricted
#[default]
NetworkAndFileWriteRestricted,
/// No restrictions; full "unsandboxed" mode
DangerousNoRestrictions,
}这版的设计粗糙之处在于:
- 维度未解耦:网络限制和文件限制虽然是两个 bool,却被组合成了 4 个枚举值;
- 没有 writable_roots 概念:所有 Linux 沙盒下的写路径都是从
cwd + $HOME/.pyenv + tempdir硬编码出来的; - 没有 read-only 子路径保护:
.git/完全暴露在 writable_roots 中。
3.2 当前版本(v0.2.0-alpha+):结构化、组合化的策略
上游 openai/codex 主分支 的 SandboxPolicy 已演进为:
rust
pub enum SandboxPolicy {
ReadOnly {
access: FileSystemReadAccess,
network_access: NetworkAccess,
},
WorkspaceWrite {
writable_roots: Vec<PathBuf>,
read_only_access: FileSystemReadAccess,
network_access: NetworkAccess,
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
},
DangerFullAccess,
ExternalSandbox {
network_access: NetworkAccess,
},
}四个变体的语义跃迁:
| 变体 | 文件系统 | 网络 | 关键不变量 |
|---|---|---|---|
ReadOnly | 全文件系统只读 | 默认 off | 适合 CI/CR 类纯查阅 |
WorkspaceWrite | 仅 writable_roots 内可写,.git/ .codex/ .agents/ 强制只读 | 默认 off,可 opt-in | 默认值;workspace=cwd + /tmp + $TMPDIR |
DangerFullAccess | 直接 SandboxType::None,不进沙盒 | 不限 | --yolo 专用 |
ExternalSandbox | 由外层(如 cloud container)管 | 仅暴露 network_access | 用于 Codex Cloud 这类已经在容器里的环境 |
3.3 关键不变量:保护 .git/ .codex/ .agents/
这是个"看似小,实则关键"的细节。在 WorkspaceWrite 模式下,即使 cwd 整体可写,下列子路径仍被强制划为只读:
| 路径 | 为什么必须只读 |
|---|---|
**/.git/ | 防止 LLM 改 git hooks(如 pre-commit)实现持久化后门,或者改 config 偷偷加 core.editor |
**/.codex/ | 防止 LLM 改 sandbox/approval 配置自我提权(sandbox_mode = "danger-full-access" 的攻击) |
**/.agents/ | 防止 LLM 篡改 subagent 角色定义(这是 0.115+ 才加的保护) |
gitdir: 指向的真实目录 | 处理 .git 是 pointer 文件的场景 |
/.gitis protected as read-only whether it appears as a directory or file. If/.gitis a pointer file (gitdir: ...), the resolved Git directory path is also protected as read-only.
这是教科书级的"自指攻击防御"——LLM 不能修改控制它自己的那张配置。
3.4 决策入口:assess_command_safety
来自本地 codex-rs/core/src/safety.rs:
trpc-codex/codex-rs/core/src/safety.rsL65–L104
rust
pub fn assess_command_safety(
command: &[String],
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
approved: &HashSet<Vec<String>>,
) -> SafetyCheck {
let approve_without_sandbox = || SafetyCheck::AutoApprove {
sandbox_type: SandboxType::None,
};
// Previously approved or allow-listed commands
if is_known_safe_command(command) || approved.contains(command) {
return approve_without_sandbox();
}
// Command was not known-safe or allow-listed
match sandbox_policy {
SandboxPolicy::DangerousNoRestrictions => approve_without_sandbox(),
_ => match get_platform_sandbox() {
Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type },
None => {
match approval_policy {
AskForApproval::Never => SafetyCheck::Reject {
reason: "auto-rejected by user approval settings".to_string(),
},
_ => SafetyCheck::AskUser,
}
}
},
}
}可视化为决策树:
is_known_safe_command 是一个内置 allow-list(ls / cat / sed / pwd / head 等纯读命令),它们走 SandboxType::None 是为了避免不必要的沙盒开销——在 Codex 的世界里,"安全的命令直接放行"和"可疑的命令进沙盒"是两条不同的快速路径。
4. 平台实现详解
4.1 macOS:Seatbelt(sandbox-exec)
4.1.1 为什么是 Seatbelt
Seatbelt 是苹果自家用来给 Safari/Chrome 做沙盒的机制,它的优势:
- 零安装:
/usr/bin/sandbox-exec系统自带; - 声明式策略语言(SBPL,Scheme 方言):表达力够强,能玩 deny-default + 选择性 allow;
- 路径参数化:
(subpath (param "WRITABLE_ROOT_0"))这样的写法允许同一份策略文本配不同 cwd 复用。
但 Seatbelt 也有臭名昭著的"未文档化"特性,所以 Codex 直接抄了 Chrome 的策略骨架。
4.1.2 基础策略文本
来自本地 codex-rs/core/src/seatbelt_readonly_policy.sbpl:
trpc-codex/codex-rs/core/src/seatbelt_readonly_policy.sbplL1–L21
txt
(version 1)
; inspired by Chrome's sandbox policy:
; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd
; start with closed-by-default
(deny default)
; allow read-only file operations
(allow file-read*)
; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)
(allow signal (target self))
(allow file-write-data
(require-all
(path "/dev/null")
(vnode-type CHARACTER-DEVICE)))核心要素:
| 规则 | 含义 |
|---|---|
(deny default) | 默认拒绝一切 |
(allow file-read*) | 允许读全文件系统(这是 read-only mode 的本质) |
(allow process-exec) (allow process-fork) | 允许子进程,且子进程继承策略 |
(allow file-write-data ...) 仅 /dev/null | 写只允许 /dev/null |
(allow sysctl-read ...) 一长串 hw.* kern.* | Python multiprocessing / 进程信息读取必需 |
4.1.3 命令拼装
来自本地 codex-rs/core/src/exec.rs::create_seatbelt_command:
trpc-codex/codex-rs/core/src/exec.rsL163–L203
rust
pub fn create_seatbelt_command(
command: Vec<String>,
sandbox_policy: SandboxPolicy,
writable_roots: &[PathBuf],
) -> Vec<String> {
let (policies, cli_args): (Vec<String>, Vec<String>) = writable_roots
.iter()
.enumerate()
.map(|(index, root)| {
let param_name = format!("WRITABLE_ROOT_{index}");
let policy: String = format!("(subpath (param \"{param_name}\"))");
let cli_arg = format!("-D{param_name}={}", root.to_string_lossy());
(policy, cli_arg)
})
.unzip();
let full_policy = if policies.is_empty() {
MACOS_SEATBELT_READONLY_POLICY.to_string()
} else {
let scoped_write_policy = format!("(allow file-write*\n{}\n)", policies.join(" "));
format!("{MACOS_SEATBELT_READONLY_POLICY}\n{scoped_write_policy}")
};
let mut seatbelt_command: Vec<String> = vec![
MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string(),
"-p".to_string(),
full_policy.to_string(),
];
seatbelt_command.extend(cli_args);
seatbelt_command.push("--".to_string());
seatbelt_command.extend(command);
seatbelt_command
}最终拼出来形如:
bash
/usr/bin/sandbox-exec -p '(version 1)
(deny default)
(allow file-read*)
...
(allow file-write*
(subpath (param "WRITABLE_ROOT_0"))
)' \
-DWRITABLE_ROOT_0=/Users/me/projects/foo \
-- bash -c "echo blah > foo.txt"4.1.4 当前版本的增强(v0.115+)
在新版本中还做了几件事:
Read-only 子路径排除:
scheme(allow file-write* (require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) ; .git (require-not (subpath (param "WRITABLE_ROOT_0_RO_1"))) ; .codex ) )网络精细化:当
network_access=true时,加载seatbelt_network_policy.sbpl,开放mach-lookup给 DNS / SecurityServer / trustd / SystemConfiguration。代理路由模式:当配置了 network proxy,只允许出站到
localhost:<proxy_port>:scheme(allow network-outbound (remote ip "localhost:43128"))这种"必须穿过代理"的设计可以让企业版做流量审计。
硬编码路径防 PATH 注入:
rustconst MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";注释解释得很清楚:
to defend against an attacker trying to inject a malicious version on the PATH. If
/usr/bin/sandbox-exechas been tampered with, then the attacker already has root access.
4.1.5 已知坑:seatbelt + network_access 配置失效
参见 Issue #10390:在 dynamic_network_policy_for_network() 里,enforce_managed_network 的判定混入了 proxy 配置探测,导致用户显式 network_access = true 在 macOS 上被静默忽略。修复版的逻辑:
| 场景 | 修复前 | 修复后 |
|---|---|---|
| 有可用 proxy | 走 proxy | 走 proxy(不变) |
无 proxy + network_access=true | 静默空策略 → 全屏蔽 | 全网放开 + tracing::warn! |
无 proxy + network_access=false | 屏蔽(fail-closed) | 屏蔽(不变) |
启示:沙盒策略一定要 fail-loud,静默拒绝是用户体验灾难。
4.2 Linux:Landlock + seccomp(新版叠加 bubblewrap)
4.2.1 早期形态:Landlock + seccomp(同进程内安装规则)
本地 codex-rs/core/src/linux.rs 展示了最早期的实现:在子线程里直接调 Landlock + seccomp,再 exec() 命令——这是"进程级"沙盒。
关键三步:
(a) Landlock 限制文件系统
trpc-codex/codex-rs/core/src/linux.rsL81–L107
rust
pub fn install_filesystem_landlock_rules_on_current_thread(
writable_roots: Vec<PathBuf>,
) -> Result<()> {
let abi = ABI::V5;
let access_rw = AccessFs::from_all(abi);
let access_ro = AccessFs::from_read(abi);
let mut ruleset = Ruleset::default()
.set_compatibility(CompatLevel::BestEffort)
.handle_access(access_rw)?
.create()?
.add_rules(landlock::path_beneath_rules(&["/"], access_ro))?
.add_rules(landlock::path_beneath_rules(&["/dev/null"], access_rw))?
.set_no_new_privs(true);
if !writable_roots.is_empty() {
ruleset = ruleset.add_rules(landlock::path_beneath_rules(&writable_roots, access_rw))?;
}
let status = ruleset.restrict_self()?;
if status.ruleset == landlock::RulesetStatus::NotEnforced {
return Err(CodexErr::Sandbox(SandboxErr::LandlockRestrict));
}
Ok(())
}逻辑:
- 默认
/全只读; /dev/null可写;writable_roots可写;set_no_new_privs(true)禁止 setuid/file capabilities 提权;restrict_self()应用规则到当前线程。
Landlock 是单调的:规则一旦应用,只能更严,不能更松。这意味着即便 LLM 调起一个被沙盒包裹的 shell,在 shell 里再 fork 子进程,子进程依然继承沙盒——不存在子进程逃逸。
(b) seccomp 屏蔽网络 syscall
trpc-codex/codex-rs/core/src/linux.rsL111–L167
rust
pub fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(), SandboxErr> {
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
let mut deny_syscall = |nr: i64| { rules.insert(nr, vec![]); };
deny_syscall(libc::SYS_connect);
deny_syscall(libc::SYS_accept);
deny_syscall(libc::SYS_accept4);
deny_syscall(libc::SYS_bind);
deny_syscall(libc::SYS_listen);
// ... 一长串 sendto / recvfrom / sendmsg / recvmsg / setsockopt / ptrace ...
// 仅允许 socket(AF_UNIX, ...)
let unix_only_rule = SeccompRule::new(vec![SeccompCondition::new(
0, SeccompCmpArgLen::Dword, SeccompCmpOp::Eq, libc::AF_UNIX as u64,
)?])?;
rules.insert(libc::SYS_socket, vec![unix_only_rule]);
let filter = SeccompFilter::new(
rules,
SeccompAction::Allow, // 默认放行
SeccompAction::Errno(libc::EPERM as u32), // 命中规则返回 EPERM
...
)?;
let prog: BpfProgram = filter.try_into()?;
apply_filter(&prog)?;
Ok(())
}要点:
| 点 | 解释 |
|---|---|
connect/accept/bind/listen 全部 deny | 切断 TCP/UDP socket |
socket(AF_UNIX, ...) 单独允许 | 保留 IPC 能力(DBus、X11 socket) |
ptrace deny | 防止子进程 attach 主进程 dump 内存 |
默认 Allow | 白名单太长,所以反过来用黑名单 |
(c) 测试用例佐证
源码里测试了一堆攻击:
rust
sandbox_blocks_curl // curl http://...
sandbox_blocks_wget // wget ...
sandbox_blocks_ping // ICMP raw socket
sandbox_blocks_nc // nc -z 127.0.0.1 80
sandbox_blocks_ssh // BatchMode 防止挂起
sandbox_blocks_getent // DNS 查询
sandbox_blocks_dev_tcp_redirection // bash 的 /dev/tcp 后门这些是 LLM 越狱时的常见姿势——封到这个程度算彻底。
4.2.2 当前形态:bubblewrap + Landlock + seccomp(外部沙盒进程)
官方文档说明:
Linux uses
bwrapplusseccompby default. WSL1 was supported through Codex0.114; starting in0.115, the Linux sandbox moved tobwrap.
这次架构升级把沙盒从"同进程线程上的 LSM"变成了"独立的 helper 进程":
为什么换成 bubblewrap?
| 老方案(同进程 LSM) | 新方案(bwrap helper) |
|---|---|
| 沙盒规则跑在 Codex 主进程的子线程 | 独立 helper 进程,主进程零污染 |
不能玩 mount namespace(/proc /sys 都能看到) | bwrap 可以 bind mount + tmpfs 隔离 |
| 子进程能看见主进程文件描述符 | helper 起新 namespace,FD 隔离 |
| 配置需要重新编译 | helper 接受 JSON CLI 参数,独立可调试 |
新版的 helper 调用形式(来自 上游源码):
bash
codex-linux-sandbox \
--sandbox-policy-cwd /home/user/proj \
--command-cwd /home/user/proj \
--sandbox-policy '{"WorkspaceWrite":{"writable_roots":[...],...}}' \
--file-system-sandbox-policy '{"kind":"Workspace",...}' \
--network-sandbox-policy '{"Restricted":...}' \
--use-legacy-landlock \
-- npm test把策略以 JSON 形式塞进 helper,是为了支持滚动升级——主进程可以是新协议,helper 可以是老协议,只要 JSON 字段兼容。
4.2.3 已知坑:sandbox_permissions 读权限未实施
参见 Issue #11316:当前 Landlock 实施分支只做了"写限制",读限制是 TODO——无论怎么配置,沙盒里都能读 /etc/passwd。这在多租户 / agent-of-agent 场景里会成为读侧信道。计划是后续加 readable_roots 字段。
4.3 Windows:Restricted Token + WSL2
| 子场景 | 沙盒方案 |
|---|---|
| WSL2 (Linux) | 复用 Linux 的 bwrap + Landlock + seccomp |
| WSL1 | 在 0.114 之前用 Linux 路径,0.115 起不再支持(因为 WSL1 不支持 bwrap) |
| Native Windows | codex-windows-sandbox crate,使用 Restricted Token(类似 Chromium 的 SetTokenInformation) |
Restricted Token 的本质是用 Windows API 创建一个权限被剥夺的访问令牌,启动子进程时让它继承这个令牌。这条路径文档相对少,工程上仍在演进。
5. 执行流程:从 LLM 工具调用到沙箱化执行
把所有片段串起来看一次完整时序:
代码层的核心调度在 codex-rs/core/src/codex.rs::handle_function_call:
trpc-codex/codex-rs/core/src/codex.rsL802–L928
rust
async fn handle_function_call(
sess: &Session,
sub_id: String,
name: String,
arguments: String,
call_id: String,
) -> ResponseInputItem {
match name.as_str() {
"container.exec" | "shell" => {
let params = serde_json::from_str::<ExecParams>(&arguments)?;
// 路径 1: apply_patch
match maybe_parse_apply_patch_verified(¶ms.command) {
MaybeApplyPatchVerified::Body(changes) => {
return apply_patch(sess, sub_id, call_id, changes).await;
}
...
}
// 路径 2: shell 命令
let safety = assess_command_safety(
¶ms.command,
sess.approval_policy,
sess.sandbox_policy,
&state.approved_commands,
);
let sandbox_type = match safety {
SafetyCheck::AutoApprove { sandbox_type } => sandbox_type,
SafetyCheck::AskUser => {
let rx_approve = sess.request_command_approval(...).await;
match rx_approve.await {
ReviewDecision::Approved => (),
ReviewDecision::ApprovedForSession => {
sess.add_approved_command(params.command.clone());
}
Denied | Abort => return reject_response(),
}
SandboxType::None // 用户已批,无需沙盒
}
SafetyCheck::Reject { reason } => return reject_response(reason),
};
let output = process_exec_tool_call(
params, sandbox_type,
&writable_roots, sess.ctrl_c.clone(),
sess.sandbox_policy,
).await;
...
}
_ => unsupported_call_response(),
}
}5.1 Resource Capping:output 长度限制
exec.rs 里的 read_capped 限制每路 stdout/stderr 的输出:
| 上限 | 值 | 设计意图 |
|---|---|---|
MAX_STREAM_OUTPUT | 10 KiB | 防止模型上下文被命令输出灌爆 |
MAX_STREAM_OUTPUT_LINES | 256 行 | 行级截断,避免一行 1MB 的 dump |
DEFAULT_TIMEOUT_MS | 10s | 默认超时,模型可在 ExecParams 里覆盖 |
超出限制后,继续读但丢弃——这避免了 child 因 pipe 反压挂死。
5.2 SIGKILL / Timeout / Ctrl+C 三态
rust
let exit_status = tokio::select! {
result = tokio::time::timeout(timeout, child.wait()) => {
match result {
Ok(Ok(s)) => s,
Err(_) => {
child.start_kill()?;
synthetic_exit_status(128 + TIMEOUT_CODE) // 64
}
}
}
_ = ctrl_c.notified() => {
child.start_kill()?;
synthetic_exit_status(128 + SIGKILL_CODE) // 9
}
};synthetic_exit_status 把超时/中断映射为合成的 ExitStatus,让上层用同一套 match raw_output.exit_status.signal() 处理。
6. Escalation 机制:沙盒失败 → 询问 → 不带沙盒重跑
这是 Codex 区别于 Claude Code 等竞品的核心差异化设计。
6.1 触发条件
process_exec_tool_call 里这段判定:
trpc-codex/codex-rs/core/src/exec.rsL140–L147
rust
// NOTE(ragona): This is much less restrictive than the previous check. If we exec
// a command, and it returns anything other than success, we assume that it may have
// been a sandboxing error and allow the user to retry. (The user of course may choose
// not to retry, or in a non-interactive mode, would automatically reject the approval.)
if exit_code != 0 && sandbox_type != SandboxType::None {
return Err(CodexErr::Sandbox(SandboxErr::Denied(
exit_code, stdout, stderr,
)));
}这个设计有意放宽——不去精确识别"这次失败是不是沙盒导致的",因为:
- Seatbelt/Landlock 拒绝后,进程通常返回 EPERM,但 EPERM 在用户代码里也可能是别的原因;
- 精确识别 = 维护一个"什么 exit_code 对应沙盒"的脆弱白名单;
- 倒不如统一处理:只要带沙盒跑挂了,就给用户一个"不带沙盒重试"的选择。
代价是:偶尔会问用户"这是 npm test 真的失败了,还是沙盒拒了?"——但这是可接受的代价。
6.2 Escalation 决策状态机
代码体现:
trpc-codex/codex-rs/core/src/codex.rsL986–L1019
rust
match rx_approve.await.unwrap_or_default() {
ReviewDecision::Approved | ReviewDecision::ApprovedForSession => {
sess.add_approved_command(params.command.clone());
sess.notify_background_event(&sub_id, "retrying command without sandbox").await;
let retry_call_id = format!("{call_id}-retry");
sess.notify_exec_command_begin(...).await;
// 关键:SandboxType::None
let retry_output_result = process_exec_tool_call(
params.clone(),
SandboxType::None,
&retry_roots,
sess.ctrl_c.clone(),
sess.sandbox_policy,
).await;
...
}
ReviewDecision::Denied | ReviewDecision::Abort => {
return reject_response("exec command rejected by user");
}
}6.3 设计反思
这个机制很聪明,但也有隐含 bug 嫌疑——代码注释自己都嘀咕:
rust// TODO(ragona): Isn't this a bug? It always saves the command in an | fork? sess.add_approved_command(params.command.clone());
意思是:用户选 "Approved"(仅本次)和 "ApprovedForSession"(永久)走的是同一分支,都被加入了 approved_commands HashSet。这看起来违反了"一次"的语义。新版应已修复。
7. 关键安全细节
7.1 主进程加固(Process Hardening)
codex-rs/process-hardening crate 用 #[ctor::ctor] 在 main() 之前跑:
| 平台 | 加固项 | 防御什么 |
|---|---|---|
| macOS | ptrace(PT_DENY_ATTACH) | 防 LLDB attach 内存读取 |
| macOS | setrlimit(RLIMIT_CORE, 0) | 禁 core dump,避免 token 落盘 |
| macOS | 删除所有 DYLD_* 环境变量 | 防 dylib 注入 |
| Linux | prctl(PR_SET_DUMPABLE, 0) | 进程 non-dumpable,防 ptrace |
| Linux | setrlimit(RLIMIT_CORE, 0) | 同上 |
| Linux | 删除所有 LD_* 环境变量 | 防 LD_PRELOAD 注入 |
注意:这是给 Codex 自己用的,不是给沙盒里的子进程。意图是防止"窃取主进程内存里的 OpenAI token / OAuth refresh_token"。
7.2 子进程的环境变量信号
rust
pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX";
pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED";子进程通过环境变量知道自己在沙盒里:
| 变量 | 取值 | 含义 |
|---|---|---|
CODEX_SANDBOX | "seatbelt" / "landlock" / "bwrap" | 当前沙盒类型 |
CODEX_SANDBOX_NETWORK_DISABLED | "1" / 不存在 | 网络是否被屏蔽 |
应用:脚本可以 if [ -n "$CODEX_SANDBOX" ]; then ... 做条件分支(比如跳过需要联网的测试)。
7.3 .env 安全:禁止 CODEX_* 前缀
~/.codex/.env 启动时被 dotenvy 加载,但有过滤:
rust
const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";防止用户(或被 LLM 诱导写入 .env)通过环境变量绕过沙盒——比如设置 CODEX_SANDBOX=disabled 是不行的,因为 .env 加载时这种变量会被丢弃。
7.4 Auth 文件权限
auth.json 写入时强制 0o600:
rust
#[cfg(unix)]
{
options.mode(0o600);
}凭据落盘是最常见的"侧信道"。0o600 + macOS Keychain 双保险。
7.5 Allow-list:is_known_safe_command
不进沙盒的"白名单"包括(来自 is_safe_command.rs):
| 命令族 | 例子 |
|---|---|
| 文件浏览 | ls, pwd, cat, head, tail, wc, file |
| 文本搜索 | grep, rg, find (limited args) |
| 元信息 | stat, du, df, which |
| Git 只读 | git status, git log, git diff (受 execpolicy 校验) |
这些命令哪怕在 read-only 模式也走 SandboxType::None,性能更优。但任何带 shell metacharacter 的版本(ls && rm -rf /)都不会被识别——is_known_safe_command 是 argv 级别的精确匹配。
7.6 execpolicy crate:参数级安全策略
codex-rs/execpolicy 是个独立 crate,提供"特定命令的特定参数模式"安全判定。比如:
sed -i 's/foo/bar/' file.txt是写操作,不安全;sed 's/foo/bar/' file.txt是读操作,安全。
这种参数级判定让 allow-list 不至于过粗("sed 不行")也不过细("任意 sed 都行")。
8. 与 Claude Code 等竞品的对比
| 维度 | Codex CLI | Claude Code | Aider |
|---|---|---|---|
| 沙盒粒度 | LLM 工具调用粒度 | LLM 工具调用粒度 | 无(依赖用户审批) |
| OS 级隔离 | ✅ Seatbelt / Landlock+seccomp / bwrap | ⚠️ Bash tool 内有 timeout,但无 LSM | ❌ |
| 写边界 | writable_roots + 自动 .git/ 保护 | 类似(permissions.toml) | ❌ |
| 网络默认 | 关闭 | 关闭 | 取决于 shell tool |
| 网络代理路由 | ✅ network-proxy MITM | ❌ | ❌ |
| 沙盒失败 escalation | ✅ 询问用户重试 | ❌ 直接报错 | N/A |
| 进程加固 | ✅ ptrace deny + 核心 dump 禁用 + DYLD/LD 清洗 | ❓ 未公开 | ❌ |
| 跨平台 | macOS/Linux/Windows/WSL2 | macOS/Linux/Windows | 全平台(无沙盒) |
| Cloud 模式 | ✅ Codex Cloud(双相执行) | ⚠️ Claude Computer Use | ❌ |
Codex 的护城河主要在两点:
- OS 级原生沙盒——不是脚本层
confirm弹窗,而是真正落到内核 LSM; - 失败可恢复——沙盒出错不是 dead end,而是 escalation。
9. 关键代码地图
按读源码的优先级排序(基于本地 trpc-codex/codex-rs/):
| 文件 | 作用 | 阅读优先级 |
|---|---|---|
core/src/protocol.rs | SandboxPolicy AskForApproval Op EventMsg 枚举定义 | ⭐⭐⭐ |
core/src/safety.rs | assess_command_safety assess_patch_safety get_platform_sandbox | ⭐⭐⭐ |
core/src/exec.rs | process_exec_tool_call create_seatbelt_command read_capped | ⭐⭐⭐ |
core/src/linux.rs | install_filesystem_landlock_rules_on_current_thread, install_network_seccomp_filter_on_current_thread | ⭐⭐⭐ |
core/src/seatbelt_readonly_policy.sbpl | macOS Seatbelt 基础策略 | ⭐⭐ |
core/src/codex.rs | handle_function_call 主调度 + escalation 流程 | ⭐⭐⭐ |
cli/src/seatbelt.rs | codex debug seatbelt 子命令实现 | ⭐ |
cli/src/landlock.rs | codex debug landlock 子命令实现 | ⭐ |
core/src/is_safe_command.rs | allow-list 实现 | ⭐⭐ |
execpolicy/ | 参数级命令安全策略 | ⭐⭐ |
core/src/approval_mode_cli_arg.rs | CLI 参数 ↔ Policy 枚举映射 | ⭐ |
新版上游还需关注:
core/src/sandboxing/mod.rs:SandboxManager,sandbox_policy_with_additional_permissionscore/src/tools/sandboxing.rs:SandboxableToolRuntimeSandboxAttempttraitslinux-sandbox/:独立的codex-linux-sandboxhelper cratenetwork-proxy/:基于rama的 MITM 代理
10. 演进时间线
| 版本 | 关键变化 |
|---|---|
| 0.x(trpc-codex 形态) | SandboxPolicy 是扁平的 4 项枚举;Linux 沙盒在主进程子线程内 apply;macOS 仅有 readonly 策略,writable_roots 用 -D 参数注入 |
| 0.114 | WSL1 最后支持版本 |
| 0.115 | Linux 沙盒迁移到 bubblewrap helper 进程;SandboxPolicy 演进为结构化枚举(ReadOnly / WorkspaceWrite / DangerFullAccess / ExternalSandbox);.git/ .codex/ .agents/ 强制只读子路径生效 |
| 0.117+ | 引入 Subagent 体系,每个子 agent 可独立配置 sandbox_mode;引入 granular approval policy 细分 5 类批准 |
| 0.120+ | 引入 auto_review 自动审批 reviewer agent;macOS 网络代理路由稳定 |
| v0.2.0-alpha | 新增 network-proxy 基于 rama 的 MITM;引入命名 permission profiles 支持 glob 级 read deny;引入 cloud 双相运行时(setup phase + agent phase) |
预测的下一步:
- Landlock 读限制会真正实施(#11316);
- macOS Seatbelt 配置 source-of-truth 统一(#10390 修复延伸);
- Windows 沙盒会从 Restricted Token 演进到 Win32 App Container。
11. 实践建议
11.1 默认就用 --full-auto,不要 --yolo
workspace-write + on-request 已经够日常用:
- 沙盒挡住
~/.ssh等敏感路径; - 命令失败有 escalation,不至于卡死;
- 网络默认关,避免无意识的 prompt injection 外发数据。
--yolo 仅用于一次性、无重要数据的临时容器。
11.2 在 CI 上使用 read-only --ask-for-approval never
bash
codex --sandbox read-only -a never -- "review this PR"非交互环境下,这是唯一安全的组合:模型只能看不能改,且不会卡在弹窗等待批准。
11.3 给 workspace-write 显式 opt-in 网络
toml
# ~/.codex/config.toml
[sandbox_workspace_write]
network_access = true并配合 web_search = "cached" 默认值——命中缓存的搜索结果,避开实时页面带来的 prompt injection 风险。
11.4 用 permission profile 精细化拒读敏感文件
toml
default_permissions = "workspace"
[permissions.workspace.filesystem]
":project_roots" = { "." = "write", "**/*.env" = "none" }
glob_scan_max_depth = 3即便整个 workspace 可读写,*.env 也对 LLM 不可见。
11.5 长跑任务用 Subagent 隔离
Subagent 可以独立配置 sandbox_mode。比如让"危险测试 runner"跑在 read-only 子 agent 里,主 agent 只接收摘要,避免子任务污染主 agent context 的同时,把脏活的能耗也限制掉。
11.6 调试沙盒:codex debug 子命令
bash
# macOS
codex debug seatbelt -s read-only -- ls /
# Linux
codex debug landlock -s network-and-file-write-restricted -- curl example.com
# 应该看到连接被 EPERM 拒绝这两个子命令直接调用沙盒入口,方便排查策略问题——本地代码 cli/src/seatbelt.rs cli/src/landlock.rs 提供了它们的实现。
11.7 验证沙盒是否生效
用副作用检测:
bash
# 应该被拒
codex --sandbox workspace-write -a never -c sandbox_workspace_write.network_access=false \
"请运行 curl https://example.com 然后告诉我结果"
# 看 CODEX_SANDBOX 环境变量是否被注入
codex "请运行 env | grep CODEX_SANDBOX"12. 一个表格收尾:什么时候选什么模式
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| 本地写代码(默认) | --full-auto | 边界合理 + escalation 兜底 |
| 审 PR / 读源码 | --sandbox read-only -a on-request | 防止意外修改 |
| CI 自动化 | --sandbox read-only -a never | 不可交互 |
| 跑数据分析脚本 | --sandbox workspace-write + network_access=true | 需要装包/拉数据 |
| 试一次性的危险脚本 | 在容器里 --yolo | 物理隔离即风险隔离 |
| 多 agent 并行 | 主 agent workspace-write,子 agent read-only | 限制脏活影响半径 |
| 企业部署 | MDM 推送 requirements.toml 强制 workspace-write 上限 | 集中管控不可绕过 |
| Codex Cloud | 默认 ExternalSandbox,setup 阶段联网拉依赖,agent 阶段离线 | 双相隔离 |
参考资料
官方
- Codex 文档:Agent approvals & security
- Codex CLI 文档:Sandboxing 概念
- Codex GitHub 仓库:openai/codex
- Codex 默认 reviewer policy
源码(上游主分支锚点 d807d44a)
codex-rs/core/src/sandboxing/mod.rscodex-rs/core/src/tools/sandboxing.rscodex-rs/core/src/landlock.rs
第三方分析
- Agent Safehouse: OpenAI Codex CLI Sandbox Analysis Report(2026-02 commit
26d9bddc) - Issue #10390: macOS network_access 静默失效
- Issue #11316: sandbox_permissions 读限制未实施
- Issue #18337: Linux 沙盒新旧策略不匹配
- Issue #8714: 项目级 config 被忽略
相关
- Chrome 沙盒策略:
sandbox/policy/mac/common.sb - Linux Landlock 文档:kernel.org/doc/html/latest/userspace-api/landlock.html
- bubblewrap:github.com/containers/bubblewrap