Codex 系统手册

早期进程加固和运行时引导

stage-23 个文件

这一阶段发生在程序刚启动、还没真正干活之前,像出门前先锁门、检查工具箱。process-hardening 先给进程上安全锁,防止被调试、被偷偷导出内存,或被危险环境变量带偏。rustls-provider 提前选好全局 HTTPS 加密工具,免得后面联网时临时找不到可用方案。arg0 则让同一个程序能按不同名字扮演不同小工具,同时准备 PATH、读取安全的 .env,并搭好异步运行框架,方便后续阶段开始工作。

本阶段涉及的状态8
  • reg-installation-environment程序知道自己装在哪里、家目录在哪里、运行在哪台机器和什么终端里的那组环境信息。
  • reg-sandbox-execution-policy命令和工具运行时要遵守的沙箱、读写、联网、身份和执行策略。
  • reg-observability-telemetry日志、指标、分析事件、请求耗时、工具结果和错误报告等观测数据的收集与发送状态。
  • reg-network-client-proxyHTTP 客户端、证书、Cookie、重试、流式连接和网络代理放行/拒绝规则的共享联网状态。
  • reg-shell-environment-snapshot启动时捕获的用户 shell、PATH、环境变量和终端习惯快照,用于后续命令执行和上下文说明。
  • reg-process-hardening-state进程启动早期施加的反调试、防转储、危险环境变量清理等加固状态,后续运行都在这组安全前提下继续。
  • reg-global-tls-provider全局 HTTPS/TLS 加密提供者的选择和初始化状态,供后续所有联网客户端复用。
  • reg-async-runtime-shutdown全局异步运行时、任务执行句柄、取消信号和关机协调状态,供服务器、后台任务、工具执行和清理流程共用。

本阶段的文件3

进程加固

在更广泛的运行时引导开始前,尽早应用 OS 级保护和环境清理。

process-hardening/src/lib.rs源码 ↗
domain_logicstartup / pre-main

这个文件像程序进门前的安检。程序一启动,它会根据操作系统选择不同做法:在 Linux/Android 上把进程设成不能被转储和调试;在 macOS 上拒绝调试器附加;在 BSD 上关闭 core dump(程序崩溃时留下的内存快照)。这些事很重要,因为 core dump 可能包含密钥、聊天内容等敏感信息,而 LD_、DYLD_ 这类环境变量可能让系统加载攻击者指定的动态库。文件还会清掉这些危险前缀的环境变量。它用了条件编译,也就是不同系统只编进适合自己的代码。遇到关键安全设置失败时,它不会假装没事,而是打印错误并直接退出,避免程序在不安全状态下继续运行。

函数细节11
pre_main_hardening12–25 ↗
fn pre_main_hardening()

作用:这是总入口,用来在 main 函数之前启动进程加固。调用者不需要关心当前是什么系统,它会自动挑合适的加固步骤。

数据流:进去没有参数;它读取编译目标操作系统这个信息;然后按系统分支调用 Linux、macOS、BSD 或 Windows 的加固函数;出来没有返回值,但可能已经改掉进程安全设置和环境变量。

调用关系:它是这套加固流程的调度员。程序启动早期会调用它,它再把具体工作交给 pre_main_hardening_linux、pre_main_hardening_macos、pre_main_hardening_bsd 或 pre_main_hardening_windows。

调用图:调用 4 个内部函数(pre_main_hardening_bsd, pre_main_hardening_linux, pre_main_hardening_macos, pre_main_hardening_windows)。

pre_main_hardening_linux44–61 ↗
fn pre_main_hardening_linux()

作用:这是 Linux 和 Android 上的加固步骤。它让当前进程不容易被同一用户的其他程序调试或转储,并清掉 LD_ 开头的危险环境变量。

数据流:进去没有参数;它调用系统接口 prctl 把进程标成不可转储;成功后把 core dump 大小限制设为 0;再扫描环境变量并删除 LD_ 开头的项;如果 prctl 失败,就打印系统错误并用固定退出码结束程序。

调用关系:它由 pre_main_hardening 在 Linux/Android 编译目标下调用。它自己把关闭 core dump 的活交给 set_core_file_size_limit_to_zero,把清理环境变量的活交给 remove_env_vars_with_prefix。

调用图:调用 2 个内部函数(remove_env_vars_with_prefix, set_core_file_size_limit_to_zero);被 1 处调用(pre_main_hardening);外部调用 3 个(eprintln!, prctl, exit)。

disable_process_dumping65–72 ↗
fn disable_process_dumping() -> std::io::Result<()>

作用:这是一个单独可用的 Linux 辅助函数,用来把当前进程设成不可转储。有人只想做这一项保护,而不跑完整套启动加固时,可以用它。

数据流:进去没有参数;它调用 prctl 让系统禁止当前进程被转储;如果系统调用成功,返回 Ok;如果失败,读取最后一次系统错误并返回 Err,不会直接退出程序。

调用关系:它不在 pre_main_hardening 的主流程里被调用,而是提供给其他代码按需使用。和 pre_main_hardening_linux 做的第一步类似,但它把失败交给调用者处理。

调用图:外部调用 2 个(last_os_error, prctl)。

pre_main_hardening_bsd75–80 ↗
fn pre_main_hardening_bsd()

作用:这是 FreeBSD 和 OpenBSD 上的加固步骤。它重点是禁止生成 core dump,并清掉 LD_ 开头的环境变量。

数据流:进去没有参数;它先把 core dump 文件大小限制改成 0;再扫描并删除 LD_ 开头的环境变量;出来没有返回值,但已经改变了当前进程的限制和环境。

调用关系:它由 pre_main_hardening 在 FreeBSD/OpenBSD 目标下调用。具体的两件事分别交给 set_core_file_size_limit_to_zero 和 remove_env_vars_with_prefix 完成。

调用图:调用 2 个内部函数(remove_env_vars_with_prefix, set_core_file_size_limit_to_zero);被 1 处调用(pre_main_hardening)。

pre_main_hardening_macos83–100 ↗
fn pre_main_hardening_macos()

作用:这是 macOS 上的加固步骤。它阻止调试器附加到当前进程,禁止 core dump,并清掉 DYLD_ 开头的危险环境变量。

数据流:进去没有参数;它调用 ptrace 告诉 macOS 拒绝调试器附加;成功后把 core dump 大小限制设为 0;再删除 DYLD_ 开头的环境变量;如果 ptrace 失败,就打印错误并直接退出。

调用关系:它由 pre_main_hardening 在 macOS 目标下调用。它使用 set_core_file_size_limit_to_zero 处理 core dump,使用 remove_env_vars_with_prefix 清理动态库加载相关的环境变量。

调用图:调用 2 个内部函数(remove_env_vars_with_prefix, set_core_file_size_limit_to_zero);被 1 处调用(pre_main_hardening);外部调用 4 个(eprintln!, ptrace, exit, null_mut)。

set_core_file_size_limit_to_zero103–117 ↗
fn set_core_file_size_limit_to_zero()

作用:这个函数把 core dump 的最大大小设为 0,也就是不允许系统在程序崩溃时保存内存快照。这样可以减少敏感信息落到磁盘上的风险。

数据流:进去没有参数;它构造一个当前限制和最大限制都为 0 的资源限制值;调用 setrlimit 交给操作系统设置;成功就正常结束;失败就打印系统错误并退出程序。

调用关系:它是多个系统加固流程共用的小零件。pre_main_hardening_linux、pre_main_hardening_macos 和 pre_main_hardening_bsd 都会调用它,因为关闭 core dump 是这些系统上共同需要的安全动作。

调用图:被 3 处调用(pre_main_hardening_bsd, pre_main_hardening_linux, pre_main_hardening_macos);外部调用 3 个(eprintln!, setrlimit, exit)。

pre_main_hardening_windows120–122 ↗
fn pre_main_hardening_windows()

作用:这是 Windows 平台预留的加固函数。目前它还没有实际动作,只是占好位置,方便以后补上 Windows 对应的安全设置。

数据流:进去没有参数;目前不读取信息,也不修改任何状态;出来没有返回值。

调用关系:它由 pre_main_hardening 在 Windows 目标下调用。现在它相当于一个空的占位步骤,不会再把工作交给其他函数。

调用图:被 1 处调用(pre_main_hardening)。

remove_env_vars_with_prefix125–131 ↗
fn remove_env_vars_with_prefix(prefix: &[u8])

作用:这个函数删除所有名字以指定字节前缀开头的环境变量。比如传入 LD_,它就会删除 LD_PRELOAD 这类可能影响库加载的变量。

数据流:进去一个前缀,比如 b"LD_" 或 b"DYLD_";它读取当前进程的所有环境变量;先用 env_keys_with_prefix 找出匹配的变量名;再逐个从环境里移除;出来没有返回值,但环境变量已经被改掉。

调用关系:它被 Linux、BSD 和 macOS 的启动加固流程调用。它自己不判断哪些前缀危险,而是按调用者给的前缀办事,并把“筛出匹配名字”的工作交给 env_keys_with_prefix。

调用图:调用 1 个内部函数(env_keys_with_prefix);被 3 处调用(pre_main_hardening_bsd, pre_main_hardening_linux, pre_main_hardening_macos);外部调用 2 个(remove_var, vars_os)。

env_keys_with_prefix134–146 ↗
fn env_keys_with_prefix(vars: I, prefix: &[u8]) -> Vec<OsString>

作用:这个函数只负责从一堆环境变量里挑出名字符合前缀的那些键。它特别用字节来比较,所以即使变量名不是正常 UTF-8 文本,也不会漏掉。

数据流:进去是一组环境变量键值对和一个字节前缀;它逐个查看变量名的原始字节;名字以这个前缀开头的就收集起来;出来是匹配的变量名列表,不会修改环境本身。

调用关系:remove_env_vars_with_prefix 调用它来先找出该删谁。两个测试函数也直接调用它,确认它既能处理非 UTF-8 名字,也只会筛出真正匹配前缀的变量。

调用图:被 3 处调用(remove_env_vars_with_prefix, env_keys_with_prefix_filters_only_matching_keys, env_keys_with_prefix_handles_non_utf8_entries);外部调用 1 个(into_iter)。

tests::env_keys_with_prefix_handles_non_utf8_entries157–178 ↗
fn env_keys_with_prefix_handles_non_utf8_entries()

作用:这个测试确认环境变量名即使不是普通可读文字,也能按原始字节正确筛选。这样可以避免攻击者用奇怪编码绕过 LD_ 前缀清理。

数据流:进去是测试自己造出的几组非 UTF-8 环境变量;它调用 env_keys_with_prefix 查找 LD_ 前缀;然后断言结果只包含那个确实以 LD_ 开头的非 UTF-8 键。

调用关系:它是 env_keys_with_prefix 的安全边界测试。因为真正的删除函数依赖这个筛选结果,所以这个测试保证底层筛选不会被非标准字符搞糊涂。

调用图:调用 1 个内部函数(env_keys_with_prefix);外部调用 5 个(from_bytes, from_vec, assert!, assert_eq!, vec!)。

tests::env_keys_with_prefix_filters_only_matching_keys181–192 ↗
fn env_keys_with_prefix_filters_only_matching_keys()

作用:这个测试确认筛选函数不会乱删,只会挑出指定前缀的变量。比如找 LD_ 时,不应该把 PATH 或 DYLD_FOO 也算进去。

数据流:进去是测试构造的 PATH、LD_TEST、DYLD_FOO 三个变量;它调用 env_keys_with_prefix 查找 LD_;然后检查结果数量为 1,并且唯一结果就是 LD_TEST。

调用关系:它直接测试 env_keys_with_prefix 的基本过滤行为。这个保证很重要,因为 remove_env_vars_with_prefix 后面会真的删除变量,筛错了就可能破坏正常运行环境。

调用图:调用 1 个内部函数(env_keys_with_prefix);外部调用 3 个(from_bytes, assert_eq!, vec!)。

全局加密提供程序设置

安装并验证进程范围的 rustls 加密后端,确保后续 TLS 使用具有确定性。

utils/rustls-provider/src/lib.rs源码 ↗
utilstartup / cross-cutting

rustls 是一个做 TLS 安全连接的库,可以理解成给网络通信加锁的工具。但当项目里同时带了 ring 和 aws-lc-rs 两套“加密工具箱”时,rustls 不能自己决定用哪一个。这个文件就是在程序里统一拍板:默认安装 aws-lc-rs。它用 Once(一把“只允许做一次”的锁)保证全进程只初始化一次,避免多个地方重复抢着设置。安装成功后,它还会检查这套加密工具箱是否支持一个必须的证书签名方案:ECDSA_NISTP521_SHA512。这个方案在一些企业 TLS 代理证书里会用到。如果已经有宿主程序提前装好了别的全局 provider,它不会硬抢,而是保持兼容。没有这个文件,程序在某些依赖组合或企业网络环境下,可能会因为 TLS 加密后端没选好或证书算法不支持而连不上网。

函数细节2
ensure_rustls_crypto_provider10–32 ↗
fn ensure_rustls_crypto_provider()

作用:确保整个进程里已经给 rustls 安装好一套默认加密工具箱。外部代码在需要安全网络连接前调用它,就能减少因为加密后端没选好而失败的风险。

数据流:进去时没有参数,它读取的是进程级的 rustls 全局状态。它先用 Once 确保初始化代码只跑一次,然后尝试安装 aws-lc-rs 作为默认加密 provider;如果别人已经提前安装过,它就安静退出。安装成功后,它取出当前默认 provider,并检查它是否支持项目要求的签名方案;如果不支持,就直接报错,避免后面网络连接时才出隐蔽问题。出来时没有返回值,但可能改变了进程里的 rustls 默认加密 provider。

调用关系:它通常会在程序较早阶段、或者第一次需要 TLS/HTTPS 能力前被调用。它自己会用 Once::new 这一类外部同步工具来做“只初始化一次”的保护;安装后,还会把检查工作交给本文件里的 provider_supports_required_signature_scheme,确认选中的加密工具箱确实够用。

调用图:外部调用 1 个(new)。

provider_supports_required_signature_scheme34–39 ↗
fn provider_supports_required_signature_scheme(provider: &rustls::crypto::CryptoProvider) -> bool

作用:检查某个 rustls 加密 provider 是否支持项目指定的证书签名方式。它像是在问这套工具箱里有没有那把必须的钥匙。

数据流:进去的是一个 rustls CryptoProvider,也就是一套加密能力集合。它读取里面“可用于验证签名的算法列表”,拿这个列表和 REQUIRED_SIGNATURE_SCHEME 这个必需项比较。出来的是 true 或 false:true 表示支持,false 表示不支持;它不修改任何外部状态。

调用关系:它是 ensure_rustls_crypto_provider 安装完默认 provider 后的验收步骤。前一个函数负责安装和兜底,这个函数只负责做清晰的小判断,让主流程能知道当前 provider 是否满足企业证书场景需要。

分发和运行时引导

为应用其余部分设置基于 argv 的命令分发、PATH 别名、进程环境和 Tokio 运行时脚手架。

arg0/src/lib.rs源码 ↗
orchestrationstartup

Codex 希望发布时尽量只有一个程序文件,但运行时又需要几个名字不同的小命令。这个文件用的是“arg0 技巧”:程序启动时先看自己是用什么名字被调用的,如果名字像 apply_patch,就直接走补丁工具;如果像 codex-linux-sandbox,就直接启动沙箱;否则才进入正常 Codex 主程序。它还会创建一个临时目录,把这些小命令名做成指向当前程序的别名,再把这个目录放到 PATH 最前面。这样别的子进程只要喊“apply_patch”,其实还是回到同一个 Codex 程序。Arg0PathEntryGuard 像一张“临时通行证”,握着临时目录和锁文件,保证这些别名在本次运行期间不会被清掉。文件还会加载 ~/.codex/.env,但禁止里面改 CODEX_ 开头的敏感变量,避免用户配置覆盖内部控制开关。

函数细节24
Arg0PathEntryGuard::new45–51 ↗
fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self

作用:创建一个守护对象,把临时目录、锁文件和几个 helper 程序路径绑在一起。有人拿着它,就表示这些临时别名还应该继续存在。

数据流:输入是一个临时目录、一个锁文件、以及一组路径信息;函数只是把它们装进 Arg0PathEntryGuard 里;输出是这个守护对象,之后对象活着,临时目录和锁文件也跟着活着。

调用关系:prepare_path_entry_for_codex_aliases 在真正创建好临时别名后会调用它。测试里也会手工造这个对象,用来验证 Linux 沙箱路径选择和别名生命周期。

调用图:被 3 处调用(prepare_path_entry_for_codex_aliases, linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias, run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns)。

Arg0PathEntryGuard::paths53–55 ↗
fn paths(&self) -> &Arg0DispatchPaths

作用:取出守护对象里保存的 helper 路径。外部代码用它知道该从哪里重新启动 Codex、沙箱或 execve 包装器。

数据流:输入是已有的 Arg0PathEntryGuard;函数读取里面的 paths 字段;输出是这组路径的只读引用,不改任何东西。

调用关系:run_main_with_arg0_guard 会通过它拿到 execve 包装器路径,linux_sandbox_exe_path 也会间接依赖这里保存的路径。

调用图:被 1 处调用(paths)。

arg0_dispatch58–162 ↗
fn arg0_dispatch() -> Option<Arg0PathEntryGuard>

作用:这是启动时的分流器。它先判断当前程序是不是被某个特殊名字或特殊参数叫起来的,如果是,就直接进入对应的小工具;如果不是,就准备正常主程序需要的环境。

数据流:它读取命令行参数、当前可执行文件名、PATH 和 Codex 安装信息;如果发现 apply_patch、沙箱、文件系统 helper 等入口,就调用对应模块并直接退出或不返回;否则加载 .env,创建 PATH 别名目录,必要时更新 PATH,最后返回一个可能存在的 Arg0PathEntryGuard。

调用关系:arg0_dispatch_or_else 在真正运行主程序前调用它。它会把活儿分给 load_dotenv、prepare_path_env_var_with_aliases,以及外部的 apply_patch、沙箱和 helper 主入口。

调用图:调用 4 个内部函数(load_dotenv, prepare_path_env_var_with_aliases, current, current_dir);被 1 处调用(arg0_dispatch_or_else);外部调用 16 个(new, apply_patch, main, run_fs_helper_main, run_main, run_shell_escalation_execve_wrapper, run_windows_sandbox_wrapper_main, eprintln!, args, args_os (+6 more))。

prepare_path_env_var_with_aliases164–181 ↗
fn prepare_path_env_var_with_aliases(
    install_context: &InstallContext,
    existing_path: Option<OsString>,
    prepare_aliases: impl FnOnce(Option<OsString>) -> std::io::Result<(Arg0PathEntryGua

作用:准备新的 PATH。它会优先把安装包自带的命令目录放进去,再尝试加入本次运行创建的临时别名目录。

数据流:输入是安装上下文、原来的 PATH、以及一个负责创建别名目录的函数;它先算出带安装包目录的 PATH,再把这个 PATH 交给别名创建函数;成功时输出守护对象和最终 PATH,失败时打印警告,并尽量保留安装包目录那部分 PATH。

调用关系:arg0_dispatch 用它统一处理 PATH。它内部调用 path_env_with_package_path_dir,别名创建这一步通常交给 prepare_path_entry_for_codex_aliases。

调用图:调用 1 个内部函数(path_env_with_package_path_dir);被 1 处调用(arg0_dispatch);外部调用 1 个(eprintln!)。

arg0_dispatch_or_else207–236 ↗
fn arg0_dispatch_or_else(main_fn: F) -> anyhow::Result<()>

作用:这是给各个 Codex 二进制入口包一层的启动外壳。它先做 arg0 分流和环境准备,然后在 Tokio 异步运行时里执行真正的主程序。

数据流:输入是一个异步 main 函数;它先调用 arg0_dispatch,拿到可能存在的临时别名守护对象,再记录当前可执行文件路径;之后新开一个栈更大的线程,在线程里创建 Tokio 运行时并执行主程序;输出是主程序的成功或错误,如果线程 panic,就继续抛出这个 panic。

调用关系:它是普通 Codex 主程序进入异步世界前的总包装。它把前半段启动分流交给 arg0_dispatch,把真正运行主程序交给 run_main_with_arg0_guard。

调用图:调用 1 个内部函数(arg0_dispatch);外部调用 3 个(current_exe, resume_unwind, new)。

run_main_with_arg0_guard238–264 ↗
async fn run_main_with_arg0_guard(
    path_entry_guard: Option<Arg0PathEntryGuard>,
    current_exe: Option<PathBuf>,
    main_fn: F,
) -> anyhow::Result<()>

作用:把 helper 路径整理好,然后把它们交给真正的异步主程序。它还特意保证临时别名目录在主程序结束前一直存在。

数据流:输入是可选的 Arg0PathEntryGuard、当前可执行文件路径、以及异步主函数;它组装 Arg0DispatchPaths,其中 Linux 下会选择沙箱可执行路径;然后等待 main_fn 跑完;最后才丢弃守护对象并返回 main_fn 的结果。

调用关系:arg0_dispatch_or_else 在线程和运行时都准备好后调用它。它会调用 linux_sandbox_exe_path 来决定 Linux 沙箱该用哪个路径。

调用图:调用 1 个内部函数(linux_sandbox_exe_path);被 1 处调用(run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns);外部调用 1 个(cfg!)。

linux_sandbox_exe_path266–276 ↗
fn linux_sandbox_exe_path(
    path_entry_guard: Option<&Arg0PathEntryGuard>,
    current_exe: Option<PathBuf>,
) -> Option<PathBuf>

作用:决定 Linux 沙箱 helper 应该用哪个程序路径。优先用临时别名,因为这个名字能再次触发 arg0 分流;没有别名时才退回当前 Codex 可执行文件。

数据流:输入是可选的路径守护对象和可选的当前程序路径;它先看守护对象里有没有 codex-linux-sandbox 别名;有就输出别名路径,没有就输出当前程序路径,两个都没有就输出空。

调用关系:run_main_with_arg0_guard 在组装 Arg0DispatchPaths 时调用它。对应测试会确认它确实优先选择别名。

调用图:被 1 处调用(run_main_with_arg0_guard)。

build_runtime278–283 ↗
fn build_runtime() -> anyhow::Result<tokio::runtime::Runtime>

作用:创建 Codex 主程序要用的 Tokio 运行时。Tokio 是 Rust 里常用的异步任务调度器,可以让很多等待网络或文件的任务一起推进。

数据流:它没有业务输入;函数配置一个多线程 Tokio 运行时,打开所有常用能力,并把工作线程栈大小设为 16MB;输出是建好的运行时,失败时返回错误。

调用关系:arg0_dispatch_or_else 的新线程里会用它来跑主程序。测试 run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns 也用它来驱动异步测试。

调用图:外部调用 1 个(new_multi_thread)。

load_dotenv291–297 ↗
fn load_dotenv()

作用:从 ~/.codex/.env 读取用户配置到环境变量里,但会避开 Codex 内部保留的变量名。这样用户可以配置普通环境,又不容易误伤内部控制变量。

数据流:它先查 Codex 主目录,再尝试打开其中的 .env 文件;如果能读到,就把解析出来的键值交给 set_filtered;它没有返回值,失败时静默跳过。

调用关系:arg0_dispatch 在创建线程前调用它,因为修改环境变量在多线程程序里不安全。真正设置变量的细活交给 set_filtered。

调用图:调用 1 个内部函数(set_filtered);被 1 处调用(arg0_dispatch);外部调用 2 个(find_codex_home, from_path_iter)。

set_filtered300–311 ↗
fn set_filtered(iter: I)

作用:把 .env 里的变量写入当前进程环境,但过滤掉 CODEX_ 开头的名字。这个过滤像门卫,防止配置文件改掉 Codex 自己的内部开关。

数据流:输入是一串 dotenv 解析结果;它跳过解析失败的项,逐个检查变量名;不是 CODEX_ 开头的就写入环境变量,是的话就不动;没有显式输出,但会改变当前进程环境。

调用关系:load_dotenv 负责找到文件并解析,set_filtered 负责安全地落地这些变量。

调用图:被 1 处调用(load_dotenv);外部调用 2 个(into_iter, set_var)。

prepare_path_entry_for_codex_aliases326–436 ↗
fn prepare_path_entry_for_codex_aliases(
    existing_path: Option<OsString>,
) -> std::io::Result<(Arg0PathEntryGuard, OsString)>

作用:创建本次运行专用的临时命令目录,并在里面放 apply_patch、沙箱等别名。这样系统 PATH 里能找到这些名字,但用户不需要真的安装多个程序。

数据流:输入是原来的 PATH;它找到 Codex 主目录,创建 tmp/arg0 临时根目录,设置权限,清理过期目录,再新建一个会话目录和 .lock 锁文件;随后在里面创建指向当前可执行文件的符号链接,Windows 下则写批处理脚本;最后输出 Arg0PathEntryGuard 和把临时目录放到最前面的新 PATH。

调用关系:prepare_path_env_var_with_aliases 通常会把它作为别名创建函数来调用。它内部会用 janitor_cleanup 清理旧目录,用 path_env_with_entry 拼出新的 PATH,用 Arg0PathEntryGuard::new 保存生命周期。

调用图:调用 3 个内部函数(new, janitor_cleanup, path_env_with_entry);外部调用 13 个(options, new, find_codex_home, from_mode, eprintln!, format!, current_exe, temp_dir, create_dir_all, set_permissions (+3 more))。

path_env_with_package_path_dir438–447 ↗
fn path_env_with_package_path_dir(
    install_context: &InstallContext,
    existing_path: Option<OsString>,
) -> Option<OsString>

作用:如果安装包说明里有一个应该加入 PATH 的目录,就把它加到 PATH 最前面。这样 Codex 自带或包装出的命令更容易被找到。

数据流:输入是安装上下文和旧 PATH;它从安装上下文里找 package_layout.path_dir;找到了就调用 path_env_with_entry 拼接新 PATH,找不到就输出空。

调用关系:prepare_path_env_var_with_aliases 先调用它,确保安装包目录能排在旧 PATH 前面。真正拼字符串的工作交给 path_env_with_entry。

调用图:调用 1 个内部函数(path_env_with_entry);被 1 处调用(prepare_path_env_var_with_aliases)。

path_env_with_entry449–467 ↗
fn path_env_with_entry(path_entry: &Path, existing_path: Option<OsString>) -> OsString

作用:把一个目录加到 PATH 最前面。PATH 可以理解成系统找命令时看的“搜索路线表”。

数据流:输入是要新增的目录和旧 PATH;它按操作系统选择分隔符,Unix 用冒号,Windows 用分号;然后拼成“新目录 + 分隔符 + 旧 PATH”的形式;输出新的 PATH 字符串。

调用关系:path_env_with_package_path_dir 用它加入安装包目录,prepare_path_entry_for_codex_aliases 用它加入临时别名目录。相关测试会检查目录顺序是否正确。

调用图:被 2 处调用(path_env_with_package_path_dir, prepare_path_entry_for_codex_aliases);外部调用 2 个(with_capacity, as_os_str)。

janitor_cleanup469–496 ↗
fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()>

作用:清理以前运行留下的 arg0 临时目录。它只删除看起来已经没人使用的目录,避免把正在运行的 Codex 的别名删掉。

数据流:输入是临时目录根路径;它遍历下面的子目录,对每个目录尝试拿 .lock 锁;拿不到锁说明有人正在用,跳过;拿到锁说明目录过期,就删除整个目录;输出成功或文件系统错误。

调用关系:prepare_path_entry_for_codex_aliases 在创建新会话目录前调用它。它把判断目录是否能删的关键动作交给 try_lock_dir,测试会覆盖无锁、锁被占用、锁空闲三种情况。

调用图:调用 1 个内部函数(try_lock_dir);被 4 处调用(prepare_path_entry_for_codex_aliases, janitor_removes_dirs_with_unlocked_lock, janitor_skips_dirs_with_held_lock, janitor_skips_dirs_without_lock_file);外部调用 2 个(read_dir, remove_dir_all)。

try_lock_dir498–511 ↗
fn try_lock_dir(dir: &Path) -> std::io::Result<Option<File>>

作用:检查一个临时目录是不是还被某个 Codex 进程占用。办法是尝试锁住目录里的 .lock 文件。

数据流:输入是目录路径;它打开目录下的 .lock 文件,没有锁文件就返回空;能打开就尝试加锁,加锁成功则输出锁文件,表示目录可清理;如果锁被别人占着,也返回空;其他错误会返回错误。

调用关系:janitor_cleanup 对每个候选目录调用它,用它来决定这个目录该跳过还是删除。

调用图:被 1 处调用(janitor_cleanup);外部调用 2 个(options, join)。

tests::create_lock543–551 ↗
fn create_lock(dir: &Path) -> std::io::Result<File>

作用:测试辅助函数,用来在指定目录里创建或打开 .lock 文件。测试借它模拟“这个临时目录有锁文件”的状态。

数据流:输入是目录路径;它拼出 .lock 文件路径,并用读写方式创建或打开文件;输出这个文件句柄,测试可以继续对它加锁。

调用关系:多个测试会调用它来准备场景,比如模拟锁被占用、锁未占用,或构造 Arg0PathEntryGuard。

调用图:外部调用 2 个(options, join)。

tests::package_path_test_fixture553–582 ↗
fn package_path_test_fixture() -> anyhow::Result<PackagePathTestFixture>

作用:搭一个假的安装环境给 PATH 相关测试用。它创建临时目录结构,让测试不用依赖真实机器上的 Codex 安装。

数据流:它新建临时目录,并在里面创建 package、bin、codex-path、existing-bin 等目录;再把这些路径装进 InstallContext;输出 PackagePathTestFixture,里面包含测试要用的路径和安装上下文。

调用关系:path_env_can_prepend_package_path_before_arg0_alias_dir 和 package_path_survives_arg0_alias_setup_failure 都用它准备统一的测试输入。

调用图:调用 1 个内部函数(from_absolute_path);外部调用 2 个(new, create_dir_all)。

tests::linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias585–604 ↗
fn linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias() -> std::io::Result<()>

作用:验证 Linux 沙箱路径选择规则:如果有 codex-linux-sandbox 别名,就必须优先用别名,而不是直接用 codex 主程序路径。

数据流:测试创建临时目录、锁文件和一个假的别名路径,再构造 Arg0PathEntryGuard;调用 linux_sandbox_exe_path;最后断言输出等于别名路径。

调用关系:它直接覆盖 linux_sandbox_exe_path 的核心行为,防止以后改代码时破坏 arg0 分流所依赖的文件名。

调用图:调用 1 个内部函数(new);外部调用 4 个(from, new, create_lock, assert_eq!)。

tests::path_env_can_prepend_package_path_before_arg0_alias_dir607–626 ↗
fn path_env_can_prepend_package_path_before_arg0_alias_dir() -> anyhow::Result<()>

作用:验证 PATH 的最终顺序正确:临时 arg0 别名目录最靠前,其次是安装包提供的 PATH 目录,最后才是原有 PATH。

数据流:测试先用 package_path_test_fixture 准备目录;再调用 path_env_with_package_path_dir 得到带安装包目录的 PATH;接着调用 path_env_with_entry 加入 arg0 目录;最后拆开 PATH 并断言顺序符合预期。

调用关系:它同时检查 path_env_with_package_path_dir 和 path_env_with_entry 的配合效果,确保启动时命令查找优先级不会错。

调用图:外部调用 4 个(package_path_test_fixture, assert_eq!, path_env_with_entry, path_env_with_package_path_dir)。

tests::package_path_survives_arg0_alias_setup_failure629–661 ↗
fn package_path_survives_arg0_alias_setup_failure() -> anyhow::Result<()>

作用:验证即使临时别名创建失败,安装包目录也仍然会被保留在 PATH 里。这样 Codex 不会因为一个辅助功能失败就丢掉更基础的 PATH 修正。

数据流:测试准备假的安装环境,然后调用 prepare_path_env_var_with_aliases,并传入一个故意返回错误的别名创建函数;它检查传给别名函数的 PATH 已包含安装包目录,也检查最终返回的 PATH 仍保留安装包目录且没有守护对象。

调用关系:它覆盖 prepare_path_env_var_with_aliases 的降级路径,确保失败时只是警告并继续,而不是把 PATH 修正全部放弃。

调用图:外部调用 4 个(package_path_test_fixture, assert!, assert_eq!, prepare_path_env_var_with_aliases)。

tests::run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns665–704 ↗
fn run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns() -> anyhow::Result<()>

作用:验证临时别名在异步主程序运行期间不会提前消失。这个行为很重要,因为主程序运行中可能还要用这些 helper 路径启动子进程。

数据流:测试创建临时目录、假的别名文件、锁文件和 Arg0PathEntryGuard;然后用 build_runtime 跑 run_main_with_arg0_guard;传入的异步主函数会在开始和让出一次调度后都检查别名文件仍然存在;成功表示守护对象活得足够久。

调用关系:它直接测试 run_main_with_arg0_guard 对 Arg0PathEntryGuard 生命周期的保护,也间接用到 build_runtime 和 Arg0PathEntryGuard::new。

调用图:调用 2 个内部函数(new, run_main_with_arg0_guard);外部调用 5 个(from, new, create_lock, write, build_runtime)。

tests::janitor_skips_dirs_without_lock_file707–716 ↗
fn janitor_skips_dirs_without_lock_file() -> std::io::Result<()>

作用:验证清理器不会删除没有 .lock 文件的目录。这样可以避免误删不属于 arg0 机制的普通目录。

数据流:测试创建一个没有锁文件的目录;调用 janitor_cleanup;最后断言这个目录还存在。

调用关系:它覆盖 janitor_cleanup 和 try_lock_dir 对“无锁文件”情况的处理,确保清理动作足够保守。

调用图:调用 1 个内部函数(janitor_cleanup);外部调用 3 个(assert!, create_dir, tempdir)。

tests::janitor_skips_dirs_with_held_lock719–730 ↗
fn janitor_skips_dirs_with_held_lock() -> std::io::Result<()>

作用:验证清理器不会删除锁正被占用的目录。锁被占用通常表示另一个 Codex 进程还在使用这个目录。

数据流:测试创建目录和 .lock 文件,并先把锁拿住;调用 janitor_cleanup;最后断言目录仍然存在。

调用关系:它检查 janitor_cleanup 通过 try_lock_dir 识别“正在使用”的目录,并正确跳过。

调用图:调用 1 个内部函数(janitor_cleanup);外部调用 4 个(create_lock, assert!, create_dir, tempdir)。

tests::janitor_removes_dirs_with_unlocked_lock733–743 ↗
fn janitor_removes_dirs_with_unlocked_lock() -> std::io::Result<()>

作用:验证清理器会删除有 .lock 文件但锁没人占用的旧目录。这代表目录很可能是上次运行留下的垃圾。

数据流:测试创建目录和 .lock 文件,但不持有锁;调用 janitor_cleanup;最后断言目录已经被删除。

调用关系:它覆盖 janitor_cleanup 的实际删除路径,确认 try_lock_dir 成功拿锁后会触发 remove_dir_all 清理。

调用图:调用 1 个内部函数(janitor_cleanup);外部调用 4 个(create_lock, assert!, create_dir, tempdir)。