Codex 项目概览
Codex CLI 在本仓库 README 中被直接定义为:运行在用户计算机上的 OpenAI coding agent。这里的关键词不是“聊天机器人”,也不是“云端开发平台”,而是 本地运行时、模型驱动决策、面向软件工程任务、能够调用受策略约束的工具。
不过,“本地”很容易产生两个错误理解:
- 它不表示模型推理必然在本机完成。默认形态仍会通过模型 provider 发起请求;源码也 提供了本地 OSS provider 接入点。
- 它不表示所有命令天然处于同一种强沙箱中。审批策略与
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
// 顶层参数同时承载多工具子命令与无子命令时的 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
// 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
// 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
// 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
// 每个审批变体决定“何时询问”,不直接等同于 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
// 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
// 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
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
// :: 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
# :: 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:
# 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如果能解释下面三个问题,就掌握了本文范围:
- “Codex 本地运行”为什么不等于模型一定在本地?
- TypeScript 子进程失败与 Python initialize 失败分别由哪一层清理?
- 为什么 approval 与 sandbox 必须分开描述?
下一步可阅读 Codex 产品形态全景 比较入口进程拓扑,阅读 Core运行时架构总览 进入 Thread/Turn 实现,并用 Codex信任边界 深入安全强制点。
