cc-switch Codex 密钥迁移机制拆解
【免费下载链接】cc-switchA cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io项目地址: https://gitcode.com/GitHub_Trending/cc/cc-switch
cc-switch v3.20.1 引入一处 Codex 集成上的 Breaking Change:第三方供应商切换从"密钥写入 auth.json(Codex 登录凭证文件)"改为配置即密钥——API 密钥注入config.toml(Codex 主配置文件)的experimental_bearer_token字段,auth.json回归纯官方 ChatGPT 登录文件。本文基于codex_config.rs的写入计划、安全门与预检代码,拆解执行顺序、拒绝逻辑与升级影响。
Codex 0.149 为何让旧版 auth.json 切换失效
旧契约下,cc-switch 把第三方 API 密钥写进auth.json的OPENAI_API_KEY,依赖 Codex 的"环境凭证继承":自定义供应商表没有自带凭证时,自动借用auth.json里的密钥。上游 Codex 0.149 取消了这一继承——自定义供应商不再读取auth.json,于是旧默认模式切换后的请求一律 401。
源码注释把新契约的边界写得很直白:
// Third-party switches are config-only. Since Codex 0.149 // (openai/codex#39214) custom providers no longer inherit ambient auth // from auth.json, so the API key travels as a provider-scoped // `experimental_bearer_token` in config.toml (honored since Codex 0.48). // auth.json is reserved for the official ChatGPT login: kept when the // preservation setting is on, deleted otherwise. It never carries // third-party keys,见 codex_config.rs。密钥落点从"共享登录文件"变为"供应商自己的表内字段"(Codex 0.48 起识别该字段),auth.json由此恢复单一职责:只承载官方 ChatGPT 登录。这一改动同时消除了一个更隐蔽的风险——第三方密钥与官方 OAuth 令牌共处一文件,跨供应商卡片可能互相"借走"不属于对方的凭证。
配置即密钥:第三方切换写入计划的执行顺序
所有 Codex 实时配置写入(供应商切换、代理接管备份重建、备份恢复)都收敛到一个纯函数式入口 plan_codex_live_write:全部变换——迁移、注入、盖章——都在构建"写入计划"(CodexLiveWritePlan)时完成,写层只负责按计划落盘。第三方分支的执行顺序如下:
- 全表语义预检:
preflight_codex_provider_table_conflicts对每个供应商表(含非活动表)检查 0.149 在加载期拒绝的字段组合,无法靠归一化修复的形状直接报错,避免写出"Codex 起不来"的配置(codex_config.rs); - 官方分支提前返回:官方卡片走
write_full_auth路径,密钥永不经过config.toml(codex_config.rs); - 提取携带密钥:
extract_codex_api_key从auth.OPENAI_API_KEY或配置文本已有的experimental_bearer_token取值,两处皆空则卡片视为"无密钥"; - 迁移废弃保留表:把旧版接管投影遗留的
[model_providers.openai]、.ollama、.lmstudio表重命名到 cc-switch 自有的cc-switchid(常量见 codex_config.rs)——必须在安全门之前做,否则"废弃表 +openai_base_url"的混合形状会被误判; - 归一化旧式改址:内置
openai供应商 + 顶层openai_base_url的旧形状改写为cc-switch自定义表,因为旧形状没有表可以承载密钥; - 判定登录文件去留:
remove_auth_file = !preserve_official_login,即"保留官方登录"开关关闭时删除auth.json而非覆写; - 两道安全门:任一命中即以可操作错误拒绝本次切换(下一节展开);
- 注入令牌并盖章:
set_codex_experimental_bearer_token把密钥写入活动供应商表,随后按登录保留状态对齐requires_openai_auth(见 codex_config.rs)。
落盘阶段 write_codex_live_for_provider 先原子写config.toml,配置提交成功后才删除auth.json;删除失败只降级为警告,不会把一次已经生效的切换回滚成"未切换"假象。顺序上"先配置、后清理"保证了崩溃窗口内不会出现"登录没了、密钥也没落位"的最坏组合。
提交前预检与两道安全门的拒绝逻辑
安全门保护同一个不变量:第三方路由解析出的凭证永远不能来自auth.json。两个判定函数分别覆盖"有密钥却没地方放"和"无密钥却会退回官方登录"两种泄漏形状。
门一:携带密钥、但配置没有可承载令牌的供应商表(自定义model_provider缺表,或仅靠顶层openai_base_url改址):
match active_codex_model_provider_id(&doc) { Some(id) if is_custom_codex_model_provider_id(&id) => doc .get("model_providers") .and_then(|item| item.as_table_like()) .and_then(|table| table.get(&id)) .and_then(|item| item.as_table_like()) .is_none(), _ => doc .get("openai_base_url") .and_then(|item| item.as_str()) .map(str::trim) .is_some_and(|url| !url.is_empty()), }见 codex_config.rs。0.149 下顶层令牌字段不被读取,把密钥留在顶层等于把凭证交给一个无人消费的位置——官方 OAuth 登录会替代它发往第三方端点,所以这类形状必须拒绝而不是"尽力而为"。
门二:无密钥、且活动表会以requires_openai_auth = true回退官方登录:
fn codex_provider_table_falls_back_to_official_auth(table: &dyn toml_edit::TableLike) -> bool { table .get("requires_openai_auth") .and_then(|item| item.as_bool()) .unwrap_or(false) && table.get("env_key").is_none() && table.get("experimental_bearer_token").is_none() }见 codex_config.rs。整份配置的判定再区分三种路由(codex_config.rs):自定义表回退、内置openai被顶层openai_base_url改址、以及其余保留内置供应商(ollama、lmstudio等)永不回退 OAuth 登录。易错点在"短路"名单:auth/aws子表被有意排除在自带凭证之外——0.149 校验它们与requires_openai_auth互斥,把它们当作"有凭证"会让死配置蒙混过关(codex_config.rs)。
拒绝必须发生在指针移动之前,这是本次变更的第二个关键设计。preflight_codex_live_write复用与写层完全相同的计划构建,只丢弃结果:
pub fn preflight_codex_live_write( category: Option<&str>, auth: &Value, config_text: Option<&str>, ) -> Result<(), AppError> { plan_codex_live_write( category, auth, config_text, crate::settings::preserve_codex_official_auth_on_switch(), ) .map(|_| ()) }见 codex_config.rs。其文档注释说明动机:若在current指针移动后才被写层拒绝,下一次切换会把旧实时配置回填进被拒卡片的数据库行,污染卡片。切换主流程因此在移动指针前调用 preflight_codex_live_write_for_state,预检与实写共用同一套归一化、安全门与令牌注入逻辑,二者不会漂移。
requires_openai_auth 盖章与登录保留开关的新契约
"保留官方登录"(login preservation)开关在新契约下只剩一个语义:开 = 第三方切换完全不动 ChatGPT 登录;关 = 删除auth.json。为了让 Codex 的登录界面与磁盘真实状态一致,每次直连切换都会对活动供应商表盖章:
if provider_table .get("requires_openai_auth") .and_then(|item| item.as_bool()) == Some(preserve_official_login) { return Ok(config_text.to_string()); } provider_table.insert( "requires_openai_auth", toml_edit::value(preserve_official_login), ); Ok(doc.to_string()) }见 align_codex_requires_openai_auth_with_login_preservation。盖章只触碰自带令牌短路(experimental_bearer_token或env_key)的表——给无短路的表盖上true,请求凭证会被路由到保留的官方 OAuth 登录,恰好是安全门要阻止的泄漏;而保留预设自带的旧值(0.149 之前auth.json承载第三方密钥时代的遗留)不可信,一律以磁盘写后状态为准覆盖。
与之配套的是数据库回填时的"令牌回收":从实时配置读回时,若config.toml携带注入令牌,会把它提回auth.OPENAI_API_KEY并清出配置文本(codex_config.rs),保证存储的供应商卡片不泄露生成物、始终保持规范形状。这也解释了编辑对话框行为的变化:表单不再从共享的auth.json预填密钥,而是重建自卡片自己的表内 bearer,避免同 base URL 的卡片密钥互相收敛(修复 #6534)。
旧配置形态自动修复、用户影响与参数速查
升级后的第一次切换/接管会按固定顺序执行一组无副作用的修复:废弃保留表重命名(幂等)、无name自定义表补名(0.149 对任何无名单元表整份拒绝加载)、旧式openai_base_url改址迁移为cc-switch表(codex_config.rs)、以及加载期字段冲突预检。这些修复全部幂等,已归一化的文本原样通过。
切换涉及的关键字段速查:
| 键名 | 类型 | 默认值/取值 | 说明 |
|---|---|---|---|
experimental_bearer_token | string | 按需注入 | 第三方密钥在config.toml的落点,优先写入活动供应商表;保留 id 路由下落顶层(0.149 不读,且会被安全门拒绝) |
requires_openai_auth | bool | 每次切换盖章 | 与磁盘上的登录保留状态对齐,只驱动 0.149 的登录界面,不改变已短路表的请求凭证 |
model_provider | string | openai(缺省时) | 活动路由选择器;缺失的第三方卡片按内置openai处理,改址走openai_base_url再迁移为自定义表 |
auth.json(OPENAI_API_KEY/登录材料) | 文件 | 仅官方登录 | 第三方切换不再写入;保留开关关闭时整个文件被删除,失败删除只出警告 |
| 保留官方登录开关 | bool(设置) | 关 | 关 = 第三方切换删除auth.json;开 = 登录跨切换保留,且代理注入 OAuth 卡片的requires_openai_auth恒为false |
用户侧影响面(来自 v3.20.1 升级说明):
- Codex 0.48 之前的版本读取不到
experimental_bearer_token,第三方认证直接失效,需升级 Codex; - 保留开关关闭(默认)时,第三方切换改为删除而非覆写
auth.json;要恢复 ChatGPT 登录,切回绑定 Auth Center 账号的官方卡片,或codex login; - 此前"碰巧能用"的两类卡片现在会被切换期拒绝:空配置卡片(无表承载密钥)、无密钥且依赖
requires_openai_auth = true或裸openai_base_url改址借用官方登录的卡片——需补[model_providers.<id>]条目或 API 密钥; - 0.149 兼容修复只在"下一次实时写入"时生效,旧形状不会单独触发重写;
- 修复 #6534 之前已互相污染的密钥不会自动纠正,需在每张卡片上重输一次。
回归测试如何锁住密钥落位契约
测试围绕三个不变量布防:
- 拒绝形状不可写:codex_config.rs 断言携带密钥但无可承载表形状的
preflight_codex_live_write返回错误(第三方与官方上下文均拒),而归一化后的形状通过——锁死"门一先于指针移动生效"; - OAuth 卡片不被门误杀:live.rs 针对 xAI Grok OAuth 这类"设计上无密钥"的代理注入卡片,断言中和
requires_openai_auth快照后预检必须放行,即 v3.20.1 中"切换被安全门误拒"的回归防护; - 保留表迁移无损且大小写精确:codex_config.rs 验证旧接管投影的保留表被重命名、活动路由保持可达、保留登录时凭证不泄漏;codex_config.rs 进一步锁定上游大小写语义——
OpenAI这类变体是合法自定义 id,而oss/ollama-chat在 0.148/0.149 上并非保留 id,迁移器不得误伤。
延伸阅读
- codex_config.rs:写入计划、两道安全门、令牌注入与保留表迁移的完整实现入口;
- live.rs:切换指针移动前的预检调用与代理注入 OAuth 卡片的标志中和;
- mod.rs:供应商切换主流程,预检与
current提交的事务顺序; - codexProviderPresets.ts:预设侧修复源,代理注入 OAuth 预设的
requires_openai_auth = false在此产出; - CHANGELOG.md:v3.20.1 的变更说明、升级注意事项与受拒卡片的处置指引。
【免费下载链接】cc-switchA cross-platform desktop All-in-One assistant for Claude Code, Codex, OpenCode, OpenClaw, Grok Build & Hermes Agent. Only official website: ccswitch.io项目地址: https://gitcode.com/GitHub_Trending/cc/cc-switch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考