Codex 系统手册

辅助二进制和开发者工具

stage-1.245 个文件

这一阶段像工具箱,不是主程序的日常入口,而是给开发、测试和后台运行帮忙。配置和协议生成器负责产出“格式说明书”,让前后端和编辑器对得上;apply-patch、file-search做补丁和找文件;app-server、MCP、代理和stdio桥接负责单独启动服务或转发消息;沙箱、执行策略工具负责安全地跑命令;日志查看器、示例和假服务器则方便排查问题、演练接口和做自动测试。

本阶段的文件45

架构和协议生成器

这些二进制程序和辅助工具用于生成或刷新其他工具和测试使用的架构、绑定和协议夹具。

core/src/bin/config_schema.rs源码 ↗
entrypoint开发或构建辅助工具运行时

这个文件解决的是“配置文件有没有写对”的问题。项目里的 config.toml 可能有很多选项,靠人记很容易写错字段名或填错类型。这个工具运行后,会把配置格式导出成 config.schema.json。JSON Schema 是一种机器能读懂的规则文件,很多编辑器可以用它来提示、补全和检查配置。它的流程很简单:先用命令行参数解析器 clap 读取用户有没有指定输出路径;如果用户没指定,就默认把文件写到当前 Rust 包目录下的 config.schema.json;最后把真正生成 schema 的活交给 codex_config::schema::write_config_schema。它自己不负责定义配置长什么样,只负责把“生成并写出 schema”这个动作包装成一个可执行命令。

函数细节1
main13–20 ↗
fn main() -> Result<()>

作用:这是这个命令行工具的入口。它读取用户给的输出路径,然后生成 config.schema.json,让外部工具可以检查和提示配置文件。

数据流:进去的是命令行参数,比如用户可能传了 --out 某个路径;它先通过 Args::parse 把这些文字参数变成程序能用的数据。然后它决定输出位置:用户给了路径就用用户的,没给就用项目目录里的默认 config.schema.json。最后它把这个路径交给 write_config_schema,由后者生成并写入文件;如果中间出错,就把错误往外返回。

调用关系:它是整个小工具的起点。运行这个二进制程序时,系统先进入 mainmain 自己只做参数读取和路径选择,然后把核心工作交给 write_config_schemaparse(ext) 负责把命令行文字解释成结构化参数,write_config_schema 负责真正产出 schema 文件。

调用图:调用 1 个内部函数(write_config_schema);外部调用 1 个(parse)。

config/src/schema.rs源码 ↗
configconfig schema generation / tooling

这个文件的核心作用,是把项目支持的配置规则整理成一份机器能读懂的格式。可以把 JSON Schema 理解成“表格填写规则”:哪些格子能填、能填什么类型、能不能乱加字段。文件先专门描述 features 这些功能开关:只允许已知功能名和旧版兼容功能名,陌生名字会被拒绝;有些功能不是简单的 true/false,而是有自己的小配置。它还描述 mcp_servers 这类服务器配置,允许用户按名字添加多个服务器,每个服务器都要符合原始配置格式。最后,它把整个 ConfigToml 生成完整 schema,再把 JSON 的键排序,输出成漂亮、稳定的 JSON 文件。键排序很重要,因为这样生成结果不会因为顺序变化而让测试或代码审查看起来像改了很多东西。

函数细节7
features_schema18–76 ↗
fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema

作用:生成 config.toml 里 [features] 这一块的填写规则。它的重点是:只允许项目认识的功能开关和老版本遗留的开关,防止用户拼错名字后系统却悄悄忽略。

数据流:进去的是一个 SchemaGenerator,也就是“schema 生成器”。它读取项目内置的功能列表 FEATURES 和旧功能名列表 legacy_feature_keys,把每个功能名变成 schema 里的一个允许字段;普通功能是布尔值 true/false,少数功能会用更具体的配置格式。最后出来的是一个 Schema 对象,表示 [features] 必须是对象,而且不能出现未声明的额外字段。

调用关系:它是生成整体配置 schema 时会被 schemars 间接用到的定制规则之一。遇到 AppsMcpPathOverride 这个特殊功能时,它会把细节交给 removed_apps_mcp_path_override_schema,因为那个功能需要兼容两种写法。

调用图:调用 1 个内部函数(removed_apps_mcp_path_override_schema);外部调用 6 个(new, default, default, Bool, Object, legacy_feature_keys)。

removed_apps_mcp_path_override_schema78–100 ↗
fn removed_apps_mcp_path_override_schema(schema_gen: &mut SchemaGenerator) -> Schema

作用:给一个已经特殊处理的旧功能生成兼容 schema。它允许用户既可以简单写 true/false,也可以写成带 enabled 和 path 的小配置对象。

数据流:进去的是 SchemaGenerator。它先做出一个对象规则:里面只允许 enabled 这个布尔字段和 path 这个字符串字段,并且不许乱加别的字段;然后再把这个对象规则和简单布尔值规则合在一起,表示“两种写法任选一种”。出来的是一个 Schema。

调用关系:它只被 features_schema 调用。features_schema 遍历功能列表时,一旦碰到 AppsMcpPathOverride,就把这个特殊字段的规则交给它来生成。

调用图:被 1 处调用(features_schema);外部调用 6 个(new, default, default, Bool, Object, vec!)。

mcp_servers_schema103–116 ↗
fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema

作用:生成 config.toml 里 [mcp_servers] 这一块的填写规则。它允许用户自己起服务器名字,但每个服务器的内容必须符合 RawMcpServerConfig 的格式。

数据流:进去的是 SchemaGenerator。它创建一个“对象”规则,把对象里的任意键都当作一个服务器名字,但每个键对应的值都必须按 RawMcpServerConfig 的 schema 来写。出来的是这个服务器映射表的 Schema。

调用关系:它和 features_schema 类似,都是给 ConfigToml 里某些比较特殊的配置块提供定制 schema。它通常由 schema 生成过程间接使用,而不是业务运行时手动调用。

调用图:外部调用 3 个(new, default, Object)。

config_schema119–126 ↗
fn config_schema() -> RootSchema

作用:生成整份 config.toml 的完整 JSON Schema。它是这个文件里“总装配”的函数,把 ConfigToml 这个配置结构变成可发布、可校验的规则文档。

数据流:它不需要外部输入。它先选择 JSON Schema draft-07 这个版本的规则格式,并关闭“Option 自动加 null 类型”的行为,也就是可选字段不默认等于可以写 null;然后基于 ConfigToml 生成 RootSchema。出来的是完整的根 schema。

调用关系:它被 config_schema_json 调用。config_schema_json 需要先拿到这份完整 schema,才能继续转成 JSON、排序并美化输出。

调用图:被 1 处调用(config_schema_json);外部调用 1 个(draft07)。

canonicalize129–143 ↗
fn canonicalize(value: &Value) -> Value

作用:把 JSON 值里的对象键按字母顺序排好。这样每次生成出来的 JSON 顺序都稳定,方便测试、比较文件和代码审查。

数据流:进去的是一个 serde_json::Value,也就是一棵 JSON 数据树。它如果看到数组,就递归整理数组里的每一项;如果看到对象,就把对象的键排序,再递归整理每个子值;如果是字符串、数字、布尔值这类普通值,就直接复制。出来的是一份内容相同但对象键顺序稳定的新 JSON 值。

调用关系:它被 config_schema_json 调用,位置在 schema 被转成 JSON 值之后、写成文本之前。它不负责生成规则,只负责把输出整理得稳定好看。

调用图:被 1 处调用(config_schema_json);外部调用 4 个(with_capacity, Array, Object, clone)。

config_schema_json146–152 ↗
fn config_schema_json() -> anyhow::Result<Vec<u8>>

作用:把完整配置 schema 渲染成漂亮的 JSON 字节内容。别人想把 schema 写进文件或作为产物保存时,通常会从这里拿结果。

数据流:它不需要输入。它先调用 config_schema 得到完整 schema,再用 serde_json 转成 JSON 值,接着调用 canonicalize 排序,最后转成带缩进的漂亮 JSON 字节数组。成功时出来的是 Vec<u8>,也就是可以直接写入文件的字节内容;出错时返回 anyhow::Result 里的错误。

调用关系:它连接了“生成规则”和“输出文本”两个阶段。write_config_schema 会调用它拿到最终 JSON,然后再负责写入磁盘。

调用图:调用 2 个内部函数(canonicalize, config_schema);被 1 处调用(write_config_schema);外部调用 2 个(to_value, to_vec_pretty)。

write_config_schema155–159 ↗
fn write_config_schema(out_path: &Path) -> anyhow::Result<()>

作用:把生成好的配置 schema 写到指定文件里。它是把内存里的 schema 产物落到磁盘上的最后一步。

数据流:进去的是一个文件路径 out_path。它先调用 config_schema_json 生成漂亮且顺序稳定的 JSON 字节,然后用 std::fs::write 写到这个路径。出来的是成功或失败的结果;成功时磁盘上会多出或更新这份 schema 文件。

调用关系:它被 main 调用,通常用于命令行工具或生成脚本。它自己不关心 schema 怎么生成,只负责拿到结果并保存。

调用图:调用 1 个内部函数(config_schema_json);被 1 处调用(main);外部调用 1 个(write)。

app-server-protocol/src/bin/export.rs源码 ↗
entrypointCLI run

这个文件相当于一个“协议文件导出按钮”。开发者在命令行里运行它,告诉它输出目录,它就会调用协议库里的生成器,把 Rust 代码里定义的接口规则转成两类更通用的文件:TypeScript 绑定(给前端或 TypeScript 项目用的类型说明)和 JSON Schema(一种描述 JSON 数据长什么样的规则文件)。它还支持传入 Prettier 路径,Prettier 是一个代码格式化工具,可以把生成的 TypeScript 文件排版得更整齐。另一个开关是 experimental,意思是是否把还在实验阶段的接口也一起导出。这个文件本身不负责“怎么生成”,它只负责读命令行参数,然后把这些参数交给真正的生成函数。没有它,开发者就少了一个方便、统一的入口来生成这些跨语言协议文件。

函数细节1
main23–34 ↗
fn main() -> Result<()>

作用:这是这个命令行工具的入口。它读取用户在命令行里给的输出目录、可选的 Prettier 路径,以及是否包含实验接口,然后启动 TypeScript 和 JSON Schema 的生成工作。

数据流:进去的是命令行参数:输出目录、Prettier 程序的位置、experimental 开关。它先用 clap 的 parse 读取并整理这些参数,clap 是一个帮程序解析命令行选项的库;然后把输出目录、格式化工具路径和生成选项交给 TypeScript 生成函数;最后再调用 JSON Schema 生成函数。出来的是一个成功或失败的结果:成功时目标目录里会多出生成的文件,失败时把错误往外返回。

调用关系:它是整个导出流程的起点。程序一启动就执行 main;main 自己不造文件内容,而是先用 Args::parse 取得用户要求,再调用 GenerateTsOptions::default 准备默认生成设置,并把 experimental_api 改成用户指定的值,接着把实际生成工作交给 codex_app_server_protocol::generate_ts_with_options 和 codex_app_server_protocol::generate_json_with_experimental。

调用图:调用 1 个内部函数(default);外部调用 3 个(parse, generate_json_with_experimental, generate_ts_with_options)。

app-server-protocol/src/bin/write_schema_fixtures.rs源码 ↗
entrypointdeveloper tooling / manual regeneration

这个文件像一个“刷新样板文件”的按钮。项目里有一套 app-server 通信协议,需要把协议结构生成成一些固定文件,放在 schema 目录下给测试、校验或其他语言使用。如果没有这个工具,开发者可能要手动更新这些文件,很容易漏掉字段、写错路径,或者格式乱掉。它先读取命令行参数:可以指定 schema 根目录,可以指定 Prettier(一个常见的代码格式化工具,用来把 TypeScript 文件排版整齐),也可以选择是否包含实验性 API。然后它决定实际使用哪个 schema 目录;如果用户没传,就默认用当前 Rust 包里的 schema 文件夹。最后它把这些信息交给库里的生成函数去真正写文件。如果生成失败,它会补上一句更清楚的错误说明,告诉人是在哪个目录下刷新失败。

函数细节1
main22–42 ↗
fn main() -> Result<()>

作用:这是这个命令行工具的入口。开发者运行该程序时,它负责读参数、决定目标目录,然后调用真正的生成逻辑来刷新 schema fixture 文件。

数据流:进去的是命令行参数:schema 根目录、可选的 Prettier 程序路径、是否包含实验性 API。它先用参数解析器把这些文字参数变成程序能用的数据;如果没有给 schema 根目录,就自动拼出默认的 schema 路径。之后它把目录、格式化工具路径和实验性选项传给生成函数。出来的是成功或失败的结果;成功时文件被刷新,失败时返回带有目录信息的错误提示。

调用关系:它是整个流程的最外层入口。运行程序后,main 先调用 Args::parse 解析命令行,再把实际工作交给 codex_app_server_protocol::write_schema_fixtures_with_options。main 自己不负责生成文件内容,只负责把用户的选择整理好,并在出错时把错误包装得更容易理解。

调用图:外部调用 2 个(parse, write_schema_fixtures_with_options)。

hooks/src/bin/write_hooks_schema_fixtures.rs源码 ↗
entrypointmanual tool run

可以把这个文件理解成一个“生成文件按钮”。运行它时,它先看看命令行有没有给一个目录。如果给了,就把那个目录当作 schema 的根目录;如果没给,就默认使用当前 Rust 包目录下的 schema 文件夹。然后它把这个目录交给 codex_hooks::write_schema_fixtures 去真正写入 fixture。这里的 fixture 可以理解成“测试用的固定样本文件”,常用来确认 schema,也就是数据格式说明,是否稳定、正确。这个文件本身不关心具体写什么内容,也不直接操作复杂文件;它只负责决定“写到哪里”,再调用真正干活的函数。没有它,开发者就少了一个方便、统一的命令来刷新这些 schema 样本,容易出现不同人用不同方式生成文件的情况。

函数细节1
main3–9 ↗
fn main() -> anyhow::Result<()>

作用:这是这个命令行工具的入口。它决定 schema fixture 要写到哪个目录,然后把实际生成工作交给 codex_hooks::write_schema_fixtures。

数据流:进去的是运行程序时带的命令行参数,以及编译时记录的包目录。它先读取第一个命令行参数:如果有,就转成路径;如果没有,就拼出默认的 schema 目录。最后它把这个路径传给 write_schema_fixtures,并把成功或失败结果原样返回。

调用关系:程序启动时会先进入 main。main 会调用系统提供的 args_os 读取命令行参数,然后调用外部的 write_schema_fixtures 做真正的文件生成;它自己只负责准备路径和传递结果。

调用图:外部调用 2 个(write_schema_fixtures, args_os)。

config/examples/generate-proto.rs源码 ↗
entrypoint手动或构建脚本生成 proto 代码时

这个文件解决的是“接口定义怎么变成可用代码”的问题。Protocol Buffers,简称 proto,可以理解成一份机器能读的“接口说明书”,里面写清楚消息长什么样、服务有哪些方法。程序运行时不能直接靠这份说明书干活,所以需要先把它翻译成 Rust 代码。这个工具启动后先看命令行有没有传入 proto 文件所在目录;如果没有,就提示正确用法并退出。拿到目录后,它会找到其中的 codex.thread_config.v1.proto 文件,然后交给 tonic_prost_build 这个生成器,把客户端和服务端都会用到的 gRPC 代码生成出来,并放回同一个目录。它像一个“代码复印机”:输入一份接口蓝图,输出 Rust 能直接编译使用的零件。

函数细节1
main3–19 ↗
fn main() -> Result<(), Box<dyn std::error::Error>>

作用:这是整个小工具的入口。它读取用户给的 proto 目录,把里面指定的 proto 文件编译成 Rust 代码;如果用户没给目录,就告诉用户该怎么运行。

数据流:进去的是命令行参数,也就是用户在运行 generate-proto 后面写的目录路径。函数先取第一个参数;没有的话就打印“Usage: generate-proto <proto-dir>”并退出。拿到目录后,它拼出 codex.thread_config.v1.proto 的完整路径,再配置代码生成器:生成客户端代码、生成服务端代码、输出到这个目录。最后调用编译步骤;成功就返回 Ok,失败就把错误交给外层显示或处理。

调用关系:它是这个文件唯一的执行入口,程序一启动就从这里开始。它自己不真正解析 proto 文件,而是把这件事交给外部库 tonic_prost_build 的 configure 和 compile_protos 来做;自己主要负责检查参数、拼路径、设置生成选项。

调用图:外部调用 5 个(from, eprintln!, args, exit, configure)。

独立工具二进制程序

此组涵盖可直接调用的小工具,用于打补丁、搜索、桥接、调试和示例执行。

apply-patch/src/main.rs源码 ↗
entrypointstartup

这个文件很小,但很关键:它相当于一扇大门。操作系统启动这个程序时,首先会进入这里的 main 函数。这里没有自己解析参数、读文件或修改补丁,而是直接调用 codex_apply_patch::main(),把所有真正的活儿交给外部库里的主函数。这样做的好处是,命令行外壳和核心逻辑分开:这个文件只负责“程序从这里开始”,核心库负责“具体怎么应用补丁”。函数返回类型里的 ! 表示“不会正常返回”,也就是说程序最后通常会直接退出,而不是把控制权再交回这里。

函数细节1
main1–3 ↗
fn main() -> !

作用:这是整个 apply-patch 可执行程序的启动点。有人运行这个命令时,系统就从这里开始执行,然后它立刻把工作交给真正的补丁应用逻辑。

数据流:进去的是操作系统启动程序时提供的运行环境,比如命令行参数和当前目录;这个函数自己不读取也不处理这些信息,而是调用 codex_apply_patch::main();出来的结果是程序进入核心实现并最终退出,这个函数本身不会正常返回。

调用关系:main 位在最外层,负责接住程序启动这一刻。它只调用外部的 codex_apply_patch::main(),像前台接待把来访者直接带到真正办事的窗口;后续所有补丁处理流程都由那个外部主函数接手。

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

apply-patch/src/standalone_executable.rs源码 ↗
entrypoint命令启动到退出

这个文件解决的是“怎么把库里的 apply_patch 能力变成一个独立小工具”的问题。它像一个前台接待员:先确认用户把补丁交过来了,而且格式能读;如果没给参数,就去标准输入里读,比如支持 echo 'PATCH' | apply_patch 这种用法;如果参数太多,就拒绝,避免程序猜错用户意思。接着它找到当前工作目录,因为补丁要应用到哪里的文件,取决于你在哪个目录运行命令。然后它创建一个 Tokio 运行时,Tokio 可以理解成 Rust 里跑异步任务的“小调度器”,因为真正的补丁应用函数是异步的。最后它把补丁、当前目录、标准输出、标准错误和本地文件系统交给核心的 apply_patch 函数。成功就返回 0,失败就返回非 0,这样脚本或自动化工具能清楚判断结果。

函数细节2
main4–7 ↗
fn main() -> !

作用:这是独立命令行程序真正的入口。它只做一件事:运行主流程,然后把得到的数字退出码交给操作系统。

数据流:程序启动后进入 main → 它调用 run_main 得到一个退出码,比如成功是 0、错误是 1 或 2 → 它调用进程退出函数,用这个退出码结束整个程序,不再返回。

调用关系main 站在最外层,是操作系统启动这个工具时最先进入的地方。它不处理补丁细节,而是把所有实际工作交给 run_main,最后负责正式退出进程。

调用图:调用 1 个内部函数(run_main);外部调用 1 个(exit)。

run_main11–83 ↗
fn run_main() -> i32

作用:这是命令行工具的主流程。它负责收集补丁内容、检查用法是否正确、准备运行环境,然后调用真正的补丁应用功能。

数据流:它先读取命令行参数;如果有一个参数,就把它当作补丁文本,并要求它必须是 UTF-8,也就是正常文本编码;如果没有参数,就从标准输入读取补丁;如果什么都没读到或参数太多,就打印提示并返回错误码。之后它取得当前目录,创建 Tokio 异步运行时,再把补丁、目录、输出通道和本地文件系统交给 apply_patch。补丁应用成功时,它刷新标准输出并返回 0;准备过程或应用过程失败时,它打印错误并返回非 0。

调用关系run_mainmain 调用,是这个文件里真正串起所有步骤的地方。它自己不修改文件,而是负责把用户输入和运行环境整理好,再交给外部的 apply_patch 做核心工作;同时它也负责把各种早期错误变成清楚的错误信息和退出码。

调用图:调用 1 个内部函数(current_dir);被 1 处调用(main);外部调用 8 个(new, apply_patch, eprintln!, args_os, stderr, stdin, stdout, new_current_thread)。

file-search/src/main.rs源码 ↗
entrypointstartup 和结果输出阶段

这个文件像一个前台接待员:用户运行命令后,先到这里报到。它用 clap 这个命令行参数解析工具,把用户输入的选项读成程序能理解的配置。然后它创建一个 StdioReporter,也就是“往标准输入输出窗口汇报结果的人”。如果用户要求 JSON,它就一行一条打印机器容易读的 JSON;如果用户要求计算匹配位置,并且输出的是正常终端,它会把文件名里匹配到的字符加粗显示;否则就只打印文件路径。真正的找文件工作不在这里做,而是交给 codex_file_search::run_main。这个文件重要的地方在于:它决定了工具怎么启动、怎么把结果说给用户听,也避免把搜索逻辑和屏幕输出混在一起。

函数细节4
main12–20 ↗
async fn main() -> anyhow::Result<()>

作用:这是整个命令行程序的入口。用户一运行 file-search,程序就从这里开始,先读参数,再准备结果汇报器,最后启动真正的搜索流程。

数据流:进去的是用户在命令行输入的参数,以及当前标准输出是不是终端的信息。它把参数解析成 Cli 配置,根据配置决定是否输出 JSON、是否显示匹配字符的位置,然后把 Cli 和 StdioReporter 交给 run_main。出来的是一个成功或失败的结果;搜索过程本身由 run_main 接着完成。

调用关系:main 是最外层的启动按钮。它调用 Cli::parse 读取命令行参数,调用 stdout 相关能力判断输出环境,然后把后续工作交给 run_main;后面的搜索结果再通过它创建的 StdioReporter 打印出来。

调用图:调用 1 个内部函数(run_main);外部调用 2 个(parse, stdout)。

StdioReporter::report_match28–62 ↗
fn report_match(&self, file_match: &FileMatch)

作用:每找到一个匹配的文件,这个函数就负责把它显示出来。它会根据用户选项选择三种说法:JSON、带加粗提示的路径,或者普通路径。

数据流:进去的是一个 FileMatch,也就是一次搜索命中的文件信息,里面有文件路径,可能还有匹配字符的位置。它先看是否要输出 JSON:要的话就把整个匹配结果转成 JSON 字符串打印;否则如果允许显示匹配位置,就遍历文件路径的每个字符,把命中的字符用终端加粗控制码包起来;如果都不需要,就只打印路径。出来的是写到屏幕上的一行结果,不改动搜索数据本身。

调用关系:它实现了 Reporter 这套汇报接口,所以真正搜索流程 run_main 找到结果时,会通过 Reporter 调到这里。它不负责找文件,只负责把已经找到的 FileMatch 翻译成用户看得懂、或机器能继续处理的输出。

调用图:外部调用 3 个(print!, println!, to_string)。

StdioReporter::warn_matches_truncated64–75 ↗
fn warn_matches_truncated(&self, total_match_count: usize, shown_match_count: usize)

作用:当匹配结果太多、程序只展示了一部分时,这个函数负责提醒用户。这样用户不会误以为屏幕上看到的就是全部结果。

数据流:进去的是总匹配数量和实际展示数量。它如果处在 JSON 输出模式,就打印一条表示结果被截断的 JSON;否则就往错误输出里打印一段人能读懂的警告,告诉用户只显示了多少条、总共有多少条,并建议缩小搜索条件或调大限制。出来的是一条提示信息,不改变匹配列表。

调用关系:它也是 Reporter 接口的一部分。搜索主流程在发现结果数量超过显示上限时,会调用它来补上一句提醒;它内部只负责组织提示文字或 JSON,并交给 println 或 eprintln 输出。

调用图:外部调用 4 个(eprintln!, json!, println!, to_string)。

StdioReporter::warn_no_search_pattern77–82 ↗
fn warn_no_search_pattern(&self, search_directory: &Path)

作用:当用户没有提供搜索关键词时,这个函数负责说明程序接下来会做什么。它会告诉用户:现在不是按关键词搜索,而是在展示当前目录内容。

数据流:进去的是要展示的搜索目录路径。它把路径转成人能看的文字,然后往错误输出打印一条说明。出来的是一条提示信息;目录和搜索状态都不会被它修改。

调用关系:搜索主流程发现用户没有给搜索模式时,会通过 Reporter 调用它。它的位置像一个说明牌,在程序继续列出目录内容前,先避免用户困惑。

调用图:外部调用 1 个(eprintln!)。

file-search/src/cli.rs源码 ↗
configstartup

这个文件像一张“点菜单”:它把文件搜索工具支持的所有命令行选项写清楚。程序启动时,会用 clap(一个帮 Rust 程序解析命令行参数的工具)把用户输入的文字变成一个 Cli 结构体。里面包括是否用 JSON 输出、最多返回多少条结果、从哪个目录开始搜、要不要计算匹配位置、用几个工作线程、排除哪些文件模式,以及真正要搜索的关键词。这里还用 NonZero<usize> 表示“必须是大于 0 的数字”,避免用户把结果数量或线程数设成 0 这种没法正常工作的值。它本身不执行搜索,只负责把用户的意图整理成程序后面能直接使用的配置。

file-search/src/lib.rs源码 ↗
domain_logicrequest handling / background search session

这个文件像一个文件搜索小引擎。它先定义搜索结果长什么样,比如路径、分数、是文件还是文件夹;再定义搜索选项,比如最多返回多少条、排除哪些文件、用几个线程、要不要遵守 .gitignore。真正工作时,它会开两个后台工人:一个负责走遍目录,把发现的路径喂给匹配器;另一个负责接收用户的新搜索词,算出当前最匹配的前几项,并通过 reporter(报告器,也就是回调通知对象)把结果送出去。它还提供一次性搜索 run,也提供可反复更新查询词的 FileSearchSession。特别重要的是,它处理了取消、关闭、搜索完成通知,以及 gitignore 的细节,避免父目录里很宽泛的忽略规则意外把项目文件全藏起来。

函数细节42
FileMatch::full_path71–73 ↗
fn full_path(&self) -> PathBuf

作用:把搜索结果里的“根目录”和“相对路径”拼成真正能打开的完整路径。调用者拿到 FileMatch 后,如果想访问磁盘上的文件,就会用它。

数据流:进去的是一个 FileMatch,里面有 root 和 path → 它把 path 接到 root 后面 → 出来一个完整的 PathBuf,不改动原来的搜索结果。

调用关系:它是 FileMatch 这个结果对象上的小工具。搜索流程产出相对路径,展示时通常够用;真正要打开文件时,就由这个函数把路径补全。

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

file_name_from_path77–82 ↗
fn file_name_from_path(path: &str) -> String

作用:从一串路径文字里取最后的文件名。比如 foo/bar.txt 会变成 bar.txt,适合界面只想显示短名字时用。

数据流:进去的是路径字符串 → 它尝试按系统路径规则找最后一段 → 成功就返回最后一段,失败就原样返回整串文字。

调用关系:这是独立的小辅助函数。测试会检查它能取出文件名,也能在空字符串这类特殊情况里安全返回。

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

FileSearchOptions::default116–126 ↗
fn default() -> Self

作用:给搜索准备一套默认设置,让调用者不必每次都填完整参数。默认返回 20 条、用 2 个线程、遵守 gitignore、不计算高亮位置。

数据流:进去没有参数 → 它构造 limit、exclude、threads、compute_indices、respect_gitignore 这些字段 → 出来一个可直接使用的 FileSearchOptions。

调用关系:很多测试和会话创建都会用它当起点,再按需要改少数字段。它保证搜索在没有特别配置时也有合理行为。

调用图:被 6 处调用(dropping_session_does_not_cancel_siblings_with_shared_cancel_flag, session_accepts_query_updates_after_walk_complete, session_emits_complete_when_query_changes_with_no_matches, session_emits_updates_when_query_changes, session_scanned_file_count_is_monotonic_across_queries, session_streams_updates_before_walk_complete);外部调用 2 个(new, new)。

FileSearchSession::update_query143–148 ↗
fn update_query(&self, pattern_text: &str)

作用:给一个正在运行的搜索会话换搜索词。它很轻量,不会重新扫目录,而是通知后台匹配工人重新算结果。

数据流:进去的是新的搜索文字 → 它把这段文字包装成 QueryUpdated 消息发到工作队列 → 后台 matcher_worker 收到后更新匹配规则并产出新快照。

调用关系:它是外部使用实时搜索会话时最常调用的入口。界面每次用户多输入一个字,都可以通过它把新查询交给后台。

调用图:被 1 处调用(update_query);外部调用 1 个(QueryUpdated)。

FileSearchSession::drop152–155 ↗
fn drop(&mut self)

作用:当搜索会话没人用了,它负责通知后台线程该收工。这样不会留下偷偷运行的扫描或匹配任务。

数据流:进去的是即将销毁的 FileSearchSession → 它把内部 shutdown 标志设为 true,并发送 Shutdown 消息 → 后台工作循环看到后退出。

调用关系:它在 Rust 自动释放对象时触发。它不会随便改共享的 cancel_flag,所以一个会话结束不会误伤别的兄弟会话。

create_session158–211 ↗
fn create_session(
    search_directories: Vec<PathBuf>,
    options: FileSearchOptions,
    reporter: Arc<dyn SessionReporter>,
    cancel_flag: Option<Arc<AtomicBool>>,
) -> anyhow::Result<FileSearc

作用:创建一个可持续更新搜索词的搜索会话。它把目录扫描工人、模糊匹配工人、通知通道和报告器都搭起来。

数据流:进去的是搜索目录、搜索选项、结果报告器和可选取消标志 → 它检查至少有一个目录,建立排除规则,创建 Nucleo 匹配器和消息通道,再启动两个线程 → 出来一个 FileSearchSession。

调用关系:run 会用它做一次性搜索,实时搜索测试也直接用它。它把后续工作分给 walker_worker 扫目录,分给 matcher_worker 算匹配和发通知。

调用图:调用 1 个内部函数(build_override_matcher);被 7 处调用(run, dropping_session_does_not_cancel_siblings_with_shared_cancel_flag, session_accepts_query_updates_after_walk_complete, session_emits_complete_when_query_changes_with_no_matches, session_emits_updates_when_query_changes, session_scanned_file_count_is_monotonic_across_queries, session_streams_updates_before_walk_complete);外部调用 6 个(new, new, new, bail!, unbounded, spawn)。

run_main219–287 ↗
async fn run_main(
    Cli {
        pattern,
        limit,
        cwd,
        compute_indices,
        json: _,
        exclude,
        threads,
    }: Cli,
    reporter: T,
) -> anyhow::Result<(

作用:这是命令行模式的主流程。它读取命令行参数,决定是列目录,还是按搜索词搜索并打印结果。

数据流:进去的是 Cli 参数和一个 Reporter → 它确定工作目录;如果没有搜索词,就提醒用户并执行系统列目录命令;如果有搜索词,就调用 run 搜索 → 最后把匹配项逐条交给 reporter,并在结果被截断时发警告。

调用关系:它被真正的 main 调用,是命令行入口和搜索核心之间的桥。搜索本身交给 run,输出样式交给外部传入的 Reporter。

调用图:调用 1 个内部函数(run);被 1 处调用(main);外部调用 7 个(report_match, warn_matches_truncated, warn_no_search_pattern, new, current_dir, inherit, vec!)。

run291–307 ↗
fn run(
    pattern_text: &str,
    roots: Vec<PathBuf>,
    options: FileSearchOptions,
    cancel_flag: Option<Arc<AtomicBool>>,
) -> anyhow::Result<FileSearchResults>

作用:做一次“给我结果就结束”的搜索。它适合命令行或测试,不需要调用者自己管理实时会话。

数据流:进去的是搜索词、根目录列表、选项和可选取消标志 → 它创建内部 RunReporter,再用 create_session 启动会话并发送查询 → 等后台报告完成后,取出最后一份快照 → 出来 FileSearchResults。

调用关系:run_main 和多个测试都调用它。它本质上是 create_session 的简化包装,把异步更新式搜索变成同步等待式搜索。

调用图:调用 1 个内部函数(create_session);被 5 处调用(run_main, git_repo_still_respects_local_gitignore_when_enabled, parent_gitignore_outside_repo_does_not_hide_repo_files, run_returns_directory_matches_for_query, run_returns_matches_for_query);外部调用 2 个(new, default)。

sort_matches311–316 ↗
fn sort_matches(matches: &mut [(u32, String)])

作用:测试用的小函数,把一组带分数的匹配结果排好序。规则是分数高的在前,分数一样时路径字母顺序靠前的在前。

数据流:进去的是可修改的匹配数组 → 它使用统一的比较规则原地排序 → 出来的还是同一个数组,但顺序已改变。

调用关系:它只在测试里使用,目的是验证排序规则没有写错。实际比较逻辑来自 cmp_by_score_desc_then_path_asc。

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

cmp_by_score_desc_then_path_asc320–333 ↗
fn cmp_by_score_desc_then_path_asc(
    score_of: FScore,
    path_of: FPath,
) -> impl FnMut(&T, &T) -> std::cmp::Ordering

作用:生成一个通用排序规则:先按分数从高到低,再按路径从小到大。这样不同类型的数据只要能取出分数和路径,都能复用这套规则。

数据流:进去的是两个取值函数,一个取分数,一个取路径 → 它返回一个比较器闭包 → 排序函数调用这个闭包时,就能知道两个项目谁该排前面。

调用关系:sort_matches 在测试中使用它。它也可以被其他需要同样排序规则的代码复用,避免各处写出不一致的排序。

create_pattern336–343 ↗
fn create_pattern(pattern: &str) -> Pattern

作用:测试用的辅助函数,用一段文字创建模糊匹配模式。这里的模糊匹配就是不要求完全连续相同,也能判断像不像。

数据流:进去的是模式字符串 → 它设置忽略大小写、智能规范化、模糊匹配这些选项 → 出来一个 Pattern,测试可以拿它打分。

调用关系:verify_score_is_none_for_non_match 调用它,专门确认完全不相关的文字不会得到匹配分数。

调用图:被 1 处调用(verify_score_is_none_for_non_match);外部调用 1 个(new)。

build_override_matcher364–378 ↗
fn build_override_matcher(
    search_directory: &Path,
    exclude: &[String],
) -> anyhow::Result<Option<ignore::overrides::Override>>

作用:把用户传入的排除规则变成目录遍历器能理解的过滤器。没有排除规则时,它什么也不建,避免多余开销。

数据流:进去的是搜索根目录和 exclude 字符串列表 → 它把每条规则加上 ! 前缀,交给 ignore 库构建覆盖规则 → 出来 Some 过滤器,或者在没有规则时出来 None。

调用关系:create_session 在启动扫描前调用它。walker_worker 后面会拿这个过滤器决定哪些路径不要扫进搜索池。

调用图:被 1 处调用(create_session);外部调用 2 个(new, format!)。

get_file_path380–397 ↗
fn get_file_path(path: &'a Path, search_directories: &[PathBuf]) -> Option<(usize, &'a str)>

作用:把一个完整路径换算成“属于哪个搜索根目录,以及相对这个根目录的路径”。这能让结果显示得短,也能支持多个根目录。

数据流:进去的是一个完整路径和一组搜索根目录 → 它找出能包含这个路径的最合适根目录,尤其会优先选择更深、更具体的根 → 出来根目录编号和相对路径字符串;如果不能转换成文字或不属于任何根,就返回 None。

调用关系:walker_worker 用它决定哪些路径要送进匹配器,matcher_worker 用它把匹配器里存的完整路径还原成结果里的相对路径。

调用图:外部调用 2 个(strip_prefix, iter)。

walker_worker411–481 ↗
fn walker_worker(
    inner: Arc<SessionInner>,
    override_matcher: Option<ignore::overrides::Override>,
    injector: Injector<Arc<str>>,
)

作用:后台扫目录的工人。它像仓库盘点员一样走遍搜索目录,把每个发现的文件或文件夹交给模糊匹配器。

数据流:进去的是共享会话信息、可选排除过滤器和 Nucleo 注入器 → 它配置目录遍历规则,包括隐藏文件、软链接、gitignore 行为和线程数;遍历时把路径放入匹配器 → 扫完或被取消后发送 WalkComplete 消息。

调用关系:create_session 启动它。它不直接算搜索结果,只负责把候选路径源源不断喂给 matcher_worker 使用的 Nucleo 引擎。

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

matcher_worker483–604 ↗
fn matcher_worker(
    inner: Arc<SessionInner>,
    work_rx: Receiver<WorkSignal>,
    mut nucleo: Nucleo<Arc<str>>,
) -> anyhow::Result<()>

作用:后台算匹配结果的工人。它接收新搜索词、接收目录扫描进度通知,然后定期产出当前最好的结果快照。

数据流:进去的是共享会话信息、工作消息接收器和 Nucleo 匹配器 → 它根据 QueryUpdated 改搜索模式,根据 NucleoNotify 或 WalkComplete 安排刷新;刷新时取前 N 个匹配项,补上路径、分数、类型和可选高亮下标 → 通过 reporter 发 on_update,空闲或退出时发 on_complete。

调用关系:create_session 启动它。FileSearchSession::update_query、walker_worker 和 Nucleo 的通知都会给它发消息,它是实时搜索结果从“候选路径”变成“用户可看的列表”的核心环节。

调用图:外部调用 3 个(new, never, select!)。

RunReporter::on_update613–617 ↗
fn on_update(&self, snapshot: &FileSearchSnapshot)

作用:保存一次性搜索过程中收到的最新结果快照。run 最后会从这里取结果。

数据流:进去的是 FileSearchSnapshot 引用 → 它拿写锁,把内部保存的快照替换成这份克隆 → 没有返回值,但内部状态更新了。

调用关系:matcher_worker 每次有新结果时调用它。RunReporter 把实时通知变成 run 可以等待和读取的一份最终数据。

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

RunReporter::on_complete619–625 ↗
fn on_complete(&self)

作用:标记一次性搜索已经完成,并叫醒正在等待的线程。没有它,run 可能一直卡着等结果。

数据流:进去没有额外数据 → 它把 completed 标志设为 true,并通知条件变量(条件变量就是线程睡觉等消息的铃铛)→ 等待中的 wait_for_complete 会醒来。

调用关系:matcher_worker 在搜索空闲、扫描完成、取消或退出时调用它。它和 wait_for_complete 配成一对,一个发完成信号,一个等完成信号。

RunReporter::wait_for_complete629–641 ↗
fn wait_for_complete(&self) -> FileSearchSnapshot

作用:让 run 停下来等后台搜索结束,然后拿到最后一份结果快照。

数据流:进去的是 RunReporter 自己 → 它拿锁检查 completed;没完成就睡在条件变量上,醒来后再检查;完成后读取 snapshot → 出来 FileSearchSnapshot 的克隆。

调用关系:run 调用它,把后台线程的异步结果变成普通函数返回值。RunReporter::on_complete 负责把它叫醒。

tests::verify_score_is_none_for_non_match661–669 ↗
fn verify_score_is_none_for_non_match()

作用:测试完全不匹配的文字不会得到分数。这样可以防止搜索把无关文件误认为有结果。

数据流:进去没有外部输入 → 它创建 haystack“hello”和模式“zzz”,让匹配器打分 → 断言结果是 None。

调用关系:它调用 create_pattern 创建测试模式,用来保护底层模糊匹配行为的基本预期。

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

tests::tie_breakers_sort_by_path_when_scores_equal672–689 ↗
fn tie_breakers_sort_by_path_when_scores_equal()

作用:测试当分数一样时,路径名会按字母顺序排序。这样用户看到的结果顺序稳定,不会每次乱跳。

数据流:进去没有外部输入 → 它准备三条假结果,调用 sort_matches 排序 → 断言排序后是高分优先、同分按路径升序。

调用关系:它直接验证 sort_matches,也间接验证 cmp_by_score_desc_then_path_asc 生成的比较规则。

调用图:调用 1 个内部函数(sort_matches);外部调用 2 个(assert_eq!, vec!)。

tests::file_name_from_path_uses_basename692–694 ↗
fn file_name_from_path_uses_basename()

作用:测试 file_name_from_path 能从普通路径里取出文件名。比如 foo/bar.txt 应该只剩 bar.txt。

数据流:进去没有外部输入 → 它调用 file_name_from_path 处理一段路径 → 断言结果是最后一段文件名。

调用关系:它保护 file_name_from_path 的主要用途,避免以后改动时破坏显示短文件名的能力。

调用图:外部调用 1 个(assert_eq!)。

tests::file_name_from_path_falls_back_to_full_path697–699 ↗
fn file_name_from_path_falls_back_to_full_path()

作用:测试特殊路径取不到文件名时不会崩溃,而是退回原字符串。这里用空字符串做例子。

数据流:进去没有外部输入 → 它调用 file_name_from_path 处理空字符串 → 断言返回仍然是空字符串。

调用关系:它补充覆盖 file_name_from_path 的兜底行为,保证边界情况安全。

调用图:外部调用 1 个(assert_eq!)。

tests::RecordingReporter::wait_until710–736 ↗
fn wait_until(
            &self,
            mutex: &Mutex<T>,
            cv: &Condvar,
            timeout: Duration,
            mut predicate: F,
        ) -> bool

作用:测试用的等待工具:一直等到某个条件变真,或者超时。它避免测试靠盲目 sleep 猜时间。

数据流:进去的是一个互斥锁、一个条件变量、超时时长和判断函数 → 它反复检查状态,不满足就带超时等待通知 → 出来 true 表示条件达成,false 表示等到超时。

调用关系:RecordingReporter::wait_for_complete 和 wait_for_updates_at_least 都用它。它让异步搜索测试更稳定。

调用图:被 2 处调用(wait_for_complete, wait_for_updates_at_least);外部调用 2 个(wait_timeout, now)。

tests::RecordingReporter::wait_for_complete738–745 ↗
fn wait_for_complete(&self, timeout: Duration) -> bool

作用:测试里等待至少收到一次完成通知。用于确认后台搜索真的走到完成状态。

数据流:进去的是超时时长 → 它调用 wait_until 观察 complete_times 列表是否非空 → 出来布尔值表示是否等到了完成。

调用关系:多个会话测试用它等 matcher_worker 发 on_complete。它依赖 RecordingReporter::on_complete 往列表里记时间。

调用图:调用 1 个内部函数(wait_until)。

tests::RecordingReporter::clear746–749 ↗
fn clear(&self)

作用:清空测试报告器里已经记录的更新和完成时间。这样同一个会话可以继续测下一次查询,不被旧记录干扰。

数据流:进去没有额外数据 → 它分别锁住 updates 和 complete_times 并清空 → 没有返回值,但测试记录被重置。

调用关系:查询变化相关的测试会先等一次完成,再 clear,然后发第二个查询,确保后面的断言只看新事件。

tests::RecordingReporter::updates751–753 ↗
fn updates(&self) -> Vec<FileSearchSnapshot>

作用:取出测试报告器记录过的所有更新快照。测试用它检查搜索过程中到底发了哪些结果。

数据流:进去没有额外数据 → 它锁住 updates,把里面的快照列表克隆出来 → 出来一个独立的 Vec<FileSearchSnapshot>。

调用关系:多个测试在搜索后调用它,检查是否有未完成时的流式更新、是否包含目标文件、更新次数是否正确。

tests::RecordingReporter::wait_for_updates_at_least755–759 ↗
fn wait_for_updates_at_least(&self, min_len: usize, timeout: Duration) -> bool

作用:测试里等待更新数量达到指定个数。它用来确认改了查询词之后,确实又收到新结果。

数据流:进去的是最少更新数和超时时长 → 它调用 wait_until 观察 updates.len() 是否达到要求 → 出来 true 或 false。

调用关系:session_accepts_query_updates_after_walk_complete 用它验证扫描完成后仍然能继续处理新查询。

调用图:调用 1 个内部函数(wait_until)。

tests::RecordingReporter::snapshot761–768 ↗
fn snapshot(&self) -> FileSearchSnapshot

作用:拿到测试报告器记录的最后一份快照;如果还没有更新,就返回默认空快照。方便测试看当前搜索状态。

数据流:进去没有额外数据 → 它锁住 updates,取最后一个并克隆;如果列表为空就用默认值 → 出来 FileSearchSnapshot。

调用关系:许多会话测试用它读取当前 scanned_file_count、matches 等字段,判断搜索行为是否符合预期。

tests::RecordingReporter::on_update772–776 ↗
fn on_update(&self, snapshot: &FileSearchSnapshot)

作用:测试报告器收到搜索更新时,把快照记录下来并叫醒等待更新的测试线程。

数据流:进去的是 FileSearchSnapshot → 它把快照克隆进 updates 列表,再通知 update_cv → 没有返回值,但测试能看到新更新。

调用关系:matcher_worker 在测试运行时调用它。它和 wait_for_updates_at_least 配合,让测试能等到异步更新。

调用图:外部调用 2 个(notify_all, clone)。

tests::RecordingReporter::on_complete778–784 ↗
fn on_complete(&self)

作用:测试报告器收到完成通知时,记录当前时间并叫醒等待完成的测试线程。

数据流:进去没有额外数据 → 它把当前时间放入 complete_times,再通知 complete_cv → 没有返回值,但等待者会被唤醒。

调用关系:matcher_worker 调用它表示一次查询已空闲或结束。wait_for_complete 靠它判断测试可以继续断言。

调用图:外部调用 2 个(notify_all, now)。

tests::create_temp_tree787–794 ↗
fn create_temp_tree(file_count: usize) -> TempDir

作用:给测试快速造一个临时目录,里面放指定数量的文件。这样测试不用依赖真实项目文件。

数据流:进去的是文件数量 → 它创建临时目录,然后写入 file-0000.txt 这类文件 → 出来 TempDir,测试结束后临时目录会自动清理。

调用关系:大量搜索测试用它制造可控的文件树,方便验证扫描、匹配、取消和流式更新。

调用图:外部调用 3 个(format!, write, tempdir)。

tests::session_scanned_file_count_is_monotonic_across_queries797–819 ↗
fn session_scanned_file_count_is_monotonic_across_queries()

作用:测试扫描到的文件数量不会因为换搜索词而倒退。用户输入变化时,后台扫描进度应该一直往前。

数据流:进去没有外部输入 → 它造 200 个文件,创建会话,先查 file-00 再查 file-01,分别取快照 → 断言后面的 scanned_file_count 不小于前面的。

调用关系:它调用 create_session 和 FileSearchOptions::default,并通过 RecordingReporter 观察 matcher_worker 发出的快照。

调用图:调用 2 个内部函数(default, create_session);外部调用 8 个(new, from_millis, from_secs, assert!, default, create_temp_tree, sleep, vec!)。

tests::session_streams_updates_before_walk_complete822–839 ↗
fn session_streams_updates_before_walk_complete()

作用:测试搜索不是等全部目录扫完才给结果,而是边扫边返回更新。这样大目录里用户能更快看到东西。

数据流:进去没有外部输入 → 它造 600 个文件,启动会话并查询 file-0,等待完成后检查所有更新 → 断言至少有一份快照显示 walk_complete 为 false。

调用关系:它验证 walker_worker 和 matcher_worker 的配合:扫描还没结束时,匹配器也能先报告已有结果。

调用图:调用 2 个内部函数(default, create_session);外部调用 6 个(new, from_secs, assert!, default, create_temp_tree, vec!)。

tests::session_accepts_query_updates_after_walk_complete842–870 ↗
fn session_accepts_query_updates_after_walk_complete()

作用:测试目录扫完之后,会话还能继续接受新的搜索词。也就是说扫描完成不等于搜索会话报废。

数据流:进去没有外部输入 → 它创建 alpha.txt 和 beta.txt,先查 alpha 并等完成,再查 beta → 等新更新出现后,断言最后结果里包含 beta.txt。

调用关系:它直接调用 FileSearchSession::update_query 两次,验证 matcher_worker 在收到 WalkComplete 后仍会处理 QueryUpdated。

调用图:调用 2 个内部函数(default, create_session);外部调用 6 个(new, assert!, default, write, tempdir, vec!)。

tests::session_emits_complete_when_query_changes_with_no_matches873–898 ↗
fn session_emits_complete_when_query_changes_with_no_matches()

作用:测试即使查询没有任何匹配,也会发完成通知。否则界面可能一直显示“还在搜”。

数据流:进去没有外部输入 → 它创建两个文件,查询 asdf 并等完成,确认结果为空;清空记录后再查询 asdfa → 再次确认能等到完成且有更新。

调用关系:它验证 matcher_worker 对“零结果查询”的处理,特别是查询变化后仍然保证 on_complete 至少被调用一次。

调用图:调用 2 个内部函数(default, create_session);外部调用 7 个(new, assert!, assert_eq!, default, write, tempdir, vec!)。

tests::dropping_session_does_not_cancel_siblings_with_shared_cancel_flag901–932 ↗
fn dropping_session_does_not_cancel_siblings_with_shared_cancel_flag()

作用:测试关闭一个搜索会话,不会把共享同一个取消标志的另一个会话也取消掉。这样多个搜索任务可以安全共存。

数据流:进去没有外部输入 → 它创建两个目录和同一个 cancel_flag,启动两个会话,发出两个查询;随后丢弃第一个会话 → 断言第二个会话仍能完成。

调用关系:它验证 FileSearchSession::drop 只设置自己的 shutdown,而不是乱改共享 cancel_flag。

调用图:调用 2 个内部函数(default, create_session);外部调用 9 个(new, new, from_millis, from_secs, assert_eq!, default, create_temp_tree, sleep, vec!)。

tests::session_emits_updates_when_query_changes935–958 ↗
fn session_emits_updates_when_query_changes()

作用:测试搜索词改变时,即使结果还是空,也会发出新的更新和完成。这样界面知道这次输入已经处理过了。

数据流:进去没有外部输入 → 它创建文件树,先查询一个无结果词并等完成,清空记录;再查询更长的无结果词 → 断言完成了,并且更新数量正好是一条。

调用关系:它通过 create_session、update_query 和 RecordingReporter 检查 matcher_worker 对连续无结果查询的通知行为。

调用图:调用 2 个内部函数(default, create_session);外部调用 7 个(new, from_secs, assert!, assert_eq!, default, create_temp_tree, vec!)。

tests::run_returns_matches_for_query961–986 ↗
fn run_returns_matches_for_query()

作用:测试一次性 run 能为普通文件查询返回匹配结果。它保证最常见的搜索入口可用。

数据流:进去没有外部输入 → 它创建 40 个临时文件,用 run 搜索 file-000 → 断言结果非空、总匹配数不少于展示数,并包含 file-0000.txt。

调用关系:它覆盖 run 到 create_session,再到 walker_worker 和 matcher_worker 的完整路径。

调用图:调用 1 个内部函数(run);外部调用 5 个(new, new, assert!, create_temp_tree, vec!)。

tests::run_returns_directory_matches_for_query989–1013 ↗
fn run_returns_directory_matches_for_query()

作用:测试搜索结果不只包含文件,也能包含文件夹。用户搜索 docs/guides 这类目录名时应该能找到目录本身。

数据流:进去没有外部输入 → 它创建 docs/guides 目录和几个文件,用 run 搜索 guides → 断言结果里有 docs/guides,且类型是 Directory。

调用关系:它验证 matcher_worker 在生成 FileMatch 时会检查路径是文件还是目录,并正确填写 MatchType。

调用图:调用 1 个内部函数(run);外部调用 7 个(new, new, assert!, create_dir_all, write, tempdir, vec!)。

tests::cancel_exits_run1016–1039 ↗
fn cancel_exits_run()

作用:测试取消标志一开始就是 true 时,run 会尽快退出。这样长搜索可以被用户取消,不会卡住。

数据流:进去没有外部输入 → 它创建文件树,设置 cancel_flag 为 true,在新线程里调用 run → 主线程限时等待返回,并断言结果为空。

调用关系:它验证 walker_worker 和 matcher_worker 都会检查取消标志,也验证 RunReporter 最终能收到完成通知。

调用图:外部调用 8 个(new, new, default, from_secs, assert_eq!, create_temp_tree, channel, spawn)。

tests::parent_gitignore_outside_repo_does_not_hide_repo_files1048–1102 ↗
fn parent_gitignore_outside_repo_does_not_hide_repo_files()

作用:测试父目录里的 .gitignore 不会在没有真正 git 仓库时,把子目录项目里的文件全藏掉。这是为了避免用户家目录的忽略规则误伤搜索。

数据流:进去没有外部输入 → 它造一个 home/repo 结构,在父目录写一个忽略所有文件的 .gitignore,但不创建 .git;然后搜索 package 和 settings → 断言 repo 里的这些文件仍能被找到。

调用关系:它验证 walker_worker 使用 require_git(true) 的效果:只有在 git 上下文里才按 gitignore 规则处理。

调用图:调用 1 个内部函数(run);外部调用 7 个(new, new, assert!, create_dir_all, write, tempdir, vec!)。

tests::git_repo_still_respects_local_gitignore_when_enabled1105–1186 ↗
fn git_repo_still_respects_local_gitignore_when_enabled()

作用:测试真正的 git 仓库里,项目自己的 .gitignore 仍然会生效。既不能被父目录误伤,也不能完全无视本地规则。

数据流:进去没有外部输入 → 它创建 repo、.git、.gitignore 和几个文件;搜索 package 应该找到,搜索被忽略的 extensions.json 不该找到,搜索被白名单放回来的 settings.json 应该找到 → 分别断言这些结果。

调用关系:它验证 walker_worker 的 gitignore 处理边界:有 .git 时尊重仓库内规则,无 .git 时不乱读父目录规则。

调用图:调用 1 个内部函数(run);外部调用 7 个(new, new, assert!, create_dir_all, write, tempdir, vec!)。

stdio-to-uds/src/main.rs源码 ↗
entrypointstartup

这个程序需要用户在启动时告诉它一个“Unix socket 路径”。Unix socket 可以简单理解成同一台机器里两个程序之间通话用的一根本地管道。这个文件负责守门:先检查命令行参数是不是刚好一个。如果没有给路径,或者给多了,它会在屏幕上打印错误提示并退出,避免程序在不知道该连哪里时继续乱跑。参数正确后,它把这个路径转成程序内部好用的路径格式 PathBuf,然后调用 codex_stdio_to_uds::run。真正把标准输入输出和 socket 连接起来的工作不在这里做,而是在库函数里做。这里还用了 Tokio 的异步入口,异步可以理解成程序在等输入、等连接时不必傻等,能更适合处理这种输入输出任务。

函数细节1
main6–20 ↗
async fn main() -> anyhow::Result<()>

作用:这是程序启动后第一个运行的函数。它读取用户在命令行里给的 socket 路径,确认参数数量正确,然后启动真正的标准输入输出到 Unix socket 的转发流程。

数据流:进去的是操作系统传给程序的命令行参数。它跳过程序名,只看后面的参数:如果没有参数,就打印用法并退出;如果参数多于一个,也打印错误并退出;如果刚好一个,就把它变成路径对象,再交给 codex_stdio_to_uds::run。出来的是 run 的执行结果;如果中途出错,错误会作为程序结果返回。

调用关系:它是整个程序的入口和检查员。启动时它先调用系统提供的参数读取功能,必要时用错误输出打印提示并直接退出;参数没问题时,它不自己处理网络或输入输出,而是把 socket 路径交给外部的 run 函数,让后者完成核心工作。

调用图:外部调用 5 个(from, run, args_os, eprintln!, exit)。

tui/src/bin/md-events.rs源码 ↗
entrypoint开发调试时作为命令行程序运行,从启动到读完输入后结束

这个程序像一台“Markdown 透视机”。平时我们看到的是加粗、标题、列表这些排版效果,但程序内部会先把 Markdown 拆成很多事件,比如“标题开始了”“读到一段文字”“链接结束了”。这个文件做的事很直接:从标准输入读取全部文本,也就是你可以把一个 Markdown 文件管道传给它;如果读取失败,就把错误打印到错误输出并退出;如果读取成功,就交给 pulldown_cmark 这个 Markdown 解析库。解析库会逐个吐出事件,程序再把每个事件用调试格式打印出来。它本身不修改文本,也不生成网页,只是把隐藏的解析过程展示出来,适合调试和理解 Markdown 被怎样拆解。

函数细节1
main4–15 ↗
fn main()

作用:这是整个命令行工具的入口。它读取用户从标准输入传进来的 Markdown 内容,然后把解析出来的每一个事件打印到屏幕上;如果连输入都读不到,就报错并退出。

数据流:进去的是标准输入里的文本,比如用户用管道传入的 Markdown 文件内容。函数先创建一个空字符串,把标准输入全部读进去;读取失败时,它把错误信息写到错误输出并用失败状态结束程序。读取成功后,它用 pulldown_cmark::Parser::new 创建 Markdown 解析器,解析器会把文本变成一串事件,函数逐个拿到这些事件并打印出来。

调用关系:它是这个小工具唯一的运行入口,没有其他项目内函数调用它。它自己把流程从头走到尾:先调用标准库的 stdin 读取输入,再在出错时用 eprintln! 和 exit 处理失败,成功时把内容交给 pulldown_cmark 的 Parser::new,最后用 println! 输出每个解析事件。

调用图:外部调用 6 个(new, eprintln!, stdin, println!, new, exit)。

thread-manager-sample/src/main.rs源码 ↗
entrypointstartup 到 single request handling 再到 teardown

这个文件像一个最小可运行的“样板间”。用户在命令行传入提示词,或者通过管道从标准输入传入;程序先准备 Codex 需要的配置、认证、状态数据库、运行环境和线程存储,然后用 ThreadManager 开一个新线程。这里的“线程”不是普通操作系统线程,而是一次 Codex 会话的容器,里面会接收用户输入、产生模型回复、执行工具事件等。之后 run_turn 会把提示词提交进去,并一直等待事件。遇到可映射的过程事件,就用 item_event_to_server_notification 转成服务器通知格式,再按“每行一个 JSON”的方式输出,方便别的程序读取。它还很保守:如果 Codex 要求执行审批、补丁审批、权限、额外用户输入或动态工具调用,这个示例不会继续交互,而是直接报错退出。最后它会关闭线程,并从 ThreadManager 里移除,避免留下后台任务。

函数细节4
main81–83 ↗
fn main() -> anyhow::Result<()>

作用:这是程序真正启动的入口。它把启动工作交给 Codex 提供的分发器,让程序在不同启动方式下都能走到同一个主流程。

数据流:程序启动后没有自己解析参数,而是先进入 arg0_dispatch_or_else。这个分发器会根据可执行文件启动路径等信息做必要处理;如果没有特殊分发,就调用 run_main。最后 main 把 run_main 或分发器产生的成功或错误结果交还给操作系统。

调用关系:main 是最外层门口。它只调用 arg0_dispatch_or_else,并把真正的业务交给 run_main;这样主流程不用关心程序是通过哪个可执行文件名或包装路径启动的。

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

run_main85–157 ↗
async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()>

作用:这是整个示例程序的总指挥。它读取用户提示词,搭好 Codex 运行所需的环境,启动一个新 Codex 线程,跑一轮对话,然后负责关停和清理。

数据流:它先尝试设置来源标识,接着读取命令行参数:有提示词就拼成一句,没有就从标准输入读取;如果两边都没有内容,就报错。然后它调用 new_config 生成配置,初始化状态数据库、认证管理器、执行环境路径、线程存储、环境管理器、安装编号和用户指令提供器。准备好这些零件后,它创建 ThreadManager,启动一个新线程,把线程和提示词交给 run_turn。run_turn 返回后,它关闭线程并从管理器里移除;如果中途或关闭时出错,就把错误返回。

调用关系:run_main 位于 main 之后,是启动、运行、收尾的串联者。它会调用 new_config 来造配置,调用 run_turn 来真正提交提示词和读取事件,也会使用 Codex 核心库里的认证、数据库、环境和线程管理组件来搭好运行现场。

调用图:调用 7 个内部函数(new, new, from_codex_home, from_optional_paths, shared_from_config, new_config, run_turn);外部调用 12 个(clone, new, new, parse, bail!, empty_extension_registry, init_state_db, resolve_installation_id, set_default_originator, thread_store_from_config (+2 more))。

new_config159–295 ↗
fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::Result<Config>

作用:这个函数手工拼出一份适合本示例使用的 Codex 配置。它的重点是让 Codex 能在当前目录、只读权限、默认功能集合下跑起来,而不依赖完整的交互式配置流程。

数据流:它接收可选的模型名和启动路径信息。先找到 Codex 的家目录,再取得当前工作目录;然后选择内置的 OpenAI 模型提供方,并填充一大份 Config 配置。配置里包括模型、权限、工作目录、日志目录、认证存储方式、线程存储方式、实时功能、网页搜索开关、分析开关等。它把权限设成无需审批但只读,意思是示例不希望真的修改用户文件。最后启用默认功能集合,并返回这份完整配置;如果找目录、模型提供方或权限配置失败,就返回错误。

调用关系:new_config 只被 run_main 调用,是启动阶段的配置工厂。run_main 后续初始化状态数据库、认证、线程管理器和运行环境时,都依赖它产出的 Config。它自己主要调用各种默认值构造函数、权限构造函数和功能开关设置函数。

调用图:调用 9 个内部函数(allow_any, default, default, default, default, from_approval_and_profile, with_defaults, read_only, current_dir);被 1 处调用(run_main);外部调用 17 个(new, default, new, new, built_in_model_providers, find_codex_home, default, default, default, default (+7 more))。

run_turn297–393 ↗
async fn run_turn(thread: &CodexThread, thread_id: &str, prompt: String) -> anyhow::Result<()>

作用:这个函数负责真正跑“一轮” Codex:把用户输入提交给线程,然后不断读取 Codex 发出的事件,并把有用事件打印成 JSON。它也是这个示例最核心的事件循环。

数据流:它接收一个已经启动的 CodexThread、线程编号字符串和提示词。先把提示词包装成 UserInput::Text,通过 submit 交给线程。之后进入循环,不断调用 next_event 等待下一个事件。看到 TurnStarted 时,它记住本轮 turn_id;看到工具调用、代理消息增量、计划增量、推理内容、命令执行、补丁应用等过程事件时,它把事件连同 thread_id 和 turn_id 交给 item_event_to_server_notification,转成通知对象,再用 serde_json 写到标准输出,并补一个换行。看到 TurnComplete 就正常返回;看到错误、取消、审批请求、权限请求、继续询问用户、动态工具请求等情况,就立即报错退出。

调用关系:run_turn 由 run_main 在新线程启动后调用。它把用户输入交给 CodexThread,然后消费 CodexThread 产出的事件;需要对外输出时,它借助 item_event_to_server_notification 做格式转换,再写到 stdout。它结束后,run_main 才会继续关闭线程和清理 ThreadManager。

调用图:调用 2 个内部函数(next_event, submit);被 1 处调用(run_main);外部调用 6 个(default, bail!, item_event_to_server_notification, to_writer, stdout, vec!)。

code-mode-host/src/main.rs源码 ↗
entrypointstartup

在 Rust 程序里,main 函数就是程序启动后第一个执行的地方,类似一家店的正门。这个文件只有一个空的 main 函数,所以现在运行这个程序时,它会立刻开始,也会立刻结束,不读取配置、不启动服务、不处理任何请求。它仍然重要,因为没有这个入口,作为可执行程序的项目就不知道从哪里开跑。可以把它理解成一个还没装修的舞台:舞台已经搭好,灯也能打开,但演员、道具和剧情还没有放进来。

函数细节1
main1–1 ↗
fn main()

作用:这是程序的入口函数,也就是程序启动时最先执行的地方。目前它是空的,所以不会做任何实际工作。

数据流:进去时没有参数,也不读取任何信息 → 它什么步骤都不执行 → 出来时没有返回有意义的结果,也不会改动文件、网络或内存里的业务状态。

调用关系:它位于整个程序流程的最开头。当前没有调用其他函数,也没有其他项目内函数调用它;它只是被操作系统在启动这个可执行程序时触发,然后马上结束。

ext/extension-api/examples/enabled_extensions.rs源码 ↗
entrypointexample run

这个例子像是在演示一家店怎么接入外部插件:先把插件登记到“扩展注册表”里,再准备几份可共享的数据盒子。session_store 代表整个会话共用的盒子,thread_store 代表某个具体线程或对话分支自己的盒子。程序分别用同一个会话盒子、不同线程盒子去请求扩展贡献提示词片段,然后打印每个盒子里记录了多少次贡献。重点是让读者看到:同一个 session_store 会让多个线程共享会话状态;不同 thread_store 则让线程自己的状态分开。最后的 block_on_ready 是一个很简陋的异步运行器,只适合这个例子里“马上完成”的异步任务。

函数细节3
main15–68 ↗
fn main()

作用:这是例子的入口。它把扩展装进注册表,准备会话和线程两类数据盒子,触发几次提示词贡献,然后把结果打印出来给人看。

数据流:进去时没有外部输入;它创建一个 ExtensionRegistryBuilder,把 shared_state_extension::install 装进去,再 build 成 registry。接着创建一个 session_store、两个 thread_store,多次调用 contribute_prompt,并用 block_on_ready 等待结果;最后把第一次拿到的提示词片段数量,以及各个数据盒子里记录的贡献次数打印到屏幕。

调用关系:它是整个演示的总导演:先调用 install 安装示例扩展,再调用 contribute_prompt 让扩展产出内容。因为 contribute_prompt 是异步函数,它又把返回的 future 交给 block_on_ready 立刻跑完。最后它通过打印结果证明共享会话状态和隔离线程状态的区别。

调用图:调用 4 个内部函数(block_on_ready, contribute_prompt, install, new);外部调用 2 个(new, println!)。

contribute_prompt70–80 ↗
async fn contribute_prompt(
    registry: &codex_extension_api::ExtensionRegistry<()>,
    session_store: &ExtensionData,
    thread_store: &ExtensionData,
) -> Vec<codex_extension_api::PromptFragment

作用:这个函数负责向注册表里的所有上下文贡献者要提示词片段。简单说,就是把“当前会话盒子”和“当前线程盒子”递给每个扩展,让它们补充要放进提示词里的内容。

数据流:输入是扩展注册表 registry、会话数据 session_store、线程数据 thread_store。它先准备一个空列表 fragments,然后遍历 registry.context_contributors() 返回的所有贡献者;每个贡献者都会拿到这两个数据盒子并异步产出一些 PromptFragment,函数把这些片段合并进列表。输出是收集好的提示词片段列表,同时贡献者可能会在传入的数据盒子里留下计数或状态记录。

调用关系:main 在每次想模拟一次提示词生成时调用它。它不自己知道有哪些扩展,而是向 registry 要贡献者列表,再把具体工作交给每个 contributor 的 contribute 方法。这样宿主程序只负责搭舞台,扩展负责真正添加内容。

调用图:调用 1 个内部函数(context_contributors);被 1 处调用(main);外部调用 1 个(new)。

block_on_ready82–93 ↗
fn block_on_ready(future: F) -> F::Output

作用:这个函数是例子专用的迷你异步执行器。它只尝试轮询一次异步任务,任务如果已经完成就拿结果;如果还没完成就直接报错。

数据流:输入是一个 future,也就是“以后会给出结果的异步任务”。它创建一个 noop waker(什么也不唤醒的通知器)和 Context,把 future 固定住后 poll 一次;如果得到 Poll::Ready,就输出任务结果;如果得到 Poll::Pending,就 panic,说明这个例子的扩展不该需要真正等待。

调用关系:main 用它来运行 contribute_prompt 返回的异步任务。它没有启动真正的异步运行时,也不会处理网络、定时器这类需要等待的事情;它只适合这里的演示,因为这些 context contributor 预期会马上完成。

调用图:被 1 处调用(main);外部调用 5 个(from_waker, as_mut, noop, panic!, pin!)。

App server 和通知辅助工具

这些文件定义 app-server 可执行界面,以及配套的测试客户端和基于文件的通知捕获辅助工具。

app-server-test-client/src/main.rs源码 ↗
entrypointstartup

这个文件就像一个电器的电源开关,本身不做复杂业务,但没有它程序就启动不起来。Rust 里的异步任务需要一个“运行时”,可以理解成一个专门调度后台任务的小管家;这里用 Tokio 创建了一个单线程运行时,也就是所有异步工作都在当前线程里排队执行。enable_all 表示把常用能力都打开,比如定时器、网络等。运行时建好后,main 用 block_on 把异步的 codex_app_server_test_client::run() 跑完:虽然 run 是异步的,但整个程序会在这里等它结束。Result 用来把启动或运行中的错误往外传,这样失败时程序能清楚地报错退出。

函数细节1
main4–7 ↗
fn main() -> Result<()>

作用:这是整个测试客户端程序最先执行的函数。它负责搭好异步任务需要的运行环境,然后启动真正的客户端流程。

数据流:进去时没有业务输入;它先调用 Tokio 的 new_current_thread 创建一个单线程异步运行时构建器,再打开运行时所需的常用功能并构建运行时;之后把 codex_app_server_test_client::run() 放进去执行。出来的是一个 Result:成功就正常结束,出错就把错误返回给系统。

调用关系:main 位于最外层,系统启动这个二进制程序时先进入它。它自己只做启动准备,把真正的测试客户端逻辑交给外部的 codex_app_server_test_client::run();运行时则由 Tokio 的 new_current_thread 这一套工具提供。

调用图:外部调用 2 个(new_current_thread, run)。

app-server/src/bin/notify_capture.rs源码 ↗
entrypointcommand invocation

这个文件定义了一个独立运行的小程序。它启动时只接受两个参数:第一个是要写入的文件路径,第二个是要保存的内容。如果参数少了或多了,它会直接报错,防止误用。真正写文件时,它不是直接改目标文件,而是先写到一个临时文件,名字是在目标文件后面加“.tmp”。写完后还会执行同步操作,也就是尽量确认内容已经落到磁盘上,而不只是停在系统缓存里。最后再把临时文件改名成目标文件。这个“先临时、后替换”的做法很重要:如果程序中途崩了,正式文件通常不会变成半截内容。它适合被别的程序调用,用来捕获一段通知或消息并保存下来,方便后续检查或测试。

函数细节1
main12–44 ↗
fn main() -> Result<()>

作用:这是这个小程序的入口。它读取命令行里给的输出路径和通知内容,然后把内容安全地写进文件;如果输入不符合预期,或者写文件失败,就返回带说明的错误。

数据流:进去的是命令行参数:程序名之后的第一个参数当作输出文件路径,第二个参数当作要保存的内容。它先检查参数数量,接着把内容转成可写入的文字字节,写进一个“.tmp”临时文件,确认写入并同步到磁盘后,再把临时文件改名为正式输出文件。出来的是成功或失败的结果;成功时目标文件会包含传入的内容,失败时会给出哪一步出错的提示。

调用关系:它是整个程序唯一的执行流程,程序一启动就从这里开始。它调用系统提供的读取参数功能来拿输入,调用创建文件和重命名文件的功能来完成安全写入;遇到多余参数时会直接交给报错机制终止流程,避免写错文件或保存错内容。

调用图:外部调用 6 个(create, from, bail!, args_os, format!, rename)。

app-server/src/bin/test_notify_capture.rs源码 ↗
entrypointtest run

这个文件定义了一个独立运行的小程序。它启动后只做一件事:从命令行参数里拿到两个东西,第一个是要写入的文件路径,第二个是要保存的文字内容。它会先把内容写到一个临时文件里,比如把目标文件旁边先写成“.json.tmp”,写完后再把临时文件改名成真正的目标文件。这样做的好处是,读文件的人不容易看到“写到一半”的坏文件,类似先把信写在草稿纸上,写完确认没问题后再放进正式信封。它还会检查参数是否缺失,以及内容是不是合法的 UTF-8 字符串(可以理解为正常的文本编码)。如果哪里不对,就直接报错退出,避免测试误以为保存成功。

函数细节1
main6–23 ↗
fn main() -> Result<()>

作用:这是这个小程序的入口。它读取命令行传来的输出路径和通知内容,把通知内容安全地写进指定文件,供测试后续查看。

数据流:进去的是命令行参数:第一个参数应该是输出文件路径,第二个参数应该是要保存的文本内容。它先检查这两个参数是否存在,再确认文本内容是合法的 UTF-8;然后把内容写到一个临时文件,最后把临时文件改名为目标文件。出来的结果是成功返回,或者在参数缺失、文本编码错误、写文件失败、改名失败时返回错误;磁盘上会多出或更新目标文件。

调用关系:它是整个流程的起点,没有别的项目内函数来调用它。运行时它把具体工作交给系统和标准库:用命令行读取函数拿参数,用文件写入函数保存临时文件,再用改名函数把临时文件变成正式文件。这样测试框架只需要启动这个程序,就能把要捕获的通知写下来。

调用图:外部调用 4 个(from, args_os, rename, write)。

app-server/src/main.rs源码 ↗
entrypointstartup

这个文件像一个“前台接待员”:程序刚启动时,先由它看用户带了哪些启动参数,比如服务器要通过标准输入输出、Unix socket,还是 WebSocket 来通信;再看配置要不要严格检查;还会处理一些只给测试用的后门环境变量,避免测试时真的去碰系统级配置。它本身不负责提供服务,也不处理具体请求,而是把启动所需的信息打包成一套清楚的运行选项。比较重要的是远程控制开关:它会结合命令行参数和环境变量,决定这次启动是临时开启、临时关闭,还是按保存过的设置来。最后,它调用应用服务器库里的主运行函数,让服务器真正跑起来。

函数细节3
main61–109 ↗
fn main() -> anyhow::Result<()>

作用:这是整个 app-server 可执行程序的入口。用户一启动这个程序,就先从这里开始读取启动参数、准备配置和运行模式,然后把控制权交给服务器主流程。

数据流:进去的是操作系统给程序的命令行参数和环境变量。它先读取远程控制是否被环境变量禁用,再通过 arg0_dispatch_or_else 处理一种按程序名分流的启动方式;随后解析命令行参数,决定监听地址、会话来源、鉴权设置、配置检查方式和运行选项。最后它把这些整理好的信息传给服务器运行函数;如果中途出错,就把错误作为启动失败结果返回。

调用关系:它是最外层的启动点。它先调用 take_remote_control_disabled_env 取得远程控制的环境状态,再把后续启动逻辑交给 arg0_dispatch_or_else 包起来执行;在这个包起来的流程里,它准备好所有选项,然后交给应用服务器库继续完成真正的启动和运行。

调用图:外部调用 2 个(take_remote_control_disabled_env, arg0_dispatch_or_else)。

disable_managed_config_from_debug_env111–120 ↗
fn disable_managed_config_from_debug_env() -> bool

作用:这个函数只在调试版里给测试用,用来判断是否要跳过“受管理配置”。受管理配置可以理解为系统统一下发的配置;测试时常常不希望读到真实机器上的那份。

数据流:进去的是当前进程的环境变量。它读取 CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG 这个变量;如果值是 1、true、TRUE、yes 或 YES,就输出 true,表示测试时禁用受管理配置。没有这个变量、值不匹配,或者不是调试版时,就输出 false,不改变正常行为。

调用关系:main 在准备配置加载选项时会用它。它内部通过 var 读取环境变量,并用 matches! 判断这个字符串是不是几个约定好的“开启”写法;结果会影响 main 后面选择正常配置加载,还是使用测试专用的跳过受管理配置模式。

调用图:外部调用 2 个(matches!, var)。

managed_config_path_from_debug_env122–135 ↗
fn managed_config_path_from_debug_env() -> Option<PathBuf>

作用:这个函数也是调试版测试钩子,用来让测试把“受管理配置文件”的位置改到临时文件。这样测试不必写入或依赖系统里的真实配置文件。

数据流:进去的是当前进程的环境变量。它读取 CODEX_APP_SERVER_MANAGED_CONFIG_PATH;如果变量不存在,或者在调试版外运行,就输出 None,表示不指定特殊路径。如果变量存在但为空,也输出 None;如果有内容,就把这段文字转换成文件路径 PathBuf 并输出 Some(path)。

调用关系:main 在决定配置加载方式时会调用它。它先通过 var 拿到环境变量,再用 from 把字符串变成路径对象;main 拿到这个路径后,会把它放进测试专用的配置加载覆盖项里,让后续服务器启动时读指定的配置文件。

调用图:外部调用 2 个(from, var)。

MCP 和代理测试服务器

此组包含可直接运行的 MCP 相关服务器、代理和连通性探测器,用于集成测试和本地实验。

rmcp-client/src/bin/test_stdio_server.rs源码 ↗
entrypointstartup, request handling, teardown

这个文件是一个可运行的小服务器,主要给集成测试和人工调试用。MCP 可以理解成“客户端和工具服务器说话的一套规矩”,这里用标准输入/输出传消息,也就是不走网络,像两个人隔着管道传纸条。服务器启动后会公布一批测试工具:回声工具会把输入原样加工返回,cwd 会返回当前目录,sync 会让多个并发请求在同一个关卡前等齐,image 和 image_scenario 用来测试图片内容是否能被客户端正确显示。它还暴露了一个固定的 memo 资源,方便测试“列资源”和“读资源”。重要的是,这不是业务服务器,而是测试夹具:它故意覆盖正常、异常、边界情况,帮助客户端确认自己在各种 MCP 场景下不会出错。

函数细节29
stdio48–50 ↗
fn stdio() -> (tokio::io::Stdin, tokio::io::Stdout)

作用:拿到当前进程的标准输入和标准输出,作为服务器和客户端通信的通道。简单说,就是把键盘输入口和屏幕输出口当成一根双向测试管道来用。

数据流:进去没有参数 → 它向 Tokio 异步运行库要标准输入和标准输出句柄 → 出来是一对输入/输出对象,后面服务器用它们收发 MCP 消息。

调用关系:main 在启动服务器时调用它,把得到的通道交给 service.serve。它自己只调用底层的 stdin 和 stdout,不处理任何 MCP 内容。

调用图:被 1 处调用(main);外部调用 2 个(stdin, stdout)。

TestToolServer::new53–85 ↗
fn new() -> Self

作用:组装一个完整的测试服务器实例,把所有可用工具、资源、资源模板都提前准备好。没有它,服务器启动后就不知道自己能提供哪些测试能力。

数据流:进去没有参数 → 它创建 sandbox_meta、echo、cwd、sync、image 等工具说明,再创建 memo 资源和模板,并用 Arc(一种可安全共享的数据指针)包起来 → 出来是一个 TestToolServer,里面已经装好测试菜单。

调用关系:main 启动时创建服务会用到它。它把具体的零件制作交给 echo_tool、cwd_tool、sync_tool、memo_resource 等函数,自己负责把这些零件装进服务器。

调用图:外部调用 7 个(new, Borrowed, new, new, from_value, json!, vec!)。

TestToolServer::echo_tool87–92 ↗
fn echo_tool() -> Tool

作用:创建名叫 echo 的测试工具说明。这个工具用来检查客户端传参、结构化结果和环境变量读取是否正常。

数据流:进去没有参数 → 它指定工具名 echo 和一段说明文字 → 出来是一个 Tool 对象,描述这个工具需要什么输入、会返回什么输出。

调用关系:TestToolServer::new 会调用它来登记工具。它把真正建工具说明的工作交给 TestToolServer::build_echo_tool。

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

TestToolServer::echo_dash_tool94–99 ↗
fn echo_dash_tool() -> Tool

作用:创建名叫 echo-tool 的测试工具说明。这个名字里有短横线,专门测试客户端能不能处理不适合作为 JavaScript 标识符的工具名。

数据流:进去没有参数 → 它指定工具名 echo-tool 和说明文字 → 出来是一个 Tool 对象,和 echo 类似但名字更特殊。

调用关系:TestToolServer::new 会调用它来加入工具列表。它复用 TestToolServer::build_echo_tool,避免重复写同一套输入输出规则。

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

TestToolServer::build_echo_tool101–138 ↗
fn build_echo_tool(name: &'static str, description: &'static str) -> Tool

作用:按给定名字和说明,搭出一个回声类工具的完整说明。它规定工具必须接收 message,也可以接收 env_var,并声明返回 echo 和 env 两项。

数据流:进去是工具名和描述文字 → 它生成输入 JSON Schema(JSON 数据格式规则)和输出 JSON Schema,并把工具标成只读 → 出来是一个配置完整的 Tool。

调用关系:TestToolServer::echo_tool 和 TestToolServer::echo_dash_tool 都依赖它。它只负责“写菜单说明”,真正执行回声是在 TestToolServer::call_tool 里。

调用图:外部调用 6 个(new, Borrowed, new, json!, new, from_value)。

TestToolServer::cwd_tool140–167 ↗
fn cwd_tool() -> Tool

作用:创建 cwd 工具说明,用来让测试客户端询问服务器进程当前在哪个目录运行。

数据流:进去没有参数 → 它声明这个工具不需要输入参数,输出里会有 cwd 字符串,并标成只读 → 出来是一个 Tool 对象。

调用关系:TestToolServer::new 调用它来登记工具。客户端真正调用 cwd 时,由 TestToolServer::call_tool 读取当前目录并返回结果。

调用图:外部调用 6 个(new, Borrowed, new, json!, new, from_value)。

TestToolServer::sync_tool169–210 ↗
fn sync_tool() -> Tool

作用:创建 sync 工具说明,用来测试多个请求同时到来时的等待、延迟和超时行为。它像一道闸门,可以让几个人到齐后再一起放行。

数据流:进去没有参数 → 它声明可选的前置睡眠、后置睡眠和 barrier(屏障,也就是等够人数才放行的同步点)参数,并声明输出 result → 出来是一个 Tool 对象。

调用关系:TestToolServer::new 用它登记普通 sync 工具。真正的等待逻辑在 TestToolServer::sync_result 和 wait_on_sync_barrier 中执行。

调用图:外部调用 5 个(new, Borrowed, json!, new, from_value)。

TestToolServer::sync_readonly_tool212–217 ↗
fn sync_readonly_tool() -> Tool

作用:创建 sync_readonly 工具说明。它和 sync 行为类似,但额外标记为只读,用来测试客户端对“只读工具”的处理。

数据流:进去没有参数 → 它先复用 sync_tool 做出基础工具,再把名字改成 sync_readonly,并加上只读标注 → 出来是一个新的 Tool 对象。

调用关系:TestToolServer::new 会调用它。它依赖 TestToolServer::sync_tool 生成主体,执行时仍由 TestToolServer::call_tool 转给 TestToolServer::sync_result。

调用图:外部调用 3 个(Borrowed, sync_tool, new)。

TestToolServer::image_tool219–235 ↗
fn image_tool() -> Tool

作用:创建 image 工具说明,用来返回一个图片内容块。它主要测试客户端能不能接收和显示 MCP 工具返回的图片。

数据流:进去没有参数 → 它声明工具不需要输入参数,并标成只读 → 出来是一个名叫 image 的 Tool 对象。

调用关系:TestToolServer::new 用它加入工具列表。客户端调用 image 时,TestToolServer::call_tool 会从环境变量读取 data URL 并生成图片结果。

调用图:外部调用 6 个(new, Borrowed, new, new, from_value, json!)。

TestToolServer::image_scenario_tool256–294 ↗
fn image_scenario_tool() -> Tool

作用:创建 image_scenario 工具说明,用来人工测试各种图片返回场景,比如先文本后图片、坏图片后好图片、多张图片等。

数据流:进去没有参数 → 它声明 scenario、caption、data_url 这些参数,其中 scenario 限定在一组测试场景里,并标成只读 → 出来是一个 Tool 对象。

调用关系:TestToolServer::new 会登记它。实际根据场景拼装返回内容的工作,由 TestToolServer::call_tool 调到 TestToolServer::image_scenario_result 完成。

调用图:外部调用 6 个(new, Borrowed, new, new, from_value, json!)。

TestToolServer::memo_resource296–308 ↗
fn memo_resource() -> Resource

作用:创建一个固定的 memo 测试资源说明。它让客户端可以测试“服务器列出资源”这一步。

数据流:进去没有参数 → 它填好资源 URI、名字、标题、描述和 text/plain 类型 → 出来是一个 Resource 对象,但还不包含正文读取结果。

调用关系:TestToolServer::new 调用它放进资源列表。客户端读取这个资源时,TestToolServer::read_resource 会按同一个 URI 返回正文。

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

TestToolServer::memo_template310–322 ↗
fn memo_template() -> ResourceTemplate

作用:创建 memo 资源模板说明,告诉客户端这类资源 URI 可以长成 memo://codex/{slug} 的样子。

数据流:进去没有参数 → 它填好 URI 模板、名字、标题、描述和 MIME 类型(文件内容类型) → 出来是一个 ResourceTemplate 对象。

调用关系:TestToolServer::new 调用它放进资源模板列表。之后 TestToolServer::list_resource_templates 会把它报告给客户端。

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

TestToolServer::memo_text324–326 ↗
fn memo_text() -> &'static str

作用:返回固定 memo 资源的正文内容。这样测试中读到的文本永远一致,方便断言结果。

数据流:进去没有参数 → 它直接取文件顶部定义的常量文本 → 出来是一段静态字符串。

调用关系:TestToolServer::read_resource 在确认 URI 匹配后调用它,把文本包装成 MCP 的资源读取结果。

default_sync_timeout_ms363–365 ↗
fn default_sync_timeout_ms() -> u64

作用:给 sync 屏障等待提供默认超时时间。调用方没写 timeout_ms 时,就用这个安全默认值,避免请求永远卡住。

数据流:进去没有参数 → 它读取默认超时常量 → 出来是 1000 毫秒。

调用关系:它被 serde 反序列化(把 JSON 参数变成 Rust 结构)时作为默认值函数使用。后续 TestToolServer::sync_result 会把参数交给 wait_on_sync_barrier。

sync_barrier_map367–369 ↗
fn sync_barrier_map() -> &'static tokio::sync::Mutex<HashMap<String, SyncBarrierState>>

作用:取得全局的同步屏障表。这个表按 id 记住正在等待的屏障,方便不同请求找到同一道“集合点”。

数据流:进去没有参数 → 它如果还没建表,就创建一个带互斥锁的 HashMap;互斥锁就是一把锁,防止多个异步任务同时乱改表 → 出来是这张全局表的引用。

调用关系:wait_on_sync_barrier 用它查找或创建屏障,remove_sync_barrier_if_current 用它清理用完或超时的屏障。

调用图:被 2 处调用(remove_sync_barrier_if_current, wait_on_sync_barrier)。

TestToolServer::get_info399–412 ↗
fn get_info(&self) -> ServerInfo

作用:告诉客户端这个服务器支持哪些能力,比如工具、资源,以及一个实验性能力标记。相当于服务器递给客户端的一张名片。

数据流:进去是服务器自身 → 它构建能力清单,打开工具和资源能力,并加入 sandbox-state-meta 实验能力 → 出来是 ServerInfo,里面还带有使用说明。

调用关系:MCP 框架在建立会话、询问服务器信息时会调用它。它不调用本文件其他核心逻辑,只负责报告能力。

调用图:外部调用 4 个(from, new, builder, new)。

TestToolServer::list_tools414–427 ↗
fn list_tools(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Future<Output = R

作用:把服务器支持的所有测试工具列给客户端。客户端只有先看到这份清单,才知道能调用 echo、sync、image 等工具。

数据流:进去是可选分页请求和请求上下文,但这里不使用分页 → 它复制已保存的工具列表 → 出来是 ListToolsResult,里面没有下一页游标。

调用关系:MCP 框架在客户端请求工具列表时调用它。工具列表是在 TestToolServer::new 中提前准备好的。

TestToolServer::list_resources429–442 ↗
fn list_resources(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Future<Output

作用:把服务器暴露的固定测试资源列给客户端。这里主要是那个 memo 示例资源。

数据流:进去是可选分页请求和上下文,但这里不分页 → 它复制服务器里的资源列表 → 出来是 ListResourcesResult,没有下一页。

调用关系:MCP 框架在客户端请求资源列表时调用它。资源由 TestToolServer::memo_resource 在启动时创建。

TestToolServer::list_resource_templates444–454 ↗
async fn list_resource_templates(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<ListResou

作用:把资源 URI 模板列给客户端,让客户端知道某类资源地址的格式。

数据流:进去是可选分页请求和上下文 → 它复制服务器保存的资源模板列表 → 出来是 ListResourceTemplatesResult,没有下一页。

调用关系:MCP 框架在客户端请求资源模板时调用它。模板由 TestToolServer::memo_template 在 TestToolServer::new 中创建。

TestToolServer::read_resource456–476 ↗
async fn read_resource(
        &self,
        ReadResourceRequestParams { uri, .. }: ReadResourceRequestParams,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Re

作用:按 URI 读取资源正文。它只认识一个固定 memo URI,其他地址都会明确报“资源不存在”。

数据流:进去是读取资源请求,里面带 uri → 它检查 uri 是否等于固定 MEMO_URI;如果匹配,就把 memo_text 包成文本资源返回;否则返回 resource_not_found 错误 → 出来是资源内容或错误。

调用关系:MCP 框架在客户端真正读取资源时调用它。它依赖 TestToolServer::memo_text 提供正文,并和 TestToolServer::list_resources 公布的资源保持一致。

调用图:外部调用 4 个(resource_not_found, new, json!, vec!)。

TestToolServer::call_tool478–554 ↗
async fn call_tool(
        &self,
        request: CallToolRequestParams,
        context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<CallToolResult, McpError>

作用:这是所有工具调用的总调度口。客户端点名调用哪个工具,它就像前台分诊一样,把请求分给对应的小功能处理。

数据流:进去是工具调用请求和请求上下文 → 它按工具名匹配:sandbox_meta 返回请求元数据,cwd 读当前目录,echo 解析参数并读环境变量,image 解析 data URL,image_scenario 生成图片测试内容,sync 做等待同步;未知名字返回参数错误 → 出来是 CallToolResult 或 MCP 错误。

调用关系:MCP 框架在客户端调用工具时进入这里。它会调用 parse_data_url、TestToolServer::parse_call_args、TestToolServer::image_scenario_result、TestToolServer::sync_result、TestToolServer::structured_result 等函数完成具体工作。

调用图:调用 1 个内部函数(parse_data_url);外部调用 13 个(invalid_params, image_scenario_result, structured_result, sync_result, format!, json!, success, Object, from_value, current_dir (+3 more))。

TestToolServer::parse_call_args558–572 ↗
fn parse_call_args(
        request: &CallToolRequestParams,
        tool_name: &'static str,
    ) -> Result<T, McpError>

作用:把工具调用里的 JSON 参数转成指定的 Rust 参数结构。它统一处理“没传参数”和“参数格式不对”的错误。

数据流:进去是工具调用请求和工具名 → 它取出 arguments,把键值对象交给 serde_json 转成目标类型;如果没有参数或格式不合,就生成 invalid_params 错误 → 出来是解析好的参数结构或错误。

调用关系:TestToolServer::call_tool 在处理 image_scenario、sync、sync_readonly 时调用它。这样各工具不用各自重复写参数解析。

调用图:外部调用 4 个(invalid_params, format!, Object, from_value)。

TestToolServer::image_scenario_result574–645 ↗
fn image_scenario_result(args: ImageScenarioArgs) -> Result<CallToolResult, McpError>

作用:按指定场景拼出图片测试结果。它故意能生成文本夹图片、坏图片夹好图片、多张图片等组合,用来检查客户端显示图片的鲁棒性。

数据流:进去是 ImageScenarioArgs,包含场景、可选标题和可选 data_url → 它先决定使用传入图片还是内置小 PNG,再根据场景往内容列表里放文本块、图片块或无效图片块 → 出来是成功的 CallToolResult,里面装着这些内容块;如果 data_url 格式错则返回参数错误。

调用关系:TestToolServer::call_tool 在工具名为 image_scenario 时调用它。它会复用 parse_data_url 解析外部传入的 data URL。

调用图:调用 1 个内部函数(parse_data_url);外部调用 8 个(new, success, new, image, text, new, Image, json!)。

TestToolServer::sync_result647–665 ↗
async fn sync_result(args: SyncArgs) -> Result<CallToolResult, McpError>

作用:执行 sync 和 sync_readonly 的实际等待逻辑。它可以先睡一会儿、等同伴到齐、再睡一会儿,帮助测试并发请求的顺序和超时。

数据流:进去是 SyncArgs → 它按 sleep_before_ms 先延迟;如果带 barrier,就调用 wait_on_sync_barrier 等够参与者;再按 sleep_after_ms 延迟 → 出来是结构化的 {"result":"ok"},或等待出错时返回 MCP 错误。

调用关系:TestToolServer::call_tool 在处理 sync 和 sync_readonly 时调用它。它把真正的屏障等待交给 wait_on_sync_barrier,最后用 TestToolServer::structured_result 包装成功结果。

调用图:调用 1 个内部函数(wait_on_sync_barrier);外部调用 4 个(from_millis, structured_result, json!, sleep)。

TestToolServer::structured_result667–671 ↗
fn structured_result(value: serde_json::Value) -> CallToolResult

作用:把一段 JSON 值包装成 MCP 工具的结构化结果。它适合返回机器容易读的字段,而不是普通文本内容。

数据流:进去是 serde_json::Value,也就是一块 JSON 数据 → 它创建一个空内容的成功结果,再把这块 JSON 塞进 structured_content → 出来是 CallToolResult。

调用关系:TestToolServer::call_tool 用它返回 sandbox_meta、cwd、echo 的结果,TestToolServer::sync_result 用它返回 sync 成功结果。

调用图:外部调用 2 个(new, success)。

wait_on_sync_barrier674–734 ↗
async fn wait_on_sync_barrier(args: SyncBarrierArgs) -> Result<(), McpError>

作用:让多个并发工具调用在同一个屏障前等待,直到人数够了才一起继续。它还会检查人数和超时,避免测试挂死。

数据流:进去是屏障 id、参与人数和超时时间 → 它先拒绝 0 人或 0 超时;然后在全局屏障表里按 id 找现有屏障,或创建新屏障;接着在限定时间内等待;超时会清理屏障并报错,成功时由 leader 清理屏障 → 出来是空成功结果或参数错误。

调用关系:TestToolServer::sync_result 调用它完成 barrier 等待。它使用 sync_barrier_map 访问全局表,并在超时或最后放行时调用 remove_sync_barrier_if_current 清理。

调用图:调用 2 个内部函数(remove_sync_barrier_if_current, sync_barrier_map);被 1 处调用(sync_result);外部调用 6 个(new, new, from_millis, invalid_params, format!, timeout)。

remove_sync_barrier_if_current736–743 ↗
async fn remove_sync_barrier_if_current(barrier_id: &str, barrier: &Arc<Barrier>)

作用:安全地从全局表里删除一个已经用完或超时的屏障。它会确认表里还是同一个屏障,避免误删别人新建的同名屏障。

数据流:进去是屏障 id 和屏障对象引用 → 它锁住全局表,查这个 id 当前对应的屏障是否和传入的是同一个;如果是就删除,不是就什么也不做 → 出来没有返回值,但可能改动全局屏障表。

调用关系:wait_on_sync_barrier 在等待超时或所有人到齐后调用它。它通过 sync_barrier_map 拿到全局表,并用指针相等判断是不是同一个屏障。

调用图:调用 1 个内部函数(sync_barrier_map);被 1 处调用(wait_on_sync_barrier);外部调用 1 个(ptr_eq)。

parse_data_url745–750 ↗
fn parse_data_url(url: &str) -> Option<(String, String)>

作用:解析 data URL,取出图片类型和 base64 数据。data URL 就是形如 data:image/png;base64,AAAA 的一整串内嵌文件内容。

数据流:进去是一段字符串 → 它检查是否以 data: 开头,再按逗号分成头部和数据部分,并从头部取出 MIME 类型 → 出来是 (mime_type, data);格式不合就返回 None。

调用关系:TestToolServer::call_tool 在 image 工具中调用它,TestToolServer::image_scenario_result 在使用自定义图片时也调用它。它只做字符串拆分,不验证 base64 数据真的能解码成图片。

调用图:被 2 处调用(call_tool, image_scenario_result)。

main753–768 ↗
async fn main() -> Result<(), Box<dyn std::error::Error>>

作用:这是这个测试服务器程序的入口。运行二进制后,程序从这里启动、注册服务、等待客户端通过标准输入/输出来交互。

数据流:进去是操作系统启动进程时给的运行环境 → 它打印启动日志;如果设置了 MCP_TEST_PID_FILE,就把当前进程号写到文件;然后创建 TestToolServer,拿到 stdio 通道,启动 MCP 服务并等待结束;最后让后台任务有机会收尾 → 出来是成功退出或把错误返回给操作系统。

调用关系:它是最顶层的函数。它调用 TestToolServer::new 组装服务器,调用 stdio 准备通信通道,再把服务交给 rmcp 框架运行。客户端断开或服务出错后,它负责结束进程。

调用图:调用 2 个内部函数(new, stdio);外部调用 5 个(eprintln!, var, write, id, yield_now)。

rmcp-client/src/bin/rmcp_test_server.rs源码 ↗
entrypointstartup, request handling, teardown

这个文件像一个“假服务员”,专门给客户端测试用。真实系统里,客户端需要连接 MCP 服务器,询问“你有什么工具”,再调用某个工具拿结果;如果没有这样一个稳定的小服务器,测试客户端时就很难判断问题出在客户端还是服务器。这里定义了 TestToolServer,它只提供一个 echo 工具:客户端传入 message,它就原样放回结果里;客户端还可以指定一个环境变量名,它会把当前进程里这个环境变量的值一并返回。文件还声明了工具的输入格式和输出格式,相当于菜单上写清楚“点菜时要填什么、端上来会是什么”。main 函数启动这个服务器,用标准输入输出作为通信通道,并在客户端断开后干净退出。

函数细节7
stdio24–26 ↗
fn stdio() -> (tokio::io::Stdin, tokio::io::Stdout)

作用:这个函数把程序的标准输入和标准输出打包返回。服务器用它和外部客户端通信,就像两根管子:一根收消息,一根发消息。

数据流:进去没有参数 → 它向 Tokio 这个异步运行环境要当前进程的 stdin 和 stdout → 出来是一对输入输出对象,后面交给服务器当通信通道使用。

调用关系:main 在启动服务器时调用它,把得到的输入输出通道交给 serve。stdio 自己只负责拿通道,不处理任何 MCP 消息。

调用图:被 1 处调用(main);外部调用 2 个(stdin, stdout)。

TestToolServer::new28–33 ↗
fn new() -> Self

作用:这个函数创建一个新的测试服务器实例。它准备好服务器能提供的工具列表,让后面的客户端查询和调用有东西可用。

数据流:进去没有参数 → 它生成工具列表,目前主要包含 echo 工具,并把列表放进 Arc 里;Arc 可以理解成“多人共用的一份只读资料夹” → 出来是一个 TestToolServer,里面带着这份工具菜单。

调用关系:main 启动时会调用它来造出服务器。之后 get_info、list_tools、call_tool 这些服务器接口都会围绕这个实例工作。

调用图:被 2 处调用(main, main);外部调用 2 个(new, vec!)。

TestToolServer::echo_tool35–71 ↗
fn echo_tool() -> Tool

作用:这个函数定义 echo 工具的“说明书”。它告诉客户端:这个工具叫什么、干什么、需要哪些输入、会返回什么格式的结果。

数据流:进去没有参数 → 它用 JSON Schema(一种描述 JSON 数据形状的规则)写出输入要求:必须有 message,可选 env_var;再写出输出要求:会有 echo 和 env → 出来是一个 Tool 对象,代表可以被客户端发现和调用的 echo 工具。

调用关系:它是 TestToolServer 准备工具菜单时的重要零件。虽然客户端不会直接调用这个函数,但客户端看到的工具名称、描述、输入输出格式都来自这里。

调用图:外部调用 5 个(new, Borrowed, json!, new, from_value)。

TestToolServer::get_info81–88 ↗
fn get_info(&self) -> ServerInfo

作用:这个函数告诉客户端:这个服务器具备哪些能力。这里它声明自己支持工具调用,也支持工具列表变更的能力标记。

数据流:进去是服务器自身 → 它组装一个 ServerInfo,里面包含 ServerCapabilities,也就是服务器能力清单 → 出来是给客户端看的服务器信息。

调用关系:当 MCP 框架需要向客户端介绍服务器时会调用它。它不处理具体工具请求,只是在连接早期或握手阶段提供“我会什么”的信息。

调用图:外部调用 2 个(builder, new)。

TestToolServer::list_tools90–103 ↗
fn list_tools(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Future<Output = R

作用:这个函数把服务器当前提供的工具列表发给客户端。客户端靠它知道可以调用一个叫 echo 的工具,以及这个工具需要什么参数。

数据流:进去有一个可选分页请求和请求上下文,但这里没有真正使用它们 → 它复制服务器里保存的工具列表,并说明没有下一页 → 出来是 ListToolsResult,里面装着工具列表。

调用关系:客户端询问“你有哪些工具”时,MCP 框架会调用它。它读取 TestToolServer::new 准备好的工具菜单,然后把结果交回框架发给客户端。

TestToolServer::call_tool105–141 ↗
async fn call_tool(
        &self,
        request: CallToolRequestParams,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<CallToolResult, McpError>

作用:这个函数真正执行客户端点名调用的工具。现在它只认识 echo:把传进来的 message 放回去,并按需附带一个环境变量的当前值。

数据流:进去是一次工具调用请求,里面有工具名和参数 → 它先看工具名是不是 echo;如果不是,就返回“参数无效”的错误;如果是,就把参数解析成 EchoArgs,缺参数或格式错也返回错误 → 然后读取当前进程的所有环境变量,找出 env_var 指定的名字,没指定就默认找 MCP_TEST_VALUE → 出来是 CallToolResult,structured_content 里放着 echo 和 env;同时它不修改服务器状态。

调用关系:客户端真正调用工具时,MCP 框架会把请求交给它。它自己完成参数检查、环境变量读取和结果组装;遇到错误时用 MCP 错误格式返回,让客户端能按协议理解失败原因。

调用图:外部调用 8 个(invalid_params, new, format!, json!, success, Object, from_value, vars)。

main145–157 ↗
async fn main() -> Result<(), Box<dyn std::error::Error>>

作用:这是这个测试服务器程序的入口。运行这个二进制文件时,最先执行的就是它。

数据流:进去没有业务参数 → 它先往错误输出打印启动提示,再创建 TestToolServer,然后用 stdio 拿到标准输入输出,并把服务器挂到这条通信通道上运行 → 客户端结束后,它等待服务停止,再让异步后台任务有机会收尾 → 最后成功退出,或把启动、通信、等待过程中的错误向外返回。

调用关系:它把整个文件里的零件串起来:调用 TestToolServer::new 得到服务器,调用 stdio 得到通信管道,再交给 rmcp 框架的 serve 运行。它不直接处理每个请求,请求进来后会由框架转给 get_info、list_tools、call_tool 等方法。

调用图:调用 2 个内部函数(new, stdio);外部调用 2 个(eprintln!, yield_now)。

rmcp-client/src/bin/test_streamable_http_server.rs源码 ↗
entrypointstartup, main loop, request handling

这个文件不是正式业务服务器,而是测试用的小型服务端。它监听一个地址,提供 /mcp 接口,让客户端按 MCP(Model Context Protocol,一种让模型和外部工具/资源通信的协议)来访问。它内置一个叫 echo 的工具,会把传入消息包装后返回;也内置一个示例资源 memo://codex/example-note,方便测试资源列表和读取。它还提供几个“控制接口”,测试程序可以先告诉服务器:下一次初始化请求、会话请求或 initialized 通知要故意返回某个错误码。这样就能模拟网络服务临时失败、需要 OAuth 鉴权、会话中断等情况。可选的 Bearer 鉴权(一种 HTTP 令牌验证方式)也在这里做,用来测试客户端是否正确带上访问令牌。

函数细节20
main116–206 ↗
async fn main() -> Result<(), Box<dyn std::error::Error>>

作用:启动整个测试服务器。它决定监听哪个地址,装好路由、中间件和 MCP 服务,然后一直对外提供 HTTP 服务。

数据流:进去的是环境变量和系统网络状态:比如绑定地址、是否要求 Bearer 令牌、是否要把实际端口写到文件。它先解析地址并反复尝试绑定端口,再搭建控制接口、OAuth 元数据接口和 /mcp 服务,最后交给 Axum HTTP 框架运行。出来的结果是一个正在监听的测试服务器;如果端口无权限或绑定失败,会打印错误或返回失败。

调用关系:这是这个二进制程序的入口。它先调用 parse_bind_addr 取得监听地址,再创建 TestToolServer::new 作为每个 MCP 会话的服务实例,并把 require_bearerfail_mcp_post_when_armed 等函数挂到 HTTP 请求流程里。之后每个请求都由 Axum 和 rmcp 框架分发到对应函数。

调用图:调用 1 个内部函数(parse_bind_addr);外部调用 18 个(new, from_millis, default, new, default, new, get, post, serve, eprintln! (+8 more))。

TestToolServer::get_info209–217 ↗
fn get_info(&self) -> ServerInfo

作用:告诉 MCP 客户端:这个测试服务器支持哪些能力。比如它支持工具、工具列表变化通知、资源读取。

数据流:进去的是当前服务器对象。它不读取外部数据,只组装一份能力说明。出来的是 ServerInfo,也就是客户端初始化时会看到的服务器能力清单。

调用关系:这个函数由 rmcp 服务框架在客户端初始化或查询服务器信息时调用。它不把工作交给本文件其他函数,只用 rmcp 的构建器生成能力描述。

调用图:外部调用 2 个(builder, new)。

TestToolServer::list_tools219–232 ↗
fn list_tools(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Future<Output = R

作用:返回这个测试服务器有哪些可调用工具。这里主要就是返回内置的 echo 工具。

数据流:进去的是可选分页参数和请求上下文,但这里没有真正分页,也不使用上下文。它读取服务器里保存的工具列表,复制一份放进结果。出来的是工具列表,且没有下一页。

调用关系:当 MCP 客户端请求“列出工具”时,rmcp 框架会调用它。工具列表是在 TestToolServer::new 里准备好的,其中 echo 工具来自 TestToolServer::echo_tool

TestToolServer::list_resources234–247 ↗
fn list_resources(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> impl std::future::Future<Output

作用:返回这个测试服务器公开的资源列表。这里返回一个示例 memo 文本资源。

数据流:进去的是可选分页参数和请求上下文,但这里忽略它们。它读取服务器里保存的资源数组,复制后返回。出来的是资源列表,且没有下一页。

调用关系:当客户端请求“有哪些资源可以读”时,rmcp 框架会调用它。资源对象是在 TestToolServer::new 中通过 TestToolServer::memo_resource 建好的。

TestToolServer::list_resource_templates249–259 ↗
async fn list_resource_templates(
        &self,
        _request: Option<PaginatedRequestParams>,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<ListResou

作用:返回资源地址模板,让客户端知道类似 memo://codex/{slug} 这种资源地址长什么样。

数据流:进去的是分页参数和上下文,但这里不用。它读取服务器保存的模板列表,复制成结果。出来的是资源模板列表,且没有下一页。

调用关系:当客户端请求“资源模板”时由 rmcp 框架调用。模板是在 TestToolServer::new 中通过 TestToolServer::memo_template 创建的。

TestToolServer::read_resource261–281 ↗
async fn read_resource(
        &self,
        ReadResourceRequestParams { uri, .. }: ReadResourceRequestParams,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Re

作用:按 URI 读取一个资源的内容。它只认识内置的 memo 示例资源,其他 URI 都会报“找不到”。

数据流:进去的是客户端要读取的资源 URI。它把 URI 和固定的 MEMO_URI 比较:匹配就返回一段纯文本内容;不匹配就返回 MCP 错误,并把请求的 URI 放进错误信息里。出来的是资源内容,或者资源不存在的错误。

调用关系:当客户端真正读取某个资源时,rmcp 框架会调用它。成功时它使用 TestToolServer::memo_text 提供正文;失败时通过 rmcp 的错误构造函数生成标准错误。

调用图:外部调用 4 个(resource_not_found, new, json!, vec!)。

TestToolServer::call_tool283–318 ↗
async fn call_tool(
        &self,
        request: CallToolRequestParams,
        _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
    ) -> Result<CallToolResult, McpError>

作用:执行客户端请求的工具调用。这里实现了唯一的 echo 工具:接收一段消息,返回带 ECHOING: 前缀的结果,并附带一个测试环境变量值。

数据流:进去的是工具名和参数。它先看工具名是不是 echo,再把 JSON 参数解析成 EchoArgs,要求必须有 message。然后读取当前进程环境变量,取出 MCP_TEST_VALUE,和回显文本一起放进结构化结果。出来的是工具调用成功结果;如果工具名未知、参数缺失或格式不对,就返回参数错误。

调用关系:当客户端调用工具时,rmcp 框架会进入这里。它依赖 TestToolServer::echo_tool 里声明的输入输出格式;也就是说,echo_tool 告诉客户端该怎么传参,而 call_tool 真正执行这次调用。

调用图:外部调用 8 个(invalid_params, new, format!, json!, success, Object, from_value, vars)。

TestToolServer::new322–331 ↗
fn new() -> Self

作用:创建一个新的测试服务器实例,并把内置工具、资源和资源模板都准备好。

数据流:进去没有外部参数。它分别创建 echo 工具、memo 资源和 memo 模板,放进共享容器里。出来的是一个 TestToolServer,可被 MCP HTTP 服务拿来处理请求。

调用关系main 在搭建 /mcp 服务时会把它作为工厂函数使用,每个服务实例都靠它生成。它把具体零件的创建交给 TestToolServer::echo_toolTestToolServer::memo_resourceTestToolServer::memo_template

调用图:外部调用 2 个(new, vec!)。

TestToolServer::echo_tool333–370 ↗
fn echo_tool() -> Tool

作用:定义 echo 工具的说明书。它告诉客户端:这个工具叫什么、需要什么输入、会返回什么结构。

数据流:进去没有参数。它先构造输入 JSON Schema(JSON 数据格式说明),要求有字符串 message,可选 env_var;再构造输出格式,包含 echoenv 字段;最后标记这个工具是只读的。出来的是一个完整的 Tool 对象。

调用关系TestToolServer::new 用它来准备工具列表。之后 list_tools 会把这个说明发给客户端,而 call_tool 会按这里声明的格式处理真正的调用。

调用图:外部调用 6 个(new, Borrowed, new, json!, new, from_value)。

TestToolServer::memo_resource372–384 ↗
fn memo_resource() -> Resource

作用:定义一个固定的示例文本资源,供客户端测试“列出资源”和“读取资源”。

数据流:进去没有参数。它填好资源 URI、名字、标题、描述和 MIME 类型(文件内容类型,这里是纯文本)。出来的是一个 Resource 对象。

调用关系TestToolServer::new 调用它来生成资源列表。之后 list_resources 会把它展示给客户端,read_resource 会用同一个 URI 判断是否返回正文。

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

TestToolServer::memo_template386–398 ↗
fn memo_template() -> ResourceTemplate

作用:定义 memo 资源的地址模板,说明这类资源 URI 可以长成什么样。

数据流:进去没有参数。它创建一个 memo://codex/{slug} 模板,并填入名字、标题、描述和纯文本类型。出来的是一个 ResourceTemplate 对象。

调用关系TestToolServer::new 用它准备模板列表。客户端请求资源模板时,list_resource_templates 会把它返回。

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

TestToolServer::memo_text400–402 ↗
fn memo_text() -> &'static str

作用:返回内置 memo 示例资源的正文。它把固定文本集中放在一个地方,避免读取资源时到处写重复字符串。

数据流:进去没有参数。它直接返回常量 MEMO_CONTENT 的静态字符串引用。出来的是 memo 资源的文本内容,不修改任何状态。

调用关系read_resource 在确认 URI 是内置 memo 资源后调用它,把这段文字包装成 MCP 资源读取结果。

parse_bind_addr405–411 ↗
fn parse_bind_addr() -> Result<SocketAddr, Box<dyn std::error::Error>>

作用:决定服务器应该监听哪个 IP 和端口。它让测试可以用环境变量改地址,不写环境变量时就用默认地址。

数据流:进去的是进程环境变量。它优先读 MCP_STREAMABLE_HTTP_BIND_ADDR,其次读 BIND_ADDR,都没有就用 127.0.0.1:3920,然后把字符串解析成网络地址。出来的是可绑定的 SocketAddr,或者解析失败的错误。

调用关系main 启动时最先调用它。拿到地址后,main 才能去绑定 TCP 监听器并启动 HTTP 服务。

调用图:被 1 处调用(main);外部调用 1 个(var)。

require_bearer413–430 ↗
async fn require_bearer(
    State(expected): State<Arc<String>>,
    request: Request<Body>,
    next: Next,
) -> Result<Response, StatusCode>

作用:检查请求有没有带正确的 Bearer 令牌。它用来模拟需要登录或授权的 MCP 服务。

数据流:进去的是预期令牌、HTTP 请求和“下一个处理步骤”。它先放行 .well-known 元数据请求;其他请求必须带 Authorization 头,且内容和预期值完全一样。通过就把请求交给后面的服务;不通过就返回 401 未授权。

调用关系main 只有在环境变量 MCP_EXPECT_BEARER 存在时才把它加进请求流水线。它像门卫一样站在 MCP 服务前面,验证通过才调用 Axum 的 next.run 继续处理。

调用图:外部调用 3 个(run, headers, uri)。

arm_session_post_failure432–437 ↗
async fn arm_session_post_failure(
    State(state): State<PostFailureState>,
    Json(request): Json<ArmSessionPostFailureRequest>,
) -> Result<StatusCode, StatusCode>

作用:接收测试控制请求,设置“后续普通会话 POST 请求要故意失败”。

数据流:进去的是共享失败状态和 JSON 请求体,里面包含状态码、失败次数、响应头和响应体等。它把目标固定为 Session,再交给通用函数保存这条失败规则。出来的是 204 成功,或者 400 之类的错误状态。

调用关系main 把它挂在 /test/control/session-post-failure。它自己不解析细节,而是调用 arm_post_failure 完成统一校验和保存。

调用图:调用 1 个内部函数(arm_post_failure)。

arm_initialize_post_failure439–444 ↗
async fn arm_initialize_post_failure(
    State(state): State<PostFailureState>,
    Json(request): Json<ArmSessionPostFailureRequest>,
) -> Result<StatusCode, StatusCode>

作用:接收测试控制请求,设置“下一些初始化 POST 请求要故意失败”。初始化请求通常还没有会话 ID。

数据流:进去的是共享失败状态和 JSON 请求体。它把失败目标设为 Initialize,然后让通用函数解析状态码、响应头、次数等。出来的是表示设置成功的 204,或表示请求不合法的错误状态。

调用关系main 把它挂在 /test/control/initialize-post-failure。它只是选择失败类型,具体保存动作交给 arm_post_failure

调用图:调用 1 个内部函数(arm_post_failure)。

arm_initialized_notification_post_failure446–451 ↗
async fn arm_initialized_notification_post_failure(
    State(state): State<PostFailureState>,
    Json(request): Json<ArmSessionPostFailureRequest>,
) -> Result<StatusCode, StatusCode>

作用:接收测试控制请求,设置“initialized 通知 POST 请求要故意失败”。这个通知是 MCP 初始化完成后的一个特定消息。

数据流:进去的是共享失败状态和 JSON 请求体。它把失败目标设为 InitializedNotification,再调用通用设置函数。出来的是 204 成功或错误状态。

调用关系main 把它挂在 /test/control/initialized-notification-post-failure。后续真正拦截请求的是 fail_mcp_post_when_armed,而这条规则由 arm_post_failure 写入共享状态。

调用图:调用 1 个内部函数(arm_post_failure)。

arm_post_failure453–482 ↗
async fn arm_post_failure(
    state: PostFailureState,
    request: ArmSessionPostFailureRequest,
    target: ArmedFailureTarget,
) -> Result<StatusCode, StatusCode>

作用:保存一条“故意让某类 MCP POST 请求失败”的规则。它是三个控制接口共用的核心设置函数。

数据流:进去的是共享状态、控制请求和失败目标。它把数字状态码转成 HTTP 状态码,检查 WWW-AuthenticateContent-Type 这些响应头是否合法;如果 remaining 是 0,就清空规则,否则保存状态码、剩余失败次数、响应头和响应体。出来的是 204 成功,或在输入不合法时返回 400。

调用关系arm_session_post_failurearm_initialize_post_failurearm_initialized_notification_post_failure 都调用它。它写入的规则之后会被 fail_mcp_post_when_armed 在真实 /mcp 请求到来时读取并消耗。

调用图:被 3 处调用(arm_initialize_post_failure, arm_initialized_notification_post_failure, arm_session_post_failure);外部调用 1 个(from_u16)。

fail_mcp_post_when_armed484–545 ↗
async fn fail_mcp_post_when_armed(
    State(state): State<PostFailureState>,
    request: Request<Body>,
    next: Next,
) -> Response

作用:在 MCP POST 请求经过时,按预先设置的规则故意返回失败。它让测试能验证客户端遇到服务器错误、鉴权挑战或会话失败时怎么处理。

数据流:进去的是共享失败状态、HTTP 请求和下一个处理步骤。它只关心发往 /mcp 的 POST;其他请求直接放行。对 MCP POST,它先读取请求体,判断有没有会话 ID,再从 JSON 里取出 MCP 方法名。如果当前规则匹配这类请求,就减少剩余次数并立即返回指定状态码、响应头和响应体;次数用完就清掉规则。如果不匹配,就把原请求体放回去,交给后面的 MCP 服务继续处理。

调用关系main 把它作为中间件加到整个路由上。它使用 request_mcp_method 判断请求是不是 notifications/initialized,并在不需要拦截时调用 next.run 把请求交回正常流程。

调用图:调用 1 个内部函数(request_mcp_method);外部调用 8 个(run, to_bytes, from_parts, into_parts, method, uri, new, from)。

request_mcp_method547–553 ↗
fn request_mcp_method(body: &[u8]) -> Option<String>

作用:从 MCP 请求的 JSON 正文里取出 method 字段。它帮助失败拦截器区分不同类型的 MCP 消息。

数据流:进去的是一段请求体字节。它尝试按 JSON 解析,然后找顶层 method 字段,并确认它是字符串。出来的是方法名字符串;如果正文不是 JSON、没有 method,或类型不对,就返回空。

调用关系fail_mcp_post_when_armed 调用它来判断当前 POST 是初始化完成通知,还是普通会话请求。它只做一个小而明确的解析工作,不直接决定是否失败。

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

mcp-server/src/main.rs源码 ↗
entrypointstartup

这个文件像一家店的前台接待。它自己不做具体业务,也不直接处理网络请求;它只负责在程序刚启动时接住入口。这里先用 arg0_dispatch_or_else 根据程序是用哪个名字启动的来做一次分流判断;“arg0”就是命令行里启动程序时的第一个名字,有些工具会用不同名字启动同一个程序,从而进入不同模式。如果没有被分流走,就继续调用 MCP 服务器的 run_main,把启动路径信息、默认的命令行配置覆盖项传进去,并且关闭严格配置检查。这样做的好处是:入口很薄,真正复杂的启动和运行逻辑都放在专门的库里,方便复用和测试。没有这个文件,操作系统就不知道启动这个二进制程序时该先执行哪段代码。

函数细节1
main6–16 ↗
fn main() -> anyhow::Result<()>

作用:这是程序启动时第一个被执行的函数。它负责把“程序是怎么被启动的”这件事先交给分流工具判断,然后在正常情况下启动 MCP 服务器主流程。

数据流:进去的是操作系统提供的启动环境,尤其是程序启动名这类命令行信息;它把这些信息交给 arg0_dispatch_or_else 做判断。如果不需要切换到别的启动模式,就在异步流程里调用服务器主入口 run_main,并传入启动路径、默认配置覆盖项和“非严格配置”开关;最后返回成功或错误结果,错误会一路交给外层运行环境显示或处理。

调用关系:main 位于整个程序最开头。它先调用 arg0_dispatch_or_else,让公共的启动分流机制有机会接管;如果没有被接管,闭包里的代码才会把工作交给 codex_mcp_server::run_main。也就是说,它是从“操作系统启动程序”到“服务器真正开始运行”之间的那一小段桥梁。

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

responses-api-proxy/src/main.rs源码 ↗
entrypointstartup

可以把这个文件理解成“开门营业”的前台。程序还没正式进入 main 之前,pre_main 会先运行一次,调用一套进程加固措施,尽量提前关掉一些不安全的运行环境风险。随后 main 才开始正常启动:它用 clap 这个命令行参数解析工具,把用户在终端里输入的选项读成结构化的参数;接着把这些参数交给 codex_responses_api_proxy::run_main。也就是说,这里不负责代理服务的具体细节,只负责把程序安全地带到“真正开始干活”的地方。没有它,程序就没有清晰的启动点,也可能错过最早期的安全初始化。

函数细节2
pre_main5–7 ↗
fn pre_main()

作用:这个函数在 main 之前自动运行,用来尽早给当前进程做安全加固。它的意义是:有些安全设置越早做越好,等程序已经跑起来再做可能就晚了。

数据流:它不接收用户输入,也不返回业务结果;它只是调用 pre_main_hardening,让外部的安全加固代码去调整当前进程的运行环境。调用前,进程还处在刚启动的早期阶段;调用后,进程带着这些安全设置继续进入正常启动流程。

调用关系:pre_main 通过 ctor 机制自动插在 main 之前执行,像开店前先检查门锁和报警器。它只把活交给 pre_main_hardening,不参与后面的参数解析或服务运行。

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

main9–12 ↗
fn main() -> anyhow::Result<()>

作用:这是程序正式启动的入口。它读取用户在命令行里写的参数,然后把这些参数交给真正运行代理服务的函数。

数据流:进去的是操作系统提供的命令行参数;main 先通过 parse 把这些文字参数变成 ResponsesApiProxyArgs 这种更好用的数据;然后把这个参数对象交给 run_main。出来的是一个 anyhow::Result,意思是程序要么正常结束,要么带着错误信息退出。

调用关系:main 在 pre_main 做完早期安全加固之后执行。它自己不实现代理服务,而是先调用 parse 完成启动配置读取,再调用 run_main 进入主流程。

调用图:外部调用 2 个(parse, run_main)。

codex-client/src/bin/custom_ca_probe.rs源码 ↗
entrypointtest subprocess startup and optional HTTPS probe

这个小程序像一个“探针”:测试先给它设置环境变量,比如证书文件位置、代理地址、要访问的网址,然后启动它,看它能不能成功建出一个 HTTPS 客户端。CA 证书可以理解成浏览器判断网站“是不是可信”的名单;这里要验证项目自己的客户端能不能按规则加载这份名单。因为环境变量是整个进程共享的,多个测试并发修改会很危险,所以这个文件把相关行为放进独立进程里验证。它先创建一个 Tokio 运行时(让异步网络请求能跑起来),再按环境变量配置 reqwest 客户端(一个常用的 HTTP 客户端库),必要时设置 TLS 1.3、代理和超时。最后,如果测试提供了目标网址,它会真的发一个 POST 请求,并确认服务端返回成功和正文“ok”。

函数细节4
main26–45 ↗
fn main()

作用:这是这个测试探针程序的入口。它负责搭好异步运行环境,启动真正的探测流程,并把成功或失败用进程退出码告诉外面的测试。

数据流:进去的是当前进程的状态和环境变量;它先创建一个单线程 Tokio 运行时(给异步代码用的小执行器),创建失败就打印错误并退出。运行时建好后,它调用 run_probe;如果成功就打印“ok”,如果失败就把错误打印到标准错误并用失败状态退出。

调用关系:外部集成测试启动这个二进制文件时,最先进入 main。main 不自己做证书或网络细节,而是把主要工作交给 run_probe;它只负责启动、收尾和把结果翻译成测试能识别的输出。

调用图:调用 1 个内部函数(run_probe);外部调用 4 个(eprintln!, println!, exit, new_current_thread)。

run_probe47–63 ↗
async fn run_probe() -> Result<(), String>

作用:这个函数读取测试传进来的环境变量,并据此决定要怎样构造 HTTP 客户端、是否要真的发一次 HTTPS 请求。

数据流:进去的是进程里的环境变量:代理地址、目标网址、是否强制 TLS 1.3。它创建 reqwest 客户端构建器;如果有目标网址,就加 5 秒超时;如果要求 TLS 1.3,就设置最低 TLS 版本。然后它调用 build_probe_client 建客户端;如果还提供了目标网址,就调用 post_probe_request 发请求。最后返回成功或一段错误文字。

调用关系:run_probe 是 main 启动后的核心流程。它在上游接收环境变量这个“测试指令”,中间把客户端创建交给 build_probe_client,最后在需要时把实际联网检查交给 post_probe_request。

调用图:调用 2 个内部函数(build_probe_client, post_probe_request);被 1 处调用(main);外部调用 4 个(from_secs, builder, var, var_os)。

build_probe_client65–78 ↗
fn build_probe_client(
    builder: reqwest::ClientBuilder,
    proxy_url: Option<&str>,
) -> Result<reqwest::Client, String>

作用:这个函数负责真正建出用于探测的 reqwest 客户端,并把自定义 CA 证书的加载逻辑接进来。有人会用它来验证证书环境变量、代理配置和客户端构造能不能正常配合。

数据流:进去的是一个 reqwest::ClientBuilder(还没建好的客户端配置)和一个可选代理地址。若有代理地址,它先把这个地址变成 HTTPS 代理配置,再调用 codex_client::build_reqwest_client_with_custom_ca 来建客户端;若没有代理,则调用 codex_client::build_reqwest_client_for_subprocess_tests。无论哪条路,出来的是建好的客户端,或一段适合打印给人的错误信息。

调用关系:它由 run_probe 调用,是探针里连接“测试参数”和“真实客户端构造逻辑”的地方。它不会重复实现 CA 加载规则,而是转交给 codex_client 里的共享函数,这样测试验证的就是产品代码真正会走的路径。

调用图:被 1 处调用(run_probe);外部调用 4 个(proxy, build_reqwest_client_for_subprocess_tests, build_reqwest_client_with_custom_ca, https)。

post_probe_request80–100 ↗
async fn post_probe_request(client: &reqwest::Client, url: &str) -> Result<(), String>

作用:这个函数用已经建好的客户端向测试服务器发一次 POST 请求,确认客户端不只是能创建,还真的能通过 HTTPS 完成通信。

数据流:进去的是 reqwest 客户端和目标网址。它向该网址发送 POST,请求头标明表单内容,正文放入一段模拟授权码交换的数据。收到响应后,它读取状态码和正文:状态码不是成功就返回错误;正文不是“ok”也返回错误;都符合才返回成功。

调用关系:它由 run_probe 在测试提供目标网址时调用。build_probe_client 先负责“把车造好”,post_probe_request 再负责“开出去跑一圈”,这样测试既能检查配置加载,也能检查实际网络请求是否通畅。

调用图:被 1 处调用(run_probe);外部调用 2 个(post, format!)。

执行策略和 exec-server 工具

这些二进制程序公开策略检查和远程执行辅助进程,包括仅用于测试的替代环境包装器。

exec-server/src/fs_helper_main.rs源码 ↗
entrypointstartup and single request handling

这个文件像一个临时请来的跑腿小帮手:主程序把要做的文件操作写到它的输入里,它读懂后去执行,然后把成功结果或错误原因写回去。这样做的好处是,文件系统操作可以放在一个单独进程里完成,和主程序隔开,出错时也更容易控制。它先启动 Tokio 异步运行时(可以理解成让程序能高效等待输入输出的调度器),然后调用真正干活的 run_main。run_main 会把标准输入全部读完,把 JSON 解析成 FsHelperRequest,再交给 run_direct_request 执行。执行成功就包装成 FsHelperResponse::Ok,失败就包装成 FsHelperResponse::Error,最后再转成 JSON 输出一行。这个程序只处理一次请求,处理完就退出。

函数细节2
main11–29 ↗
fn main() -> !

作用:这是这个助手进程的入口。它负责搭好异步运行环境,运行实际逻辑,并根据成功或失败决定进程退出码。

数据流:程序启动后进入 main;它先创建一个 Tokio 运行时,然后在里面运行 run_main;如果 run_main 成功,就用退出码 0 表示一切正常,如果出错,就把错误打印到标准错误并用退出码 1 表示失败;如果运行时本身都创建不了,也打印错误并失败退出。

调用关系:main 是最外层的门口。它不直接处理请求内容,而是把真正的读输入、执行请求、写输出这些活交给 run_main;自己只负责启动环境、收尾和向操作系统报告成功还是失败。

调用图:调用 1 个内部函数(run_main);外部调用 3 个(eprintln!, exit, new_current_thread)。

run_main31–45 ↗
async fn run_main() -> Result<(), Box<dyn Error + Send + Sync>>

作用:这个函数完成一次完整的请求处理:读入请求、执行文件系统操作、写回响应。有人调用它,就是想让这个助手真正干一单活。

数据流:它从标准输入读取全部字节,把这些字节当作 JSON 解析成 FsHelperRequest;然后把请求交给 run_direct_request 执行;执行成功时生成成功响应,执行失败时生成错误响应;最后把响应转成 JSON,写到标准输出,并额外写一个换行。

调用关系:run_main 由 main 启动后调用,是这个小程序的核心流程。它自己负责输入输出和格式转换,但真正的文件系统动作交给 run_direct_request;拿到结果后,它再统一包装成 FsHelperResponse 返回给调用方所在的外部程序。

调用图:调用 1 个内部函数(run_direct_request);被 1 处调用(main);外部调用 7 个(new, Error, Ok, stdin, stdout, from_slice, to_string)。

exec-server/src/server.rs源码 ↗
entrypointstartup

这个文件本身不做复杂工作,更像一块总开关面板。exec-server 内部有很多小模块:有的负责听网络连接,有的负责处理请求,有的负责记录会话,有的负责操作进程或文件系统。外部代码如果直接碰这些零件,会很容易用错,也会被内部结构绑住。所以这里把这些模块声明进来,并只公开少数真正需要给外面用的东西,比如 ExecServerHandler、ConnectionProcessor、默认监听地址,以及监听地址解析错误。最重要的是 run_main:调用者只要给它一个监听地址和运行时路径,它就把启动服务这件事交给 transport 层去做。可以把它理解成酒店前台:客人不用知道后厨、客房、安保怎么分工,只要从前台进入,前台会把事情转给对应部门。

函数细节1
run_main16–21 ↗
async fn run_main(
    listen_url: &str,
    runtime_paths: ExecServerRuntimePaths,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>

作用:这是启动 exec-server 的主入口。外部只需要传入服务器要监听的地址,以及运行时需要用到的路径,它就会开始启动底层的网络服务。

数据流:进去的是 listen_url,也就是服务要在哪个地址上等待连接,以及 runtime_paths,也就是运行时要用到的一组文件路径。它自己不解析请求、不处理进程,也不直接收发网络数据,而是把这两样信息原样交给 transport::run_transport。出来的是一个异步结果:成功时表示服务正常结束,失败时带回一个可描述的错误。

调用关系:它位于整个服务启动流程的最外层,通常由更上层的程序入口在启动时调用。run_main 接到启动参数后,把实际监听网络、接受连接、驱动后续处理的工作交给 run_transport;后面的连接处理、请求分发等内部流程都从这个传输层启动。

调用图:调用 1 个内部函数(run_transport)。

exec-server/testing/windows_exec_server.rs源码 ↗
entrypointtest startup

这个文件像一个“测试专用的小开关”。在 Windows 相关测试里,测试只需要一个能启动命令执行服务的程序,不需要完整的 Codex 应用。完整程序在当前构建系统里还不好做 Windows 交叉编译,所以这里单独做一个最小入口。程序启动后,先找到自己这个可执行文件的位置,然后把它交给 exec-server 的运行路径配置。因为这个程序本身就是 Windows 可执行文件,所以不需要额外指定 Linux 沙箱程序。最后它让 exec-server 监听一个本机 WebSocket 地址。WebSocket 可以理解成一条长期连接的通信管道;这里端口写 0,意思是让系统自动挑一个可用端口。这样测试可以在 Wine 等环境里更快、更稳定地启动这个小服务。

函数细节1
main11–18 ↗
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>>

作用:这是这个测试用 Windows 执行服务器的启动入口。它负责准备运行所需的路径,然后把真正的服务器启动工作交给 exec-server 库。

数据流:进去时没有命令行参数参与主要逻辑;它先读取当前正在运行的可执行文件路径,也就是自己的位置。接着用这个路径创建 ExecServerRuntimePaths,并明确告诉系统这里不需要 Linux 沙箱程序。最后它把本机 WebSocket 地址和这些运行路径交给 codex_exec_server::run_main,结果是启动一个用于测试的执行服务器;如果中途找路径或启动失败,就把错误返回出去。

调用关系:测试运行这个 Windows 小程序时会先进入 main。main 自己只做启动前的准备:调用 current_exe 找到自身位置,调用 ExecServerRuntimePaths::new 组装运行路径,然后调用 codex_exec_server::run_main,把后续监听连接、接收请求、执行任务这些真正的服务流程交出去。

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

exec-server/testing/wine_remote_test_runner.rs源码 ↗
entrypoint测试进程启动到测试结束期间

这个文件解决的是“测试要在特殊环境里跑”的问题。Bazel 测试规则会先把真正的测试程序路径放进环境变量里,这个启动器拿到路径后,再决定怎么运行它。如果只是测试框架来询问“有哪些测试”(参数正好是 --list --format terse),它就直接运行测试程序列清单,不启动 Wine 执行服务器,避免多余开销。真正跑测试时,它会临时启动一个 WineExecServer,可以理解成给测试准备一个“远程跑程序的小房间”。然后它把服务器地址写进环境变量,清掉旧的远程环境变量,转发命令行参数,并把测试程序的输出直接接到当前终端。测试结束后,如果子进程失败,它也会把失败传出来,让外层测试系统知道这次测试没过。

函数细节2
main18–54 ↗
async fn main() -> Result<()>

作用:这是这个测试启动器的入口。它读取真正要跑的测试程序位置,判断当前是“列出测试”还是“执行测试”,然后用合适的方式启动子进程。

数据流:进去的是当前进程的环境变量和命令行参数:环境变量里有真正测试二进制文件的位置,命令行参数来自测试框架。它先把测试程序路径取出来,再把自己的参数原样收集起来;如果发现只是请求列测试清单,就直接启动测试程序并等待结果。否则,它启动一个 WineExecServer,把服务器地址和测试环境标记塞进子进程环境里,移除旧环境变量,运行真正的测试程序。出来的是成功或失败结果;同时它会把子进程的标准输出和错误输出原样显示出来。

调用关系:它是整个流程的总指挥。开始时会调用 is_terse_list_request 判断是不是只要测试清单;如果是,就把活儿直接交给真实测试程序。不是的话,它把测试执行包在 WineExecServer 的作用域里,让服务器先准备好,再把真实测试程序作为子进程跑起来,最后根据子进程退出状态决定自己是否报错。

调用图:调用 1 个内部函数(is_terse_list_request);外部调用 5 个(from, ensure!, new, args_os, var_os)。

is_terse_list_request56–62 ↗
fn is_terse_list_request(args: &[OsString]) -> bool

作用:这个小函数专门判断命令行参数是不是测试框架常用的“简洁列出测试列表”请求。这样 main 就能避免为一个简单查询启动完整的 Wine 远程执行环境。

数据流:进去的是一串命令行参数。它把这些参数按顺序和固定的三个值比较:--list、--format、terse。完全一样就返回 true,表示只是列测试;只要数量或内容不同,就返回 false,表示应该按正常测试执行来处理。

调用关系:它只被 main 使用,位置很靠前,像一个门口分流牌。main 先问它“这次是不是只查名单”,如果答案是 yes,就走轻量路径;如果答案是 no,main 才继续启动 WineExecServer 并运行完整集成测试。

调用图:被 1 处调用(main);外部调用 2 个(new, iter)。

execpolicy-legacy/src/main.rs源码 ↗
entrypointcommand invocation

这个文件像一个“门卫”。外部程序把想运行的命令交给它,它先读安全策略:用户可以用参数指定策略文件,不指定就用默认策略。然后它支持两种输入方式:一种是直接把命令和参数写在命令行里,另一种是传入一段 JSON。拿到命令后,它会组装成一次执行请求,交给策略对象去检查。检查结果分几类:确认安全、匹配到允许规则但可能会改文件、明确禁止、或者因为信息不够无法验证。最后它把结果统一变成机器容易读取的 JSON,并根据 require_safe 选项决定是否用特殊退出码提醒上层程序。这里重要的一点是:它不真的执行命令,只是提前判断“能不能执行、风险多大”。

函数细节4
main62–95 ↗
fn main() -> Result<()>

作用:这是程序启动后第一个真正干活的地方。它读取命令行参数,加载策略,拿到要检查的命令,然后输出检查结果并结束进程。

数据流:进去的是用户在命令行里给的参数,以及可选的策略文件路径。它先初始化日志,再解析参数;如果给了策略文件,就从磁盘读出来并解析,如果没给就取默认策略;接着把输入命令整理成 ExecArg;最后调用 check_command 得到结果和退出码,把结果转成 JSON 打印到标准输出,并用对应退出码退出。

调用关系:它是整个工具的总调度员。启动时它会调用日志初始化、命令行解析、默认策略获取或策略解析;真正判断命令安不安全的活交给 check_command。check_command 返回以后,main 负责把结论包装成对外可读的 JSON 和进程退出码。

调用图:调用 3 个内部函数(check_command, get_default_policy, new);外部调用 7 个(parse, init, eprintln!, println!, to_string, read_to_string, exit)。

check_command97–125 ↗
fn check_command(
    policy: &Policy,
    ExecArg { program, args }: ExecArg,
    check: bool,
) -> (Output, i32)

作用:这个函数负责把“某个程序加一组参数”拿去问策略:这条命令到底安全不安全。它把策略库返回的内部判断,翻译成这个命令行工具对外输出的结果和退出码。

数据流:进去的是一个 Policy,也就是安全规则集合;一个 ExecArg,也就是程序名和参数;还有 check 标记,表示失败时是否要用特殊退出码。它把程序名和参数变成 ExecCall 后调用策略的 check 方法。出来的是一个 Output 和一个整数退出码:安全就是 safe,可能写文件就是 match,禁止就是 forbidden,判断不了就是 unverified;如果 require_safe 模式打开,后三类会带不同的非零退出码。

调用关系:main 在整理好输入后会调用它。它自己不读文件、不解析命令行,只专心向 policy.check 询问结果,然后把策略层的回答翻译成命令行程序最终要打印和返回的形式。

调用图:被 1 处调用(main);外部调用 1 个(check)。

deserialize_from_json153–161 ↗
fn deserialize_from_json(deserializer: D) -> Result<ExecArg, D::Error>

作用:这个函数专门处理 check-json 子命令里的 JSON 参数。它把命令行里传进来的字符串,解析成程序能理解的 ExecArg。

数据流:进去的是反序列化器提供的一段字符串。它先把输入当作普通字符串取出来,再用 JSON 解析器把这段字符串解码成 ExecArg;如果 JSON 写错了,就把错误包装成更清楚的“JSON parse error”。出来的是成功解析出的 ExecArg,或者一个能告诉用户 JSON 有问题的错误。

调用关系:它不是 main 直接调用的,而是在 clap 和 serde 解析命令行参数时被自动用到。也就是说,当用户使用 check-json 并传入 JSON 对象时,这个函数负责把那段文本转换成后续 main 和 check_command 能使用的数据。

调用图:外部调用 2 个(deserialize, from_str)。

ExecArg::from_str166–168 ↗
fn from_str(s: &str) -> Result<Self, Self::Err>

作用:这个函数让 ExecArg 可以从一段字符串直接生成。简单说,就是支持“把 JSON 字符串变成一条待检查命令”。

数据流:进去的是一段字符串,通常应该是 JSON,比如包含 program 和 args 的对象。它调用 JSON 解析器,把字符串转成 ExecArg;成功时出来的是程序名和参数列表,失败时出来的是解析错误。

调用关系:它实现的是 Rust 的 FromStr 约定,也就是“从字符串创建这个类型”的通用接口。其他需要把文本形式的命令变成 ExecArg 的地方可以复用它;内部实际工作交给 serde_json::from_str 完成。

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

execpolicy/src/main.rs源码 ↗
entrypointstartup / CLI invocation

这个文件很小,但位置很关键:它就像一家店的前台。用户运行 codex-execpolicy check ... 时,程序先来到这里。它用 clap 这个命令行解析工具,把用户输入的文字参数整理成程序能理解的结构。这里目前只支持一个子命令:Check,意思是“拿一个命令去对照某个执行策略,看看能不能执行”。真正怎么检查策略,并不写在这里,而是交给 codex_execpolicy::ExecPolicyCheckCommand 去做。这样做的好处是入口文件保持简单,只管“接待和分发”;具体规则判断放在专门的地方,后面扩展新命令时也更清楚。

函数细节1
main13–18 ↗
fn main() -> Result<()>

作用main 是这个命令行程序启动后第一个执行的函数。它负责把用户输入的命令行参数解析出来,并根据用户选择的子命令,把工作交给对应的处理代码。

数据流:进去的是操作系统传给程序的命令行参数;它先通过 Cli::parse() 读取并解析这些参数,把一串文字变成明确的命令类型;然后如果用户选择的是 Check,就调用这个检查命令的 run() 去执行实际检查;最后返回成功或错误结果。

调用关系:它处在整个程序的最前面,是入口。它会调用外部库提供的 parse 来理解命令行输入;解析完成后,它不自己做策略判断,而是把任务交给 ExecPolicyCheckCommand 对应的执行逻辑。

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

execpolicy/src/execpolicycheck.rs源码 ↗
orchestration用户运行 execpolicycheck 子命令时活跃

可以把这个文件理解成一个“政策查询窗口”。用户给它一批 execpolicy 规则文件,再给它一条想执行的命令,它就先把规则文件从磁盘读出来,交给策略解析器变成程序能理解的规则表。然后它拿这条命令去规则表里查,看哪些规则命中了,以及最终得到什么决定。这里的 JSON 是一种常见的结构化文本格式,方便机器读取;如果用户加了 pretty 选项,输出会排版得更好看。还有一个重要开关是 resolve_host_executables,它会影响绝对路径程序名和规则里简单文件名之间的匹配方式,但前提是策略里允许这样做。没有这个文件,用户就很难直接从命令行快速验证“这条命令会被哪些规则管住”。

函数细节3
ExecPolicyCheckCommand::run43–57 ↗
fn run(&self) -> Result<()>

作用:这是这个子命令真正开始干活的地方。它负责把“读规则、查命令、输出结果”这几步串起来,让用户一次运行就能看到检查结果。

数据流:进去的是用户在命令行里填的规则文件路径、要检查的命令、是否美化 JSON、是否解析主机可执行文件路径这些选项。它先调用 load_policies 把规则文件变成 Policy,再用这份 Policy 去匹配命令,得到命中的规则列表。接着它把匹配结果交给 format_matches_json 变成 JSON 字符串,最后打印到屏幕上;如果中途读文件、解析或序列化失败,就把错误返回出去。

调用关系:它由 run_execpolicycheck 在需要执行这个子命令时调用。它自己不亲自读文件细节,也不亲自拼 JSON,而是把读规则的活交给 load_policies,把整理输出的活交给 format_matches_json,最后用 println! 把结果交给用户。

调用图:调用 2 个内部函数(format_matches_json, load_policies);被 1 处调用(run_execpolicycheck);外部调用 1 个(println!)。

format_matches_json60–71 ↗
fn format_matches_json(matched_rules: &[RuleMatch], pretty: bool) -> Result<String>

作用:这个函数把“哪些规则命中了、最终决定是什么”整理成 JSON 文本。有人想把匹配结果给人看或给别的程序吃时,就会用到它。

数据流:进去的是一组 RuleMatch,也就是命中的规则记录,以及一个 pretty 开关。它先把这些命中记录放进输出结构里,再从所有命中规则里取出最高优先级的 decision;如果没有命中,就不写 decision 字段。最后根据 pretty 开关,输出普通紧凑 JSON,或者带缩进和换行的好读 JSON。

调用关系:它在 ExecPolicyCheckCommand::run 已经完成规则匹配之后被调用。它不参与判断规则,只负责把结果包装成稳定的输出格式,并借助 serde_json 的 to_string 或 to_string_pretty 完成真正的 JSON 转换。

调用图:被 1 处调用(run);外部调用 3 个(iter, to_string, to_string_pretty)。

load_policies73–86 ↗
fn load_policies(policy_paths: &[PathBuf]) -> Result<Policy>

作用:这个函数负责把一个或多个策略文件从磁盘读出来,并解析成一份可查询的 Policy。它解决的是“文件里的规则文本怎么变成程序能用的规则对象”的问题。

数据流:进去的是一串策略文件路径。它先创建一个 PolicyParser,也就是策略解析器;然后逐个读取文件内容,把文件路径当作标识,把文本交给解析器解析。所有文件都处理完后,它让解析器生成最终的 Policy 返回。读文件失败或解析失败时,它会带上具体文件路径,返回更容易理解的错误信息。

调用关系:它由 ExecPolicyCheckCommand::run 在开始匹配命令前调用。它先通过 new 准备解析器,再通过 read_to_string 从磁盘取出每个规则文件的文本;完成后把整理好的 Policy 交回给 run,让后续的命令匹配可以继续。

调用图:调用 1 个内部函数(new);被 1 处调用(run);外部调用 1 个(read_to_string)。

state/src/bin/logs_client.rs源码 ↗
entrypointstartup 后进入 main loop,持续轮询并打印日志

这个文件就是 codex-state-logs 这个日志查看工具的入口。它先读命令行参数,比如日志库在哪里、只看 warn 以上、从哪个时间开始、是否只显示简洁格式等。然后它把这些参数整理成一个 LogFilter,再转换成真正查询数据库用的 LogQuery。启动后,它会先打印一批历史日志,叫 backfill,可以理解成先补看前面的几页;之后进入循环,每隔一小段时间去数据库问一次“有没有比上次更新的日志”,有就打印出来。打印时还会做一些人性化处理:时间和级别会带颜色;如果日志内容像代码补丁,新增行显示绿色、删除行显示红色。没有这个文件,用户仍然可能有日志数据库,但缺少一个方便、安全、带过滤能力的查看窗口。

函数细节20
LogLevelThreshold::levels_upper93–102 ↗
fn levels_upper(self) -> Vec<String>

作用:把用户选的最低日志级别,变成一组实际要查的级别。比如选 Warn,意思不是只看 WARN,而是看 WARN 和更严重的 ERROR。

数据流:进去的是一个日志级别门槛,比如 Trace、Info、Warn → 它按照“严重程度从低到高”的规则列出应该包含的级别,并统一转成大写字符串 → 出来的是字符串列表,后面会拿去过滤数据库里的日志。

调用关系:它是过滤条件准备过程里的小零件。build_filter 在看到用户传了 --level 时,会用它把一个门槛值扩展成数据库能理解的一组级别。

main106–131 ↗
async fn main() -> anyhow::Result<()>

作用:这是整个命令行工具真正开始运行的地方。它负责读参数、准备数据库运行环境、先打印历史日志,然后一直等待并打印新日志。

数据流:进去的是用户在命令行输入的选项和环境变量 → 它解析参数,找到日志数据库位置,建立过滤条件,初始化 StateRuntime(访问状态数据库的一套运行环境),先查一批旧日志并打印,再记住最后看到的日志编号 → 之后它不断查询编号更大的新日志,打印到终端,并更新最后编号。

调用关系main 像一个现场指挥。它把路径问题交给 resolve_db_path,把过滤条件交给 build_filter,把历史日志交给 print_backfill,如果没有历史输出就用 fetch_max_id 对齐当前位置,循环里再用 fetch_new_rows 拿新日志。

调用图:调用 6 个内部函数(build_filter, fetch_max_id, fetch_new_rows, print_backfill, resolve_db_path, init);外部调用 4 个(from_millis, parse, println!, sleep)。

resolve_db_path133–140 ↗
fn resolve_db_path(args: &Args) -> anyhow::Result<PathBuf>

作用:决定这次应该看哪个日志数据库文件。用户如果直接给了数据库路径,就用用户给的;否则按 Codex 的默认目录推算。

数据流:进去的是命令行参数 Args → 它先看 --db 有没有指定;如果有,就直接返回这个路径;如果没有,就取 --codex-home 或默认家目录下的 .codex,再拼出日志数据库路径 → 出来的是一个数据库文件路径。

调用关系main 启动时先调用它,解决“日志库到底在哪”这个基础问题。它内部会用 default_codex_home 的规则,并调用外部的 logs_db_path 来拼出标准日志库位置。

调用图:被 1 处调用(main);外部调用 1 个(logs_db_path)。

default_codex_home142–147 ↗
fn default_codex_home() -> PathBuf

作用:给 Codex 主目录找一个兜底位置。用户没指定目录时,就需要它来决定默认去哪找。

数据流:进去没有参数 → 它先尝试读取当前用户的家目录;如果能读到,就返回家目录下面的 .codex;如果连家目录都拿不到,就返回当前目录下的 .codex → 出来的是一个目录路径。

调用关系:它是 resolve_db_path 的后备方案。只有用户没有传 --codex-home,也没有直接传 --db 时,这个默认路径才会派上用场。

调用图:外部调用 2 个(from, home_dir)。

build_filter149–195 ↗
fn build_filter(args: &Args) -> anyhow::Result<LogFilter>

作用:把零散的命令行筛选项整理成一个统一的日志过滤器。这样后面查询数据库时不用再到处理解命令行参数。

数据流:进去的是 Args,里面有级别、起止时间、模块名、文件名、线程号、搜索词等 → 它解析时间,去掉空字符串,把日志级别展开成大写列表,并保留是否包含无线程日志的开关 → 出来的是 LogFilter,也就是一包干净的筛选条件。

调用关系main 在启动阶段调用它。它会间接用 parse_timestamp 处理 --from--to,并用 LogLevelThreshold::levels_upper 处理 --level。后面的 to_log_query 会继续把这个过滤器翻译成数据库查询对象。

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

parse_timestamp197–205 ↗
fn parse_timestamp(value: &str) -> anyhow::Result<i64>

作用:把用户输入的时间变成统一的秒数。它支持两种写法:Unix 秒数,或者 RFC3339 时间字符串,比如带时区的标准时间。

数据流:进去的是一段文本 → 它先尝试把文本当整数秒数读;如果失败,再按 RFC3339 标准时间解析 → 出来的是 Unix 时间戳秒数;如果两种都不对,就返回带说明的错误。

调用关系build_filter 在处理 --from--to 时会调用它。它把人类输入的时间格式变成数据库查询更容易比较的数字。

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

print_backfill207–226 ↗
async fn print_backfill(
    runtime: &StateRuntime,
    filter: &LogFilter,
    backfill: usize,
    compact: bool,
) -> anyhow::Result<i64>

作用:启动时先打印一批已经存在的日志。这样用户不是只看到“从现在开始”的内容,也能看到刚刚发生过的上下文。

数据流:进去的是数据库运行环境、过滤器、要补看的条数,以及是否使用简洁格式 → 如果条数是 0,它什么也不查,返回 0;否则它查询最近的若干条匹配日志,把顺序反过来变成从旧到新,逐条打印 → 出来的是已经打印过的最大日志编号。

调用关系main 启动后调用它来补看历史日志。它把真正查数据库的活交给 fetch_backfill,打印每一行时使用 format_row 做终端显示格式。

调用图:调用 1 个内部函数(fetch_backfill);被 1 处调用(main);外部调用 1 个(println!)。

fetch_backfill228–243 ↗
async fn fetch_backfill(
    runtime: &StateRuntime,
    filter: &LogFilter,
    backfill: usize,
) -> anyhow::Result<Vec<LogRow>>

作用:从数据库里取出启动时要补看的那批历史日志。它默认按倒序拿最近的日志,避免一次扫太多。

数据流:进去的是运行环境、过滤器、条数上限 → 它用 to_log_query 把过滤器变成查询条件,并设置限制条数、按新到旧排序 → 出来的是一组 LogRow 日志行;如果查询失败,会带上“获取历史日志失败”的说明。

调用关系:它被 print_backfill 调用,专门服务启动时的历史补看。它不负责打印,只负责把符合条件的日志行从状态运行环境里取出来。

调用图:调用 1 个内部函数(to_log_query);被 1 处调用(print_backfill);外部调用 1 个(query_logs)。

fetch_new_rows245–260 ↗
async fn fetch_new_rows(
    runtime: &StateRuntime,
    filter: &LogFilter,
    last_id: i64,
) -> anyhow::Result<Vec<LogRow>>

作用:查询上次看到之后新增的日志。它是持续 tail 日志时最核心的读取动作。

数据流:进去的是运行环境、过滤器、上次看到的最大日志编号 last_id → 它构造一个“只要编号大于 last_id”的查询,并按从旧到新的顺序取 → 出来的是新出现的日志行列表。

调用关系main 的无限循环里反复调用它。查到新日志后,main 会打印并更新 last_id,下一轮就不会重复打印这些日志。

调用图:调用 1 个内部函数(to_log_query);被 1 处调用(main);外部调用 1 个(query_logs)。

fetch_max_id262–270 ↗
async fn fetch_max_id(runtime: &StateRuntime, filter: &LogFilter) -> anyhow::Result<i64>

作用:在没有打印历史日志时,找出当前符合条件的最大日志编号。这样工具可以从“现在”开始看,不会突然把旧日志全刷出来。

数据流:进去的是运行环境和过滤器 → 它构造一个没有条数限制、没有起始编号的查询条件,然后询问数据库当前最大的日志 ID → 出来的是一个数字,表示当前最新日志的位置。

调用关系mainprint_backfill 没有产生任何最后编号时调用它。它的作用像给书签定位:先把书签夹到当前最后一页,再开始等新内容。

调用图:调用 1 个内部函数(to_log_query);被 1 处调用(main);外部调用 1 个(max_log_id)。

to_log_query272–291 ↗
fn to_log_query(
    filter: &LogFilter,
    limit: Option<usize>,
    after_id: Option<i64>,
    descending: bool,
) -> LogQuery

作用:把本文件内部使用的 LogFilter 转成状态库真正会执行的 LogQuery。这是“界面筛选条件”和“数据库查询条件”之间的翻译器。

数据流:进去的是过滤器、可选条数限制、可选的起始日志编号、排序方向 → 它把级别、时间、模块、文件、线程、搜索词等字段复制到 LogQuery,再补上本次查询特有的限制和排序 → 出来的是可交给状态运行环境执行的查询对象。

调用关系fetch_backfillfetch_new_rowsfetch_max_id 都用它。这样三种查询虽然用途不同,但过滤规则始终保持一致,不会各写各的导致结果不一致。

调用图:被 3 处调用(fetch_backfill, fetch_max_id, fetch_new_rows)。

format_row293–311 ↗
fn format_row(row: &LogRow, compact: bool) -> String

作用:把一条数据库里的日志记录变成适合终端显示的一行文字。它不仅拼字段,还会加颜色和简洁/详细两种版式。

数据流:进去的是一条 LogRowcompact 开关 → 它格式化时间,给日志级别上色,处理线程号、目标模块和消息内容;如果是简洁模式,只显示时间、级别、消息;否则还显示线程和目标 → 出来的是一段可以直接 println! 的字符串。

调用关系:它是打印前的最后一道包装工序。mainprint_backfill 在拿到日志行后会用它生成终端文本;它又把时间格式交给 formatter::ts,级别颜色交给 formatter::level,消息内容交给 heuristic_formatting

调用图:调用 1 个内部函数(heuristic_formatting);外部调用 3 个(format!, level, ts)。

heuristic_formatting313–319 ↗
fn heuristic_formatting(message: &str) -> String

作用:给日志消息做一点“猜测式美化”。目前它主要识别补丁内容,让代码增删行更容易看清。

数据流:进去的是日志消息文本 → 它先问 matcher::apply_patch 这段消息是不是 apply_patch 工具调用;如果是,就按补丁格式染色;如果不是,就简单加粗 → 出来的是带终端颜色效果的字符串。

调用关系format_row 在处理消息正文时调用它。它自己不懂数据库,也不管整行日志,只负责让消息内容更好读。

调用图:被 1 处调用(format_row);外部调用 2 个(apply_patch, apply_patch)。

matcher::apply_patch322–324 ↗
fn apply_patch(message: &str) -> bool

作用:判断一段日志消息是不是和 apply_patch 工具调用有关。这个判断很简单,就是看消息里有没有固定文字。

数据流:进去的是消息文本 → 它检查里面是否包含 ToolCall: apply_patch → 出来的是 true 或 false,表示要不要按补丁内容特殊显示。

调用关系:它被 heuristic_formatting 用作分岔判断。判断为真时,后续会交给 formatter::apply_patch 做红绿着色。

formatter::apply_patch333–347 ↗
fn apply_patch(message: &str) -> String

作用:把补丁样式的日志内容染成更像代码差异的样子。新增行绿色,删除行红色,其他行加粗。

数据流:进去的是多行消息文本 → 它逐行检查开头字符:以 + 开头当新增行,以 - 开头当删除行,其他行按普通重点内容处理 → 出来的是重新拼好的带颜色多行字符串。

调用关系heuristic_formatting 在确认消息是 apply_patch 相关内容后调用它。它只负责显示效果,不改变日志本身。

formatter::ts349–356 ↗
fn ts(ts: i64, ts_nanos: i64, compact: bool) -> String

作用:把日志里的秒和纳秒时间变成人能看懂的时间字符串。简洁模式下只显示时分秒,详细模式下显示标准时间。

数据流:进去的是秒数、纳秒数和 compact 开关 → 它先把纳秒转成合法范围,再尝试构造 UTC 时间;成功时按简洁或详细格式输出,失败时退回到原始数字拼接 → 出来的是时间文本。

调用关系format_row 每打印一条日志都会用它格式化时间。它让数据库里适合计算的数字时间,变成终端里适合阅读的时间。

调用图:外部调用 3 个(from_timestamp, format!, try_from)。

formatter::level358–376 ↗
fn level(level: &str) -> String

作用:把日志级别变成固定宽度并带颜色的文字。这样 ERROR、WARN、INFO 等在终端里一眼能分出来。

数据流:进去的是级别字符串 → 它先补齐到 5 个字符宽,然后按级别选择颜色:错误红色、警告黄色、信息绿色、调试蓝色、追踪紫色,未知级别就只加粗 → 出来的是带颜色效果的级别文本。

调用关系format_row 在拼接每一行日志时调用它。它专门负责级别这一小段的可读性。

调用图:外部调用 1 个(format!)。

tests::log_level_threshold_includes_more_severe_levels385–400 ↗
fn log_level_threshold_includes_more_severe_levels()

作用:这个测试确认日志级别门槛的含义是“包含它和更严重的级别”。比如 Warn 必须包含 WARN 和 ERROR。

数据流:进去没有外部输入 → 它调用 LogLevelThreshold::levels_upper,检查 Warn 和 Trace 的输出列表是否符合预期 → 如果结果不对,测试失败;如果一致,说明级别展开规则没被改坏。

调用关系:它保护的是 LogLevelThreshold::levels_upper 的核心规则。这个规则会影响 build_filter,也就是会直接影响用户用 --level 时看到哪些日志。

调用图:外部调用 1 个(assert_eq!)。

tests::log_level_rejects_aliases_and_unknown_values403–407 ↗
fn log_level_rejects_aliases_and_unknown_values()

作用:这个测试确认命令行只接受规定好的日志级别名字,不接受别名或混合写法。比如 warningerrwarn,error 都不行。

数据流:进去没有外部输入 → 它模拟用户传入几种非法 --level 参数,让命令行解析器尝试解析 → 期望结果是都报错,否则测试失败。

调用关系:它保护 Args 的命令行解析行为。这样用户输入不标准时,工具会明确拒绝,而不是悄悄按错误方式查询日志。

调用图:外部调用 1 个(assert!)。

tests::log_level_accepts_canonical_values_case_insensitively410–415 ↗
fn log_level_accepts_canonical_values_case_insensitively()

作用:这个测试确认标准日志级别可以不区分大小写输入。比如用户写 WARN,也能被识别成 Warn。

数据流:进去没有外部输入 → 它模拟命令行参数 --level WARN,让解析器读取 → 出来应该是 Some(LogLevelThreshold::Warn);如果不是,测试失败。

调用关系:它保护命令行的易用性。前面的拒绝测试保证不乱收别名,这个测试保证合法值即使大小写不同也能正常使用。

调用图:外部调用 2 个(try_parse_from, assert_eq!)。

Linux 沙盒和 shell 包装器

此组涵盖面向 Linux 的进程隔离入口点、bubblewrap 包装器,以及 Unix execve 拦截辅助工具。

linux-sandbox/src/lib.rs源码 ↗
entrypointstartup

这个文件像一个门厅:外面的人只从这里进来,里面再分流到真正的房间。项目的目标是在 Linux 上把要运行的程序关进“沙箱”,限制它能改什么文件、能做什么系统操作。这里先按操作系统判断:只有在 Linux 上,才加载 bubblewrap、seccomp、landlock 等相关模块;这些名字都是 Linux 下的隔离和权限限制工具,用来减少程序乱动系统的机会。然后它公开一个 run_main 函数。在 Linux 上,这个函数把控制权交给 linux_run_main::run_main,也就是实际启动沙箱流程的地方。在非 Linux 系统上,它直接报错退出,因为这套沙箱依赖 Linux 特有能力,换到别的系统就不能安全工作。

函数细节1
run_main29–31 ↗
fn run_main() -> !

作用:这是外部启动 Linux 沙箱时调用的统一入口。它的用处是隐藏平台差异:Linux 上继续启动真正的沙箱流程,非 Linux 上明确告诉用户“不支持”。

数据流:进去时没有普通参数,它主要读取的是编译时的操作系统条件。若目标系统是 Linux,它不自己处理细节,而是把执行权交给 linux_run_main::run_main,之后流程不会返回;若不是 Linux,它触发 panic,也就是直接报错中止,说明 codex-linux-sandbox 只能在 Linux 上用。

调用关系:它站在整个沙箱程序的最外层。外部启动代码会先调用 run_main;在 Linux 环境下,它立刻把活儿交给真正的运行主流程 run_main;在不支持的平台上,它调用 panic!,避免程序假装能运行却没有安全隔离效果。

调用图:调用 1 个内部函数(run_main);外部调用 1 个(panic!)。

linux-sandbox/src/linux_run_main.rs源码 ↗
entrypointstartup, sandbox setup, child process lifetime, teardown

这个文件像一个“安全门卫”。用户给它一个命令和一份权限说明,它先读懂这份说明,再决定用哪种 Linux 沙箱办法执行:默认用 bubblewrap(一种把进程放进隔离文件系统和网络空间的工具),必要时再在里面加 seccomp(一道系统调用过滤网,限制程序能向内核请求什么)。如果走旧方案,也可以用 Landlock(Linux 的文件访问限制机制)。它还处理一些很容易出错的边角:有的容器不允许挂载新的 /proc,它会先试跑一次,失败就换方案;有些临时创建的挂载目标要登记和清理,避免多个沙箱互相踩;收到 Ctrl+C 等信号时,要转发给真正运行的子进程;如果沙箱里的程序偷偷创建了不该创建的工作区元数据,它会删除并把这次运行视为违规。整体上,它负责从“收到命令”到“安全运行并正确退出”的完整流程。

函数细节62
run_main147–255 ↗
fn run_main() -> !

作用:这是这个沙箱助手真正开始干活的入口。它读取命令行参数,确认权限配置,然后选择用 bubblewrap、seccomp 或旧的 Landlock 路线来运行用户命令。

数据流:进去的是命令行参数和权限配置 → 它解析出工作目录、网络策略、文件策略和用户命令 → 根据情况先搭沙箱、再加限制,最后用 exec 把当前进程替换成用户命令;如果配置不合法就直接报错退出。

调用关系:它是整个文件的总调度员。它先调用 resolve_permission_profile 和两个 ensure 检查函数,再按路线调用 build_inner_seccomp_command、run_bwrap_with_proc_fallback、apply_permission_profile_to_current_thread、activate_proxy_routes_in_netns 或 exec_or_panic。

调用图:调用 9 个内部函数(apply_permission_profile_to_current_thread, build_inner_seccomp_command, ensure_inner_stage_mode_is_valid, ensure_legacy_landlock_mode_supports_policy, exec_or_panic, resolve_permission_profile, run_bwrap_with_proc_fallback, activate_proxy_routes_in_netns, prepare_host_proxy_route_spec);被 1 处调用(run_main);外部调用 2 个(parse, panic!)。

ResolvePermissionProfileError::fmt270–274 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:把权限配置解析错误变成人能看懂的错误文字。这里主要用于提示“缺少权限配置”。

数据流:进去的是一个错误类型和格式化输出位置 → 它按错误种类写入固定说明 → 出来的是可显示给用户或 panic 信息里的文字。

调用关系:resolve_permission_profile 发现没有权限配置时会产生这个错误;run_main 把它显示出来,让启动失败的原因更清楚。

调用图:外部调用 1 个(write!)。

parse_permission_profile277–279 ↗
fn parse_permission_profile(value: &str) -> std::result::Result<PermissionProfile, String>

作用:把命令行里传进来的权限配置 JSON 字符串,变成程序能使用的 PermissionProfile 对象。JSON 可以理解成一种文本格式的配置单。

数据流:进去的是一段字符串 → 它用 JSON 解析器读取 → 成功就得到权限配置对象,失败就返回带原因的错误文字。

调用关系:它挂在 LandlockCommand 的命令行参数解析上。run_main 解析参数时会间接用到它,从而拿到后续沙箱设置需要的权限说明。

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

resolve_permission_profile281–293 ↗
fn resolve_permission_profile(
    permission_profile: Option<PermissionProfile>,
) -> Result<EffectivePermissions, ResolvePermissionProfileError>

作用:确认权限配置真的存在,并把它拆成运行时要用的文件系统策略和网络策略。

数据流:进去的是一个可能为空的 PermissionProfile → 为空就返回 MissingConfiguration 错误 → 不为空就调用配置自身的转换方法,产出 EffectivePermissions,里面有原始配置、文件策略和网络策略。

调用关系:run_main 在任何沙箱动作之前调用它。它给后面的 ensure 检查、bubblewrap 参数构造和 Landlock/seccomp 应用提供统一的权限结果。

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

ensure_inner_stage_mode_is_valid295–299 ↗
fn ensure_inner_stage_mode_is_valid(apply_seccomp_then_exec: bool, use_legacy_landlock: bool)

作用:检查两个内部模式开关有没有互相冲突。它防止“已经在 bubblewrap 里面准备加 seccomp”的模式和“旧 Landlock 模式”同时打开。

数据流:进去的是两个布尔开关 → 如果两者同时为真,就立即 panic 报错 → 如果不冲突,就什么也不改,允许流程继续。

调用关系:run_main 一开始就调用它,属于早期安全检查。这样后面不会进入一个设计上说不通的沙箱组合。

调用图:被 1 处调用(run_main);外部调用 1 个(panic!)。

ensure_legacy_landlock_mode_supports_policy301–315 ↗
fn ensure_legacy_landlock_mode_supports_policy(
    use_legacy_landlock: bool,
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
    network_sandbox_policy: NetworkSandboxPolicy,
    sandbox_p

作用:检查旧 Landlock 路线能不能承载当前权限策略。某些策略必须在运行时更直接地控制,旧模式做不到,就要提前拒绝。

数据流:进去的是是否使用旧模式、文件策略、网络策略和策略工作目录 → 如果旧模式遇到不支持的策略,就 panic → 否则不产生新数据,只放行。

调用关系:run_main 在决定具体沙箱路线前调用它。它依赖 FileSystemSandboxPolicy 的 needs_direct_runtime_enforcement 判断,避免把不兼容策略交给旧实现。

调用图:调用 1 个内部函数(needs_direct_runtime_enforcement);被 1 处调用(run_main);外部调用 1 个(panic!)。

run_bwrap_with_proc_fallback317–359 ↗
fn run_bwrap_with_proc_fallback(
    sandbox_policy_cwd: &Path,
    command_cwd: Option<&Path>,
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
    network_sandbox_policy: NetworkSandboxPoli

作用:用 bubblewrap 启动沙箱,并处理 /proc 挂载可能失败的情况。/proc 是 Linux 暴露进程信息的特殊目录,有些受限容器不让重新挂载它。

数据流:进去的是工作目录、文件策略、网络策略、内部命令、是否挂载 /proc 和代理网络开关 → 它先算网络模式,必要时试跑一次检查 /proc 是否能挂 → 再生成 bubblewrap 参数,修正内部命令 argv0,最后执行或派生 bubblewrap。

调用关系:run_main 在默认 bubblewrap 路线调用它。它把具体工作分给 bwrap_network_mode、preflight_proc_mount_support、build_bwrap_argv、apply_inner_command_argv0 和 run_or_exec_bwrap。

调用图:调用 5 个内部函数(apply_inner_command_argv0, build_bwrap_argv, bwrap_network_mode, preflight_proc_mount_support, run_or_exec_bwrap);被 1 处调用(run_main);外部调用 1 个(default)。

bwrap_network_mode361–372 ↗
fn bwrap_network_mode(
    network_sandbox_policy: NetworkSandboxPolicy,
    allow_network_for_proxy: bool,
) -> BwrapNetworkMode

作用:把项目里的网络策略翻译成 bubblewrap 能理解的网络模式。比如完全隔离、完全允许,或只允许走代理桥。

数据流:进去的是网络沙箱策略和是否为代理放行的开关 → 它按优先级判断:代理模式优先,其次看网络策略是否允许联网 → 出来是 BwrapNetworkMode。

调用关系:run_bwrap_with_proc_fallback 调用它来决定 bubblewrap 的网络参数。后面的 build_bwrap_argv 会把这个模式写进真正的启动参数。

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

build_bwrap_argv374–397 ↗
fn build_bwrap_argv(
    inner: Vec<String>,
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
    sandbox_policy_cwd: &Path,
    command_cwd: &Path,
    options: BwrapOptions,
) -> CodexResul

作用:生成启动 bubblewrap 所需的完整命令行参数。它把内部要运行的命令、文件权限视图和工作目录拼成 bwrap 能执行的形式。

数据流:进去的是内部命令、文件策略、两个工作目录和 bwrap 选项 → 它调用 create_bwrap_command_args 生成核心参数,再在最前面加上 bwrap 命令名 → 出来是 BwrapArgs,里面还带着需要保留的文件和后续清理信息。

调用关系:run_bwrap_with_proc_fallback 用它构造真实运行参数;build_preflight_bwrap_argv 也用它构造试跑参数。它是本文件和 crate::bwrap 参数生成模块之间的连接点。

调用图:调用 1 个内部函数(create_bwrap_command_args);被 2 处调用(build_preflight_bwrap_argv, run_bwrap_with_proc_fallback);外部调用 1 个(vec!)。

exit_with_bwrap_build_error399–402 ↗
fn exit_with_bwrap_build_error(err: codex_protocol::error::CodexErr) -> !

作用:当 bubblewrap 命令构造失败时,用清楚的错误信息退出程序。

数据流:进去的是构造参数时产生的错误 → 它把错误写到标准错误输出 → 然后以退出码 1 结束进程。

调用关系:run_bwrap_with_proc_fallback 在 build 或 preflight 参数失败时通过 unwrap_or_else 使用它。它让失败不会继续进入不完整的沙箱启动流程。

调用图:外部调用 2 个(eprintln!, exit)。

apply_inner_command_argv0404–410 ↗
fn apply_inner_command_argv0(argv: &mut Vec<String>)

作用:调整 bubblewrap 里面重新调用本程序时显示给程序自己的名字 argv0。argv0 可以理解成进程认为自己叫什么名字。

数据流:进去的是 bubblewrap 参数列表 → 它查询当前 bubblewrap 启动器是否支持 --argv0,并拿到当前进程 argv0 作为备用 → 然后交给 apply_inner_command_argv0_for_launcher 修改参数列表。

调用关系:run_bwrap_with_proc_fallback 在真正执行 bwrap 前调用它。这样内层阶段能以预期身份重新进入本助手。

调用图:调用 3 个内部函数(preferred_bwrap_supports_argv0, apply_inner_command_argv0_for_launcher, current_process_argv0);被 1 处调用(run_bwrap_with_proc_fallback)。

apply_inner_command_argv0_for_launcher412–435 ↗
fn apply_inner_command_argv0_for_launcher(
    argv: &mut Vec<String>,
    supports_argv0: bool,
    argv0_fallback_command: String,
)

作用:根据 bubblewrap 是否支持 --argv0,用两种办法之一设置内层命令的 argv0。支持时插入专门参数,不支持时直接替换命令路径作为兼容方案。

数据流:进去的是可修改的参数数组、是否支持 --argv0、备用命令名 → 它先找到 -- 分隔符,再要么插入 --argv0 和固定名字,要么替换分隔符后的命令 → 参数数组被原地修改。

调用关系:apply_inner_command_argv0 调用它。它是一个可测试的小核心,把“不同 bwrap 版本怎么兼容”的细节集中在一处。

调用图:被 1 处调用(apply_inner_command_argv0);外部调用 1 个(panic!)。

current_process_argv0437–442 ↗
fn current_process_argv0() -> String

作用:取得当前进程启动时的第一个参数,也就是当前程序名或路径。它用于老版本 bubblewrap 不支持 --argv0 时的备用方案。

数据流:进去没有显式参数 → 它读取操作系统提供的 args_os → 成功就转成字符串返回,读不到就 panic。

调用关系:apply_inner_command_argv0 调用它,把结果交给 apply_inner_command_argv0_for_launcher 使用。

调用图:被 1 处调用(apply_inner_command_argv0);外部调用 2 个(panic!, args_os)。

preflight_proc_mount_support444–458 ↗
fn preflight_proc_mount_support(
    sandbox_policy_cwd: &Path,
    command_cwd: &Path,
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
    network_mode: BwrapNetworkMode,
) -> CodexResult<b

作用:提前测试当前环境能不能让 bubblewrap 挂载新的 /proc。这样正式运行用户命令前就知道要不要降级到 no-proc。

数据流:进去的是工作目录、文件策略和网络模式 → 它构造一个只运行 true 的临时 bwrap 命令,捕获 stderr → 如果错误文字像 /proc 挂载失败,就返回 false,否则返回 true。

调用关系:run_bwrap_with_proc_fallback 在 mount_proc 为真时调用它。它内部使用 build_preflight_bwrap_argv、run_bwrap_in_child_capture_stderr 和 is_proc_mount_failure。

调用图:调用 3 个内部函数(build_preflight_bwrap_argv, is_proc_mount_failure, run_bwrap_in_child_capture_stderr);被 1 处调用(run_bwrap_with_proc_fallback)。

build_preflight_bwrap_argv460–478 ↗
fn build_preflight_bwrap_argv(
    sandbox_policy_cwd: &Path,
    command_cwd: &Path,
    file_system_sandbox_policy: &FileSystemSandboxPolicy,
    network_mode: BwrapNetworkMode,
) -> CodexResult<cra

作用:构造用于“试跑 /proc 挂载”的 bubblewrap 参数。这个试跑命令很短,只执行 true,也就是立刻成功退出的小程序。

数据流:进去的是工作目录、文件策略和网络模式 → 它选出 true 命令,并强制设置 mount_proc 为 true → 返回一组用于预检查的 BwrapArgs。

调用关系:preflight_proc_mount_support 调用它。它复用 build_bwrap_argv,保证试跑环境和正式环境尽量一致。

调用图:调用 1 个内部函数(build_bwrap_argv);被 1 处调用(preflight_proc_mount_support);外部调用 2 个(default, vec!)。

resolve_true_command480–487 ↗
fn resolve_true_command() -> String

作用:找到系统里的 true 命令路径。true 是一个什么也不做、直接成功退出的小程序,常用于测试环境是否能启动。

数据流:进去没有显式参数 → 它依次检查 /usr/bin/true 和 /bin/true 是否存在 → 找到就返回绝对路径,找不到就返回 true 让系统 PATH 去找。

调用关系:build_preflight_bwrap_argv 用它决定预检查时运行哪个简单命令。

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

run_or_exec_bwrap489–496 ↗
fn run_or_exec_bwrap(bwrap_args: crate::bwrap::BwrapArgs) -> !

作用:决定是直接把当前进程替换成 bubblewrap,还是先派生子进程以便结束后清理临时挂载目标。

数据流:进去的是完整 BwrapArgs → 如果没有需要登记清理的 synthetic mount 或 protected create 目标,就直接 exec_bwrap → 否则进入带清理逻辑的子进程运行流程。

调用关系:run_bwrap_with_proc_fallback 最后调用它。它把简单场景交给 launcher::exec_bwrap,把复杂场景交给 run_bwrap_in_child_with_synthetic_mount_cleanup。

调用图:调用 2 个内部函数(exec_bwrap, run_bwrap_in_child_with_synthetic_mount_cleanup);被 1 处调用(run_bwrap_with_proc_fallback)。

run_bwrap_in_child_with_synthetic_mount_cleanup498–547 ↗
fn run_bwrap_in_child_with_synthetic_mount_cleanup(bwrap_args: crate::bwrap::BwrapArgs) -> !

作用:在需要事后清理时,用父子进程方式运行 bubblewrap。父进程负责看护、转发信号、清理临时文件,并按子进程结果退出。

数据流:进去的是 bwrap 参数和清理目标 → 它先登记目标、建同步管道、fork 出子进程;子进程等待父进程准备好后执行 bwrap;父进程启动监控、转发信号、等待结束、清理登记和临时目标 → 最后用子进程状态或违规结果退出。

调用关系:run_or_exec_bwrap 在有 synthetic mount 或 protected create 目标时调用它。它串起信号屏蔽、ProtectedCreateMonitor、登记清理、wait_for_bwrap_child 和 exit_with_wait_status_or_policy_violation。

调用图:调用 16 个内部函数(exec_bwrap, block, start, cleanup_protected_create_targets, cleanup_synthetic_mount_targets, close_child_exec_start_read, create_exec_start_pipe, exit_with_wait_status_or_policy_violation, install_bwrap_signal_forwarders, register_protected_create_targets (+6 more));被 1 处调用(run_or_exec_bwrap);外部调用 5 个(last_os_error, fork, getpid, setpgid, panic!)。

ProtectedCreateMonitor::start550–581 ↗
fn start(targets: &[crate::bwrap::ProtectedCreateTarget]) -> Option<Self>

作用:启动一个后台监控线程,盯着那些“不允许被创建”的路径。一旦沙箱里的程序创建了它们,就尽力删掉并记录违规。

数据流:进去的是 protected create 目标列表 → 如果为空就返回 None;否则复制目标,创建停止标记和违规标记,启动线程循环检查并等待文件创建事件 → 出来是可停止的 ProtectedCreateMonitor。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 在父进程开始放行子进程前调用它。监控线程内部会用 ProtectedCreateWatcher 和 remove_protected_create_target_best_effort。

调用图:被 1 处调用(run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 6 个(clone, new, new, is_empty, to_vec, spawn)。

ProtectedCreateMonitor::stop583–589 ↗
fn stop(self) -> bool

作用:停止 protected create 后台监控,并告诉调用者监控期间是否发现过违规创建。

数据流:进去的是 monitor 自身 → 它设置停止标记,等待线程结束 → 返回违规标记的布尔值。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 在 bubblewrap 子进程结束后调用它。返回值会参与最终是否把运行判为策略违规。

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

ProtectedCreateWatcher::new593–631 ↗
fn new(targets: &[crate::bwrap::ProtectedCreateTarget]) -> Option<Self>

作用:建立 Linux inotify 监听器,用来更快发现目标父目录里有新文件或移动进来的文件。inotify 是 Linux 通知文件变化的机制。

数据流:进去的是 protected create 目标列表 → 它打开 inotify 文件描述符,为每个目标的父目录添加监听 → 如果一个监听都没成功就关闭并返回 None,否则返回 watcher。

调用关系:ProtectedCreateMonitor::start 的后台线程会创建它。有 watcher 时线程可以等事件;没有 watcher 时只能短暂 sleep 后轮询。

调用图:外部调用 6 个(new, new, new, close, inotify_add_watch, inotify_init1)。

ProtectedCreateWatcher::wait_for_create_event633–654 ↗
fn wait_for_create_event(&self, stop: &AtomicBool)

作用:等待一小段时间,看受监听目录是否出现创建或移动事件。这样监控线程不用一直空转耗 CPU。

数据流:进去的是 watcher 和停止标记 → 它用 poll 等待 inotify 文件描述符最多约 10 毫秒 → 有事件就清空事件并返回;被打断会重试,出错或超时也返回。

调用关系:ProtectedCreateMonitor::start 创建的监控线程在每轮检查后调用它。它把实际读取事件的活交给 drain_events。

调用图:调用 1 个内部函数(drain_events);外部调用 3 个(load, last_os_error, poll)。

ProtectedCreateWatcher::drain_events656–672 ↗
fn drain_events(&self)

作用:把 inotify 队列里的文件变化事件读空。这里不关心具体事件内容,只需要知道“发生过变化”并清掉积压。

数据流:进去的是 watcher → 它循环从 inotify 文件描述符读取数据 → 读到没有更多、被打断后重试,或遇到其他错误就结束;不返回业务数据。

调用关系:ProtectedCreateWatcher::wait_for_create_event 在 poll 发现有事件后调用它,避免同一批事件反复触发。

调用图:被 1 处调用(wait_for_create_event);外部调用 2 个(last_os_error, read)。

ProtectedCreateWatcher::drop676–680 ↗
fn drop(&mut self)

作用:在 watcher 不再使用时关闭 inotify 文件描述符。文件描述符可以理解成操作系统打开资源的编号。

数据流:进去的是即将销毁的 watcher → 它调用 close 关闭 fd → 不产生返回值,只释放系统资源。

调用关系:Rust 自动在 ProtectedCreateWatcher 生命周期结束时调用它,通常发生在监控线程退出时。

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

create_exec_start_pipe683–693 ↗
fn create_exec_start_pipe(enabled: bool) -> [libc::c_int; 2]

作用:创建一个父子进程之间的小管道,用来控制子进程什么时候真正执行 bubblewrap。管道像一根只传一个信号的小水管。

数据流:进去的是是否启用同步的布尔值 → 不启用就返回两个 -1;启用就创建带 close-on-exec 标记的 pipe → 返回读端和写端 fd。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 在 fork 前调用它。子进程会等读端,父进程准备好监控和信号处理后通过写端放行。

调用图:被 1 处调用(run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 3 个(last_os_error, pipe2, panic!)。

wait_for_parent_exec_start695–719 ↗
fn wait_for_parent_exec_start(read_fd: libc::c_int, write_fd: libc::c_int)

作用:让子进程等父进程发出“可以开始执行”的信号。这样可以避免子进程太早运行,父进程还没装好监控。

数据流:进去的是管道读端和写端 fd → 子进程先关掉不用的写端,再从读端读一个字节或等到管道关闭 → 最后关闭读端继续执行。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 的子进程分支调用它。父进程对应调用 release_child_exec_start。

调用图:被 1 处调用(run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 3 个(last_os_error, close, read)。

close_child_exec_start_read721–727 ↗
fn close_child_exec_start_read(read_fd: libc::c_int)

作用:父进程关闭自己不需要的管道读端。这样资源不会泄漏,也不会影响子进程等待逻辑。

数据流:进去的是读端 fd → 如果 fd 有效就关闭 → 没有返回值。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 的父进程分支在 fork 后调用它。

调用图:被 1 处调用(run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 1 个(close)。

release_child_exec_start729–738 ↗
fn release_child_exec_start(write_fd: libc::c_int)

作用:父进程向同步管道写一个字节,告诉子进程现在可以开始执行 bubblewrap 了。

数据流:进去的是写端 fd → 如果 fd 无效就直接返回;否则写入一个字节并关闭写端 → 子进程的等待会结束。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 在装好 protected-create 监控和信号转发器后调用它。

调用图:被 1 处调用(run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 2 个(close, write)。

ForwardedSignalMask::block749–763 ↗
fn block() -> Self

作用:临时挡住一组要转发给 bubblewrap 子进程的信号,比如 Ctrl+C 对应的 SIGINT。这样在安装处理器时不会漏掉或乱处理信号。

数据流:进去没有显式参数 → 它构造包含 SIGHUP、SIGINT、SIGQUIT、SIGTERM 的信号集合,并让当前线程阻塞这些信号,同时保存旧设置 → 返回可恢复的 ForwardedSignalMask。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 在 fork 和清理等敏感阶段调用它;之后会用 restore 恢复。

调用图:被 2 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 6 个(last_os_error, sigaddset, sigemptyset, sigprocmask, panic!, zeroed)。

ForwardedSignalMask::restore765–776 ↗
fn restore(&self)

作用:恢复之前的信号屏蔽设置,让进程重新按正常方式接收信号。

数据流:进去的是保存了旧设置的 mask → 它把相关信号从恢复集合里调整好,再调用 sigprocmask 设置回去 → 不返回数据。

调用关系:凡是调用 ForwardedSignalMask::block 的流程,完成敏感操作后都会调用它。它保证临时挡信号不会影响后续运行。

调用图:外部调用 5 个(last_os_error, sigdelset, sigprocmask, panic!, null_mut)。

terminate_with_parent779–790 ↗
fn terminate_with_parent(parent_pid: libc::pid_t)

作用:让 bubblewrap 子进程在父进程意外死亡时也跟着结束,避免留下没人管的孤儿沙箱。

数据流:进去的是父进程 pid → 它设置“父进程死亡时给我 SIGTERM”的内核选项,并立刻检查父进程是否已经变了 → 如果父进程已不在,就给自己发 SIGTERM。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 的子进程分支调用它。它是父子进程看护关系的一道保险。

调用图:被 1 处调用(run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 5 个(last_os_error, getppid, prctl, raise, panic!)。

ForwardedSignalHandlers::restore793–804 ↗
fn restore(self)

作用:卸载之前装上的信号转发处理器,恢复原来的信号处理方式。

数据流:进去的是保存了旧处理器的对象 → 它清空全局记录的子进程 pid 和待转发信号,再逐个恢复原 sigaction → 不返回数据。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 在子进程结束、清理完后调用它。

调用图:外部调用 4 个(last_os_error, sigaction, panic!, null_mut)。

install_bwrap_signal_forwarders807–825 ↗
fn install_bwrap_signal_forwarders(pid: libc::pid_t) -> ForwardedSignalHandlers

作用:安装信号转发器:父进程收到退出、打断等信号时,把同样的信号转发给 bubblewrap 子进程。

数据流:进去的是 bubblewrap 子进程 pid → 它保存旧信号处理器,给几种常见终止信号安装新的处理函数,并重放安装前短暂积压的信号 → 返回一个可恢复的处理器集合。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 在父进程等待子进程前调用它;测试辅助也会用它。它依赖 replay_pending_forwarded_signal 和 forward_signal_to_bwrap_child。

调用图:调用 1 个内部函数(replay_pending_forwarded_signal);被 3 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup, run_bwrap_signal_forwarder_test_supervisor);外部调用 6 个(with_capacity, last_os_error, sigaction, sigemptyset, panic!, zeroed)。

forward_signal_to_bwrap_child827–833 ↗
fn forward_signal_to_bwrap_child(signal: libc::c_int)

作用:这是实际的信号处理函数。父进程收到指定信号时,它记录这个信号,并尽量立刻发给 bubblewrap 子进程。

数据流:进去的是信号编号 → 它把编号存入全局 pending 变量,读取当前子进程 pid → 如果 pid 有效,就调用 send_signal_to_bwrap_child 发送。

调用关系:install_bwrap_signal_forwarders 把它注册给操作系统。它是异步信号到达时触发的低层回调。

调用图:调用 1 个内部函数(send_signal_to_bwrap_child)。

replay_pending_forwarded_signal835–840 ↗
fn replay_pending_forwarded_signal(pid: libc::pid_t)

作用:把安装信号转发器前后短暂窗口里记录下来的信号补发给子进程,避免漏掉用户的 Ctrl+C 等操作。

数据流:进去的是子进程 pid → 它取出并清空 pending 信号 → 如果有信号,就调用 send_signal_to_bwrap_child 发出去。

调用关系:install_bwrap_signal_forwarders 安装完处理器后立刻调用它,用来补齐竞态窗口。

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

send_signal_to_bwrap_child842–847 ↗
fn send_signal_to_bwrap_child(pid: libc::pid_t, signal: libc::c_int)

作用:把一个信号发给 bubblewrap 子进程以及它所在的进程组。进程组可以理解成一组相关进程的集合。

数据流:进去的是 pid 和信号编号 → 它先向负 pid 代表的进程组发送,再向单个 pid 发送 → 不返回结果。

调用关系:forward_signal_to_bwrap_child 和 replay_pending_forwarded_signal 都调用它。这样无论子进程有没有再派生进程,都尽量能收到信号。

调用图:被 2 处调用(forward_signal_to_bwrap_child, replay_pending_forwarded_signal);外部调用 1 个(kill)。

reset_forwarded_signal_handlers_to_default849–858 ↗
fn reset_forwarded_signal_handlers_to_default()

作用:在子进程里把继承来的信号处理方式改回系统默认。这样 bubblewrap 子进程不会继续用父进程的转发逻辑。

数据流:进去没有显式参数 → 它遍历需要转发的信号,逐个设置成默认处理 → 如果设置失败就 panic。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 的子进程分支在 exec_bwrap 前调用它。

调用图:被 2 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 3 个(last_os_error, signal, panic!)。

wait_for_bwrap_child860–873 ↗
fn wait_for_bwrap_child(pid: libc::pid_t) -> libc::c_int

作用:等待 bubblewrap 子进程结束,并拿到它的退出状态。

数据流:进去的是子进程 pid → 它循环调用 waitpid;如果被信号打断就重试 → 成功返回原始退出状态,其他错误就 panic。

调用关系:父进程在 run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 中调用它;测试代码也会用它检查信号转发行为。

调用图:被 4 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup, bwrap_signal_forwarder_terminates_child_and_keeps_parent_alive, run_bwrap_signal_forwarder_test_supervisor);外部调用 3 个(last_os_error, waitpid, panic!)。

register_synthetic_mount_targets875–922 ↗
fn register_synthetic_mount_targets(
    targets: &[crate::bwrap::SyntheticMountTarget],
) -> Vec<SyntheticMountTargetRegistration>

作用:登记本次沙箱将临时创建的 synthetic mount 目标。synthetic mount 目标就是为了让 bubblewrap 能挂载而临时造出来的空文件或空目录。

数据流:进去的是 synthetic mount 目标列表 → 它拿注册表锁,为每个目标创建标记目录和以当前 pid 命名的标记文件,必要时根据已有活跃标记调整目标含义 → 返回登记记录列表,供之后清理。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 在启动 bwrap 前调用它。它通过 with_synthetic_mount_registry_lock 防止多个沙箱同时登记时互相冲突。

调用图:调用 1 个内部函数(with_synthetic_mount_registry_lock);被 2 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup)。

register_protected_create_targets924–953 ↗
fn register_protected_create_targets(
    targets: &[crate::bwrap::ProtectedCreateTarget],
) -> Vec<ProtectedCreateTargetRegistration>

作用:登记那些沙箱运行期间不该被创建的路径。登记后,监控和清理逻辑就知道哪些地方要盯住。

数据流:进去的是 protected create 目标列表 → 它拿注册表锁,给每个目标创建标记目录和当前 pid 标记文件,内容写入 protected-create 标记 → 返回登记记录。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 在启动 bwrap 前调用它,后面由 cleanup_protected_create_targets 清理。

调用图:调用 1 个内部函数(with_synthetic_mount_registry_lock);被 2 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup)。

synthetic_mount_marker_contents955–961 ↗
fn synthetic_mount_marker_contents(target: &crate::bwrap::SyntheticMountTarget) -> &'static [u8]

作用:决定 synthetic mount 的标记文件里应该写什么内容,用来区分这是保护已有路径,还是纯粹临时创建的路径。

数据流:进去的是一个 SyntheticMountTarget → 它查看目标是否保留预先存在的路径 → 返回 existing 或 synthetic 对应的字节内容。

调用关系:register_synthetic_mount_targets 写标记文件时调用它。后续 synthetic_mount_marker_dir_has_active_synthetic_owner 会读取这些内容判断是否有活跃的 synthetic 拥有者。

调用图:调用 1 个内部函数(preserves_pre_existing_path)。

synthetic_mount_marker_dir_has_active_synthetic_owner963–974 ↗
fn synthetic_mount_marker_dir_has_active_synthetic_owner(marker_dir: &Path) -> bool

作用:判断某个标记目录里是否还有活着的进程拥有“纯临时创建”的 synthetic 目标。

数据流:进去的是标记目录路径 → 它扫描目录里的 pid 标记文件,只匹配内容为 synthetic 的文件,并忽略或清掉已经死亡进程的标记 → 返回是否找到活跃拥有者。

调用关系:register_synthetic_mount_targets 用它避免在已有 synthetic 拥有者时误把目标当作原本就存在的路径处理。它把扫描工作交给 synthetic_mount_marker_dir_has_active_process_matching。

调用图:调用 1 个内部函数(synthetic_mount_marker_dir_has_active_process_matching)。

synthetic_mount_marker_dir_has_active_process976–978 ↗
fn synthetic_mount_marker_dir_has_active_process(marker_dir: &Path) -> bool

作用:判断某个标记目录里是否还有任何活着的进程在使用这个临时目标。

数据流:进去的是标记目录路径 → 它扫描所有 pid 标记,不额外过滤内容 → 返回是否存在活跃进程。

调用关系:cleanup_synthetic_mount_targets 和 cleanup_protected_create_targets 用它决定现在能不能真正删除目标和标记目录。

调用图:调用 1 个内部函数(synthetic_mount_marker_dir_has_active_process_matching)。

synthetic_mount_marker_dir_has_active_process_matching980–1024 ↗
fn synthetic_mount_marker_dir_has_active_process_matching(
    marker_dir: &Path,
    matches_marker: impl Fn(&Path) -> bool,
) -> bool

作用:扫描标记目录,找出是否有活跃进程的标记文件,并可用自定义条件筛选标记内容。

数据流:进去的是标记目录和一个匹配函数 → 它读取目录项,把文件名当 pid,清理已死亡进程的旧标记;对活跃 pid 调用匹配函数 → 只要有一个匹配就返回 true,否则返回 false。

调用关系:synthetic_mount_marker_dir_has_active_process 和 synthetic_mount_marker_dir_has_active_synthetic_owner 都复用它。它依赖 process_is_active 判断 pid 是否还活着。

调用图:调用 1 个内部函数(process_is_active);被 2 处调用(synthetic_mount_marker_dir_has_active_process, synthetic_mount_marker_dir_has_active_synthetic_owner);外部调用 3 个(read_dir, remove_file, panic!)。

cleanup_synthetic_mount_targets1026–1055 ↗
fn cleanup_synthetic_mount_targets(targets: &[SyntheticMountTargetRegistration])

作用:清理本次登记过的 synthetic mount 标记,并在没有其他活跃沙箱使用时删除临时创建的文件或目录。

数据流:进去的是登记记录列表 → 它拿注册表锁,先删除本进程的标记文件,再逐个检查是否还有活跃使用者 → 没有使用者时调用 remove_synthetic_mount_target 删除目标,并尝试删除标记目录。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 和 run_bwrap_in_child_capture_stderr 在 bwrap 结束后调用它。它和 register_synthetic_mount_targets 成对出现。

调用图:调用 1 个内部函数(with_synthetic_mount_registry_lock);被 2 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup)。

cleanup_protected_create_targets1057–1091 ↗
fn cleanup_protected_create_targets(targets: &[ProtectedCreateTargetRegistration]) -> bool

作用:清理 protected create 登记,并判断沙箱是否创建了不该创建的路径。

数据流:进去的是 protected create 登记记录 → 它拿锁,删除本进程标记;如果还有其他活跃使用者,就只检查目标是否存在;如果没有,就尝试删除目标 → 返回是否发现违规创建。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 用它的返回值影响最终退出码;run_bwrap_in_child_capture_stderr 也会调用它做预检查后的清理。

调用图:调用 1 个内部函数(with_synthetic_mount_registry_lock);被 2 处调用(run_bwrap_in_child_capture_stderr, run_bwrap_in_child_with_synthetic_mount_cleanup)。

remove_protected_create_target1093–1109 ↗
fn remove_protected_create_target(target: &crate::bwrap::ProtectedCreateTarget) -> bool

作用:严格删除一个不该被创建的目标,并告诉调用者是否真的删掉了东西。删目录时可能短暂非空,所以会重试。

数据流:进去的是 protected create 目标 → 它最多尝试 100 次调用 try_remove_protected_create_target;目录非空时稍等再试 → 成功返回是否发生删除,其他错误会 panic。

调用关系:cleanup_protected_create_targets 在最终清理时使用它。它是“必须处理干净,否则报错”的版本。

调用图:调用 1 个内部函数(try_remove_protected_create_target);外部调用 4 个(from_millis, panic!, sleep, unreachable!)。

remove_protected_create_target_best_effort1111–1124 ↗
fn remove_protected_create_target_best_effort(
    target: &crate::bwrap::ProtectedCreateTarget,
) -> Option<ProtectedCreateRemoval>

作用:尽力删除一个不该被创建的目标,但遇到某些错误时不让监控线程崩掉。best effort 的意思是“尽量做,失败也给出迹象”。

数据流:进去的是 protected create 目标 → 它反复调用 try_remove_protected_create_target;目录非空时短暂重试,其他错误返回 Other → 返回 None 表示没东西可删,Some 表示发现并处理或遇到异常。

调用关系:ProtectedCreateMonitor::start 的后台线程调用它。它适合运行中监控,不适合因为一次删除失败就立刻终止整个父进程。

调用图:调用 1 个内部函数(try_remove_protected_create_target);外部调用 2 个(from_millis, sleep)。

try_remove_protected_create_target1126–1156 ↗
fn try_remove_protected_create_target(
    target: &crate::bwrap::ProtectedCreateTarget,
) -> std::io::Result<Option<ProtectedCreateRemoval>>

作用:实际检查并删除 protected create 目标。它会区分目标是目录还是普通文件,并在删除时打印一条“沙箱阻止了创建”的提示。

数据流:进去的是目标路径 → 它读取路径元数据;不存在就返回 None;存在则按目录或文件删除 → 删除成功后输出提示,并返回删除类型。

调用关系:remove_protected_create_target 和 remove_protected_create_target_best_effort 都调用它。它是 protected create 删除逻辑的底层执行者。

调用图:调用 1 个内部函数(path);被 2 处调用(remove_protected_create_target, remove_protected_create_target_best_effort);外部调用 4 个(eprintln!, remove_dir_all, remove_file, symlink_metadata)。

remove_synthetic_mount_target1158–1190 ↗
fn remove_synthetic_mount_target(target: &crate::bwrap::SyntheticMountTarget)

作用:删除 bubblewrap 运行前临时造出来的空文件或空目录,但只在确认它确实应该被删除时才动手。

数据流:进去的是 SyntheticMountTarget → 它查看路径元数据,问目标对象 should_remove_after_bwrap 是否该删 → 如果该删,就按目标类型删除文件或空目录;如果不该删或已经不存在,就跳过。

调用关系:cleanup_synthetic_mount_targets 在没有其他活跃使用者时调用它。它避免误删用户原本就有的文件或目录。

调用图:调用 3 个内部函数(kind, path, should_remove_after_bwrap);外部调用 4 个(remove_dir, remove_file, symlink_metadata, panic!)。

process_is_active1192–1199 ↗
fn process_is_active(pid: libc::pid_t) -> bool

作用:判断一个 pid 对应的进程是否还活着。pid 是 Linux 给每个进程分配的编号。

数据流:进去的是 pid → 它用 kill(pid, 0) 做无害探测;成功说明活着,错误 ESRCH 表示不存在,其他错误按仍可能存在处理 → 返回布尔值。

调用关系:synthetic_mount_marker_dir_has_active_process_matching 用它清理死亡进程留下的旧标记,并判断某个临时目标是否仍被其他沙箱使用。

调用图:被 1 处调用(synthetic_mount_marker_dir_has_active_process_matching);外部调用 3 个(last_os_error, kill, matches!)。

with_synthetic_mount_registry_lock1201–1238 ↗
fn with_synthetic_mount_registry_lock(f: impl FnOnce() -> T) -> T

作用:给 synthetic mount 注册表加一把文件锁,再执行传入的操作。文件锁像公共登记簿上的钥匙,防止多个进程同时乱改。

数据流:进去的是一个闭包,也就是稍后要执行的一段操作 → 它创建注册表目录,打开 lock 文件,使用 flock 加独占锁,执行闭包,再解锁 → 返回闭包的结果。

调用关系:登记和清理 synthetic/protected 目标的函数都通过它进入临界区,包括 register_synthetic_mount_targets、register_protected_create_targets、cleanup_synthetic_mount_targets 和 cleanup_protected_create_targets。

调用图:调用 1 个内部函数(synthetic_mount_registry_root);被 4 处调用(cleanup_protected_create_targets, cleanup_synthetic_mount_targets, register_protected_create_targets, register_synthetic_mount_targets);外部调用 5 个(new, last_os_error, create_dir_all, flock, panic!)。

synthetic_mount_marker_dir1240–1242 ↗
fn synthetic_mount_marker_dir(path: &Path) -> PathBuf

作用:根据目标路径算出它在注册表里的标记目录位置。路径本身会被哈希成短名字,避免直接把复杂路径放进临时目录名。

数据流:进去的是目标路径 → 它调用 hash_path 得到数字指纹,再拼到注册表根目录下面 → 返回标记目录 PathBuf。

调用关系:register_synthetic_mount_targets 和 register_protected_create_targets 用它为每个目标找登记位置。它依赖 synthetic_mount_registry_root。

调用图:调用 1 个内部函数(synthetic_mount_registry_root);外部调用 1 个(format!)。

synthetic_mount_registry_root1244–1249 ↗
fn synthetic_mount_registry_root() -> PathBuf

作用:给当前有效用户算出 synthetic mount 注册表的根目录。不同用户使用不同目录,减少互相干扰。

数据流:进去没有显式参数 → 它读取当前有效用户 ID,把它拼进系统临时目录下的固定目录名 → 返回根目录路径。

调用关系:with_synthetic_mount_registry_lock 用它创建和锁定注册表;synthetic_mount_marker_dir 用它生成具体目标的标记目录。

调用图:被 2 处调用(synthetic_mount_marker_dir, with_synthetic_mount_registry_lock);外部调用 3 个(format!, geteuid, temp_dir)。

hash_path1251–1258 ↗
fn hash_path(path: &Path) -> u64

作用:把一个文件路径转换成稳定的 64 位数字指纹。这样可以用短目录名代表长路径。

数据流:进去的是路径 → 它逐字节进行 FNV 风格的哈希计算 → 返回 u64 数字。

调用关系:synthetic_mount_marker_dir 用它给标记目录命名。这里不用于安全加密,只用于生成不太容易冲突的登记名。

调用图:外部调用 2 个(as_os_str, from)。

exit_with_wait_status1260–1275 ↗
fn exit_with_wait_status(status: libc::c_int) -> !

作用:按照子进程的退出状态来结束当前父进程,让外部看到的结果像真正命令自己退出一样。

数据流:进去的是 waitpid 得到的状态 → 如果子进程正常退出,就用同样退出码退出;如果被信号杀死,就让当前进程也收到同样信号;其他情况退出 1。

调用关系:exit_with_wait_status_or_policy_violation 调用它;run_bwrap_in_child_capture_stderr 在预检查子进程被信号终止时也调用它。

调用图:被 2 处调用(exit_with_wait_status_or_policy_violation, run_bwrap_in_child_capture_stderr);外部调用 8 个(WEXITSTATUS, WIFEXITED, WIFSIGNALED, WTERMSIG, getpid, kill, signal, exit)。

exit_with_wait_status_or_policy_violation1277–1286 ↗
fn exit_with_wait_status_or_policy_violation(
    status: libc::c_int,
    protected_create_violation: bool,
) -> !

作用:在保留子进程退出结果的基础上,额外处理 protected create 违规。即使命令本身说成功,只要违反策略也要让整体失败。

数据流:进去的是子进程状态和是否发现 protected create 违规 → 如果有违规且子进程退出码是 0,就改为退出 1 → 否则交给 exit_with_wait_status 按原状态退出。

调用关系:run_bwrap_in_child_with_synthetic_mount_cleanup 在所有清理和违规判断完成后调用它,作为最终出口。

调用图:调用 1 个内部函数(exit_with_wait_status);被 1 处调用(run_bwrap_in_child_with_synthetic_mount_cleanup);外部调用 3 个(WEXITSTATUS, WIFEXITED, exit)。

run_bwrap_in_child_capture_stderr1299–1368 ↗
fn run_bwrap_in_child_capture_stderr(bwrap_args: crate::bwrap::BwrapArgs) -> String

作用:短暂运行一次 bubblewrap,并捕获它的标准错误输出。主要用来判断 /proc 挂载失败是不是当前环境的问题。

数据流:进去的是预检查用 BwrapArgs → 它登记临时目标、创建 stderr 管道、fork 子进程;子进程把 stderr 接到管道后执行 bwrap;父进程读取最多 64KB 错误输出,等待子进程并清理 → 返回 stderr 字符串。

调用关系:preflight_proc_mount_support 调用它。它复用了信号屏蔽、信号转发、目标登记清理和 wait_for_bwrap_child,和正式运行路径保持相近。

调用图:调用 11 个内部函数(exec_bwrap, block, cleanup_protected_create_targets, cleanup_synthetic_mount_targets, close_fd_or_panic, exit_with_wait_status, install_bwrap_signal_forwarders, register_protected_create_targets, register_synthetic_mount_targets, reset_forwarded_signal_handlers_to_default (+1 more));被 1 处调用(preflight_proc_mount_support);外部调用 9 个(from_raw_fd, from_utf8_lossy, new, last_os_error, WIFSIGNALED, dup2, fork, pipe2, panic!)。

close_fd_or_panic1375–1381 ↗
fn close_fd_or_panic(fd: libc::c_int, context: &str)

作用:关闭一个文件描述符,如果关闭失败就带上下文报错。这里不忽略错误,是为了避免底层资源问题被掩盖。

数据流:进去的是 fd 和一段说明文字 → 它调用 close → 成功无返回,失败就读取系统错误并 panic。

调用关系:run_bwrap_in_child_capture_stderr 在父子进程关闭管道端点时调用它。

调用图:被 1 处调用(run_bwrap_in_child_capture_stderr);外部调用 3 个(last_os_error, close, panic!)。

is_proc_mount_failure1383–1389 ↗
fn is_proc_mount_failure(stderr: &str) -> bool

作用:根据 bubblewrap 的错误文字判断是不是 /proc 挂载失败。它匹配几种常见错误,如权限不足或参数无效。

数据流:进去的是 stderr 字符串 → 它检查是否同时包含 proc 挂载位置和典型失败原因 → 返回 true 或 false。

调用关系:preflight_proc_mount_support 用它解释 run_bwrap_in_child_capture_stderr 捕获到的错误输出,从而决定正式运行是否关闭 /proc 挂载。

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

build_inner_seccomp_command1401–1443 ↗
fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>

作用:构造“内层命令”:让 bubblewrap 先启动隔离环境,然后在里面重新运行本助手,加上 seccomp 后再执行用户命令。

数据流:进去的是内层命令参数包,包括工作目录、权限配置、代理路由和用户命令 → 它找到当前可执行文件,把权限配置序列化成 JSON,拼出带 --apply-seccomp-then-exec 的命令行 → 返回字符串数组。

调用关系:run_main 在默认 bubblewrap 路线调用它。返回的命令会作为 bubblewrap 里面实际启动的程序,随后内层 run_main 会识别 --apply-seccomp-then-exec。

调用图:被 1 处调用(run_main);外部调用 4 个(panic!, to_string, current_exe, vec!)。

exec_or_panic1446–1466 ↗
fn exec_or_panic(command: Vec<String>) -> !

作用:用给定命令替换当前进程执行。exec 的意思是当前程序不再回来,直接变成另一个程序。

数据流:进去的是命令及其参数列表 → 它把 Rust 字符串转成 C 字符串数组,调用 execvp → 成功时没有返回;如果 execvp 返回,说明失败,于是读取系统错误并 panic。

调用关系:run_main 在已经应用好限制后调用它,真正启动用户命令。它是沙箱设置完成后的最后一步。

调用图:被 1 处调用(run_main);外部调用 5 个(new, last_os_error, execvp, panic!, null)。

linux-sandbox/src/main.rs源码 ↗
entrypointstartup

这个文件就像一家店的正门:用户运行 linux-sandbox 时,系统最先进入这里。它的 main 函数没有自己解析参数、设置环境或执行命令,而是直接调用 codex_linux_sandbox::run_main(),把真正的工作交给库里的主流程。注释特别提醒了一点:最后执行新程序时,当前工作目录、环境变量和命令参数会原样保留下去,所以调用这个沙箱程序的人必须提前把这些东西准备对。也就是说,这个文件很薄,但很关键;没有它,编译出来的可执行程序就没有明确的起点。

函数细节1
main4–6 ↗
fn main() -> !

作用:这是程序启动时第一个被调用的函数。它的作用是立刻进入真正的 linux 沙箱主流程,并且不会正常返回,因为后续流程会接管当前进程。

数据流:进去的是操作系统启动程序时已经准备好的当前目录、环境变量和命令行参数;main 自己不改这些东西,只调用 run_main(ext);出来不是普通返回值,而是程序继续进入沙箱运行流程,最终可能用新的命令替换当前进程。

调用关系:main 位于最外层入口,操作系统启动这个可执行文件时会先到这里。它马上把活儿交给 run_main(ext),由后者去完成具体的沙箱设置和运行命令。

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

bwrap/src/main.rs源码 ↗
entrypointstartup

这个文件像一个“门口接待员”。用户启动 bwrap 时,程序先来到这里。真正干活的是 bubblewrap 的 C 语言入口函数 bwrap_main,但 Rust 程序收到的命令行参数不能直接塞给 C 代码用,所以这里先把每个参数转换成 C 字符串(C 语言常用的一种以空字符结尾的文本格式),再做成 argv 参数表,也就是传统命令行程序收到的参数列表。最后它调用 bwrap_main,并把返回值当作整个进程的退出码。如果不是 Linux,或者编译时没有准备好 bubblewrap 需要的依赖,它不会假装能运行,而是立刻 panic(直接报错并停止),告诉使用者原因。这个文件重要在于:它是 Rust 外壳和 bubblewrap C 代码之间的桥,没有它,命令行参数和退出状态就无法正确交接。

函数细节1
main43–45 ↗
fn main()

作用:这是程序启动后第一个执行的函数。它要么把命令行参数转交给 bubblewrap 的真正入口,要么在系统或编译条件不合适时直接报错,避免程序以错误方式运行。

数据流:进去的是操作系统给这个进程的命令行参数,以及编译时留下的条件信息,比如是不是 Linux、有没有可用的 bubblewrap。它把参数逐个转成 C 语言格式,补上一个空指针作为参数列表结尾,然后调用底层的 bwrap_main。出来的是 bwrap_main 给出的退出码,main 会用这个退出码结束整个进程;如果环境不支持,则直接 panic,程序停止并显示原因。

调用关系:在整个流程里,main 是最外层入口,用户一运行这个二进制文件就会到这里。它自己不实现沙箱功能,而是把准备好的参数交给外部的 bubblewrap 主函数;如果走不到这一步,它会通过 panic! 给出明确错误。它还会使用 args_os 读取原始命令行参数,用 null 准备 C 参数表结尾,并最终调用 exit 结束进程。

调用图:外部调用 4 个(panic!, args_os, exit, null)。

shell-escalation/src/bin/main_execve_wrapper.rs源码 ↗
entrypointstartup

这个文件本身很短,但位置很关键:它是一个可执行程序的入口文件。所谓入口,就是程序一启动最先走到的地方。在 UNIX 系统上,它并不自己干活,而是把真正的 main 入口交给库里的 codex_shell_escalation::main_execve_wrapper,就像前台把客人转给真正办事的柜台。这样可以让主要逻辑放在库代码里,方便复用和测试。在非 UNIX 系统上,它会打印一条错误信息,然后用退出码 1 结束程序。这里的 UNIX 指 Linux、macOS 这类支持 execve 系统调用的系统;execve 可以理解成“把当前进程替换成另一个程序”的底层启动方式。没有这个文件,程序要么没有清晰的启动点,要么可能在不支持的平台上莫名其妙失败。

函数细节1
main2–5 ↗
fn main()

作用:这是非 UNIX 系统上的程序入口。它的作用不是继续运行,而是明确告诉用户这个工具只支持 UNIX,然后立刻失败退出,避免后面出现更难懂的错误。

数据流:进去时没有参数;它读取到的关键信息其实来自编译条件:当前不是 UNIX 系统。它把一条错误说明写到标准错误输出,也就是通常用来显示报错的地方;随后调用退出函数,让进程以状态码 1 结束,表示运行失败。

调用关系:当这个二进制程序在非 UNIX 平台被启动时,系统会进入这个 main。它只做两件事:用 eprintln! 输出错误,再把控制权交给 std::process::exit 结束进程。在 UNIX 平台上,这个文件不会使用这个函数,而是把 main 名字指向库里的 codex_shell_escalation::main_execve_wrapper,让真正的包装器逻辑接手。

调用图:外部调用 2 个(eprintln!, exit)。

shell-escalation/src/unix/execve_wrapper.rs源码 ↗
entrypointstartup

在 Unix 系统里,execve 是启动新程序的底层方式。这个文件就像一个“门口接待员”:用户或系统先运行这个包装器,它先把日志设置好,再读取命令行参数,知道真正要运行哪个文件、带哪些参数,然后把这些信息交给 crate::run_shell_escalation_execve_wrapper。这里用了 clap 解析命令行;clap 可以理解成“自动读命令行表单”的工具。它还把日志输出到标准错误 stderr,也就是专门放诊断信息的通道,避免和正常输出混在一起。最后,它不会自己慢慢返回,而是直接用得到的退出码结束进程,这样外部程序看到的成功或失败状态会和被包装的执行结果一致。

函数细节1
main_execve_wrapper15–25 ↗
async fn main_execve_wrapper() -> anyhow::Result<()>

作用:这是这个包装器程序真正开始运行的地方。它负责准备日志、读取命令行参数,然后调用项目里实际处理 execve 包装的函数,并用那个结果作为进程退出码。

数据流:进去的是命令行参数和环境变量里的日志设置。它先从环境变量读取日志过滤规则,初始化日志输出;再用 clap 解析出 file,也就是要执行的目标文件,以及 argv,也就是传给目标文件的参数列表;接着把 file 和 argv 交给 crate::run_shell_escalation_execve_wrapper 异步执行;最后拿到一个退出码,并让当前进程按这个退出码结束。

调用关系:它是最外层入口,启动时首先运行。它自己不做真正的拦截和执行细节,而是把准备好的 file 和 argv 交给 crate::run_shell_escalation_execve_wrapper;日志初始化依赖 tracing_subscriber::fmt 和 EnvFilter::from_default_env,参数读取依赖 ExecveWrapperCli::parse,结束进程时调用 std::process::exit。

调用图:外部调用 5 个(from_default_env, run_shell_escalation_execve_wrapper, parse, exit, fmt)。

Windows 沙盒辅助工具

这些文件实现 Windows 沙盒设置、提权命令运行器,以及用于启动受限进程的包装器协议。

windows-sandbox-rs/src/bin/command_runner/main.rs源码 ↗
entrypointstartup

这个文件像一个门卫:先看当前操作系统是不是 Windows。因为这个命令运行器只为 Windows 沙箱环境准备,很多能力依赖 Windows 自己的机制,所以在 Linux、macOS 等系统上继续跑只会出错或产生误导。代码用条件编译,也就是“编译时按操作系统选择代码”,在 Windows 上会加载 win 模块,并把 main 的工作转交给 win::main();在非 Windows 上,main 会立刻 panic,也就是主动中止程序并说明“这里只支持 Windows”。它本身不做具体业务,只负责把程序带到正确的平台实现里,或者在错误平台上及时刹车。

函数细节1
main10–12 ↗
fn main()

作用:这是程序启动时进入的函数。它检查当前编译出来的程序是不是给 Windows 用的:是的话就把控制权交给 Windows 版本的主流程,不是的话就直接中止,告诉使用者这个工具只能在 Windows 上运行。

数据流:进去时没有普通参数;它读取的是编译时确定的操作系统信息。若目标系统是 Windows,它调用 win::main(),让 Windows 专用代码继续处理,并把那边的成功或失败结果传出来;若不是 Windows,它调用 panic!,程序会停止并显示错误信息。

调用关系:它处在整个程序的最前面,是入口点。正常的 Windows 运行路径下,它只是一个转接头,把真正的启动工作交给 win::main();非 Windows 路径下,它不再交给任何业务代码,而是用 panic! 立即阻止程序继续运行。

调用图:外部调用 2 个(panic!, main)。

windows-sandbox-rs/src/bin/command_runner/win.rs源码 ↗
entrypoint沙箱命令执行期间:启动后读取请求,运行子进程,持续转发输入输出,最后收尾退出

这个程序像一个被派进沙箱里的“代办员”。主程序先启动它,再通过命名管道(一种本机进程之间传话的通道)告诉它要运行什么命令、在哪个目录运行、环境变量是什么、是否需要终端界面。它会先检查通信协议版本,避免两边说的不是同一种话;然后根据权限配置生成一个受限制的 Windows 令牌(可以理解成一张权限很小的临时通行证),再用这张通行证启动子进程。命令有两种跑法:需要终端时走 ConPTY(Windows 的伪终端),不需要终端时走普通输入输出管道。之后它一边把子进程的 stdout/stderr 打包成消息发回主程序,一边接收主程序发来的 stdin、调整终端大小、终止进程等命令。最后它等待子进程结束或超时,发送退出码。文件里还特别注意关闭 Windows 原始句柄,避免资源泄漏。

函数细节14
OwnedWinHandle::new101–103 ↗
fn new(handle: HANDLE) -> Self

作用:把一个 Windows 原始句柄包起来,让它从“没人管的号码”变成“有人负责关闭的资源”。这样中途出错时也不容易忘记释放。

数据流:进去的是一个 HANDLE,也就是 Windows 给资源分配的句柄编号 → 它把这个编号存在 OwnedWinHandle 里 → 出来的是一个带自动清理能力的小包装对象,本身不改动系统资源。

调用关系:创建作业对象、打开管道、准备受限令牌时都会先用它兜住句柄。后面如果确认要交给别的对象接管,就会配合 OwnedWinHandle::into_raw 把句柄交出去。

调用图:被 3 处调用(create_job_kill_on_close, main, spawn_ipc_process)。

OwnedWinHandle::raw105–107 ↗
fn raw(&self) -> HANDLE

作用:拿到里面保存的原始 Windows 句柄,方便传给 Windows API 使用。它只是借看一下,不转移所有权。

数据流:进去的是这个包装对象自身 → 它读取里面的 HANDLE 数值 → 出来的是原始句柄;包装对象仍然负责以后关闭它。

调用关系:在创建受限令牌、设置作业对象等步骤里,Windows API 需要原始 HANDLE,所以这些地方会通过它取出句柄来用。

OwnedWinHandle::into_raw109–115 ↗
fn into_raw(mut self) -> HANDLE

作用:把句柄从自动关闭包装里拿出来,交给调用者自己负责。常用于确认资源已经要交给 File 或别的生命周期管理者时。

数据流:进去的是一个 OwnedWinHandle → 它取出里面的 HANDLE,并把自己内部值清成 0,防止析构时再关一次 → 出来的是裸 HANDLE,之后谁拿到谁负责关闭。

调用关系:main 打开管道后会把句柄转给 File;create_job_kill_on_close 创建好作业对象后也会把句柄交给上层 main。它是防止“双重关闭”的关键交接动作。

OwnedWinHandle::drop119–125 ↗
fn drop(&mut self)

作用:当 OwnedWinHandle 离开作用范围时,自动关闭里面还没交出去的 Windows 句柄。它相当于给资源装了一个“自动关门器”。

数据流:进去的是即将销毁的包装对象 → 它检查句柄是不是有效,不是 0,也不是无效句柄 → 如果有效,就调用 CloseHandle 关闭系统资源;没有返回业务结果。

调用关系:它由 Rust 在对象生命周期结束时自动调用。前面的 OwnedWinHandle::new 负责包住资源,OwnedWinHandle::into_raw 负责交出资源,而这里负责兜底清理没交出去的资源。

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

create_job_kill_on_close128–145 ↗
fn create_job_kill_on_close() -> Result<HANDLE>

作用:创建一个 Windows 作业对象,并设置成“作业句柄一关,里面的进程也一起杀掉”。这能防止沙箱里的子进程逃成孤儿进程。

数据流:进去没有业务参数 → 它创建作业对象,设置 KILL_ON_JOB_CLOSE 限制 → 成功时出来一个作业句柄;失败时返回错误,并靠 OwnedWinHandle 自动清理已创建的句柄。

调用关系:main 在子进程启动后调用它,并把子进程加入这个作业对象。这样 main 后面关闭作业句柄时,Windows 会帮忙清理相关进程。

调用图:调用 1 个内部函数(new);被 1 处调用(main);外部调用 6 个(anyhow!, zeroed, null, null_mut, CreateJobObjectW, SetInformationJobObject)。

open_pipe148–166 ↗
fn open_pipe(name: &str, access: u32) -> Result<HANDLE>

作用:打开主程序提前创建好的命名管道。这个管道就是 runner 和主程序互相传消息的电话线。

数据流:进去的是管道名字和访问方式,比如读或写 → 它把名字转成 Windows 需要的宽字符格式,再调用 CreateFileW 打开 → 成功返回管道句柄;失败返回带错误码的信息。

调用关系:main 启动时先解析 --pipe-in 和 --pipe-out,然后用它分别打开读管道和写管道。后续 read_spawn_request、send_error、输出转发都依赖这些管道。

调用图:被 1 处调用(main);外部调用 5 个(anyhow!, to_wide, null_mut, GetLastError, CreateFileW)。

send_error169–183 ↗
fn send_error(writer: &Arc<StdMutex<File>>, code: &str, message: String) -> Result<()>

作用:把启动失败等错误包装成统一格式,发回主程序。这样主程序不是只看到 runner 崩了,而是能知道失败原因。

数据流:进去的是写管道、错误代码和错误文字 → 它组成带协议版本的 Error 消息,锁住写端文件并写入一帧消息 → 出来是成功或写入失败的结果,不直接结束进程。

调用关系:main 在读取请求失败或启动子进程失败时调用它。它把错误交给 write_frame 发送,是异常路径上通知父进程的出口。

调用图:被 1 处调用(main);外部调用 1 个(write_frame)。

read_spawn_request186–197 ↗
fn read_spawn_request(reader: &mut File) -> Result<SpawnRequest>

作用:读取主程序发来的第一条“启动命令请求”,并确认它格式正确。没有这一步,runner 可能会按错误消息乱启动程序。

数据流:进去的是管道读端 File → 它读一帧消息,检查协议版本,再确认消息类型是 SpawnRequest → 出来是 SpawnRequest;如果管道提前关闭、版本不对或类型不对,就返回错误。

调用关系:main 打开管道后马上调用它。只有它读到合法请求后,main 才会继续交给 spawn_ipc_process 真正创建受限子进程。

调用图:被 1 处调用(main);外部调用 2 个(bail!, read_frame)。

read_acl_mutex_exists199–213 ↗
fn read_acl_mutex_exists() -> Result<bool>

作用:检查一个叫 CodexSandboxReadAcl 的系统互斥锁是否存在。互斥锁可以理解成一块“有人正在使用某项机制”的告示牌。

数据流:进去没有业务参数 → 它尝试打开固定名字的 Windows 互斥锁 → 如果不存在返回 false;如果打开成功就立刻关闭并返回 true;如果出现别的系统错误就返回错误。

调用关系:effective_cwd 用它判断是否需要走工作目录 junction。它不直接改变目录,只提供“ACL 辅助机制是否活跃”的判断。

调用图:被 1 处调用(effective_cwd);外部调用 6 个(new, anyhow!, to_wide, CloseHandle, GetLastError, OpenMutexW)。

effective_cwd216–234 ↗
fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf

作用:决定子进程真正使用哪个当前工作目录。某些权限场景下,它会用 junction(Windows 里的目录转接点,像一个特殊快捷入口)来绕开访问控制的麻烦。

数据流:进去的是请求里的工作目录和可选日志目录 → 它先用 read_acl_mutex_exists 看 ACL 辅助机制是否开启;开启或探测失败时尝试创建 cwd junction,否则直接用原目录 → 出来的是最终要传给子进程的目录路径。

调用关系:spawn_ipc_process 在启动命令前调用它。它会把目录选择结果交给 ConPTY 启动函数或管道启动函数使用;如果探测失败,还会写日志提醒。

调用图:调用 2 个内部函数(create_cwd_junction, read_acl_mutex_exists);被 1 处调用(spawn_ipc_process);外部调用 3 个(to_path_buf, log_note, format!)。

spawn_ipc_process236–348 ↗
fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess>

作用:按照主程序的请求,在沙箱用户下创建真正要运行的子进程。它是把“请求说明书”变成“正在运行的受限进程”的核心步骤。

数据流:进去的是 SpawnRequest,里面有命令、目录、环境变量、权限配置、能力 SID、是否需要终端等信息 → 它隐藏当前用户配置目录,计算令牌模式,解析能力 SID,基于当前令牌做出受限令牌,允许访问 null 设备,选择有效工作目录,然后按 tty 决定用 ConPTY 或普通管道启动 → 出来的是 IpcSpawnedProcess,里面包含进程信息、stdin/stdout/stderr 句柄、伪终端句柄和日志目录。

调用关系:main 读到合法启动请求后调用它。它内部会用 effective_cwd 决定目录,用 OwnedWinHandle 防止令牌句柄泄漏,并把实际启动工作交给沙箱库里的 ConPTY 或 pipe 启动函数。

调用图:调用 3 个内部函数(new, effective_cwd, from_string);被 1 处调用(main);外部调用 11 个(new, bail!, allow_null_device, create_readonly_token_with_caps_and_user_from, create_workspace_write_token_with_caps_and_user_from, get_current_token_for_restriction, hide_current_user_profile_dir, spawn_conpty_process_as_user, spawn_process_with_pipes, token_mode_for_permission_profile (+1 more))。

spawn_output_reader351–376 ↗
fn spawn_output_reader(
    writer: Arc<StdMutex<File>>,
    handle: HANDLE,
    stream: OutputStream,
    log_dir: Option<PathBuf>,
) -> std::thread::JoinHandle<()>

作用:开一个后台线程,把子进程的输出持续读出来,再转成消息发回主程序。stdout 和 stderr 都靠它传回去。

数据流:进去的是写管道、要读取的输出句柄、输出流类型和日志目录 → 它启动读取循环,每读到一块字节就做 base64 编码,包装成 Output 消息并写入管道 → 出来的是线程句柄;如果写回失败,会记日志。

调用关系:main 在发送 SpawnReady 后调用它,一次用于 stdout,必要时再一次用于 stderr。它把底层读句柄的工作交给 read_handle_loop,把发消息的工作交给 write_frame。

调用图:被 1 处调用(main);外部调用 1 个(read_handle_loop)。

spawn_input_loop379–497 ↗
fn spawn_input_loop(
    mut reader: File,
    stdin_handle: Option<HANDLE>,
    hpc_handle: Arc<StdMutex<Option<HANDLE>>>,
    process_handle: Arc<StdMutex<Option<HANDLE>>>,
    log_dir: Option<PathB

作用:开一个后台线程,监听主程序发来的输入和控制命令,并转给子进程。它处理键盘输入、关闭 stdin、调整终端大小和强制终止。

数据流:进去的是读管道、可选 stdin 写句柄、伪终端句柄、进程句柄和日志目录 → 它循环读消息;收到 Stdin 就解码并写入子进程 stdin,收到 CloseStdin 就关输入,收到 Resize 就调整伪终端尺寸,收到 Terminate 就杀进程 → 出来的是线程句柄;过程中可能关闭 stdin 句柄或终止进程。

调用关系:main 在子进程启动并开始读输出后调用它。它和 spawn_output_reader 同时工作:一个负责从父进程到子进程的输入方向,一个负责从子进程到父进程的输出方向。

调用图:被 1 处调用(main);外部调用 1 个(spawn)。

main500–653 ↗
fn main() -> Result<()>

作用:这是 runner 进程的总入口。它把所有步骤串起来:连管道、读请求、启动受限进程、转发输入输出、等待结束、汇报退出码。

数据流:进去的是命令行参数,主要是 --pipe-in 和 --pipe-out → 它打开管道,读取 SpawnRequest,失败就 send_error;成功就 spawn_ipc_process,创建作业对象并绑定进程,发送 SpawnReady,启动输出线程和输入线程,等待进程结束或超时,读取或设置退出码,关闭句柄,发送 Exit 消息 → 最后用子进程退出码结束 runner 自己。

调用关系:它是本文件所有零件的指挥者。启动阶段调用 open_pipe 和 read_spawn_request;创建阶段调用 spawn_ipc_process 和 create_job_kill_on_close;运行阶段调用 spawn_output_reader、spawn_input_loop;出错时调用 send_error;收尾时写 Exit 并退出。

调用图:调用 8 个内部函数(new, create_job_kill_on_close, open_pipe, read_spawn_request, send_error, spawn_input_loop, spawn_ipc_process, spawn_output_reader);外部调用 16 个(clone, new, from_raw_handle, new, bail!, log_note, write_frame, format!, args, exit (+6 more))。

windows-sandbox-rs/src/bin/setup_main/main.rs源码 ↗
entrypointstartup

这个文件很小,但位置很关键:它决定程序一启动后该往哪里走。因为这个工具是专门给 Windows 用的,所以代码先用条件编译(根据操作系统选择要编进程序的代码)做了分流:在 Windows 上,它会加载同目录下的 win 模块,并把真正的工作交给 win::main();在非 Windows 系统上,它不会尝试做任何安装动作,而是直接 panic,也就是立刻中止并提示“这是 Windows 专用的”。可以把它理解成门口的接待员:先看你是不是来对地方了;是 Windows,就带你去真正办事的窗口;不是 Windows,就直接拦下,防止后面发生更难懂的错误。

函数细节1
main10–12 ↗
fn main()

作用:这是这个二进制程序的入口函数,也就是程序开始运行时第一个被调用的地方。它的作用是判断当前系统能不能运行这个工具,并把真正的安装流程交给 Windows 专用代码。

数据流:程序启动时没有接收业务输入;它读取的是编译时已经确定的操作系统信息。若目标系统是 Windows,它调用 win::main(),把后续安装工作交出去,并把成功或失败结果返回;若不是 Windows,它直接触发 panic,让程序停止并说明这个工具只能在 Windows 上用。

调用关系:它站在整个程序最前面。启动时先到 main;在 Windows 版本里,main 不自己做安装,而是调用外部的 win::main();在非 Windows 版本里,它调用 panic! 结束程序,避免继续进入不适用的流程。

调用图:外部调用 2 个(panic!, main)。

windows-sandbox-rs/src/bin/setup_main/win.rs源码 ↗
entrypointstartup / setup refresh

可以把这个文件看成沙盒启动前的“物业安检员”。主程序把一份配置包传给它,里面写着沙盒用户叫什么、哪些目录能读、哪些目录能写、哪些路径要禁止访问、代理端口是什么。它先解码并检查配置版本,然后按模式执行:完整设置、只创建用户、或只刷新读取权限。它会创建/隐藏专用 Windows 用户,配置离线用户的网络规则,给文件夹写入 ACL(访问控制列表,也就是 Windows 用来规定“谁能读写删除”的权限表),并把一些权限刷新放到后台助手里做,避免启动过程太慢。它也非常重视失败记录:日志写在沙盒目录,严重错误会写成报告,方便上层知道哪里坏了。没有这个文件,沙盒可能没有正确用户、网络没被封住、目录权限过宽或过窄,都会让安全性或可用性出问题。

函数细节23
log_line114–123 ↗
fn log_line(log: &mut dyn Write, msg: &str) -> Result<()>

作用:往安装日志里写一行带时间的文字。它的作用是留下“刚才做了什么、哪里失败了”的痕迹,方便之后排查问题。

数据流:输入是一支可写的日志对象和一段消息;它先取当前 UTC 时间,拼成“时间 + 消息”的格式,再写入日志;成功时没有额外结果,失败时会把写日志失败包装成明确的安装错误返回。

调用关系:很多关键步骤都会调用它,比如刷新读权限、完整设置、主流程报错时。它不决定流程,只负责把流程中的重要事件记录下来。

调用图:被 5 处调用(apply_read_acls, read_mask_allows_or_log, real_main, run_read_acl_only, run_setup_full);外部调用 2 个(now, writeln!)。

workspace_write_cap_sids_for_path125–159 ↗
fn workspace_write_cap_sids_for_path(
    codex_home: &Path,
    command_cwd: &Path,
    write_roots: &[PathBuf],
    path: &Path,
) -> Result<Vec<String>>

作用:找出某个受保护路径应该对应哪些“写权限能力 SID”。SID 可以理解成 Windows 里代表某个用户或权限身份的身份证号。

数据流:输入是 Codex 主目录、命令工作目录、所有可写根目录,以及一个目标路径;它检查哪些可写根目录和目标路径有重叠,就取这些根目录对应的能力 SID;如果一个都匹配不上,就退回到默认工作目录或全部活跃写根目录;输出是一组 SID 字符串。

调用关系:完整设置在给“禁止写”的路径加保护时会用它。几个测试函数专门验证它不会拿过期根目录的 SID,也会正确处理嵌套目录。

调用图:被 4 处调用(run_setup_full, deny_path_includes_nested_active_root_sid, deny_path_outside_active_roots_falls_back_to_all_active_root_sids, deny_path_under_active_root_uses_only_matching_root_sid);外部调用 4 个(is_empty, new, workspace_write_cap_sid_for_root, workspace_write_root_overlaps_path)。

spawn_read_acl_helper161–177 ↗
fn spawn_read_acl_helper(payload: &Payload, _log: &mut dyn Write) -> Result<()>

作用:启动另一个同样的安装助手进程,让它只负责补齐读取权限。这样主设置流程不用一直等大量目录权限刷新完成。

数据流:输入是当前配置;它复制配置,把模式改成只处理读权限并标记为刷新,然后转成 JSON、再用 Base64 编码,作为参数传给当前可执行文件的新进程;输出是启动成功或失败,实际读权限工作由新进程继续做。

调用关系:run_setup_full 在发现需要处理 read_roots 时会调用它。它把耗时的读 ACL 工作交给后台的 run_read_acl_only 路径。

调用图:被 1 处调用(run_setup_full);外部调用 5 个(null, new, to_vec, current_exe, clone)。

apply_read_acls184–255 ↗
fn apply_read_acls(
    read_roots: &[PathBuf],
    subjects: &ReadAclSubjects<'_>,
    log: &mut dyn Write,
    refresh_errors: &mut Vec<String>,
    access_mask: u32,
    access_label: &str,
    inh

作用:给一批可读目录补上沙盒用户需要的读取/执行权限。ACL 是访问控制列表,像门禁名单,写着谁能进、能做什么。

数据流:输入是读取根目录列表、要检查的用户身份、日志、错误收集器、权限掩码和继承方式;它逐个目录检查是否存在、公共用户组是否已有权限、沙盒组是否已有权限;如果都没有,就尝试给沙盒组加允许读取的 ACE(访问控制项,也就是门禁名单里的一条规则);输出是成功或失败,并把可继续处理的错误记到列表和日志。

调用关系:run_read_acl_only 会调用它。它内部先找 read_mask_allows_or_log 判断是否已经有权限,必要时再交给底层 Windows 权限函数写 ACL。

调用图:调用 2 个内部函数(log_line, read_mask_allows_or_log);被 1 处调用(run_read_acl_only);外部调用 2 个(ensure_allow_mask_aces_with_inheritance, format!)。

read_mask_allows_or_log257–290 ↗
fn read_mask_allows_or_log(
    root: &Path,
    psids: &[*mut c_void],
    label: Option<&str>,
    read_mask: u32,
    access_label: &str,
    refresh_errors: &mut Vec<String>,
    log: &mut dyn Wri

作用:检查某个路径对某些身份是否已经允许指定的读取权限,并在检查失败时记日志而不是立刻中断。

数据流:输入是路径、身份指针列表、标签、要检查的权限位、错误列表和日志;它调用权限检查函数判断是否满足要求;如果检查函数报错,它把错误写入 refresh_errors 和日志,然后返回 false,意思是“当作没有权限,后面再尝试修”。

调用关系:apply_read_acls 用它做前置检查。它让读权限刷新更稳健:单个目录检查失败不会马上毁掉整批处理。

调用图:调用 1 个内部函数(log_line);被 1 处调用(apply_read_acls);外部调用 2 个(path_mask_allows, format!)。

lock_sandbox_dir292–387 ↗
fn lock_sandbox_dir(
    dir: &Path,
    real_user: &str,
    sandbox_group_sid: &[u8],
    sandbox_group_access_mode: i32,
    sandbox_group_mask: u32,
    real_user_mask: u32,
    _log: &mut dyn Wri

作用:创建并锁定一个沙盒相关目录,精确规定沙盒组、真实用户、SYSTEM 和管理员分别能做什么。

数据流:输入是目录路径、真实用户名、沙盒组 SID、沙盒组的允许或拒绝方式、各自权限范围和日志;它先确保目录存在,再解析几个系统身份的 SID,组装 Windows ACL 规则,最后把这些规则写到目录上;输出是成功或具体的 Windows 权限设置错误。

调用关系:lock_persistent_sandbox_dirs 和 lock_sandbox_bin_dir 都靠它完成真正的目录上锁。它是本文件里直接接触 Windows 安全 API 的核心工具之一。

调用图:调用 1 个内部函数(resolve_sid);被 2 处调用(lock_persistent_sandbox_dirs, lock_sandbox_bin_dir);外部调用 12 个(new, as_os_str, new, anyhow!, string_from_sid_bytes, to_wide, create_dir_all, null_mut, LocalFree, ConvertStringSidToSidW (+2 more))。

main389–407 ↗
fn main() -> Result<()>

作用:这是这个 Windows 设置助手二进制程序的入口。它负责启动真实主流程,并在最外层兜底记录意外错误。

数据流:没有业务输入,主要读取命令行参数和环境变量;它调用 real_main 执行全部设置;如果 real_main 失败,它尝试从 CODEX_HOME 找到沙盒目录并写一条顶层错误日志;最后把 real_main 的结果原样返回。

调用关系:操作系统启动这个程序时先进入它。它把主要工作交给 real_main,自己只做最外层的错误兜底。

调用图:调用 1 个内部函数(real_main);外部调用 6 个(new, log_writer, sandbox_dir, var, create_dir_all, writeln!)。

real_main409–477 ↗
fn real_main() -> Result<()>

作用:解析上层传来的配置包,准备日志,然后运行沙盒设置主流程。它也是把普通错误变成可读错误报告的地方。

数据流:输入来自命令行的一个 Base64 参数;它解码成 JSON,再转成 Payload,检查版本,创建沙盒目录并打开日志;随后调用 run_setup;如果失败,它写日志、写提示,并把错误整理成 SetupErrorReport 存回 Codex 主目录;输出是设置成功或失败。

调用关系:main 只调用它一次。它负责把“外部传来的请求”变成内部可执行的设置流程,并把 run_setup 的失败翻译成上层能理解的报告。

调用图:调用 3 个内部函数(log_line, run_setup, new);被 1 处调用(main);外部调用 10 个(new, extract_setup_failure, log_note, log_writer, sandbox_dir, write_setup_error_report, format!, from_slice, args, create_dir_all)。

run_setup479–499 ↗
fn run_setup(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()>

作用:根据配置里的模式选择要跑哪一种设置流程。它像调度员,决定是完整安装、只建用户,还是只刷新读权限。

数据流:输入是 Payload、日志和沙盒目录;它先判断本次是否需要写“设置进行中/已完成”的标记;然后按 SetupMode 调用对应函数;成功后再提交设置标记;输出是整个模式执行的结果。

调用关系:real_main 调用它。它把工作分发给 run_read_acl_only、run_provision_only 或 run_setup_full,并在这些流程前后调用 marker 相关函数记录状态。

调用图:调用 5 个内部函数(run_provision_only, run_read_acl_only, run_setup_full, commit_setup_marker, prepare_setup_marker);被 1 处调用(real_main)。

run_read_acl_only501–562 ↗
fn run_read_acl_only(payload: &Payload, log: &mut dyn Write) -> Result<()>

作用:只刷新读取权限,不创建用户、不改网络。它通常是后台助手进程执行的轻量流程。

数据流:输入是配置和日志;它先抢一个互斥锁(一把锁,防止两个助手同时改同一批权限);如果已有助手在跑就跳过;然后解析沙盒用户组和常见 Windows 用户组的 SID,调用 apply_read_acls 给 read_roots 补权限;最后释放临时 SID 内存,并在刷新模式下把错误变成失败返回。

调用关系:run_setup 在 ReadAclsOnly 模式下调用它。spawn_read_acl_helper 启动的新进程最终也会走到这里。

调用图:调用 6 个内部函数(apply_read_acls, log_line, acquire_read_acl_mutex, resolve_sandbox_users_group_sid, resolve_sid, sid_bytes_to_psid);被 1 处调用(run_setup);外部调用 5 个(new, bail!, format!, vec!, LocalFree)。

provision_and_hide_sandbox_users564–590 ↗
fn provision_and_hide_sandbox_users(
    payload: &Payload,
    log: &mut dyn Write,
    sbx_dir: &Path,
) -> Result<()>

作用:创建或确认沙盒用的两个 Windows 用户,并把新建用户从普通登录界面里隐藏起来。

数据流:输入是配置、日志和沙盒目录;它调用 provision_sandbox_users 准备离线/在线两个沙盒用户;如果失败,会把普通错误包装成“用户创建失败”;成功后把用户名列表交给 hide_newly_created_users;输出是用户准备好的结果。

调用关系:run_provision_only 和 run_setup_full 在需要创建用户时都会调用它。它把用户准备工作和隐藏用户这两个步骤绑在一起。

调用图:调用 2 个内部函数(provision_sandbox_users, new);被 2 处调用(run_provision_only, run_setup_full);外部调用 5 个(new, extract_setup_failure, hide_newly_created_users, format!, vec!)。

configure_offline_sandbox_network592–631 ↗
fn configure_offline_sandbox_network(
    payload: &Payload,
    offline_sid_str: &str,
    log: &mut dyn Write,
) -> Result<()>

作用:给离线沙盒用户设置网络边界:允许必要代理端口,同时阻止普通外连。

数据流:输入是配置、离线用户 SID 字符串和日志;它先确保代理端口在防火墙允许名单里,再确保离线用户的出站连接被阻断,最后安装更底层的 WFP 过滤器;输出是网络规则配置成功或失败。

调用关系:run_provision_only 和 run_setup_full 都会在非纯刷新场景调用它。它把具体防火墙工作交给 firewall 模块和 install_wfp_filters。

调用图:调用 3 个内部函数(ensure_offline_outbound_block, ensure_offline_proxy_allowlist, new);被 2 处调用(run_provision_only, run_setup_full);外部调用 4 个(new, extract_setup_failure, install_wfp_filters, format!)。

lock_persistent_sandbox_dirs633–679 ↗
fn lock_persistent_sandbox_dirs(
    payload: &Payload,
    sandbox_group_sid: &[u8],
    log: &mut dyn Write,
) -> Result<()>

作用:锁定沙盒长期保存的目录,尤其保护 secrets 目录不被沙盒用户随便读写。

数据流:输入是配置、沙盒组 SID 和日志;它先锁定普通沙盒目录,让沙盒组可用;再锁定 secrets 目录,对沙盒组使用拒绝访问;最后顺手删除一个旧版遗留的 sandbox_users.json 文件;输出是目录锁定是否成功。

调用关系:run_provision_only 和 run_setup_full 在设置持久目录时调用它。它内部依赖 lock_sandbox_dir 真正写 Windows ACL。

调用图:调用 1 个内部函数(lock_sandbox_dir);被 2 处调用(run_provision_only, run_setup_full);外部调用 3 个(sandbox_dir, sandbox_secrets_dir, remove_file)。

lock_sandbox_bin_dir681–704 ↗
fn lock_sandbox_bin_dir(
    payload: &Payload,
    sandbox_group_sid: &[u8],
    log: &mut dyn Write,
) -> Result<()>

作用:锁定沙盒运行时二进制目录,让沙盒用户主要只能读取和执行,不能随意改里面的程序。

数据流:输入是配置、沙盒组 SID 和日志;它找到 sandbox bin 目录,然后调用 lock_sandbox_dir 写入权限规则;输出是锁定成功或带有目录路径的失败信息。

调用关系:run_provision_only 和 run_setup_full 都会调用它。它专门保护可执行文件目录,避免沙盒内进程篡改自己的工具。

调用图:调用 1 个内部函数(lock_sandbox_dir);被 2 处调用(run_provision_only, run_setup_full);外部调用 1 个(sandbox_bin_dir)。

run_provision_only706–732 ↗
fn run_provision_only(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()>

作用:执行“只准备基础设施”的流程:创建用户、配置网络、锁目录,但不处理每次命令相关的读写根目录。

数据流:输入是配置、日志和沙盒目录;它先准备并隐藏沙盒用户,再解析离线用户和沙盒组的 SID,配置离线网络,然后锁定 bin 目录和持久目录;最后写一条完成提示;输出是这套基础准备是否成功。

调用关系:run_setup 在 ProvisionOnly 模式下调用它。它复用 provision_and_hide_sandbox_users、configure_offline_sandbox_network 和目录锁定函数。

调用图:调用 6 个内部函数(configure_offline_sandbox_network, lock_persistent_sandbox_dirs, lock_sandbox_bin_dir, provision_and_hide_sandbox_users, resolve_sandbox_users_group_sid, resolve_sid);被 1 处调用(run_setup);外部调用 2 个(log_note, string_from_sid_bytes)。

run_setup_full734–1033 ↗
fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()>

作用:执行完整沙盒设置流程。它把用户、网络、读权限、写权限、禁止读写路径和目录锁定全部串起来,是最主要的工作函数。

数据流:输入是完整 Payload、日志和沙盒目录;它根据 refresh_only 决定是否重新创建用户和网络规则;解析离线用户、沙盒组和能力 SID;同步应用禁止读取权限;必要时启动后台读 ACL 助手;检查每个写根目录是否已有写权限,没有就并发补写 ACE;再给 deny_write_paths 加拒绝写入规则,必要时先创建占位目录;最后锁定 bin 和持久目录,释放 SID 内存,若刷新模式收集到错误则失败返回;成功时写完成提示。

调用关系:run_setup 在 Full 模式下调用它。它是整个文件的中心调度点,会调用几乎所有辅助函数,并把底层 Windows ACL、防火墙、用户准备等模块连成一条安全启动流水线。

调用图:调用 12 个内部函数(configure_offline_sandbox_network, lock_persistent_sandbox_dirs, lock_sandbox_bin_dir, log_line, provision_and_hide_sandbox_users, read_acl_mutex_exists, resolve_sandbox_users_group_sid, resolve_sid, sid_bytes_to_psid, ensure_codex_app_runtime_bin_readable (+2 more));被 1 处调用(run_setup);外部调用 16 个(new, new, bail!, add_deny_write_ace, canonicalize_path, convert_string_sid_to_sid, is_command_cwd_root, log_note, path_mask_allows, string_from_sid_bytes (+6 more))。

tests::payload_json1047–1059 ↗
fn payload_json() -> serde_json::Value

作用:构造一份最小可用的测试配置 JSON。这样多个测试不用重复写同一堆字段。

数据流:没有输入;它生成包含版本号、用户名、目录、空读写根、代理端口和真实用户的 JSON 值;输出是这份测试用 JSON。

调用关系:后面的 Payload 解析测试都会调用它,把它当作标准样板配置。

调用图:外部调用 1 个(json!)。

tests::payload_defaults_otel_absent1062–1066 ↗
fn payload_defaults_otel_absent()

作用:验证配置里不写 otel 时,程序会把它当成没有遥测设置,而不是报错。

数据流:输入来自 payload_json 的默认 JSON;它解析成 Payload,然后检查 otel 字段是 None;输出是测试通过或断言失败。

调用关系:它测试 Payload 的默认值行为,防止以后改结构时破坏老配置兼容性。

调用图:外部调用 3 个(assert_eq!, from_value, payload_json)。

tests::payload_accepts_provision_only_mode1069–1075 ↗
fn payload_accepts_provision_only_mode()

作用:验证 JSON 里的 mode 写成 provision-only 时,能正确解析成只准备用户和基础设施的模式。

数据流:输入是默认测试 JSON,并把 mode 字段改成 provision-only;它解析成 Payload,再断言 mode 等于 SetupMode::ProvisionOnly;输出是测试结果。

调用关系:它保护 run_setup 的模式分发入口,确保外部传来的模式字符串能被正确理解。

调用图:外部调用 4 个(assert_eq!, json!, from_value, payload_json)。

tests::payload_accepts_otel_settings1078–1091 ↗
fn payload_accepts_otel_settings()

作用:验证配置可以携带遥测设置。这里的遥测设置指运行环境等用于统计/观测的信息。

数据流:输入是默认测试 JSON,并加入 otel.environment;它解析成 Payload,再检查生成的 StatsigMetricsSettings 内容正确;输出是测试通过或失败。

调用关系:它确保 configure_offline_sandbox_network 传给 WFP 安装流程的可选遥测配置能从请求里正常进入 Payload。

调用图:外部调用 4 个(assert_eq!, json!, from_value, payload_json)。

tests::deny_path_under_active_root_uses_only_matching_root_sid1094–1127 ↗
fn deny_path_under_active_root_uses_only_matching_root_sid()

作用:验证当禁止写路径位于某个活跃写根目录下面时,只使用这个匹配根目录的能力 SID。

数据流:输入是测试临时目录中构造的 codex_home、workspace、active_root、stale_root 和 deny_path;它创建目录,分别计算几个根目录的 SID,再调用 workspace_write_cap_sids_for_path;最后断言结果只包含 active_root 的 SID,不包含 workspace、stale_root 或全局能力 SID。

调用关系:它直接测试 workspace_write_cap_sids_for_path,防止给禁止写规则套上太多无关身份,导致权限范围变乱。

调用图:调用 1 个内部函数(workspace_write_cap_sids_for_path);外部调用 6 个(assert!, assert_eq!, load_or_create_cap_sids, workspace_write_cap_sid_for_root, create_dir_all, tempdir)。

tests::deny_path_outside_active_roots_falls_back_to_all_active_root_sids1130–1164 ↗
fn deny_path_outside_active_roots_falls_back_to_all_active_root_sids()

作用:验证当禁止写路径不在任何活跃写根目录下时,会退回使用所有当前活跃写根目录的 SID。

数据流:输入是临时创建的多个目录和一个位于外部的 deny_path;它计算 workspace、active_root、stale_root 等 SID,再调用 workspace_write_cap_sids_for_path;最后检查结果包含当前活跃的两个 SID,不包含过期根目录和全局能力 SID。

调用关系:它覆盖 workspace_write_cap_sids_for_path 的兜底逻辑,确保路径无法匹配时仍然能用当前有效写权限身份来保护。

调用图:调用 1 个内部函数(workspace_write_cap_sids_for_path);外部调用 6 个(assert!, assert_eq!, load_or_create_cap_sids, workspace_write_cap_sid_for_root, create_dir_all, tempdir)。

tests::deny_path_includes_nested_active_root_sid1167–1191 ↗
fn deny_path_includes_nested_active_root_sid()

作用:验证嵌套写根目录的情况:如果受保护目录同时和外层、内层写根有关,就两个 SID 都要包含。

数据流:输入是临时 workspace、其中的 .codex 保护目录,以及 .codex 下的 nested-root;它创建目录并计算外层 workspace 和内层 nested-root 的 SID,然后调用 workspace_write_cap_sids_for_path;输出是断言结果正好包含这两个 SID。

调用关系:它测试复杂目录重叠时的行为,保证 run_setup_full 给 deny_write_paths 加保护时不会漏掉嵌套可写根。

调用图:调用 1 个内部函数(workspace_write_cap_sids_for_path);外部调用 4 个(assert_eq!, workspace_write_cap_sid_for_root, create_dir_all, tempdir)。

windows-sandbox-rs/src/wrapper.rs源码 ↗
entrypointsandbox launch

这个文件解决的是“沙箱启动信息怎么安全、完整地传过去”的问题。Windows 沙箱需要知道很多东西:要运行什么命令、在哪个目录运行、哪些文件夹能读写、哪些路径要禁止、环境变量是什么、权限档案是什么。如果这些信息传错,轻则命令跑不起来,重则沙箱权限放得太宽。文件前半部分负责把这些信息打包成 argv,也就是程序启动时收到的那串参数;中间的入口函数会建立一个异步运行环境(可以等待沙箱进程和输入输出);后半部分再把参数解析成一个请求对象,检查必填项和绝对路径,然后调用真正的 Windows 沙箱启动函数。最后,它把当前终端的输入、输出、错误输出转接给沙箱里的命令,就像人在和里面的程序直接对话一样。

函数细节10
create_windows_sandbox_command_args_for_permission_profile37–111 ↗
fn create_windows_sandbox_command_args_for_permission_profile(
    command: Vec<String>,
    command_cwd: &AbsolutePathBuf,
    workspace_roots: &[AbsolutePathBuf],
    env_map: &HashMap<String, Strin

作用:把一次沙箱运行请求打包成命令行参数,供外部代码去启动 codex.exe --run-as-windows-sandbox。别人用它是为了不用手工拼一大串容易出错的参数。

数据流:进去的是要运行的命令、工作目录、工作区路径、环境变量、权限档案、沙箱等级、读写规则等信息。它把复杂对象先转成 JSON 字符串,也就是适合塞进命令行的一段文本;再按固定标志位依次放进一个字符串列表;如果没有给工作区,就默认用当前命令目录。出来的是一份完整的参数列表,最后用 -- 分开沙箱自己的参数和真正要执行的命令。

调用关系:这是打包端的核心函数。它会把需要 JSON 表达的路径列表交给 push_json_arg 追加到参数里;之后这些参数会被另一端的 parse_windows_sandbox_wrapper_args 按同样规则拆回来。

调用图:调用 1 个内部函数(push_json_arg);外部调用 4 个(to_string, from_ref, is_empty, vec!)。

push_json_arg113–119 ↗
fn push_json_arg(args: &mut Vec<String>, flag: &str, value: &T)

作用:给参数列表追加一个“标志名 + JSON 内容”的小工具。它避免每个地方都重复写“先加标志,再把值转成 JSON”的代码。

数据流:进去的是正在组装的参数列表、一个标志名、一个可以序列化的数据值。它把标志名变成字符串放进去,再把数据转成 JSON 文本放进去。出来时没有单独返回值,但原来的参数列表被多加了两项。

调用关系:它只被 create_windows_sandbox_command_args_for_permission_profile 调用,专门服务于参数打包流程。读写根目录、禁止读取路径、禁止写入路径这些成组数据都会通过它塞进命令行。

调用图:被 1 处调用(create_windows_sandbox_command_args_for_permission_profile);外部调用 1 个(to_string)。

run_windows_sandbox_wrapper_main121–141 ↗
fn run_windows_sandbox_wrapper_main() -> !

作用:这是这个包装器真正被执行时的入口。它负责接住命令行参数,启动异步运行环境,然后把沙箱命令跑完并用正确退出码结束当前进程。

数据流:进去的是操作系统传给当前进程的命令行参数。它跳过前两个参数,建立一个 Tokio 运行时;Tokio 运行时可以理解成让程序等待异步任务的“小调度室”。然后它调用异步处理函数,拿到沙箱中命令的退出码;如果出错,就打印错误并用退出码 1 表示失败。最后它直接结束进程。

调用关系:当 codex.exe 以 Windows 沙箱包装模式启动时,会走到这里。它不亲自解析细节,也不亲自启动沙箱,而是把工作交给 run_windows_sandbox_wrapper_args,自己负责最外层的运行环境和进程退出。

调用图:调用 1 个内部函数(run_windows_sandbox_wrapper_args);外部调用 4 个(eprintln!, args, exit, new_current_thread)。

run_windows_sandbox_wrapper_args143–146 ↗
async fn run_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<i32>

作用:把“收到一堆字符串参数”变成“执行一份沙箱请求”的桥梁。它让解析参数和真正运行沙箱这两步分开,便于检查和测试。

数据流:进去的是已经截取好的参数字符串列表。它先调用解析函数,把字符串拆成结构化请求;再把请求交给运行函数。出来的是一个整数退出码,表示沙箱里那个命令成功、失败或以什么状态结束。

调用关系:它由 run_windows_sandbox_wrapper_main 调用,是入口函数下面的第一层。它先找 parse_windows_sandbox_wrapper_args 做翻译,再找 run_windows_sandbox_wrapper_request 执行。

调用图:调用 2 个内部函数(parse_windows_sandbox_wrapper_args, run_windows_sandbox_wrapper_request);被 1 处调用(run_windows_sandbox_wrapper_main)。

run_windows_sandbox_wrapper_request165–192 ↗
async fn run_windows_sandbox_wrapper_request(request: WindowsSandboxWrapperRequest) -> Result<i32>

作用:根据已经解析好的请求,真正启动 Windows 沙箱里的命令,并转发输入输出。它是从“参数处理”进入“实际运行”的地方。

数据流:进去的是一份 WindowsSandboxWrapperRequest,里面已经有命令、目录、权限、读写路径、环境变量等信息。它先确认命令不为空;然后组装成底层沙箱会话请求,调用沙箱启动函数。启动成功后,它把当前程序的标准输入输出转接给沙箱会话,并等待里面的命令结束。出来的是沙箱命令的退出码。

调用关系:它由 run_windows_sandbox_wrapper_args 调用。它把真正启动沙箱的工作交给 spawn_windows_sandbox_session_for_level,再把终端转接和等待结束的工作交给 forward_sandbox_session_stdio

调用图:被 1 处调用(run_windows_sandbox_wrapper_args);外部调用 3 个(bail!, forward_sandbox_session_stdio, spawn_windows_sandbox_session_for_level)。

parse_windows_sandbox_wrapper_args194–292 ↗
fn parse_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<WindowsSandboxWrapperRequest>

作用:把命令行里的字符串参数解析成一份清楚的沙箱请求。它负责发现缺失参数、未知参数、路径不合法、JSON 格式错误等问题,避免带着坏配置去启动沙箱。

数据流:进去的是一串参数字符串。它从左到右读取:遇到需要值的标志,就取下一个字符串;遇到 JSON,就解析成原来的数据;遇到路径,就确认它是绝对路径;遇到 --,就把后面的内容当成真正要在沙箱里执行的命令。出来的是 WindowsSandboxWrapperRequest。如果缺少 codex home、工作目录、环境变量、权限档案、沙箱等级或命令分隔符,就返回错误。

调用关系:它由 run_windows_sandbox_wrapper_args 调用,是包装器的“验票口”。它会多次使用 next_flag_value 取参数值,用 absolute_path_arg 检查路径,用 json_flag_value 解析 JSON,用 parse_windows_sandbox_level 识别沙箱等级。

调用图:调用 4 个内部函数(absolute_path_arg, json_flag_value, next_flag_value, parse_windows_sandbox_level);被 1 处调用(run_windows_sandbox_wrapper_args);外部调用 4 个(from, new, bail!, from_str)。

next_flag_value294–297 ↗
fn next_flag_value(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<String>

作用:读取某个命令行标志后面紧跟的值。它解决的是“写了 --xxx 但忘了写具体内容”这种常见错误。

数据流:进去的是参数迭代器和当前标志名。它尝试取下一个字符串;取到了就返回这个值,取不到就返回“这个标志缺少值”的错误。它会推进参数读取位置。

调用关系:它只服务于 parse_windows_sandbox_wrapper_args。每当解析器遇到需要值的标志,比如工作目录、环境变量 JSON、权限档案 JSON,就会叫它拿下一项。

调用图:被 1 处调用(parse_windows_sandbox_wrapper_args);外部调用 1 个(next)。

absolute_path_arg299–303 ↗
fn absolute_path_arg(value: String, flag: &str) -> Result<AbsolutePathBuf>

作用:把一个路径字符串变成“确认过是绝对路径”的路径对象。绝对路径就是从磁盘根位置开始写清楚的路径,避免沙箱误解相对位置。

数据流:进去的是路径文本和对应的标志名。它先把文本变成普通路径,再尝试转换成 AbsolutePathBuf,也就是项目里表示绝对路径的类型。成功就返回绝对路径;失败就返回带有标志名和路径内容的错误。

调用关系:它由 parse_windows_sandbox_wrapper_args 调用,用来检查工作目录和工作区根目录。这样后面的沙箱启动代码可以放心使用这些路径,不用再猜它们相对哪里。

调用图:调用 1 个内部函数(from_absolute_path);被 1 处调用(parse_windows_sandbox_wrapper_args);外部调用 1 个(from)。

json_flag_value305–307 ↗
fn json_flag_value(value: String, flag: &str) -> Result<T>

作用:把命令行里的 JSON 文本还原成程序里的数据。JSON 可以理解成一种通用文本格式,适合把列表、对象这类复杂信息塞进一条命令行参数。

数据流:进去的是一段 JSON 字符串和标志名。它尝试按调用者需要的类型解析这段文本;成功就返回对应的数据,比如路径列表;失败就返回“某个标志解析失败”的错误。

调用关系:它由 parse_windows_sandbox_wrapper_args 调用,专门处理读写根目录、禁止读写路径这些 JSON 参数。它和打包端的 push_json_arg 是一对:一个负责写成 JSON,一个负责读回来。

调用图:被 1 处调用(parse_windows_sandbox_wrapper_args);外部调用 1 个(from_str)。

parse_windows_sandbox_level309–316 ↗
fn parse_windows_sandbox_level(value: &str) -> Result<WindowsSandboxLevel>

作用:把文本形式的 Windows 沙箱等级翻译成程序内部认识的枚举值。这样后面的代码不用到处比较字符串。

数据流:进去的是一个字符串,比如 disabledrestricted-tokenelevated。它按固定名单匹配,成功就返回对应的 WindowsSandboxLevel;如果传来不认识的值,就返回错误,阻止继续运行。

调用关系:它由 parse_windows_sandbox_wrapper_args 在读取 --windows-sandbox-level 时调用。解析出的等级会跟随请求传给 run_windows_sandbox_wrapper_request,最终影响底层沙箱会话如何启动。

调用图:被 1 处调用(parse_windows_sandbox_wrapper_args);外部调用 1 个(bail!)。