Skip to content

Codex 项目概览

说明 Codex 的本地运行方式、产品入口、Thread/Turn 模型、安全边界与 SDK 定位。

基于rust-v0.150.0
CodexCoding AgentArchitecture

Codex 项目概览 ​

Codex CLI 在本仓库 README 中被直接定义为:运行在用户计算机上的 OpenAI coding agent。这里的关键词不是“聊天机器人”,也不是“云端开发平台”,而是 本地运行时、模型驱动决策、面向软件工程任务、能够调用受策略约束的工具。

不过,“本地”很容易产生两个错误理解:

  1. 它不表示模型推理必然在本机完成。默认形态仍会通过模型 provider 发起请求;源码也 提供了本地 OSS provider 接入点。
  2. 它不表示所有命令天然处于同一种强沙箱中。审批策略与 SandboxPolicy 是两个独立的 决策维度,甚至存在明确命名为 danger-full-access 的策略。

Codex 会在本地组织任务和执行工具,但可以通过网络请求模型;模型可以提出工具调用, 但本地运行时仍负责审批、沙箱和实际执行。下文从运行时、会话、安全和 SDK 四个角度 说明这些职责如何配合。

本文是 Codex 源码系列的起点,不要求前置知识。它只建立产品入口、Thread/Turn、本地执行、安全与 SDK 五个基本坐标;具体 Session 构造、工具调度、平台沙箱和协议字段由后续专题展开,不能把概览图 当作完整调用链实现。

1. Codex 定义 ​

1.1 Agent Runtime ​

下面的系统上下文图展示了仓库内运行时与外部系统的边界。蓝色入口、深蓝核心、青色模型 provider、红色本地执行和橙色扩展分别对应 colors.toml 的语义色板。

这张图表达三个边界:

  • 控制边界在本地:入口、会话核心、工具执行和默认持久化由本地进程组织。
  • 推理边界可跨网络:模型 provider 是独立依赖;本地运行时不等于本地推理。
  • 副作用边界受策略控制:模型提出工具调用,但本地 runtime 决定审批、沙箱和执行。

1.2 运行时入口 ​

codex 不带子命令时进入交互式 TUI;同一顶层程序还分派非交互执行、代码审查、MCP、 App Server、沙箱、会话恢复和其他管理命令。

源码位置:codex-rs/cli/src/main.rs :: MultitoolCli

rust
// 顶层参数同时承载多工具子命令与无子命令时的 TUI 入口。

/// Codex CLI 顶层参数。
///
/// 没有子命令时,参数转交给交互式 TUI。
#[derive(Debug, Parser)]
#[clap(
    author,
    version,
    // 子命令存在时,不再要求默认交互参数必须满足。
    subcommand_negates_reqs = true,
    // 无论平台产物文件名是什么,帮助文本统一显示 `codex`。
    bin_name = "codex",
    override_usage = "codex [OPTIONS] [PROMPT]\n       codex [OPTIONS] <COMMAND> [ARGS]"
)]
struct MultitoolCli {
    /// 来自 CLI 的配置覆盖层。
    #[clap(flatten)]
    pub config_overrides: CliConfigOverrides,

    /// Feature flag 的显式启停。
    #[clap(flatten)]
    pub feature_toggles: FeatureToggles,

    /// 交互式远程连接参数。
    #[clap(flatten)]
    remote: InteractiveRemoteOptions,

    /// 默认交互入口的 TUI 参数。
    #[clap(flatten)]
    interactive: TuiCli,

    /// 只有显式指定时才进入其他产品形态。
    #[clap(subcommand)]
    subcommand: Option<Subcommand>,
}

该结构说明 codex 是一个 multitool entry point。Subcommand 在同一文件中进一步声明 了 Exec、Review、McpServer、AppServer、Sandbox、Resume、Fork、Cloud 等分支。它们不是互不相关的脚本,而是共享配置、认证、协议与 core runtime 的不同入口。

下面的图只画源码仓库可验证的入口关系,不推断 IDE 或桌面应用的内部实现。

1.3 Thread 和 Turn ​

入口的差异最终收敛到 thread/session/turn 运行时。SessionSource 直接记录一个会话来自 CLI、VS Code、Exec、MCP、自定义调用方、内部任务还是 sub-agent。

源码位置:codex-rs/protocol/src/protocol.rs :: SessionSource

rust
// SessionSource 是产品入口、限制策略与遥测归属共同使用的身份轴。

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "lowercase")]
#[ts(rename_all = "lowercase")]
pub enum SessionSource {
    Cli,                              // Codex CLI/TUI 会话
    #[default]
    VSCode,                           // App Server 默认的 IDE 来源
    Exec,                             // 非交互 codex exec
    Mcp,                              // MCP server 驱动的会话
    Custom(String),                   // 自定义嵌入调用方
    Internal(InternalSessionSource),  // 内部维护或后台任务
    SubAgent(SubAgentSource),         // 父 Agent 创建的子 Agent
    #[serde(other)]
    Unknown,                          // 前向兼容未知来源
}

SessionSource 证明“同一个 core 被多入口复用”,但它不证明这些入口拥有完全相同的功能。 例如不同来源可以派生产品限制、元数据和默认配置;这些差异将在 CLI、App Server、TUI 与 协议系列中分别核对。

ThreadManager 的公开职责也非常克制:创建 thread,并在内存中维护它们。创建参数则把 配置、历史、来源、动态工具、trace、环境和客户端 MCP 扩展集中到一个边界对象中。

源码位置:codex-rs/core/src/thread_manager.rs :: ThreadManager, StartThreadOptions

rust
// manager 持有进程级共享 State,options 只描述一次 Thread 创建输入。

/// 创建 Codex thread,并维护当前进程内的 thread。
pub struct ThreadManager {
    state: Arc<ThreadManagerState>,
    // 仅测试环境持有临时 CODEX_HOME 的生命周期 guard。
    _test_codex_home_guard: Option<TempCodexHomeGuard>,
}

/// 创建 thread 所需的输入边界。
pub struct StartThreadOptions {
    pub config: Config, // 已解析的运行配置
    pub allow_provider_model_fallback: bool, // 是否允许 provider/model 回退
    pub initial_history: InitialHistory, // 新建或恢复的初始历史
    pub history_mode: Option<ThreadHistoryMode>, // 历史保留/投影模式
    pub session_source: Option<SessionSource>, // 产品入口来源
    pub thread_source: Option<ThreadSource>, // 用户、子代理或内部 feature
    pub dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
    pub metrics_service_name: Option<String>, // 指标服务归属
    pub parent_trace: Option<W3cTraceContext>, // 跨边界 trace 上下文
    pub environments: Option<Vec<TurnEnvironmentSelection>>, // 可选执行环境
    pub thread_extension_init: ExtensionDataInit, // thread 扩展初始化数据
    pub client_mcp_extensions: ClientMcpExtensions, // 客户端提供的 MCP 扩展
    pub reserved_thread_id: Option<ThreadId>, // 启动前预留的 thread ID
}

impl StartThreadOptions {
    /// 以新会话、无动态扩展和无显式来源构造最小选项。
    pub fn new(config: Config) -> Self {
        Self {
            config,
            allow_provider_model_fallback: false,
            initial_history: InitialHistory::New,
            history_mode: None,
            session_source: None,
            thread_source: None,
            dynamic_tools: Vec::new(),
            metrics_service_name: None,
            parent_trace: None,
            environments: None,
            thread_extension_init: ExtensionDataInit::default(),
            client_mcp_extensions: ClientMcpExtensions::default(),
        }
    }
}

ThreadManager 不直接等于一个 thread,也不等于操作系统线程。这里的 Thread 是可创建、 恢复、分叉和持久化的对话执行单元;Turn 是 thread 中一次由输入驱动、可能包含多个模型 step 和工具调用的工作周期。

1.4 任务控制流 ​

下图抽象了最小主链。它刻意把“模型提出调用”和“本地决定是否执行”画成两个步骤,避免 把模型响应误写成系统已经产生副作用。

这个闭环是项目定位的核心:Codex 不是只生成一段建议文本,而是维护会话、提供工具规格、 接收模型的工具调用、在本地策略边界内执行,再把结果回灌给模型。

2. 本地运行的含义 ​

2.1 Runtime 控制面 ​

能力默认发生位置说明
CLI/TUI/App Server 进程用户设备Rust 二进制在本机运行
工作目录与文件访问用户设备或显式执行环境受 sandbox/permission 配置约束
命令子进程用户设备或 exec backend不是在模型服务端直接执行
会话 rollout 与状态本地 Codex home具体格式由持久化系列解释
模型推理provider endpoint可以是远端,也存在本地 OSS provider 接入点
MCP 工具和资源取决于 server transport可能是本地 stdio,也可能跨网络

2.2 本地与远端模型 ​

create_oss_provider 默认构造指向 localhost 的 provider,并允许用环境变量覆盖 URL。 这证明 provider 边界可指向本地服务;同时也证明模型是独立 endpoint,而不是编译进 Codex runtime 的推理引擎。

源码位置:codex-rs/model-provider-info/src/lib.rs :: create_oss_provider, create_oss_provider_with_base_url

rust
// provider 构造把本地 OSS 与外部 endpoint 收敛为同一运行时描述。

pub fn create_oss_provider(default_provider_port: u16, wire_api: WireApi) -> ModelProviderInfo {
    // 实验性环境变量决定 localhost 端口;无效值回退到调用方默认端口。
    let default_codex_oss_base_url = format!(
        "http://localhost:{codex_oss_port}/v1",
        codex_oss_port = std::env::var("CODEX_OSS_PORT")
            .ok()
            .filter(|value| !value.trim().is_empty())
            .and_then(|value| value.parse::<u16>().ok())
            .unwrap_or(default_provider_port)
    );

    // 显式 base URL 可以把 provider 指向另一个 endpoint。
    let codex_oss_base_url = std::env::var("CODEX_OSS_BASE_URL")
        .ok()
        .filter(|v| !v.trim().is_empty())
        .unwrap_or(default_codex_oss_base_url);
    create_oss_provider_with_base_url(&codex_oss_base_url, wire_api)
}

pub fn create_oss_provider_with_base_url(base_url: &str, wire_api: WireApi) -> ModelProviderInfo {
    ModelProviderInfo {
        name: "gpt-oss".into(),
        base_url: Some(base_url.into()),
        env_key: None,
        env_key_instructions: None,
        experimental_bearer_token: None,
        auth: None,
        aws: None,
        wire_api,
        query_params: None,
        http_headers: None,
        env_http_headers: None,
        request_max_retries: None,
        stream_max_retries: None,
        stream_idle_timeout_ms: None,
        websocket_connect_timeout_ms: None,
        requires_openai_auth: false,
        supports_websockets: false,
        supports_standalone_web_search: false,
    }
}

源码直接确认 Codex 把模型访问抽象为 provider,并提供 localhost OSS provider 构造器。但能否真正离线工作,还取决于所选模型、认证、工具、MCP 和任务是否访问 网络;不能仅凭 localhost provider 宣称整个系统“完全离线”。

3. 安全边界 ​

3.1 审批与沙箱 ​

AskForApproval 决定何时询问用户;SandboxPolicy 决定命令落地时拥有怎样的文件和网络 限制。二者组合后才构成一次工具执行的实际边界。

源码位置:codex-rs/protocol/src/protocol.rs :: AskForApproval

rust
// 每个审批变体决定“何时询问”,不直接等同于 OS 沙箱强度。

pub enum AskForApproval {
    // 只有安全分类器认可且只读的命令自动通过,其余询问用户。
    #[serde(rename = "untrusted")]
    #[strum(serialize = "untrusted")]
    UnlessTrusted,

    // 由模型在需要时发起审批;这是枚举默认值。
    #[serde(alias = "on-failure")]
    #[default]
    OnRequest,

    // 对 sandbox、rule、skill、permission、MCP 等流程分别控制。
    #[strum(serialize = "granular")]
    Granular(GranularApprovalConfig),

    // 不询问用户;失败直接返回模型,不代表命令自动获得额外权限。
    Never,
}

注意 Never 的含义是“不弹出审批”,不是“无条件允许”。如果沙箱或规则拒绝执行,失败仍然 返回模型。反过来,用户批准也不等于绕过所有平台强制边界;具体行为取决于选定 runtime。

源码位置:codex-rs/protocol/src/protocol.rs :: SandboxPolicy

rust
// SandboxPolicy 表达执行限制;最终强制仍由平台后端实现。

/// 模型发起的 shell 命令采用哪种执行限制。
pub enum SandboxPolicy {
    // 不施加限制,源码明确要求谨慎使用。
    #[serde(rename = "danger-full-access")]
    DangerFullAccess,

    // 文件系统只读;网络默认关闭,但可以显式打开。
    #[serde(rename = "read-only")]
    ReadOnly {
        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
        network_access: bool,
    },

    // 进程已经位于外部沙箱;Codex 记录外部网络能力。
    #[serde(rename = "external-sandbox")]
    ExternalSandbox {
        #[serde(default)]
        network_access: NetworkAccess,
    },

    // 在只读基础上允许写入工作区和额外根目录。
    #[serde(rename = "workspace-write")]
    WorkspaceWrite {
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        writable_roots: Vec<AbsolutePathBuf>,
        #[serde(default)]
        network_access: bool,
        #[serde(default)]
        exclude_tmpdir_env_var: bool,
        #[serde(default)]
        exclude_slash_tmp: bool,
    },
}

下面的决策图强调了“模型建议”“用户审批”“策略选择”“操作系统执行”之间的责任分离。

因此,“Codex 会执行命令”是事实;“Codex 总是在强沙箱中执行”则不是。准确说法必须包含 所用 approval policy、sandbox policy、平台 backend、额外权限和外部环境。

4. SDK 的定位 ​

TypeScript SDK 的 Codex 类并没有重新实现 agent runtime。它持有 CodexExec,并用 Thread 对象建立新会话或恢复已有会话。

源码位置:sdk/typescript/src/codex.ts :: Codex

typescript
// SDK 的 Codex 对象只负责配置与 Thread factory,实际执行委托给 CodexExec。

import { CodexOptions } from "./codexOptions";
import { CodexExec } from "./exec";
import { Thread } from "./thread";
import { ThreadOptions } from "./threadOptions";

/** Codex agent 的 TypeScript 主入口。 */
export class Codex {
  private exec: CodexExec;       // 负责启动和驱动 Codex 进程
  private options: CodexOptions; // 保留全局 SDK 配置

  constructor(options: CodexOptions = {}) {
    const { codexPathOverride, env, config } = options;
    this.exec = new CodexExec(codexPathOverride, env, config);
    this.options = options;
  }

  /** 创建一个新的 agent conversation thread。 */
  startThread(options: ThreadOptions = {}): Thread {
    return new Thread(this.exec, this.options, options);
  }

  /** 通过 thread id 恢复已经持久化的会话。 */
  resumeThread(id: string, options: ThreadOptions = {}): Thread {
    return new Thread(this.exec, this.options, options, id);
  }
}

Python SDK 采取另一种桥接方式:同步 Codex client 在构造时启动 runtime connection, 执行 initialize,并要求调用方及时关闭资源。两者都说明 SDK 是 运行时接入层,不是独立 模型实现。

下面的类图给出这几层的所有权关系。它只展示公共边界,没有展开 SDK 与 App Server 的全部内部类型。

因此,TypeScript 和 Python SDK 虽然提供了不同语言的 API,但任务的会话状态、模型交互和工具 执行仍由 Codex runtime 统一管理。

5. 失败测试入口 ​

正向类图只能说明入口最终连接到 Codex runtime,失败测试才能说明每层由谁拥有进程和资源。npm launcher 负责找到原生包,TypeScript SDK 负责观察 codex exec 子进程,Python SDK 负责建立并初始化 App Server connection;三层失败不会被统一包装成同一个异常。

5.1 原生包缺失 ​

Node launcher 先按 platform + arch 得到 target triple,再解析 optional platform package。无法映射的 主机立即报 Unsupported platform;已知 target 缺少 binary 时给出重装命令,不尝试运行其他平台产物。

源码位置:codex-cli/bin/codex.js :: target选择, findCodexExecutable

javascript
if (!targetTriple) {
  // 未知platform/arch在启动Rust进程前失败,不选择“最相近”的binary。
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
}

const platformPackage = PLATFORM_PACKAGE_BY_TARGET[targetTriple];
if (!platformPackage) {
  throw new Error(`Unsupported target triple: ${targetTriple}`);
}

function findCodexExecutable() {
  let vendorRoot;
  try {
    const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
    vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
  } catch {
    vendorRoot = path.join(__dirname, "..", "vendor");
  }

  const codexExecutable = path.join(
    vendorRoot,
    targetTriple,
    "bin",
    process.platform === "win32" ? "codex.exe" : "codex",
  );
  if (existsSync(codexExecutable)) {
    return codexExecutable;
  }

  const packageManager = detectPackageManager();
  const updateCommand =
    packageManager === "bun"
      ? "bun install -g @openai/codex@latest"
      : packageManager === "pnpm"
        ? "pnpm add -g @openai/codex@latest"
        : "npm install -g @openai/codex@latest";
  // 已知target但binary缺失时给出包管理器修复路径,而不是进入Core。
  throw new Error(
    `Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
  );
}

当前测试没有直接注入未知 platform 或缺失 optional package 的 launcher 场景;这里的失败语义可由 显式源码分支复述。平台包与 codex app 的编译差异详见 跨平台能力矩阵。

5.2 TypeScript SDK ​

CodexExec.run() 同时读取 stdout JSONL 和等待 child exit。真实进程可能先发出 exit,stdout/stderr 稍后才关闭;测试刻意制造这个顺序,并用 500ms race 证明 iterator 会 reject,而不是永久挂起或当作成功。

源码位置:sdk/typescript/tests/exec.test.ts

typescript
// :: rejects when exit happens before stdout closes(完整核心断言)
const child = createEarlyExitChild();
spawnMock.mockReturnValue(child as unknown as child_process.ChildProcess);

const exec = new CodexExec("codex");
const runPromise = (async () => {
  for await (const _ of exec.run({ input: "hi" })) {
    // no-op
  }
})().then(
  () => ({ status: "resolved" as const }),
  (error) => ({ status: "rejected" as const, error }),
);

const result = await Promise.race([
  runPromise,
  delay(500).then(() => ({ status: "timeout" as const })),
]);

// 非零退出必须先于timeout变成SDK异常,不能依赖stream先关闭。
expect(result.status).toBe("rejected");
if (result.status === "rejected") {
  expect(result.error).toBeInstanceOf(Error);
  expect(result.error.message).toMatch(/Codex Exec exited/);
}

这组测试证明 TypeScript SDK 拥有 codex exec 子进程生命周期。它不证明 Core 内部 Session 已经建立; 子进程可能在 CLI 解析、配置加载、认证、Session 构造或 Turn 执行任一阶段退出。

5.3 Python初始化失败 ​

同步 Python Codex 在构造函数中 start 并 initialize。如果 initialize metadata 无效,异常路径必须先 close client 再重新抛出,避免构造失败却留下 App Server 子进程或 transport。

源码位置:sdk/python/tests/test_public_api_runtime_behavior.py

python
# :: test_codex_init_failure_closes_client(完整测试)
def test_codex_init_failure_closes_client(monkeypatch: pytest.MonkeyPatch) -> None:
    closed: list[bool] = []

    class FakeClient:
        def __init__(self, config=None) -> None:  # noqa: ANN001,ARG002
            self._closed = False

        def start(self) -> None:
            return None

        def initialize(self) -> InitializeResponse:
            return InitializeResponse.model_validate({})

        def close(self) -> None:
            self._closed = True
            closed.append(True)

    monkeypatch.setattr(public_api_module, "CodexClient", FakeClient)

    with pytest.raises(RuntimeError, match="missing required metadata"):
        Codex()

    # 构造没有返回对象,底层client仍必须恰好关闭一次。
    assert closed == [True]

异步 client 还有并发 initialize-once 测试;这说明 Python SDK 连接 App Server,而不是像 TypeScript SDK 一样以“一次 exec 进程对应一次 run”作为唯一边界。

5.4 产品入口验证 ​

读完后,可以用下面的只读命令定位三种入口 owner:

bash
# CLI如何选择默认TUI和显式subcommand?
rg -n "struct MultitoolCli|enum Subcommand|match cli.subcommand" codex-rs/cli/src/main.rs

# TypeScript和Python分别在哪里拥有process/connection?
rg -n "spawn\(|exitPromise|class Codex|initialize\(|close\(" \
  sdk/typescript/src sdk/python/src/openai_codex

# 所有产品入口最终在哪里创建Thread?
rg -n "start_thread|resume_thread" codex-rs/core/src/thread_manager.rs

如果能解释下面三个问题,就掌握了本文范围:

  1. “Codex 本地运行”为什么不等于模型一定在本地?
  2. TypeScript 子进程失败与 Python initialize 失败分别由哪一层清理?
  3. 为什么 approval 与 sandbox 必须分开描述?

下一步可阅读 Codex 产品形态全景 比较入口进程拓扑,阅读 Core运行时架构总览 进入 Thread/Turn 实现,并用 Codex信任边界 深入安全强制点。