Codex 系统手册

配置、元数据、schema、认证与网络衔接工具

stage-22.326 个文件

这一阶段像系统的工具箱,平时不在主流程中央,却让开机、联网、登录和排错都不乱。配置工具把命令行临时改项、旧名新名、审批和沙盒权限统一起来,并拦住危险值。认证和网络工具准备请求“标签”、PKCE 防冒用密钥、API key 和可信主机规则,避免走错门或泄密。其他工具整理连接器资料、数据格式说明书、版本、插件开关和错误位置,给各模块一套可靠小零件。

本阶段的文件26

API 和认证请求衔接

这些辅助工具整理出站请求元数据、规范化服务 URL,并支持客户端和代理复用的轻量认证流程。

cloud-tasks/src/util.rs源码 ↗
utilcross-cutting

云任务要和 ChatGPT 后台说话,也要把任务信息展示给人看。这个文件就像一个工具箱:先把用户填的后台地址修成统一格式,避免多一个斜杠或少一段路径就访问错地方;再从本机配置里加载登录信息,拼出请求后台时需要带的 HTTP 头(HTTP 头可以理解成网络请求的“附加说明”,比如我是谁、用什么客户端);还会把任务编号变成浏览器能打开的任务页面链接。它也提供简单的错误日志,把问题写进 error.log,方便出故障后追查。最后,它把精确时间变成“5m ago”这种人更容易读的相对时间。整体上,这里不做复杂业务,而是帮主流程把边角工作做稳。

函数细节8
set_user_agent_suffix9–13 ↗
fn set_user_agent_suffix(suffix: &str)

作用:给程序的 User-Agent 加一个后缀。User-Agent 是发网络请求时告诉服务器“我是哪个客户端”的文字,这里用后缀区分云任务界面这类来源。

数据流:进去的是一个后缀字符串 → 函数尝试拿到全局的用户代理后缀存放处,并把旧值替换成这个新后缀 → 出来没有返回值,但之后生成 User-Agent 时会带上这个标记;如果没拿到锁,就安静地什么也不做。

调用关系:初始化后台的 init_backend 会用它先标记客户端身份;build_chatgpt_headers 在组装请求头前也会调用它,保证后面 get_codex_user_agent 拿到的是带云任务标识的 User-Agent。

调用图:被 2 处调用(init_backend, build_chatgpt_headers)。

append_error_log15–25 ↗
fn append_error_log(message: impl AsRef<str>)

作用:把一条错误信息追加写到本地的 error.log 文件里,并带上当前 UTC 时间。它的作用是让程序出错后还有线索可查。

数据流:进去的是一段错误文字 → 函数取当前时间,尝试打开或创建 error.log,并以追加方式写入一行“时间 + 错误内容” → 没有返回值;如果文件打不开或写失败,它不会再制造新的错误,只是忽略。

调用关系:init_backend 和 run_main 在关键流程出问题时会叫它记一笔。它自己只负责写日志,不参与修复错误,也不把工作再交给项目里的其他函数。

调用图:被 2 处调用(init_backend, run_main);外部调用 3 个(now, new, writeln!)。

normalize_base_url30–42 ↗
fn normalize_base_url(input: &str) -> String

作用:把后台基础网址整理成程序内部统一使用的样子。这样其他代码不用猜用户输入的是不是多了斜杠、少了 /backend-api。

数据流:进去的是用户配置或外部传来的网址字符串 → 函数先去掉末尾多余的斜杠;如果发现是 chatgpt.com 或 chat.openai.com,并且还没有 /backend-api,就自动补上 → 返回整理后的网址字符串。

调用关系:resolve_environment_id 和 run_main 在访问后台前会用它统一地址;task_url 也会先调用它,再根据整理后的地址拼出浏览器可打开的任务链接。

调用图:被 3 处调用(resolve_environment_id, run_main, task_url);外部调用 1 个(format!)。

load_auth_manager44–57 ↗
async fn load_auth_manager(chatgpt_base_url: Option<String>) -> Option<AuthManager>

作用:加载登录管理器,也就是一个知道本机登录凭据放在哪、该怎么取出来的对象。没有它,程序就很难代表用户去访问需要登录的 ChatGPT 后台。

数据流:进去的是一个可选的 ChatGPT 基础网址 → 函数先加载 Codex 配置;如果配置加载失败,就返回空;如果成功,就用配置里的主目录、凭据保存方式、基础网址、钥匙串后端等信息创建 AuthManager → 返回这个登录管理器。

调用关系:init_backend 会在后台初始化时用它准备认证能力;build_chatgpt_headers 会用它读取当前登录状态,再决定请求头里要不要带授权信息。它把真正的凭据读取工作交给 AuthManager。

调用图:调用 1 个内部函数(new);被 2 处调用(init_backend, build_chatgpt_headers);外部调用 2 个(new, load_with_cli_overrides)。

build_chatgpt_headers61–79 ↗
async fn build_chatgpt_headers() -> HeaderMap

作用:为访问 ChatGPT 后台准备一组 HTTP 请求头。它至少放入 User-Agent,如果用户已登录并且适用,还会放入授权信息。

数据流:进去没有显式参数 → 函数先设置云任务界面的 User-Agent 后缀,再生成 User-Agent 文字并放进 HeaderMap;然后尝试加载登录管理器、读取认证信息;如果认证信息适合 Codex 后台,就把授权相关请求头合进去 → 返回完整的请求头集合。

调用关系:resolve_environment_id 和 run_main 在准备调用后台接口时会使用它。它内部会调用 set_user_agent_suffix 标记客户端,调用 load_auth_manager 找登录信息,再借助 auth_provider_from_auth 把登录信息变成真正能随请求发送的头。

调用图:调用 3 个内部函数(load_auth_manager, set_user_agent_suffix, get_codex_user_agent);被 2 处调用(resolve_environment_id, run_main);外部调用 4 个(new, from_static, from_str, auth_provider_from_auth)。

task_url82–94 ↗
fn task_url(base_url: &str, task_id: &str) -> String

作用:把后台地址和任务编号拼成一个人可以在浏览器里打开的任务页面地址。它解决的是“接口地址”和“网页地址”长得不完全一样的问题。

数据流:进去的是后台基础网址和任务 ID → 函数先用 normalize_base_url 把网址整理好;如果网址末尾是 /backend-api 或 /api/codex,就去掉接口专用部分再拼 /codex/tasks/任务ID;如果已经以 /codex 结尾,就直接拼 /tasks/任务ID;其他情况按默认规则拼接 → 返回最终网页链接。

调用关系:format_task_list_lines 在显示任务列表时会用它给任务生成链接;run_exec_command 在执行相关命令时也会用它。它依赖 normalize_base_url 先把输入网址变干净。

调用图:调用 1 个内部函数(normalize_base_url);被 2 处调用(format_task_list_lines, run_exec_command);外部调用 1 个(format!)。

format_relative_time96–114 ↗
fn format_relative_time(reference: DateTime<Utc>, ts: DateTime<Utc>) -> String

作用:把一个时间点转换成“多久以前”的短文字,比如“30s ago”“5m ago”“2h ago”。这比直接显示完整时间更适合任务列表和状态栏。

数据流:进去的是一个参考时间和一个要显示的时间 → 函数计算两者相差多少秒;未来时间会按 0 秒处理;小于一分钟显示秒,小于一小时显示分钟,小于一天显示小时;更久的时间则转成本地时区并显示月、日、时、分 → 返回一段可读的时间文字。

调用关系:format_task_status_lines 会用它展示任务状态时间;format_relative_time_now 也把实际工作交给它,只是自动把参考时间设成当前时间。

调用图:被 2 处调用(format_task_status_lines, format_relative_time_now);外部调用 2 个(with_timezone, format!)。

format_relative_time_now116–118 ↗
fn format_relative_time_now(ts: DateTime<Utc>) -> String

作用:用“现在”作为参照,显示某个时间点是多久以前。调用者不用自己先取当前时间。

数据流:进去的是一个 UTC 时间点 → 函数取得当前 UTC 时间,再把当前时间和目标时间交给 format_relative_time → 返回类似“10m ago”的字符串,不改动外部状态。

调用关系:render_task_item 在渲染单个任务条目时会调用它,让界面能显示友好的相对时间。它本身是 format_relative_time 的简便包装。

调用图:调用 1 个内部函数(format_relative_time);被 1 处调用(render_task_item);外部调用 1 个(now)。

codex-api/src/requests/headers.rs源码 ↗
io_transportrequest handling

可以把这里的代码理解成给快递贴面单:请求本身是包裹,请求头就是写在外面的关键信息。这个文件会把 session-id、thread-id 这类标识放进 HTTP 头里,让后端知道这次请求属于哪个会话、哪个对话线程。它还会把某些子代理来源,比如 review、compact,翻译成服务端能读懂的字符串。插入请求头时,它不会硬塞无效内容;如果头名或头值格式不合法,就直接跳过,避免因为一条坏标签把整个请求弄崩。

函数细节3
build_session_headers5–14 ↗
fn build_session_headers(session_id: Option<String>, thread_id: Option<String>) -> HeaderMap

作用:这个函数用来生成一组 HTTP 请求头,里面可选地带上会话编号和线程编号。调用方在准备发请求时用它,确保服务端能认出这次请求属于哪段上下文。

数据流:进去的是两个可能有、也可能没有的字符串:session_id 和 thread_id。它先新建一张空的请求头表;如果有 session_id,就尝试放入名为 session-id 的头;如果有 thread_id,就尝试放入名为 thread-id 的头。出来的是整理好的 HeaderMap;它不改外部数据,只返回这张新表。

调用关系:它在 stream_request 准备网络请求时被调用,属于发请求前的打包步骤。真正把某一条头安全放进去的活儿,会交给 insert_header,这样生成头表的逻辑不用关心格式校验细节。

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

subagent_header16–31 ↗
fn subagent_header(source: &Option<SessionSource>) -> Option<String>

作用:这个函数把“请求来自哪个子代理”转换成一个适合放进请求头的短字符串。比如代码审查子代理会变成 review,压缩上下文的子代理会变成 compact。

数据流:进去的是一个可能为空的 SessionSource,也就是会话来源信息。它先确认来源确实是 SubAgent;如果不是,直接返回空。然后按具体子代理类型翻译成固定字符串,或者在 Other 的情况下保留自定义标签。出来的是一个可选字符串,不会修改输入。

调用关系:它由 stream_request 在准备请求时使用,用来补充说明这次请求是不是由某个子任务触发。它不负责真正写入 HTTP 头,只负责把内部枚举值翻译成网络上传输更方便的文本。

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

insert_header33–40 ↗
fn insert_header(headers: &mut HeaderMap, name: &str, value: &str)

作用:这个函数负责把一条请求头安全地塞进 HeaderMap。它的作用像门卫:只有头名和头值都符合 HTTP 规则,才允许放进去。

数据流:进去的是一张可修改的请求头表、一个头名字符串、一个头值字符串。它会先把头名解析成 HTTP 认可的 HeaderName,再把头值转换成 HeaderValue;两步都成功时才插入。出来没有单独返回值,但可能会改动传入的 HeaderMap;如果格式不对,就什么也不改。

调用关系:它被 build_session_headers 用来插入 session-id 和 thread-id,也会被 stream_request 直接用于其他请求头。它把“检查格式”和“插入”封装在一起,让上层流程不用每次都重复写这套保护逻辑。

调用图:被 2 处调用(stream_request, build_session_headers);外部调用 2 个(insert, from_str)。

codex-client/src/chatgpt_hosts.rs源码 ↗
utilrequest handling / cross-cutting

这个文件做的事很小,但很关键:给一个主机名,比如“chatgpt.com”或“foo.chatgpt.com”,判断它是不是允许当作第一方 ChatGPT 流量来处理。这里的“第一方”可以理解成“确实是自家站点”,不是别人套了个相似名字来冒充。它先列出完全匹配的官方域名,比如 chatgpt.com、chat.openai.com,再列出允许的子域名后缀,比如“.chatgpt.com”。注意它用的是带点的后缀,所以“foo.chatgpt.com”可以通过,但“evilchatgpt.com”不会通过。这就像门卫看证件,不只是看名字里有没有“ChatGPT”几个字,而是看是不是正规地址。文件里还带了一个测试,专门检查常见的真假主机名,防止以后有人不小心把规则改松,留下安全漏洞。

函数细节2
is_allowed_chatgpt_host3–11 ↗
fn is_allowed_chatgpt_host(host: &str) -> bool

作用:判断传进来的主机名是不是 Codex 认可的 ChatGPT 官方主机。有人在处理 ChatGPT 相关网址或 Cookie 时会用它来挡掉假冒域名。

数据流:进去的是一个主机名字符串,比如“chatgpt.com”或“chatgpt.com.evil.example”。函数先看它是不是几个明确写死的官方域名之一;如果不是,再看它是不是以允许的 ChatGPT 子域名后缀结尾。最后出来的是 true 或 false:true 表示可信,false 表示不能当作官方 ChatGPT 主机处理;它不修改任何外部状态。

调用关系:它是一个小门卫函数。上层的 is_chatgpt_cookie_url 在判断某个 Cookie 网址是否属于 ChatGPT 时会调用它;它自己不再把工作交给项目里的其他函数,只用字符串匹配完成判断。

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

tests::recognizes_chatgpt_hosts_without_suffix_tricks18–38 ↗
fn recognizes_chatgpt_hosts_without_suffix_tricks()

作用:这是给主机名判断规则做的安全测试。它确认真正的 ChatGPT 域名能通过,同时确认带迷惑性的假域名不会钻空子。

数据流:进去的是测试里列好的两组主机名:一组应该被允许,一组应该被拒绝。测试逐个调用判断函数,然后用断言检查结果是否符合预期;如果有一个结果错了,测试就失败,提醒开发者规则被改坏了。

调用关系:它由测试运行器在跑测试时执行,不参与正常用户请求。它通过 assert! 这类断言工具验证结果,主要是在旁边守住 is_allowed_chatgpt_host 的行为,防止以后改代码时引入“后缀伪装”这类安全问题。

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

login/src/auth/util.rs源码 ↗
utilrequest handling / error handling

登录出错时,服务器可能直接回一句话,也可能回一段 JSON(可以理解成一种带层级的文本格式),里面真正有用的信息藏在 error.message 里。这个文件做的事很简单:先尝试把服务器返回的文本当成 JSON 读出来;如果里面有 error.message,就把这句话拿出来;如果没有,就退一步返回原始文本;如果连文本都是空的,就给一个兜底的 “Unknown error”。它还会用 debug 日志记录正在解析的内容,方便开发者排查问题。可以把它想成客服系统里的“摘要员”:面对一封格式复杂的投诉邮件,它只挑出最关键、最能告诉用户发生了什么的那句话。

函数细节3
try_parse_error_message3–16 ↗
fn try_parse_error_message(text: &str) -> String

作用:把服务器返回的错误文本变成一条更清楚的错误消息。别人会在刷新登录令牌或撤销 OAuth 令牌失败时用它,让错误提示不要又长又乱。

数据流:进去的是一段服务器返回的文本。它先写一条调试日志,然后尝试把文本按 JSON 解析;如果找到 error 里面的 message 字段,就输出这个 message;如果找不到但原文不空,就原样输出;如果原文为空,就输出 “Unknown error”。它不修改外部状态,只返回整理后的字符串。

调用关系:它处在认证请求失败后的收尾位置:request_chatgpt_token_refresh 和 revoke_oauth_token 在拿到失败响应后会调用它,把服务器的原始错误变成更适合展示或记录的话。测试函数也会调用它,确认它在标准错误格式和非标准格式下都能给出合理结果。它自己只额外使用 debug! 记录调试信息。

调用图:被 4 处调用(request_chatgpt_token_refresh, revoke_oauth_token, try_parse_error_message_extracts_openai_error_message, try_parse_error_message_falls_back_to_raw_text);外部调用 1 个(debug!)。

tests::try_parse_error_message_extracts_openai_error_message23–37 ↗
fn try_parse_error_message_extracts_openai_error_message()

作用:验证当服务器返回 OpenAI 风格的错误 JSON 时,函数能准确抽出 error.message 里的那句话。

数据流:进去的是一段测试用的 JSON 字符串,里面包含 error.message。测试把这段文本交给 try_parse_error_message,然后用 assert_eq! 检查输出是不是预期的错误说明。它不产生业务结果,只是在测试运行时证明行为正确。

调用关系:这是 try_parse_error_message 的测试用例之一,模拟真实认证接口返回的结构化错误。它调用 try_parse_error_message 做实际解析,再用断言工具 assert_eq! 判断结果是否正确。

调用图:调用 1 个内部函数(try_parse_error_message);外部调用 1 个(assert_eq!)。

tests::try_parse_error_message_falls_back_to_raw_text40–44 ↗
fn try_parse_error_message_falls_back_to_raw_text()

作用:验证当 JSON 里没有 error.message 这个标准位置时,函数不会乱猜,而是把原始文本返回。

数据流:进去的是一段只有 message 字段、没有 error.message 的 JSON 字符串。测试调用 try_parse_error_message 后,检查输出是否仍然是原始文本。这样能确认兜底逻辑没有把信息丢掉。

调用关系:这是另一个保护 try_parse_error_message 行为的测试。它覆盖的是非标准错误格式,确保主函数在看不懂结构时仍然给调用者留下原始错误内容,而不是返回空或错误解析结果。

调用图:调用 1 个内部函数(try_parse_error_message);外部调用 1 个(assert_eq!)。

login/src/pkce.rs源码 ↗
domain_logiclogin flow startup

这个文件解决的是“怎么证明发起登录的人,和最后拿授权码来换令牌的人,是同一个人”这个问题。它会先造出一段很难猜的随机字符串,叫 code_verifier,可以理解成临时暗号。然后把这个暗号做一次 SHA-256 哈希(一种把内容变成固定长度指纹的算法),再用 URL 安全的 Base64(一种适合放进网址里的编码方式)变成 code_challenge。登录开始时,程序把 challenge 交给认证服务;登录结束时,再用 verifier 去证明自己。别人即使半路偷到了授权码,也没有这个 verifier,就很难冒充成功。文件里的 PkceCodes 就是一个小盒子,专门装这两个字符串;generate_pkce 则是制造这个小盒子的工厂。

函数细节1
generate_pkce12–27 ↗
fn generate_pkce() -> PkceCodes

作用:生成一套新的 PKCE 登录用代码:一个私下保存的 code_verifier,和一个可以发给认证服务器的 code_challenge。每次登录都应该重新生成,避免旧暗号被复用带来风险。

数据流:进去时不需要调用者提供任何参数;它自己向随机数生成器要 64 个随机字节,把这些字节编码成适合放进网址的 code_verifier;接着把 verifier 做 SHA-256 指纹计算,再编码成 code_challenge;出来的是一个 PkceCodes,里面装着这两个字符串。它不修改外部状态,只返回新生成的结果。

调用关系:它会在 run_login_server 准备发起登录流程时被调用。它把“造安全随机暗号”和“计算 challenge 指纹”这两件事做完,其中随机数来自外部的 rng,指纹计算交给外部的 digest。run_login_server 拿到结果后,通常会把 challenge 放进登录请求,把 verifier 留到后面换取登录令牌时使用。

调用图:被 1 处调用(run_login_server);外部调用 2 个(digest, rng)。

ollama/src/url.rs源码 ↗
utilprovider setup / config conversion

不同客户端可能用不同样子的服务地址。OpenAI 兼容接口通常把地址写成类似 http://localhost:11434/v1,而 Ollama 自己的原生服务根地址是 http://localhost:11434。这个文件就是一个小工具箱,避免别的代码到处手写字符串裁剪,写错就可能把请求发到错误路径。它先会把地址末尾多余的斜杠去掉,再判断最后是不是 /v1。如果是,就认为这是 OpenAI 兼容的根地址;如果要转成 Ollama 主机根地址,就把这个 /v1 去掉。可以把它理解成“地址门牌号清洁工”:先擦掉多余的尾巴,再按需要拆掉 OpenAI 那一层门牌。文件里还带了一个测试,确认常见输入都会得到预期结果。

函数细节3
is_openai_compatible_base_url2–4 ↗
fn is_openai_compatible_base_url(base_url: &str) -> bool

作用:判断一个基础地址是不是 OpenAI 兼容接口的根地址,也就是整理后是否以 /v1 结尾。这样上层代码就能知道该按 OpenAI 风格还是 Ollama 原生风格来理解这个地址。

数据流:进去的是一个地址字符串。它先去掉地址末尾多余的 /,再检查整理后的地址是否以 /v1 结尾。出来的是一个布尔值:true 表示像 OpenAI 兼容地址,false 表示不像;它不修改外部数据。

调用关系:它会被 try_from_provider 调用。也就是说,当系统从某个 provider 配置里尝试建立 Ollama 连接信息时,会先用它判断这个 provider 给的地址是不是 OpenAI 兼容格式。

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

base_url_to_host_root8–18 ↗
fn base_url_to_host_root(base_url: &str) -> String

作用:把 provider 里给出的基础地址转换成 Ollama 原生主机根地址。最典型的用途是把 http://localhost:11434/v1 变成 http://localhost:11434

数据流:进去的是一个地址字符串。它先去掉末尾多余的 /;如果整理后的地址以 /v1 结尾,就把这段 /v1 去掉,并再次清理末尾斜杠;如果没有 /v1,就直接使用清理后的地址。出来的是一个新的字符串,表示可用于 Ollama 原生接口的主机根地址;它不会改原来的输入字符串。

调用关系:它也会被 try_from_provider 调用。try_from_provider 在根据 provider 信息创建连接配置时,需要一个干净的 Ollama 主机地址,于是把地址交给这个函数做统一转换。

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

tests::test_base_url_to_host_root25–38 ↗
fn test_base_url_to_host_root()

作用:验证 base_url_to_host_root 对几种常见地址写法都能给出正确结果。它防止以后有人改代码时,不小心把地址转换规则弄坏。

数据流:进去的是测试里写死的几个示例地址,比如带 /v1、不带 /v1、末尾带 / 的地址。测试调用 base_url_to_host_root 得到实际结果,再用 assert_eq! 比较它和预期字符串是否一样。测试通过就没有输出;测试失败会报错,指出哪个结果不符合预期。

调用关系:它是测试代码,在运行测试时由测试框架调用。它不参与正常运行流程,只专门检查 base_url_to_host_root 这个地址转换小工具是否可靠。

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

responses-api-proxy/src/read_api_key.rs源码 ↗
io_transportstartup

这个文件解决的是一个很实际的问题:代理程序启动时需要拿到 API key,但 API key 是密码一样的敏感东西,不能随便在内存里复制、缓存或留下垃圾。它会从标准输入读取一小段内容,加上 Bearer 前缀,组成给服务器认证用的 Authorization 头。读入时它限制最大长度,去掉末尾换行,检查只能包含字母、数字、短横线和下划线,防止混进奇怪字符。Unix 系统上还故意不用 Rust 标准输入的缓冲读取,因为那可能偷偷多存一份密钥;它直接调用底层 read。读完后,临时缓冲区会被清零。最终字符串会被“泄漏”为静态引用,意思是程序运行期间一直保留,并尝试用 mlock 把它锁在内存里,避免被系统换到磁盘上。可以把它理解成:密钥进门后只放进一个上锁抽屉,用完的草稿纸立刻粉碎。

函数细节13
read_auth_header_from_stdin21–30 ↗
fn read_auth_header_from_stdin() -> Result<&'static str>

作用:这是外部真正会调用的入口,用来从标准输入读取 API key,并返回可以直接放进请求里的认证头。程序启动时需要拿密钥,就会走这里。

数据流:进去的是标准输入里的文本,也就是用户通过管道传进来的 API key;它把具体读取动作交给平台对应的读取函数,再交给 read_auth_header_with 做拼接、校验和保护;出来的是一个形如 Bearer sk-... 的长期可用字符串,失败时返回错误。

调用关系run_main 在程序启动流程里调用它。它自己不做复杂处理,而是把核心工作交给 read_auth_header_with;在 Unix 上读取动作由 read_from_unix_stdin 完成,在 Windows 上暂时用标准库的 stdin 读取。

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

read_from_unix_stdin41–70 ↗
fn read_from_unix_stdin(buffer: &mut [u8]) -> std::io::Result<usize>

作用:这是 Unix 系统上的低层读取函数,直接从标准输入读字节。它这样做是为了绕开标准库可能产生的内部缓冲,避免 API key 被额外复制一份。

数据流:进去的是一块已经准备好的字节缓冲区;它调用操作系统的 read 往这块缓冲区里写入标准输入内容,如果被信号打断就重试,如果读到结尾就返回 0,如果出错就返回系统错误;出来的是实际读到的字节数。

调用关系:它被 read_auth_header_from_stdin 作为读取工具传给 read_auth_header_with。它只负责安全地“把字节搬进来”,不判断 API key 是否合规,也不生成认证头。

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

read_auth_header_with72–162 ↗
fn read_auth_header_with(mut read_fn: F) -> Result<&'static str>

作用:这是本文件的核心函数:它把“读取字节”变成“安全可用的 Authorization 头”。测试也大量调用它,因为它可以接收一个假的读取函数,方便模拟各种输入情况。

数据流:进去的是一个读取函数,调用它可以把 API key 字节写进缓冲区;函数先在缓冲区开头放入 Bearer ,再一点点读取密钥,遇到换行或文件结尾就停,超长就报错。然后它去掉末尾换行,拒绝空输入,调用 validate_auth_header_bytes 检查字符,再把字节转成 UTF-8 字符串。临时缓冲区随后被清零,最终字符串被放到堆内存里并交给 mlock_str 尝试锁住;出来的是长期有效的认证头,或者一个说明原因的错误。

调用关系read_auth_header_from_stdin 用它处理真实标准输入;各个测试函数用它处理假输入。它内部会把字符合法性检查交给 validate_auth_header_bytes,把内存锁定交给 mlock_str,并在出错时用 anyhow 包装出人能看懂的错误。

调用图:调用 2 个内部函数(mlock_str, validate_auth_header_bytes);被 9 处调用(read_auth_header_from_stdin, errors_on_invalid_characters, errors_on_invalid_utf8, errors_when_buffer_filled, errors_when_no_input_provided, propagates_io_error, reads_key_and_trims_newlines, reads_key_with_no_newlines, reads_key_with_short_reads);外部调用 3 个(from, anyhow!, from_utf8)。

mlock_str204–204 ↗
fn mlock_str(_value: &str)

作用:这个函数尝试把保存 API key 的那段内存锁住,避免操作系统把它换到磁盘上。磁盘比内存更容易留下痕迹,所以这是一个安全加固步骤。

数据流:进去的是已经生成好的认证头字符串;在 Unix 上,它会找到字符串所在内存跨过的整页范围,并调用系统的 mlock 尝试锁住这些页;在非 Unix 平台上,这个函数什么也不做。它不改变字符串内容,也不把失败当成致命错误。

调用关系read_auth_header_with 在最终字符串准备好后调用它。它位于流程最后一段:不是用来读取或校验密钥,而是尽量保护已经留在内存里的那一份密钥。

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

validate_auth_header_bytes208–219 ↗
fn validate_auth_header_bytes(key_bytes: &[u8]) -> Result<()>

作用:这个函数检查 API key 里有没有不该出现的字符。它只允许英文字母、数字、短横线和下划线,防止空字节、感叹号、乱码等内容混进去。

数据流:进去的是 API key 的原始字节,不包括前面的 Bearer ;它逐个字节检查是否属于允许范围;如果全部合格就返回成功,如果发现非法字符就返回错误信息。

调用关系read_auth_header_with 在把输入正式当成认证头之前调用它。它像门口安检,先确认密钥内容干净,再允许后面的 UTF-8 转换和内存锁定继续进行。

调用图:被 1 处调用(read_auth_header_with);外部调用 1 个(anyhow!)。

tests::reads_key_with_no_newlines228–242 ↗
fn reads_key_with_no_newlines()

作用:这个测试确认:如果输入的 API key 后面没有换行,函数也能正常读出来。很多命令输出不一定带换行,所以这个情况要保证可用。

数据流:进去的是测试里伪造的一次性输入 sk-abc123;测试把它喂给 read_auth_header_with;出来的结果应该正好是 Bearer sk-abc123,并用断言检查。

调用关系:它直接测试 read_auth_header_with 的基本成功路径,不涉及真实标准输入。它证明核心函数不依赖换行才能结束读取,读到文件结尾也可以完成。

调用图:调用 1 个内部函数(read_auth_header_with);外部调用 1 个(assert_eq!)。

tests::reads_key_with_short_reads245–258 ↗
fn reads_key_with_short_reads()

作用:这个测试确认:如果输入被分成几小段才读到,函数也能拼成完整 API key。真实系统读取数据时,经常不是一次读完。

数据流:进去的是三段模拟输入:sk-abc123\n;测试让 read_auth_header_with 分多次读取这些片段;出来的结果应该是完整的 Bearer sk-abc123

调用关系:它测试 read_auth_header_with 的循环读取能力。这个测试说明核心函数不假设底层读取一次就给齐全部内容,因此更适合真实管道和终端输入。

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

tests::reads_key_and_trims_newlines261–275 ↗
fn reads_key_and_trims_newlines()

作用:这个测试确认:如果 API key 末尾带有 Windows 风格或普通换行,函数会把换行去掉。这样用户用 echoprintenv 管道传值时不会出错。

数据流:进去的是带 \r\n 结尾的 sk-abc123read_auth_header_with 读取后会裁掉末尾的回车和换行;出来的结果应该是 Bearer sk-abc123

调用关系:它验证 read_auth_header_with 的收尾清理逻辑。这个行为让命令行使用更自然,因为多数命令输出环境变量时都会自动带一个换行。

调用图:调用 1 个内部函数(read_auth_header_with);外部调用 1 个(assert_eq!)。

tests::errors_when_no_input_provided278–282 ↗
fn errors_when_no_input_provided()

作用:这个测试确认:如果标准输入什么都没有,函数会明确报错,而不是生成一个只有 Bearer 的空认证头。

数据流:进去的是一个总是返回文件结尾的假读取函数;read_auth_header_with 发现没有读到密钥内容;出来的是错误,测试检查错误信息里包含“必须提供”的提示。

调用关系:它覆盖 read_auth_header_with 的空输入失败路径。这个测试保证用户忘记传 API key 时,会得到清楚提示,而不是后面请求服务器时才出现更难懂的认证失败。

调用图:调用 1 个内部函数(read_auth_header_with);外部调用 2 个(assert!, format!)。

tests::errors_when_buffer_filled285–296 ↗
fn errors_when_buffer_filled()

作用:这个测试确认:API key 太长时会被拒绝。固定缓冲区是为了避免无限读入敏感数据,超出范围就要停下来报错。

数据流:进去的是刚好填满密钥区域、但没有换行也没有结束标记的一大串字节;read_auth_header_with 判断缓冲区满了还没读完;出来的是“API key 太大”的错误,测试检查错误内容。

调用关系:它测试 read_auth_header_with 的长度保护。这个保护避免输入异常时把程序拖进不清楚的状态,也避免密钥处理逻辑扩大内存暴露面。

调用图:调用 1 个内部函数(read_auth_header_with);外部调用 2 个(assert!, format!)。

tests::propagates_io_error299–305 ↗
fn propagates_io_error()

作用:这个测试确认:底层读取失败时,错误会原样传出来。这样调用者能知道问题是输入读取失败,而不是密钥格式问题。

数据流:进去的是一个总是返回 boom 读取错误的假读取函数;read_auth_header_with 接到错误后清理缓冲区并停止;出来的是同一个 I/O 错误,测试检查错误类型和文字。

调用关系:它验证 read_auth_header_with 对读取层错误的处理方式。这里的重点是:核心函数不吞掉底层错误,也不把它改得面目全非。

调用图:调用 1 个内部函数(read_auth_header_with);外部调用 1 个(assert_eq!)。

tests::errors_on_invalid_utf8308–323 ↗
fn errors_on_invalid_utf8()

作用:这个测试确认:输入里有非法字节时会报错。API key 应该是简单 ASCII 字符,不能混入无法正常解释的乱码。

数据流:进去的是包含 0xff 这种非法字节的模拟输入;read_auth_header_with 在校验阶段发现它不属于允许字符;出来的是字符非法的错误,测试检查错误信息。

调用关系:它测试 read_auth_header_with 调用 validate_auth_header_bytes 的效果。虽然后面还有 UTF-8 转换,这个测试说明非法内容会更早被拦住。

调用图:调用 1 个内部函数(read_auth_header_with);外部调用 2 个(assert!, format!)。

tests::errors_on_invalid_characters326–341 ↗
fn errors_on_invalid_characters()

作用:这个测试确认:即使输入是普通可读字符,只要不在允许范围内,也会被拒绝。比如感叹号看起来正常,但不应该出现在这种 API key 里。

数据流:进去的是包含 !sk-abc!23read_auth_header_with 读取后交给校验函数;校验失败,出来的是“只能包含字母、数字、短横线或下划线”的错误,测试检查这条提示。

调用关系:它覆盖 validate_auth_header_bytes 在真实读取流程中的非法字符分支。这个测试保证认证头不会带着奇怪字符继续进入网络请求流程。

调用图:调用 1 个内部函数(read_auth_header_with);外部调用 2 个(assert!, format!)。

rmcp-client/src/utils.rs源码 ↗
utilcross-cutting

启动 MCP 服务器时,程序不能把当前进程的所有环境变量一股脑传过去。环境变量就像一张“随身便签”,里面可能有路径、语言设置、密钥等信息;传少了服务器可能跑不起来,传多了又可能泄露不该给它看的东西。这个文件把这件事拆清楚:本地 stdio MCP 会继承一组常见基础变量,比如 PATH、HOME;远程 stdio MCP 则更谨慎,只转发配置里明确点名的变量。它还会识别哪些变量应该由远程执行端提供。另一部分是 HTTP 请求头,配置里可以直接写请求头,也可以说“从某个环境变量里读值”。文件会检查请求头名字和值是否合法,不合法就警告并跳过,避免坏配置把整个客户端拖垮。测试部分用一个小守卫临时改环境变量,测试结束再恢复,防止测试互相污染。

函数细节15
create_env_for_mcp_server12–25 ↗
fn create_env_for_mcp_server(
    extra_env: Option<HashMap<OsString, OsString>>,
    env_vars: &[McpServerEnvVar],
) -> Result<HashMap<OsString, OsString>>

作用:为本地启动的 MCP 服务器准备一份环境变量表。它保证服务器能拿到必要的基础变量,也允许配置额外点名要带过去的变量和覆盖值。

数据流:进去的是一份可选的额外环境变量,以及配置里声明要继承的变量列表。它先让 local_stdio_env_var_names 检查这些变量是不是适合本地 stdio 使用,再从当前进程里读取默认白名单变量和额外点名变量,最后把手动传入的覆盖值合进去。出来的是一张新的环境变量表;如果配置里混入了只能远程使用的变量,就返回错误。

调用关系:它是本地 MCP 启动前的准备步骤,会被 new 和 launch_server 这类创建或启动流程调用。它把“哪些变量能带过去”的判断交给 local_stdio_env_var_names,自己负责真正拼出要给子进程的环境变量。多个测试会调用它,确认覆盖、额外变量、非法远程变量和非 UTF-8 路径都能按预期处理。

调用图:调用 1 个内部函数(local_stdio_env_var_names);被 6 处调用(new, launch_server, create_env_honors_overrides, create_env_includes_additional_whitelisted_variables, create_env_preserves_path_when_it_is_not_utf8, create_local_env_rejects_remote_source_variables)。

create_env_overlay_for_remote_mcp_server27–40 ↗
fn create_env_overlay_for_remote_mcp_server(
    extra_env: Option<HashMap<OsString, OsString>>,
    env_vars: &[McpServerEnvVar],
) -> HashMap<OsString, OsString>

作用:为远程 MCP 服务器准备一份“环境变量补丁”。远程场景下,它不会复制本机的 PATH、HOME 等默认变量,只转发配置明确允许从本地拿的变量。

数据流:进去的是可选的额外环境变量,以及配置声明的变量列表。它跳过标记为 remote 来源的变量,只从当前进程读取非 remote 的变量值,再合并配置里直接给出的覆盖值。出来的是一张较小的环境变量表,只包含应该由本地编排端提供的内容。

调用关系:它在 launch_server 启动远程 stdio MCP 时使用。和 create_env_for_mcp_server 不同,它刻意不带默认环境变量,因为远程执行端应该用自己的 PATH、HOME 等基础环境。相关测试会确认默认变量不会被偷偷转发,remote 来源的变量也不会从本地复制。

调用图:被 3 处调用(launch_server, create_remote_env_overlay_does_not_copy_remote_source_variables, create_remote_env_overlay_only_forwards_explicit_variables);外部调用 1 个(iter)。

remote_mcp_env_var_names42–48 ↗
fn remote_mcp_env_var_names(env_vars: &[McpServerEnvVar]) -> Vec<String>

作用:找出配置里标明“应该从远程那边取值”的环境变量名字。调用方可以据此告诉远程执行端:这些变量由你来提供。

数据流:进去的是一组 MCP 环境变量配置。它逐个查看变量来源,只挑出 source 为 remote 的项,并取出名字。出来的是字符串列表,里面只有远程来源变量名。

调用关系:它会在 launch_server 的远程启动流程里使用,配合 create_env_overlay_for_remote_mcp_server 区分“本地给的变量”和“远程自己给的变量”。测试 remote_mcp_env_var_names_returns_remote_source_names 会验证它只返回 remote 项,不返回旧格式或 local 项。

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

local_stdio_env_var_names50–58 ↗
fn local_stdio_env_var_names(env_vars: &[McpServerEnvVar]) -> Result<impl Iterator<Item = &str>>

作用:检查本地 stdio MCP 的环境变量配置是否合法,并给出可以从本机继承的变量名。stdio 可以理解为用标准输入输出和子进程说话的方式。

数据流:进去的是配置里的环境变量列表。它先查有没有标记为 remote 来源的变量;如果有,就生成一个错误,提醒这种变量必须走远程 MCP stdio。没有问题时,它把每个配置项转换成变量名迭代器交出去。

调用关系:它只被 create_env_for_mcp_server 调用,是本地环境变量组装前的“门卫”。这样 create_env_for_mcp_server 不需要自己理解 remote 规则,只要拿到通过检查的变量名继续读取环境即可。

调用图:被 1 处调用(create_env_for_mcp_server);外部调用 2 个(anyhow!, iter)。

build_default_headers60–116 ↗
fn build_default_headers(
    http_headers: Option<HashMap<String, String>>,
    env_http_headers: Option<HashMap<String, String>>,
) -> Result<HeaderMap>

作用:把配置里的 HTTP 请求头整理成 reqwest 能直接使用的 HeaderMap。HTTP 请求头就是随请求一起带的小标签,常用来放认证信息、客户端信息等。

数据流:进去的是两类可选配置:一类是直接写死的请求头名和值,另一类是请求头名加环境变量名。它先处理写死的请求头,检查名字和值是否合法;再从环境变量读取动态值,空值会跳过,同样检查合法性。不合法的项只记录警告并跳过。出来的是一张可用于 HTTP 客户端的默认请求头表。

调用关系:它会被认证检查、OAuth 发现、客户端创建和待定传输创建流程调用。它处在网络请求真正发出之前,负责把人类配置或环境变量变成安全可用的 HTTP 头,后续可再交给 apply_default_headers 装到 HTTP 客户端构建器上。

调用图:被 4 处调用(determine_streamable_http_auth_status, discover_streamable_http_oauth, new, create_pending_transport);外部调用 5 个(new, from_bytes, from_str, var, warn!)。

apply_default_headers118–127 ↗
fn apply_default_headers(
    builder: ClientBuilder,
    default_headers: &HeaderMap,
) -> ClientBuilder

作用:把已经整理好的默认 HTTP 请求头装到 reqwest 的 ClientBuilder 上。ClientBuilder 可以理解为“HTTP 客户端的施工图”。

数据流:进去的是一个 HTTP 客户端构建器和一张默认请求头表。它先看请求头表是不是空的;如果空,就原样返回构建器;如果不空,就把请求头复制一份设置进去。出来的是更新后的构建器。

调用关系:它会被 OAuth 发现、客户端创建以及 OAuth 传输运行时创建流程调用。通常 build_default_headers 先负责做出 HeaderMap,apply_default_headers 再负责把这份 HeaderMap 接到具体 HTTP 客户端上。

调用图:被 3 处调用(discover_streamable_http_oauth_with_headers, new, create_oauth_transport_and_runtime);外部调用 3 个(default_headers, clone, is_empty)。

tests::EnvVarGuard::set162–171 ↗
fn set(key: &str, value: impl AsRef<OsStr>) -> Self

作用:在测试里临时设置一个环境变量,并记住它原来的值。这样测试可以放心改环境,不会永久影响后面的测试。

数据流:进去的是环境变量名和值。它先读取这个变量原来的值,再把变量设置成测试需要的新值,最后返回一个 EnvVarGuard 对象,里面保存变量名和原始值。结果是当前进程环境被临时改了,同时有了恢复现场所需的信息。

调用关系:它被多个环境变量相关测试调用。测试函数创建这个守卫后,等守卫离开作用域,tests::EnvVarGuard::drop 会自动把环境恢复回去,像借了东西用完自动归还。

调用图:外部调用 3 个(as_ref, set_var, var_os)。

tests::EnvVarGuard::drop175–185 ↗
fn drop(&mut self)

作用:在测试结束或守卫对象销毁时,把被改过的环境变量恢复原状。它防止一个测试留下的环境变量影响另一个测试。

数据流:进去的是 EnvVarGuard 保存的变量名和原始值。如果原来有值,它就把变量设回原值;如果原来不存在,它就删除这个变量。出来没有返回值,但当前进程环境被恢复。

调用关系:它不是被测试代码手动调用,而是 Rust 在 EnvVarGuard 生命周期结束时自动调用。它和 tests::EnvVarGuard::set 成对工作,保证环境变量测试可以安全地临时改全局状态。

调用图:外部调用 2 个(remove_var, set_var)。

tests::create_env_honors_overrides189–198 ↗
async fn create_env_honors_overrides()

作用:验证本地 MCP 环境变量里的手动覆盖值会生效。也就是说,配置明确给出的值应该优先于默认继承的值。

数据流:进去的是测试构造的额外环境变量,其中 TZ 被设为 custom。测试调用 create_env_for_mcp_server 生成环境表,再检查结果里的 TZ 是否等于 custom。出来是一次断言结果;如果实现不尊重覆盖值,测试会失败。

调用关系:它直接测试 create_env_for_mcp_server 的合并顺序。这个测试保证启动 MCP 服务器时,用户配置的显式覆盖不会被系统默认环境变量冲掉。

调用图:调用 1 个内部函数(create_env_for_mcp_server);外部调用 3 个(from, from, assert_eq!)。

tests::create_env_includes_additional_whitelisted_variables202–210 ↗
fn create_env_includes_additional_whitelisted_variables()

作用:验证配置里额外点名的环境变量会被带给本地 MCP 服务器。这样用户可以让服务器拿到默认白名单之外的必要变量。

数据流:进去的是一个测试环境变量名 EXTRA_RMCP_ENV 和对应值。测试先用 tests::EnvVarGuard::set 临时设置它,再把这个名字传给 create_env_for_mcp_server,最后检查生成的环境表是否包含该值。出来是断言是否通过。

调用关系:它测试 create_env_for_mcp_server 通过 local_stdio_env_var_names 接收额外变量名的能力。EnvVarGuard 负责临时布置环境,create_env_for_mcp_server 负责读取并打包。

调用图:调用 2 个内部函数(set, create_env_for_mcp_server);外部调用 2 个(from, assert_eq!)。

tests::create_remote_env_overlay_only_forwards_explicit_variables214–228 ↗
fn create_remote_env_overlay_only_forwards_explicit_variables()

作用:验证远程 MCP 的环境变量补丁只转发明确点名的变量,不会顺手带上默认变量。这样可以避免把本机环境误传到远程端。

数据流:测试先临时设置一个默认变量和一个自定义变量。然后调用 create_env_overlay_for_remote_mcp_server,只把自定义变量列入配置。最后检查结果里只有自定义变量,没有默认变量。出来是一个断言结果。

调用关系:它直接保护 create_env_overlay_for_remote_mcp_server 的核心规则:远程启动时不要继承本地默认环境。EnvVarGuard 用来模拟当前进程确实有这些变量,测试再确认函数仍然只挑配置点名的那个。

调用图:调用 2 个内部函数(set, create_env_overlay_for_remote_mcp_server);外部调用 2 个(from, assert_eq!)。

tests::create_remote_env_overlay_does_not_copy_remote_source_variables232–257 ↗
fn create_remote_env_overlay_does_not_copy_remote_source_variables()

作用:验证标记为 remote 来源的变量不会从本地环境复制。remote 来源的意思是这个变量应该由远程机器自己提供。

数据流:测试临时设置一个 remote 变量和一个 local 变量。它把两个变量都放进配置,但一个标 remote,一个标 local,然后调用 create_env_overlay_for_remote_mcp_server。结果应该只包含 local 变量的本地值,不包含 remote 变量。出来是断言结果。

调用关系:它测试 create_env_overlay_for_remote_mcp_server 对变量来源的区分能力。这个行为和 remote_mcp_env_var_names 配合使用:remote 变量名另交给远程端处理,本地 overlay 不复制它的值。

调用图:调用 2 个内部函数(set, create_env_overlay_for_remote_mcp_server);外部调用 2 个(from, assert_eq!)。

tests::remote_mcp_env_var_names_returns_remote_source_names260–274 ↗
fn remote_mcp_env_var_names_returns_remote_source_names()

作用:验证 remote_mcp_env_var_names 只返回来源标为 remote 的变量名。旧格式变量和 local 变量都不应该出现在结果里。

数据流:进去的是测试构造的三个变量:旧格式 LEGACY、local 的 LOCAL、remote 的 REMOTE。测试调用 remote_mcp_env_var_names,然后检查返回列表是否只有 REMOTE。出来是断言结果。

调用关系:它直接测试 remote_mcp_env_var_names。这个测试保证远程启动流程拿到的“需要远程提供的变量清单”不会混入本地变量。

调用图:调用 1 个内部函数(remote_mcp_env_var_names);外部调用 1 个(assert_eq!)。

tests::create_local_env_rejects_remote_source_variables277–291 ↗
fn create_local_env_rejects_remote_source_variables()

作用:验证本地 MCP 环境创建会拒绝 remote 来源变量。因为本地 stdio 启动没有远程执行端,无法正确处理这类变量。

数据流:进去的是一个标记为 remote 来源的变量配置。测试调用 create_env_for_mcp_server,预期它返回错误,然后检查错误文字里是否说明需要 remote MCP stdio。出来是断言结果。

调用关系:它覆盖 create_env_for_mcp_server 和 local_stdio_env_var_names 的错误路径。local_stdio_env_var_names 发现不合适的 remote 变量后报错,create_env_for_mcp_server 把错误传回给调用方。

调用图:调用 1 个内部函数(create_env_for_mcp_server);外部调用 1 个(assert!)。

tests::create_env_preserves_path_when_it_is_not_utf8296–307 ↗
fn create_env_preserves_path_when_it_is_not_utf8()

作用:验证本地 MCP 环境创建能保留不是合法 UTF-8 文本的 PATH。UTF-8 是常见文字编码,但有些系统路径字节并不一定能当普通字符串解释。

数据流:在 Unix 系统上,测试构造一个包含非法 UTF-8 字节的 PATH,并用 tests::EnvVarGuard::set 临时设置。然后调用 create_env_for_mcp_server,最后检查生成环境表里的 PATH 是否原样保留。出来是断言结果。

调用关系:它测试 create_env_for_mcp_server 使用 OsString 这类系统原生字符串的必要性。这个测试防止代码以后改成普通 String 后,把某些真实系统路径弄坏。

调用图:调用 2 个内部函数(set, create_env_for_mcp_server);外部调用 2 个(assert_eq!, from_bytes)。

配置规范化基础

本组提供共享构建块,用于在各类接口中标记、约束、重命名、转换和覆盖配置值。

config/src/config_layer_source.rs源码 ↗
utilconfig load / diagnostics

这个文件只做一件小但很重要的事:把配置来源写成清楚的标签。配置可能来自公司设备管理 MDM(可以理解成公司统一下发设置的工具)、系统级文件、企业托管设置、用户自己的配置文件、项目里的配置、启动时临时传入的参数,或者旧版的托管配置。程序内部用的是 ConfigLayerSource 这种枚举,也就是“几种可能情况里选一种”的数据类型;但给人看时,直接显示枚举不友好。所以这里根据不同来源拼出类似 user (/path/config.toml)project (/path/.codex/config.toml) 这样的文字。它像给每个配置来源贴一张清晰的标签,方便日志、界面或诊断信息告诉用户:这条配置到底从哪里来的。

函数细节1
format_config_layer_source3–31 ↗
fn format_config_layer_source(source: &ConfigLayerSource, config_toml_file: &str) -> String

作用:把一个配置来源对象转换成简短、可读的说明文字。别人想在日志、报错信息或界面里展示“这份配置来自哪里”时,就会用它。

数据流:进去的是一个 ConfigLayerSource 配置来源,以及配置文件名 config_toml_file。函数先看来源属于哪一种:比如公司 MDM、系统文件、用户文件、项目文件或会话参数;然后把里面的域名、键名、文件路径、项目目录等信息拼进一段文字。出来的是一个 String 字符串;它不修改原来的配置来源,只生成一段给人看的说明。

调用关系:它是一个辅助格式化函数,通常在加载配置后、需要展示配置层来源时被调用。它自己不读取文件,也不决定配置优先级,只把已经拿到的来源信息交给 format! 这种字符串拼接工具,变成最终文字。

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

config/src/constraint.rs源码 ↗
configconfig load / cross-cutting

这个文件的核心是 Constrained<T>,可以把它想成一个带规则的盒子:盒子里放着一个配置值,但每次放进去新值前,都要先过检查。检查不通过,就返回 ConstraintError,旧值保持不变。它还支持“正规化”,也就是先把输入修一修再保存,比如把负数自动变成 0。这里的 Arc 是一种可共享的引用计数指针,方便多个克隆出来的盒子共用同一套检查规则。文件还定义了几种常见错误:值不在允许范围、字段为空、需求规则解析失败。整体作用是让配置系统既灵活,又不会在不该放开的地方失控。

函数细节24
ConstraintError::empty_field30–34 ↗
fn empty_field(field_name: impl Into<String>) -> Self

作用:快速造出一个“某个字段不能为空”的错误。调用者不用手写完整错误结构,只要告诉它字段名。

数据流:进去的是字段名,可以是字符串或能转成字符串的东西 → 函数把它转成真正的字符串 → 出来的是 ConstraintError::EmptyField,说明这个配置字段缺了必要内容。

调用关系:它是配置转换过程里的小工具。调用图里显示 try_from 会用它,在把外部配置转换成内部配置时,如果发现空字段,就用这个函数生成清楚的错误。

调用图:被 1 处调用(try_from);外部调用 1 个(into)。

Error::from40–42 ↗
fn from(err: ConstraintError) -> Self

作用:把本文件里的约束错误转换成标准的输入输出错误。这样外层如果只认 std::io::Error,也能接住这些配置错误。

数据流:进去的是一个 ConstraintError → 函数把它包装成 std::io::Error,错误类型标成 InvalidInput,意思是“输入不合法” → 出来的是标准 IO 错误对象。

调用关系:它是错误类型之间的适配器。别的代码在需要 IO 风格错误时,可以自动或手动走到这里,不必理解配置约束错误的内部枚举。

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

Constrained::new58–69 ↗
fn new(
        initial_value: T,
        validator: impl Fn(&T) -> ConstraintResult<()> + Send + Sync + 'static,
    ) -> ConstraintResult<Self>

作用:创建一个带检查规则的配置值。它会立刻检查初始值,保证盒子一出生就是合法状态。

数据流:进去的是初始值和一个检查函数 → 它先把检查函数放进可共享的 Arc,再用它检查初始值 → 检查通过就返回新的 Constrained,失败就返回错误,不会创建出坏对象。

调用关系:这是最常用的入口之一。配置转换、权限策略推导、测试代码都会用它来建立“只能保存合法值”的配置项;后续 setcan_set 会继续使用同一个检查规则。

调用图:被 13 处调用(try_from, constrained_add_validator_composes_with_existing_validator, constrained_can_set_allows_probe_without_setting, constrained_new_rejects_invalid_initial_value, constrained_set_rejects_invalid_value_and_leaves_previous, derive_sandbox_policy_falls_back_to_read_only_for_implicit_defaults, derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallback, test_requirements_web_search_mode_allowlist_does_not_warn_when_unset, web_search_mode_for_turn_falls_back_when_live_is_disallowed, from_constrained_resolved (+3 more));外部调用 1 个(new)。

Constrained::normalized72–85 ↗
fn normalized(
        initial_value: T,
        normalizer: impl Fn(T) -> T + Send + Sync + 'static,
    ) -> ConstraintResult<Self>

作用:创建一个会自动“修正输入”的配置值。它适合那些不想拒绝输入、而是想统一成标准格式的场景。

数据流:进去的是初始值和一个正规化函数 → 它先把初始值交给正规化函数处理,再保存处理后的结果 → 出来的是一个允许任意值、但每次保存前都会先正规化的 Constrained

调用关系:它给配置系统提供“先修再存”的路径。测试会验证初始化和设置时都会正规化,实际的 constrain_mcp_servers 也会用它来整理 MCP 服务器配置。

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

Constrained::allow_any87–93 ↗
fn allow_any(initial_value: T) -> Self

作用:创建一个不限制取值的配置盒子。适合那些暂时没有额外规则、但仍想统一使用 Constrained 包装的配置项。

数据流:进去的是初始值 → 它放入一个永远返回成功的检查函数 → 出来的是一个可以接受任何新值的 Constrained

调用关系:很多工具列表、服务器信息、会话配置相关代码会用它。这样即使当前不限制,也能和其他受限制配置走同一套读取、设置和快照流程。

调用图:被 39 处调用(list_all_tools_accepts_canonical_namespaced_tool_names, list_all_tools_adds_server_metadata_to_cached_tools, list_all_tools_applies_legacy_mcp_prefix_by_default, list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_snapshot, list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty, list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails, list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending, list_available_server_infos_uses_cache_while_client_is_pending, no_local_runtime_fails_local_stdio_but_keeps_local_http_server, shutdown_cancels_pending_tool_listing (+15 more));外部调用 1 个(new)。

Constrained::allow_only95–116 ↗
fn allow_only(only_value: T) -> Self

作用:创建一个只能等于指定值的配置盒子。它像把旋钮焊死在一个位置上,别人想改成别的值就会被拒绝。

数据流:进去的是唯一允许的值 → 函数克隆一份作为比较标准,并设置检查函数 → 以后候选值相同就通过,不同就返回 InvalidValue 错误;初始保存的就是这个唯一值。

调用关系:权限和沙箱相关流程会用它锁住某些设置,例如从会话快照恢复权限约束、构建审查会话配置、读取配置时限制允许的模式。

调用图:被 8 处调用(resolve_allowed_windows_sandbox_setup_mode_rejects_disallowed_mode, constrained_allow_only_rejects_different_values, replace_permission_profile_from_session_snapshot, permission_snapshot_setter_preserves_permission_constraints, build_guardian_review_session_config, start_review_conversation, get_config, on_session_configured_with_display_and_fork_parent_title);外部调用 2 个(new, clone)。

Constrained::allow_any_from_default119–124 ↗
fn allow_any_from_default() -> Self

作用:用类型自己的默认值创建一个不限制取值的配置盒子。适合调用者懒得手动提供初始值的情况。

数据流:进去没有具体值,但要求类型有默认值 → 它先取 T::default(),再交给 allow_any → 出来的是初始值为默认值、之后可随便设置的 Constrained

调用关系:默认配置和配置转换会用它。它把“取默认值”和“允许任意值”这两步合在一起,减少外层重复代码。

调用图:被 2 处调用(default, try_from);外部调用 2 个(allow_any, default)。

Constrained::get126–128 ↗
fn get(&self) -> &T

作用:借用当前保存的值,不把值拿走。适合读取复杂配置,比如字符串、列表、结构体。

数据流:进去的是这个 Constrained 自身 → 它直接返回内部值的引用 → 外面能看这个值,但不能通过这个引用替换它。

调用关系:MCP 配置转换、权限配置读取、工作区根目录查询等流程会用它取当前值。它让外层读取配置时仍然绕不开约束盒子的边界。

调用图:被 7 处调用(new_uninitialized, to_mcp_config_with_plugin_registrations, active_permission_profile, from_constrained_active_profile, from_constrained_legacy, permission_profile, profile_workspace_roots)。

Constrained::value130–135 ↗
fn value(&self) -> T

作用:直接拿到当前值的一个拷贝。它只适用于可以轻松复制的类型,比如数字、布尔值、小枚举。

数据流:进去的是这个 Constrained 自身 → 它把内部值复制一份 → 出来的是这个值本身,盒子里的原值不变。

调用关系:会话配置、审批策略、回合上下文等代码会用它读取简单配置。和 get 不同,它给的是值的副本,外层改副本不会影响盒子。

调用图:被 6 处调用(new, new_uninitialized_with_permission_profile, set_approval_policy, resolve_web_search_mode_for_turn, thread_config_snapshot, to_turn_context_item)。

Constrained::can_set137–139 ↗
fn can_set(&self, candidate: &T) -> ConstraintResult<()>

作用:提前问一句:“这个值能不能设置?”但不真的修改当前配置。适合做预检查。

数据流:进去的是候选值的引用 → 它只运行检查函数 → 出来是成功或错误;无论结果如何,内部保存的旧值都不变。

调用关系:例如解析某一回合的网络搜索模式、检查旧权限配置能否设置时会用它。它让上层可以先试探规则,再决定是否真的调用 set

调用图:被 2 处调用(resolve_web_search_mode_for_turn, can_set_legacy_permission_profile)。

Constrained::add_validator144–160 ↗
fn add_validator(
        &mut self,
        validator: impl Fn(&T) -> ConstraintResult<()> + Send + Sync + 'static,
    ) -> ConstraintResult<()>

作用:给已经存在的配置盒子再加一条检查规则。新规则会和旧规则叠加,必须都通过才算合法。

数据流:进去的是新的检查函数 → 它把旧检查函数和新检查函数包成一个组合检查函数 → 先检查当前值是否满足组合规则,通过才替换内部检查器;失败则不安装新规则。

调用关系:它是给约束加码的接口。虽然调用图里主要在测试中覆盖,但设计上用于运行中逐步收紧配置规则,并继续被 can_setset 使用。

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

Constrained::set162–171 ↗
fn set(&mut self, value: T) -> ConstraintResult<()>

作用:尝试把配置改成新值。它会先正规化、再检查,只有完全通过才真正替换旧值。

数据流:进去的是新值 → 如果有正规化函数,先把新值修成标准样子;然后运行检查函数 → 成功就保存新值,失败就返回错误并保留原来的值。

调用关系:权限配置更新、权限快照恢复等流程会用它真正改值。它是约束盒子的关键守门动作,保证坏值不会悄悄进入系统。

调用图:被 2 处调用(set_legacy_permission_profile, set_permission_profile_snapshot)。

Constrained::deref177–179 ↗
fn deref(&self) -> &Self::Target

作用:让 Constrained<T> 在很多场合可以像 &T 一样被读取。这样代码用起来更顺手。

数据流:进去的是 Constrained 自身的引用 → 它返回内部值的引用 → 外层可以按普通值的引用来读,但不能绕过规则替换内部值。

调用关系:这是 Rust 的解引用支持。它不主动参与某个业务流程,但会在编译器需要把 Constrained 当成内部值引用时自动派上用场。

Constrained::fmt183–187 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:定义调试打印时显示什么。它只打印当前值,不把检查函数这些内部细节打印出来。

数据流:进去的是格式化器和 Constrained → 它构造一个名为 Constrained 的调试结构,只填入 value 字段 → 出来的是格式化结果。

调用关系:这是给日志、调试、测试失败信息用的。它调用标准的调试结构构造方法,让开发者看到值,但不用看到无法直观显示的闭包规则。

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

Constrained::eq191–193 ↗
fn eq(&self, other: &Self) -> bool

作用:定义两个受约束值怎么比较是否相等。这里的规则很简单:只看里面的值,不比较检查规则。

数据流:进去的是两个 Constrained → 它取出两边内部值做相等比较 → 出来是布尔结果,表示值是否一样。

调用关系:这是相等比较的基础支持。测试或配置快照比较时,关心的是当前配置值是否相同,而不是背后的检查函数是不是同一个。

tests::invalid_value201–208 ↗
fn invalid_value(candidate: impl Into<String>, allowed: impl Into<String>) -> ConstraintError

作用:测试里用来快速造一个“值不在允许范围内”的错误。这样测试不用每次重复写完整错误结构。

数据流:进去的是候选值和允许范围的文字 → 它们被转成字符串,并填入固定的字段名和来源 → 出来的是 ConstraintError::InvalidValue

调用关系:它只在本文件测试中服务。多个测试用它来构造期望错误,再和实际错误做精确比较。

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

tests::constrained_allow_any_accepts_any_value211–217 ↗
fn constrained_allow_any_accepts_any_value()

作用:验证 allow_any 真的什么值都接受。测试用一个正数开头,再改成负数。

数据流:先创建初始值为 5 的不受限盒子 → 调用 set 改成 -10 → 最后检查盒子里的值确实变成 -10。

调用关系:它直接测试 Constrained::allow_anyset 的配合,证明“允许任意值”的检查函数不会误拦截。

调用图:调用 1 个内部函数(allow_any);外部调用 1 个(assert_eq!)。

tests::constrained_allow_any_default_uses_default_value220–223 ↗
fn constrained_allow_any_default_uses_default_value()

作用:验证 allow_any_from_default 会使用类型默认值。这里用 i32,默认值应该是 0。

数据流:先创建一个默认的、不受限的 Constrained<i32> → 读取当前值 → 检查它等于 0。

调用关系:它覆盖 Constrained::allow_any_from_default,间接确认它正确调用默认值和 allow_any

调用图:外部调用 2 个(allow_any_from_default, assert_eq!)。

tests::constrained_allow_only_rejects_different_values226–237 ↗
fn constrained_allow_only_rejects_different_values()

作用:验证 allow_only 只允许指定值,遇到不同值会拒绝,而且不会把旧值改坏。

数据流:先创建只允许 5 的盒子 → 设置 5 成功 → 再尝试设置 6,得到错误 → 最后确认盒子里仍然是 5。

调用关系:它测试 Constrained::allow_only 生成的检查规则,以及 set 在失败时保留旧值的行为。

调用图:调用 1 个内部函数(allow_only);外部调用 1 个(assert_eq!)。

tests::constrained_normalizer_applies_on_init_and_set240–249 ↗
fn constrained_normalizer_applies_on_init_and_set() -> anyhow::Result<()>

作用:验证正规化函数在创建时和设置时都会生效。例子里把小于 0 的数都抬到 0。

数据流:先用 -1 创建带正规化的盒子,保存后应变成 0 → 设置 -5 也应变成 0 → 设置 10 则保留 10 → 每一步都检查结果。

调用关系:它直接测试 Constrained::normalized,也测试后续 set 是否沿用同一个正规化函数。

调用图:调用 1 个内部函数(normalized);外部调用 1 个(assert_eq!)。

tests::constrained_add_validator_composes_with_existing_validator252–279 ↗
fn constrained_add_validator_composes_with_existing_validator() -> anyhow::Result<()>

作用:验证追加检查规则时,新旧规则会一起生效。例子里先要求不小于 0,再追加不大于 10。

数据流:先创建只接受非负数的盒子 → 再追加只接受不超过 10 的规则 → 检查 7 通过、11 失败、-1 失败。

调用关系:它测试 Constrained::newadd_validator 的组合效果,证明追加规则不是覆盖旧规则,而是叠加旧规则。

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

tests::constrained_new_rejects_invalid_initial_value282–292 ↗
fn constrained_new_rejects_invalid_initial_value()

作用:验证创建时就会检查初始值。如果初始值不合法,就不会创建出一个坏盒子。

数据流:尝试用 0 创建一个只接受正数的盒子 → 检查函数返回错误 → 测试确认 new 的结果就是这个错误。

调用关系:它直接保护 Constrained::new 的重要承诺:初始状态必须合法。配置系统依赖这个承诺,不能先放坏值再等以后修。

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

tests::constrained_set_rejects_invalid_value_and_leaves_previous295–310 ↗
fn constrained_set_rejects_invalid_value_and_leaves_previous()

作用:验证设置非法新值时,旧值不会被覆盖。这个行为对配置安全很重要。

数据流:先创建当前值为 1、只接受正数的盒子 → 尝试设置 -5,得到错误 → 再读取当前值,确认仍然是 1。

调用关系:它测试 Constrained::new 建立规则后,set 在失败路径上的保护动作。也就是检查失败时不能留下半更新状态。

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

tests::constrained_can_set_allows_probe_without_setting313–331 ↗
fn constrained_can_set_allows_probe_without_setting()

作用:验证 can_set 只是试探,不会真的改值。它可以告诉你某个候选值是否合法。

数据流:先创建当前值为 1、只接受正数的盒子 → 用 can_set 检查 2 成功,检查 -1 失败 → 最后确认当前值仍然是 1。

调用关系:它测试 Constrained::new 建好规则后,can_set 只跑检查、不调用真正保存逻辑。上层流程可以用这种方式安全预判配置变化。

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

config/src/key_aliases.rs源码 ↗
configconfig load

配置文件会随着项目发展而改名。比如这里把 memories 表里的旧配置项 no_memories_if_mcp_or_web_search,统一换成新名字 disable_on_external_context。这个文件做的事很像“通讯录改名”:有人还用旧称呼写配置,程序读进来后先帮他翻译成新称呼。它会递归地走完整个 TOML 配置结构;TOML 可以理解成一种常见的配置文件格式,里面有表、数组和值。遇到表时,它先处理表里的子内容,再检查当前表是不是需要改名的位置。如果发现旧键,就把旧键删掉,并在新键不存在时把值放到新键上。一个重要细节是:如果新旧两个键都写了,它保留新键,不让旧键覆盖新键,这样用户明确写的新配置优先。

函数细节2
normalize_key_aliases17–30 ↗
fn normalize_key_aliases(path: &[String], table: &mut TomlMap<String, TomlValue>)

作用:这个函数只处理“当前这一层配置表”的键名翻译。它检查当前位置是不是有已知的旧键,如果有,就把旧键的值搬到新键下面。

数据流:进去的是当前位置路径 path,以及一张可修改的 TOML 表 table。它拿路径去和内置的别名规则比对;如果路径匹配,并且表里有旧键,就把旧键从表里移走,再尝试放到新键上。出来时没有单独返回值,但 table 可能已经被改过:旧名字消失,新名字出现;如果新名字原来就有,旧值会被丢弃,不会覆盖新值。

调用关系:它是具体执行改名的小工具。normalized_with_key_aliases 在递归走到每个表之后会调用它;merge_toml_values_at_path 在合并配置时也会用它,确保合并过程中同样认得旧键。它自己只借助底层表操作,比如查找、删除和插入,不再把工作交给项目里的其他函数。

调用图:被 2 处调用(normalized_with_key_aliases, merge_toml_values_at_path);外部调用 2 个(entry, remove)。

normalized_with_key_aliases32–52 ↗
fn normalized_with_key_aliases(value: &TomlValue, path: &[String]) -> TomlValue

作用:这个函数会把一整份 TOML 配置复制成“键名已经规范化”的新配置。它不只是看最外层,而是会深入到所有表和数组里,确保藏在里面的旧键也能被改成新键。

数据流:进去的是一个 TOML 值 value,以及它在配置树里的当前位置 path。它先判断这个值是什么:如果是表,就逐个处理里面的子项,并为子项更新路径,然后对处理好的表调用 normalize_key_aliases;如果是数组,就对数组里的每一项做同样的规范化;如果只是普通值,比如字符串、数字或布尔值,就原样复制。出来的是一个新的 TOML 值,内容和原来等价,但已知的旧配置键会尽量变成统一的新键。

调用关系:它是整份配置改名流程的入口式工具。配置来源读取后,origins 会用它来得到统一后的配置;merge_toml_values_at_path 在合并多份配置时也会调用它。它把真正的单层改名动作交给 normalize_key_aliases,自己负责像巡检员一样走遍整棵配置树。

调用图:调用 1 个内部函数(normalize_key_aliases);被 2 处调用(merge_toml_values_at_path, origins);外部调用 4 个(new, Array, Table, clone)。

utils/json-to-toml/src/lib.rs源码 ↗
utilcross-cutting

JSON 和 TOML 都是常见的数据写法,常用来放配置。问题是它们支持的值不完全一样,比如 JSON 有 null,但 TOML 没有完全对应的空值。这个文件的核心函数 json_to_toml 就像一个“翻译员”:看到 JSON 的布尔值,就翻成 TOML 的布尔值;看到整数或小数,就翻成 TOML 的整数或浮点数;看到数组,就把数组里的每一项也逐个翻译;看到对象,就翻成 TOML 的表,也就是一组键和值。比较特别的是 JSON 的 null 会被翻成空字符串,因为 TOML 没有直接的 null。文件后半部分是测试,用几个典型例子确认翻译没有跑偏,比如数字、数组、布尔值、小数、空值和嵌套对象。

函数细节7
json_to_toml5–28 ↗
fn json_to_toml(v: JsonValue) -> TomlValue

作用:把一个 serde_json::Value,也就是 Rust 里表示 JSON 值的通用容器,转换成 toml::Value,也就是 Rust 里表示 TOML 值的通用容器。别人需要把 JSON 配置改写成 TOML 配置时,会用它来做核心转换。

数据流:进去的是一个 JSON 值。函数先看它是哪一类:空值、真假值、数字、文字、数组,还是对象;然后按类型换成最接近的 TOML 值。数组和对象会继续把里面的每个小值递归转换,最后出来一个完整的 TOML 值;原来的输入值会被消耗掉,不再保留。

调用关系:这是这个文件真正给外部使用的主函数。测试函数会用各种 JSON 示例调用它,检查它是否能正确产出 TOML 的 String、Boolean、Integer、Float、Array 和 Table 等值。它自己不依赖项目里的其他函数,只使用 serde_json 和 toml 这些库提供的值类型。

调用图:外部调用 7 个(new, Array, Boolean, Float, Integer, String, Table)。

tests::json_number_to_toml37–40 ↗
fn json_number_to_toml()

作用:检查普通 JSON 整数能不能被翻成 TOML 整数。这个测试防止以后改代码时,把整数误翻成小数或字符串。

数据流:进去的是测试里临时造出的 JSON 数字 123。测试把它交给 json_to_toml,再把返回结果和期望的 TOML 整数 123 做比较;如果不一样,测试就失败。

调用关系:这是围绕 json_to_toml 的单项检查。它用 json! 这个测试辅助宏造 JSON 值,再用 assert_eq! 比较结果,帮助确认主转换函数在数字场景下可靠。

调用图:外部调用 2 个(assert_eq!, json!)。

tests::json_array_to_toml43–49 ↗
fn json_array_to_toml()

作用:检查 JSON 数组能不能被翻成 TOML 数组,并且数组里的每一项也被正确翻译。它证明转换不是只看外层,还会处理里面的内容。

数据流:进去的是测试创建的 JSON 数组 [true, 1]。测试调用 json_to_toml 后,期望得到一个 TOML 数组,里面依次是 TOML 布尔值 true 和 TOML 整数 1;比较通过就说明数组递归转换正常。

调用关系:这个测试专门覆盖 json_to_toml 处理数组的分支。它间接验证主函数会把数组元素继续交回 json_to_toml 转换,而不是原样塞进去。

调用图:外部调用 2 个(assert_eq!, json!)。

tests::json_bool_to_toml52–55 ↗
fn json_bool_to_toml()

作用:检查 JSON 的真假值能不能正确变成 TOML 的真假值。这样可以保证 true 或 false 这类配置开关不会在转换后变样。

数据流:进去的是测试创建的 JSON false。它被传给 json_to_toml,函数返回的结果再和 TOML 的 Boolean(false) 比较;一致就通过。

调用关系:这是 json_to_toml 的布尔值分支测试。它用 json! 构造输入,用 assert_eq! 判断主函数的输出是否符合预期。

调用图:外部调用 2 个(assert_eq!, json!)。

tests::json_float_to_toml58–61 ↗
fn json_float_to_toml()

作用:检查 JSON 小数能不能被翻成 TOML 小数。它避免像 1.25 这样的值在转换时被误当成整数或文字。

数据流:进去的是测试里的 JSON 小数 1.25。测试调用 json_to_toml,拿到结果后和 TOML 的 Float(1.25) 比较;如果两边一致,说明小数转换正确。

调用关系:这是 json_to_toml 数字处理逻辑中的小数场景测试。它和整数测试一起,确认数字会按实际形态分成整数或浮点数。

调用图:外部调用 2 个(assert_eq!, json!)。

tests::json_null_to_toml64–67 ↗
fn json_null_to_toml()

作用:检查 JSON 的 null 会按约定变成 TOML 的空字符串。这个测试很重要,因为 TOML 没有直接对应 null 的值,必须明确选择一种替代方式。

数据流:进去的是 JSON 的 Null。测试把它传给 json_to_toml,期望出来的是 TOML 字符串,而且内容为空;如果不是空字符串,测试就会失败。

调用关系:这是 json_to_toml 里最容易让人意外的规则的保护测试。它说明这个工具的设计选择是:遇到 JSON null,不报错,也不丢掉,而是变成空字符串。

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

tests::json_object_nested70–82 ↗
fn json_object_nested()

作用:检查带嵌套结构的 JSON 对象能不能变成嵌套的 TOML 表。它确保复杂配置里的层级关系不会在转换时丢失。

数据流:进去的是一个 JSON 对象,外层键是 outer,里面还有 inner: 2。测试手工搭出期望的 TOML 表结构:outer 对应一个内层表,内层表里 inner 对应整数 2;然后和 json_to_toml 的输出比较。

调用关系:这是 json_to_toml 对象分支和递归能力的综合测试。它既用 json! 造嵌套 JSON,也用 TOML 表的 new 和插入操作拼出期望结果,最后用 assert_eq! 确认主函数能保住对象的层级。

调用图:外部调用 5 个(Integer, Table, assert_eq!, json!, new)。

utils/cli/src/config_override.rs源码 ↗
configconfig load / command-line parsing

这个文件解决的是“配置临时改一下”的问题。平时程序会从配置文件读设置,但用户有时只想这次运行换个模型、开个开关,或者改一个很深的配置项。这里定义了 CliConfigOverrides,让命令行可以反复接收 -c key=value。它先把用户输入原样收下来,再把等号左边当成配置路径,比如 foo.bar 表示嵌套配置;等号右边会尽量按 TOML 解析。TOML 是一种配置文件格式,所以 true42、数组、表格都能变成真正的布尔值、数字或列表。如果解析不了,就当普通字符串用。最后它还能把这些改动写进一棵 TOML 配置树里,必要时自动创建中间层,就像给柜子里不存在的抽屉先装上,再把东西放进去。它还保留了一个旧名字 use_legacy_landlock 的兼容写法,避免老用户的命令突然失效。

函数细节13
CliConfigOverrides::prepend_root_overrides42–45 ↗
fn prepend_root_overrides(&mut self, root_overrides: Self)

作用:把顶层命令收到的 -c 配置改动放到当前列表最前面。这样后面子命令自己的配置改动可以排在后面,通常也就更有机会覆盖前面的值。

数据流:输入是当前这组配置改动,以及另一组来自根命令的配置改动。它把根命令那组原始字符串插到当前列表开头。结果是同一个 CliConfigOverrides 被改写,里面的顺序变成“根命令的在前,当前命令的在后”。

调用关系:它会被 prepend_config_flags 使用,通常发生在命令行参数已经分成根命令和子命令之后。它不解析配置,只调整顺序,为后面的解析和应用配置打基础。

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

CliConfigOverrides::parse_overrides49–84 ↗
fn parse_overrides(&self) -> Result<Vec<(String, Value)>, String>

作用:把用户在命令行写的 -c key=value 字符串变成程序能理解的“路径和值”。它会检查格式,识别数字、布尔值、数组等 TOML 值,解析不了时再退回成普通文字。

数据流:进去的是 raw_overrides 里一串原始命令行文本。它逐条只按第一个等号切开,左边清理成配置路径,右边先尝试按 TOML 值解析;如果失败,就去掉外层引号后当字符串。它还会把旧配置名规范成新路径。出来的是一个列表,每项都是“配置路径 → TOML 值”;如果缺少等号或键为空,就返回错误信息。

调用关系:这是很多命令真正读取 -c 参数时会走的入口,例如 run_main_with_transport_optionsrun_command_under_sandboxload_configload_exec_server_configload_config_or_exitrun_addrun_getrun_list 等。它位于命令行输入和配置系统之间,负责把人写的文字翻译成配置系统能用的数据。

调用图:被 17 处调用(run_main_with_transport_options, run_command_under_sandbox, load_config, load_exec_server_config, load_config_or_exit, run_add, run_get, run_list, run_login, run_logout (+7 more))。

CliConfigOverrides::apply_on_value89–95 ↗
fn apply_on_value(&self, target: &mut Value) -> Result<(), String>

作用:把所有命令行里的配置覆盖项真正写到一棵配置数据里。调用它之后,目标配置会被这些 -c 参数修改。

数据流:输入是当前保存的原始覆盖项,以及一个可修改的 TOML 配置值 target。它先调用 parse_overrides 得到路径和值,再把每一项交给 apply_single_override 写入目标配置。成功时没有额外返回内容,只是 target 已经被改好;如果解析阶段出错,就返回错误文字。

调用关系:它是更高层配置加载流程的方便接口:上层不必自己循环处理每个 -c。它先把解析工作交给 parse_overrides,再把单项写入工作交给 apply_single_override

调用图:调用 2 个内部函数(parse_overrides, apply_single_override)。

canonicalize_override_key98–104 ↗
fn canonicalize_override_key(key: &str) -> String

作用:把某些旧的配置名字改成现在真正使用的配置路径。这样老命令还能继续用,不会因为配置结构调整就坏掉。

数据流:输入是一个配置键名字符串。它检查这个键是不是旧名字 use_legacy_landlock;如果是,就输出 features.use_legacy_landlock,否则原样输出。它不改别的数据。

调用关系:它服务于解析覆盖项的过程,通常在 parse_overrides 把用户输入变成标准路径时使用。它像一个小小的地址转发表,把旧地址指到新地址。

apply_single_override108–148 ↗
fn apply_single_override(root: &mut Value, path: &str, value: Value)

作用:把一个具体的配置改动写进配置树里的指定位置。路径中间缺了哪一层,它会自动补出来。

数据流:输入是一棵可修改的 TOML 配置树、一个点号分隔的路径,比如 a.b.c,以及要写入的值。它从根开始一层层往下找;如果当前层不是表格,或下一层不存在,就创建新的表格。走到最后一段路径时,把旧值替换成新值。输出没有单独返回值,但配置树已经被改动。

调用关系:它只处理“一条”覆盖规则,被 CliConfigOverrides::apply_on_value 在循环里调用。它依赖 TOML 表格这种结构来创建中间节点,就像按地址把包裹放到正确房间。

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

parse_toml_value150–157 ↗
fn parse_toml_value(raw: &str) -> Result<Value, toml::de::Error>

作用:把一小段文字当成 TOML 值来解析。它让命令行里的 true42[1, 2] 这类文字变成真正的布尔值、数字或数组。

数据流:输入是一段原始文字。因为 TOML 解析器通常需要 键 = 值 这种完整写法,它会临时包成 _x_ = 原始文字,交给 TOML 解析器读取,再取出 _x_ 对应的值。成功时输出 TOML 值;解析失败或没取到值时输出解析错误。

调用关系:它是命令行配置值的底层翻译器,会被解析逻辑使用,也被多个测试函数直接调用来确认数字、布尔值、数组和内联表格都能正确识别。

调用图:被 4 处调用(parses_array, parses_basic_scalar, parses_bool, parses_inline_table);外部调用 2 个(format!, from_str)。

tests::parses_basic_scalar164–167 ↗
fn parses_basic_scalar()

作用:测试普通数字能不能被当成 TOML 值正确解析。这里确认字符串 42 会变成数字 42。

数据流:输入是测试里写死的文字 42。它调用 parse_toml_value 解析,然后检查结果的整数值是不是 42。通过测试表示数字解析正常;失败则说明解析行为出问题。

调用关系:它由测试运行器执行,用来保护 parse_toml_value 的基础能力。它直接验证最常见的标量值,也就是单个简单值。

调用图:调用 1 个内部函数(parse_toml_value);外部调用 1 个(assert_eq!)。

tests::parses_bool170–176 ↗
fn parses_bool()

作用:测试布尔值,也就是“真/假”开关,能不能从命令行文字正确变成程序里的布尔值。

数据流:输入是测试里写死的 truefalse。它分别调用 parse_toml_value,再检查解析结果是不是对应的真和假。输出是测试通过或失败,不产生业务数据。

调用关系:它由测试运行器执行,专门盯住配置开关这类常见写法。它保障用户写 -c some_flag=true 时不会被当成普通字符串。

调用图:调用 1 个内部函数(parse_toml_value);外部调用 1 个(assert_eq!)。

tests::fails_on_unquoted_string179–181 ↗
fn fails_on_unquoted_string()

作用:测试未加引号的普通单词不会被 TOML 解析器误认为合法 TOML 字符串。比如 hello 按 TOML 规则不是字符串,字符串通常要写成 "hello"

数据流:输入是测试文字 hello。它检查直接按 TOML 解析会失败。结果是测试通过或失败,用来确认解析器遵守 TOML 规则。

调用关系:它由测试运行器执行,验证 parse_toml_value 的失败场景。这个失败很重要,因为外层解析逻辑会在失败后把内容当普通字符串处理。

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

tests::parses_array184–188 ↗
fn parses_array()

作用:测试数组写法能否被正确解析。数组就是一组值,比如 [1, 2, 3]

数据流:输入是测试文字 [1, 2, 3]。它调用 parse_toml_value,确认结果是数组,并检查数组长度是 3。输出是测试是否通过。

调用关系:它由测试运行器执行,保护命令行覆盖复杂配置的能力。这样用户可以用 -c list=[1,2,3] 这类写法传入列表。

调用图:调用 1 个内部函数(parse_toml_value);外部调用 1 个(assert_eq!)。

tests::canonicalizes_use_legacy_landlock_alias191–198 ↗
fn canonicalizes_use_legacy_landlock_alias()

作用:测试旧配置名 use_legacy_landlock 会不会被自动改成新路径 features.use_legacy_landlock。这是为了兼容老用户的命令。

数据流:输入是一个只包含 use_legacy_landlock=trueCliConfigOverrides。它调用 parse_overrides,然后检查解析出的路径是不是新路径,值是不是布尔值 true。输出是测试通过或失败。

调用关系:它由测试运行器执行,覆盖兼容旧配置名这一行为。它间接验证了解析覆盖项时的键名规范化逻辑。

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

tests::prepends_root_overrides201–216 ↗
fn prepends_root_overrides()

作用:测试根命令的配置改动是否会被放到子命令配置改动前面。这样顺序规则不会悄悄变错。

数据流:输入是两组配置:子命令里有一个模型值,根命令里也有一个模型值。它调用 prepend_root_overrides 后,检查列表顺序是不是根命令的值在前、子命令的值在后。输出是测试是否通过。

调用关系:它由测试运行器执行,专门保护 CliConfigOverrides::prepend_root_overrides 的顺序行为。这个顺序会影响之后同名配置谁覆盖谁。

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

tests::parses_inline_table219–224 ↗
fn parses_inline_table()

作用:测试 TOML 的内联表格写法能不能被解析。内联表格就是在一行里写一个小对象,比如 {a = 1, b = 2}

数据流:输入是测试文字 {a = 1, b = 2}。它调用 parse_toml_value,确认结果是一张表,并检查里面的 ab 分别是 1 和 2。输出是测试通过或失败。

调用关系:它由测试运行器执行,保护较复杂配置值的解析能力。这样用户可以在命令行里一次传入一个小配置块。

调用图:调用 1 个内部函数(parse_toml_value);外部调用 1 个(assert_eq!)。

utils/cli/src/approval_mode_cli_arg.rs源码 ↗
configconfig load

这个文件解决的是“用户在命令行里怎么指定是否需要批准执行命令”的问题。比如模型想运行一个命令,有些情况下应该先问用户,有些情况下可以直接跑。文件里定义了 ApprovalModeCliArg 这个枚举(枚举就是一组选项),包括 untrustedon-failureon-requestnever。它还用 clapValueEnum 让这些选项能被命令行解析器自动识别,并规定写法是短横线风格,比如 on-request。最后,它提供了一个转换,把命令行层的选项变成协议层的 AskForApproval。可以把它想成前台菜单和后台开关之间的翻译表:用户点的是菜单项,程序真正用的是内部开关。

函数细节1
AskForApproval::from30–37 ↗
fn from(value: ApprovalModeCliArg) -> Self

作用:这个函数把命令行里读到的审批模式,转换成系统内部实际执行时使用的审批规则。有人会在解析完 --approval-mode 后用它,确保后面的执行流程拿到的是统一格式。

数据流:进去的是一个 ApprovalModeCliArg,也就是用户在命令行选择的模式。函数根据它是哪一个选项,逐一对应到 AskForApproval 里的内部模式:比如 Untrusted 变成 UnlessTrustedNever 变成 Never。出来的是一个内部审批策略;它不修改别的东西,只做这次翻译。

调用关系:它处在命令行配置进入核心逻辑的交界处:前面通常是命令行解析器把文字参数变成 ApprovalModeCliArg,这里再把它交给协议层能理解的 AskForApproval。后续真正决定命令执行前要不要问用户的流程,会使用这个转换后的结果。

utils/cli/src/sandbox_mode_cli_arg.rs源码 ↗
configstartup / config load

这个文件解决的是“用户在命令行输入的沙盒模式,程序内部怎么准确理解”的问题。沙盒可以理解成给程序划出来的一块活动范围:只读、允许写工作区、或者危险的完全访问。这里的 SandboxModeCliArg 是专门给命令行解析器 clap 用的枚举,也就是一组选项清单。它故意只保留三个简单名字,不带复杂参数,这样用户可以直接写 --sandbox read-only 这类命令。真正运行时用的是协议层的 SandboxMode,所以文件里还提供了一个转换:把命令行选到的值一一映射成内部模式。最后的测试会检查这三个映射没有接错,防止以后改代码时把权限含义弄反,尤其是“完全访问”这种高风险选项。

函数细节2
SandboxMode::from21–27 ↗
fn from(value: SandboxModeCliArg) -> Self

作用:把用户在命令行里选择的沙盒模式,转换成程序内部统一使用的沙盒模式。这样后面的代码不用关心这个值最初是从命令行来的,还是从配置文件来的。

数据流:进去的是一个 SandboxModeCliArg,也就是命令行解析出来的三个简单选项之一;函数根据它是哪一种,选择对应的内部 SandboxMode;出来的是内部配置能直接使用的沙盒模式,不改动其他数据。

调用关系:它通常在命令行参数解析之后被用到,是命令行世界和内部配置世界之间的翻译员。测试函数 tests::maps_cli_args_to_protocol_modes 会专门验证它的三个翻译结果都正确。

tests::maps_cli_args_to_protocol_modes36–46 ↗
fn maps_cli_args_to_protocol_modes()

作用:检查命令行沙盒选项到内部沙盒模式的转换是否一一对应。它的重点是防止权限模式被错误映射,比如把只读误变成完全访问。

数据流:进去的是代码里写死的三个命令行选项;测试把它们分别转换成内部 SandboxMode,再用断言比较结果是不是预期值;出来的是测试通过或失败的结果,不会影响正式运行的数据。

调用关系:它只在运行测试时执行,用来守住 SandboxMode::from 这个转换函数的正确性。它调用外部的 assert_eq! 断言工具,把实际转换结果和期望结果逐个对比。

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

元数据和 schema 整形

这些文件定义小型可复用适配器,用于连接器元数据、插件和技能命名状态、提及语法,以及生成的 JSON schema。

connectors/src/metadata.rs源码 ↗
utilcross-cutting

这里的“连接器”可以理解成系统能接上的外部应用或插件,比如某个服务的入口。这个文件不做复杂业务,主要是把连接器的原始信息整理成统一格式:显示给用户看的名字、在消息里提到连接器时用的短标识、安装链接,以及更安全的内部名字。它还提供一个排序规则:先把用户能访问的连接器放前面,再按名字排序,名字相同再按编号排序。这样做的好处是,界面、搜索、提及和安装流程都能用同一套规则,不会出现同一个连接器在不同地方叫法不一样、顺序乱跳的问题。它像一张“命名和排队规则表”,让别的模块不用各自发明规则。

函数细节6
connector_display_label3–5 ↗
fn connector_display_label(connector: &AppInfo) -> String

作用:拿到一个连接器要显示给用户看的名字。简单说,就是从连接器资料里取出它的 name 字段。

数据流:输入是一个 AppInfo,也就是一份连接器资料;函数读取其中的 name,把它复制成一个新的字符串;输出就是这个显示名称,不会改动原来的连接器资料。

调用关系:它是其他流程取“人类可读名字”的统一入口。connector_mention_slug 会先用它拿名字再生成提及标识;mention_items 和 connectors_popup_params 也会在准备界面内容时用它显示名称。

调用图:被 3 处调用(connector_mention_slug, mention_items, connectors_popup_params)。

connector_mention_slug7–9 ↗
fn connector_mention_slug(connector: &AppInfo) -> String

作用:把一个连接器变成可以在消息里提到它的短标识。比如用户输入某种 @ 提及时,系统需要一个稳定、容易匹配的名字。

数据流:输入是一份 AppInfo;它先通过 connector_display_label 取出显示名称,再把这个名称交给 connector_mention_slug_from_name 做成规范化短标识;输出是这个短标识字符串,原始资料不变。

调用关系:它连接了“连接器资料”和“消息提及匹配”两边。构建提及列表、统计同名短标识、从技能项里找显式应用编号、提交用户消息时识别提及、查找应用提及时,都会用它得到统一的提及写法。

调用图:调用 2 个内部函数(connector_display_label, connector_mention_slug_from_name);被 5 处调用(build_connector_slug_counts, collect_explicit_app_ids_from_skill_items, mention_items, submit_user_message_with_history_and_shell_escape_policy, find_app_mentions)。

connector_mention_slug_from_name11–13 ↗
fn connector_mention_slug_from_name(name: &str) -> String

作用:把一个普通名字转换成连接器提及时使用的规范短名。它让不同地方对同一个名字的处理结果保持一致。

数据流:输入是一个名字字符串;它把名字交给 crate::connector_name_slug 这个外部统一规则来清理和转换;输出是转换后的短名,不会保存状态,也不会改原字符串。

调用关系:它被 connector_mention_slug 调用,专门负责“从名字到短标识”这一步。真正的转换规则交给 crate::connector_name_slug,这样本文件不用重复实现命名规则。

调用图:被 1 处调用(connector_mention_slug);外部调用 1 个(connector_name_slug)。

connector_install_url15–17 ↗
fn connector_install_url(name: &str, connector_id: &str) -> String

作用:根据连接器名字和编号生成安装链接。别的地方要引导用户安装连接器时,会用这个函数拿到正确的网址。

数据流:输入是连接器的 name 和 connector_id;它把这两个值交给 crate::connector_install_url 这个统一生成链接的函数;输出是安装 URL 字符串,不改动任何输入。

调用关系:它是安装链接的薄封装,保证合并连接器、按名字找应用、插件连接器转应用资料等流程都用同一套链接生成方式。merged_app、named_app、merge_connectors 和 plugin_connector_to_app_info 会在整理应用信息时调用它。

调用图:被 4 处调用(merged_app, named_app, merge_connectors, plugin_connector_to_app_info);外部调用 1 个(connector_install_url)。

sanitize_name19–21 ↗
fn sanitize_name(name: &str) -> String

作用:把连接器名字变成更适合内部使用的安全名字。它会先做成短标识,再把短横线换成下划线。

数据流:输入是一个名字字符串;函数先调用 crate::connector_name_slug 得到规范短名,然后把里面的 '-' 替换成 '_';输出是清理后的字符串,不修改原输入。

调用关系:它复用统一的名字规范化规则,但额外把短横线换成下划线,适合某些不方便使用短横线的场景。当前调用图里没有显示谁调用它,但它作为工具函数保留给需要安全内部名称的代码使用。

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

sort_connectors_by_accessibility_and_name23–31 ↗
fn sort_connectors_by_accessibility_and_name(connectors: &mut [AppInfo])

作用:把连接器列表排成更适合展示和选择的顺序:能访问的排前面,然后按名字和编号稳定排序。

数据流:输入是一段可修改的 AppInfo 列表;函数直接在原列表上排序,先比较 is_accessible,让可访问的在前,再比较 name,最后比较 id;输出没有单独返回值,但传入的列表顺序会被改好。

调用关系:它在连接器合并完成后发挥作用,像给货架重新排货。merge_connectors 和 merge_plugin_connectors 会调用它,让最终拿给界面或后续流程的连接器列表顺序一致、可预测。

调用图:被 2 处调用(merge_connectors, merge_plugin_connectors);外部调用 1 个(sort_by)。

core-plugins/src/toggles.rs源码 ↗
domain_logicconfig write handling

插件配置可能用几种不同写法改动:有人只改 plugins.xxx.enabled,有人一次写整个 plugins.xxx,也有人一次替换整个 plugins 表。这个文件把这些写法统一看懂,并抽出一个简单结果:插件编号对应 true 或 false。这里的 JSON 指的是一种常见的数据格式,像一棵由对象、数组、字符串、真假值组成的树。函数会逐条查看配置改动的路径和值,只认 plugins 下面的 enabled 布尔值(布尔值就是 true/false),其他内容会跳过。结果放进 BTreeMap,可以理解成一张按名字排好序的表。一个重要行为是:同一个插件如果被写了多次,后面的写法会覆盖前面的写法,就像清单上最后一次登记才算数。文件里的两个测试用例就是在确认这些规则不会被改坏。

函数细节3
collect_plugin_enabled_candidates4–43 ↗
fn collect_plugin_enabled_candidates(
    edits: impl Iterator<Item = (&'a String, &'a JsonValue)>,
) -> BTreeMap<String, bool>

作用:这个函数把一批配置编辑记录翻译成“插件 ID → 是否启用”的清单。别人会在写配置时调用它,用来提前知道哪些插件的开关状态可能需要更新。

数据流:进去的是一串配置改动,每条都有一个路径字符串和一个 JSON 值。它把路径按点号拆开,识别三种情况:直接写 plugins.某插件.enabled、写整个 plugins.某插件、或者写整个 plugins 表;只要里面能找到 true/false 形式的 enabled,就记到一张有序表里。出来的是 BTreeMap<String, bool>,也就是插件名到开关状态的表;如果同一个插件出现多次,后写入的状态会覆盖前面的状态。

调用关系:它是这个文件的核心工具。配置写入流程里的 batch_write_innerwrite_value 会用它来判断插件开关变化;测试函数也直接调用它来验证各种写法都能被识别。它内部会创建一张新的有序表来收集结果,不再把工作交给项目里的其他函数。

调用图:被 4 处调用(batch_write_inner, write_value, collect_plugin_enabled_candidates_tracks_direct_and_table_writes, collect_plugin_enabled_candidates_uses_last_write_for_same_plugin);外部调用 1 个(new)。

tests::collect_plugin_enabled_candidates_tracks_direct_and_table_writes53–80 ↗
fn collect_plugin_enabled_candidates_tracks_direct_and_table_writes()

作用:这个测试确认函数能同时看懂三种常见配置写法:直接改 enabled、改某个插件对象、改整个 plugins 表。它保证系统不会因为用户写法不同就漏掉插件开关变化。

数据流:进去的是测试临时造出的三条 JSON 配置改动:一个插件设为 true,一个插件对象里 enabled 为 false,一个完整 plugins 表里包含一个有效插件和一个没有 enabled 的插件。测试调用 collect_plugin_enabled_candidates 后,拿结果和预期的三项清单比较。出来没有业务结果;如果结果不一致,测试就失败,提醒开发者这块逻辑被破坏了。

调用关系:它只在测试运行时活跃。它把样例数据交给 collect_plugin_enabled_candidates,再用断言工具检查输出是否正确,作用像一张验收单,证明核心函数能处理直接写入和整表写入。

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

tests::collect_plugin_enabled_candidates_uses_last_write_for_same_plugin83–99 ↗
fn collect_plugin_enabled_candidates_uses_last_write_for_same_plugin()

作用:这个测试确认同一个插件被连续写入多次时,最后一次写入才算数。它防止系统误用旧状态,导致插件实际开关和用户最后的设置不一致。

数据流:进去的是两条针对同一个插件的配置改动:先把它设为 true,随后又通过插件对象把它设为 false。测试调用 collect_plugin_enabled_candidates,然后检查最终结果里这个插件是 false。出来没有普通返回值;如果最终不是 false,测试就会失败。

调用关系:它在测试阶段调用 collect_plugin_enabled_candidates,专门覆盖“重复写同一个插件”的场景。它和另一个测试一起保护这个文件的核心规则:不同写法要能识别,同名插件要以后来的设置为准。

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

core-skills/src/mention_counts.rs源码 ↗
domain_logiccross-cutting

这个文件解决的是“技能名字会不会撞名”的问题。系统里可能有很多技能,每个技能都有名字和对应的 skills.md 文件路径。有些路径被禁用了,就不该参与统计,否则会把已经不用的技能也算进去,导致误判。这里的做法很直接:逐个查看技能,先看它的文件路径是不是在禁用名单里;如果在,就跳过;如果不在,就给这个技能名计数。它同时维护两份账本:一份按原样名字统计,比如 “Writer” 和 “writer” 会分开;另一份把名字转成 ASCII 小写后统计,比如这两个会算成同一个。可以把它想成登记处有两本花名册,一本严格照身份证写法登记,一本统一小写方便查重。这样后面的代码既能知道精确重名,也能知道大小写差异造成的潜在冲突。

函数细节1
build_skill_name_counts8–24 ↗
fn build_skill_name_counts(
    skills: &[SkillMetadata],
    disabled_paths: &HashSet<AbsolutePathBuf>,
) -> (HashMap<String, usize>, HashMap<String, usize>)

作用:这个函数统计可用技能的名字出现次数,并分别给出“原样名字”和“统一小写名字”的统计结果。别人会用它来发现技能名重复,或者发现只是大小写不同的名字冲突。

数据流:输入是一组技能信息和一组被禁用的文件路径。函数先建两张空表,然后逐个看技能:如果这个技能的 skills.md 路径在禁用名单里,就不算它;否则把它的原始名字加到第一张表里,再把它转成 ASCII 小写后加到第二张表里。最后输出两张计数表;它不会改动输入的技能列表或禁用名单,只返回新算出来的结果。

调用关系:它是一个独立的小统计步骤,通常会在系统需要检查或展示技能名之前被调用。函数内部只做简单的数据整理,会创建新的哈希表(像按名字快速查账的字典),不把工作再交给项目里的其他复杂模块。

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

ext/memories/src/schema.rs源码 ↗
utilcross-cutting

这个文件解决的是“程序内部类型怎么告诉外面的人或机器”的问题。Rust 类型本身外部系统看不懂,所以这里借助 schemars 生成 JSON Schema(JSON 数据格式的结构说明)。它先按 2019-09 版本的规范生成完整 schema,再把它转成普通 JSON。然后它不会把所有生成内容都原样吐出去,而是只保留工具真正关心的几块,比如 properties(字段列表)、required(必填字段)、type(类型)、additionalProperties(是否允许多余字段)以及定义区。可以把它想成把一份很厚的产品说明书裁剪成接口调用时需要看的那几页。输入 schema 和输出 schema 的区别主要在于:输出时允许把可选值明确写成 null,而输入时不额外加这个 null 类型。

函数细节3
input_schema_for6–8 ↗
fn input_schema_for() -> Value

作用:这个函数为某个 Rust 类型生成“输入参数”的 JSON Schema。别人要知道调用工具时该传什么 JSON,就会用到它。

数据流:进去的是一个实现了 JsonSchema 的类型 T,也就是这个类型知道怎样描述自己的 JSON 形状。函数把“不要额外把可选值标成 null”这个设置交给 schema_for。出来的是一份 serde_json::Value,也就是普通 JSON 形式的 schema;它不直接改动外部状态。

调用关系:它是给外部调用的简化入口,专门用于输入数据。真正生成和裁剪 schema 的活儿交给 schema_for 来做,它只是决定使用“输入用”的配置。

output_schema_for10–12 ↗
fn output_schema_for() -> Value

作用:这个函数为某个 Rust 类型生成“输出结果”的 JSON Schema。别人要知道工具会返回什么 JSON,就会用到它。

数据流:进去的是一个实现了 JsonSchema 的类型 T。函数把“可选值可以表示成 null”这个设置传给 schema_for。出来的是一份 JSON 格式的 schema,用来描述返回值结构;它不保存状态,也不写文件。

调用关系:它是输出数据的便捷入口。和 input_schema_for 一样,它不自己做复杂工作,而是把具体生成过程交给 schema_for,只是换了一个更适合输出的选项。

schema_for14–42 ↗
fn schema_for(option_add_null_type: bool) -> Value

作用:这个函数是真正干活的地方:它根据 Rust 类型生成 JSON Schema,并把生成结果裁剪成工具接口需要的精简版本。

数据流:进去的是类型 T,以及一个布尔开关 option_add_null_type,表示可选字段是否要在 schema 里明确允许 null。它先创建 draft2019_09 版本的 schema 生成器,设置内联子结构,并应用这个 null 开关;然后生成 T 的根 schema,再转成 JSON 值。接着它确认根部确实是 JSON 对象,并只挑出 properties、required、type、additionalProperties、$defs、definitions 这些关键字段。最后出来的是一份更小、更适合给工具使用的 JSON 对象。

调用关系:它被 input_schema_for 和 output_schema_for 调用,是两个公开入口背后的共同核心。它自己会调用外部库来创建 schema 设置、生成 schema、把结果序列化成 JSON;如果生成结果不是预期的对象,它会触发不可达错误,因为正常情况下根 schema 必须是对象。

调用图:外部调用 5 个(new, draft2019_09, Object, to_value, unreachable!)。

utils/plugins/src/mention_syntax.rs源码 ↗
configcross-cutting

这个文件像一本很小的“标点规则表”。在这套项目里,用户可能会在普通文字中提到某个工具或插件,比如用某个特殊符号开头来表示“这不是普通文字,而是在点名一个工具或插件”。这里定义了两个固定符号:工具默认用 $,插件在终端界面之外的链接文本里用 @。把这些符号集中放在一个文件里,好处是其他代码不用各自硬写一遍;以后如果规则变了,只改这里就行。它没有函数,也不做解析,只提供两个常量,也就是不会变化的固定值。可以把它理解成全项目共用的一张小标签纸,大家都照着它贴标签。

网络策略和代理配置

本组涵盖错误模型、主机和域名规范化规则,以及用于验证和应用这些规则的更高层代理配置。

execpolicy/src/error.rs源码 ↗
data_modelcross-cutting

这个文件像一套“错误说明书”。策略规则、匹配模式、示例、Starlark 脚本都可能写错;如果每处代码都随便返回一段字符串,用户就很难定位问题。这里先定义了 Result<T>,意思是“要么成功拿到 T,要么拿到本文件里的 Error”。TextPosition、TextRange、ErrorLocation 用来描述错误位置:文件路径、开始行列、结束行列。Error 枚举则列出几类具体错误,比如无效规则、示例没有匹配到任何规则、示例不该匹配却匹配了规则,以及来自 Starlark 的错误。比较重要的是,它不只保存错误文字,还能给某些错误补上位置;如果错误来自 Starlark,它还会把 Starlark 自带的位置换成本项目统一使用的位置格式。这样后面展示错误时,就能像编辑器报错一样告诉用户“哪个文件的哪一段有问题”。

函数细节2
Error::with_location54–76 ↗
fn with_location(self, location: ErrorLocation) -> Self

作用:给某些还没有位置信息的错误补上“发生在文件哪里”。它主要用于检查规则或示例时,发现问题后再把当前位置贴到错误上,方便之后报给用户。

数据流:进去的是一个 Error 和一个 ErrorLocation。函数先看这个错误是不是“示例没有匹配到规则”或“示例不该匹配却匹配了规则”,并且当前位置还是空的;如果是,就把传入的位置放进去。出来的是一个新的 Error;如果原错误不适合加位置,或者已经有位置了,就原样返回,不乱改。

调用关系:它通常处在“发现错误”和“把错误交给上层显示”之间,像给包裹贴地址标签。调用者先构造出错误,再用它补位置信息;它自己不继续调用别的项目函数,只负责安全地把位置信息塞到合适的错误里。

Error::location78–100 ↗
fn location(&self) -> Option<ErrorLocation>

作用:从一个错误里取出“它发生在哪里”。如果错误本身带位置,就直接拿出来;如果是 Starlark 错误,就把 Starlark 的位置转换成这里统一的 ErrorLocation。

数据流:进去的是一个 Error 的引用。函数检查错误种类:对于带 location 字段的错误,它复制出里面的位置;对于 Starlark 错误,它读取 Starlark 提供的 span,也就是源码范围,再换算成从 1 开始的行号和列号;其他错误没有明确位置,就返回空。出来的是 Option<ErrorLocation>,意思是“可能有位置,也可能没有”。

调用关系:它通常被错误展示、日志打印或诊断信息生成的代码使用,用来决定能不能告诉用户具体行列。它把内部不同来源的位置信息统一成同一种格式,让后面的代码不用关心错误最初来自普通规则检查还是 Starlark。

network-proxy/src/policy.rs源码 ↗
domain_logicconfig load 和 request handling

网络代理收到一个目标地址时,不能只看原始字符串,因为同一个地方可能写成 Example.COM、example.com.、[::1]:443,甚至带 IPv6 网卡范围标记。这个文件就像门卫手里的地址清洗和黑白名单手册:先把主机名规范化,再识别 localhost、私有地址、测试地址、链路本地地址等“不该随便访问的内部地址”,最后把用户配置的域名规则编译成可快速匹配的集合。它支持 exact host(精确主机)、*.example.com(只匹配子域名)、**.example.com(根域名和子域名都匹配)这些规则。一个重要细节是:拒绝列表不允许单独写全局通配符 *,否则等于把所有网站都拒了,配置风险太大;允许列表则可以在明确场景下使用。

函数细节44
Host::parse21–25 ↗
fn parse(input: &str) -> Result<Self>

作用:把外部传进来的主机名字符串变成一个统一格式的 Host。这样后面判断规则时,不会被大小写、端口、末尾点号这些写法差异干扰。

数据流:输入是一段主机名文字 → 它调用 normalize_host 做清洗和统一,并检查结果不能是空的 → 输出一个 Host 对象;如果清洗后没有内容,就返回错误。

调用关系:host_blocked 和 update_domain_list 会先用它把原始地址变成可靠的 Host,再继续做封禁判断或更新域名列表。它把具体清洗工作交给 normalize_host。

调用图:调用 1 个内部函数(normalize_host);被 2 处调用(host_blocked, update_domain_list);外部调用 1 个(ensure!)。

Host::as_str27–29 ↗
fn as_str(&self) -> &str

作用:取出 Host 里面保存的规范化主机名文本。别人需要读取这个地址但不想改动它时会用这个函数。

数据流:输入是一个 Host → 它直接拿出内部字符串引用 → 输出可读取的主机名文本,不改变任何状态。

调用关系:is_loopback_host 和 is_explicit_local_allowlisted 会用它读取 Host 内容,然后继续判断是不是本机地址或是否被明确允许。

调用图:被 2 处调用(is_loopback_host, is_explicit_local_allowlisted)。

is_loopback_host33–43 ↗
fn is_loopback_host(host: &Host) -> bool

作用:判断一个主机是不是指向“本机”的地址,比如 localhost、127.0.0.1、::1。本机地址很敏感,因为访问它可能碰到代理所在机器上的内部服务。

数据流:输入是一个 Host → 它取出文本,先去掉 IPv6 作用域标记,再看是不是 localhost,或者能不能解析成 IP 并判断是否为回环地址 → 输出 true 或 false。

调用关系:host_blocked 在决定是否拦截请求时会调用它。它依赖 Host::as_str 读取地址,也用 unscoped_ip_literal 处理带作用域的 IP。

调用图:调用 2 个内部函数(as_str, unscoped_ip_literal);被 1 处调用(host_blocked)。

is_non_public_ip45–50 ↗
fn is_non_public_ip(ip: IpAddr) -> bool

作用:判断一个 IP 地址是不是公网地址之外的特殊地址,比如内网、本机、未指定、组播、测试网段等。这是防止代理被拿去访问内部网络的重要安全检查。

数据流:输入是一个 IP 地址 → 它先区分 IPv4 和 IPv6 → 分别交给 is_non_public_ipv4 或 is_non_public_ipv6 → 输出是否属于非公网范围。

调用关系:connect、host_blocked 和 host_resolves_to_non_public_ip 会在连接或域名解析后调用它,确认目标 IP 是否安全。

调用图:调用 2 个内部函数(is_non_public_ipv4, is_non_public_ipv6);被 3 处调用(connect, host_blocked, host_resolves_to_non_public_ip)。

is_non_public_ipv452–70 ↗
fn is_non_public_ipv4(ip: Ipv4Addr) -> bool

作用:专门判断 IPv4 地址是不是非公网地址。它覆盖常见内网段,也覆盖一些标准库没直接标出来的特殊网段。

数据流:输入是一个 IPv4 地址 → 它用系统自带判断检查本机、私有、链路本地、未指定、组播、广播等,再用 ipv4_in_cidr 检查 CGNAT、测试网段、保留网段 → 输出 true 或 false。

调用关系:is_non_public_ip 会把 IPv4 地址交给它;is_non_public_ipv6 遇到 IPv4 映射的 IPv6 地址时也会复用它。它把具体网段包含判断交给 ipv4_in_cidr。

调用图:调用 1 个内部函数(ipv4_in_cidr);被 2 处调用(is_non_public_ip, is_non_public_ipv6);外部调用 6 个(is_broadcast, is_link_local, is_loopback, is_multicast, is_private, is_unspecified)。

ipv4_in_cidr72–81 ↗
fn ipv4_in_cidr(ip: Ipv4Addr, base: [u8; 4], prefix: u8) -> bool

作用:判断一个 IPv4 地址是否落在某个 CIDR 网段里。CIDR 可以理解成“从某个地址开始、按前多少位相同来划的一片地址范围”。

数据流:输入是待检查 IP、网段起点和前缀长度 → 它把地址转成数字,用掩码只比较前缀部分 → 输出这个 IP 是否属于该网段。

调用关系:is_non_public_ipv4 用它补充检查那些标准库没有稳定覆盖的特殊 IPv4 范围。

调用图:被 1 处调用(is_non_public_ipv4);外部调用 2 个(from, from)。

is_non_public_ipv683–98 ↗
fn is_non_public_ipv6(ip: Ipv6Addr) -> bool

作用:专门判断 IPv6 地址是不是非公网地址。它会处理 ::1、fe80:: 这类本地地址,也能识别嵌在 IPv6 里的 IPv4 地址。

数据流:输入是一个 IPv6 地址 → 如果它能转成 IPv4,就交给 is_non_public_ipv4 再加上回环判断;否则检查回环、未指定、组播、唯一本地、链路本地等特征 → 输出是否非公网。

调用关系:is_non_public_ip 会把 IPv6 地址交给它。它在需要时复用 is_non_public_ipv4,保证 IPv4 映射地址也按同一套安全规则处理。

调用图:调用 1 个内部函数(is_non_public_ipv4);被 1 处调用(is_non_public_ip);外部调用 6 个(is_loopback, is_multicast, is_unicast_link_local, is_unique_local, is_unspecified, to_ipv4)。

normalize_host101–119 ↗
fn normalize_host(host: &str) -> String

作用:把主机名清洗成统一写法,方便后面匹配规则。它会处理空格、大小写、端口、IPv6 方括号、域名末尾点号等常见差异。

数据流:输入是一段可能很乱的主机名 → 它去掉首尾空白,必要时剥掉 IPv6 方括号和端口,避免误改未加括号的 IPv6,再交给 normalize_dns_host_or_ip_literal → 输出规范化后的字符串。

调用关系:HTTP 代理、CONNECT 代理、MITM 策略、hook 处理、Host::parse 等多个入口都会调用它。它是这个文件里最基础的“地址清洗机”。

调用图:调用 1 个内部函数(normalize_dns_host_or_ip_literal);被 12 处调用(http_connect_accept, http_connect_proxy, http_plain_proxy, evaluate_mitm_policy, mitm_stream, evaluate_mitm_hooks, normalize_hook_host, parse, normalize_pattern, host_has_mitm_hooks (+2 more))。

normalize_dns_host_or_ip_literal121–128 ↗
fn normalize_dns_host_or_ip_literal(host: &str) -> String

作用:继续清洗域名或 IP 字面量:统一小写,去掉域名末尾的点,并规范 IPv6 作用域写法。

数据流:输入是已初步拆好的主机文本 → 它转小写、去掉末尾点号,然后尝试按 IP 字面量规范化 → 输出清洗后的 IP 或域名字符串。

调用关系:normalize_host 把真正的域名/IP 标准化工作交给它;它再调用 normalize_ip_literal 识别 IP。

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

unscoped_ip_literal130–134 ↗
fn unscoped_ip_literal(host: &str) -> Option<&str>

作用:从带作用域的 IP 里取出纯 IP 部分,比如把 fe80::1%lo0 看成 fe80::1。这样做是为了判断地址类型时不被网卡名干扰。

数据流:输入是一段主机文本 → 它按 % 分开,确认前半段真的是 IP → 如果是,就输出不带作用域的 IP 文本;否则输出空。

调用关系:is_loopback_host、host_blocked、globset_matches_host_or_unscoped 和 is_explicit_local_allowlisted 会用它,把“地址本身”和“本地网卡范围”分开看。

调用图:被 4 处调用(is_loopback_host, host_blocked, globset_matches_host_or_unscoped, is_explicit_local_allowlisted)。

normalize_ip_literal136–148 ↗
fn normalize_ip_literal(host: &str) -> Option<String>

作用:识别并整理 IP 字面量,尤其是带 IPv6 作用域的写法。它会把 URL 编码的 %25 还原成普通 %。

数据流:输入是一段主机文本 → 如果本身就是 IP,直接返回;否则尝试按 %25 或 % 拆出 IP 和作用域,确认 IP 合法后重新拼成统一格式 → 输出规范 IP 字符串或空。

调用关系:normalize_dns_host_or_ip_literal 调用它来判断当前文本是不是 IP,而不是普通域名。

调用图:被 1 处调用(normalize_dns_host_or_ip_literal);外部调用 1 个(format!)。

normalize_pattern150–170 ↗
fn normalize_pattern(pattern: &str) -> String

作用:把配置里的域名匹配规则也统一格式,避免规则因为大小写、末尾点号或端口写法不同而失效。

数据流:输入是一条模式,比如 .Example.COM. → 它保留开头的通配前缀 ./**.,只规范化后面的域名部分 → 输出统一后的模式字符串。

调用关系:compile_globset_with_policy 在编译黑白名单前会调用它;is_global_wildcard_domain_pattern 也用它确认某条规则是不是全局通配。

调用图:调用 1 个内部函数(normalize_host);被 2 处调用(compile_globset_with_policy, is_global_wildcard_domain_pattern);外部调用 1 个(format!)。

is_global_wildcard_domain_pattern172–177 ↗
fn is_global_wildcard_domain_pattern(pattern: &str) -> bool

作用:判断一条域名规则最终是不是会变成“匹配所有主机”的 *。这主要用于防止拒绝列表里误放一个过于危险的规则。

数据流:输入是一条规则文本 → 它先规范化,再展开成实际匹配候选 → 只要其中有 *,就输出 true,否则 false。

调用关系:compile_globset_with_policy 在处理拒绝列表时会调用它;它依赖 normalize_pattern 和 expand_domain_pattern。

调用图:调用 2 个内部函数(expand_domain_pattern, normalize_pattern);被 1 处调用(compile_globset_with_policy)。

compile_allowlist_globset185–187 ↗
fn compile_allowlist_globset(patterns: &[String]) -> Result<GlobSet>

作用:把允许访问的域名规则编译成一个高效匹配器。之后代理只要拿主机名问这个匹配器,就能快速知道是否在允许范围内。

数据流:输入是一组允许规则字符串 → 它用“允许全局通配符”的策略交给 compile_globset_with_policy → 输出 GlobSet,也就是一组可快速匹配的通配规则。

调用关系:network_proxy_state_for_policy 和 build_config_state 在建立代理状态或加载配置时会调用它。测试 compile_globset_allows_global_wildcard_when_enabled 也覆盖它的行为。

调用图:调用 1 个内部函数(compile_globset_with_policy);被 3 处调用(network_proxy_state_for_policy, compile_globset_allows_global_wildcard_when_enabled, build_config_state)。

compile_denylist_globset189–191 ↗
fn compile_denylist_globset(patterns: &[String]) -> Result<GlobSet>

作用:把禁止访问的域名规则编译成一个高效匹配器。它会拒绝全局 *,避免一个配置把所有目标都挡住。

数据流:输入是一组禁止规则字符串 → 它用“拒绝全局通配符”的策略交给 compile_globset_with_policy → 输出可匹配的 GlobSet,或在规则太危险/不合法时返回错误。

调用关系:network_proxy_state_for_policy 会在构建策略时调用它;多个测试用它确认域名规范化、IPv6 和通配规则都按预期工作。

调用图:调用 1 个内部函数(compile_globset_with_policy);被 12 处调用(compile_globset_normalizes_apex_and_subdomains, compile_globset_normalizes_bracketed_ipv6_literals, compile_globset_normalizes_trailing_dots, compile_globset_normalizes_wildcards, compile_globset_preserves_scoped_ipv6_literals, compile_globset_supports_mid_label_wildcards, network_proxy_state_for_policy, compile_globset_dedupes_patterns_without_changing_behavior, compile_globset_excludes_apex_for_subdomain_patterns, compile_globset_includes_apex_for_double_wildcard_patterns (+2 more))。

compile_globset_with_policy193–223 ↗
fn compile_globset_with_policy(
    patterns: &[String],
    global_wildcard: GlobalWildcard,
) -> Result<GlobSet>

作用:真正把域名规则列表变成 GlobSet。GlobSet 可以理解成一叠通配符规则做成的快速查询表。

数据流:输入是规则列表和是否允许全局通配符的策略 → 它逐条检查危险的 *,规范化规则,展开 **. 和 *.,去重,然后用 globset 库编译 → 输出可匹配集合;规则不合法时返回错误。

调用关系:compile_allowlist_globset 和 compile_denylist_globset 都只是给它传不同策略。它内部会用 is_global_wildcard_domain_pattern、normalize_pattern 和 expand_domain_pattern。

调用图:调用 3 个内部函数(expand_domain_pattern, is_global_wildcard_domain_pattern, normalize_pattern);被 2 处调用(compile_allowlist_globset, compile_denylist_globset);外部调用 4 个(new, new, new, bail!)。

DomainPattern::parse237–249 ↗
fn parse(input: &str) -> Self

作用:把一条域名模式拆成三类:精确匹配、只匹配子域名、匹配根域名加子域名。这样后续不用反复解析字符串。

数据流:输入是一条模式文本 → 它去空白,识别 **. 和 *. 前缀 → 输出 DomainPattern 枚举值;空输入会变成空的精确匹配。

调用关系:expand_domain_pattern 会调用它,把人写的模式转换成 globset 能理解的候选规则。

调用图:被 1 处调用(expand_domain_pattern);外部调用 3 个(Exact, parse_domain, new)。

DomainPattern::parse_for_constraints252–264 ↗
fn parse_for_constraints(input: &str) -> Self

作用:为“策略约束比较”解析域名模式,并尽量用 URL 库验证和规范域名。约束比较就是判断一条规则是否被另一条更大的规则覆盖。

数据流:输入是一条模式文本 → 它识别 **. 或 *.,再调用 parse_domain_for_constraints 清洗域名部分 → 输出对应的 DomainPattern。

调用关系:它是约束检查一侧的解析入口;解析出的结果通常会再交给 DomainPattern::allows 做包含关系判断。

调用图:调用 1 个内部函数(parse_domain_for_constraints);外部调用 4 个(ApexAndSubdomains, Exact, SubdomainsOnly, new)。

DomainPattern::parse_domain266–272 ↗
fn parse_domain(domain: &str, build: impl FnOnce(String) -> Self) -> Self

作用:辅助解析域名部分,把非空域名交给指定的构造方式,空域名则安全地变成空精确匹配。

数据流:输入是域名文本和一个构造函数 → 它去掉首尾空白,判断是否为空 → 输出构造好的 DomainPattern。

调用关系:DomainPattern::parse 在识别到 *. 或 **. 后会调用它,避免重复写空域名处理。

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

DomainPattern::allows274–299 ↗
fn allows(&self, candidate: &DomainPattern) -> bool

作用:判断当前域名模式是否覆盖另一个域名模式。比如 **.example.com 可以覆盖 example.com 和 api.example.com。

数据流:输入是当前规则和候选规则 → 它按两边的模式类型比较域名是否相等、是否是子域名、是否必须严格子域名 → 输出是否允许/覆盖。

调用关系:它服务于策略约束判断。内部把具体比较交给 domain_eq、is_subdomain_or_equal 和 is_strict_subdomain。

调用图:调用 3 个内部函数(domain_eq, is_strict_subdomain, is_subdomain_or_equal)。

parse_domain_for_constraints302–319 ↗
fn parse_domain_for_constraints(domain: &str) -> String

作用:把用于约束比较的域名清洗成更可靠的形式。它会去掉末尾点号、剥掉 IPv6 方括号,并借助 URL 库识别合法主机。

数据流:输入是域名文本 → 它修剪空白和末尾点号;如果含有通配符、问号或百分号就保留原样;否则用 UrlHost 解析 → 输出规范域名,解析失败则输出空字符串。

调用关系:DomainPattern::parse_for_constraints 会调用它处理具体域名内容。

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

expand_domain_pattern321–331 ↗
fn expand_domain_pattern(pattern: &str) -> Vec<String>

作用:把人容易读的域名规则展开成 globset 能匹配的字符串。比如 **.example.com 会变成 example.com 和 ?*.example.com 两条。

数据流:输入是一条规范或未规范的模式 → 它先用 DomainPattern::parse 分类 → 输出一个或多个实际 glob 候选字符串。

调用关系:compile_globset_with_policy 用它生成最终匹配规则;is_global_wildcard_domain_pattern 用它检查是否包含全局 *。

调用图:调用 1 个内部函数(parse);被 2 处调用(compile_globset_with_policy, is_global_wildcard_domain_pattern);外部调用 1 个(vec!)。

normalize_domain333–335 ↗
fn normalize_domain(domain: &str) -> String

作用:把域名比较前统一成小写,并去掉末尾点号。这样 example.com、Example.COM. 会被看成同一个域名。

数据流:输入是域名文本 → 它去掉末尾点号并转成小写 → 输出规范域名字符串。

调用关系:domain_eq、is_subdomain_or_equal 和 is_strict_subdomain 都先调用它,再做真正比较。

调用图:被 3 处调用(domain_eq, is_strict_subdomain, is_subdomain_or_equal)。

domain_eq337–339 ↗
fn domain_eq(left: &str, right: &str) -> bool

作用:判断两个域名是否代表同一个域名,不受大小写和末尾点号影响。

数据流:输入两个域名 → 它分别 normalize_domain → 比较规范化结果 → 输出是否相等。

调用关系:DomainPattern::allows 在精确匹配规则比较时调用它。

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

is_subdomain_or_equal341–348 ↗
fn is_subdomain_or_equal(child: &str, parent: &str) -> bool

作用:判断一个域名是不是另一个域名本身,或者它的下级域名。比如 api.example.com 属于 example.com,example.com 也算自己。

数据流:输入 child 和 parent 两个域名 → 它先统一格式;如果完全相等返回 true,否则检查 child 是否以 .parent 结尾 → 输出判断结果。

调用关系:DomainPattern::allows 在 **.example.com 这类“根域名和子域名都允许”的规则里调用它。

调用图:调用 1 个内部函数(normalize_domain);被 1 处调用(allows);外部调用 1 个(format!)。

is_strict_subdomain350–354 ↗
fn is_strict_subdomain(child: &str, parent: &str) -> bool

作用:判断一个域名是不是另一个域名的真正子域名,不包括它自己。比如 api.example.com 是,example.com 不是。

数据流:输入 child 和 parent → 它统一格式后,要求两者不相等,并且 child 以 .parent 结尾 → 输出 true 或 false。

调用关系:DomainPattern::allows 在 *.example.com 这种“只允许子域名,不允许根域名”的规则里调用它。

调用图:调用 1 个内部函数(normalize_domain);被 1 处调用(allows);外部调用 1 个(format!)。

tests::method_allowed_full_allows_everything363–367 ↗
fn method_allowed_full_allows_everything()

作用:测试 Full 网络模式是否允许所有常见 HTTP 方法。HTTP 方法就是 GET、POST、CONNECT 这类请求动作。

数据流:输入是固定的 GET、POST、CONNECT 样例 → 它调用 NetworkMode::Full 的方法判断 → 用断言确认都返回允许。

调用关系:这是测试代码,运行测试时执行,用来保护网络模式规则不被改坏。

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

tests::method_allowed_limited_allows_only_safe_methods370–376 ↗
fn method_allowed_limited_allows_only_safe_methods()

作用:测试 Limited 网络模式是否只允许相对安全的 HTTP 方法,比如 GET、HEAD、OPTIONS,并拒绝 POST、CONNECT。

数据流:输入是几种固定方法名 → 它分别检查 Limited 模式的判断结果 → 用断言确认安全方法通过,风险方法不通过。

调用关系:这是测试代码,运行测试时执行,验证限制模式的边界。

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

tests::compile_globset_normalizes_trailing_dots379–384 ↗
fn compile_globset_normalizes_trailing_dots()

作用:测试拒绝列表规则会把域名末尾的点号忽略掉。域名末尾点号是合法写法,但策略里通常应该和无点号等价。

数据流:输入规则 Example.COM. → 编译成拒绝匹配器 → 检查 example.com 能匹配,而 api.example.com 不能匹配。

调用关系:它调用 compile_denylist_globset,确认规则编译时的规范化行为。

调用图:调用 1 个内部函数(compile_denylist_globset);外部调用 1 个(assert_eq!)。

tests::compile_globset_normalizes_wildcards387–392 ↗
fn compile_globset_normalizes_wildcards()

作用:测试带 *. 的通配规则会被规范化,并且只匹配子域名,不匹配根域名。

数据流:输入规则 *.Example.COM. → 编译成匹配器 → 检查 api.example.com 命中,example.com 不命中。

调用关系:它通过 compile_denylist_globset 验证 normalize_pattern 和 expand_domain_pattern 配合正确。

调用图:调用 1 个内部函数(compile_denylist_globset);外部调用 1 个(assert_eq!)。

tests::compile_globset_supports_mid_label_wildcards395–402 ↗
fn compile_globset_supports_mid_label_wildcards()

作用:测试规则中间也可以出现通配符,比如 region.v2.argotunnel.com。这里的 只在当前标签里扩展,不应该跨到更上级域名。

数据流:输入 region*.v2.argotunnel.com → 编译匹配器 → 检查 region1 和 region 能匹配,xregion1 或多一层子域名不能匹配。

调用关系:它调用 compile_denylist_globset,保证 globset 的通配能力按预期暴露给策略系统。

调用图:调用 1 个内部函数(compile_denylist_globset);外部调用 1 个(assert_eq!)。

tests::compile_globset_normalizes_apex_and_subdomains405–410 ↗
fn compile_globset_normalizes_apex_and_subdomains()

作用:测试 **.example.com 这种规则能同时匹配根域名和子域名。

数据流:输入 **.Example.COM. → 编译匹配器 → 检查 example.com 和 api.example.com 都能命中。

调用关系:它调用 compile_denylist_globset,覆盖 expand_domain_pattern 对 **. 前缀的展开。

调用图:调用 1 个内部函数(compile_denylist_globset);外部调用 1 个(assert_eq!)。

tests::compile_globset_normalizes_bracketed_ipv6_literals413–417 ↗
fn compile_globset_normalizes_bracketed_ipv6_literals()

作用:测试带方括号的 IPv6 地址规则会被规范成不带方括号的形式。

数据流:输入规则 [::1] → 编译匹配器 → 检查 ::1 能命中。

调用关系:它调用 compile_denylist_globset,确认 normalize_host 对 IPv6 方括号的处理。

调用图:调用 1 个内部函数(compile_denylist_globset);外部调用 1 个(assert_eq!)。

tests::compile_globset_preserves_scoped_ipv6_literals420–426 ↗
fn compile_globset_preserves_scoped_ipv6_literals()

作用:测试带作用域的 IPv6 地址不会丢掉作用域信息。作用域通常表示本机某个网络接口。

数据流:输入 [fe80::1%25lo0] → 编译匹配器 → 检查 fe80::1%lo0 命中,而不同作用域或无作用域不命中。

调用关系:它调用 compile_denylist_globset,验证 normalize_ip_literal 对 %25 和 % 的处理不会误放宽规则。

调用图:调用 1 个内部函数(compile_denylist_globset);外部调用 1 个(assert_eq!)。

tests::is_loopback_host_handles_localhost_variants429–434 ↗
fn is_loopback_host_handles_localhost_variants()

作用:测试 localhost 的大小写和末尾点号变体都能被识别成本机地址。

数据流:输入 localhost、localhost.、LOCALHOST、notlocalhost → 分别构造 Host 并判断 → 断言前三个为本机,最后一个不是。

调用关系:这是 is_loopback_host 与 Host::parse 组合行为的测试,防止本机地址被写法变化绕过。

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

tests::is_loopback_host_handles_ip_literals437–441 ↗
fn is_loopback_host_handles_ip_literals()

作用:测试 IP 形式的本机地址能被识别,比如 127.0.0.1 和 ::1。

数据流:输入几个 IP 字符串 → 构造成 Host 后调用 is_loopback_host → 断言本机 IP 为 true,普通公网样例为 false。

调用关系:它覆盖 is_loopback_host 对 IP 字面量的判断路径。

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

tests::is_non_public_ip_rejects_private_and_loopback_ranges444–465 ↗
fn is_non_public_ip_rejects_private_and_loopback_ranges()

作用:测试各种非公网 IP 都会被识别出来,包括内网、本机、测试网段、保留网段、IPv4 映射 IPv6 等。

数据流:输入一批固定 IP 样例 → 调用 is_non_public_ip → 断言敏感或非公网地址返回 true,8.8.8.8 这类公网地址返回 false。

调用关系:它覆盖 is_non_public_ip、is_non_public_ipv4 和 is_non_public_ipv6 的核心安全判断。

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

tests::normalize_host_lowercases_and_trims468–470 ↗
fn normalize_host_lowercases_and_trims()

作用:测试主机名会去掉首尾空格并转成小写。

数据流:输入带空格和大小写混合的 ExAmPlE.CoM → 调用 normalize_host → 断言输出 example.com。

调用关系:它验证 normalize_host 最基础的清洗行为。

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

tests::normalize_host_strips_port_for_host_port473–475 ↗
fn normalize_host_strips_port_for_host_port()

作用:测试普通 host:port 写法会去掉端口,只留下主机名。

数据流:输入 example.com:1234 → 调用 normalize_host → 断言输出 example.com。

调用关系:它覆盖 normalize_host 对单个冒号的普通域名加端口处理。

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

tests::normalize_host_preserves_unbracketed_ipv6478–480 ↗
fn normalize_host_preserves_unbracketed_ipv6()

作用:测试未加方括号的 IPv6 地址不会被当成 host:port 错误截断。

数据流:输入 2001:db8::1 → 调用 normalize_host → 断言输出仍是 2001:db8::1。

调用关系:它保护 normalize_host 中“多个冒号表示可能是 IPv6,不要乱拆”的行为。

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

tests::normalize_host_strips_trailing_dot483–486 ↗
fn normalize_host_strips_trailing_dot()

作用:测试域名末尾点号会被去掉,并且大小写会统一。

数据流:输入 example.com. 和 ExAmPlE.CoM. → 调用 normalize_host → 断言都输出 example.com。

调用关系:它验证 normalize_dns_host_or_ip_literal 对完整域名写法的规范化。

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

tests::normalize_host_strips_trailing_dot_with_port489–491 ↗
fn normalize_host_strips_trailing_dot_with_port()

作用:测试带端口且域名末尾有点号时,也能同时去掉端口和末尾点号。

数据流:输入 example.com.:443 → 调用 normalize_host → 断言输出 example.com。

调用关系:它覆盖 normalize_host 先拆端口、再规范域名的组合路径。

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

tests::normalize_host_strips_brackets_for_ipv6494–497 ↗
fn normalize_host_strips_brackets_for_ipv6()

作用:测试 IPv6 地址外面的方括号会被去掉,不管后面有没有端口。

数据流:输入 [::1] 和 [::1]:443 → 调用 normalize_host → 断言都输出 ::1。

调用关系:它验证 normalize_host 对 URL 常见 IPv6 写法的处理。

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

tests::normalize_host_preserves_ipv6_scope_ids500–504 ↗
fn normalize_host_preserves_ipv6_scope_ids()

作用:测试 IPv6 的作用域标记会被保留,并且 URL 编码的 %25 会被还原成 %。

数据流:输入 fe80::1%lo0、[fe80::1%lo0]、[fe80::1%25lo0] → 调用 normalize_host → 断言都输出 fe80::1%lo0。

调用关系:它覆盖 normalize_host 和 normalize_ip_literal 对 scoped IPv6 的关键安全行为。

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

network-proxy/src/config.rs源码 ↗
configconfig load / startup

这个文件像网络代理的“说明书加安检员”。用户可以配置 HTTP 代理地址、SOCKS5 地址、允许或禁止访问哪些域名、是否允许 Unix socket(类 Unix 系统里本机进程通信用的特殊文件)等。文件先给出安全的默认值,比如默认只监听 127.0.0.1,也就是只让本机访问。然后它会解析用户写的地址,补默认端口,把 localhost 变成 127.0.0.1。最重要的是,它会检查危险配置:如果用户想监听非本机地址,默认会被改回本机地址;如果开启了 Unix socket 代理,更会强制只监听本机,防止远程用户借代理碰到本机服务。它还处理域名 allow/deny 冲突,规则是 deny 优先,像门卫名单里“黑名单优先于白名单”。

函数细节52
NetworkDomainPermissions::serialize46–55 ↗
fn serialize(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>

作用:把域名权限列表转换成适合写进配置文件或 JSON 的样子。它会先整理重复规则,确保最后输出的是实际生效的规则。

数据流:输入是一组可能有重复的域名权限条目 → 它调用 effective_entries 算出每个域名最终该允许还是拒绝 → 输出一个按域名映射到权限的表,交给序列化库写出去。

调用关系:它在配置保存或转成 JSON 时被使用,真正的冲突处理交给 NetworkDomainPermissions::effective_entries。

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

NetworkDomainPermissions::deserialize59–71 ↗
fn deserialize(deserializer: D) -> std::result::Result<Self, D::Error>

作用:把配置文件里写的域名权限表读回程序能用的列表。也就是把“example.com: deny”这种配置变成内部条目。

数据流:输入是反序列化器读到的键值表 → 它逐项取出域名模式和权限 → 输出 NetworkDomainPermissions,里面保存成条目列表。

调用关系:它在配置加载时由 serde(Rust 常用的序列化/反序列化库)调用,是配置文件进入程序的入口之一。

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

NetworkDomainPermissions::effective_entries75–103 ↗
fn effective_entries(&self) -> Vec<NetworkDomainPermissionEntry>

作用:算出域名权限的最终结果。遇到同一个域名既 allow 又 deny 时,它让 deny 赢。

数据流:输入是原始条目列表,可能重复、可能冲突 → 它按第一次出现的顺序记住域名,同时比较权限优先级 → 输出去重后的有效条目列表。

调用关系:NetworkDomainPermissions::serialize 会用它,其他读取允许/拒绝域名的逻辑也间接受益于这个统一的冲突规则。

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

NetworkProxySettings::default149–166 ↗
fn default() -> Self

作用:给网络代理配置提供一套安全、可用的默认值。没有写配置时,程序就靠它决定怎么启动。

数据流:没有外部输入 → 它填入默认 HTTP 代理地址、默认 SOCKS5 地址、默认 full 模式,以及各种安全开关的默认状态 → 输出完整的 NetworkProxySettings。

调用关系:配置加载、测试和很多代理构建流程都会从它开始,再只改用户明确设置的字段。它会调用 default_proxy_url 和 default_socks_url。

调用图:调用 2 个内部函数(default_proxy_url, default_socks_url);被 50 处调用(network_domain_permissions_serialize_to_effective_map_shape, partial_network_config_uses_struct_defaults_for_missing_fields, set_allowed_domains_preserves_existing_deny_for_same_pattern, settings_with_unix_sockets, direct_connector_allows_non_public_target_when_local_binding_enabled, direct_connector_rejects_non_public_target_when_local_binding_disabled, http_connect_accept_allows_allowlisted_host_in_full_mode, http_connect_accept_blocks_in_limited_mode, http_connect_accept_denies_denylisted_host, http_plain_proxy_attempts_allowed_unix_socket_proxy (+15 more));外部调用 2 个(new, default)。

NetworkProxySettings::allowed_domains170–172 ↗
fn allowed_domains(&self) -> Option<Vec<String>>

作用:取出当前配置里明确允许访问的域名列表。没有允许项时返回空结果。

数据流:输入是当前网络代理设置 → 它要求 domain_entries 按 allow 权限筛选 → 输出允许域名列表,或者没有列表。

调用关系:它是外部读取 allow 域名的便捷入口,实际筛选工作交给 NetworkProxySettings::domain_entries。

调用图:调用 1 个内部函数(domain_entries);被 2 处调用(entries, opposite_entries)。

NetworkProxySettings::denied_domains174–176 ↗
fn denied_domains(&self) -> Option<Vec<String>>

作用:取出当前配置里明确禁止访问的域名列表。没有禁止项时返回空结果。

数据流:输入是当前网络代理设置 → 它要求 domain_entries 按 deny 权限筛选 → 输出禁止域名列表,或者没有列表。

调用关系:它是外部读取 deny 域名的便捷入口,实际筛选工作交给 NetworkProxySettings::domain_entries。

调用图:调用 1 个内部函数(domain_entries);被 2 处调用(entries, opposite_entries)。

NetworkProxySettings::domain_entries178–190 ↗
fn domain_entries(&self, permission: NetworkDomainPermission) -> Option<Vec<String>>

作用:按指定权限筛选域名规则,比如只要允许项或只要拒绝项。它还会过滤掉空列表,避免调用者误以为有配置。

数据流:输入是当前设置和目标权限 → 它先拿到整理后的有效域名规则,再挑出权限匹配的域名模式 → 输出非空域名列表,若没有匹配则输出 None。

调用关系:NetworkProxySettings::allowed_domains 和 NetworkProxySettings::denied_domains 都靠它完成实际查找。

调用图:被 2 处调用(allowed_domains, denied_domains)。

NetworkProxySettings::allow_unix_sockets192–206 ↗
fn allow_unix_sockets(&self) -> Vec<String>

作用:取出被允许代理访问的 Unix socket 路径列表。Unix socket 可以连到本机服务,所以这个列表很敏感。

数据流:输入是当前设置里的 Unix socket 权限表 → 它只挑出权限为 allow 的路径 → 输出路径字符串列表,没有配置就输出空列表。

调用关系:clamp_bind_addrs 会调用它判断是否启用了 Unix socket 代理,从而决定是否必须强制监听本机地址。

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

NetworkProxySettings::set_allowed_domains208–210 ↗
fn set_allowed_domains(&mut self, allowed_domains: Vec<String>)

作用:一次性设置允许访问的域名列表。它不会直接乱改底层结构,而是交给统一的域名写入函数。

数据流:输入是一组允许域名 → 它把权限标成 Allow 后交给 set_domain_entries → 当前配置中的允许项被替换为新列表。

调用关系:这是给调用者用的简单接口,内部复用 NetworkProxySettings::set_domain_entries。

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

NetworkProxySettings::set_denied_domains212–214 ↗
fn set_denied_domains(&mut self, denied_domains: Vec<String>)

作用:一次性设置禁止访问的域名列表。常用于把用户输入的黑名单写进配置。

数据流:输入是一组禁止域名 → 它把权限标成 Deny 后交给 set_domain_entries → 当前配置中的禁止项被替换为新列表。

调用关系:这是给调用者用的简单接口,内部复用 NetworkProxySettings::set_domain_entries。

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

NetworkProxySettings::upsert_domain_permission216–232 ↗
fn upsert_domain_permission(
        &mut self,
        host: String,
        permission: NetworkDomainPermission,
        normalize: impl Fn(&str) -> String,
    )

作用:新增或更新某个域名的权限,并先删掉“看起来等价”的旧规则。upsert 的意思是有就更新、没有就插入。

数据流:输入是域名、权限和一个规范化函数 → 它把新旧域名都规范化后比较,删掉同一个目标的旧条目,再加入新条目 → 当前配置里的域名权限被更新,若列表为空则清掉 domains。

调用关系:它适合交互式修改规则时使用,比如用户临时允许或禁止某个主机;它不依赖其他本文件函数。

NetworkProxySettings::set_allow_unix_sockets234–236 ↗
fn set_allow_unix_sockets(&mut self, allow_unix_sockets: Vec<String>)

作用:一次性设置允许代理访问的 Unix socket 路径。它是修改 Unix socket 白名单的简便入口。

数据流:输入是一组路径字符串 → 它把这些路径标成 Allow 后交给 set_unix_socket_entries → 当前配置里的允许 Unix socket 项被替换。

调用关系:调用者不用懂底层权限表结构,只要调用它;实际写入由 NetworkProxySettings::set_unix_socket_entries 完成。

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

NetworkProxySettings::set_domain_entries238–256 ↗
fn set_domain_entries(&mut self, entries: Vec<String>, permission: NetworkDomainPermission)

作用:统一写入某一种域名权限的条目。它负责去掉旧的同类规则,并避免重复加入完全一样的规则。

数据流:输入是域名列表和权限类型 → 它先移除已有的同权限条目,再把新条目逐个加入且跳过重复 → 更新 domains,若没有任何条目就设为 None。

调用关系:NetworkProxySettings::set_allowed_domains 和 NetworkProxySettings::set_denied_domains 都把具体工作交给它。

调用图:被 2 处调用(set_allowed_domains, set_denied_domains)。

NetworkProxySettings::set_unix_socket_entries258–271 ↗
fn set_unix_socket_entries(
        &mut self,
        entries: Vec<String>,
        permission: NetworkUnixSocketPermission,
    )

作用:统一写入某一种 Unix socket 权限的条目。这里目前主要用于写 allow 列表。

数据流:输入是路径列表和权限类型 → 它先删除已有同权限路径,再把新路径插入权限表 → 更新 unix_sockets,若表为空就设为 None。

调用关系:NetworkProxySettings::set_allow_unix_sockets 调用它,隐藏底层 BTreeMap 这种有序表结构。

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

NetworkMode::allows_method288–293 ↗
fn allows_method(self, method: &str) -> bool

作用:判断当前网络模式是否允许某个 HTTP 方法。HTTP 方法就是 GET、POST 这种请求动作。

数据流:输入是网络模式和方法名 → full 模式直接允许,limited 模式只允许 GET、HEAD、OPTIONS → 输出 true 或 false。

调用关系:代理处理请求时会用它做权限判断,尤其是 limited 模式下限制会改变请求能否通过。

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

default_proxy_url296–298 ↗
fn default_proxy_url() -> String

作用:给 HTTP 代理提供默认监听地址。默认是本机的 3128 端口。

数据流:没有输入 → 返回字符串 http://127.0.0.1:3128 → 被默认配置填入 proxy_url。

调用关系:NetworkProxySettings::default 调用它,确保没有配置时也有安全的 HTTP 代理地址。

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

default_socks_url300–302 ↗
fn default_socks_url() -> String

作用:给 SOCKS5 代理提供默认监听地址。默认是本机的 8081 端口。

数据流:没有输入 → 返回字符串 http://127.0.0.1:8081 → 被默认配置填入 socks_url。

调用关系:NetworkProxySettings::default 调用它,确保 SOCKS5 代理默认只在本机可访问。

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

clamp_non_loopback305–325 ↗
fn clamp_non_loopback(
    addr: SocketAddr,
    allow_non_loopback: bool,
    name: &str,
    override_setting_name: &str,
) -> SocketAddr

作用:把非本机监听地址改回本机地址,除非用户明确打开危险开关。loopback 指 127.0.0.1 这类“只回到本机”的地址。

数据流:输入是一个地址、是否允许非本机监听、名字和提示用的配置项名 → 如果地址已是本机就原样返回;如果允许危险监听就警告后原样返回;否则警告并改成 127.0.0.1 同端口 → 输出安全处理后的地址。

调用关系:clamp_bind_addrs 分别用它检查 HTTP 和 SOCKS5 监听地址,是防止代理暴露到外网的第一道安全闸。

调用图:被 1 处调用(clamp_bind_addrs);外部调用 4 个(from, ip, port, warn!)。

clamp_bind_addrs327–366 ↗
fn clamp_bind_addrs(
    http_addr: SocketAddr,
    socks_addr: SocketAddr,
    cfg: &NetworkProxySettings,
) -> (SocketAddr, SocketAddr)

作用:统一检查 HTTP 和 SOCKS5 代理最终要监听的地址。它既处理普通非本机监听,也处理 Unix socket 开启时的更严格限制。

数据流:输入是 HTTP 地址、SOCKS5 地址和网络配置 → 先分别通过 clamp_non_loopback 做普通安全收紧;如果启用了 Unix socket 代理,则无论危险开关如何都强制改成本机地址 → 输出两个最终监听地址。

调用关系:resolve_runtime 在启动解析配置时会调用它;代理构建流程也会依赖它拿到真正安全的绑定地址。

调用图:调用 2 个内部函数(allow_unix_sockets, clamp_non_loopback);被 5 处调用(resolve_runtime, clamp_bind_addrs_allows_non_loopback_when_enabled, clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled, clamp_bind_addrs_forces_loopback_when_unix_sockets_enabled, build);外部调用 4 个(from, ip, port, warn!)。

UnixStyleAbsolutePath::parse377–379 ↗
fn parse(value: &str) -> Option<Self>

作用:识别以 / 开头的类 Unix 绝对路径。它用于接受像 /tmp/a.sock 这样的写法。

数据流:输入是路径字符串 → 如果以斜杠开头就包装成 UnixStyleAbsolutePath,否则返回 None → 输出可选的路径对象。

调用关系:ValidatedUnixSocketPath::parse 在本机路径判断之外,会用它兼容 Unix 风格绝对路径。

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

ValidatedUnixSocketPath::parse389–402 ↗
fn parse(socket_path: &str) -> Result<Self>

作用:检查 Unix socket 路径是不是绝对路径。相对路径会被拒绝,因为它容易指向不确定位置。

数据流:输入是 socket 路径字符串 → 先按当前系统的绝对路径解析并规范化;不行时再接受以 / 开头的 Unix 风格路径;两者都不是就报错 → 输出验证过的路径类型或错误。

调用关系:validate_unix_socket_allowlist_paths 和 Unix socket 访问判断会调用它,保证白名单里的路径不是随便写的相对路径。

调用图:调用 2 个内部函数(parse, from_absolute_path);被 2 处调用(validate_unix_socket_allowlist_paths, is_unix_socket_allowed);外部调用 4 个(new, Native, UnixStyleAbsolute, bail!)。

validate_unix_socket_allowlist_paths405–411 ↗
fn validate_unix_socket_allowlist_paths(cfg: &NetworkProxyConfig) -> Result<()>

作用:检查配置里所有允许的 Unix socket 路径是否合格。它会指出具体是哪一项坏了。

数据流:输入是完整网络代理配置 → 它取出 allow_unix_sockets 列表,逐个调用 ValidatedUnixSocketPath::parse → 全部通过就返回成功,任何一项失败就带着索引返回错误。

调用关系:resolve_runtime 和配置状态构建流程会先调用它,避免代理启动后才发现危险或无效路径。

调用图:调用 1 个内部函数(parse);被 2 处调用(resolve_runtime, build_config_state)。

resolve_runtime413–426 ↗
fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig>

作用:把用户配置变成运行时真正使用的地址。它是配置加载后、代理启动前的重要转换步骤。

数据流:输入是 NetworkProxyConfig → 先验证 Unix socket 白名单,再解析 HTTP 和 SOCKS5 地址,最后用 clamp_bind_addrs 做安全收紧 → 输出 RuntimeConfig,里面有最终 http_addr 和 socks_addr。

调用关系:代理启动构建时会调用它;它把 validate_unix_socket_allowlist_paths、resolve_addr、clamp_bind_addrs 串起来。

调用图:调用 3 个内部函数(clamp_bind_addrs, resolve_addr, validate_unix_socket_allowlist_paths);被 2 处调用(resolve_runtime_rejects_relative_allow_unix_sockets_entries, build)。

resolve_addr428–439 ↗
fn resolve_addr(url: &str, default_port: u16) -> Result<SocketAddr>

作用:把配置里写的地址字符串变成程序能监听的 SocketAddr。SocketAddr 就是“IP 地址 + 端口”。

数据流:输入是地址字符串和默认端口 → 它用 parse_host_port 拆出主机和端口,把 localhost 改成 127.0.0.1;如果主机不是 IP,就退回到 127.0.0.1 但保留端口 → 输出 SocketAddr。

调用关系:resolve_runtime 用它分别解析 HTTP 代理地址和 SOCKS5 代理地址。

调用图:调用 1 个内部函数(parse_host_port);被 1 处调用(resolve_runtime);外部调用 2 个(from, new)。

host_and_port_from_network_addr441–455 ↗
fn host_and_port_from_network_addr(value: &str, default_port: u16) -> String

作用:把各种网络地址写法整理成易读的“主机:端口”格式。常用于显示给用户看。

数据流:输入是地址字符串和默认端口 → 空字符串直接变成 <missing>;能解析就用解析出的主机端口,不能解析就按原字符串加默认端口 → 输出格式化后的字符串。

调用关系:它调用 parse_host_port 和 format_host_and_port,偏向展示用途,不负责真正绑定网络地址。

调用图:调用 2 个内部函数(format_host_and_port, parse_host_port)。

format_host_and_port457–463 ↗
fn format_host_and_port(host: &str, port: u16) -> String

作用:把主机名和端口拼成标准显示格式。IPv6 地址里本来有冒号,所以会自动加方括号。

数据流:输入是 host 和 port → 如果 host 含冒号,就输出 [host]:port;否则输出 host:port → 返回字符串。

调用关系:host_and_port_from_network_addr 调用它,专门处理最终格式化。

调用图:被 1 处调用(host_and_port_from_network_addr);外部调用 1 个(format!)。

parse_host_port471–506 ↗
fn parse_host_port(url: &str, default_port: u16) -> Result<SocketAddressParts>

作用:从地址字符串里拆出主机和端口。它兼容 URL、host:port、IPv6 等多种写法。

数据流:输入是原始地址和默认端口 → 先去空格,空则报错;对未加方括号的 IPv6 特判;再尝试用标准 URL 解析器;失败时交给 fallback 手工拆 → 输出 SocketAddressParts 或错误。

调用关系:resolve_addr 和 host_and_port_from_network_addr 都依赖它,是地址解析的主入口。

调用图:调用 1 个内部函数(parse_host_port_fallback);被 2 处调用(host_and_port_from_network_addr, resolve_addr);外部调用 4 个(parse, bail!, format!, matches!)。

parse_host_port_fallback508–557 ↗
fn parse_host_port_fallback(input: &str, default_port: u16) -> Result<SocketAddressParts>

作用:当标准 URL 解析不适用时,手工拆主机和端口。它处理带 scheme、路径、用户名密码、IPv6 方括号等情况。

数据流:输入是地址文本和默认端口 → 去掉协议头、路径、用户信息,再识别 [IPv6]:端口 或普通 host:port;端口无效时用默认端口 → 输出主机和端口,缺主机时报错。

调用关系:parse_host_port 在标准解析失败时调用它,作为更宽松的兜底解析器。

调用图:被 1 处调用(parse_host_port);外部调用 1 个(bail!)。

tests::settings_with_unix_sockets565–576 ↗
fn settings_with_unix_sockets(unix_sockets: &[&str]) -> NetworkProxySettings

作用:给测试快速造一个带 Unix socket 白名单的配置。这样测试不用每次重复写设置代码。

数据流:输入是一组路径引用 → 它从默认配置开始,如果路径不为空就写入允许 Unix socket 列表 → 输出 NetworkProxySettings。

调用关系:多个测试调用它来验证 Unix socket 开启时的安全收紧和路径校验行为。

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

tests::network_proxy_settings_default_matches_local_use_baseline579–599 ↗
fn network_proxy_settings_default_matches_local_use_baseline()

作用:确认默认网络代理配置符合预期的本机安全基线。防止以后有人改默认值却没注意安全影响。

数据流:没有业务输入 → 它构造默认配置,并和手写的期望配置逐字段比较 → 测试通过表示默认值没变坏。

调用关系:这是默认配置的回归测试,保护 NetworkProxySettings::default 的行为。

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

tests::partial_network_config_uses_struct_defaults_for_missing_fields602–617 ↗
fn partial_network_config_uses_struct_defaults_for_missing_fields()

作用:确认配置文件只写一部分字段时,没写的字段会自动补默认值。

数据流:输入是一段只写 enabled 的 JSON → 反序列化成配置,再和“默认配置加 enabled=true”的结果比较 → 测试通过表示缺省逻辑正常。

调用关系:它验证 serde 默认值和 NetworkProxySettings::default 能正确配合。

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

tests::set_allowed_domains_preserves_existing_deny_for_same_pattern620–631 ↗
fn set_allowed_domains_preserves_existing_deny_for_same_pattern()

作用:确认同一个域名同时出现在允许和禁止里时,禁止优先。这样黑名单不会被白名单意外盖掉。

数据流:它先默认配置中加入 deny example.com,再加入 allow example.com → 读取允许和禁止列表 → 期望允许为空、禁止仍有 example.com。

调用关系:它保护域名权限冲突规则,间接验证 set_domain_entries 和 effective_entries 的设计。

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

tests::network_domain_permissions_serialize_to_effective_map_shape634–665 ↗
fn network_domain_permissions_serialize_to_effective_map_shape()

作用:确认域名权限保存成 JSON 时,只输出最终生效的结果。重复冲突项不会原样泄漏出去。

数据流:它构造一个同域名 deny 后 allow 的配置 → 转成 JSON 值 → 期望 domains 里只有 example.com: deny。

调用关系:它直接保护 NetworkDomainPermissions::serialize 和 effective_entries 的输出形状。

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

tests::parse_host_port_defaults_for_empty_string668–670 ↗
fn parse_host_port_defaults_for_empty_string()

作用:确认空地址不会被当成合法地址。缺少主机应该立刻报错。

数据流:输入空字符串和默认端口 → 调用 parse_host_port → 期望返回错误。

调用关系:它保护 parse_host_port 对明显无效输入的处理。

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

tests::parse_host_port_defaults_for_whitespace673–675 ↗
fn parse_host_port_defaults_for_whitespace()

作用:确认只有空白字符的地址也会被拒绝。用户误填空格不能蒙混过关。

数据流:输入空白字符串和默认端口 → 调用 parse_host_port → 期望返回错误。

调用关系:它保护 parse_host_port 的 trim 后空值检查。

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

tests::parse_host_port_parses_host_port_without_scheme678–686 ↗
fn parse_host_port_parses_host_port_without_scheme()

作用:确认没有 http:// 这类协议头的 host:port 也能被解析。

数据流:输入 127.0.0.1:8080 和默认端口 → 调用 parse_host_port → 期望主机是 127.0.0.1、端口是 8080。

调用关系:它验证 parse_host_port 对简写地址的兼容性。

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

tests::parse_host_port_parses_host_port_with_scheme_and_path689–701 ↗
fn parse_host_port_parses_host_port_with_scheme_and_path()

作用:确认完整 URL 里的主机和端口能被正确取出,路径部分会被忽略。

数据流:输入 http://example.com:8080/some/path → 调用 parse_host_port → 输出主机 example.com 和端口 8080。

调用关系:它验证 parse_host_port 使用标准 URL 解析器时的行为。

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

tests::parse_host_port_strips_userinfo704–716 ↗
fn parse_host_port_strips_userinfo()

作用:确认 URL 里用户名密码不会被误当成主机。真正主机应该是 @ 后面的部分。

数据流:输入 http://user:pass@host.example:5555 → 调用 parse_host_port → 期望主机 host.example、端口 5555。

调用关系:它保护 parse_host_port 和兜底解析对带用户信息 URL 的处理。

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

tests::parse_host_port_parses_ipv6_with_brackets719–727 ↗
fn parse_host_port_parses_ipv6_with_brackets()

作用:确认带方括号的 IPv6 地址能正确解析。IPv6 本身含冒号,所以格式更特殊。

数据流:输入 http://[::1]:9999 → 调用 parse_host_port → 期望主机 ::1、端口 9999。

调用关系:它验证 parse_host_port 对 IPv6 标准写法的支持。

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

tests::parse_host_port_does_not_treat_unbracketed_ipv6_as_host_port730–738 ↗
fn parse_host_port_does_not_treat_unbracketed_ipv6_as_host_port()

作用:确认未加方括号的 IPv6 不会被误拆成主机和端口。否则会把 IPv6 里的冒号看错。

数据流:输入 2001:db8::1 和默认端口 → 调用 parse_host_port → 期望整个字符串是主机,端口用默认值。

调用关系:它保护 parse_host_port 中专门为未加方括号 IPv6 写的特判。

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

tests::parse_host_port_falls_back_to_default_port_when_port_is_invalid741–749 ↗
fn parse_host_port_falls_back_to_default_port_when_port_is_invalid()

作用:确认端口写坏时会使用默认端口,而不是直接崩掉。

数据流:输入 example.com:notaport 和默认端口 3128 → 调用 parse_host_port → 期望主机 example.com、端口 3128。

调用关系:它验证 parse_host_port_fallback 的宽容处理。

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

tests::host_and_port_from_network_addr_defaults_for_empty_string752–757 ↗
fn host_and_port_from_network_addr_defaults_for_empty_string()

作用:确认展示地址时,空输入会显示成 <missing>。这比显示奇怪的冒号端口更容易懂。

数据流:输入空字符串和默认端口 → 调用 host_and_port_from_network_addr → 期望输出 <missing>。

调用关系:它保护 host_and_port_from_network_addr 面向用户显示时的空值行为。

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

tests::host_and_port_from_network_addr_formats_ipv6760–765 ↗
fn host_and_port_from_network_addr_formats_ipv6()

作用:确认展示 IPv6 地址时会加方括号,避免和端口冒号混在一起。

数据流:输入 http://[::1]:8080 → 调用 host_and_port_from_network_addr → 期望输出 [::1]:8080。

调用关系:它验证 host_and_port_from_network_addr 会通过 format_host_and_port 正确格式化 IPv6。

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

tests::resolve_addr_maps_localhost_to_loopback768–773 ↗
fn resolve_addr_maps_localhost_to_loopback()

作用:确认 localhost 会被固定解析成 127.0.0.1。这样不会受系统 DNS 或 hosts 配置影响。

数据流:输入 localhost 和默认端口 → 调用 resolve_addr → 期望输出 127.0.0.1:3128。

调用关系:它保护 resolve_addr 对 localhost 的特殊处理。

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

tests::resolve_addr_parses_ip_literals776–781 ↗
fn resolve_addr_parses_ip_literals()

作用:确认普通 IPv4 字面地址能被直接解析。字面地址就是直接写 IP,不写域名。

数据流:输入 1.2.3.4 和默认端口 80 → 调用 resolve_addr → 期望输出 1.2.3.4:80。

调用关系:它验证 resolve_addr 对 IPv4 的基本支持。

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

tests::resolve_addr_parses_ipv6_literals784–789 ↗
fn resolve_addr_parses_ipv6_literals()

作用:确认 IPv6 字面地址也能解析成可用监听地址。

数据流:输入 http://[::1]:8080 → 调用 resolve_addr → 期望输出 [::1]:8080。

调用关系:它验证 resolve_addr 和 parse_host_port 对 IPv6 的组合行为。

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

tests::resolve_addr_falls_back_to_loopback_for_hostnames792–797 ↗
fn resolve_addr_falls_back_to_loopback_for_hostnames()

作用:确认普通域名不会真的解析到外部 IP,而是回退到本机地址。这里配置主要用于绑定监听,不是访问远程。

数据流:输入 http://example.com:5555 → 调用 resolve_addr → 期望输出 127.0.0.1:5555。

调用关系:它保护 resolve_addr 的安全兜底:非 IP 主机名只保留端口,地址用本机。

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

tests::clamp_bind_addrs_allows_non_loopback_when_enabled800–812 ↗
fn clamp_bind_addrs_allows_non_loopback_when_enabled()

作用:确认危险开关打开时,非本机监听地址可以保留。这个行为是有意的,但必须明确测试。

数据流:输入 0.0.0.0 的 HTTP 和 SOCKS 地址,以及允许非本机监听的配置 → 调用 clamp_bind_addrs → 期望两个地址仍是 0.0.0.0。

调用关系:它直接验证 clamp_bind_addrs 和 clamp_non_loopback 在危险开关开启时的行为。

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

tests::clamp_bind_addrs_forces_loopback_when_unix_sockets_enabled815–828 ↗
fn clamp_bind_addrs_forces_loopback_when_unix_sockets_enabled()

作用:确认只要启用了 Unix socket 白名单,即使危险开关允许公网监听,也会强制改回本机。

数据流:输入带 Unix socket 允许项的配置和 0.0.0.0 地址 → 调用 clamp_bind_addrs → 期望 HTTP 和 SOCKS 都变成 127.0.0.1 同端口。

调用关系:它保护 clamp_bind_addrs 中针对 Unix socket 的额外安全规则。

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

tests::clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled831–844 ↗
fn clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled()

作用:确认允许所有 Unix socket 时也会强制只监听本机。这种配置更危险,所以限制更不能少。

数据流:输入 dangerously_allow_all_unix_sockets 和 dangerously_allow_non_loopback_proxy 都为 true 的配置 → 调用 clamp_bind_addrs → 期望地址仍被改成 127.0.0.1。

调用关系:它验证 clamp_bind_addrs 对“允许所有 Unix socket”这个特殊开关的保护。

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

tests::resolve_runtime_rejects_relative_allow_unix_sockets_entries847–863 ↗
fn resolve_runtime_rejects_relative_allow_unix_sockets_entries()

作用:确认 Unix socket 白名单里写相对路径会导致启动解析失败。相对路径太含糊,不安全。

数据流:输入包含 relative.sock 的配置 → 调用 resolve_runtime → 期望返回错误,并且错误信息指出 network.allow_unix_sockets[0]。

调用关系:它验证 resolve_runtime 会先调用 validate_unix_socket_allowlist_paths,并把错误定位清楚。

调用图:调用 1 个内部函数(resolve_runtime);外部调用 3 个(assert!, settings_with_unix_sockets, panic!)。

tests::resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries866–875 ↗
fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries()

作用:确认以 / 开头的 Unix 风格绝对路径会被接受。这样常见的 /private/tmp/example.sock 可以正常配置。

数据流:输入包含 /private/tmp/example.sock 的配置 → 调用 resolve_runtime → 期望成功。

调用关系:它保护 ValidatedUnixSocketPath::parse 对 Unix 风格绝对路径的兼容支持。

调用图:外部调用 2 个(assert!, settings_with_unix_sockets)。

版本和发布诊断

这些工具公开当前构建版本,并验证或比较外部发布元数据,以便进行更新和发布检查。

tui/src/npm_registry.rs源码 ↗
domain_logicupdate check

这个文件像一个“发货检查员”。npm 是 JavaScript 生态常用的包仓库,里面会记录某个包有哪些版本、哪个版本是 latest(最新标签)、以及每个版本的下载地址和完整性校验码。这里先定义了能读懂 npm 返回 JSON 数据的结构,比如 NpmPackageInfo。真正关键的是 ensure_version_ready:它会确认 npm 的 latest 标签是否正好等于 GitHub 说的最新版本,然后再检查这个版本有没有 dist 信息。dist 可以理解成“货物的发货单”,里面必须有 tarball(压缩包下载地址)和 integrity(校验码,用来确认下载内容没坏也没被换)。如果任何一步不对,它会直接给出明确错误。文件底部的测试则模拟几种 npm 数据,确认正常情况会通过,latest 过期或缺少 dist 时会报错。

函数细节7
ensure_version_ready25–41 ↗
fn ensure_version_ready(
    package_info: &NpmPackageInfo,
    version: &str,
) -> anyhow::Result<()>

作用:检查某个 npm 包版本是否已经可以安全使用。它主要确认 npm 标记的 latest 版本和传入的目标版本一致,并且该版本有完整的下载信息。

数据流:输入是一份已经解析好的 npm 包信息 package_info,以及一个版本号字符串 version。它先去掉版本号前后的空白,再读取 package_info 里的 dist-tags.latest;如果 latest 不存在或不等于目标版本,就返回错误。通过后,它把包信息和版本号交给 version_info_with_dist 做更细的检查。最后如果所有检查都通过,就返回成功,不改动原始数据。

调用关系:它是这个文件对外最重要的检查入口。更新检查流程里的 check_for_update 会调用它,测试里的三个用例也会调用它来验证不同场景。它自己不检查 dist 的每个字段,而是把这部分工作交给 version_info_with_dist;一旦发现问题,就用 bail! 立刻停止并返回说明原因的错误。

调用图:调用 1 个内部函数(version_info_with_dist);被 4 处调用(ready_version_rejects_missing_root_dist, ready_version_rejects_stale_latest_dist_tag, ready_version_requires_latest_dist_tag_and_root_dist, check_for_update);外部调用 1 个(bail!)。

version_info_with_dist43–69 ↗
fn version_info_with_dist(
    package_info: &'a NpmPackageInfo,
    version: &str,
) -> anyhow::Result<&'a NpmPackageVersionInfo>

作用:确认指定版本在 npm 元数据里存在,并且带有可下载、可校验的发布文件信息。它是更细的“包裹内容检查”。

数据流:输入是 npm 包信息和目标版本号。它先在 versions 里查这个版本;查不到就返回错误。查到后继续看 dist 是否存在,再看 dist.tarball 是否有非空下载地址,dist.integrity 是否有非空校验码。全部满足时,它返回这个版本的信息引用;如果缺任何一项,就返回对应的错误,不修改数据。

调用关系:它只被 ensure_version_ready 调用,属于内部辅助函数。ensure_version_ready 负责先看 latest 标签是否正确;标签正确后,再由它确认这个版本本身是否真的有可下载的发布内容。

调用图:被 1 处调用(ensure_version_ready);外部调用 1 个(bail!)。

tests::version_json75–82 ↗
fn version_json(version: &str) -> serde_json::Value

作用:为测试快速造出一个“看起来像 npm 版本信息”的 JSON 片段。这样测试不用每次手写一大段重复数据。

数据流:输入是一个版本号字符串。它把这个版本号拼进 integrity 校验字符串和 tarball 下载地址里,然后返回一份 JSON 值,里面包含 dist、integrity 和 tarball。它只生成测试数据,不影响真实 npm 数据。

调用关系:它是测试里的小工具,被 tests::package_info 用来组装完整的假 npm 包信息。它借助 json! 这个宏创建 JSON 数据,让测试更短、更容易读。

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

tests::package_info84–93 ↗
fn package_info(github_latest: &str, npm_latest: &str) -> NpmPackageInfo

作用:为测试构造一份假的 NpmPackageInfo,用来模拟 npm 仓库返回的包元数据。它可以分别指定 GitHub 期待的版本和 npm 标记的 latest 版本。

数据流:输入是 github_latest 和 npm_latest 两个版本号。它先创建 versions 表,把 github_latest 对应的版本信息放进去;版本信息来自 tests::version_json。然后它再拼出带 dist-tags.latest 和 versions 的 JSON,最后用 serde_json::from_value 把 JSON 转成 NpmPackageInfo 结构。输出就是可供 ensure_version_ready 检查的假包信息。

调用关系:它服务于测试用例,帮助测试快速制造“npm latest 正确”或“npm latest 过期”的场景。它把生成单个版本 JSON 的活交给 tests::version_json,再把整体数据转成真实代码会使用的 NpmPackageInfo。

调用图:外部调用 4 个(new, from_value, json!, version_json)。

tests::ready_version_requires_latest_dist_tag_and_root_dist96–101 ↗
fn ready_version_requires_latest_dist_tag_and_root_dist()

作用:验证正常情况:npm 的 latest 标签等于目标版本,而且这个版本有完整 dist 信息时,检查应该通过。

数据流:它先设定版本号 1.2.3,再用 tests::package_info 造出一份 latest 也是 1.2.3 的假 npm 数据。然后把这份数据和版本号传给 ensure_version_ready。预期结果是成功;如果失败,测试就会报错。

调用关系:这是 ensure_version_ready 的正向测试,证明最理想的数据会被接受。它通过 tests::package_info 准备输入,然后直接检查 ensure_version_ready 的结果。

调用图:调用 1 个内部函数(ensure_version_ready);外部调用 1 个(package_info)。

tests::ready_version_rejects_stale_latest_dist_tag104–113 ↗
fn ready_version_rejects_stale_latest_dist_tag()

作用:验证 npm 的 latest 标签落后时会被拒绝。也就是说,GitHub 说最新是 1.2.3,但 npm 还标着 1.2.2,这种情况不能当作准备好了。

数据流:它用 tests::package_info 构造假数据:版本列表里有 1.2.3,但 dist-tags.latest 写成 1.2.2。然后调用 ensure_version_ready 检查 1.2.3,并期待拿到错误。最后用 assert! 确认错误信息里提到了 latest dist-tag,说明报错原因是对的。

调用关系:这是 ensure_version_ready 的异常路径测试。它模拟发布流程中常见的时间差:包版本可能存在,但 latest 标签还没更新。测试确保这种情况不会被误判成可更新。

调用图:调用 1 个内部函数(ensure_version_ready);外部调用 2 个(assert!, package_info)。

tests::ready_version_rejects_missing_root_dist116–129 ↗
fn ready_version_rejects_missing_root_dist()

作用:验证某个版本虽然存在,但缺少 dist 发布信息时会被拒绝。这样可以防止程序拿到一个没有下载地址或校验信息的“空壳版本”。

数据流:它直接用 json! 构造一份假 npm 数据:latest 是 1.2.3,versions 里也有 1.2.3,但这个版本对象是空的,没有 dist。接着用 serde_json::from_value 转成 NpmPackageInfo,再调用 ensure_version_ready。预期是返回错误,并用 assert! 确认错误信息提到 missing dist metadata。

调用关系:这是 ensure_version_ready 经过 version_info_with_dist 时的失败场景测试。它证明代码不只看版本号和 latest 标签,还会继续检查真正的发布文件信息是否齐全。

调用图:调用 1 个内部函数(ensure_version_ready);外部调用 3 个(assert!, from_value, json!)。

tui/src/update_versions.rs源码 ↗
util检查更新时使用;运行测试时验证这些版本判断规则

检查更新看起来简单,其实容易出错:发布标签可能带前缀,版本号可能有空格,也可能是测试版名字。这个文件把这些小坑集中处理。它只认可最朴素的三段版本号,也就是“主版本.次版本.补丁版本”,例如 1.2.3;如果遇到 beta、rc 这类预发布版本,就不硬猜,直接说“无法判断”。它还把源码构建常用的 0.0.0 当成特殊版本,表示不该参与普通升级检查。可以把它想成一个门卫:先看标签格式对不对,再看版本号是不是标准,最后才决定要不要提示用户升级。

函数细节10
is_newer1–6 ↗
fn is_newer(latest: &str, current: &str) -> Option<bool>

作用:判断“latest”这个版本是不是比“current”这个版本新。它不会强行理解复杂版本号;只要两边有一个看不懂,就返回“无法判断”。

数据流:输入是两个字符串:最新版本和当前版本。它先把两个字符串都交给 parse_version 变成三个数字;如果都成功,就按数字大小比较,输出 Some(true) 或 Some(false);如果有任何一边解析失败,就输出 None,不改动任何外部状态。

调用关系:它是更新检查里的比较器,本身不负责联网或显示提示。它把“读懂版本号”的小活交给 parse_version,然后把结果交还给调用它的更新流程去决定是否提醒用户。

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

extract_version_from_latest_tag8–13 ↗
fn extract_version_from_latest_tag(latest_tag_name: &str) -> anyhow::Result<String>

作用:从 GitHub 最新发布标签里取出真正的版本号。项目的发布标签要求长得像 rust-v1.5.0,这个函数会去掉前面的 rust-v。

数据流:输入是一个标签名字字符串。它检查这个字符串是不是以 rust-v 开头;如果是,就去掉这个前缀并输出剩下的版本号;如果不是,就返回一个错误,说明这个标签名不能按预期解析。

调用关系:fetch_latest_github_release_version 在拿到 GitHub 发布信息后会调用它,把“发布标签”转换成“可比较的版本号”。它自己不访问 GitHub,只负责拆标签。

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

is_source_build_version15–17 ↗
fn is_source_build_version(version: &str) -> bool

作用:判断某个版本是不是源码构建用的特殊版本 0.0.0。这个特殊版本通常不适合拿来做正常升级比较。

数据流:输入是一个版本字符串。它调用 parse_version 把字符串解析成三个数字;如果结果正好是 0、0、0,就输出 true,否则输出 false。它不保存数据,也不修改别的东西。

调用关系:get_upgrade_version 和 get_upgrade_version_for_popup 会先用它过滤掉源码构建版本,避免给这类用户弹出不合适的升级提示。它依赖 parse_version 来保证判断基于标准三段版本号。

调用图:调用 1 个内部函数(parse_version);被 2 处调用(get_upgrade_version, get_upgrade_version_for_popup)。

parse_version19–25 ↗
fn parse_version(v: &str) -> Option<(u64, u64, u64)>

作用:把像 1.2.3 这样的版本号字符串拆成三个数字,方便程序比较大小。它是这个文件里最基础的“小尺子”。

数据流:输入是一个字符串。它先去掉首尾空白,再按点号分成几段,依次把前三段转成数字;三段都能转成功,就输出一个数字三元组;缺段或某段不是纯数字时,就输出 None。

调用关系:is_newer 和 is_source_build_version 都靠它把人类写的版本号变成程序能比较的数字。它不调用别的项目逻辑,是底层辅助函数。

调用图:被 2 处调用(is_newer, is_source_build_version)。

tests::extracts_version_from_latest_tag33–38 ↗
fn extracts_version_from_latest_tag()

作用:验证正常的发布标签 rust-v1.5.0 能被正确拆出 1.5.0。这个测试保证最常见的升级检查入口不会坏。

数据流:测试输入固定字符串 rust-v1.5.0。它调用 extract_version_from_latest_tag,并用断言检查输出是不是 1.5.0;如果不是,测试失败,但不会改动运行时数据。

调用关系:它由测试运行器执行,主要保护 extract_version_from_latest_tag 的正常路径。它通过 assert_eq! 这个测试断言来判断结果是否符合预期。

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

tests::latest_tag_without_prefix_is_invalid41–43 ↗
fn latest_tag_without_prefix_is_invalid()

作用:验证没有 rust-v 前缀的标签会被判为无效。这样可以避免程序把格式不对的发布标签误当成正式版本。

数据流:测试输入固定字符串 v1.5.0。它调用 extract_version_from_latest_tag,然后检查结果是不是错误;如果函数误认为它有效,测试就失败。

调用关系:它由测试运行器执行,覆盖 extract_version_from_latest_tag 的错误路径。它用 assert! 确认“格式错就报错”这个约定没有被改坏。

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

tests::prerelease_version_is_not_considered_newer46–49 ↗
fn prerelease_version_is_not_considered_newer()

作用:验证 beta、rc 这类预发布版本不会被当成普通新版本来比较。这样可以避免把测试版误提示给普通用户。

数据流:测试把 0.11.0-beta.1 和 1.0.0-rc.1 作为最新版本输入,并分别和正式版比较。它期望 is_newer 返回 None,意思是“这种版本号不做判断”。

调用关系:它由测试运行器执行,保护 is_newer 和 parse_version 的共同规则:只处理简单三段数字版本,不处理预发布后缀。它通过 assert_eq! 检查这个约定。

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

tests::plain_semver_comparisons_work52–57 ↗
fn plain_semver_comparisons_work()

作用:验证普通三段版本号的大小比较是对的。比如 0.11.1 应该比 0.11.0 新,1.0.0 应该比 0.9.9 新。

数据流:测试输入多组标准版本号字符串。每组都交给 is_newer 比较,然后用断言检查输出 true 或 false 是否符合直觉和规则。

调用关系:它由测试运行器执行,是 is_newer 的核心正确性测试。它间接也检查 parse_version 能把标准版本号解析成可比较的数字。

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

tests::source_build_version_is_not_checked60–63 ↗
fn source_build_version_is_not_checked()

作用:验证 0.0.0 会被识别为源码构建版本,而普通版本不会。这样升级提示逻辑能跳过特殊构建。

数据流:测试分别输入 0.0.0 和 0.1.0。它调用 is_source_build_version,并检查第一个返回 true、第二个返回 false。

调用关系:它由测试运行器执行,保护 is_source_build_version 的特殊规则。这个规则会被 get_upgrade_version 和 get_upgrade_version_for_popup 依赖。

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

tests::whitespace_is_ignored66–69 ↗
fn whitespace_is_ignored()

作用:验证版本号前后的空格和换行不会影响解析和比较。这样来自外部文本的数据稍微不干净时也能正常处理。

数据流:测试输入带空格和换行的版本字符串。它先检查 parse_version 能解析成 1、2、3,再检查 is_newer 能把带空格的 1.2.3 判断为比 1.2.2 新。

调用关系:它由测试运行器执行,同时保护 parse_version 的去空白行为和 is_newer 对这个行为的依赖。它用 assert_eq! 确认结果不受首尾空白影响。

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

tui/src/version.rs源码 ↗
configcross-cutting

这个文件很小,但很关键:它提供了一个统一的版本号来源。版本号不是手写在代码里的,而是在编译时从 Cargo 的包信息里自动取出来。Cargo 可以理解成 Rust 项目的“打包和构建工具”,里面记录了项目名称、版本等信息。这里的 CODEX_CLI_VERSION 就像产品包装盒上的版本标签,程序需要告诉用户“我现在是哪一版”时,就看这个标签。这样做可以避免一个常见问题:项目配置里已经改了版本号,但代码里忘了同步,导致显示出来的版本是错的。