Codex 系统手册

交互式和持久化登录流程

stage-5.115 个文件

这一阶段像系统的“办证和保管处”,出现在首次使用、启动引导和账号操作时,也在后台保管通行证。命令行和终端界面负责让用户选择登录方式:开浏览器、设备码、API key 或 Bedrock key。login 库把这些入口串起来,本机小服务器接住浏览器回跳。账号处理器负责查状态、额度和退出。保存层、钥匙串和配置规则决定令牌放哪里;退出时还会尽量撤销旧令牌。MCP 登录也用同一套办法拿取、保存和刷新通行证。

本阶段的文件15

CLI 与账户入口

这些文件通过 CLI 和 app-server 账户 API 向用户提供登录、登出和认证状态流程。

cli/src/login.rs源码 ↗
orchestrationCLI login/status/logout command handling

这个文件像登录流程的“前台接待员”。用户可能用浏览器登录 ChatGPT,也可能把 API key 或 access token 通过标准输入传进来,还可能在没有浏览器的服务器上用设备码登录。这里先读取配置,检查管理员有没有强制某种登录方式,然后选择对应的登录办法。登录前会尽量清掉旧凭据,避免新旧账号混在一起。直接运行 codex login 时,它还会单独写一份 codex-login.log 日志文件,方便出问题时让支持人员查看。它也负责把结果用简单文字告诉用户,并在成功或失败后退出进程。文件底部还有几个小测试,确认旧登录会被清掉,以及 API key 展示时不会把完整密钥泄露出来。

函数细节19
init_login_file_logging51–110 ↗
fn init_login_file_logging(config: &Config) -> Option<WorkerGuard>

作用:为一次性的登录命令打开一个专用日志文件。这样用户登录失败时,不只是在屏幕上一闪而过,还能留下 codex-login.log 供排查。

数据流:输入是已经加载好的配置;它从配置推算日志目录,创建目录,打开日志文件,然后把登录相关的日志写入这个文件。成功时返回一个守卫对象,用来保持后台写日志任务活着;失败时只打印警告并返回空,不阻止登录继续。

调用关系:各个登录入口在真正开始登录前都会调用它。它把日志系统准备好后,后面的 ChatGPT、API key、access token、设备码登录流程就可以把关键信息写进去。

调用图:调用 1 个内部函数(log_dir);被 5 处调用(run_login_with_access_token, run_login_with_api_key, run_login_with_chatgpt, run_login_with_device_code, run_login_with_device_code_fallback_to_browser);外部调用 7 个(try_from_default_env, new, eprintln!, create_dir_all, non_blocking, layer, registry)。

print_login_server_start112–116 ↗
fn print_login_server_start(actual_port: u16, auth_url: &str)

作用:把本地浏览器登录服务器的地址告诉用户。尤其是浏览器没自动打开时,用户可以手动复制这个地址继续登录。

数据流:输入是实际监听的端口和认证网址;它把这些信息拼成一段清楚的提示,输出到错误输出。它不返回业务结果,也不改动登录状态。

调用关系:浏览器登录服务器启动后会调用它。普通 ChatGPT 登录会用到它;设备码不可用并回退到浏览器登录时也会用到它。

调用图:被 2 处调用(login_with_chatgpt, run_login_with_device_code_fallback_to_browser);外部调用 1 个(eprintln!)。

clear_existing_auth_before_login118–132 ↗
async fn clear_existing_auth_before_login(
    codex_home: &Path,
    auth_credentials_store_mode: AuthCredentialsStoreMode,
    auth_keyring_backend_kind: AuthKeyringBackendKind,
)

作用:在开始新登录前,尽量把旧登录清掉。这样可以避免用户以为换了账号,实际程序还在用旧凭据。

数据流:输入是 Codex 主目录、凭据保存方式和钥匙串后端类型;它调用退出登录并撤销凭据的逻辑。如果清理失败,它只记录警告,不会让整个登录流程立刻失败。

调用关系:ChatGPT 登录、设备码登录和带回退的设备码登录都会先经过它。测试也会直接调用它,确认旧的认证文件确实能被清掉。

调用图:被 4 处调用(login_with_chatgpt, run_login_with_device_code, run_login_with_device_code_fallback_to_browser, clears_existing_auth_before_login);外部调用 2 个(logout_with_revoke, warn!)。

login_with_chatgpt134–159 ↗
async fn login_with_chatgpt(
    codex_home: PathBuf,
    forced_chatgpt_workspace_id: Option<Vec<String>>,
    cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
    auth_keyring_backend_kind

作用:执行真正的 ChatGPT 浏览器登录流程。它启动本地小服务器,等用户在浏览器里完成认证。

数据流:输入是 Codex 主目录、可选的工作区限制、凭据保存方式和钥匙串后端;它先清理旧登录,再组装服务器选项,启动本地登录服务器,打印登录网址,最后等待登录完成。成功返回空结果,失败返回输入输出错误。

调用关系:它是浏览器登录的核心步骤,由 run_login_with_chatgpt 在命令行入口里调用。它把清理旧凭据交给 clear_existing_auth_before_login,把提示用户打开网址交给 print_login_server_start

调用图:调用 3 个内部函数(clear_existing_auth_before_login, print_login_server_start, new);被 1 处调用(run_login_with_chatgpt);外部调用 1 个(run_login_server)。

run_login_with_chatgpt161–190 ↗
async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> !

作用:处理用户执行 ChatGPT 登录命令时的完整流程。它负责加载配置、检查是否允许这种登录、启动日志,并给出最终成功或失败提示。

数据流:输入是命令行配置覆盖项;它加载配置,初始化登录日志,检查配置是否强制只能用 API key。如果允许,就调用浏览器登录。最后根据结果打印消息,并用退出码告诉操作系统成功还是失败。

调用关系:它由顶层 CLI 主程序在用户选择 ChatGPT 登录时调用。它本身不直接做认证细节,而是把具体登录交给 login_with_chatgpt

调用图:调用 3 个内部函数(init_login_file_logging, load_config_or_exit, login_with_chatgpt);被 1 处调用(cli_main);外部调用 4 个(eprintln!, matches!, exit, info!)。

run_login_with_api_key192–220 ↗
async fn run_login_with_api_key(
    cli_config_overrides: CliConfigOverrides,
    api_key: String,
) -> !

作用:处理用户用 API key 登录的命令。API key 就像一把固定的访问钥匙,程序会把它保存起来以后使用。

数据流:输入是命令行配置覆盖项和 API key 字符串;它加载配置,初始化日志,检查配置是否禁止 API key 登录。若允许,就把 API key 写入认证存储。最后打印成功或错误,并退出进程。

调用关系:它由 CLI 主程序在用户选择 --with-api-key 时调用。API key 通常先由 read_api_key_from_stdin 从标准输入读到,再传给这里保存。

调用图:调用 2 个内部函数(init_login_file_logging, load_config_or_exit);被 1 处调用(cli_main);外部调用 5 个(login_with_api_key, eprintln!, matches!, exit, info!)。

run_login_with_access_token222–254 ↗
async fn run_login_with_access_token(
    cli_config_overrides: CliConfigOverrides,
    access_token: String,
) -> !

作用:处理用户用 access token 登录的命令。access token 可以理解为一张已经签发好的临时或长期通行证。

数据流:输入是命令行配置覆盖项和 access token;它加载配置,初始化日志,检查是否允许这种登录。允许时,它把 token、工作区限制、服务地址和凭据保存设置交给底层登录函数。最后打印结果并退出。

调用关系:它由 CLI 主程序在用户选择 --with-access-token 时调用。token 通常先由 read_access_token_from_stdin 读取,然后交给这里完成保存和校验。

调用图:调用 2 个内部函数(init_login_file_logging, load_config_or_exit);被 1 处调用(cli_main);外部调用 5 个(login_with_access_token, eprintln!, matches!, exit, info!)。

read_api_key_from_stdin256–262 ↗
fn read_api_key_from_stdin() -> String

作用:从标准输入读取 API key。这样用户可以用管道传入密钥,避免把密钥直接写在命令历史里。

数据流:它没有额外输入,只是指定三段提示文案,然后调用通用的秘密读取函数。返回的是去掉前后空白后的 API key;如果没有通过管道传入或内容为空,程序会直接报错退出。

调用关系:CLI 主程序在处理 --with-api-key 时会调用它。它把实际读取和检查工作交给 read_stdin_secret

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

read_access_token_from_stdin264–270 ↗
fn read_access_token_from_stdin() -> String

作用:从标准输入读取 access token。它的目的和读取 API key 一样,是让敏感内容通过管道进入程序,而不是暴露在命令行参数里。

数据流:它准备好 access token 专用的提示文字,然后调用通用读取函数。成功时返回整理后的 token;如果用户没有用管道输入或输入为空,就打印说明并退出。

调用关系:CLI 主程序在处理 --with-access-token 时会调用它。具体的终端检测、读取、去空白和空值检查都由 read_stdin_secret 完成。

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

read_stdin_secret272–295 ↗
fn read_stdin_secret(terminal_message: &str, reading_message: &str, empty_message: &str) -> String

作用:读取一段敏感文本,比如 API key 或 access token,并确保它是从管道传进来的。这样可以降低密钥被命令历史记录下来的风险。

数据流:输入是三段提示文案;它先检查标准输入是不是终端。如果是终端,就说明用户没有用管道传密钥,于是报错退出。否则它读完整个标准输入,去掉首尾空白,检查是否为空,最后返回秘密字符串。

调用关系read_api_key_from_stdinread_access_token_from_stdin 都复用它。它是这两个函数背后的通用安全入口。

调用图:被 2 处调用(read_access_token_from_stdin, read_api_key_from_stdin);外部调用 4 个(new, eprintln!, stdin, exit)。

run_login_with_device_code298–337 ↗
async fn run_login_with_device_code(
    cli_config_overrides: CliConfigOverrides,
    issuer_base_url: Option<String>,
    client_id: Option<String>,
) -> !

作用:处理设备码登录。设备码登录适合没有浏览器的机器:程序给出一个码,用户去另一台能打开网页的设备上完成认证。

数据流:输入是命令行配置覆盖项、可选认证服务地址和可选客户端 ID;它加载配置、初始化日志、检查是否允许 ChatGPT 类登录,清理旧凭据,然后组装设备码登录选项。登录成功就提示成功并退出 0,失败就打印错误并退出 1。

调用关系:CLI 主程序在用户明确选择设备码登录时调用它。它先用 clear_existing_auth_before_login 清场,再把真正的设备码流程交给底层 run_device_code_login

调用图:调用 4 个内部函数(clear_existing_auth_before_login, init_login_file_logging, load_config_or_exit, new);被 1 处调用(cli_main);外部调用 5 个(run_device_code_login, eprintln!, matches!, exit, info!)。

run_login_with_device_code_fallback_to_browser343–408 ↗
async fn run_login_with_device_code_fallback_to_browser(
    cli_config_overrides: CliConfigOverrides,
    issuer_base_url: Option<String>,
    client_id: Option<String>,
) -> !

作用:优先尝试设备码登录,如果服务端不支持设备码,就自动退回到浏览器登录。这样在无头环境和普通桌面环境里都尽量能登录成功。

数据流:输入是命令行配置覆盖项、可选认证服务地址和可选客户端 ID;它加载配置、开日志、检查登录方式限制、清理旧凭据,然后关闭自动打开浏览器并尝试设备码登录。如果失败原因是“不支持设备码”,它启动本地浏览器登录服务器继续尝试;其他失败则直接报错退出。

调用关系:这是一个更聪明的登录调度器。它先把工作交给 run_device_code_login,在特定错误下再改交给 run_login_server,并用 print_login_server_start 告诉用户浏览器登录地址。

调用图:调用 5 个内部函数(clear_existing_auth_before_login, init_login_file_logging, load_config_or_exit, print_login_server_start, new);外部调用 6 个(run_device_code_login, run_login_server, eprintln!, matches!, exit, info!)。

run_login_status410–458 ↗
async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> !

作用:查看当前是否已经登录,以及大概是用哪种方式登录的。它不会暴露完整 API key,只会显示脱敏后的片段。

数据流:输入是命令行配置覆盖项;它加载配置,再从认证存储里读取当前凭据。如果找到凭据,就按登录类型打印对应说明;如果是 API key,还会先用安全格式隐藏中间内容。没有登录或读取失败时,会打印相应消息并设置失败退出码。

调用关系:CLI 主程序在用户执行登录状态命令时调用它。它读取认证存储,并在 API key 场景下使用 safe_format_key 避免泄密。

调用图:调用 2 个内部函数(load_config_or_exit, from_auth_storage);被 1 处调用(cli_main);外部调用 2 个(eprintln!, exit)。

run_logout460–483 ↗
async fn run_logout(cli_config_overrides: CliConfigOverrides) -> !

作用:执行退出登录。它会删除本地保存的凭据,并尽量通知远端撤销登录。

数据流:输入是命令行配置覆盖项;它加载配置,然后调用退出登录逻辑。若确实退出了,就提示成功;如果本来没登录,也会友好提示;出错时打印错误并用失败状态退出。

调用关系:CLI 主程序在用户执行 logout 时调用它。真正清理凭据和撤销远端授权的工作交给 logout_with_revoke

调用图:调用 1 个内部函数(load_config_or_exit);被 1 处调用(cli_main);外部调用 3 个(logout_with_revoke, eprintln!, exit)。

load_config_or_exit485–501 ↗
async fn load_config_or_exit(cli_config_overrides: CliConfigOverrides) -> Config

作用:读取命令行覆盖项和配置文件。如果配置读不出来,它会直接打印错误并退出,避免后续登录流程在错误配置上继续跑。

数据流:输入是 CLI 传来的配置覆盖项;它先解析这些覆盖项,再把它们交给配置加载器。成功时返回完整配置;解析失败或加载失败时打印错误并退出进程。

调用关系:几乎所有登录、状态、退出入口都会先调用它。它把“准备配置”这一步统一起来,后面的函数就不用重复处理配置错误。

调用图:调用 1 个内部函数(parse_overrides);被 7 处调用(run_login_status, run_login_with_access_token, run_login_with_api_key, run_login_with_chatgpt, run_login_with_device_code, run_login_with_device_code_fallback_to_browser, run_logout);外部调用 3 个(load_with_cli_overrides, eprintln!, exit)。

safe_format_key503–510 ↗
fn safe_format_key(key: &str) -> String

作用:把 API key 变成安全展示格式,只露出开头和结尾。这样用户能认出是哪把 key,又不会把完整密钥显示出来。

数据流:输入是完整 key 字符串;如果太短,就直接返回 ***。如果够长,就保留前 8 个字符和后 5 个字符,中间用星号代替,返回脱敏后的字符串。

调用关系run_login_status 在显示 API key 登录状态时会用它。底部测试也专门检查长 key 和短 key 的展示结果。

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

tests::clears_existing_auth_before_login525–549 ↗
async fn clears_existing_auth_before_login()

作用:测试新登录前清理旧凭据这件事是否真的有效。它防止以后改代码时不小心留下旧账号。

数据流:它先创建一个临时 Codex 主目录,在里面保存一个假的旧 API key;然后调用清理函数;最后再读取认证文件,确认里面已经没有登录信息。

调用关系:这是对 clear_existing_auth_before_login 的直接验证。它借用真实的 API key 保存和读取函数,模拟用户机器上的认证文件变化。

调用图:调用 2 个内部函数(clear_existing_auth_before_login, default);外部调用 4 个(assert_eq!, load_auth_dot_json, login_with_api_key, tempdir)。

tests::formats_long_key552–555 ↗
fn formats_long_key()

作用:测试长 API key 的脱敏显示是否符合预期。这样可以确认状态命令不会把完整密钥打印出来。

数据流:输入是一段示例长 key;测试调用脱敏函数,并检查输出是否只保留开头和结尾,中间变成星号。

调用关系:它验证 safe_format_key 的正常场景,保障 run_login_status 展示 API key 时既可识别又安全。

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

tests::short_key_returns_stars558–561 ↗
fn short_key_returns_stars()

作用:测试很短的 key 不会被展示任何真实片段。短 key 如果还显示头尾,可能等于泄露大部分内容。

数据流:输入是一段较短的示例 key;测试调用脱敏函数,并确认结果只有 ***

调用关系:它覆盖 safe_format_key 的边界情况,确保状态展示在短密钥上也不会泄密。

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

app-server/src/request_processors/account_processor.rs源码 ↗
domain_logicrequest handling

账号登录不是简单地存一个密码。这里要支持 API Key 登录、浏览器 ChatGPT 登录、设备码登录,以及外部传入的 ChatGPT token;还要能取消正在进行的登录,避免两个登录流程互相打架。这个文件里的 AccountRequestProcessor 就像账号事务的前台柜员:收到请求后先检查当前配置是否允许,再调用认证组件保存或刷新登录信息,最后把结果和“账号已变化”的通知发回客户端。它还会在账号变化后刷新插件相关缓存,因为不同账号可能看到不同的远程插件。ActiveLogin 用来记住当前正在跑的登录流程,并保证被替换或丢弃时会自动取消,像给每个排队办事的人发一个可撤销的号码牌。文件还负责向后端查询限额、token 用量,以及发送“请工作区管理员加额度”的邮件提醒。

函数细节45
ActiveLogin::login_id24–30 ↗
fn login_id(&self) -> Uuid

作用:取出当前登录尝试的编号。这个编号用来确认“我要取消的”是不是眼前这个登录流程。

数据流:进去的是一个 ActiveLogin,里面可能是浏览器登录,也可能是设备码登录 → 它只看两种情况里共同保存的 login_id → 出来的是这个登录编号,不改动任何状态。

调用关系:取消登录和后台登录完成清理时,会用这个编号和请求里的编号对比,避免误取消另一个刚开始的新登录。

ActiveLogin::cancel32–39 ↗
fn cancel(&self)

作用:取消一个正在进行的登录。浏览器登录会关掉本地登录服务器,设备码登录会发出取消信号。

数据流:进去的是当前活跃登录对象 → 它按登录类型选择关闭句柄或取消令牌 → 出来没有返回值,但对应的登录流程会被叫停。

调用关系:ActiveLogin::drop 会调用它,所以只要这个登录对象被丢弃,登录流程就会自动停止。

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

ActiveLogin::drop54–56 ↗
fn drop(&mut self)

作用:这是 ActiveLogin 被销毁时自动执行的收尾动作。它保证登录对象没了,背后的登录流程也不会继续偷偷跑。

数据流:进去的是即将被释放的 ActiveLogin → 它调用 ActiveLogin::cancel → 出来没有返回值,但浏览器服务器或设备码等待会被取消。

调用关系:当新的登录替换旧登录、用户取消登录、退出账号,或者处理器清理活跃登录时,旧的 ActiveLogin 被丢弃,就会走到这里。

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

AccountRequestProcessor::new70–85 ↗
fn new(
        auth_manager: Arc<AuthManager>,
        thread_manager: Arc<ThreadManager>,
        outgoing: Arc<OutgoingMessageSender>,
        config: Arc<Config>,
        config_manager: ConfigMan

作用:创建一个账号请求处理器,把认证、线程、发消息、配置这些依赖装进去。没有它,账号相关请求就没有统一入口。

数据流:进去的是 AuthManager、ThreadManager、OutgoingMessageSender、Config 和 ConfigManager → 它保存这些共享对象,并新建一个空的 active_login 槽位 → 出来是可被复制使用的 AccountRequestProcessor。

调用关系:上层初始化请求处理体系时会调用它;之后所有账号登录、退出、查询请求都会通过这个对象继续往下分发。

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

AccountRequestProcessor::login_account87–93 ↗
async fn login_account(
        &self,
        request_id: ConnectionRequestId,
        params: LoginAccountParams,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理客户端发来的“开始登录账号”请求。它是公开的请求入口,真正细分登录方式的工作交给 login_v2。

数据流:进去的是请求编号和登录参数 → 它调用 AccountRequestProcessor::login_v2 执行登录流程 → 出来是给 JSON-RPC 使用的空响应或错误。

调用关系:handle_initialized_client_request 收到账号登录请求时调用它;它把活儿交给 login_v2,再由 login_v2 根据登录方式分派。

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

AccountRequestProcessor::logout_account95–100 ↗
async fn logout_account(
        &self,
        request_id: ConnectionRequestId,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理客户端发来的“退出账号”请求。它让账号凭据失效,并通知客户端账号状态变了。

数据流:进去的是请求编号 → 它调用 AccountRequestProcessor::logout_v2 → 出来是空响应或错误。

调用关系:handle_initialized_client_request 会调用它;它只是外层包装,实际退出和通知由 logout_v2 完成。

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

AccountRequestProcessor::cancel_login_account102–109 ↗
async fn cancel_login_account(
        &self,
        params: CancelLoginAccountParams,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理“取消某次登录”的请求。它让用户可以中途放弃浏览器登录或设备码登录。

数据流:进去的是包含 login_id 的取消参数 → 它调用 AccountRequestProcessor::cancel_login_response → 出来是“已取消”或“没找到这次登录”的响应。

调用关系:handle_initialized_client_request 收到取消登录请求时调用它;内部会继续走 cancel_login_response 和 cancel_login_chatgpt_common。

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

AccountRequestProcessor::get_account111–118 ↗
async fn get_account(
        &self,
        params: GetAccountParams,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理“查询当前账号信息”的请求。客户端用它知道现在登录的是谁,以及是否需要 OpenAI 认证。

数据流:进去的是查询参数,比如是否刷新 token → 它调用 AccountRequestProcessor::get_account_response → 出来是账号信息响应或错误。

调用关系:handle_initialized_client_request 调用它;它把具体查账号状态的工作交给 get_account_response。

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

AccountRequestProcessor::get_auth_status120–127 ↗
async fn get_auth_status(
        &self,
        params: GetAuthStatusParams,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理“查询认证状态”的请求。它告诉客户端当前有没有登录、用什么方式登录,必要时也能带回 token。

数据流:进去的是是否包含 token、是否刷新 token 的参数 → 它调用 AccountRequestProcessor::get_auth_status_response → 出来是认证状态响应或错误。

调用关系:handle_initialized_client_request 调用它;它是外层入口,具体判断逻辑在 get_auth_status_response。

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

AccountRequestProcessor::get_account_rate_limits129–135 ↗
async fn get_account_rate_limits(
        &self,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理“查询账号限额”的请求。限额就是后端允许这个账号使用多少资源。

数据流:进去没有额外参数 → 它调用 AccountRequestProcessor::get_account_rate_limits_response → 出来是限额信息响应或错误。

调用关系:handle_initialized_client_request 调用它;它把认证检查和后端请求交给 get_account_rate_limits_response。

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

AccountRequestProcessor::get_account_token_usage137–143 ↗
async fn get_account_token_usage(
        &self,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理“查询 token 使用量”的请求。token 可以粗略理解为模型处理文字时消耗的计量单位。

数据流:进去没有额外参数 → 它调用 AccountRequestProcessor::get_account_token_usage_response → 出来是累计用量、每日用量等信息或错误。

调用关系:handle_initialized_client_request 调用它;它把真正访问后端和整理数据的工作交给 get_account_token_usage_response。

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

AccountRequestProcessor::send_add_credits_nudge_email145–152 ↗
async fn send_add_credits_nudge_email(
        &self,
        params: SendAddCreditsNudgeEmailParams,
    ) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError>

作用:处理“提醒工作区管理员加额度”的请求。用户额度不够时,可以通过它触发一封提醒邮件。

数据流:进去的是要提醒的额度类型 → 它调用 AccountRequestProcessor::send_add_credits_nudge_email_response → 出来是已发送、冷却中或错误。

调用关系:handle_initialized_client_request 调用它;具体检查登录状态、访问后端和处理冷却期由后续函数完成。

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

AccountRequestProcessor::cancel_active_login154–159 ↗
async fn cancel_active_login(&self)

作用:取消当前任何正在进行的登录流程。它常用于系统清理或切换状态时,防止旧登录继续占资源。

数据流:进去的是处理器自身 → 它拿到 active_login 这把互斥锁(一把锁,防止两个任务同时改同一份数据),取出当前登录并丢弃 → 出来没有返回值,但旧登录会因被丢弃而自动取消。

调用关系:上层的 cancel_active_login 清理动作会调用它;它依赖 ActiveLogin::drop 间接完成真正取消。

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

AccountRequestProcessor::clear_external_auth161–166 ↗
fn clear_external_auth(&self)

作用:清掉外部传入的认证信息,并让插件系统知道认证方式变了。外部认证通常是别的系统直接给的 ChatGPT token。

数据流:进去的是处理器自身 → 它让 AuthManager 清除外部认证,再把新的 API 认证模式同步给插件管理器 → 出来没有返回值,但运行中的认证模式会更新。

调用关系:clear_runtime_references 做运行期清理时会调用它;它连接认证系统和插件系统,避免插件继续按旧账号工作。

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

AccountRequestProcessor::current_account_updated_notification168–174 ↗
fn current_account_updated_notification(&self) -> AccountUpdatedNotification

作用:生成一条“当前账号已更新”的通知内容。里面包含当前认证方式和套餐类型。

数据流:进去的是处理器自身 → 它读取 AuthManager 缓存里的认证信息 → 出来是 AccountUpdatedNotification,不发送,只负责组装内容。

调用关系:send_login_success_notifications 会调用它,然后把生成的通知发给客户端。

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

AccountRequestProcessor::maybe_refresh_plugin_caches_for_current_config176–214 ↗
async fn maybe_refresh_plugin_caches_for_current_config(
        config_manager: &ConfigManager,
        thread_manager: &Arc<ThreadManager>,
        auth: Option<CodexAuth>,
    )

作用:账号变化后,尝试刷新插件缓存。因为换账号后,可用插件或推荐插件可能不同。

数据流:进去的是配置管理器、线程管理器和当前认证信息 → 它先更新插件认证模式并清掉推荐缓存,再重新加载最新配置;如果成功,就可能启动远程插件缓存刷新 → 出来没有直接结果,但插件缓存会被清理或刷新。

调用关系:登录成功、浏览器登录完成、退出账号后都会用到它;如果刷新后有效插件变化,它会安排 spawn_effective_plugins_changed_task 做进一步清理和刷新。

调用图:调用 1 个内部函数(load_latest_config);外部调用 4 个(clone, new, clone, warn!)。

AccountRequestProcessor::spawn_effective_plugins_changed_task216–228 ↗
fn spawn_effective_plugins_changed_task(
        thread_manager: Arc<ThreadManager>,
        config_manager: ConfigManager,
    )

作用:启动一个后台任务,处理“实际可用插件变了”这件事。这样主请求不用等所有缓存刷新完成。

数据流:进去的是线程管理器和配置管理器 → 它异步清空插件缓存和技能缓存;如果当前有线程,还会排队做一次尽力而为的 MCP 刷新 → 出来没有返回值,工作在后台继续。

调用关系:maybe_refresh_plugin_caches_for_current_config 在远程插件缓存刷新后可能调用它;它再把刷新请求交给 mcp_refresh::queue_best_effort_refresh。

调用图:调用 1 个内部函数(queue_best_effort_refresh);外部调用 1 个(spawn)。

AccountRequestProcessor::login_v2230–264 ↗
async fn login_v2(
        &self,
        request_id: ConnectionRequestId,
        params: LoginAccountParams,
    ) -> Result<(), JSONRPCErrorError>

作用:按登录方式分派登录请求。它像一个分诊台,看用户选的是 API Key、浏览器、设备码还是外部 token。

数据流:进去的是请求编号和 LoginAccountParams → 它匹配参数类型,分别调用 login_api_key_v2、login_chatgpt_v2、login_chatgpt_device_code_v2 或 login_chatgpt_auth_tokens → 出来是成功的空结果;具体错误会由被调用函数发回客户端。

调用关系:login_account 调用它;它是所有登录方式的总分发点。

调用图:调用 4 个内部函数(login_api_key_v2, login_chatgpt_auth_tokens, login_chatgpt_device_code_v2, login_chatgpt_v2);被 1 处调用(login_account)。

AccountRequestProcessor::external_auth_active_error266–270 ↗
fn external_auth_active_error(&self) -> JSONRPCErrorError

作用:生成一个固定错误:当前外部认证正在使用,不能用普通登录方式覆盖。这样可以避免系统管理的 token 被误改。

数据流:进去的是处理器自身 → 它构造一条 JSON-RPC 错误消息 → 出来是可直接返回给客户端的错误对象。

调用关系:login_api_key_common 和 login_chatgpt_common 在发现外部 ChatGPT 认证已激活时会调用它。

调用图:被 2 处调用(login_api_key_common, login_chatgpt_common)。

AccountRequestProcessor::login_api_key_common272–309 ↗
async fn login_api_key_common(
        &self,
        params: &LoginApiKeyParams,
    ) -> std::result::Result<(), JSONRPCErrorError>

作用:执行 API Key 登录的核心检查和保存。API Key 可以理解为一串访问后端的钥匙。

数据流:进去的是 API Key 参数 → 它先拒绝外部认证冲突和配置禁用的情况,再取消旧登录,然后把 API Key 保存到本地凭据存储,并重新加载认证状态 → 成功时账号状态更新,失败时返回错误。

调用关系:login_api_key_v2 调用它;它会在需要时使用 external_auth_active_error 生成冲突错误。

调用图:调用 1 个内部函数(external_auth_active_error);被 1 处调用(login_api_key_v2);外部调用 2 个(format!, matches!)。

AccountRequestProcessor::login_api_key_v2311–323 ↗
async fn login_api_key_v2(&self, request_id: ConnectionRequestId, params: LoginApiKeyParams)

作用:处理 API Key 登录请求并把结果发回客户端。登录成功后还会广播账号变化。

数据流:进去的是请求编号和 API Key 参数 → 它调用 login_api_key_common,包装成登录响应,然后通过 outgoing 发送结果;如果成功,再发送登录成功和账号更新通知 → 出来没有直接返回给调用者的数据。

调用关系:login_v2 在识别到 API Key 登录时调用它;它成功后会调用 send_login_success_notifications。

调用图:调用 2 个内部函数(login_api_key_common, send_login_success_notifications);被 1 处调用(login_v2)。

AccountRequestProcessor::login_chatgpt_common326–365 ↗
async fn login_chatgpt_common(
        &self,
        codex_streamlined_login: bool,
    ) -> std::result::Result<LoginServerOptions, JSONRPCErrorError>

作用:为 ChatGPT 登录准备共同选项,并做通用校验。浏览器登录和设备码登录都会先走这里。

数据流:进去的是是否使用 streamlined login 的布尔值 → 它检查外部认证冲突、配置是否禁止 ChatGPT 登录,然后根据配置拼出 LoginServerOptions;调试构建下还可用环境变量覆盖登录发行方 → 出来是登录服务器或设备码流程要用的选项。

调用关系:login_chatgpt_response 和 login_chatgpt_device_code_response 都会调用它;它在冲突时调用 external_auth_active_error。

调用图:调用 1 个内部函数(external_auth_active_error);被 2 处调用(login_chatgpt_device_code_response, login_chatgpt_response);外部调用 3 个(new, matches!, var)。

AccountRequestProcessor::login_chatgpt_device_code_start_error367–374 ↗
fn login_chatgpt_device_code_start_error(err: IoError) -> JSONRPCErrorError

作用:把申请设备码时的底层输入输出错误,翻译成客户端能理解的 JSON-RPC 错误。

数据流:进去的是一个 IoError,也就是文件、网络这类输入输出错误 → 它判断是不是“找不到”这类用户可修正问题 → 出来是 invalid_request 或 internal_error。

调用关系:设备码登录开始失败时会用它把错误包装好,再返回给客户端。

调用图:外部调用 3 个(kind, to_string, format!)。

AccountRequestProcessor::login_chatgpt_v2376–383 ↗
async fn login_chatgpt_v2(
        &self,
        request_id: ConnectionRequestId,
        codex_streamlined_login: bool,
    )

作用:处理浏览器 ChatGPT 登录请求,并立刻把登录网址和登录编号发回客户端。

数据流:进去的是请求编号和登录模式参数 → 它调用 login_chatgpt_response 创建登录流程 → 然后通过 outgoing 把结果发回客户端。

调用关系:login_v2 在识别到 ChatGPT 浏览器登录时调用它;它把主要工作交给 login_chatgpt_response。

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

AccountRequestProcessor::login_chatgpt_response385–450 ↗
async fn login_chatgpt_response(
        &self,
        codex_streamlined_login: bool,
    ) -> Result<LoginAccountResponse, JSONRPCErrorError>

作用:启动浏览器 ChatGPT 登录。它会开一个本地登录服务器,等浏览器授权回调回来。

数据流:进去的是是否使用 streamlined login → 它先拿登录选项,再启动登录服务器,生成 login_id,替换掉旧的活跃登录;随后启动后台任务等待登录完成或超时 → 立即出来的是登录响应,包含 login_id 和 auth_url。

调用关系:login_chatgpt_v2 调用它;后台任务完成后会调用 send_chatgpt_login_completion_notifications,并用 ActiveLogin::login_id 确认是否清理当前登录。

调用图:调用 1 个内部函数(login_chatgpt_common);被 1 处调用(login_chatgpt_v2);外部调用 7 个(clone, send_chatgpt_login_completion_notifications, new_v4, clone, format!, spawn, timeout)。

AccountRequestProcessor::login_chatgpt_device_code_v2452–455 ↗
async fn login_chatgpt_device_code_v2(&self, request_id: ConnectionRequestId)

作用:处理设备码 ChatGPT 登录请求。设备码登录通常用于不方便自动打开浏览器的环境。

数据流:进去的是请求编号 → 它调用 login_chatgpt_device_code_response 创建设备码流程 → 然后把验证码、验证网址和登录编号发给客户端。

调用关系:login_v2 在识别到设备码登录时调用它;主要工作由 login_chatgpt_device_code_response 完成。

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

AccountRequestProcessor::login_chatgpt_device_code_response457–523 ↗
async fn login_chatgpt_device_code_response(
        &self,
    ) -> Result<LoginAccountResponse, JSONRPCErrorError>

作用:启动设备码登录流程。用户拿到 user_code 后,去网页输入这个码完成授权。

数据流:进去没有额外参数 → 它准备登录选项,向后端申请设备码,生成 login_id 和取消令牌,替换旧活跃登录;随后开后台任务等待登录完成或取消 → 出来是包含验证网址、用户码和 login_id 的响应。

调用关系:login_chatgpt_device_code_v2 调用它;后台任务结束后会调用 send_chatgpt_login_completion_notifications,并在确认 login_id 匹配后清理 active_login。

调用图:调用 1 个内部函数(login_chatgpt_common);被 1 处调用(login_chatgpt_device_code_v2);外部调用 7 个(clone, new, send_chatgpt_login_completion_notifications, new_v4, clone, select!, spawn)。

AccountRequestProcessor::cancel_login_chatgpt_common525–538 ↗
async fn cancel_login_chatgpt_common(
        &self,
        login_id: Uuid,
    ) -> std::result::Result<(), CancelLoginError>

作用:按登录编号取消当前 ChatGPT 登录。编号不匹配时,它不会乱动别的登录流程。

数据流:进去的是一个 Uuid 格式的 login_id → 它锁住 active_login,比对当前登录编号;匹配就取出并丢弃,不匹配就返回 NotFound → 出来是取消成功或没找到。

调用关系:cancel_login_response 调用它;真正停止登录流程依靠 ActiveLogin 被丢弃时触发的取消动作。

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

AccountRequestProcessor::cancel_login_response540–552 ↗
async fn cancel_login_response(
        &self,
        params: CancelLoginAccountParams,
    ) -> Result<CancelLoginAccountResponse, JSONRPCErrorError>

作用:把客户端传来的登录编号字符串转成内部编号,并返回取消结果。它负责把输入错误和业务结果说清楚。

数据流:进去的是 CancelLoginAccountParams,里面有 login_id 字符串 → 它先解析成 Uuid,解析失败就返回无效请求;解析成功后调用 cancel_login_chatgpt_common → 出来是 Canceled 或 NotFound 状态。

调用关系:cancel_login_account 调用它;它是取消登录请求的主要执行函数。

调用图:调用 1 个内部函数(cancel_login_chatgpt_common);被 1 处调用(cancel_login_account);外部调用 1 个(parse_str)。

AccountRequestProcessor::login_chatgpt_auth_tokens554–571 ↗
async fn login_chatgpt_auth_tokens(
        &self,
        request_id: ConnectionRequestId,
        access_token: String,
        chatgpt_account_id: String,
        chatgpt_plan_type: Option<String>,

作用:处理外部系统直接交给本服务的 ChatGPT token 登录。成功后会通知客户端账号已登录。

数据流:进去的是请求编号、access_token、账号 ID 和可选套餐类型 → 它调用 login_chatgpt_auth_tokens_response 保存这些外部认证信息,再发送请求结果;成功时发送登录成功通知 → 出来没有直接返回值。

调用关系:login_v2 在识别到 chatgptAuthTokens 登录时调用它;成功后调用 send_login_success_notifications。

调用图:调用 2 个内部函数(login_chatgpt_auth_tokens_response, send_login_success_notifications);被 1 处调用(login_v2)。

AccountRequestProcessor::login_chatgpt_auth_tokens_response573–621 ↗
async fn login_chatgpt_auth_tokens_response(
        &self,
        access_token: String,
        chatgpt_account_id: String,
        chatgpt_plan_type: Option<String>,
    ) -> Result<LoginAccountRes

作用:保存外部传入的 ChatGPT 认证 token,并同步相关配置。这个路径用于由宿主应用或外部系统接管登录。

数据流:进去的是 access_token、ChatGPT 账号 ID、可选套餐类型 → 它检查配置是否允许、取消旧登录、校验工作区是否匹配,然后把 token 写入认证存储,重新加载认证,替换云配置加载器,并同步默认数据驻留要求 → 出来是 ChatgptAuthTokens 登录响应或错误。

调用关系:login_chatgpt_auth_tokens 调用它;它会和 ConfigManager 配合,让后续云配置读取使用新的账号认证。

调用图:调用 2 个内部函数(replace_cloud_config_bundle_loader, sync_default_client_residency_requirement);被 1 处调用(login_chatgpt_auth_tokens);外部调用 2 个(format!, matches!)。

AccountRequestProcessor::send_login_success_notifications623–647 ↗
async fn send_login_success_notifications(&self, login_id: Option<Uuid>)

作用:在非浏览器异步登录成功后,通知客户端“登录完成”和“账号更新”。同时刷新插件缓存。

数据流:进去的是可选 login_id → 它先调用 maybe_refresh_plugin_caches_for_current_config,再组装 AccountLoginCompletedNotification 和 AccountUpdatedNotification,依次发给客户端 → 出来没有返回值,但客户端会收到通知。

调用关系:login_api_key_v2 和 login_chatgpt_auth_tokens 成功后调用它;账号更新通知内容由 current_account_updated_notification 生成。

调用图:调用 1 个内部函数(current_account_updated_notification);被 2 处调用(login_api_key_v2, login_chatgpt_auth_tokens);外部调用 3 个(maybe_refresh_plugin_caches_for_current_config, AccountLoginCompleted, AccountUpdated)。

AccountRequestProcessor::send_chatgpt_login_completion_notifications649–691 ↗
async fn send_chatgpt_login_completion_notifications(
        outgoing: &OutgoingMessageSender,
        config_manager: ConfigManager,
        thread_manager: Arc<ThreadManager>,
        chatgpt_base_

作用:浏览器或设备码 ChatGPT 登录后台结束后,用它统一发送完成通知。成功时还会重新加载账号并刷新缓存。

数据流:进去的是消息发送器、配置管理器、线程管理器、后端地址、login_id、成功标记和错误信息 → 它先发送“登录已完成”;如果成功,再重新加载认证、更新云配置加载器、同步数据驻留要求、刷新插件缓存,并发送账号更新通知 → 出来没有返回值。

调用关系:login_chatgpt_response 和 login_chatgpt_device_code_response 启动的后台任务会调用它;它负责把异步登录结果带回客户端。

调用图:调用 3 个内部函数(replace_cloud_config_bundle_loader, sync_default_client_residency_requirement, send_server_notification);外部调用 4 个(maybe_refresh_plugin_caches_for_current_config, AccountLoginCompleted, AccountUpdated, to_string)。

AccountRequestProcessor::logout_common693–722 ↗
async fn logout_common(&self) -> std::result::Result<Option<AuthMode>, JSONRPCErrorError>

作用:执行退出账号的核心工作。它会取消未完成登录、撤销当前认证,并刷新依赖账号的插件缓存。

数据流:进去的是处理器自身 → 它先清掉 active_login,再调用 AuthManager 做带撤销的退出;成功后刷新插件缓存,并读取退出后的认证模式 → 出来是当前认证模式,通常是没有账号,或错误。

调用关系:logout_v2 调用它;它把退出后的缓存刷新交给 maybe_refresh_plugin_caches_for_current_config。

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

AccountRequestProcessor::logout_v2724–745 ↗
async fn logout_v2(&self, request_id: ConnectionRequestId) -> Result<(), JSONRPCErrorError>

作用:处理退出请求,发送请求结果,并在账号确实变化时通知客户端。

数据流:进去的是请求编号 → 它调用 logout_common,基于结果准备 AccountUpdatedNotification,然后发送 LogoutAccountResponse;如果有账号更新内容,再发服务器通知 → 出来是空成功或错误。

调用关系:logout_account 调用它;它把真正退出交给 logout_common,把消息发回客户端。

调用图:调用 1 个内部函数(logout_common);被 1 处调用(logout_account);外部调用 1 个(AccountUpdated)。

AccountRequestProcessor::refresh_token_if_requested747–760 ↗
async fn refresh_token_if_requested(&self, do_refresh: bool) -> RefreshTokenRequestOutcome

作用:按需刷新登录 token。token 可以理解为短期通行证,过期前后需要更新。

数据流:进去的是 do_refresh 布尔值 → 如果当前是外部 ChatGPT 认证就不刷新;如果要求刷新,就调用 AuthManager 刷新 token,并区分临时失败和永久失败 → 出来是刷新结果类别。

调用关系:get_auth_status_response 和 get_account_response 在查询前会调用它,尽量让客户端看到较新的认证状态。

调用图:被 2 处调用(get_account_response, get_auth_status_response);外部调用 1 个(warn!)。

AccountRequestProcessor::get_auth_status_response762–830 ↗
async fn get_auth_status_response(
        &self,
        params: GetAuthStatusParams,
    ) -> Result<GetAuthStatusResponse, JSONRPCErrorError>

作用:生成认证状态响应。它回答“现在需不需要登录、用什么方式登录、能不能给 token”。

数据流:进去的是 GetAuthStatusParams → 它按需刷新 token,查看当前模型提供方是否要求 OpenAI 认证;如果需要认证,就读取当前 auth,决定是否报告认证方式和是否带回 token → 出来是 GetAuthStatusResponse。

调用关系:get_auth_status 调用它;它会先调用 refresh_token_if_requested,再读取 AuthManager 和配置来组织答案。

调用图:调用 1 个内部函数(refresh_token_if_requested);被 1 处调用(get_auth_status);外部调用 2 个(matches!, warn!)。

AccountRequestProcessor::get_account_response832–854 ↗
async fn get_account_response(
        &self,
        params: GetAccountParams,
    ) -> Result<GetAccountResponse, JSONRPCErrorError>

作用:生成当前账号信息响应。它把模型提供方看到的账号状态转成客户端协议里的格式。

数据流:进去的是 GetAccountParams → 它按需刷新 token,然后创建模型提供方,读取 account_state,把内部账号对象转换成客户端 Account → 出来是 GetAccountResponse 或无效请求错误。

调用关系:get_account 调用它;它会调用 refresh_token_if_requested,并依赖模型提供方判断账号状态。

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

AccountRequestProcessor::get_account_rate_limits_response856–917 ↗
async fn get_account_rate_limits_response(
        &self,
    ) -> Result<GetAccountRateLimitsResponse, JSONRPCErrorError>

作用:向后端查询当前账号的使用限额。只有使用 Codex/ChatGPT 后端认证的账号才能查。

数据流:进去没有额外参数 → 它先确认已登录,并确认认证能访问 Codex 后端;然后用认证创建 BackendClient,获取限额和重置额度信息,整理成默认限额和按 limit_id 分组的限额 → 出来是 GetAccountRateLimitsResponse 或错误。

调用关系:get_account_rate_limits 调用它;它通过 BackendClient::from_auth 建立后端客户端,再访问后端接口。

调用图:被 1 处调用(get_account_rate_limits);外部调用 1 个(from_auth)。

AccountRequestProcessor::get_account_token_usage_response919–944 ↗
async fn get_account_token_usage_response(
        &self,
    ) -> Result<GetAccountTokenUsageResponse, JSONRPCErrorError>

作用:向后端查询当前账号的 token 使用概况。它还给请求设置了超时,避免客户端一直等。

数据流:进去没有额外参数 → 它确认已登录且能访问 Codex 后端,创建 BackendClient,在限定时间内获取 token usage profile,再调用 account_token_usage_response 转成协议响应 → 出来是 GetAccountTokenUsageResponse 或错误。

调用关系:get_account_token_usage 调用它;它负责访问后端,数据格式转换交给 account_token_usage_response。

调用图:被 1 处调用(get_account_token_usage);外部调用 3 个(from_auth, account_token_usage_response, timeout)。

AccountRequestProcessor::account_token_usage_response946–966 ↗
fn account_token_usage_response(profile: TokenUsageProfile) -> GetAccountTokenUsageResponse

作用:把后端返回的 token 用量资料,转换成客户端认识的响应格式。它只做字段搬运和重命名。

数据流:进去的是 TokenUsageProfile → 它取出总用量、峰值日用量、连续使用天数、每日桶等统计字段,装进 AccountTokenUsageSummary 和每日列表 → 出来是 GetAccountTokenUsageResponse。

调用关系:get_account_token_usage_response 会调用它处理真实后端数据;测试 tests::account_token_usage_response_maps_profile_stats_and_daily_buckets 也调用它验证映射是否正确。

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

AccountRequestProcessor::send_add_credits_nudge_email_response968–975 ↗
async fn send_add_credits_nudge_email_response(
        &self,
        params: SendAddCreditsNudgeEmailParams,
    ) -> Result<SendAddCreditsNudgeEmailResponse, JSONRPCErrorError>

作用:包装发送加额度提醒邮件的结果。它把内部状态装进客户端响应对象。

数据流:进去的是 SendAddCreditsNudgeEmailParams → 它调用 send_add_credits_nudge_email_inner 得到发送状态 → 出来是 SendAddCreditsNudgeEmailResponse 或错误。

调用关系:send_add_credits_nudge_email 调用它;真正访问后端的工作由 send_add_credits_nudge_email_inner 完成。

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

AccountRequestProcessor::send_add_credits_nudge_email_inner977–1008 ↗
async fn send_add_credits_nudge_email_inner(
        &self,
        params: SendAddCreditsNudgeEmailParams,
    ) -> Result<AddCreditsNudgeEmailStatus, JSONRPCErrorError>

作用:真正请求后端发送“请加额度”的提醒邮件。它会把 429 这类冷却期错误转成正常状态。

数据流:进去的是提醒参数 → 它检查账号已登录且是 ChatGPT/Codex 后端认证,创建 BackendClient,把前端的额度类型转换成后端类型,然后调用后端发送邮件;成功返回 Sent,遇到 429 返回 CooldownActive,其他错误返回内部错误。

调用关系:send_add_credits_nudge_email_response 调用它;它会用 backend_credit_type 做类型转换,再把请求交给后端客户端。

调用图:被 1 处调用(send_add_credits_nudge_email_response);外部调用 3 个(from_auth, backend_credit_type, format!)。

AccountRequestProcessor::backend_credit_type1010–1015 ↗
fn backend_credit_type(value: AddCreditsNudgeCreditType) -> BackendAddCreditsNudgeCreditType

作用:把客户端协议里的额度类型,翻译成后端客户端使用的额度类型。它像一个小字典,保证两边枚举能对上。

数据流:进去的是 AddCreditsNudgeCreditType,可能是 Credits 或 UsageLimit → 它按同名含义映射到 BackendAddCreditsNudgeCreditType → 出来是后端能识别的类型。

调用关系:send_add_credits_nudge_email_inner 在调用后端发送邮件前会用它转换参数。

tests::account_token_usage_response_maps_profile_stats_and_daily_buckets1026–1057 ↗
fn account_token_usage_response_maps_profile_stats_and_daily_buckets()

作用:测试 token 用量转换函数是否把统计字段和每日用量都正确搬过去。它防止以后改代码时不小心丢字段。

数据流:进去的是测试里手写的 TokenUsageProfile 样例 → 它调用 AccountRequestProcessor::account_token_usage_response 转换 → 然后用断言比较实际响应和预期响应是否完全一致。

调用关系:这是测试代码,只在测试运行时执行;它专门验证 account_token_usage_response 的数据映射行为。

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

登录流程编排

这些文件定义 login crate 接口,并实现交互式浏览器和 device-code ChatGPT 登录路径,包括引导期间的无头 UX。

login/src/lib.rs源码 ↗
orchestrationcross-cutting

可以把这个文件理解成一家店的前台目录:真正干活的人在后面的房间里,比如 auth 处理账号和令牌,device_code_auth 处理设备码登录,server 处理本地网页登录服务器,token_data 保存令牌相关数据。这个文件本身几乎不写业务步骤,而是决定哪些东西对外公开、哪些只是内部使用。比如 pkce 和 server 模块是内部零件,但 LoginServer、run_login_server 这些名字会被公开给外部调用。这样做的好处是,别的项目不用知道登录系统内部拆成了多少文件,只要从这个库入口拿需要的类型和函数就行。没有它,调用方就得到处找具体模块路径,代码会更乱,也更容易依赖到不该碰的内部细节。

login/src/device_code_auth.rs源码 ↗
orchestrationlogin flow

这个文件解决的是命令行登录不方便的问题。程序先向服务器要一个一次性用户码和轮询间隔,然后把网址和验证码打印出来,让用户打开浏览器登录。之后命令行会像“隔几秒去前台问一次办好了吗”一样,不断询问服务器是否已经批准。批准后,服务器返回授权码和 PKCE 信息;PKCE 可以理解成一对临时暗号,用来证明这次换令牌的人就是刚才发起登录的人。接着它把授权码换成真正的登录令牌,检查是否属于允许的工作区,最后把令牌保存到本机。这里还特别处理了服务器不支持设备码登录、等待超过 15 分钟、验证码钓鱼提醒等情况。

函数细节7
deserialize_interval46–52 ↗
fn deserialize_interval(deserializer: D) -> Result<u64, D::Error>

作用:把服务器返回的轮询间隔读成数字。服务器给的间隔可能是字符串,比如“5”,这个函数把它变成程序能用的秒数。

数据流:进去的是 JSON 解析器正在读到的一个字段 → 它先按字符串读出来,去掉前后空格,再转成 u64 这种非负整数 → 出来的是秒数;如果内容不是数字,就返回解析错误。

调用关系:它不是被登录流程直接手动调用,而是挂在 UserCodeResp 的 interval 字段上。request_user_code 解析服务器响应时,serde 这个 JSON 解析工具会自动用它来读 interval。

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

request_user_code62–96 ↗
async fn request_user_code(
    client: &reqwest::Client,
    auth_base_url: &str,
    client_id: &str,
) -> std::io::Result<UserCodeResp>

作用:向登录服务器申请一个给用户看的设备验证码。没有这一步,命令行就不知道该让用户打开哪个页面、输入哪个码。

数据流:进去的是 HTTP 客户端、认证服务器地址和客户端 ID → 它拼出 /deviceauth/usercode 地址,发送 JSON 请求 → 如果服务器成功,就把返回内容解析成 user_code、device_auth_id 和 interval;如果接口不存在,会给出“这个服务器没开设备码登录”的明确错误。

调用关系:它由 request_device_code 调用,是整个设备码登录的第一步。它自己把网络请求发出去,并把服务器原始回答整理成后续流程能使用的 UserCodeResp。

调用图:被 1 处调用(request_device_code);外部调用 6 个(post, new, other, format!, from_str, to_string)。

poll_for_token99–146 ↗
async fn poll_for_token(
    client: &reqwest::Client,
    auth_base_url: &str,
    device_auth_id: &str,
    user_code: &str,
    interval: u64,
) -> std::io::Result<CodeSuccessResp>

作用:反复询问服务器:用户是否已经在浏览器里完成登录。它负责等待批准,但最多等 15 分钟,避免程序一直卡住。

数据流:进去的是 HTTP 客户端、认证服务器地址、设备认证 ID、用户码和每次等待的秒数 → 它循环向 /deviceauth/token 发送请求 → 成功时返回授权码和 PKCE 暗号;如果还没批准,就睡一会儿再问;如果超时或服务器返回异常状态,就返回错误。

调用关系:它由 complete_device_code_login 调用,发生在用户已经看到验证码之后。它只负责“等服务器发放授权码”,拿到结果后把后续换令牌的工作交回 complete_device_code_login。

调用图:被 1 处调用(complete_device_code_login);外部调用 7 个(from_secs, now, post, other, format!, to_string, sleep)。

print_device_code_prompt148–157 ↗
fn print_device_code_prompt(verification_url: &str, code: &str)

作用:把登录提示打印到终端,让用户知道要打开哪个网址、输入哪个一次性验证码。它还提醒用户不要把设备码告诉别人,因为这类码常被钓鱼利用。

数据流:进去的是验证网址和用户码 → 它读取当前 Codex 版本号,拼成带颜色的终端提示文字 → 出来的是显示在屏幕上的操作说明;它不返回业务数据。

调用关系:它由 run_device_code_login 调用,夹在“申请到设备码”和“等待登录完成”之间。它是用户真正看到并执行登录动作的那一步。

调用图:被 1 处调用(run_device_code_login);外部调用 2 个(env!, println!)。

request_device_code159–171 ↗
async fn request_device_code(opts: &ServerOptions) -> std::io::Result<DeviceCode>

作用:准备设备码登录所需的全部公开信息。调用者拿到它后,就可以把网址和验证码展示给用户。

数据流:进去的是 ServerOptions,里面有服务器地址、客户端 ID 等配置 → 它创建支持自定义证书的 HTTP 客户端,整理服务器基础地址,调用 request_user_code 申请验证码 → 出来的是 DeviceCode,包含给用户看的网址、用户码,以及后面轮询要用的内部 ID 和间隔。

调用关系:它由 run_device_code_login 调用,是登录编排的开头。它把底层的 request_user_code 包起来,让上层不用关心具体接口路径和 HTTP 细节。

调用图:调用 1 个内部函数(request_user_code);被 1 处调用(run_device_code_login);外部调用 3 个(builder, build_reqwest_client_with_custom_ca, format!)。

complete_device_code_login173–223 ↗
async fn complete_device_code_login(
    opts: ServerOptions,
    device_code: DeviceCode,
) -> std::io::Result<()>

作用:在用户输入设备码后,完成剩下的登录收尾:等授权、换令牌、检查工作区、保存凭证。它是设备码登录真正落地成“本机已登录”的地方。

数据流:进去的是服务器配置和前面拿到的 DeviceCode → 它先调用 poll_for_token 等待授权码,再用授权码和 PKCE 信息调用 exchange_code_for_tokens 换取登录令牌 → 然后用 ensure_workspace_allowed 检查账号是否属于允许的工作区,最后调用 persist_tokens_async 把令牌保存到本机;失败时返回相应错误。

调用关系:它由 run_device_code_login 在打印提示之后调用。它把等待、换令牌、权限检查和保存凭证串起来,并把具体的令牌交换、工作区检查、持久化保存交给 server 模块里的函数。

调用图:调用 4 个内部函数(poll_for_token, ensure_workspace_allowed, exchange_code_for_tokens, persist_tokens_async);被 1 处调用(run_device_code_login);外部调用 4 个(builder, new, build_reqwest_client_with_custom_ca, format!)。

run_device_code_login225–229 ↗
async fn run_device_code_login(opts: ServerOptions) -> std::io::Result<()>

作用:这是设备码登录的一键流程入口。外部只要调用它,就能完成从申请验证码、提示用户,到保存登录令牌的完整过程。

数据流:进去的是 ServerOptions 配置 → 它先调用 request_device_code 拿到网址和验证码,再调用 print_device_code_prompt 显示给用户,最后调用 complete_device_code_login 等待并保存登录结果 → 成功时本机获得可用登录凭证,失败时返回错误。

调用关系:它位于这个文件的最上层流程位置,把三个主要步骤串成一条线:申请码、给用户看、完成登录。其它模块如果要启动设备码登录,通常会从这里进入。

调用图:调用 3 个内部函数(complete_device_code_login, print_device_code_prompt, request_device_code)。

login/src/server.rs源码 ↗
orchestrationinteractive login callback

命令行登录有个麻烦:用户在浏览器里点完登录,结果要回到命令行里。这个文件就像临时开了一个“收件窗口”,只监听本机 localhost 上的端口,等浏览器把授权码送回来。它会生成防伪用的 state 和 PKCE(OAuth 里防止授权码被偷用的一套验证码),拼出登录网址,必要时打开浏览器。收到回调后,它检查 state 是否匹配,拿授权码去换 token,再按 workspace 限制做校验,最后把 token 和可选 API key 存到本地。它还特别注意一件事:错误信息要够排查问题,但日志里不能泄露 code、token、密码这类秘密,所以有一整套“打码”函数。成功或失败时,它会给浏览器返回对应页面,并在流程结束后关掉这个临时服务器。

函数细节41
ServerOptions::new78–97 ↗
fn new(
        codex_home: PathBuf,
        client_id: String,
        forced_chatgpt_workspace_id: Option<Vec<String>>,
        cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
        aut

作用:创建一份登录服务器的默认配置。调用者只要给出必要信息,它会补上默认登录地址、默认端口、是否打开浏览器等常用设置。

数据流:输入 Codex 主目录、客户端 ID、workspace 限制和凭据保存方式 → 填入默认 issuer、端口 1455、打开浏览器等值 → 输出一个完整的 ServerOptions 配置对象。

调用关系:这是启动登录流程前的准备步骤。login_with_chatgpt、设备码登录 fallback、测试代码等都会先用它做出配置,再交给后续登录流程使用。

调用图:被 8 处调用(login_with_chatgpt, run_login_with_device_code, run_login_with_device_code_fallback_to_browser, file_reads_reject_named_pipes, device_code_login_integration_handles_error_payload, device_code_login_integration_persists_without_api_key_on_exchange_failure, server_opts, falls_back_to_registered_fallback_port_when_default_port_is_in_use)。

LoginServer::block_until_done110–114 ↗
async fn block_until_done(self) -> io::Result<()>

作用:等待这个临时登录服务器跑完。外部代码用它来知道用户登录到底成功、失败,还是中途取消了。

数据流:输入一个正在运行的 LoginServer → 等待后台任务结束 → 返回成功或 io::Error;如果后台任务崩了,会把崩溃信息变成普通错误。

调用关系:run_login_server 启动服务器后会返回 LoginServer。调用方通常拿到它后打开浏览器,然后用这个函数等整个回调流程收尾。

LoginServer::cancel117–119 ↗
fn cancel(&self)

作用:请求停止正在等待登录回调的服务器。比如用户关掉登录流程时,可以用它让服务器别再等了。

数据流:输入当前 LoginServer → 找到里面的 ShutdownHandle → 发出 shutdown 通知;没有直接返回数据,只改变后台任务的运行状态。

调用关系:它只是一个方便入口,真正发通知的活交给 ShutdownHandle::shutdown。后台的 run_login_server 循环收到通知后会退出。

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

LoginServer::cancel_handle122–124 ↗
fn cancel_handle(&self) -> ShutdownHandle

作用:取出一个可以复制、可以传给别处的取消句柄。这样其他任务也能在需要时停止登录服务器。

数据流:输入当前 LoginServer → 复制内部的 ShutdownHandle → 输出这个可独立保存的取消句柄。

调用关系:它让取消动作不必绑定在 LoginServer 本体上。别的异步任务或界面逻辑拿到句柄后,稍后可以调用 ShutdownHandle::shutdown。

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

ShutdownHandle::shutdown135–137 ↗
fn shutdown(&self)

作用:发出“该停了”的信号。它像按门铃一样通知等待中的服务器循环退出。

数据流:输入 ShutdownHandle → 对内部的 Notify 发通知 → 等待中的登录循环被唤醒并开始结束流程。

调用关系:LoginServer::cancel 会调用它。run_login_server 创建的后台任务一直监听这个通知,一旦收到就返回“登录未完成”的错误。

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

run_login_server141–251 ↗
fn run_login_server(opts: ServerOptions) -> io::Result<LoginServer>

作用:启动整个本机 OAuth 回调服务器,并给调用者一个浏览器登录网址。它是这个文件最核心的“开机按钮”。

数据流:输入 ServerOptions → 生成 PKCE 和 state,绑定端口,拼授权 URL,可选打开浏览器,再启动接收 HTTP 请求的线程和异步处理任务 → 输出 LoginServer,里面带 auth_url、实际端口、等待句柄和取消句柄。

调用关系:它把很多零件串起来:用 generate_pkce 和 generate_state 准备安全参数,用 bind_server 占端口,用 build_authorize_url 拼登录地址,收到请求后交给 process_request,结束时解除服务器阻塞。

调用图:调用 3 个内部函数(generate_pkce, bind_server, build_authorize_url);外部调用 8 个(new, new, format!, spawn, select!, spawn, new, open)。

process_request264–439 ↗
async fn process_request(
    url_raw: &str,
    opts: &ServerOptions,
    redirect_uri: &str,
    pkce: &PkceCodes,
    actual_port: u16,
    state: &str,
) -> HandledRequest

作用:处理浏览器打到本机服务器的每一个请求。它决定这是登录回调、成功页面、取消请求,还是无效地址。

数据流:输入请求 URL、服务器配置、redirect_uri、PKCE、端口和 state → 解析路径和查询参数;回调时校验 state、处理 OAuth 错误、用 code 换 token、检查 workspace、保存凭据、生成成功跳转;成功页或取消页则直接返回终止结果 → 输出 HandledRequest,告诉上层要回什么 HTTP 响应、是否结束服务器。

调用关系:run_login_server 的请求循环会把每个浏览器请求交给它。它再按需要调用 exchange_code_for_tokens、obtain_api_key、persist_tokens_async、ensure_workspace_allowed、compose_success_url 和 login_error_response。

调用图:调用 7 个内部函数(compose_success_url, ensure_workspace_allowed, exchange_code_for_tokens, login_error_response, oauth_callback_error_message, obtain_api_key, persist_tokens_async);外部调用 15 个(from_bytes, new, new, from_string, eprintln!, error!, format!, include_str!, info!, RedirectWithHeader (+5 more))。

send_response_with_disconnect450–483 ↗
fn send_response_with_disconnect(
    req: Request,
    mut headers: Vec<Header>,
    body: Vec<u8>,
) -> io::Result<()>

作用:手写 HTTP 响应,并强制告诉浏览器连接要关闭。这样可以避免旧连接卡住,影响下一次登录。

数据流:输入 tiny_http 的 Request、响应头和响应正文 → 取出底层写入器,写状态行、过滤旧 Connection 头、加上 Connection: close 和 Content-Length,再写正文 → 输出写入结果,并让连接从服务端关闭。

调用关系:run_login_server 在返回成功页、失败页或取消页这类“流程要结束”的响应时使用它,而不是普通 req.respond,因为 tiny_http 默认会过滤 Connection 头。

调用图:外部调用 5 个(from_bytes, into_writer, format!, StatusCode, write!)。

build_authorize_url485–521 ↗
fn build_authorize_url(
    issuer: &str,
    client_id: &str,
    redirect_uri: &str,
    pkce: &PkceCodes,
    state: &str,
    forced_chatgpt_workspace_ids: Option<&[String]>,
) -> String

作用:拼出用户要在浏览器打开的 OAuth 授权地址。这个地址里包含客户端 ID、回调地址、防伪参数和所需权限。

数据流:输入 issuer、client_id、redirect_uri、PKCE、state 和可选 workspace ID → 把它们编码成 URL 查询参数 → 输出完整的 authorize URL 字符串。

调用关系:run_login_server 在启动时调用它生成 auth_url。浏览器打开这个 URL 后,用户登录完成才会回到本机回调地址。

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

generate_state523–527 ↗
fn generate_state() -> String

作用:生成一段随机 state,用来防止别人伪造登录回调。可以把它理解成登录流程的一次性暗号。

数据流:不接收业务输入 → 随机生成 32 字节,再用 URL 安全的 base64 编码 → 输出一段适合放进网址的字符串。

调用关系:run_login_server 在没有强制指定 state 时会调用它。process_request 之后会检查浏览器带回来的 state 是否和它一致。

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

send_cancel_request529–544 ↗
fn send_cancel_request(port: u16) -> io::Result<()>

作用:向本机某个端口发送 /cancel 请求,用来叫停可能还占着端口的旧登录服务器。

数据流:输入端口号 → 连接 127.0.0.1:端口,写入一个 HTTP GET /cancel 请求,并短暂读取响应 → 成功返回空结果,失败返回网络或解析错误。

调用关系:bind_server 发现默认端口被占用时会调用它,尝试先让旧登录流程自己退出,再重新绑定端口。

调用图:被 1 处调用(bind_server);外部调用 3 个(from_secs, connect_timeout, format!)。

bind_server546–604 ↗
fn bind_server(port: u16) -> io::Result<Server>

作用:为登录回调服务器占用本机端口。它会处理端口被旧进程占用的情况,并在默认端口不可用时尝试备用端口。

数据流:输入希望使用的端口 → 尝试绑定 127.0.0.1:端口;如果端口占用,先发 cancel 请求,再多次等待重试;默认端口仍失败时切到 1457 → 输出 tiny_http Server,或返回端口占用等错误。

调用关系:run_login_server 启动时必须先调用它。它内部会在必要时调用 send_cancel_request 来清理旧的登录服务器。

调用图:调用 1 个内部函数(send_cancel_request);被 1 处调用(run_login_server);外部调用 8 个(from_millis, http, new, other, eprintln!, format!, sleep, warn!)。

TokenEndpointErrorDetail::fmt621–623 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:把 token 接口错误详情变成一段可显示的文字。这样错误对象在格式化时能直接显示用户该看的信息。

数据流:输入 TokenEndpointErrorDetail 和格式化器 → 取 display_message 写进去 → 输出格式化结果。

调用关系:exchange_code_for_tokens 解析 token 接口错误后,会把这个结构放进错误文本里;这个 fmt 决定最终显示哪句话。

redact_sensitive_query_value642–651 ↗
fn redact_sensitive_query_value(key: &str, value: &str) -> String

作用:判断 URL 查询参数是不是敏感信息,如果是就打码。比如 code、token、api_key 都不能原样进日志。

数据流:输入参数名和参数值 → 参数名和敏感名单做不区分大小写比较 → 敏感则输出 <redacted>,否则原样输出值。

调用关系:redact_sensitive_url_parts 会逐个查询参数调用它。相关测试会确认只打码已知敏感字段,不误伤普通字段。

redact_sensitive_url_parts657–687 ↗
fn redact_sensitive_url_parts(url: &mut url::Url)

作用:清理一个 URL 里容易泄露秘密的部分,同时保留主机和路径,方便排查问题。

数据流:输入可修改的 URL → 删除用户名、密码和片段 fragment;逐个检查查询参数,把敏感参数值替换成 <redacted> → 原 URL 被就地改成安全版本。

调用关系:redact_sensitive_error_url 和 sanitize_url_for_logging 都会调用它。测试也直接验证它能保留 URL 大体形状但去掉秘密。

调用图:被 3 处调用(redact_sensitive_error_url, sanitize_url_for_logging, redact_sensitive_url_parts_preserves_safe_url_shape);外部调用 7 个(new, query_pairs, set_fragment, set_password, set_query, set_username, new)。

redact_sensitive_error_url690–695 ↗
fn redact_sensitive_error_url(mut err: reqwest::Error) -> reqwest::Error

作用:把 reqwest 网络错误里附带的 URL 做打码。这样记录网络错误时不会把 token 或授权码写进日志。

数据流:输入 reqwest::Error → 如果错误里带 URL,就取出来交给 redact_sensitive_url_parts 清理 → 输出同一个错误对象,但 URL 已安全化。

调用关系:exchange_code_for_tokens 在 token 请求发送失败时调用它,然后再把错误写日志和返回给上层。

调用图:调用 1 个内部函数(redact_sensitive_url_parts);被 1 处调用(exchange_code_for_tokens);外部调用 1 个(url_mut)。

sanitize_url_for_logging701–709 ↗
fn sanitize_url_for_logging(url: &str) -> String

作用:把一段外部传进来的 URL 字符串变成适合写日志的安全版本。尤其用于 issuer 这类可能被自定义的地址。

数据流:输入 URL 字符串 → 能解析就打码敏感部分并转回字符串;不能解析就输出 <invalid-url> → 返回安全日志字符串。

调用关系:exchange_code_for_tokens 写开始交换 token 的日志时会用它处理 issuer 和 token_endpoint。测试会检查用户名、密码和 token 参数会被隐藏。

调用图:调用 1 个内部函数(redact_sensitive_url_parts);被 1 处调用(sanitize_url_for_logging_redacts_sensitive_issuer_parts);外部调用 1 个(parse)。

exchange_code_for_tokens716–787 ↗
async fn exchange_code_for_tokens(
    issuer: &str,
    client_id: &str,
    redirect_uri: &str,
    pkce: &PkceCodes,
    code: &str,
) -> io::Result<ExchangedTokens>

作用:把浏览器回调带来的授权码换成真正可用的登录 token。这是 OAuth 登录从“用户同意了”变成“本机拿到凭据”的关键一步。

数据流:输入 issuer、client_id、redirect_uri、PKCE 和 code → 构造 HTTP POST 请求发到 /oauth/token;成功时解析 id_token、access_token、refresh_token;失败时解析错误、打码 URL、保留可显示的后端错误 → 输出 ExchangedTokens 或 io::Error。

调用关系:process_request 在收到 /auth/callback 且校验 state 后调用它。设备码登录完成函数也会复用它;它内部会调用 parse_token_endpoint_error 和 redact_sensitive_error_url。

调用图:调用 2 个内部函数(parse_token_endpoint_error, redact_sensitive_error_url);被 2 处调用(complete_device_code_login, process_request);外部调用 7 个(builder, other, build_reqwest_client_with_custom_ca, error!, format!, info!, warn!)。

persist_tokens_async790–832 ↗
async fn persist_tokens_async(
    codex_home: &Path,
    api_key: Option<String>,
    id_token: String,
    access_token: String,
    refresh_token: String,
    auth_credentials_store_mode: AuthCrede

作用:把登录拿到的凭据保存到本地认证存储里。这样用户下次运行命令行时不必重新登录。

数据流:输入 Codex 主目录、可选 API key、id/access/refresh token、保存模式和钥匙串后端类型 → 在线程池中解析 id_token 的声明,补 account_id,组装 AuthDotJson,再调用 save_auth 保存 → 输出保存成功或失败的 io::Result。

调用关系:process_request 成功换到 token 后调用它。设备码登录完成流程也会调用它;它把同步保存逻辑放进 spawn_blocking,避免堵住异步运行时。

调用图:被 2 处调用(complete_device_code_login, process_request);外部调用 2 个(to_path_buf, spawn_blocking)。

compose_success_url834–889 ↗
fn compose_success_url(
    port: u16,
    issuer: &str,
    id_token: &str,
    access_token: &str,
    codex_streamlined_login: bool,
) -> String

作用:生成登录成功后浏览器要跳到的本机成功页地址。这个地址里带着前端成功页需要展示或继续处理的信息。

数据流:输入端口、issuer、id_token、access_token 和是否使用新版简化登录页 → 从 JWT 里读取组织、项目、是否需要设置、套餐类型等声明,选择平台网址,拼成 /success 查询参数 → 输出成功页 URL。

调用关系:process_request 保存凭据成功后调用它,然后把浏览器重定向到这个地址。相关测试检查是否按开关加入 codex_streamlined_login 参数。

调用图:调用 1 个内部函数(jwt_auth_claims);被 3 处调用(process_request, compose_success_url_includes_streamlined_success_when_requested, compose_success_url_omits_streamlined_success_by_default);外部调用 2 个(format!, vec!)。

jwt_auth_claims891–920 ↗
fn jwt_auth_claims(jwt: &str) -> serde_json::Map<String, serde_json::Value>

作用:从 JWT token 里取出 OpenAI 认证相关的声明。JWT 可以理解成三段拼起来的签名令牌,这里只读取中间的 JSON 载荷。

数据流:输入 JWT 字符串 → 按点号拆成三段,解码第二段 base64url,解析 JSON,并取出 https://api.openai.com/auth 这个对象 → 输出声明 map;格式错误时输出空 map 并打印错误。

调用关系:compose_success_url 用它读取组织和套餐等信息,ensure_workspace_allowed 用它读取 chatgpt_account_id。

调用图:被 2 处调用(compose_success_url, ensure_workspace_allowed);外部调用 2 个(eprintln!, new)。

ensure_workspace_allowed923–937 ↗
fn ensure_workspace_allowed(
    expected: Option<&[String]>,
    id_token: &str,
) -> Result<(), String>

作用:检查 id_token 是否属于允许的 ChatGPT workspace。它用来防止用户登录到不被当前配置允许的工作区。

数据流:输入可选的期望 workspace ID 列表和 id_token → 没有限制就直接通过;有限制就从 JWT 声明里取 chatgpt_account_id,再比较是否在列表中 → 输出 Ok 或错误说明。

调用关系:process_request 和设备码登录完成流程都会在保存凭据前调用它。它把最终比较动作交给 ensure_workspace_account_allowed。

调用图:调用 2 个内部函数(ensure_workspace_account_allowed, jwt_auth_claims);被 2 处调用(complete_device_code_login, process_request)。

ensure_workspace_account_allowed942–958 ↗
fn ensure_workspace_account_allowed(
    expected: Option<&[String]>,
    actual: &str,
) -> Result<(), String>

作用:检查一个已知的 ChatGPT account ID 是否在允许列表里。它适合没有 id_token、但已经从别处拿到账户 ID 的登录方式。

数据流:输入可选期望 workspace ID 列表和实际 account ID → 没有限制则通过;有列表则查实际 ID 是否匹配其中之一 → 输出 Ok 或包含允许 ID 的错误信息。

调用关系:ensure_workspace_allowed 会调用它。个人访问令牌 PAT 登录的 workspace 检查也会直接使用它,因为 PAT 流程可能从 /whoami 得到账户 ID。

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

login_error_response961–977 ↗
fn login_error_response(
    message: &str,
    kind: io::ErrorKind,
    error_code: Option<&str>,
    error_description: Option<&str>,
) -> HandledRequest

作用:生成一个会结束登录流程的失败响应。它既给浏览器一页好看的错误说明,也把错误返回给命令行调用方。

数据流:输入错误消息、io 错误类型、可选错误码和错误描述 → 设置 HTML Content-Type,调用 render_login_error_page 生成页面 → 输出 ResponseAndExit,里面带浏览器正文和命令行错误结果。

调用关系:process_request 遇到 state 不对、OAuth 返回错误、缺授权码、换 token 失败、保存失败等情况时会调用它。

调用图:调用 1 个内部函数(render_login_error_page);被 1 处调用(process_request);外部调用 3 个(from_bytes, new, new)。

is_missing_codex_entitlement_error980–987 ↗
fn is_missing_codex_entitlement_error(error_code: &str, error_description: Option<&str>) -> bool

作用:判断 OAuth 错误是不是“这个 workspace 没开 Codex 权限”。这种错误要给用户更明确的提示。

数据流:输入 OAuth 错误码和可选描述 → 检查错误码是否是 access_denied,描述里是否包含 missing_codex_entitlement → 输出 true 或 false。

调用关系:oauth_callback_error_message 用它决定命令行错误文案,render_login_error_page 用它决定浏览器错误页的专门文案。

调用图:被 2 处调用(oauth_callback_error_message, render_login_error_page)。

oauth_callback_error_message990–1002 ↗
fn oauth_callback_error_message(error_code: &str, error_description: Option<&str>) -> String

作用:把 OAuth 回调里的错误变成用户能看懂的话。它避免只显示冷冰冰的错误码。

数据流:输入错误码和可选错误描述 → 如果是缺 Codex 权限,返回专门的管理员提示;否则优先使用错误描述;没有描述就用错误码 → 输出错误消息字符串。

调用关系:process_request 收到 /auth/callback 里的 error 参数时调用它,然后再交给 login_error_response 生成失败页面。

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

parse_token_endpoint_error1009–1071 ↗
fn parse_token_endpoint_error(body: &str) -> TokenEndpointErrorDetail

作用:解析 token 接口返回的错误内容,尽量提取安全、结构化的信息。它还会保留纯文本错误给用户看,但不让这些未知文本随便进结构化日志。

数据流:输入 token 接口响应正文 → 如果为空,返回 unknown error;如果是 JSON,提取 error、error_description 或嵌套 error.message;如果不是 JSON,保留原文本作为 display_message → 输出 TokenEndpointErrorDetail。

调用关系:exchange_code_for_tokens 在 token 接口返回非成功状态时调用它。多组测试覆盖描述优先、嵌套错误、只有码、纯文本等情况。

调用图:被 5 处调用(exchange_code_for_tokens, parse_token_endpoint_error_falls_back_to_error_code, parse_token_endpoint_error_prefers_error_description, parse_token_endpoint_error_preserves_plain_text_for_display, parse_token_endpoint_error_reads_nested_error_message_and_code)。

render_login_error_page1074–1109 ↗
fn render_login_error_page(
    message: &str,
    error_code: Option<&str>,
    error_description: Option<&str>,
) -> Vec<u8>

作用:把登录失败信息渲染成品牌化 HTML 错误页。用户在浏览器里看到的失败页面就是它生成的。

数据流:输入错误消息、可选错误码和错误描述 → 根据是否缺 Codex 权限选择不同标题和帮助文案;所有动态文字先 html_escape 防注入,再填入模板 → 输出 HTML 字节。

调用关系:login_error_response 调用它生成浏览器正文。测试会检查特殊字符会被转义,以及缺 Codex 权限时不会直接暴露内部错误串。

调用图:调用 2 个内部函数(html_escape, is_missing_codex_entitlement_error);被 3 处调用(login_error_response, render_login_error_page_escapes_dynamic_fields, render_login_error_page_uses_entitlement_copy)。

html_escape1112–1125 ↗
fn html_escape(input: &str) -> String

作用:把可能破坏 HTML 的字符转义掉,防止错误内容被浏览器当成网页代码执行。

数据流:输入任意字符串 → 逐字符扫描,把 &, <, >, 双引号、单引号替换成 HTML 安全写法 → 输出安全字符串。

调用关系:render_login_error_page 在把错误信息塞进 HTML 模板前会调用它。

调用图:被 1 处调用(render_login_error_page);外部调用 1 个(with_capacity)。

obtain_api_key1128–1162 ↗
async fn obtain_api_key(
    issuer: &str,
    client_id: &str,
    id_token: &str,
) -> io::Result<String>

作用:用已经拿到的 ID token 再换一个 API-key 风格的访问 token。这个 API key 保存失败不一定阻断登录主流程。

数据流:输入 issuer、client_id 和 id_token → 向 /oauth/token 发送 token-exchange 请求,请求 openai-api-key → 成功解析 access_token 并返回;状态码失败或网络/JSON 失败则返回 io::Error。

调用关系:process_request 在 OAuth token 交换成功后调用它,并用 .ok() 把失败降级成没有 API key,随后仍继续保存主要登录 token。

调用图:被 1 处调用(process_request);外部调用 4 个(builder, other, build_reqwest_client_with_custom_ca, format!)。

tests::parse_token_endpoint_error_prefers_error_description1179–1192 ↗
fn parse_token_endpoint_error_prefers_error_description()

作用:验证 token 错误解析时会优先使用 error_description。这样用户看到的是更具体的说明,而不是只有错误码。

数据流:输入一段包含 error 和 error_description 的 JSON 测试字符串 → 调用 parse_token_endpoint_error → 断言输出的错误码、错误消息和显示文字都符合预期。

调用关系:这是 parse_token_endpoint_error 的单元测试之一,保护 exchange_code_for_tokens 的错误展示行为不被改坏。

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

tests::parse_token_endpoint_error_reads_nested_error_message_and_code1195–1208 ↗
fn parse_token_endpoint_error_reads_nested_error_message_and_code()

作用:验证解析器能读懂嵌套格式的错误对象。有些后端会把 code 和 message 放在 error 对象里面。

数据流:输入嵌套 error JSON → 调用 parse_token_endpoint_error → 断言能拿到 proxy_auth_required 和对应消息。

调用关系:它补充覆盖另一种后端错误格式,确保 exchange_code_for_tokens 遇到代理认证等错误时能显示清楚。

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

tests::parse_token_endpoint_error_falls_back_to_error_code1211–1222 ↗
fn parse_token_endpoint_error_falls_back_to_error_code()

作用:验证如果只有错误码,没有描述,解析器会把错误码当成显示内容。这样至少不会显示空白错误。

数据流:输入只有 error 字段的 JSON → 调用 parse_token_endpoint_error → 断言 display_message 等于这个错误码。

调用关系:这是 parse_token_endpoint_error 的兜底行为测试,间接保障 exchange_code_for_tokens 的错误返回总有可读内容。

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

tests::parse_token_endpoint_error_preserves_plain_text_for_display1225–1236 ↗
fn parse_token_endpoint_error_preserves_plain_text_for_display()

作用:验证非 JSON 的后端错误文本会保留下来给用户看。这样代理或网关返回纯文本时,也能帮助排查。

数据流:输入 plain text 的 service unavailable → 调用 parse_token_endpoint_error → 断言显示文字就是这段文本,结构化字段为空。

调用关系:它对应文件顶部提到的安全策略:用户可见错误保留细节,但结构化日志只记录审查过的字段。

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

tests::redact_sensitive_query_value_only_scrubs_known_keys1239–1248 ↗
fn redact_sensitive_query_value_only_scrubs_known_keys()

作用:验证查询参数打码只处理敏感字段,不会把普通调试信息全抹掉。

数据流:输入 code 和 redirect_uri 两组参数 → 分别调用 redact_sensitive_query_value → 断言 code 被替换成 <redacted>,redirect_uri 保持原样。

调用关系:它保护 redact_sensitive_query_value 的名单行为,避免日志既泄密,也避免过度打码导致排查困难。

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

tests::redact_sensitive_url_parts_preserves_safe_url_shape1251–1263 ↗
fn redact_sensitive_url_parts_preserves_safe_url_shape()

作用:验证 URL 打码后仍保留主机、路径和安全参数形状。排查网络问题时仍能看出请求打到了哪里。

数据流:输入带用户名、密码、code、redirect_uri 和 fragment 的 URL → 调用 redact_sensitive_url_parts → 断言用户名密码和 fragment 被删,code 被打码,redirect_uri 保留。

调用关系:这是 redact_sensitive_url_parts 的直接测试,也支撑 redact_sensitive_error_url 和 sanitize_url_for_logging 的安全性。

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

tests::sanitize_url_for_logging_redacts_sensitive_issuer_parts1266–1274 ↗
fn sanitize_url_for_logging_redacts_sensitive_issuer_parts()

作用:验证写日志前清理 issuer URL 时会隐藏账号密码和 token 参数。

数据流:输入带 user:pass 和 token 查询参数的 URL 字符串 → 调用 sanitize_url_for_logging → 断言输出去掉凭据,并把 token 值变成 <redacted>。

调用关系:它保护 exchange_code_for_tokens 的日志入口,确保自定义 issuer 即使带敏感内容也不会原样记录。

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

tests::compose_success_url_omits_streamlined_success_by_default1277–1292 ↗
fn compose_success_url_omits_streamlined_success_by_default()

作用:验证默认情况下成功页 URL 不带新版简化登录标记。这样旧流程不会被意外切到新页面。

数据流:输入测试端口、默认 issuer、空声明 JWT 和 codex_streamlined_login=false → 调用 compose_success_url 并解析 URL → 断言查询参数里没有 codex_streamlined_login。

调用关系:它测试 process_request 成功后跳转 URL 的默认兼容行为。

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

tests::compose_success_url_includes_streamlined_success_when_requested1295–1311 ↗
fn compose_success_url_includes_streamlined_success_when_requested()

作用:验证开启简化登录时,成功页 URL 会明确带上 codex_streamlined_login=true。

数据流:输入同样的测试 token,但 codex_streamlined_login=true → 调用 compose_success_url 并解析 URL → 断言查询参数中该标记存在且为 true。

调用关系:它测试 process_request 在新版登录体验开启时能把开关传给 /success 页面。

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

tests::render_login_error_page_escapes_dynamic_fields1314–1326 ↗
fn render_login_error_page_escapes_dynamic_fields()

作用:验证错误页会转义用户或后端提供的动态文字,避免 HTML 注入。

数据流:输入包含 <、&、引号的错误消息、错误码和描述 → 调用 render_login_error_page,再转成 UTF-8 字符串 → 断言页面里出现的是转义后的安全文本。

调用关系:它保护 render_login_error_page 和 html_escape 的配合,确保 login_error_response 返回给浏览器的页面安全。

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

tests::render_login_error_page_uses_entitlement_copy1329–1346 ↗
fn render_login_error_page_uses_entitlement_copy()

作用:验证缺 Codex 权限时错误页使用友好的专门文案,而不是暴露内部错误标记。

数据流:输入 access_denied 和 missing_codex_entitlement → 先确认识别函数返回 true,再调用 render_login_error_page → 断言页面提示联系管理员,且不包含内部标记文本。

调用关系:它覆盖 is_missing_codex_entitlement_error 与 render_login_error_page 的联动,保障用户看到的是可行动的说明。

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

tui/src/onboarding/auth/headless_chatgpt_login.rs源码 ↗
orchestrationonboarding login flow

这个文件处理一种比较特殊的登录方式:程序本身在终端里跑,但真正登录要去浏览器完成。流程像取号排队:先生成一个本次登录专用的编号,然后向后台服务器申请“设备码登录”。后台返回登录网址和一次性验证码后,界面就告诉用户打开网址、输入验证码。如果用户中途取消,或者又开始了另一轮登录,旧请求回来时不能再改界面,所以这里用 request_id 做核对,确保只更新“当前还活着”的那次登录。它还会在界面上显示加载动画、把网址标成可点击链接,并在出错时退回选择登录方式的页面。这里的读写锁 RwLock 可以理解成一把门锁,防止多个任务同时乱改登录状态。

函数细节9
start_headless_chatgpt_login23–83 ↗
fn start_headless_chatgpt_login(widget: &mut AuthModeWidget)

作用:启动一次 ChatGPT 设备码登录。它会先把界面切到“正在准备登录码”,再异步向后台要登录网址和验证码。

数据流:进去的是当前登录组件 widget,里面带着界面状态、后台请求通道、错误信息和刷新界面的工具。函数先生成一个新的 request_id,把登录状态改成等待中,并要求界面重画;然后开一个后台任务去请求 LoginAccount。请求成功且返回设备码时,它把网址、验证码和 login_id 写回当前登录状态;如果返回结果不对或请求失败,就写入错误并回到选择登录方式。若用户已经取消或切换到别的登录尝试,它不会更新界面,还会取消后台那次多余的登录尝试。

调用关系:它由 start_device_code_login 调用,是设备码登录流程的开关。它会调用 set_device_code_state_for_active_attempt 或 set_device_code_error_for_active_attempt 来安全更新状态,也会用 onboarding_request_id 生成给后台看的请求标识;如果发现后台返回的是一场已经不需要的登录,它把收尾工作交给 cancel_login_attempt。

调用图:调用 4 个内部函数(pending, ready, set_device_code_error_for_active_attempt, set_device_code_state_for_active_attempt);被 1 处调用(start_device_code_login);外部调用 6 个(new_v4, format!, cancel_login_attempt, onboarding_request_id, spawn, ChatGptDeviceCode)。

render_device_code_login85–153 ↗
fn render_device_code_login(
    widget: &AuthModeWidget,
    area: Rect,
    buf: &mut Buffer,
    state: &ContinueWithDeviceCodeState,
)

作用:把设备码登录页面画到终端界面上。用户看到的“正在准备”、登录网址、一次性验证码和取消提示,都是这里拼出来的。

数据流:进去的是登录组件 widget、要绘制的屏幕区域 area、终端缓冲区 buf,以及当前设备码登录状态 state。函数先判断现在是还在申请验证码,还是已经拿到网址和验证码;如果还没拿到,就显示“正在请求一次性验证码”。如果拿到了,就显示打开浏览器的网址、要输入的一次性验证码、15 分钟过期提醒和防钓鱼提示。最后它把这些文字画进 buf,并在有网址时把网址标成可点击的超链接。

调用关系:它由 render_ref 在界面刷新时调用,是这个登录流程的展示层。它会根据 state.is_showing_copyable_auth 判断显示哪种文案;动画开启时会调用 shimmer_text 生成闪动文字,并要求稍后再刷新一帧;画完后调用 mark_url_hyperlink 给网址加上终端可识别的链接标记。

调用图:调用 2 个内部函数(shimmer_text, is_showing_copyable_auth);被 1 处调用(render_ref);外部调用 5 个(from, new, from_millis, mark_url_hyperlink, vec!)。

device_code_attempt_matches155–160 ↗
fn device_code_attempt_matches(state: &SignInState, request_id: &str) -> bool

作用:判断某个后台结果是不是属于当前这次设备码登录。它的作用是防止旧请求、已取消请求或者别的登录方式误改当前界面。

数据流:进去的是一个登录状态 state 和一个 request_id。函数检查 state 是否正好是 ChatGPT 设备码登录,并且里面保存的 request_id 是否和传入的一样;符合就出来 true,不符合就出来 false,不改任何状态。

调用关系:它是两个安全更新函数的门卫。set_device_code_state_for_active_attempt 和 set_device_code_error_for_active_attempt 在真正写入状态前都会先问它一句:这次结果还算数吗?

调用图:被 2 处调用(set_device_code_error_for_active_attempt, set_device_code_state_for_active_attempt);外部调用 1 个(matches!)。

set_device_code_state_for_active_attempt162–177 ↗
fn set_device_code_state_for_active_attempt(
    sign_in_state: &std::sync::Arc<std::sync::RwLock<SignInState>>,
    request_frame: &crate::tui::FrameRequester,
    request_id: &str,
    next_state: C

作用:只在“这次登录仍然是当前登录”时,把设备码登录状态更新成下一步。它主要用于把“等待中”改成“已经拿到网址和验证码”。

数据流:进去的是共享的登录状态 sign_in_state、刷新界面的 request_frame、本次登录的 request_id,以及要写入的新状态 next_state。函数先拿写锁,检查当前状态是否匹配 request_id;不匹配就直接返回 false。匹配时,它把状态改成 ChatGptDeviceCode(next_state),释放锁,请求界面重画,然后返回 true。

调用关系:它由 start_headless_chatgpt_login 在后台成功拿到设备码后调用。它内部先调用 device_code_attempt_matches 做身份核对,更新成功后调用 schedule_frame 让终端界面尽快显示新网址和验证码。

调用图:调用 2 个内部函数(device_code_attempt_matches, schedule_frame);被 1 处调用(start_headless_chatgpt_login);外部调用 1 个(ChatGptDeviceCode)。

set_device_code_error_for_active_attempt179–196 ↗
fn set_device_code_error_for_active_attempt(
    sign_in_state: &std::sync::Arc<std::sync::RwLock<SignInState>>,
    request_frame: &crate::tui::FrameRequester,
    error: &std::sync::Arc<std::sync::R

作用:只在“这次登录仍然是当前登录”时,记录设备码登录失败的错误。它会把用户带回登录方式选择页,并显示错误原因。

数据流:进去的是共享登录状态 sign_in_state、界面刷新器 request_frame、共享错误信息 error、本次登录的 request_id,以及错误文字 message。函数先检查当前状态是否还对应这个 request_id;如果不是,就返回 false,什么都不改。如果是,它把登录状态改回 PickMode,把错误信息写成 message,请求界面重画,并返回 true。

调用关系:它由 start_headless_chatgpt_login 在后台返回异常结果或请求失败时调用。它同样依赖 device_code_attempt_matches 避免旧错误覆盖新界面,最后用 schedule_frame 触发一次界面刷新,让错误能被用户看到。

调用图:调用 2 个内部函数(device_code_attempt_matches, schedule_frame);被 1 处调用(start_headless_chatgpt_login)。

tests::pending_device_code_state205–209 ↗
fn pending_device_code_state(request_id: &str) -> Arc<RwLock<SignInState>>

作用:给测试准备一个“正在等待设备码”的假登录状态。这样每个测试不用重复写一大段初始化代码。

数据流:进去的是 request_id 字符串。函数用这个编号创建 ContinueWithDeviceCodeState::pending,再包成 SignInState::ChatGptDeviceCode,最后放进 Arc<RwLock<...>> 里返回;Arc 可以理解成多人共用同一份数据的引用,RwLock 是保护这份数据的锁。

调用关系:它只在测试里使用。后面的两个状态更新测试会调用它,快速搭出“当前有一场设备码登录正在进行”的场景。

调用图:调用 1 个内部函数(pending);外部调用 3 个(new, new, ChatGptDeviceCode)。

tests::device_code_attempt_matches_only_for_matching_request_id212–223 ↗
fn device_code_attempt_matches_only_for_matching_request_id()

作用:测试 request_id 核对规则是否正确。它确认只有同一场设备码登录会被认为匹配。

数据流:测试先创建一个 request_id 为 request-1 的设备码登录状态。然后分别拿 request-1、request-2、以及非设备码登录状态去检查,期望结果依次是真、假、假。它不改生产代码里的真实状态,只验证判断函数的输出。

调用关系:它直接测试 device_code_attempt_matches。这个测试很关键,因为后面所有“别让旧请求乱改界面”的保护,都建立在这个判断准确的前提上。

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

tests::set_device_code_state_for_active_attempt_updates_only_when_active226–268 ↗
fn set_device_code_state_for_active_attempt_updates_only_when_active()

作用:测试成功更新状态时必须认准当前登录。它确认匹配的 request_id 会更新,不匹配的 request_id 不会误更新。

数据流:测试先准备一个 request-1 的等待状态和一个假的界面刷新器,然后尝试写入带 login-1、网址和验证码的 ready 状态;预期返回 true,并且状态里确实出现 login-1。接着它准备一个 request-2 的等待状态,却用 request-1 去更新;预期返回 false,并且 login_id 仍然为空。

调用关系:它测试 set_device_code_state_for_active_attempt 的两条路:当前登录有效时更新,当前登录已经不是同一场时拒绝。它用 tests::pending_device_code_state 搭测试数据,用 FrameRequester::test_dummy 避免真的刷新界面。

调用图:调用 1 个内部函数(test_dummy);外部调用 3 个(assert!, assert_eq!, pending_device_code_state)。

tests::set_device_code_error_for_active_attempt_updates_only_when_active271–312 ↗
fn set_device_code_error_for_active_attempt_updates_only_when_active()

作用:测试写入错误时也必须认准当前登录。它确认只有当前这场登录失败,才会把界面退回选择页并显示错误。

数据流:测试先准备 request-1 的等待状态、空错误信息和假的刷新器,然后写入“device code unavailable”错误;预期返回 true,登录状态变成 PickMode,错误信息被保存。接着它准备 request-2 的等待状态,却用 request-1 的错误去更新;预期返回 false,状态仍是设备码登录,错误信息仍为空。

调用关系:它测试 set_device_code_error_for_active_attempt 的安全边界。这个测试保证后台旧错误不会盖掉用户后来开始的新登录流程,也不会把界面莫名其妙带回选择页。

调用图:调用 1 个内部函数(test_dummy);外部调用 5 个(new, new, assert!, assert_eq!, pending_device_code_state)。

认证持久化与登出

这些文件定义认证子系统边界、错误接口、持久化凭据格式、后端选择、专用 API-key 存储和令牌撤销行为。

login/src/auth/error.rs源码 ↗
data_modelcross-cutting

登录系统里常会有“刷新令牌”这一步:用户不用重新输入密码,程序拿旧凭据去换新的登录凭据。如果这一步失败,就需要说清楚失败本身是什么,以及失败原因是什么。这个文件没有自己写新逻辑,而是把 codex_protocol 里的两个认证错误类型重新导出。所谓“重新导出”,可以理解成把别的货架上的商品搬到本货架展示:外部代码只要从 login 的 auth error 位置拿就行,不必直接依赖更底层的协议模块。这样做的好处是减少耦合。以后底层错误类型的位置如果调整,很多调用方不一定要跟着改。

login/src/auth/mod.rs源码 ↗
othercross-cutting

这个文件本身不做登录检查,也不保存密码。它更像一本书的目录页:告诉 Rust 编译器这里有哪些认证相关的小模块,比如访问令牌、个人访问令牌、Bedrock API Key、身份信息、存储、撤销登录等。同时,它把外部最常用的东西重新导出,比如 BedrockApiKeyAuth、login_with_bedrock_api_key、刷新令牌失败的错误类型,以及 manager 里的公共能力。这样别的地方只要引用 auth 这一层,就能拿到认证系统的主要工具,不需要直接摸到每个内部文件。没有它,认证代码会散在多个文件里,调用方要记住很多具体路径,项目也更容易变乱。

core/src/config/auth_keyring.rs源码 ↗
configstartup / config load

这个文件像一个小开关转换器:它不真正保存密码或令牌,而是根据功能开关来决定用哪种保存方式。这里的关键开关叫 SecretAuthStorage,意思是“把认证秘密放进更安全的秘密存储里”。如果这个开关打开,就选择 Secrets,也就是使用系统或平台提供的密钥库;如果没打开,就选择 Direct,也就是按旧方式直接处理。特别之处在于,程序刚启动时还没有完整配置,但可能已经需要读取登录信息,所以文件里专门提供了一个“启动早期用”的解析函数。它会从半加载的配置里先拼出功能开关状态,再得出同样的结论,避免启动阶段和完整配置阶段判断不一致。

函数细节3
Config::auth_keyring_backend_kind11–15 ↗
fn auth_keyring_backend_kind(&self) -> AuthKeyringBackendKind

作用:这个方法在完整配置已经准备好之后,告诉其他代码应该使用哪种认证密钥存储方式。有人需要读写登录信息时,会先问它该走安全密钥库还是直接方式。

数据流:进去的是一个已经完整加载好的 Config。它读取里面的功能开关状态,特别是 SecretAuthStorage 是否启用;然后把这个真假值交给统一的判断函数;出来的是 AuthKeyringBackendKind,也就是“用 Secrets”或“用 Direct”的明确选择。

调用关系:它属于正常运行阶段的配置查询入口。它自己不做复杂判断,而是把核心选择交给 auth_keyring_backend_kind_from_secret_auth_storage,这样启动早期和完整配置阶段可以共用同一条判断规则。

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

resolve_bootstrap_auth_keyring_backend_kind22–45 ↗
fn resolve_bootstrap_auth_keyring_backend_kind(
    bootstrap_config: &ConfigTomlLoadResult,
) -> std::io::Result<AuthKeyringBackendKind>

作用:这个函数给程序刚启动、完整配置还没建好时使用,用来提前决定认证信息该从哪里读。没有它,启动时读取登录信息可能会因为配置还不完整而不知道该走哪种存储方式。

数据流:进去的是 ConfigTomlLoadResult,也就是“刚从配置文件里读出一部分内容”的结果。它先拿出配置里的功能设置,合成一份 Features;再结合配置层里的强制要求,生成 ManagedFeatures;接着查看 SecretAuthStorage 是否启用;最后输出应该使用的 AuthKeyringBackendKind。如果中间的受管功能要求不合法,它会返回一个输入输出错误结果,而不是强行继续。

调用关系:它活跃在启动早期,比完整 Config 出现得更早。它会调用 from_sources 把配置来源合成普通功能开关,再调用 from_configured 套上受管理的要求,最后调用 auth_keyring_backend_kind_from_secret_auth_storage 做最终选择。

调用图:调用 3 个内部函数(auth_keyring_backend_kind_from_secret_auth_storage, from_configured, from_sources);外部调用 2 个(default, default)。

auth_keyring_backend_kind_from_secret_auth_storage47–55 ↗
fn auth_keyring_backend_kind_from_secret_auth_storage(
    secret_auth_storage_enabled: bool,
) -> AuthKeyringBackendKind

作用:这是最核心、也最简单的判断规则:SecretAuthStorage 开了就用安全秘密存储,没开就用直接方式。它把一个布尔开关翻译成更具体的存储后端类型。

数据流:进去的是一个真假值,表示 SecretAuthStorage 是否启用。它只做一次判断:为真就返回 AuthKeyringBackendKind::Secrets,为假就返回 AuthKeyringBackendKind::Direct。它不读取文件,也不修改任何状态。

调用关系:它是这个文件里的公共规则中心。Config::auth_keyring_backend_kind 在完整配置阶段调用它,resolve_bootstrap_auth_keyring_backend_kind 在启动早期调用它,所以两条路径最终得到的选择标准完全一致。

调用图:被 2 处调用(auth_keyring_backend_kind, resolve_bootstrap_auth_keyring_backend_kind)。

keyring-store/src/lib.rs源码 ↗
io_transportcross-cutting

这个文件解决的是“敏感凭据放哪里、怎么安全取用”的问题。它定义了 KeyringStore 这个统一接口,意思是:不管背后是真正的系统钥匙串,还是测试用的假钥匙串,外面都只需要调用 load、save、delete。DefaultKeyringStore 是真实实现,会通过 keyring 库访问电脑或系统提供的安全存储区,类似把密码放进系统保险柜。CredentialStoreError 则把底层错误包装成项目自己的错误,方便统一处理。文件里还带了 MockKeyringStore,这是测试时用的内存版保险柜,不碰真实系统钥匙串,避免测试污染用户电脑里的真实凭据。一个重要细节是:读取或删除时,如果根本没有这条凭据,不算故障,而是返回“没有”或“没删到”。

函数细节14
CredentialStoreError::new14–16 ↗
fn new(error: KeyringError) -> Self

作用:把 keyring 库给出的底层错误包装成项目自己的 CredentialStoreError。这样外面的代码不用认识各种底层错误来源,只要处理一种统一错误。

数据流:进去的是一个 KeyringError,也就是系统钥匙串操作失败时给出的错误;函数把它塞进 CredentialStoreError::Other 这个包装里;出来的是一个项目内部统一使用的错误值,不会修改别的状态。

调用关系:真实钥匙串的读取、保存、删除,以及测试版钥匙串遇到错误时,都会调用它。它位于底层 keyring 错误和项目上层错误处理之间,像一个翻译员。

调用图:被 5 处调用(delete, load, save, delete, load);外部调用 1 个(Other)。

CredentialStoreError::message18–22 ↗
fn message(&self) -> String

作用:把凭据存储错误变成一段普通文字,方便显示、记录日志,或者交给调用方阅读。

数据流:进去的是这个错误对象本身;函数取出里面包着的底层 keyring 错误,并转成字符串;出来的是一段错误说明文字,不改变原错误。

调用关系:它是错误对象的辅助入口。其他地方如果不想拿到底层错误,只想拿一句可读的说明,就可以用它。

CredentialStoreError::into_error24–28 ↗
fn into_error(self) -> KeyringError

作用:把项目自己的错误包装拆开,取回原始的 KeyringError。有人需要直接看底层钥匙串错误时会用到它。

数据流:进去的是一个 CredentialStoreError,并且这个错误会被消耗掉;函数把里面的 KeyringError 拿出来;出来的是原始底层错误,原来的包装对象不再保留。

调用关系:它和 CredentialStoreError::new 正好相反:new 是包装错误,into_error 是拆开错误。适合更底层或更细致的错误处理代码使用。

CredentialStoreError::fmt32–36 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:规定这个错误被当成普通文字打印时该显示什么。比如日志里写这个错误,就会走这里。

数据流:进去的是错误对象和一个格式化输出器;函数把内部 KeyringError 的文字写进去;出来的是格式化是否成功的结果,不改变错误本身。

调用关系:它是 Rust 标准 Display 显示接口的一部分。日志、报错信息或 format 这类格式化工具需要把 CredentialStoreError 变成文字时,会间接用到它。

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

DefaultKeyringStore::load52–69 ↗
fn load(&self, service: &str, account: &str) -> Result<Option<String>, CredentialStoreError>

作用:从真实的系统钥匙串里读取某个账号对应的密码或令牌。如果没有存过,它不会报错,而是明确告诉调用方“没有”。

数据流:进去的是 service 和 account,service 像应用或服务名,account 像用户名或账号名;函数先创建一条钥匙串记录入口,再尝试读取密码;出来要么是 Some(password),要么是 None 表示没有记录,要么是 CredentialStoreError 表示真的出错了,同时会写 trace 级别日志方便排查。

调用关系:这是 KeyringStore 接口的真实读取实现。上层代码需要拿凭据时会调用它;它把具体读取工作交给 keyring::Entry,并在底层错误出现时交给 CredentialStoreError::new 包装。

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

DefaultKeyringStore::save71–87 ↗
fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError>

作用:把某个账号的密码或令牌保存到真实系统钥匙串里。这样项目不用把敏感信息明文放在普通文件中。

数据流:进去的是 service、account 和要保存的 value;函数创建对应的钥匙串入口,然后把 value 写入系统钥匙串;出来是成功的空结果,或者一个包装后的 CredentialStoreError,同时日志只记录长度,不直接记录敏感内容。

调用关系:这是 KeyringStore 接口的真实保存实现。上层代码登录成功或刷新令牌后会调用它;它调用 keyring::Entry 的保存能力,失败时用 CredentialStoreError::new 统一错误格式。

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

DefaultKeyringStore::delete89–106 ↗
fn delete(&self, service: &str, account: &str) -> Result<bool, CredentialStoreError>

作用:从真实系统钥匙串里删除某个账号的凭据。它会告诉调用方到底删到了没有。

数据流:进去的是 service 和 account;函数找到对应钥匙串入口并尝试删除;出来是 true 表示确实删掉了,false 表示原本就没有这条记录,或者返回 CredentialStoreError 表示删除过程中发生了真正错误。

调用关系:这是 KeyringStore 接口的真实删除实现。上层代码退出登录、清理账号或撤销凭据时会调用它;它依赖 keyring::Entry 做实际删除,并用 CredentialStoreError::new 包装异常。

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

tests::MockKeyringStore::credential126–135 ↗
fn credential(&self, account: &str) -> Arc<MockCredential>

作用:在测试用的假钥匙串里,取出某个账号对应的假凭据对象;如果还没有,就现场创建一个。它让测试可以像操作真实钥匙串一样操作内存里的数据。

数据流:进去的是 account;函数先给内部 HashMap 加互斥锁,互斥锁就是一把防止多个线程同时改同一份数据的锁,然后查找账号,没有就插入新的 MockCredential;出来的是这个账号对应的共享假凭据对象。

调用关系:测试版 save 会用它拿到要写入的假凭据,set_error 也会用它给某个账号安排错误。它是 MockKeyringStore 内部创建和复用测试凭据的入口。

调用图:被 2 处调用(save, set_error)。

tests::MockKeyringStore::saved_value137–146 ↗
fn saved_value(&self, account: &str) -> Option<String>

作用:查看测试假钥匙串里某个账号当前保存了什么值。它通常用来在测试断言里确认“刚才真的保存成功了”。

数据流:进去的是 account;函数从内存表里找这个账号的 MockCredential,如果找不到就返回 None,如果找到了就读取里面的密码;出来是可能存在的字符串值,不会修改存储内容。

调用关系:它是测试辅助函数,不属于正式钥匙串流程。测试代码在调用 save 之后,可以用它检查保存结果,而不用访问真实系统钥匙串。

tests::MockKeyringStore::set_error148–151 ↗
fn set_error(&self, account: &str, error: KeyringError)

作用:给测试里的某个账号故意设置一个错误。这样测试可以模拟“钥匙串坏了、权限不够、记录不存在”等异常场景。

数据流:进去的是 account 和一个 KeyringError;函数先通过 credential 找到或创建该账号的假凭据,再把指定错误塞进去;之后这个账号的相关操作就可能按这个错误表现出来。

调用关系:它调用 tests::MockKeyringStore::credential 来拿到假凭据。测试代码会在执行 load、save 或 delete 前使用它,故意制造失败路径,检查上层代码会不会正确处理错误。

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

tests::MockKeyringStore::contains153–159 ↗
fn contains(&self, account: &str) -> bool

作用:检查测试假钥匙串里是否已经有某个账号的记录。它适合用来验证删除或创建行为。

数据流:进去的是 account;函数锁住内部内存表并检查是否存在这个账号键;出来是 true 或 false,不会改动任何凭据。

调用关系:它是测试观察工具。测试代码可以在调用 save 或 delete 前后用它确认 MockKeyringStore 的内部状态是否符合预期。

tests::MockKeyringStore::load163–185 ↗
fn load(
            &self,
            _service: &str,
            account: &str,
        ) -> Result<Option<String>, CredentialStoreError>

作用:从测试用的内存钥匙串里读取某个账号的值,行为尽量模仿真实 DefaultKeyringStore::load。这样测试不用碰用户电脑的真实钥匙串。

数据流:进去的是 service 和 account,其中 service 在这个假实现里不使用;函数从内部内存表找账号,找不到就返回 None,找到了就读 MockCredential 的密码;出来是 Some(password)、None,或包装后的 CredentialStoreError。

调用关系:这是测试版 KeyringStore 的读取实现。上层代码在测试中以为自己在读钥匙串,实际上调用的是它;它遇到底层模拟错误时会交给 CredentialStoreError::new 包装。

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

tests::MockKeyringStore::save187–197 ↗
fn save(
            &self,
            _service: &str,
            account: &str,
            value: &str,
        ) -> Result<(), CredentialStoreError>

作用:把值保存到测试用的内存钥匙串里。它让测试能验证保存逻辑,而不会把假数据写进真实系统。

数据流:进去的是 service、account 和 value,其中 service 在假实现里不使用;函数通过 credential 找到或创建账号对应的 MockCredential,再写入 value;出来是成功或包装后的错误,并会改变内存里的测试数据。

调用关系:这是测试版 KeyringStore 的保存实现。它把找凭据或建凭据的工作交给 tests::MockKeyringStore::credential,然后调用假凭据的写入能力。

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

tests::MockKeyringStore::delete199–224 ↗
fn delete(&self, _service: &str, account: &str) -> Result<bool, CredentialStoreError>

作用:从测试用的内存钥匙串里删除某个账号的凭据,并返回是否真的删除到了。它用来模拟真实删除流程。

数据流:进去的是 service 和 account,其中 service 在假实现里不使用;函数先查内存表,没有账号就返回 false,有账号就调用假凭据的删除动作;如果删除成功,再从内存表移除账号;出来是 true、false,或包装后的 CredentialStoreError。

调用关系:这是测试版 KeyringStore 的删除实现。测试中的退出登录或清理凭据流程会走到它;如果假凭据报告错误,它会用 CredentialStoreError::new 转成统一错误。

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

login/src/auth/storage.rs源码 ↗
io_transportcross-cutting:登录、启动读取认证、保存认证、登出清理时都会用到

登录信息很敏感,不能随便散落在代码各处。这个文件就像一个“保险柜接口”:上层只管说我要读取、保存、删除认证信息,至于保险柜是普通文件 auth.json、系统钥匙串,还是本地加密秘密库,由这里决定。它先定义 auth.json 里可能有哪些字段,再定义统一的 AuthStorageBackend 接口。不同实现各管一种存法:FileAuthStorage 写磁盘文件;DirectKeyringAuthStorage 直接写系统钥匙串;SecretsKeyringAuthStorage 通过 secrets 管理器加密保存;AutoAuthStorage 会优先用钥匙串,失败时退回文件;EphemeralAuthStorage 只存在内存里,适合测试或短期会话。一个重要细节是,保存到更安全的位置后,它会尽量删除旧的 auth.json 文件,避免敏感信息残留。

函数细节33
AgentIdentityAuthRecord::from_agent_identity_jwt75–80 ↗
fn from_agent_identity_jwt(jwt: &str) -> std::io::Result<Self>

作用:把一段代理身份 JWT 转成程序内部更好用的身份记录。JWT 可以理解成一张带签名的电子身份证,这里会先解开并检查它。

数据流:输入是一段 JWT 字符串 → 它调用解码函数拿到里面的身份声明,比如账号、邮箱、计划类型、私钥等 → 输出一个 AgentIdentityAuthRecord;如果 JWT 不合法,就变成读取错误返回。

调用关系:它被 verified_agent_identity_record 使用,处在“确认代理身份是否可信”这一步的前面。它把底层的 decode_agent_identity_jwt 解码结果接过来,再交给 From 转换成存储用记录。

调用图:被 1 处调用(verified_agent_identity_record);外部调用 1 个(decode_agent_identity_jwt)。

AgentIdentityAuthRecord::from84–94 ↗
fn from(claims: AgentIdentityJwtClaims) -> Self

作用:把解码后的代理身份声明转换成本文件保存用的记录格式。它主要是字段搬运,把外部 claims 变成内部 auth record。

数据流:输入是 AgentIdentityJwtClaims,也就是 JWT 解出来的身份内容 → 它逐项取出运行时 ID、私钥、账号、用户 ID、邮箱、套餐类型和 FedRAMP 标记 → 输出 AgentIdentityAuthRecord。

调用关系:它通常接在 AgentIdentityAuthRecord::from_agent_identity_jwt 后面使用。前一步负责解码和校验,这一步负责把结果整理成认证存储能保存的形状。

get_auth_file97–99 ↗
fn get_auth_file(codex_home: &Path) -> PathBuf

作用:根据 Codex 的主目录算出 auth.json 的完整路径。这样其他地方不用自己拼路径,避免拼错。

数据流:输入是 codex_home 目录路径 → 它在这个目录后面加上 auth.json → 输出完整文件路径。

调用关系:它是文件存储的基础小工具。FileAuthStorage::load、FileAuthStorage::save、delete_file_if_exists 等都会先问它 auth.json 在哪里。

调用图:被 5 处调用(logout_removes_auth_file, write_auth_file, load, save, delete_file_if_exists);外部调用 1 个(join)。

delete_file_if_exists101–108 ↗
fn delete_file_if_exists(codex_home: &Path) -> std::io::Result<bool>

作用:删除 codex_home 里的 auth.json,如果文件本来就不存在,也不当成错误。它常用于登出或迁移到更安全存储后清理旧文件。

数据流:输入是 codex_home → 它先用 get_auth_file 找到 auth.json,再尝试删除 → 如果删掉了返回 true,不存在返回 false,其他删除失败则返回错误。

调用关系:它被多种存储后端调用。文件后端删除时用它;钥匙串或加密存储保存成功后也会调用它,目的是清掉磁盘上的旧凭据备份。

调用图:调用 1 个内部函数(get_auth_file);被 5 处调用(delete, save, delete, delete, save);外部调用 1 个(remove_file)。

FileAuthStorage::new122–124 ↗
fn new(codex_home: PathBuf) -> Self

作用:创建一个“用 auth.json 文件保存认证信息”的存储对象。调用者只需要给它 Codex 主目录。

数据流:输入是 codex_home 路径 → 它把路径放进 FileAuthStorage 结构里 → 输出一个可以读写 auth.json 的对象。

调用关系:它被很多登录、登出和测试流程使用,也会被 AutoAuthStorage 当作备用方案创建。后续真正读写文件的工作交给 load、save、delete。

调用图:被 15 处调用(login_with_access_token_writes_only_personal_access_token, login_with_access_token_writes_only_token, login_with_api_key_overwrites_existing_auth_json, bedrock_only_auth_storage_creates_primary_auth, login_with_api_key_clears_bedrock_api_key, login_with_bedrock_api_key_replaces_openai_auth, logout_removes_bedrock_auth, new, create_auth_storage_with_store, file_storage_delete_removes_auth_file (+5 more))。

FileAuthStorage::try_read_auth_json128–135 ↗
fn try_read_auth_json(&self, auth_file: &Path) -> std::io::Result<AuthDotJson>

作用:真正打开并解析 auth.json 文件。它把磁盘上的 JSON 文本变成程序能直接使用的 AuthDotJson。

数据流:输入是 auth.json 文件路径 → 它打开文件、读成字符串、用 JSON 解析器转换 → 输出 AuthDotJson;文件打不开或格式不对就返回错误。

调用关系:它由 FileAuthStorage::load 调用。load 负责决定“找不到文件算不算正常”,而这个函数只专心做读取和解析。

调用图:被 1 处调用(load);外部调用 3 个(open, new, from_str)。

FileAuthStorage::load139–147 ↗
fn load(&self) -> std::io::Result<Option<AuthDotJson>>

作用:从 auth.json 文件加载登录信息。如果文件不存在,它会返回“没有认证信息”,而不是报错。

数据流:输入是 FileAuthStorage 里保存的 codex_home → 它用 get_auth_file 找路径,再调用 try_read_auth_json 读取 → 输出 Some(AuthDotJson)、None,或者读取错误。

调用关系:它实现 AuthStorageBackend 的 load。上层 load_auth_dot_json 之类的流程会通过统一接口调用它,不需要知道底层是文件。

调用图:调用 2 个内部函数(try_read_auth_json, get_auth_file)。

FileAuthStorage::save149–166 ↗
fn save(&self, auth_dot_json: &AuthDotJson) -> std::io::Result<()>

作用:把当前认证信息写进 auth.json。它会确保目录存在,并在 Unix 系统上把文件权限设成只有当前用户可读写。

数据流:输入是 AuthDotJson → 它算出 auth.json 路径,创建父目录,把认证信息转成漂亮的 JSON 文本,然后覆盖写入文件并 flush 落盘 → 输出成功或写入错误,同时磁盘文件被更新。

调用关系:它实现 AuthStorageBackend 的 save。AutoAuthStorage 在钥匙串保存失败时会退回调用它,直接文件模式也会用它。

调用图:调用 1 个内部函数(get_auth_file);外部调用 3 个(new, to_string_pretty, create_dir_all)。

FileAuthStorage::delete168–170 ↗
fn delete(&self) -> std::io::Result<bool>

作用:删除 auth.json 文件,用于登出或清理本地认证信息。

数据流:输入是 FileAuthStorage 里的 codex_home → 它调用 delete_file_if_exists → 输出是否真的删掉了文件。

调用关系:它实现 AuthStorageBackend 的 delete。上层登出流程通过统一存储接口调用,不需要关心文件路径细节。

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

compute_store_key181–192 ↗
fn compute_store_key(codex_home: &Path) -> std::io::Result<String>

作用:把 Codex 主目录路径变成一个短而稳定的钥匙串键名。这样不同目录的登录信息不会互相覆盖。

数据流:输入是 codex_home 路径 → 它尽量转成规范路径,转成字符串,用 SHA-256 哈希算法算摘要,再截取前 16 位并加上 cli| 前缀 → 输出类似 cli|xxxx 的键名。

调用关系:钥匙串存储和内存临时存储都会用它。DirectKeyringAuthStorage 的 load、save、delete,以及 EphemeralAuthStorage::with_store 都靠它决定该读写哪一份记录。

调用图:被 4 处调用(delete, load, save, with_store);外部调用 3 个(canonicalize, new, format!)。

DirectKeyringAuthStorage::new201–206 ↗
fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self

作用:创建一个直接使用系统钥匙串保存认证信息的存储对象。系统钥匙串可以理解成操作系统提供的密码保险柜。

数据流:输入是 codex_home 和一个钥匙串访问对象 → 它把两者保存起来 → 输出 DirectKeyringAuthStorage。

调用关系:它由 create_keyring_auth_storage 或 SecretsKeyringAuthStorage::new 调用。后续 load、save、delete 会借助传入的 keyring_store 操作真实钥匙串。

调用图:被 5 处调用(new, create_keyring_auth_storage, direct_keyring_auth_storage_delete_removes_keyring_and_file, direct_keyring_auth_storage_saves_legacy_keyring_entry, secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry)。

DirectKeyringAuthStorage::load_from_keyring208–221 ↗
fn load_from_keyring(&self, key: &str) -> std::io::Result<Option<AuthDotJson>>

作用:用指定键名从系统钥匙串里读取认证 JSON,并解析成 AuthDotJson。

数据流:输入是钥匙串键名 → 它从 KEYRING_SERVICE 这个服务名下读取字符串 → 有内容就解析 JSON 输出 Some(AuthDotJson),没内容输出 None,钥匙串失败或 JSON 坏了就返回错误。

调用关系:它被 DirectKeyringAuthStorage::load 调用。load 先算键名,这个函数再负责真正访问钥匙串并解析内容。

调用图:被 1 处调用(load);外部调用 3 个(other, format!, from_str)。

DirectKeyringAuthStorage::save_to_keyring223–235 ↗
fn save_to_keyring(&self, key: &str, value: &str) -> std::io::Result<()>

作用:把序列化后的认证字符串写入系统钥匙串。失败时会记录警告,方便排查为什么安全存储不可用。

数据流:输入是钥匙串键名和要保存的字符串 → 它调用 keyring_store.save 写入 → 成功返回空结果,失败则把钥匙串错误包装成标准 IO 错误并记录警告。

调用关系:它被 DirectKeyringAuthStorage::save 调用。save 负责把 AuthDotJson 转成字符串,这个函数负责把字符串塞进钥匙串。

调用图:被 1 处调用(save);外部调用 3 个(other, format!, warn!)。

DirectKeyringAuthStorage::load239–242 ↗
fn load(&self) -> std::io::Result<Option<AuthDotJson>>

作用:从系统钥匙串加载当前 Codex 主目录对应的认证信息。

数据流:输入是 DirectKeyringAuthStorage 自己保存的 codex_home → 它用 compute_store_key 算出键名,再调用 load_from_keyring → 输出认证信息、空结果或错误。

调用关系:它实现 AuthStorageBackend 的 load。AutoAuthStorage 优先尝试它;直接钥匙串模式下,上层认证加载也会走到这里。

调用图:调用 2 个内部函数(load_from_keyring, compute_store_key)。

DirectKeyringAuthStorage::save244–253 ↗
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()>

作用:把认证信息保存到系统钥匙串,并尽量删除旧的 auth.json 文件。这样敏感信息不再留在普通磁盘文件里。

数据流:输入是 AuthDotJson → 它算钥匙串键名,把认证信息压成 JSON 字符串,写入钥匙串 → 成功后尝试删除 auth.json;删除失败只记录警告,不影响保存成功。

调用关系:它实现 AuthStorageBackend 的 save。AutoAuthStorage 或直接钥匙串模式会调用它;实际写钥匙串的动作交给 save_to_keyring。

调用图:调用 3 个内部函数(save_to_keyring, compute_store_key, delete_file_if_exists);外部调用 2 个(to_string, warn!)。

DirectKeyringAuthStorage::delete255–265 ↗
fn delete(&self) -> std::io::Result<bool>

作用:删除系统钥匙串里的认证信息,同时也删除可能遗留的 auth.json 文件。

数据流:输入是 DirectKeyringAuthStorage 的 codex_home → 它算出钥匙串键名,删除钥匙串项,再调用 delete_file_if_exists 删除文件 → 输出是否至少删除了一处内容。

调用关系:它实现 AuthStorageBackend 的 delete,也会被 SecretsKeyringAuthStorage::delete 用来清理旧版直接钥匙串记录。这样迁移后也不会留下旧凭据。

调用图:调用 2 个内部函数(compute_store_key, delete_file_if_exists);被 1 处调用(delete)。

SecretsKeyringAuthStorage::fmt276–280 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:给 SecretsKeyringAuthStorage 提供安全的调试显示。它只显示 codex_home,不把秘密内容打印出来。

数据流:输入是格式化器和存储对象 → 它构造一个调试结构,只写入 codex_home,并标记还有未展开字段 → 输出格式化结果。

调用关系:这是 Debug trait 的实现,主要在日志、测试失败信息或调试输出里被系统自动调用。它避免把 secrets_manager 内部细节或凭据泄露到日志。

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

SecretsKeyringAuthStorage::new284–298 ↗
fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self

作用:创建一个通过 secrets 管理器保存认证信息的存储对象。它比直接钥匙串多了一层本地加密秘密管理。

数据流:输入是 codex_home 和钥匙串访问对象 → 它创建一个 DirectKeyringAuthStorage 用于兼容旧记录,再创建 SecretsManager,并指定 CodexAuth 这个命名空间 → 输出 SecretsKeyringAuthStorage。

调用关系:它由 create_keyring_auth_storage 调用。后续 load 和 save 主要走 secrets_manager;delete 还会顺手调用 direct_storage.delete 清掉老格式数据。

调用图:调用 2 个内部函数(new, new_with_keyring_store_and_namespace);被 5 处调用(create_keyring_auth_storage, secrets_keyring_auth_storage_delete_removes_keyring_and_file, secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry, secrets_keyring_auth_storage_load_returns_deserialized_auth, secrets_keyring_auth_storage_save_persists_and_removes_fallback_file);外部调用 2 个(clone, clone)。

SecretsKeyringAuthStorage::load302–318 ↗
fn load(&self) -> std::io::Result<Option<AuthDotJson>>

作用:从加密认证存储里读取认证信息。这里的“加密认证存储”是由 SecretsManager 管理的本地秘密库。

数据流:输入是 SecretsKeyringAuthStorage 自身状态 → 它从 Global 作用域读取名为 CODEX_AUTH 的秘密 → 有内容就按 JSON 解析成 AuthDotJson,没内容返回 None,读取或解析失败则返回错误。

调用关系:它实现 AuthStorageBackend 的 load。选择 Secrets 后端时,上层加载认证会走这里,而不是直接读 auth.json。

调用图:调用 1 个内部函数(get);外部调用 1 个(from_str)。

SecretsKeyringAuthStorage::save320–334 ↗
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()>

作用:把认证信息写入加密秘密库,并尽量删除旧 auth.json 文件。它是更安全的保存路径之一。

数据流:输入是 AuthDotJson → 它转成 JSON 字符串,调用 secrets_manager.set 保存到 Global/CODEX_AUTH → 成功后尝试删除 auth.json;写入失败会记录警告并返回错误。

调用关系:它实现 AuthStorageBackend 的 save。AutoAuthStorage 或 Keyring+Secrets 模式会调用它;底层真正保存交给 SecretsManager。

调用图:调用 2 个内部函数(delete_file_if_exists, set);外部调用 2 个(to_string, warn!)。

SecretsKeyringAuthStorage::delete336–348 ↗
fn delete(&self) -> std::io::Result<bool>

作用:彻底清理加密认证存储、普通 auth.json 文件,以及旧版直接钥匙串里的认证记录。

数据流:输入是 SecretsKeyringAuthStorage 自身状态 → 它先删 secrets_manager 里的 CODEX_AUTH,再删 auth.json,最后调用 direct_storage.delete 删除旧钥匙串项 → 输出是否至少有一处内容被删除。

调用关系:它实现 AuthStorageBackend 的 delete。登出时如果当前后端是 Secrets,就会用它,确保新旧存储位置都被清干净。

调用图:调用 3 个内部函数(delete, delete_file_if_exists, delete)。

AutoAuthStorage::new358–371 ↗
fn new(
        codex_home: PathBuf,
        keyring_store: Arc<dyn KeyringStore>,
        keyring_backend_kind: AuthKeyringBackendKind,
    ) -> Self

作用:创建一个自动选择存储方式的对象:优先用钥匙串或加密秘密库,同时准备一个文件存储做备用。

数据流:输入是 codex_home、钥匙串访问对象和钥匙串后端类型 → 它创建 keyring_storage,再创建 file_storage → 输出 AutoAuthStorage。

调用关系:它由 create_auth_storage_with_store 在 Auto 模式下调用。之后 load/save 会先试安全存储,失败再回退到文件。

调用图:调用 2 个内部函数(new, create_keyring_auth_storage);被 7 处调用(create_auth_storage_with_store, auto_auth_storage_delete_removes_keyring_and_file, auto_auth_storage_load_falls_back_when_keyring_errors, auto_auth_storage_load_prefers_keyring_value, auto_auth_storage_load_uses_file_when_keyring_empty, auto_auth_storage_save_falls_back_when_keyring_errors, auto_auth_storage_save_prefers_keyring);外部调用 2 个(new, clone)。

AutoAuthStorage::load375–384 ↗
fn load(&self) -> std::io::Result<Option<AuthDotJson>>

作用:自动加载认证信息:先尝试更安全的钥匙串存储,拿不到或失败时再读 auth.json。

数据流:输入是 AutoAuthStorage → 它先调用 keyring_storage.load → 如果读到认证就返回;如果钥匙串为空就读文件;如果钥匙串报错,会记录警告后读文件 → 输出认证信息、空结果或文件读取错误。

调用关系:它实现 AuthStorageBackend 的 load。上层只看到一次 load 调用,内部则像先问保险柜、再翻备用抽屉。

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

AutoAuthStorage::save386–394 ↗
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()>

作用:自动保存认证信息:优先写入更安全的钥匙串或秘密库,失败时退回写 auth.json。

数据流:输入是 AuthDotJson → 它先调用 keyring_storage.save → 成功就结束;失败则记录警告,再调用 file_storage.save → 输出最终保存结果。

调用关系:它实现 AuthStorageBackend 的 save。它让用户在钥匙串不可用的机器上仍然能登录,只是安全存储失败时会退回文件。

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

AutoAuthStorage::delete396–399 ↗
fn delete(&self) -> std::io::Result<bool>

作用:自动删除认证信息,主要交给钥匙串存储后端处理。注释说明钥匙串后端也会顺手删除磁盘文件。

数据流:输入是 AutoAuthStorage → 它调用 keyring_storage.delete → 输出是否删除了任何认证数据。

调用关系:它实现 AuthStorageBackend 的 delete。登出时走这里,内部依赖具体 keyring_storage 把安全存储和文件残留都清掉。

EphemeralAuthStorage::new412–414 ↗
fn new(codex_home: PathBuf) -> Self

作用:创建一个只把认证信息存在内存里的存储对象。程序结束后这些信息就没了,适合测试或不想落盘的场景。

数据流:输入是 codex_home → 它保存这个路径,用来区分不同工作目录的临时认证记录 → 输出 EphemeralAuthStorage。

调用关系:它由 create_auth_storage_with_store 在 Ephemeral 模式下调用。后续 load/save/delete 都会通过全局内存表完成。

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

EphemeralAuthStorage::with_store416–425 ↗
fn with_store(&self, action: F) -> std::io::Result<T>

作用:安全地访问全局内存认证表。这里用了互斥锁,也就是“一把锁”,防止两个任务同时修改同一张表。

数据流:输入是一个要对内存表执行的动作 → 它先用 compute_store_key 算当前目录的键名,再锁住全局 HashMap,把表和键名交给传入动作 → 输出这个动作的结果;如果锁坏了就返回错误。

调用关系:它是 EphemeralAuthStorage::load、save、delete 的共同入口。三个函数不用重复写加锁和算键名的代码,只把具体动作传进来。

调用图:调用 1 个内部函数(compute_store_key);被 3 处调用(delete, load, save)。

EphemeralAuthStorage::load429–431 ↗
fn load(&self) -> std::io::Result<Option<AuthDotJson>>

作用:从内存临时表里读取当前目录对应的认证信息。

数据流:输入是 EphemeralAuthStorage → 它通过 with_store 锁住内存表,然后按键名查找并克隆一份认证信息 → 输出 Some(AuthDotJson) 或 None。

调用关系:它实现 AuthStorageBackend 的 load。调用链上,上层认证加载不必知道这是内存模式;具体加锁和取表由 with_store 完成。

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

EphemeralAuthStorage::save433–438 ↗
fn save(&self, auth: &AuthDotJson) -> std::io::Result<()>

作用:把认证信息保存到内存临时表,不写磁盘也不写钥匙串。

数据流:输入是 AuthDotJson → 它通过 with_store 锁住内存表,把认证信息克隆后按当前目录键名插入 → 输出成功,同时内存表被更新。

调用关系:它实现 AuthStorageBackend 的 save。Ephemeral 模式下,上层登录保存会走这里;with_store 负责统一处理锁和键名。

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

EphemeralAuthStorage::delete440–442 ↗
fn delete(&self) -> std::io::Result<bool>

作用:从内存临时表里删除当前目录对应的认证信息。

数据流:输入是 EphemeralAuthStorage → 它通过 with_store 锁住内存表,移除对应键名 → 输出是否真的移除了记录。

调用关系:它实现 AuthStorageBackend 的 delete。登出或清理临时认证时会走这里,不会碰磁盘或系统钥匙串。

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

create_auth_storage445–452 ↗
fn create_auth_storage(
    codex_home: PathBuf,
    mode: AuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Arc<dyn AuthStorageBackend>

作用:按照配置创建合适的认证存储对象。上层只要告诉它想用文件、钥匙串、自动还是临时模式即可。

数据流:输入是 codex_home、存储模式和钥匙串后端类型 → 它创建默认钥匙串访问对象,再调用 create_auth_storage_with_store → 输出一个统一的 AuthStorageBackend 对象。

调用关系:它被 load_auth、save_auth、logout 等高层认证流程调用,是外部进入本文件存储体系的主要入口。实际分支选择交给 create_auth_storage_with_store。

调用图:调用 1 个内部函数(create_auth_storage_with_store);被 6 处调用(create_dummy_chatgpt_auth_for_testing, from_auth_dot_json, load_auth, load_auth_dot_json, logout, save_auth);外部调用 1 个(new)。

create_auth_storage_with_store454–472 ↗
fn create_auth_storage_with_store(
    codex_home: PathBuf,
    mode: AuthCredentialsStoreMode,
    keyring_store: Arc<dyn KeyringStore>,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Arc<dyn

作用:根据存储模式组装具体后端,并允许传入自定义钥匙串对象。这个设计让测试可以塞入假的钥匙串。

数据流:输入是 codex_home、存储模式、keyring_store 和钥匙串后端类型 → File 模式创建 FileAuthStorage,Keyring 模式创建钥匙串后端,Auto 模式创建 AutoAuthStorage,Ephemeral 模式创建 EphemeralAuthStorage → 输出统一接口对象。

调用关系:它由 create_auth_storage 调用,也是测试中更容易控制依赖的工厂函数。遇到 Keyring 模式时,会把工作继续交给 create_keyring_auth_storage。

调用图:调用 4 个内部函数(new, new, new, create_keyring_auth_storage);被 1 处调用(create_auth_storage);外部调用 1 个(new)。

create_keyring_auth_storage474–487 ↗
fn create_keyring_auth_storage(
    codex_home: PathBuf,
    keyring_store: Arc<dyn KeyringStore>,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Arc<dyn AuthStorageBackend>

作用:在两种钥匙串方案之间做选择:直接写系统钥匙串,或通过 SecretsManager 加密保存。

数据流:输入是 codex_home、keyring_store 和 keyring_backend_kind → 如果类型是 Direct,就创建 DirectKeyringAuthStorage;如果是 Secrets,就创建 SecretsKeyringAuthStorage → 输出统一的 AuthStorageBackend 对象。

调用关系:它被 AutoAuthStorage::new 和 create_auth_storage_with_store 调用。它把“钥匙串到底是哪一种实现”这个选择集中在一个地方,其他流程只拿统一接口使用。

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

login/src/auth/bedrock_api_key.rs源码 ↗
domain_logiclogin/auth setup

这个文件解决的是登录信息落地保存的问题。用户选择 Amazon Bedrock API key 登录后,程序不能只把 key 放在内存里,否则一关掉就丢了;也不能混着保留别的登录方式,否则以后启动时可能不知道该用哪套身份。这里定义了 BedrockApiKeyAuth,里面只放两样东西:api_key 和 region,也就是钥匙和它对应的地区。login_with_bedrock_api_key 会新建一份 auth.json 的内容,把登录模式明确标成 BedrockApiKey,并把其他 OpenAI token、个人访问令牌等字段都清空,只留下 Bedrock 的认证信息。最后它把保存动作交给 save_auth,由后者按配置决定写到普通文件、钥匙串一类的安全存储里。可以把它理解成给系统换了一张“只写 Bedrock 凭证”的身份证。

函数细节1
login_with_bedrock_api_key20–45 ↗
fn login_with_bedrock_api_key(
    codex_home: &Path,
    api_key: &str,
    region: &str,
    auth_credentials_store_mode: AuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,

作用:这个函数在用户用 Amazon Bedrock API key 登录时调用。它把 API key 和地区整理成标准的认证文件内容,并保存起来,方便以后程序启动时直接使用。

数据流:进去的是 Codex 的主目录位置、用户输入的 api_key、region,以及“凭证该怎么存”的设置和钥匙串后端类型。它先组装出一份 AuthDotJson:登录方式设为 BedrockApiKey,其他登录字段全部留空,只填入 Bedrock 的 api_key 和 region。出来的是保存结果:成功就是空结果,失败就返回输入输出错误;实际改动是认证信息被写入 auth.json 或对应的安全存储。

调用关系:它是 Bedrock API key 登录流程里负责“准备并提交凭证”的一步。它自己不直接碰磁盘或钥匙串,而是把组装好的认证内容交给 save_auth;save_auth 再根据传入的存储模式和后端类型完成真正的保存工作。

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

login/src/auth/revoke.rs源码 ↗
domain_logiclogout / teardown

这个文件处理的是“退出登录时顺手注销服务器端令牌”这件事。令牌可以理解成门票:有了它,程序就能代表用户访问服务。退出时不能只把本地门票扔掉,最好也告诉发票处“这张票作废”。它会先看本地 auth.json 里是不是 ChatGPT 托管登录,而不是 API Key 登录;只有前者才有这里要撤销的 OAuth 令牌。它优先撤销 refresh token(刷新令牌,用来换新门票,权限更关键),没有的话才撤销 access token(访问令牌,直接用来访问服务)。真正撤销时,它发一个带 10 秒超时的 HTTP POST 请求,避免退出登录被网络拖死。撤销地址可以用环境变量改,方便测试或私有部署;如果只改了刷新令牌地址,它还能自动推导出撤销地址。整体是“尽力而为”:失败会返回错误信息,但设计上调用方仍会清掉本地登录状态。

函数细节11
RevokeTokenKind::as_str31–36 ↗
fn as_str(self) -> &'static str

作用:把“要撤销的是访问令牌还是刷新令牌”翻译成服务器接口认识的文字。别人用它是为了在请求里告诉服务器,这个 token 大概是哪一类。

数据流:进去的是一个令牌种类:Access 或 Refresh → 它做一次简单判断 → 出来的是字符串 access_token 或 refresh_token,不改任何外部状态。

调用关系:它是发送撤销请求前的小翻译器。revoke_oauth_token 在组装 HTTP 请求和错误消息时会用它,让请求内容和报错文字都说清楚正在撤销哪种令牌。

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

RevokeTokenKind::client_id38–43 ↗
fn client_id(self) -> Option<String>

作用:决定撤销请求里要不要带客户端编号。刷新令牌需要带上 OAuth 客户端 ID,访问令牌则不带。

数据流:进去的是令牌种类 → 如果是 Refresh,它调用 oauth_client_id 拿到当前应用的客户端编号;如果是 Access,就返回空 → 出来的是一个可有可无的 client_id 字段。

调用关系:它服务于 revoke_oauth_token。撤销请求要符合 OAuth 服务器的要求,所以 revoke_oauth_token 会把这里给出的 client_id 放进请求体里。

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

revoke_auth_tokens54–64 ↗
async fn revoke_auth_tokens(
    auth_dot_json: Option<&AuthDotJson>,
) -> Result<(), std::io::Error>

作用:这是退出登录时对外使用的主入口:从本地登录信息里找一个值得撤销的令牌,然后尝试通知服务器作废它。没有可撤销的令牌时,它什么也不做并直接成功。

数据流:进去的是可能存在的 AuthDotJson,也就是本地保存的登录信息 → 它先找出该撤销的 token 和类型;找不到就结束 → 找到后创建 HTTP 客户端,确定撤销接口地址,再调用 revoke_oauth_token 发请求 → 出来是成功或一个输入输出错误;它本身不删除本地文件。

调用关系:它被 logout_with_revoke 在退出登录流程中调用。它负责把“本地有什么令牌”“该打哪个地址”“用哪个 HTTP 客户端发请求”串起来,最后把真正的网络请求交给 revoke_oauth_token。

调用图:调用 3 个内部函数(create_client, revoke_oauth_token, revoke_token_endpoint);被 2 处调用(logout_with_revoke, logout_with_revoke)。

revocable_token66–75 ↗
fn revocable_token(auth_dot_json: &AuthDotJson) -> Option<(&str, RevokeTokenKind)>

作用:从本地登录信息里挑出最应该撤销的那张“票”。它优先选择刷新令牌,因为刷新令牌能不断换出新的访问令牌,更需要作废。

数据流:进去的是 AuthDotJson → 它先通过 managed_chatgpt_tokens 确认这是托管 ChatGPT 登录并拿到 token 数据 → 如果 refresh_token 不为空就返回它和 Refresh 类型;否则如果 access_token 不为空就返回它和 Access 类型;都没有就返回空。

调用关系:它是撤销流程前面的筛选步骤。revoke_auth_tokens 需要先知道“有没有令牌、撤销哪一个”,这个函数就负责给出答案;它把判断登录模式的工作交给 managed_chatgpt_tokens。

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

managed_chatgpt_tokens77–83 ↗
fn managed_chatgpt_tokens(auth_dot_json: &AuthDotJson) -> Option<&TokenData>

作用:确认这份本地登录信息是不是 ChatGPT 托管登录,并在确认后取出里面的 OAuth 令牌。API Key 登录不走这套撤销流程,所以会被排除。

数据流:进去的是 AuthDotJson → 它调用 resolved_auth_mode 判断实际登录方式 → 如果是 Chatgpt,就返回里面保存的 TokenData;否则返回空 → 不修改任何数据。

调用关系:它夹在 revocable_token 和 resolved_auth_mode 中间。revocable_token 只想处理可撤销的 OAuth token,于是让它先把不相关的 API Key 登录过滤掉。

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

resolved_auth_mode85–93 ↗
fn resolved_auth_mode(auth_dot_json: &AuthDotJson) -> ApiAuthMode

作用:判断本地登录信息到底代表哪种登录方式。它处理了老配置或不完整配置的情况,避免因为字段缺失就判断错。

数据流:进去的是 AuthDotJson → 如果里面明确写了 auth_mode,就直接用;如果没写但有 openai_api_key,就认为是 ApiKey;如果两者都没有,就默认认为是 Chatgpt → 出来是一个明确的登录模式。

调用关系:它给 managed_chatgpt_tokens 提供基础判断。撤销令牌前必须先分清是 ChatGPT 登录还是 API Key 登录,这个函数就是这一步的裁判。

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

revoke_oauth_token95–130 ↗
async fn revoke_oauth_token(
    client: &CodexHttpClient,
    endpoint: &str,
    token: &str,
    kind: RevokeTokenKind,
    timeout: Duration,
) -> Result<(), std::io::Error>

作用:真正向 OAuth 服务器发送“请作废这个令牌”的网络请求。它也负责处理超时、服务器失败响应和错误消息。

数据流:进去的是 HTTP 客户端、撤销地址、token 字符串、token 类型和超时时间 → 它组装 JSON 请求体,填入 token、token_type_hint,以及必要时的 client_id → 用 POST 发到服务器 → 如果服务器返回成功状态,就返回成功;如果失败,就读取响应正文,尝试解析出人能看懂的错误,再返回一个错误。

调用关系:它是这个文件里真正碰网络的核心步骤。revoke_auth_tokens 会在准备好客户端和地址后调用它;测试 tests::revoke_request_times_out 也直接调用它,确认网络卡住时会按预期超时。它内部依赖 RevokeTokenKind::as_str、RevokeTokenKind::client_id 和 try_parse_error_message 来把请求和错误做得完整。

调用图:调用 4 个内部函数(post, as_str, client_id, try_parse_error_message);被 2 处调用(revoke_auth_tokens, revoke_request_times_out);外部调用 2 个(other, format!)。

revoke_token_endpoint132–144 ↗
fn revoke_token_endpoint() -> String

作用:决定撤销令牌请求应该发到哪个网址。它允许环境变量覆盖默认地址,方便测试、开发环境或特殊部署。

数据流:进去没有显式参数,但会读取环境变量 → 如果设置了专门的撤销地址覆盖值,就直接用它;否则如果设置了刷新令牌地址覆盖值,就尝试从它推导撤销地址;都没有就使用内置默认地址 → 出来是一个网址字符串。

调用关系:它在 revoke_auth_tokens 发送请求前被调用。revoke_auth_tokens 不自己关心配置细节,只向它要最终 endpoint;如果需要从刷新地址推导撤销地址,它会把这一步交给 derive_revoke_token_endpoint。

调用图:调用 1 个内部函数(derive_revoke_token_endpoint);被 1 处调用(revoke_auth_tokens);外部调用 1 个(var)。

derive_revoke_token_endpoint146–151 ↗
fn derive_revoke_token_endpoint(refresh_endpoint: &str) -> Option<String>

作用:从“刷新令牌”的网址推导出“撤销令牌”的网址。这样测试或自定义服务器只配一个基础地址时,也能自动找到撤销接口。

数据流:进去的是一个刷新令牌地址字符串 → 它先尝试把字符串解析成 URL;解析失败就返回空 → 成功后把路径改成 /oauth/revoke,并清掉查询参数 → 出来是新的撤销地址字符串。

调用关系:它是 revoke_token_endpoint 的辅助工具。只有在没有专门配置撤销地址、但配置了刷新地址时,revoke_token_endpoint 才会请它帮忙推导。测试 tests::derives_revoke_url_from_refresh_token_override 会验证这个推导是否正确。

调用图:被 1 处调用(revoke_token_endpoint);外部调用 1 个(parse)。

tests::derives_revoke_url_from_refresh_token_override164–169 ↗
fn derives_revoke_url_from_refresh_token_override()

作用:这个测试确认:给一个带查询参数的刷新令牌地址,程序能正确推导出撤销令牌地址。它防止以后有人改代码时把地址拼错。

数据流:进去的是测试里写死的示例地址 http://127.0.0.1:1234/oauth/token?unified=true → 它调用 derive_revoke_token_endpoint → 对比结果是不是 http://127.0.0.1:1234/oauth/revoke → 测试通过或失败,不影响真实运行数据。

调用关系:它只在测试时运行,专门盯住 derive_revoke_token_endpoint 这个小工具。这样 revoke_token_endpoint 依赖这个推导逻辑时会更可靠。

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

tests::revoke_request_times_out172–199 ↗
async fn revoke_request_times_out()

作用:这个测试确认:如果撤销令牌的服务器迟迟不回应,请求会超时,而不是一直卡着。退出登录不能因为网络不动就无限等待。

数据流:进去的是测试搭好的假服务器和一个很短的超时时间 → 假服务器收到 POST /oauth/revoke 后故意延迟 60 秒 → 测试调用 revoke_oauth_token,并把超时设成 20 毫秒 → 结果应该得到一个超时错误,而且错误里保留了底层 reqwest 的超时信息。

调用关系:它只在测试环境里运行,用假 HTTP 服务器模拟坏网络。它直接检查 revoke_oauth_token 的关键安全行为:网络请求必须尊重 timeout,不能拖死退出流程。

调用图:调用 3 个内部函数(new, new, revoke_oauth_token);外部调用 10 个(from_millis, from_secs, given, start, new, assert!, format!, skip_if_no_network!, method, path)。

MCP OAuth 登录与存储

这些文件为 MCP HTTP 服务器实现交互式 OAuth 登录,以及支持该功能的凭据持久化和刷新机制。

rmcp-client/src/perform_oauth_login.rs源码 ↗
orchestration登录认证期间,从发起 OAuth 登录到保存令牌后结束

OAuth 可以理解成“让用户去网页登录确认,然后网站把一张临时通行证交回给程序”。这个文件就是这整套流程的指挥台。它先在本机开一个很小的 HTTP 回调服务器,生成授权地址,再让用户用浏览器打开。授权服务完成后,会带着 code 和 state 回跳到本机地址;代码会检查路径、解析参数,确认不是跑错门。之后它把 code 交给 OAuth 库换成真正的令牌,计算过期时间,并按配置保存到文件或系统钥匙串里。它还支持两种用法:一种直接打开浏览器完成登录,另一种只返回授权链接,让外部界面自己展示并等待完成。

函数细节41
CallbackServerGuard::drop45–47 ↗
fn drop(&mut self)

作用:在回调服务器不再需要时,把它从等待请求的状态里叫醒并停止。这样登录结束或出错后,不会留下一个卡住的本地小服务器。

数据流:进去的是 guard 里保存的本地服务器引用 → 它调用服务器的 unblock,让正在等待浏览器回跳的接收循环退出 → 出来没有返回值,但本地监听会被释放。

调用关系:它不是被普通代码直接调用,而是在 CallbackServerGuard 被丢弃时自动运行。OauthLoginFlow::finish 结束前会丢弃这个 guard,确保回调服务器收尾。

OAuthProviderError::new57–62 ↗
fn new(error: Option<String>, error_description: Option<String>) -> Self

作用:把 OAuth 服务商返回的错误代码和错误说明包成一个统一的错误对象。这样后面显示错误时,不用到处拼字符串。

数据流:进去的是可选的 error 和 error_description → 它原样保存到 OAuthProviderError 结构里 → 出来是一个可以继续传递、打印的错误对象。

调用关系:parse_oauth_callback 在浏览器回跳里看到 error 参数时会调用它,把网页授权失败转成程序内部能理解的错误。

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

OAuthProviderError::fmt66–75 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:把 OAuthProviderError 变成人能看懂的一句话。比如有错误码和说明时,会写成“服务商返回某错误:某说明”。

数据流:进去的是错误对象和格式化输出器 → 它根据有没有错误码、有没有说明选择不同文案 → 出来是写入输出器的错误文字。

调用关系:当错误被打印、记录或包装进 anyhow 错误时会用到它;spawn_callback_server 也会把这个错误文字回给浏览器。

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

perform_oauth_login81–109 ↗
async fn perform_oauth_login(
    server_name: &str,
    server_url: &str,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
    http_headers: Option<HashMap

作用:这是最常用的 OAuth 登录入口。它会正常显示授权链接,并尝试打开浏览器,让用户完成登录。

数据流:进去的是服务器名、服务器地址、保存方式、请求头、权限范围、客户端 ID、回调地址等配置 → 它把这些转交给带显示开关的内部函数,并指定要显示浏览器链接 → 出来是登录成功或失败的结果。

调用关系:外部命令或上层客户端会调用它发起登录;它自己不做细节,而是把流程交给 perform_oauth_login_with_browser_output。

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

perform_oauth_login_silent112–140 ↗
async fn perform_oauth_login_silent(
    server_name: &str,
    server_url: &str,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
    http_headers: Option<

作用:这是“安静版”登录入口。它仍会尝试打开浏览器,但默认不把授权链接打印出来,适合不想在终端暴露链接的场景。

数据流:进去的是与普通登录相同的一组配置 → 它调用内部登录函数,并把“是否输出浏览器链接”设为 false → 出来是登录是否完成的结果。

调用关系:上层如果想少输出信息,会调用它;真正的搭建流程仍由 perform_oauth_login_with_browser_output 和 OauthLoginFlow 完成。

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

perform_oauth_login_with_browser_output143–178 ↗
async fn perform_oauth_login_with_browser_output(
    server_name: &str,
    server_url: &str,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
    http_hea

作用:这是普通登录和静默登录共用的内部入口。它负责创建登录流程,然后把流程跑到结束。

数据流:进去的是登录所需配置,加上 emit_browser_url 这个是否打印链接的开关 → 它整理请求头,创建 OauthLoginFlow,再调用 finish 等待回调、换令牌、保存令牌 → 出来是成功或具体错误。

调用关系:perform_oauth_login 和 perform_oauth_login_silent 都会调用它;它往下依赖 OauthLoginFlow::new 搭建环境,再由 OauthLoginFlow::finish 执行收尾。

调用图:调用 1 个内部函数(new);被 2 处调用(perform_oauth_login, perform_oauth_login_silent)。

perform_oauth_login_return_url181–219 ↗
async fn perform_oauth_login_return_url(
    server_name: &str,
    server_url: &str,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
    http_headers: Opt

作用:这个入口不自动打开浏览器,而是返回授权网址和一个等待完成的句柄。它适合图形界面或别的程序自己展示链接、自己控制等待时机。

数据流:进去的是服务器、保存、请求头、权限、回调和超时配置 → 它创建不自动开浏览器的 OauthLoginFlow,取出授权 URL,并把登录流程放到后台任务里跑 → 出来是 OauthLoginHandle,里面有 URL 和完成信号。

调用关系:外部需要“拿到链接再说”的场景会调用它;它调用 OauthLoginFlow::new 建流程,再用 OauthLoginFlow::spawn 后台完成,最后用 OauthLoginHandle::new 包装给调用者。

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

spawn_callback_server221–264 ↗
fn spawn_callback_server(
    server: Arc<Server>,
    tx: oneshot::Sender<CallbackResult>,
    expected_callback_path: String,
)

作用:启动本地回调服务器的接待员。它等浏览器授权后访问本机地址,然后把收到的 code 或错误交回主登录流程。

数据流:进去的是本地服务器、一次性发送器和期望的回调路径 → 它在阻塞线程里循环接收 HTTP 请求,解析路径和查询参数,给浏览器返回成功或错误页面,并通过通道发回结果 → 出来没有直接返回值,但会发送 CallbackResult。

调用关系:OauthLoginFlow::new 在准备好回调地址后调用它。它把网络请求交给 parse_oauth_callback 判断,并用 oneshot 通道把结果送给 OauthLoginFlow::finish。

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

parse_oauth_callback285–324 ↗
fn parse_oauth_callback(path: &str, expected_callback_path: &str) -> CallbackOutcome

作用:检查浏览器回跳地址是不是本次登录要等的地址,并从里面取出授权 code、state 或错误信息。它像门卫,只放正确门牌号的回调进来。

数据流:进去的是请求路径和期望路径 → 它先分出路径和问号后的参数,确认路径一致,再逐个解码 code、state、error、error_description → 出来是成功、服务商错误或无效请求三种结果之一。

调用关系:spawn_callback_server 每收到一个浏览器请求都会用它判断。多个测试函数专门验证它接受正确路径、拒绝错误路径,并能识别服务商返回的错误。

调用图:调用 1 个内部函数(new);被 6 处调用(parse_oauth_callback_accepts_callback_id_path, parse_oauth_callback_accepts_custom_path, parse_oauth_callback_accepts_default_path, parse_oauth_callback_rejects_missing_callback_id_path, parse_oauth_callback_rejects_wrong_path, parse_oauth_callback_returns_provider_error);外部调用 3 个(Error, Success, decode)。

OauthLoginHandle::new332–337 ↗
fn new(authorization_url: String, completion: oneshot::Receiver<Result<()>>) -> Self

作用:把授权网址和后台登录完成信号装成一个句柄,方便外部拿着用。句柄可以理解成“登录流程的遥控器”。

数据流:进去的是 authorization_url 和 completion 接收器 → 它保存这两样东西 → 出来是 OauthLoginHandle。

调用关系:perform_oauth_login_return_url 在创建后台登录流程后调用它,把 URL 和等待通道一起交给调用者。

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

OauthLoginHandle::authorization_url339–341 ↗
fn authorization_url(&self) -> &str

作用:让调用者读取需要打开的授权网址,但不拿走句柄本身。适合界面上展示“请打开这个链接”。

数据流:进去的是句柄自身 → 它返回内部保存的授权 URL 的只读引用 → 不改变任何状态。

调用关系:使用 perform_oauth_login_return_url 的外部代码通常会先调用它拿链接,然后再调用 wait 或 into_parts 等待登录结果。

OauthLoginHandle::into_parts343–345 ↗
fn into_parts(self) -> (String, oneshot::Receiver<Result<()>>)

作用:把句柄拆开,交出授权网址和完成信号。适合调用者想自己管理等待流程的情况。

数据流:进去的是整个 OauthLoginHandle → 它消费掉句柄,把 authorization_url 和 completion 拆出来 → 出来是一个二元组。

调用关系:这是给高级调用者用的出口;它不继续调用别的登录逻辑,只是把 perform_oauth_login_return_url 包好的东西拆回去。

OauthLoginHandle::wait347–351 ↗
async fn wait(self) -> Result<()>

作用:等待后台 OAuth 登录真正完成。调用者拿到链接并让用户打开后,可以用它等最终成功或失败。

数据流:进去的是句柄自身 → 它等待 completion 通道收到结果;如果后台任务被取消,就改成清楚的错误 → 出来是登录成功或失败。

调用关系:它等待的是 OauthLoginFlow::spawn 里后台任务发送的结果。外部不想自己处理通道时,会直接调用这个方法。

resolve_callback_port367–378 ↗
fn resolve_callback_port(callback_port: Option<u16>) -> Result<Option<u16>>

作用:检查用户配置的本地回调端口是否有效。端口是电脑上服务的“门牌号”,0 在这里不允许作为固定配置。

数据流:进去的是可选端口 → 如果有端口且是 0,就返回错误;如果有合法端口就保留;如果没给就返回 None,让系统自动分配 → 出来是可用端口配置。

调用关系:OauthLoginFlow::new 在启动本地回调服务器前调用它,避免绑定到明显错误的端口。

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

local_redirect_uri380–395 ↗
fn local_redirect_uri(server: &Server) -> Result<String>

作用:根据本地回调服务器实际监听的地址,生成 OAuth 服务商要回跳的地址。没有手动 callback_url 时就靠它。

数据流:进去的是已经启动的 tiny_http 服务器 → 它读取服务器实际地址和端口,拼成 http://地址:端口/callback,IPv6 会加方括号 → 出来是 redirect URI 字符串,失败时返回错误。

调用关系:resolve_redirect_uri 在用户没有指定回调 URL 时调用它。OauthLoginFlow::new 间接用它得到可注册给 OAuth 流程的回跳地址。

调用图:被 1 处调用(resolve_redirect_uri);外部调用 3 个(server_addr, anyhow!, format!)。

resolve_redirect_uri397–404 ↗
fn resolve_redirect_uri(server: &Server, callback_url: Option<&str>) -> Result<String>

作用:决定这次 OAuth 应该使用哪个回调地址:用户给了就校验后使用,没给就用本机自动生成的地址。

数据流:进去的是本地服务器和可选 callback_url → 如果没有 URL,就调用 local_redirect_uri;如果有 URL,就解析确认它是合法网址 → 出来是最终 redirect URI。

调用关系:OauthLoginFlow::new 在启动服务器后调用它,后面会继续给这个地址追加 callback_id,并交给授权流程。

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

callback_id_from_server_url406–416 ↗
fn callback_id_from_server_url(server_url: &str) -> Result<String>

作用:从 MCP 服务器地址生成一个短的回调标识。这样不同服务器的登录回调路径不容易混在一起。

数据流:进去的是 server_url → 它解析 URL,确认有主机名,去掉 fragment 片段,对剩余 URL 做 SHA-256 摘要,再取前 9 字节做 URL 安全的 base64 编码 → 出来是 12 个字符左右的 callback_id。

调用关系:OauthLoginFlow::new 用它把服务器身份绑定到回调路径;测试 callback_id_is_bound_to_server_url 验证同一地址和不同地址会得到符合预期的标识。

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

append_callback_id_to_redirect_uri418–429 ↗
fn append_callback_id_to_redirect_uri(redirect_uri: &str, callback_id: &str) -> Result<String>

作用:把 callback_id 加到回调地址路径后面,让回调 URL 更具体。比如 /callback 变成 /callback/abc123。

数据流:进去的是 redirect_uri 和 callback_id → 它解析 URL,检查原路径是否以斜杠结尾,再把标识追加到路径末尾,同时保留查询参数 → 出来是新的 redirect URI。

调用关系:OauthLoginFlow::new 生成 callback_id 后调用它。相关测试确认它能追加到普通路径,也能放在查询参数前面。

调用图:被 3 处调用(new, callback_id_is_appended_before_redirect_uri_query, callback_id_is_appended_to_redirect_uri_path);外部调用 2 个(parse, format!)。

callback_path_from_redirect_uri431–435 ↗
fn callback_path_from_redirect_uri(redirect_uri: &str) -> Result<String>

作用:从完整回调网址里只取路径部分,用来匹配浏览器真正访问的是不是正确回调地址。

数据流:进去的是 redirect_uri → 它解析 URL 并取出 path → 出来是类似 /callback/abc123 的路径字符串。

调用关系:OauthLoginFlow::new 用它得到 expected_callback_path,然后传给 spawn_callback_server;测试 callback_path_comes_from_redirect_uri 验证结果。

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

callback_bind_host437–450 ↗
fn callback_bind_host(callback_url: Option<&str>) -> &'static str

作用:决定本地回调服务器应该只监听本机,还是监听所有网卡。这样既能支持自定义回调地址,也尽量避免不必要地暴露端口。

数据流:进去的是可选 callback_url → 没给、解析失败、或主机是 localhost/127.0.0.1/::1 时返回 127.0.0.1;如果是别的主机名,就返回 0.0.0.0 → 出来是绑定用的主机地址。

调用关系:OauthLoginFlow::new 在启动本地 HTTP 服务器前调用它,决定 Server::http 要绑定到哪个地址。

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

OauthLoginFlow::new454–526 ↗
async fn new(
        server_name: &str,
        server_url: &str,
        store_mode: OAuthCredentialsStoreMode,
        keyring_backend_kind: AuthKeyringBackendKind,
        headers: OauthHeaders,

作用:搭好一次 OAuth 登录所需的所有舞台:本地回调服务器、回调地址、HTTP 客户端、授权状态和授权链接。它只是准备,不负责最终等待和保存。

数据流:进去的是服务器信息、保存方式、请求头、权限范围、客户端 ID、资源参数、回调配置、是否开浏览器、超时等 → 它选择绑定地址和端口,启动本地服务器,生成带 callback_id 的回调 URI,启动回调监听,构建 HTTP 客户端,向 OAuth 端发起授权准备,并生成最终授权 URL → 出来是 OauthLoginFlow。

调用关系:perform_oauth_login_with_browser_output 和 perform_oauth_login_return_url 都先调用它。它把细活分给 resolve_callback_port、resolve_redirect_uri、callback_id_from_server_url、append_callback_id_to_redirect_uri、callback_path_from_redirect_uri、spawn_callback_server、start_authorization 和 append_query_param。

调用图:调用 11 个内部函数(append_callback_id_to_redirect_uri, append_query_param, callback_bind_host, callback_id_from_server_url, callback_path_from_redirect_uri, resolve_callback_port, resolve_redirect_uri, spawn_callback_server, start_authorization, apply_default_headers (+1 more));被 2 处调用(perform_oauth_login_return_url, perform_oauth_login_with_browser_output);外部调用 7 个(clone, new, new, from_secs, http, format!, channel)。

OauthLoginFlow::authorization_url528–530 ↗
fn authorization_url(&self) -> String

作用:取出这次登录需要用户打开的授权链接。返回的是拷贝,所以调用者拿去展示不会影响流程内部状态。

数据流:进去的是已经准备好的 OauthLoginFlow → 它复制内部 auth_url → 出来是授权 URL 字符串。

调用关系:perform_oauth_login_return_url 用它把链接放进 OauthLoginHandle。普通自动登录流程一般不需要单独调用它。

OauthLoginFlow::finish532–599 ↗
async fn finish(mut self, emit_browser_url: bool) -> Result<()>

作用:真正跑完 OAuth 登录:提示或打开浏览器,等待回调,换取令牌,然后保存。它是这条登录流水线的最后一段。

数据流:进去的是已经准备好的登录流程和是否打印链接的开关 → 它按需打印并打开浏览器,等待本地回调通道在超时时间内收到 code 或错误;收到 code 后交给 OAuthState 验证并换令牌,再读取 client_id 和凭据,计算过期时间,包装成 StoredOAuthTokens 并保存 → 出来是成功或详细失败,同时会关闭回调服务器。

调用关系:perform_oauth_login_with_browser_output 会直接等待它完成;OauthLoginFlow::spawn 会在后台调用它。它依赖回调服务器送来的结果,也调用 compute_expires_at_millis 和 save_oauth_tokens 完成令牌落盘。

调用图:调用 1 个内部函数(compute_expires_at_millis);被 1 处调用(spawn);外部调用 9 个(get_credentials, handle_callback, anyhow!, save_oauth_tokens, eprintln!, println!, new, timeout, open)。

OauthLoginFlow::spawn601–618 ↗
fn spawn(self) -> oneshot::Receiver<Result<()>>

作用:把登录收尾流程放到后台任务里跑,并返回一个能等待结果的通道。这样调用者可以先拿到授权链接,不被整个登录过程卡住。

数据流:进去的是 OauthLoginFlow → 它创建一次性通道,启动 tokio 后台任务,在任务里调用 finish;如果失败就打印错误,最后把结果发回通道 → 出来是接收登录结果的 oneshot Receiver。

调用关系:perform_oauth_login_return_url 调用它来支持“返回 URL 后后台等待”。它把主要工作交给 OauthLoginFlow::finish。

调用图:调用 1 个内部函数(finish);外部调用 3 个(eprintln!, channel, spawn)。

start_authorization621–650 ↗
async fn start_authorization(
    server_url: &str,
    http_client: reqwest::Client,
    scopes: &[&str],
    redirect_uri: &str,
    oauth_client_id: Option<&str>,
) -> Result<OAuthState>

作用:向 OAuth 服务端发起授权准备,拿到授权页面地址和后续换令牌所需的状态。它处理“自动注册客户端”和“使用指定客户端 ID”两种路线。

数据流:进去的是服务器 URL、HTTP 客户端、权限范围、回调 URI 和可选客户端 ID → 如果没给有效客户端 ID,就用 OAuthState 自己启动授权;如果给了,就用 AuthorizationManager 发现服务端元数据、配置客户端、生成授权 URL,并包装成 OAuthState::Session → 出来是 OAuthState。

调用关系:OauthLoginFlow::new 调用它获得 OAuth 状态和授权链接。测试 start_authorization_uses_configured_client_id 用假元数据服务器验证指定 client_id 会进入授权地址。

调用图:被 2 处调用(new, start_authorization_uses_configured_client_id);外部调用 5 个(new, for_scope_upgrade, new, Session, new)。

append_query_param652–667 ↗
fn append_query_param(url: &str, key: &str, value: Option<&str>) -> String

作用:给授权 URL 追加一个查询参数,比如给 OAuth 授权地址加 resource。空值会被忽略,避免生成没意义的参数。

数据流:进去的是原 URL、参数名和可选参数值 → 如果值不存在或全是空白,就原样返回;如果 URL 能正常解析,就用 URL 工具安全追加;如果 URL 解析不了,就手动编码后拼接 ? 或 & → 出来是新 URL 字符串。

调用关系:OauthLoginFlow::new 用它给授权地址追加 resource。相关测试覆盖正常 URL、空值和不可解析 URL 三种情况。

调用图:被 4 处调用(new, append_query_param_adds_resource_to_absolute_url, append_query_param_handles_unparseable_url, append_query_param_ignores_empty_values);外部调用 3 个(parse, format!, encode)。

tests::spawn_oauth_metadata_server688–723 ↗
async fn spawn_oauth_metadata_server() -> String

作用:为测试启动一个假的 OAuth 元数据服务器。它不做真实登录,只提供测试需要的授权端点和令牌端点地址。

数据流:进去没有参数 → 它绑定本机随机端口,构造两条 well-known 元数据路由,后台启动 axum 服务 → 出来是这个假服务器的 base_url。

调用关系:tests::start_authorization_uses_configured_client_id 调用它,给 start_authorization 一个可访问的测试 OAuth 服务端。

调用图:外部调用 7 个(new, bind, get, serve, format!, json!, spawn)。

tests::start_authorization_uses_configured_client_id726–749 ↗
async fn start_authorization_uses_configured_client_id()

作用:验证当用户配置了 OAuth 客户端 ID 时,生成的授权 URL 里真的带着这个 ID。

数据流:进去是测试框架启动的异步测试环境 → 它启动假元数据服务器,调用 start_authorization,再解析得到的授权 URL,取出 client_id 参数 → 出来是断言通过或测试失败。

调用关系:它覆盖 start_authorization 的指定客户端 ID 分支,防止以后改代码时把用户配置的 client_id 丢掉。

调用图:调用 2 个内部函数(new, start_authorization);外部调用 4 个(parse, assert_eq!, format!, spawn_oauth_metadata_server)。

tests::parse_oauth_callback_accepts_default_path752–755 ↗
fn parse_oauth_callback_accepts_default_path()

作用:验证默认 /callback 路径带 code 和 state 时会被接受。

数据流:进去是固定的测试回调路径字符串 → 它调用 parse_oauth_callback → 出来用断言确认结果是 Success。

调用关系:它保护 parse_oauth_callback 的基本成功场景,确保默认回调地址可用。

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

tests::parse_oauth_callback_accepts_custom_path758–761 ↗
fn parse_oauth_callback_accepts_custom_path()

作用:验证用户自定义的回调路径也能被接受。这样配置了 /oauth/callback 之类路径时不会误判。

数据流:进去是自定义路径的测试 URL 和期望路径 → 它调用 parse_oauth_callback → 出来断言结果为 Success。

调用关系:它补充覆盖 parse_oauth_callback 的自定义路径匹配行为。

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

tests::parse_oauth_callback_accepts_callback_id_path764–768 ↗
fn parse_oauth_callback_accepts_callback_id_path()

作用:验证带 callback_id 的回调路径会被接受。这是防止不同服务器登录串线的重要路径格式。

数据流:进去是 /callback/abc123 加 code、state 的测试路径 → 它调用 parse_oauth_callback → 出来断言成功。

调用关系:它对应 OauthLoginFlow::new 追加 callback_id 后的真实回调形式。

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

tests::parse_oauth_callback_rejects_missing_callback_id_path771–774 ↗
fn parse_oauth_callback_rejects_missing_callback_id_path()

作用:验证期望带 callback_id 时,少了这个标识的 /callback 会被拒绝。

数据流:进去是不带 callback_id 的实际路径和带 callback_id 的期望路径 → 它调用 parse_oauth_callback → 出来断言结果是 Invalid。

调用关系:它保护 parse_oauth_callback 的严格路径检查,避免旧路径误接收新流程的回调。

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

tests::parse_oauth_callback_rejects_wrong_path777–780 ↗
fn parse_oauth_callback_rejects_wrong_path()

作用:验证路径不一致的回调会被拒绝。哪怕里面有 code 和 state,也不能算本次登录成功。

数据流:进去是实际 /callback 和期望 /oauth/callback → 它调用 parse_oauth_callback → 出来断言 Invalid。

调用关系:它确保 spawn_callback_server 依赖 parse_oauth_callback 时,不会接收跑错地址的请求。

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

tests::parse_oauth_callback_returns_provider_error783–796 ↗
fn parse_oauth_callback_returns_provider_error()

作用:验证 OAuth 服务商返回 error 和 error_description 时,会被解析成明确的错误,而不是无效请求。

数据流:进去是带 invalid_scope 和说明的测试回调路径 → 它调用 parse_oauth_callback → 出来断言得到 CallbackOutcome::Error,且错误内容一致。

调用关系:它覆盖 parse_oauth_callback 调用 OAuthProviderError::new 的分支,保证用户能看到服务商给出的失败原因。

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

tests::callback_path_comes_from_redirect_uri799–803 ↗
fn callback_path_comes_from_redirect_uri()

作用:验证完整回调 URL 能正确提取出路径部分。

数据流:进去是 https://example.com/oauth/callback → 它调用 callback_path_from_redirect_uri → 出来断言路径是 /oauth/callback。

调用关系:它保护 OauthLoginFlow::new 后续传给 spawn_callback_server 的 expected_callback_path 是正确的。

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

tests::callback_id_is_bound_to_server_url806–829 ↗
fn callback_id_is_bound_to_server_url()

作用:验证 callback_id 确实由服务器 URL 决定:fragment 不影响它,但路径、查询参数和端口变化会影响它。

数据流:进去是几组相似或不同的 server_url → 它多次调用 callback_id_from_server_url,比较相等和不相等,并检查长度和字符集 → 出来是断言通过或失败。

调用关系:它保护 callback_id_from_server_url 的安全边界,避免不同 MCP 服务器共用同一个回调标识。

调用图:调用 1 个内部函数(callback_id_from_server_url);外部调用 3 个(assert!, assert_eq!, assert_ne!)。

tests::callback_id_is_appended_to_redirect_uri_path832–838 ↗
fn callback_id_is_appended_to_redirect_uri_path()

作用:验证 callback_id 会追加到普通回调路径后面。

数据流:进去是 http://127.0.0.1:1234/callback 和 abc123 → 它调用 append_callback_id_to_redirect_uri → 出来断言结果是 /callback/abc123。

调用关系:它覆盖 append_callback_id_to_redirect_uri 的基本追加行为,对应真实本机回调地址。

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

tests::callback_id_is_appended_before_redirect_uri_query841–852 ↗
fn callback_id_is_appended_before_redirect_uri_query()

作用:验证回调 URL 带查询参数时,callback_id 会加在路径里,而不是错误地拼到查询参数后面。

数据流:进去是带 ?provider=github 的回调 URL 和 abc123 → 它调用 append_callback_id_to_redirect_uri → 出来断言路径变长且查询参数保留。

调用关系:它保护 append_callback_id_to_redirect_uri 处理复杂 URL 的行为,避免 OAuth 服务商收到错误回调地址。

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

tests::append_query_param_adds_resource_to_absolute_url855–866 ↗
fn append_query_param_adds_resource_to_absolute_url()

作用:验证 append_query_param 能给正常的绝对 URL 追加 resource 参数,并正确编码特殊字符。

数据流:进去是已有 scope 参数的授权 URL 和 resource 值 → 它调用 append_query_param → 出来断言追加后的 URL 符合预期。

调用关系:它覆盖 OauthLoginFlow::new 给授权地址追加 oauth_resource 的常见场景。

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

tests::append_query_param_ignores_empty_values869–877 ↗
fn append_query_param_ignores_empty_values()

作用:验证参数值为空白时不会改动 URL。这样不会把 resource= 这种无用参数塞进授权地址。

数据流:进去是授权 URL 和只包含空格的参数值 → 它调用 append_query_param → 出来断言 URL 原样返回。

调用关系:它保护 append_query_param 的空值处理,避免配置为空时影响 OAuth 服务端判断。

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

tests::append_query_param_handles_unparseable_url880–884 ↗
fn append_query_param_handles_unparseable_url()

作用:验证即使原 URL 不能被标准 URL 解析,也会用简单安全的方式把参数拼上。

数据流:进去是 not a url、参数名 resource 和值 api/resource → 它调用 append_query_param → 出来断言使用 ?resource= 并对斜杠做了编码。

调用关系:它覆盖 append_query_param 的兜底分支,保证异常格式下也有可预测结果。

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

rmcp-client/src/oauth.rs源码 ↗
domain_logicauth startup, request handling, token refresh, logout/cleanup, cross-cutting

MCP 服务器需要 OAuth 凭证才能访问。OAuth 可以简单理解为“授权登录票据”,里面有访问令牌、刷新令牌、过期时间等信息。这个文件解决的问题是:程序下次启动时还能找到这些票据,票据快过期时能刷新,用户退出授权时能清掉。它优先使用操作系统的钥匙串,也就是系统自带的安全保险柜;如果保险柜不可用,就写到 CODEX_HOME 下的 .credentials.json 作为兜底。文件里还会给每个服务器和地址算一个稳定的存储名字,避免不同服务器的令牌串在一起。OAuthPersistor 则像后台巡检员,会检查授权管理器里的最新凭证,发现变化就保存,发现快过期就刷新,发现凭证没了就删除。

函数细节63
WrappedOAuthTokenResponse::eq78–83 ↗
fn eq(&self, other: &Self) -> bool

作用:比较两个 OAuth 令牌响应是不是一样。因为原始类型不好直接比较,它先把两边转成 JSON 字符串,再看字符串是否相同。

数据流:进去两个 WrappedOAuthTokenResponse → 分别序列化成 JSON 文本 → 如果两段文本一样就返回 true,否则返回 false;如果序列化失败,也当作不相等。

调用关系:它主要被保存逻辑用来判断令牌有没有真的变化,避免每次检查都重复写入钥匙串或文件。

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

load_oauth_tokens93–113 ↗
fn load_oauth_tokens(
    server_name: &str,
    url: &str,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Result<Option<StoredOAuthTokens>>

作用:按配置读取某个服务器的 OAuth 凭证。它会根据用户选择,决定从钥匙串读、从文件读,还是先试钥匙串再退回文件。

数据流:进去服务器名、服务器地址、保存模式和钥匙串后端类型 → 选择合适的读取路线 → 出来是找到的 StoredOAuthTokens,或者 None 表示没有凭证。

调用关系:oauth_token_status 会先调用它拿凭证;它再把具体工作交给文件读取或钥匙串读取函数。

调用图:调用 3 个内部函数(load_oauth_tokens_from_file, load_oauth_tokens_from_keyring, load_oauth_tokens_from_keyring_with_fallback_to_file);被 1 处调用(oauth_token_status)。

oauth_token_status115–128 ↗
fn oauth_token_status(
    server_name: &str,
    url: &str,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Result<StoredOAuthTokenStatus>

作用:判断某个服务器现在的 OAuth 状态:没有凭证、凭证可用,还是需要重新授权。

数据流:进去服务器名、地址和存储配置 → 调用 load_oauth_tokens 读取凭证 → 再用 oauth_tokens_are_usable 检查是否还能用 → 返回状态枚举。

调用关系:上层的 determine_streamable_http_auth_status 会用它决定连接 MCP HTTP 服务前是否需要走登录授权流程。

调用图:调用 2 个内部函数(load_oauth_tokens, oauth_tokens_are_usable);被 1 处调用(determine_streamable_http_auth_status)。

oauth_tokens_are_usable130–143 ↗
fn oauth_tokens_are_usable(tokens: &StoredOAuthTokens) -> bool

作用:检查已保存的 OAuth 凭证是不是足够可靠,可以直接拿来用。它会看客户端 ID、访问令牌、过期时间和刷新令牌。

数据流:进去一份 StoredOAuthTokens → 先排除空的 client_id → 如果快过期或已过期,就要求有非空刷新令牌;否则要求访问令牌非空 → 返回可用或不可用。

调用关系:oauth_token_status 调用它做最后判断;它依赖 token_needs_refresh 判断令牌是否已经接近过期。

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

refresh_expires_in_from_timestamp145–165 ↗
fn refresh_expires_in_from_timestamp(tokens: &mut StoredOAuthTokens)

作用:把保存下来的绝对过期时间,换算回“还剩多少秒过期”。这是为了让 OAuth 库启动后能正确理解令牌寿命。

数据流:进去可修改的 StoredOAuthTokens → 如果有 expires_at,就用当前时间算剩余秒数 → 写回 token_response 的 expires_in;如果已经过期,就写成 0 秒。

调用关系:从钥匙串或文件读出凭证后都会调用它,保证内存里的令牌对象带着正确的剩余时间。

调用图:调用 1 个内部函数(expires_in_from_timestamp);被 3 处调用(load_oauth_tokens_from_direct_keyring, load_oauth_tokens_from_file, load_oauth_tokens_from_secrets_keyring);外部调用 1 个(from_secs)。

load_oauth_tokens_from_keyring_with_fallback_to_file167–182 ↗
fn load_oauth_tokens_from_keyring_with_fallback_to_file(
    keyring_store: &K,
    keyring_backend_kind: AuthKeyringBackendKind,
    server_name: &str,
    url: &str,
) -> Result<Option<StoredOAuthTo

作用:先从系统钥匙串读凭证;如果没有或钥匙串出错,就尝试从备用文件读。这样系统安全存储坏了时,用户不一定立刻掉线。

数据流:进去钥匙串对象、后端类型、服务器名和地址 → 先调用钥匙串读取 → 找到就返回;找不到或失败就读 .credentials.json → 返回凭证或 None。

调用关系:load_oauth_tokens 在 Auto 模式下会调用它;它把工作分给 load_oauth_tokens_from_keyring 和 load_oauth_tokens_from_file。

调用图:调用 2 个内部函数(load_oauth_tokens_from_file, load_oauth_tokens_from_keyring);被 1 处调用(load_oauth_tokens);外部调用 1 个(warn!)。

load_oauth_tokens_from_keyring184–198 ↗
fn load_oauth_tokens_from_keyring(
    keyring_store: &K,
    keyring_backend_kind: AuthKeyringBackendKind,
    server_name: &str,
    url: &str,
) -> Result<Option<StoredOAuthTokens>>

作用:按钥匙串后端类型读取 OAuth 凭证。这里的后端类型决定是直接存钥匙串,还是通过加密 secrets 存储。

数据流:进去钥匙串对象、后端类型、服务器名和地址 → 根据 Direct 或 Secrets 分支调用不同读取函数 → 返回找到的凭证或 None。

调用关系:它是所有钥匙串读取的分流口,被 load_oauth_tokens 和自动兜底读取函数调用。

调用图:调用 2 个内部函数(load_oauth_tokens_from_direct_keyring, load_oauth_tokens_from_secrets_keyring);被 2 处调用(load_oauth_tokens, load_oauth_tokens_from_keyring_with_fallback_to_file)。

load_oauth_tokens_from_direct_keyring200–216 ↗
fn load_oauth_tokens_from_direct_keyring(
    keyring_store: &K,
    server_name: &str,
    url: &str,
) -> Result<Option<StoredOAuthTokens>>

作用:从系统钥匙串里的直接条目读取 OAuth 凭证。直接条目可以理解为“按一个账号名直接存进去的一段 JSON”。

数据流:进去钥匙串对象、服务器名和地址 → 计算存储键 → 从钥匙串取 JSON → 反序列化成 StoredOAuthTokens → 修正剩余过期时间 → 返回结果。

调用关系:load_oauth_tokens_from_keyring 在 Direct 模式下调用它。

调用图:调用 2 个内部函数(compute_store_key, refresh_expires_in_from_timestamp);被 1 处调用(load_oauth_tokens_from_keyring);外部调用 3 个(load, new, from_str)。

load_oauth_tokens_from_secrets_keyring218–243 ↗
fn load_oauth_tokens_from_secrets_keyring(
    keyring_store: &K,
    server_name: &str,
    url: &str,
) -> Result<Option<StoredOAuthTokens>>

作用:从 Codex 的加密 secrets 存储里读取 MCP OAuth 凭证。它比直接钥匙串多一层统一的加密秘密管理。

数据流:进去钥匙串对象、服务器名和地址 → 找到 CODEX_HOME → 创建 SecretsManager → 算出合法的 secret 名 → 读取并解析 JSON → 修正过期时间 → 返回凭证或 None。

调用关系:load_oauth_tokens_from_keyring 在 Secrets 模式下调用它,用于新的加密存储路线。

调用图:调用 3 个内部函数(compute_secret_name, refresh_expires_in_from_timestamp, new_with_keyring_store_and_namespace);被 1 处调用(load_oauth_tokens_from_keyring);外部调用 4 个(new, clone, find_codex_home, from_str)。

save_oauth_tokens245–267 ↗
fn save_oauth_tokens(
    server_name: &str,
    tokens: &StoredOAuthTokens,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Result<()>

作用:保存某个服务器的 OAuth 凭证。它根据配置选择写钥匙串、写文件,或者优先钥匙串失败后写文件。

数据流:进去服务器名、凭证、保存模式和钥匙串后端类型 → 选择保存路线 → 成功时没有返回数据,只保证凭证已经落到对应存储里。

调用关系:OAuthPersistor::persist_if_needed 检测到凭证变化后会调用它;它再交给具体的钥匙串或文件保存函数。

调用图:调用 3 个内部函数(save_oauth_tokens_to_file, save_oauth_tokens_with_keyring, save_oauth_tokens_with_keyring_with_fallback_to_file);被 1 处调用(persist_if_needed)。

save_oauth_tokens_with_keyring269–283 ↗
fn save_oauth_tokens_with_keyring(
    keyring_store: &K,
    keyring_backend_kind: AuthKeyringBackendKind,
    server_name: &str,
    tokens: &StoredOAuthTokens,
) -> Result<()>

作用:把凭证保存到钥匙串相关后端。它只负责按后端类型分流,不做文件兜底。

数据流:进去钥匙串对象、后端类型、服务器名和凭证 → Direct 就写直接钥匙串,Secrets 就写加密 secrets → 返回成功或错误。

调用关系:save_oauth_tokens 和自动兜底保存函数都会调用它。

调用图:调用 2 个内部函数(save_oauth_tokens_to_direct_keyring, save_oauth_tokens_to_secrets_keyring);被 2 处调用(save_oauth_tokens, save_oauth_tokens_with_keyring_with_fallback_to_file)。

save_oauth_tokens_to_direct_keyring285–309 ↗
fn save_oauth_tokens_to_direct_keyring(
    keyring_store: &K,
    server_name: &str,
    tokens: &StoredOAuthTokens,
) -> Result<()>

作用:把 OAuth 凭证直接写进系统钥匙串。写成功后,它会清理旧的备用文件,避免同一份秘密散落两处。

数据流:进去钥匙串对象、服务器名和凭证 → 把凭证转成 JSON → 计算存储键 → 写入钥匙串 → 成功后删除备用文件里的同键记录 → 返回结果。

调用关系:save_oauth_tokens_with_keyring 在 Direct 模式下调用它;它会调用 delete_oauth_tokens_from_file 做迁移清理。

调用图:调用 2 个内部函数(compute_store_key, delete_oauth_tokens_from_file);被 1 处调用(save_oauth_tokens_with_keyring);外部调用 5 个(save, new, format!, to_string, warn!)。

save_oauth_tokens_to_secrets_keyring311–334 ↗
fn save_oauth_tokens_to_secrets_keyring(
    keyring_store: &K,
    server_name: &str,
    tokens: &StoredOAuthTokens,
) -> Result<()>

作用:把 OAuth 凭证写进 Codex 的加密 secrets 存储。写完也会清掉备用文件,减少明文文件残留。

数据流:进去钥匙串对象、服务器名和凭证 → 序列化成 JSON → 找 CODEX_HOME 并创建 SecretsManager → 计算 secret 名 → 写入加密存储 → 删除备用文件记录。

调用关系:save_oauth_tokens_with_keyring 在 Secrets 模式下调用它;它同时用 compute_secret_name 和 compute_store_key 生成不同存储系统需要的名字。

调用图:调用 4 个内部函数(compute_secret_name, compute_store_key, delete_oauth_tokens_from_file, new_with_keyring_store_and_namespace);被 1 处调用(save_oauth_tokens_with_keyring);外部调用 5 个(new, clone, find_codex_home, to_string, warn!)。

save_oauth_tokens_with_keyring_with_fallback_to_file336–351 ↗
fn save_oauth_tokens_with_keyring_with_fallback_to_file(
    keyring_store: &K,
    keyring_backend_kind: AuthKeyringBackendKind,
    server_name: &str,
    tokens: &StoredOAuthTokens,
) -> Result<()>

作用:保存时优先用钥匙串;钥匙串失败就退回写文件。它保证在安全存储不可用时,程序仍能继续保存登录状态。

数据流:进去钥匙串对象、后端类型、服务器名和凭证 → 先尝试 save_oauth_tokens_with_keyring → 成功就结束;失败就记录警告并写入备用文件。

调用关系:save_oauth_tokens 在 Auto 模式下调用它,是“安全优先、可用性兜底”的保存入口。

调用图:调用 2 个内部函数(save_oauth_tokens_to_file, save_oauth_tokens_with_keyring);被 1 处调用(save_oauth_tokens);外部调用 1 个(warn!)。

delete_oauth_tokens353–367 ↗
fn delete_oauth_tokens(
    server_name: &str,
    url: &str,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
) -> Result<bool>

作用:删除某个服务器的 OAuth 凭证。通常用于用户取消授权、令牌失效,或授权管理器里已经没有凭证时。

数据流:进去服务器名、地址、存储模式和后端类型 → 创建默认钥匙串存储 → 调用统一删除函数 → 返回是否真的删掉了东西。

调用关系:OAuthPersistor::persist_if_needed 发现凭证消失时会调用它。

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

delete_oauth_tokens_from_keyring_and_file369–395 ↗
fn delete_oauth_tokens_from_keyring_and_file(
    keyring_store: &K,
    store_mode: OAuthCredentialsStoreMode,
    keyring_backend_kind: AuthKeyringBackendKind,
    server_name: &str,
    url: &str,

作用:同时清理钥匙串和备用文件里的凭证。这样不会出现“钥匙串删了,但文件里还留着旧票据”的情况。

数据流:进去钥匙串对象、存储模式、后端类型、服务器名和地址 → 算存储键 → 先删钥匙串相关位置 → 再删文件记录 → 返回任一位置是否删除成功。

调用关系:delete_oauth_tokens 把实际删除交给它;它再调用 delete_oauth_tokens_from_keyring 和 delete_oauth_tokens_from_file。

调用图:调用 3 个内部函数(compute_store_key, delete_oauth_tokens_from_file, delete_oauth_tokens_from_keyring);被 1 处调用(delete_oauth_tokens);外部调用 1 个(warn!)。

delete_oauth_tokens_from_keyring397–415 ↗
fn delete_oauth_tokens_from_keyring(
    keyring_store: &K,
    keyring_backend_kind: AuthKeyringBackendKind,
    server_name: &str,
    url: &str,
) -> Result<bool>

作用:按钥匙串后端类型删除凭证。Secrets 模式下还会顺手删除旧的直接钥匙串条目,兼容迁移前的数据。

数据流:进去钥匙串对象、后端类型、服务器名和地址 → Direct 只删直接钥匙串;Secrets 同时删直接钥匙串和加密 secrets → 返回是否删到内容。

调用关系:delete_oauth_tokens_from_keyring_and_file 调用它清理安全存储部分。

调用图:调用 2 个内部函数(delete_oauth_tokens_from_direct_keyring, delete_oauth_tokens_from_secrets_keyring);被 1 处调用(delete_oauth_tokens_from_keyring_and_file)。

delete_oauth_tokens_from_direct_keyring417–426 ↗
fn delete_oauth_tokens_from_direct_keyring(
    keyring_store: &K,
    server_name: &str,
    url: &str,
) -> Result<bool>

作用:从系统钥匙串的直接条目里删除 OAuth 凭证。

数据流:进去钥匙串对象、服务器名和地址 → 计算存储键 → 调用钥匙串删除 → 返回是否删除了条目。

调用关系:delete_oauth_tokens_from_keyring 在 Direct 模式下调用它;Secrets 模式也会调用它来清掉旧位置。

调用图:调用 1 个内部函数(compute_store_key);被 1 处调用(delete_oauth_tokens_from_keyring);外部调用 1 个(delete)。

delete_oauth_tokens_from_secrets_keyring428–445 ↗
fn delete_oauth_tokens_from_secrets_keyring(
    keyring_store: &K,
    server_name: &str,
    url: &str,
) -> Result<bool>

作用:从 Codex 的加密 secrets 存储里删除 OAuth 凭证。

数据流:进去钥匙串对象、服务器名和地址 → 找 CODEX_HOME → 创建 SecretsManager → 计算 secret 名 → 删除对应秘密 → 返回是否删除成功。

调用关系:delete_oauth_tokens_from_keyring 在 Secrets 模式下调用它。

调用图:调用 2 个内部函数(compute_secret_name, new_with_keyring_store_and_namespace);被 1 处调用(delete_oauth_tokens_from_keyring);外部调用 3 个(new, clone, find_codex_home)。

OAuthPersistor::new462–480 ↗
fn new(
        server_name: String,
        url: String,
        authorization_manager: Arc<Mutex<AuthorizationManager>>,
        store_mode: OAuthCredentialsStoreMode,
        keyring_backend_kind:

作用:创建一个 OAuthPersistor,也就是负责跟踪并持久化 OAuth 凭证的小对象。

数据流:进去服务器名、地址、授权管理器、存储配置和初始凭证 → 包装进共享对象 Arc 和互斥锁 Mutex → 返回可复制使用的 OAuthPersistor。

调用关系:create_oauth_transport_and_runtime 创建 OAuth 传输运行环境时会调用它,让后续刷新和保存有地方执行。

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

OAuthPersistor::persist_if_needed488–544 ↗
async fn persist_if_needed(&self) -> Result<()>

作用:检查授权管理器里的最新凭证,只有变化时才保存;如果凭证没了,就删除已保存内容。它避免无意义地频繁写安全存储。

数据流:进去 self → 从 AuthorizationManager 读取 client_id 和当前凭证 → 有凭证就计算过期时间并和上次保存内容比较,变了就 save_oauth_tokens;没凭证就 delete_oauth_tokens → 更新内存里的 last_credentials。

调用关系:它是 OAuthPersistor 的核心保存动作;refresh_if_needed 刷新完令牌后会调用它把新令牌落盘。

调用图:调用 3 个内部函数(compute_expires_at_millis, delete_oauth_tokens, save_oauth_tokens);被 1 处调用(refresh_if_needed);外部调用 1 个(warn!)。

OAuthPersistor::refresh_if_needed550–572 ↗
async fn refresh_if_needed(&self) -> Result<()>

作用:如果令牌快过期,就让授权管理器刷新令牌,然后保存刷新后的结果。

数据流:进去 self → 从 last_credentials 读取 expires_at → 如果还不需要刷新就直接结束;如果需要,就调用 AuthorizationManager 刷新令牌 → 再调用 persist_if_needed 保存新凭证。

调用关系:它通常在请求前或运行过程中被调用,确保真正访问服务器前令牌还有效。

调用图:调用 2 个内部函数(persist_if_needed, token_needs_refresh)。

load_oauth_tokens_from_file594–635 ↗
fn load_oauth_tokens_from_file(server_name: &str, url: &str) -> Result<Option<StoredOAuthTokens>>

作用:从备用文件 .credentials.json 读取某个服务器的 OAuth 凭证。

数据流:进去服务器名和地址 → 读取备用文件 → 算目标存储键 → 遍历文件里的条目找匹配项 → 拼回 OAuthTokenResponse 和 StoredOAuthTokens → 修正过期剩余时间 → 返回结果。

调用关系:load_oauth_tokens 在 File 模式下直接调用它;钥匙串自动兜底失败或缺失时也会调用它。

调用图:调用 3 个内部函数(compute_store_key, read_fallback_file, refresh_expires_in_from_timestamp);被 2 处调用(load_oauth_tokens, load_oauth_tokens_from_keyring_with_fallback_to_file);外部调用 4 个(new, new, new, default)。

save_oauth_tokens_to_file637–664 ↗
fn save_oauth_tokens_to_file(tokens: &StoredOAuthTokens) -> Result<()>

作用:把 OAuth 凭证写入备用文件 .credentials.json。这个文件是钥匙串不可用时的兜底存储。

数据流:进去一份 StoredOAuthTokens → 读取现有文件内容或创建空表 → 提取访问令牌、刷新令牌、范围和过期时间 → 按存储键写入表 → 调用 write_fallback_file 落盘。

调用关系:save_oauth_tokens 在 File 模式下调用它;钥匙串保存失败的自动兜底也会调用它。

调用图:调用 3 个内部函数(compute_store_key, read_fallback_file, write_fallback_file);被 2 处调用(save_oauth_tokens, save_oauth_tokens_with_keyring_with_fallback_to_file)。

delete_oauth_tokens_from_file666–679 ↗
fn delete_oauth_tokens_from_file(key: &str) -> Result<bool>

作用:从备用文件里删除某个存储键对应的凭证。

数据流:进去存储键 → 读取备用文件 → 没文件就返回 false → 有文件就删除对应条目 → 如果真的删了就重写文件 → 返回是否删除到内容。

调用关系:统一删除流程会调用它;钥匙串保存成功后也会调用它清理旧的文件兜底记录。

调用图:调用 2 个内部函数(read_fallback_file, write_fallback_file);被 3 处调用(delete_oauth_tokens_from_keyring_and_file, save_oauth_tokens_to_direct_keyring, save_oauth_tokens_to_secrets_keyring)。

compute_expires_at_millis681–693 ↗
fn compute_expires_at_millis(response: &OAuthTokenResponse) -> Option<u64>

作用:把 OAuth 响应里的“多少秒后过期”换算成一个绝对时间戳,单位是毫秒。

数据流:进去 OAuthTokenResponse → 读取 expires_in → 加上当前时间 → 得到 expires_at 毫秒值;如果太大就用 u64 最大值;没有 expires_in 就返回 None。

调用关系:persist_if_needed 保存新令牌时会用它;OAuth 授权完成流程 finish 也会用它记录过期时间。

调用图:被 2 处调用(persist_if_needed, finish);外部调用 3 个(expires_in, now, from)。

expires_in_from_timestamp695–706 ↗
fn expires_in_from_timestamp(expires_at: u64) -> Option<u64>

作用:把保存的绝对过期时间换算成“还剩多少秒”。

数据流:进去 expires_at 毫秒时间戳 → 和当前时间比较 → 如果已经过期返回 None;否则返回剩余秒数。

调用关系:refresh_expires_in_from_timestamp 调用它,把文件或钥匙串里的时间戳恢复成 OAuth 库需要的格式。

调用图:被 1 处调用(refresh_expires_in_from_timestamp);外部调用 1 个(now)。

token_needs_refresh708–719 ↗
fn token_needs_refresh(expires_at: Option<u64>) -> bool

作用:判断令牌是不是已经过期,或者离过期太近。这里会提前 30 秒刷新,避免请求刚发出去令牌就失效。

数据流:进去可选的 expires_at → 没有过期时间就认为不需要刷新 → 有时间就用当前时间加 30 秒缓冲比较 → 返回是否需要刷新。

调用关系:oauth_tokens_are_usable 用它判断是否必须有刷新令牌;OAuthPersistor::refresh_if_needed 用它决定是否真的刷新。

调用图:被 2 处调用(refresh_if_needed, oauth_tokens_are_usable);外部调用 1 个(now)。

compute_store_key721–732 ↗
fn compute_store_key(server_name: &str, server_url: &str) -> Result<String>

作用:为某个服务器和 URL 生成稳定的存储键。这个键像标签,用来在钥匙串或文件里找到对应凭证。

数据流:进去服务器名和服务器 URL → 组装包含类型、URL、空 headers 的 JSON → 算 SHA-256 哈希前缀 → 拼成 server_name|hash → 返回字符串键。

调用关系:几乎所有读、写、删函数都会调用它,保证同一服务器总是使用同一个存储位置。

调用图:调用 1 个内部函数(sha_256_prefix);被 8 处调用(compute_secret_name, delete_oauth_tokens_from_direct_keyring, delete_oauth_tokens_from_keyring_and_file, load_oauth_tokens_from_direct_keyring, load_oauth_tokens_from_file, save_oauth_tokens_to_direct_keyring, save_oauth_tokens_to_file, save_oauth_tokens_to_secrets_keyring);外部调用 4 个(new, Object, String, format!)。

compute_secret_name740–747 ↗
fn compute_secret_name(server_name: &str, server_url: &str) -> Result<SecretName>

作用:把普通存储键转换成 secrets 系统允许的名字。因为 SecretName 只允许大写字母、数字和下划线,所以要重新哈希。

数据流:进去服务器名和 URL → 先算 compute_store_key → 对这个键做 SHA-256 → 取前 32 位大写十六进制 → 加上 MCP_OAUTH_ 前缀 → 返回 SecretName。

调用关系:Secrets 后端的读、写、删都会调用它,确保加密秘密存储的名字合法且稳定。

调用图:调用 2 个内部函数(compute_store_key, new);被 3 处调用(delete_oauth_tokens_from_secrets_keyring, load_oauth_tokens_from_secrets_keyring, save_oauth_tokens_to_secrets_keyring);外部调用 2 个(new, format!)。

fallback_file_path749–751 ↗
fn fallback_file_path() -> Result<PathBuf>

作用:找到备用凭证文件 .credentials.json 的完整路径。

数据流:进去无参数 → 调用 find_codex_home 找到 CODEX_HOME → 拼上 .credentials.json → 返回路径。

调用关系:read_fallback_file 和 write_fallback_file 都靠它确定应该读写哪个文件。

调用图:被 2 处调用(read_fallback_file, write_fallback_file);外部调用 1 个(find_codex_home)。

read_fallback_file753–773 ↗
fn read_fallback_file() -> Result<Option<FallbackFile>>

作用:读取备用凭证文件,并把 JSON 内容解析成内存里的表。

数据流:进去无参数 → 找到文件路径 → 文件不存在就返回 None → 文件存在就读文本并解析 JSON → 返回凭证表;读写或解析失败就带路径报错。

调用关系:文件读取、文件保存和文件删除都会先调用它拿到当前内容。

调用图:调用 1 个内部函数(fallback_file_path);被 3 处调用(delete_oauth_tokens_from_file, load_oauth_tokens_from_file, save_oauth_tokens_to_file);外部调用 2 个(format!, read_to_string)。

write_fallback_file775–800 ↗
fn write_fallback_file(store: &FallbackFile) -> Result<()>

作用:把备用凭证表写回 .credentials.json。表为空时直接删除文件,避免留下空壳。

数据流:进去凭证表 → 找到文件路径 → 如果表为空且文件存在就删除 → 否则创建目录、序列化 JSON、写文件;在 Unix 上设置权限为 600,只允许当前用户读写。

调用关系:save_oauth_tokens_to_file 和 delete_oauth_tokens_from_file 调用它完成真正落盘。

调用图:调用 1 个内部函数(fallback_file_path);被 2 处调用(delete_oauth_tokens_from_file, save_oauth_tokens_to_file);外部调用 7 个(is_empty, from_mode, create_dir_all, remove_file, set_permissions, write, to_string)。

sha_256_prefix802–811 ↗
fn sha_256_prefix(value: &Value) -> Result<String>

作用:给一段 JSON 算 SHA-256 哈希,并取前 16 个十六进制字符作为短标识。

数据流:进去 JSON 值 → 序列化成字符串 → 用 SHA-256 算摘要 → 转成小写十六进制 → 截取前 16 位 → 返回短字符串。

调用关系:compute_store_key 用它把服务器 URL 等信息压缩成稳定且较短的键片段。

调用图:被 1 处调用(compute_store_key);外部调用 3 个(new, format!, to_string)。

tests::TempCodexHome::new835–849 ↗
fn new() -> Self

作用:测试用:创建一个临时 CODEX_HOME,避免测试读写真实用户目录。

数据流:进去无参数 → 加锁防止多个测试同时改环境变量 → 创建临时目录 → 设置 CODEX_HOME 指向它 → 返回 TempCodexHome。

调用关系:很多测试开头都会创建它,给钥匙串兜底文件和 secrets 文件一个隔离空间。

调用图:外部调用 3 个(new, set_var, tempdir)。

tests::TempCodexHome::path851–853 ↗
fn path(&self) -> &std::path::Path

作用:测试用:拿到临时 CODEX_HOME 的路径。

数据流:进去 TempCodexHome → 读取内部临时目录路径 → 返回路径引用。

调用关系:需要检查 secrets 文件或创建 SecretsManager 的测试会用它。

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

tests::TempCodexHome::drop857–861 ↗
fn drop(&mut self)

作用:测试用:临时环境结束时清理 CODEX_HOME 环境变量。

数据流:进去即将销毁的 TempCodexHome → 删除 CODEX_HOME 环境变量 → 临时目录随对象释放而清掉。

调用关系:测试结束时自动运行,防止一个测试污染另一个测试。

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

tests::load_oauth_tokens_reads_from_keyring_when_available865–883 ↗
fn load_oauth_tokens_reads_from_keyring_when_available() -> Result<()>

作用:测试钥匙串里有凭证时,读取函数确实优先读钥匙串。

数据流:准备临时目录、模拟钥匙串和样例凭证 → 把凭证写入模拟钥匙串 → 调用钥匙串读取 → 断言读出的内容和样例一致。

调用关系:由 Rust 测试框架运行,覆盖 load_oauth_tokens_from_keyring 的 Direct 读取路径。

调用图:外部调用 7 个(default, new, assert_tokens_match_without_expiry, sample_tokens, to_string, compute_store_key, load_oauth_tokens_from_keyring)。

tests::load_oauth_tokens_falls_back_when_missing_in_keyring886–903 ↗
fn load_oauth_tokens_falls_back_when_missing_in_keyring() -> Result<()>

作用:测试钥匙串没有凭证时,自动模式会去读备用文件。

数据流:准备模拟钥匙串和样例凭证 → 只把凭证写入文件 → 调用带兜底的读取函数 → 断言能从文件读到。

调用关系:验证 load_oauth_tokens_from_keyring_with_fallback_to_file 在钥匙串缺失时的行为。

调用图:外部调用 6 个(default, new, assert_tokens_match_without_expiry, sample_tokens, load_oauth_tokens_from_keyring_with_fallback_to_file, save_oauth_tokens_to_file)。

tests::load_oauth_tokens_falls_back_when_keyring_errors906–925 ↗
fn load_oauth_tokens_falls_back_when_keyring_errors() -> Result<()>

作用:测试钥匙串报错时,自动模式仍会尝试读取备用文件。

数据流:准备模拟钥匙串并设置读取错误 → 把凭证写入文件 → 调用带兜底的读取函数 → 断言最终读到文件里的凭证。

调用关系:覆盖钥匙串异常分支,确认错误不会直接让自动模式失败。

调用图:外部调用 8 个(Invalid, default, new, assert_tokens_match_without_expiry, sample_tokens, compute_store_key, load_oauth_tokens_from_keyring_with_fallback_to_file, save_oauth_tokens_to_file)。

tests::save_oauth_tokens_prefers_keyring_when_available928–948 ↗
fn save_oauth_tokens_prefers_keyring_when_available() -> Result<()>

作用:测试保存时如果钥匙串可用,会写钥匙串并删除旧的备用文件。

数据流:先把样例凭证写进文件 → 调用自动兜底保存 → 检查备用文件消失、模拟钥匙串里有正确 JSON。

调用关系:验证 save_oauth_tokens_with_keyring_with_fallback_to_file 的优先钥匙串策略。

调用图:外部调用 9 个(assert!, assert_eq!, default, new, sample_tokens, compute_store_key, fallback_file_path, save_oauth_tokens_to_file, save_oauth_tokens_with_keyring_with_fallback_to_file)。

tests::save_oauth_tokens_writes_fallback_when_keyring_fails951–979 ↗
fn save_oauth_tokens_writes_fallback_when_keyring_fails() -> Result<()>

作用:测试钥匙串保存失败时,凭证会写入备用文件。

数据流:设置模拟钥匙串在指定键上保存失败 → 调用自动兜底保存 → 检查 .credentials.json 被创建且内容正确,同时钥匙串没有保存值。

调用关系:覆盖自动保存的失败兜底分支。

调用图:外部调用 10 个(Invalid, assert!, assert_eq!, default, new, sample_tokens, compute_store_key, fallback_file_path, read_fallback_file, save_oauth_tokens_with_keyring_with_fallback_to_file)。

tests::save_oauth_tokens_with_secrets_backend_writes_encrypted_storage982–1014 ↗
fn save_oauth_tokens_with_secrets_backend_writes_encrypted_storage() -> Result<()>

作用:测试 Secrets 后端会把 OAuth 凭证写入专门的加密 secrets 文件,并清理备用文件。

数据流:准备临时目录、模拟钥匙串和样例凭证 → 调用 Secrets 保存 → 用 SecretsManager 读回 → 检查内容正确、mcp_oauth.age 存在、备用文件不存在。

调用关系:验证 save_oauth_tokens_to_secrets_keyring 以及自动兜底包装的正常路径。

调用图:调用 1 个内部函数(new_with_keyring_store_and_namespace);外部调用 11 个(new, assert!, assert_eq!, default, new, sample_tokens, to_string, compute_secret_name, compute_store_key, save_oauth_tokens_to_file (+1 more))。

tests::load_oauth_tokens_with_secrets_backend_reads_encrypted_storage1017–1039 ↗
fn load_oauth_tokens_with_secrets_backend_reads_encrypted_storage() -> Result<()>

作用:测试 Secrets 后端能从加密 secrets 存储读回 OAuth 凭证。

数据流:先用 Secrets 后端保存样例凭证 → 再调用钥匙串读取入口并指定 Secrets 后端 → 断言读出的凭证匹配样例。

调用关系:覆盖 load_oauth_tokens_from_secrets_keyring 的成功读取路径。

调用图:外部调用 6 个(default, new, assert_tokens_match_without_expiry, sample_tokens, load_oauth_tokens_from_keyring, save_oauth_tokens_with_keyring)。

tests::load_oauth_tokens_with_secrets_backend_ignores_direct_entry1042–1059 ↗
fn load_oauth_tokens_with_secrets_backend_ignores_direct_entry() -> Result<()>

作用:测试选择 Secrets 后端时,不会误读直接钥匙串里的旧条目。

数据流:只往直接钥匙串写样例凭证 → 用 Secrets 后端读取 → 断言结果是 None。

调用关系:保证 Direct 和 Secrets 两种存储路线不会混淆读取。

调用图:外部调用 7 个(assert!, default, new, sample_tokens, to_string, compute_store_key, load_oauth_tokens_from_keyring)。

tests::save_oauth_tokens_with_secrets_backend_falls_back_to_file_when_keyring_fails1062–1083 ↗
fn save_oauth_tokens_with_secrets_backend_falls_back_to_file_when_keyring_fails() -> Result<()>

作用:测试 Secrets 后端底层钥匙串失败时,会退回写备用文件。

数据流:设置模拟钥匙串在 secrets 所需账号上报错 → 调用自动兜底保存 → 读取备用文件 → 断言里面有对应凭证键。

调用关系:覆盖 Secrets 保存失败后的文件兜底路径。

调用图:外部调用 9 个(Invalid, assert!, compute_keyring_account, default, new, sample_tokens, compute_store_key, read_fallback_file, save_oauth_tokens_with_keyring_with_fallback_to_file)。

tests::delete_oauth_tokens_with_secrets_backend_removes_secrets_and_file1086–1122 ↗
fn delete_oauth_tokens_with_secrets_backend_removes_secrets_and_file() -> Result<()>

作用:测试 Secrets 后端删除时,会同时清掉加密 secrets、直接钥匙串旧条目和备用文件。

数据流:准备三处都有可能存在的凭证 → 调用统一删除 → 检查返回 true,并确认 secrets、钥匙串和文件都没有残留。

调用关系:验证 delete_oauth_tokens_from_keyring_and_file 在 Secrets 模式下的全面清理能力。

调用图:调用 1 个内部函数(new_with_keyring_store_and_namespace);外部调用 11 个(new, assert!, default, new, sample_tokens, to_string, compute_secret_name, compute_store_key, delete_oauth_tokens_from_keyring_and_file, save_oauth_tokens_to_file (+1 more))。

tests::delete_oauth_tokens_removes_all_storage1125–1145 ↗
fn delete_oauth_tokens_removes_all_storage() -> Result<()>

作用:测试 Direct 模式删除会清掉钥匙串和备用文件。

数据流:把样例凭证写到模拟钥匙串和文件 → 调用统一删除 → 断言删除成功、钥匙串不再包含键、备用文件消失。

调用关系:覆盖 delete_oauth_tokens_from_keyring_and_file 的常规清理路径。

调用图:外部调用 8 个(assert!, default, new, sample_tokens, to_string, compute_store_key, delete_oauth_tokens_from_keyring_and_file, save_oauth_tokens_to_file)。

tests::delete_oauth_tokens_file_mode_removes_keyring_only_entry1148–1168 ↗
fn delete_oauth_tokens_file_mode_removes_keyring_only_entry() -> Result<()>

作用:测试即使只有钥匙串里有条目,统一删除也能把它删掉。

数据流:只往模拟钥匙串写样例凭证 → 调用统一删除 → 断言返回 true,钥匙串条目消失,备用文件不存在。

调用关系:验证删除逻辑不会只盯着文件,也会处理钥匙串残留。

调用图:外部调用 7 个(assert!, default, new, sample_tokens, to_string, compute_store_key, delete_oauth_tokens_from_keyring_and_file)。

tests::delete_oauth_tokens_propagates_keyring_errors1171–1189 ↗
fn delete_oauth_tokens_propagates_keyring_errors() -> Result<()>

作用:测试钥匙串删除失败时,Auto 模式会把错误传出去,而不是偷偷删文件造成状态不一致。

数据流:设置模拟钥匙串删除报错,并写入备用文件 → 调用统一删除 → 断言返回错误,备用文件仍保留。

调用关系:覆盖删除时的错误保护逻辑。

调用图:外部调用 8 个(Invalid, assert!, default, new, sample_tokens, compute_store_key, delete_oauth_tokens_from_keyring_and_file, save_oauth_tokens_to_file)。

tests::refresh_expires_in_from_timestamp_restores_future_durations1192–1209 ↗
fn refresh_expires_in_from_timestamp_restores_future_durations()

作用:测试未来过期时间能被恢复成正确的剩余秒数。

数据流:创建样例凭证并清空 expires_in → 调用 refresh_expires_in_from_timestamp → 比较恢复出的秒数和预期差距很小。

调用关系:验证从存储加载凭证后恢复过期信息的逻辑。

调用图:外部调用 4 个(assert!, sample_tokens, expires_in_from_timestamp, refresh_expires_in_from_timestamp)。

tests::refresh_expires_in_from_timestamp_marks_expired_tokens1212–1226 ↗
fn refresh_expires_in_from_timestamp_marks_expired_tokens()

作用:测试已经过期的时间戳会被标成 0 秒,而不是被当成未知过期时间。

数据流:创建样例凭证 → 把 expires_at 改成过去 → 调用刷新函数 → 断言 expires_in 变成 Duration::ZERO。

调用关系:保证启动时能立刻刷新已过期令牌。

调用图:外部调用 5 个(from_secs, now, assert_eq!, sample_tokens, refresh_expires_in_from_timestamp)。

tests::oauth_tokens_are_usable_when_expiry_is_unknown1229–1235 ↗
fn oauth_tokens_are_usable_when_expiry_is_unknown()

作用:测试没有过期时间时,只要访问令牌有效,就认为凭证可用。

数据流:创建样例凭证 → 清空 expires_at 和刷新令牌 → 调用可用性判断 → 断言为可用。

调用关系:覆盖 oauth_tokens_are_usable 对未知过期时间的宽松处理。

调用图:外部调用 2 个(assert!, sample_tokens)。

tests::oauth_tokens_are_usable_when_unexpired_without_refresh_token1238–1243 ↗
fn oauth_tokens_are_usable_when_unexpired_without_refresh_token()

作用:测试未过期的令牌即使没有刷新令牌,也能被直接使用。

数据流:创建样例凭证 → 移除刷新令牌但保留未来过期时间 → 调用可用性判断 → 断言为可用。

调用关系:验证刷新令牌只在接近过期或已过期时才是必需的。

调用图:外部调用 2 个(assert!, sample_tokens)。

tests::oauth_tokens_are_usable_when_expired_but_refreshable1246–1251 ↗
fn oauth_tokens_are_usable_when_expired_but_refreshable()

作用:测试过期令牌如果有刷新令牌,仍然算可用,因为可以先刷新再用。

数据流:创建样例凭证 → 把 expires_at 设成 0 → 保留刷新令牌 → 调用可用性判断 → 断言为可用。

调用关系:覆盖 oauth_tokens_are_usable 的“可刷新即还能救”分支。

调用图:外部调用 2 个(assert!, sample_tokens)。

tests::oauth_tokens_are_not_usable_when_expired_and_unrefreshable1254–1260 ↗
fn oauth_tokens_are_not_usable_when_expired_and_unrefreshable()

作用:测试过期且没有刷新令牌的凭证不可用。

数据流:创建样例凭证 → 设置已过期并移除刷新令牌 → 调用可用性判断 → 断言为不可用。

调用关系:验证需要重新授权的典型情况。

调用图:外部调用 2 个(assert!, sample_tokens)。

tests::oauth_tokens_are_not_usable_when_near_expiry_and_unrefreshable1263–1273 ↗
fn oauth_tokens_are_not_usable_when_near_expiry_and_unrefreshable()

作用:测试令牌快过期且无法刷新时,也会被判定不可用。

数据流:创建样例凭证 → 把过期时间设到 30 秒缓冲窗口内 → 移除刷新令牌 → 调用判断 → 断言不可用。

调用关系:验证 token_needs_refresh 的提前刷新缓冲会影响可用性判断。

调用图:外部调用 3 个(now, assert!, sample_tokens)。

tests::oauth_tokens_are_not_usable_when_client_id_is_blank1276–1281 ↗
fn oauth_tokens_are_not_usable_when_client_id_is_blank()

作用:测试 client_id 为空白时,凭证不可用。

数据流:创建样例凭证 → 把 client_id 改成空白字符串 → 调用可用性判断 → 断言不可用。

调用关系:覆盖 oauth_tokens_are_usable 对关键身份字段的检查。

调用图:外部调用 2 个(assert!, sample_tokens)。

tests::oauth_tokens_are_not_usable_when_access_token_is_blank1284–1292 ↗
fn oauth_tokens_are_not_usable_when_access_token_is_blank()

作用:测试访问令牌为空白时,凭证不可用。

数据流:创建样例凭证 → 把 access token 改成空白字符串 → 调用可用性判断 → 断言不可用。

调用关系:覆盖 oauth_tokens_are_usable 对访问票据本身的检查。

调用图:外部调用 3 个(new, assert!, sample_tokens)。

tests::oauth_tokens_are_not_usable_when_required_refresh_token_is_blank1295–1304 ↗
fn oauth_tokens_are_not_usable_when_required_refresh_token_is_blank()

作用:测试需要刷新时,如果刷新令牌只是空白字符串,也不能算可用。

数据流:创建样例凭证 → 设置已过期 → 把 refresh token 改成空白 → 调用判断 → 断言不可用。

调用关系:验证刷新令牌不只是存在,还必须有实际内容。

调用图:外部调用 3 个(new, assert!, sample_tokens)。

tests::assert_tokens_match_without_expiry1306–1318 ↗
fn assert_tokens_match_without_expiry(
        actual: &StoredOAuthTokens,
        expected: &StoredOAuthTokens,
    )

作用:测试辅助函数:比较两份 StoredOAuthTokens 是否匹配,但不过分计较 expires_in 的细微变化。

数据流:进去实际凭证和期望凭证 → 比较服务器名、URL、client_id、expires_at → 再调用令牌响应比较辅助函数 → 不匹配时让测试失败。

调用关系:多个读取测试用它断言读回来的凭证和原始样例一致。

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

tests::assert_token_response_match_without_expiry1320–1345 ↗
fn assert_token_response_match_without_expiry(
        actual: &WrappedOAuthTokenResponse,
        expected: &WrappedOAuthTokenResponse,
    )

作用:测试辅助函数:比较两个 OAuthTokenResponse 的主要内容,比如访问令牌、刷新令牌、范围和额外字段。

数据流:进去实际响应和期望响应 → 逐项比较令牌内容、类型、刷新令牌、scopes、extra fields,以及是否带 expires_in → 不一致就触发断言失败。

调用关系:由 assert_tokens_match_without_expiry 调用,专门处理令牌响应内部字段。

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

tests::sample_tokens1347–1369 ↗
fn sample_tokens() -> StoredOAuthTokens

作用:测试辅助函数:生成一份固定的 OAuth 样例凭证。

数据流:进去无参数 → 创建 access token、refresh token、两个 scope 和一小时过期时间 → 计算 expires_at → 包成 StoredOAuthTokens 返回。

调用关系:几乎所有 OAuth 存储测试都用它准备一致的测试数据。

调用图:外部调用 7 个(new, from_secs, new, new, default, compute_expires_at_millis, vec!)。