跳转至

openai/codex:OpenAI 官方的命令行 AI 编程助手

用大白话先说清楚它是什么:Codex CLI 是 OpenAI 官方出的命令行 AI 编程助手。你在终端里敲一行话描述需求,它会去读你项目里的代码、生成或修改代码、跑测试、把结果反馈给你——跟你在终端里指挥一个刚入职的工程师干活的感觉类似。

说明:本文已对照 openai/codex 仓库(codex-rs/ Rust 实现)的实际源码做过逐点核对,文中标注了具体文件路径的代码片段均直接摘自源码,而非转述。codex-rs 是一个演进速度很快、体量已经远超"一个 CLI 工具"的仓库(core crate 下有数百个源文件,覆盖多 Agent 编排、插件、Skills、实时语音会话、云端任务等),本文只挑了会话循环、工具执行、审批与沙箱这几条最核心的链路做拆解,其余子系统不做展开,请以官方仓库当前状态为准。

最简单的一个场景:你在项目目录下敲下 codex "给这个函数加上单元测试" 回车之后,Codex 大致会做这几件事——读一遍相关源码文件、生成要修改/新增的代码或测试文件、执行 pytest/go test 之类的命令看结果对不对、如果测试没通过就再改一轮,直到成功或者需要向你确认某个有风险的操作(比如要不要覆盖写一个已有文件)。表面上看只是一行命令,但背后这个"读—改—跑—看结果"的循环,以及"什么时候该停下来问你一句"的判断,就是本文要拆的两块核心逻辑;再往下才是沙箱隔离这些更底层的安全机制。

说人话版本的核心设计:Codex 的运行时说到底就是一个循环——读你的指令 → 决定要不要用工具(读文件/写文件/跑命令)→ 实际执行 → 把结果显示给你,而在"要不要用工具"和"执行"之间,专门挂了一层负责判断"这个操作要不要先问你一下"的审批机制。这个循环具体是怎么用 Rust 代码实现的、审批机制里都有哪些具体的枚举和判断逻辑,是接下来两节要看的内容;再往下才是这套循环真正执行命令时,操作系统层面到底是怎么把命令"关"在沙箱里的。


1. 会话循环:一次对话是怎么"跑"起来的(SessionTurnContextSessionTask

前面用大白话说的"读指令→决定用不用工具→执行→显示结果"这个循环,说起来简单,但如果以为它就是一个"生成命令 → 执行 → 回填结果"的单线程脚本,那就想得太简单了。实际翻开 codex-rs/core/src/session/codex-rs/core/src/tasks/ 之后,会发现它是一套结构相当规整的状态机:一个长期存活的 Session 持有跨轮次的状态,每一轮对话对应一个 TurnContext,而具体做什么(常规回复、compact 压缩历史、review 走查)被抽象成实现了 SessionTask trait 的任务类型,由取消令牌(CancellationToken)统一管理生命周期。

TurnContextcodex-rs/core/src/session/turn_context.rs:115)是"这一轮该怎么跑"的快照,字段远比想象中丰富——不只是 model 和 cwd,审批策略、权限画像、网络代理、多 Agent 版本、个性化设定都挂在这里:

// codex-rs/core/src/session/turn_context.rs
pub struct TurnContext {
    pub(crate) sub_id: String,
    pub(crate) config: Arc<Config>,
    pub(crate) mode: ModeKind,
    pub(crate) approval_policy: Constrained<AskForApproval>,
    pub(crate) permission_profile: PermissionProfile,
    pub(crate) network: Option<NetworkProxy>,
    pub(crate) windows_sandbox_level: WindowsSandboxLevel,
    pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
    // ……
}

真正驱动"一轮对话怎么跑完"的是 RegularTaskcodex-rs/core/src/tasks/regular.rs),它实现 SessionTask::run,核心是一个 loop:调用一次 run_turn 拿到模型这一轮的最终消息,如果输入队列里还有排队的用户输入(比如用户在模型思考时又发了消息),就继续下一轮,直到队列清空为止:

// codex-rs/core/src/tasks/regular.rs
loop {
    let last_agent_message = run_turn(
        Arc::clone(&sess), Arc::clone(&ctx), Arc::clone(&turn_extension_data),
        next_input, prewarmed_client_session.take(), cancellation_token.child_token(),
    ).instrument(run_turn_span.clone()).await?;
    if !sess.input_queue.has_pending_input(&sess.active_turn).await {
        return Ok(last_agent_message);
    }
    next_input = Vec::new();
}

Session::newcodex-rs/core/src/session/session.rs:494)的构造函数签名本身就是一份"这个项目现在有多复杂"的说明书:一次性注入了 agent_control(多 Agent 编排)、environment_manager(多环境/工作区)、skills_serviceplugins_managermcp_managercode_mode_session_providerthread_storeattestation_provider 等十几个子系统。也就是说,今天的 Codex CLI 内核早已不是一个"调用一次模型、跑一次 shell"的小工具,而是一个多 Agent、多环境、可插件化的运行时——这也是本文只挑会话循环和工具执行这两条主链路讲的原因,铺开讲会偏离"沙箱与执行安全"这个主题。


2. 审批机制:什么时候要停下来问你一句

上一节 TurnContext 结构体里那个 approval_policy: Constrained<AskForApproval> 字段,就是这一节要展开的东西——它决定"这个操作要不要停下来问用户一句"的具体逻辑。为降低沙箱被绕过或误操作的风险,Codex 引入了声明式的执行策略与分级审批。审批模式 AskForApproval 定义在 codex-rs/protocol/src/protocol.rs:908,实际只有四种取值,比"从人工确认到完全自动"这种模糊描述精确得多:

// codex-rs/protocol/src/protocol.rs
pub enum AskForApproval {
    UnlessTrusted,        // 只有 is_safe_command() 认定为"只读安全"的命令才自动放行
    #[default]
    OnRequest,             // 默认值:由模型自己决定什么时候请求审批
    Granular(GranularApprovalConfig), // 按 sandbox_approval / rules / skill_approval 等细分开关
    Never,                 // 从不询问用户,失败直接返回给模型,不升级为用户审批
}

具体一条命令能不能自动放行,落在 codex-rs/core/src/safety.rsSafetyCheck 三态判定上(AutoApprove / AskUser / Reject),assess_patch_safety 会先看策略是否直接拒绝沙箱审批,再检查这次改动是否被约束在可写路径内,最终才决定是自动放行、转人工,还是直接拒绝。真正跨进程/跨会话传递"要不要批准"的载体是 codex-rs/core/src/tools/approvals.rs 里的 ApprovalAction 枚举(Shell / ExecCommand / ApplyPatch 三种请求,各自带上命令、cwd、SandboxPermissions)。

值得单独指出的一点是:OnRequest 模式下并不是每次都真的弹窗打断用户。codex-rs/core/src/guardian/ 模块实现了一套"Guardian 自动复核"——用一个继承父会话网络代理/白名单配置的独立评审会话,重建一段精简的上下文,让它对这次具体的操作给出结构化的允许/拒绝判断,超时或评审失败则按"fail closed"处理(直接拒绝)。也就是说,"要不要问用户"这件事本身也可能先经过一次自动化的模型评审,这是此前版本文档完全没有覆盖到的一层。

  • 默认无网络访问:沙箱默认不允许出站网络连接,如需访问外部 API,需要显式在配置中开放网络权限或走受信任的代理通道(NetworkProxy)。
  • 文件系统写入范围限制:默认只允许在用户明确批准的目录(通常是当前工作目录)内写入,越权路径写入会被 FileSystemSandboxPolicy 与平台沙箱共同拦截。

3. 更底层的安全边界:进程级沙箱隔离

审批机制解决的是"要不要问你",但就算某个操作被自动放行、或者用户已经点了同意,Codex 也不能完全信任大模型生成的代码会规规矩矩地跑——真正兜底的安全边界,要下沉到操作系统原生隔离机制这一层,而不是只停留在应用层的判断逻辑上。

大模型在生成代码(如 Python、Go 脚本或 Shell 命令)时,如果不经过隔离直接在本地宿主机上运行,无异于直接向大模型交出系统的执行权限。OpenAI 的 Codex CLI 在设计上采用了一套进程级沙箱隔离(非虚拟化容器)与代码执行验证机制。

需要特别澄清一处常见的误传:Codex 的沙箱并不是基于 gVisor/Firecracker 这类虚拟化容器,也没有公开资料显示它依赖 Celery/Redis 驱动的异步任务队列。根据 OpenAI 官方文档与社区对 openai/codex 仓库的分析,其沙箱是同步的、进程级的操作系统原生隔离,具体技术栈随平台而异: * macOS:使用 Apple 的 Seatbeltsandbox-exec),运行时根据所选权限模式动态生成 Sandbox Profile Language(SBPL)脚本。 * Linux:默认使用 Bubblewrap(bwrap) 通过命名空间(namespace)与挂载(mount)机制构建受限的文件系统视图,这是默认路径下承担文件系统访问控制的主力机制;Landlock 在源码注释中被明确标注为历史遗留/兜底工具(当 bwrap 不可用时的降级方案),并非默认组合的常规组成部分;此外还有 seccomp(系统调用过滤,除了在禁网模式下阻断 socket 等网络相关系统调用外,还无条件禁用 ptraceprocess_vm_readv/writevio_uring 等系统调用)。 * Windows:使用受限令牌(Restricted Token)与 ACL 机制;在 WSL2 下则复用 Linux 方案。

命令执行的整体流程是同步的:PermissionProfile 被翻译成平台相关的限制策略,沙箱包装进程直接派生子进程执行命令,结果同步返回给调用方——这与"提交任务到独立 Exec Server、由后台 Worker 异步拉起容器、再通过 Webhook 回调"的架构是两回事。

 ┌────────────────────────────────────────────────────────┐
 │            Codex CLI / 宿主运行时 (Host Runtime)        │
 └────────────────────────┬───────────────────────────────┘
                          │ 根据 PermissionProfile 派生沙箱子进程
                          v
+─────────────────────────┼──────────────────────────────────────────+
|  平台原生沙箱层(同步、进程级,非容器虚拟化)                        |
|                                                                    |
|   macOS: sandbox-exec + 动态生成的 SBPL Profile                    |
|   Linux: Bubblewrap(文件系统 Namespace,默认路径下的文件访问控制主力)|
|          + Landlock(bwrap 不可用时的历史/兜底方案,非默认组合常规项)|
|          + seccomp(除禁网模式下过滤 socket 外,还无条件禁用          |
|          ptrace / process_vm_readv 等系统调用)                     |
|   Windows: 受限令牌 (Restricted Token) + ACL                       |
|                                                                    |
|   核心约束:仅允许写入用户显式批准的目录;默认无出站网络访问            |
+────────────────────────────────────────────────────────────────────+

3.1 macOS:Seatbelt (sandbox-exec)

Codex 在 macOS 上通过系统自带的 Seatbelt 沙箱执行命令,运行时按当前的权限模式动态生成对应的 SBPL 沙箱描述文件,用来限制文件系统写入路径和网络访问范围。

3.2 Linux:Bubblewrap(+ Landlock 兜底)+ seccomp

Linux 上默认使用 Bubblewrap 构造受限的文件系统命名空间/挂载视图,这是默认组合下承担路径级文件访问控制的主力机制;根据源码注释,Landlock 被定位为 bwrap 不可用场景下的历史遗留/兜底工具,而不是与 bwrap 并列生效的常规组成部分。seccomp 除了在禁用网络的模式下过滤 socket 等相关系统调用、阻断出站网络连接外,还无条件禁用 ptraceprocess_vm_readv/writevio_uring 等系统调用,覆盖范围比单纯的网络过滤更广。


4. 命令执行与自动化验证流程:把循环、审批、沙箱串起来

前面分别看了会话循环怎么驱动一轮对话、审批机制怎么判断要不要问用户、沙箱怎么在操作系统层面拦住越权操作,这一节看这三者是怎么串起来的:一条命令从"决定要执行"到"结果返回给模型",具体走的是哪几步。

Codex 在执行大模型生成的命令或测试时,遵循一套同步、进程级的执行链路(而非独立的分布式 Exec Server + 消息队列架构)。这条链路在源码里落在 codex-rs/core/src/exec.rsprocess_exec_tool_call 上,签名和实现都比"内部逻辑"这种描述具体得多:

// codex-rs/core/src/exec.rs
pub async fn process_exec_tool_call(
    params: ExecParams,
    permission_profile: &PermissionProfile,
    sandbox_cwd: &AbsolutePathBuf,
    windows_sandbox_workspace_roots: &[AbsolutePathBuf],
    codex_linux_sandbox_exe: &Option<PathBuf>,
    use_legacy_landlock: bool,
    stdout_stream: Option<StdoutStream>,
) -> Result<ExecToolCallOutput> {
    let exec_req = build_exec_request(
        params, permission_profile, sandbox_cwd,
        windows_sandbox_workspace_roots, codex_linux_sandbox_exe, use_legacy_landlock,
    )?;
    // 统一走 sandboxing 模块的单一执行路径
    crate::sandboxing::execute_env(exec_req, stdout_stream).await
}

use_legacy_landlock: bool 直接出现在这个入口函数的参数列表里,这也是之前"Bubblewrap 默认、Landlock 是历史遗留兜底"这个结论的又一处直接证据——它不是两个并列生效的机制,而是一个布尔开关,决定这次执行走新路径还是老路径。整体流程可以概括为:

  1. 权限判定:根据 TurnContext 里的 approval_policyAskForApproval)与 permission_profile,判断该命令是否需要用户确认、是否允许写盘、是否允许联网。
  2. 沙箱内执行process_exec_tool_callExecParamsPermissionProfile 交给 sandboxing::execute_env,由平台相关的沙箱包装器(Seatbelt / Bubblewrap+seccomp / Windows 受限令牌)派生沙箱子进程运行命令。
  3. 同步返回结果:命令的 stdout/stderr/退出码同步返回给调用方(RegularTask 里的 run_turn),用于判断是否需要重试或调整下一步操作。
  4. 测试与验证:如果任务包含运行测试(如 pytestgo test)的步骤,这些命令同样在沙箱内以同样的同步方式执行,测试输出直接反馈给驱动 Codex 的模型用于判断是否修复成功。

是否存在独立的远程/托管执行服务(例如云端运行的 Codex 任务,仓库里确实有 cloud-taskscloud-tasks-client 等 crate),公开资料中有所提及,但其内部是否使用消息队列、Webhook 等异步机制,没有找到足够可靠的公开细节,这里不做具体断言。


5. 常见故障与排查

故障现象 底层诱因 系统级表现 预防与排查手段
命令挂起 / 执行超时 代码产生无限递归、死循环或长时间阻塞的操作,没有外部超时兜底。 沙箱内的子进程长时间不返回,Agent 状态机的当前回合被卡住。 1. 在调用侧对沙箱子进程设置超时并强制 kill。
2. 引导模型编写有明确终止条件的代码,避免生成潜在死循环。
越权路径写入被拦截 生成的代码尝试写入沙箱允许目录之外的路径(如 /etc、用户主目录下的敏感文件)。 Bubblewrap(Linux 默认路径,--ro-bind/--bind 挂载策略)或 Seatbelt(macOS)直接拒绝写操作,命令返回 Permission Denied;仅当显式开启 use_legacy_landlock 时才由 Landlock 承担这层拦截。 1. 明确沙箱工作目录边界,业务代码不依赖越权路径。
2. 引导模型将输出限定在项目目录内。
网络访问被拒绝 代码中包含需要向外获取 API 数据的操作,但当前会话处于禁网模式,被 seccomp 过滤的 socket 系统调用拦截。 抛出网络超时或 Permission Denied 的 Socket 错误。 1. 对确需联网的场景显式在配置中开放网络权限或走受信任的代理通道。
2. 推荐使用 MCP SDK 将外部能力作为受控的工具/资源引入,而非让代码直接发起任意 TCP 请求。

6. 资深系统架构师面试表达方案

面试提问:大模型生成的代码是高度不可信的,在设计自动执行大模型代码的系统时,你们是如何做安全防护与性能保障的?

回答模版: 大模型生成的代码不能直接在宿主机上跑,这是基本共识。我们的思路是参考 Codex 这类工具的做法:用操作系统原生的进程级沙箱,而不是自己搭一套容器虚拟化平台——后者运维成本更高,前者在单机场景下已经够用。

具体是两层: 第一,平台原生沙箱做隔离: macOS 上用 sandbox-exec(Seatbelt)动态生成沙箱描述文件;Linux 上默认用 Bubblewrap 做文件系统隔离(读写挂载点级别的命名空间隔离),配合 seccomp 过滤网络相关的系统调用——Landlock 只是 bwrap 不可用时的历史遗留兜底路径,需要显式开启才会启用,并非默认和 Bubblewrap 一起生效。核心约束很简单:只能写用户明确批准的目录,默认没有出站网络。这套东西比自己维护一套 gVisor/Firecracker 虚拟化方案要轻量很多,适合"单机跑一个不受信任的命令"这种场景。

第二,同步执行 + 外部超时兜底: 命令是同步派生子进程执行、同步拿结果返回给上层判断的,我们没有再引入一层独立的异步任务队列。测试运行(pytest/go test)也走同样的沙箱执行路径。为了防止死循环卡住整个流程,调用侧会加超时控制强制终止子进程。

再往上一层,审批策略我会做成显式的档位,而不是一个模糊的"信任等级"滑块——参考 Codex 的做法,直接定义成"只读安全命令自动放行 / 由模型自己申请审批 / 按具体操作类型细分放行规则 / 完全不问用户"这几档,落到代码里就是一个枚举而不是一堆散落的 if-else。同时我会考虑加一层自动化的预审:可疑但不算高危的操作,先丢给一个独立的、上下文更精简的评审会话做结构化判断,只有它也拿不准或者超时了,才真正打断用户——这样能把大部分"看起来危险其实安全"的请求挡在用户审批之前,同时保证一旦评审这一层出问题,默认行为是拒绝而不是放行。

这套方案在单机场景下已经能覆盖大部分风险,如果要支持多租户强隔离或需要限制 CPU/内存的场景,可能还是需要在此基础上叠加容器或虚拟化层,这块我会持续跟进官方仓库的实现变化。