Skip to content

feat(MOC-62): MCP 授权可移植保险箱(file 模式 + transfer 镜像/恢复)#307

Merged
Cmochance merged 6 commits into
mainfrom
feat/moc-62-mcp-portable-store
May 29, 2026
Merged

feat(MOC-62): MCP 授权可移植保险箱(file 模式 + transfer 镜像/恢复)#307
Cmochance merged 6 commits into
mainfrom
feat/moc-62-mcp-portable-store

Conversation

@Cmochance
Copy link
Copy Markdown
Owner

@Cmochance Cmochance commented May 29, 2026

Refs MOC-62。Stacked 在 #306 之上(base = fix/moc-54-misc)—— 复用 #306main.rs 加的 auto_apply JoinHandle,启动同步 await 它后再跑,避开与 apply 抢写 config.toml#306 合并后这条 PR retarget 到 main。

背景(先纠正前提)

源码级排查(Codex v0.133)证实:MCP OAuth 凭据按 server_name+url 存 macOS 钥匙串,与 provider / 账号 / auth.json 无关 —— 切 provider、甚至 keychain 模式下切账号都不丢。真正会丢的是 OAuth 过期(快照救不回)或凭据被擦除 / 换机。所以本特性不是"防 provider 切换丢失",而是按你的选择把 transfer 做成 MCP 授权的可移植、可恢复的保险箱

做了什么

设置页新增默认开启的开关 mcpCredentialsPortableStore。开启后:

  1. ~/.codex/config.toml 写根级 mcp_oauth_credentials_store = "file" → Codex 把 MCP OAuth 凭据落 ~/.codex/.credentials.json(0o600 明文,单 JSON blob)。
  2. ~/.codex 之外维护镜像 ~/.codex-app-transfer/mcp-credentials.json;启动(await auto_apply 后)+ provider 切换后做并集合并:实时缺失的从镜像恢复、镜像缺失的从实时捕获,默认取 live(Codex 现状),仅 live 有真实到期且 mirror 严格更新时取 mirror;任一侧损坏整体跳过不覆盖;原子写 0o600。
  3. 关闭 → 删 config key 回退 Codex 默认(.credentials.json 保留,非破坏)。

明确不解决:OAuth 自然过期(过期 token 恢复回去仍过期,需重新授权)。安全代价:token 明文落盘(0o600,Codex 官方支持、注释称"与其它 CLI agent 一致")。
升级注意:默认开 → 首次切到 file 模式后,原本存在钥匙串里的已授权远程 MCP 需重新授权一次(v1 不碰钥匙串以避开跨 app ACL 弹框;钥匙串→文件导入留作可选 Phase 2)。

验证

  • crates/codex_integration 新模块 10 个单测全过(并集 / prefer-fresher / live 无到期不被旧 mirror 覆盖 / 缺失恢复 / 损坏跳过 / 0o600 / ensure key 写删)。
  • cargo check -p codex-app-transfer ✓ / cargo fmt --check ✓。
  • pr-review-toolkit:code-reviewer 对照 Codex v0.133 源码复核:无 BLOCKER;1 个 IMPORTANT(merge tie-break 在 token 无 expires_at 时可能用旧 mirror 覆盖新授权)已修(改 as_u64 + 默认取 live + 加回归测试);NIT 注释措辞已修;另两个 NIT(失败 revert 已 warn / 0o644 短窗与既有 write_auth 一致)按现状保留。
  • 本地 cargo test -p codex-app-transfer 有 1 个 既有 flaky 并发测试 gemini_oauth::...cancel_slot_epoch... 在 macOS 全并行下偶发失败(本分支未碰 gemini_oauth;隔离跑 5/5 过;fix(MOC-54): 杂项修复 — 残留扫描竞态 + 致谢长度门禁 + 活跃度图单点态 #306 同 Linux CI 已验该测试通过)—— 非本 PR 引入,CI(Linux)不受影响。

范围

新 feature,独立于 #306 杂项修复。不自动 merge,等你 review。

🤖 Generated with Claude Code


Open in Devin Review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@Cmochance
Copy link
Copy Markdown
Owner Author

设计修正(review 后,commit 5a6329b):同步不再是逐 key 并集。chatgpt-codex-connector P2 指出原并集会把只在镜像里的 key 当意外丢失写回 live,从而复活用户已 codex mcp logout / 撤销的凭据。已改为「整文件灾难性丢失备份」语义 —— live 整文件不在才从镜像恢复;live 存在则它是权威,镜像精确跟随(捕获新授权 + 传播删除),绝不把 live 没有的 key 写回 live。详见已 resolve 的 review thread。PR 描述里早先的『union-merge / prefer fresher by expires_at』已被取代。

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Cmochance added 6 commits May 29, 2026 19:57
Codex 默认把 MCP OAuth 凭据存 OS 钥匙串(按 server_name+url,与账号/auth.json
无关)。新增默认开启的 mcpCredentialsPortableStore:开启后向 ~/.codex/config.toml
写根级 mcp_oauth_credentials_store="file",让 Codex 把凭据落 ~/.codex/.credentials.json,
并在 ~/.codex 之外维护镜像 ~/.codex-app-transfer/mcp-credentials.json。

- crates/codex_integration/mcp_credentials.rs:ensure_file_store_mode +
  sync_mcp_credentials(并集合并:缺失则恢复、存在则捕获;默认取 live 这个
  source-of-truth,仅 live 有真实到期且 mirror 严格更新时取 mirror;任一侧损坏
  整体跳过不覆盖;原子写 0o600)。10 个单测。
- paths.rs:加 mcp_credentials / mcp_credentials_mirror 两路径。
- 接线:main.rs 启动(复用 #306 的 auto_apply await,避开 config.toml 写竞争)
  + snapshot.rs 切换后并集同步 + save_settings 仅在开关真变时即时生效。
  该 key 是全局偏好,不入 MANAGED_TOML_KEYS(restore 不剥)。

不解决 OAuth 自然过期;token 明文落盘(0o600,Codex 官方支持模式)。Refs MOC-62。
设置页加 #mcpCredentialsPortableStore checkbox(load/save/listener 同
restoreCodexOnExit 范式,默认开 via !== false);i18n 中英文案点明明文落盘
0o600 + 切账号/误删/换机自动恢复 + 不解决过期。Refs MOC-62。
README 中英 config 守护段加可移植保险箱说明、存储清单补
~/.codex/.credentials.json 与 mcp-credentials.json 镜像;ONBOARDING 仓库地图
codex_integration 描述补该能力。Refs MOC-62。
chatgpt-codex-connector P2:原并集合并把"只在镜像里的 key"一律当意外丢失写回
live —— 用户 codex mcp logout / 撤销某 MCP 后,下次启动/切换又被复活,logout 失效
+ 撤销的凭据被恢复(轻度安全问题)。已对照 Codex v0.133 oauth.rs:462-471 确认
delete_oauth_tokens_from_file 是 store.remove(key)+重写整文件的单 key 删除。

改为"整文件灾难性丢失备份"语义:live 整文件不在才从镜像恢复;live 存在则它是
权威,镜像精确跟随(捕获新授权 + 传播删除),绝不把 live 没有的 key 写回 live。
移除不再需要的 expires_at 并集/tie-break;SyncReport 加 dropped。11 单测(含
logout 不复活 / logout-all 清空 / 共享 key 取 live / 损坏跳过)。Refs MOC-62。
chatgpt-codex-connector 第二轮 P2:登出最后一个 MCP server 时 Codex 会删掉整个
.credentials.json(write_fallback_file 空 store 即 remove_file,已对照 oauth.rs:556),
故"整文件不在"既可能是有意登出全部、也可能误删/换机,同步时无从区分 —— 原
Missing→自动恢复会静默复活用户已撤销的凭据。

按用户选择改成「每次缺失弹确认」:
- sync 不再自动写 live;live 整文件缺失只报 restore_available(镜像可恢复条数)。
- main.rs 启动 await auto_apply 后,restore_available>0 → emit
  mcp-credentials-restore-available 事件 → 前端 __TAURI__.dialog.ask 弹确认:
  恢复 → POST /api/desktop/mcp-credentials/restore(镜像写回 live,仅 live 仍空时);
  忽略 → /discard(删镜像,停止再弹)。
- live 存在仍权威,镜像跟随(捕获 + 传播删除),单 server 主动登出绝不复活。
新增 restore_mcp_credentials_from_mirror / discard_mcp_mirror + 2 路由 + 2 CCApi +
前端监听/确认 + zh/en 文案;README/hint「自动恢复」改「弹确认恢复」。13 单测。Refs MOC-62。
chatgpt-codex-connector 第三轮 P2:restore_available 的一次性 Tauri event 在快路径
(autoApplyOnStart 关 / 无 active provider → sync 几乎即时跑完)可能在前端 listener
注册前就 emit → 提示静默丢失,用户无从发现可恢复的备份。

改成只读状态端点 + 前端 load 轮询:
- 新增 restore_available_count(只读,无副作用)+ GET /api/desktop/mcp-credentials/status。
- main.rs 不再 emit 一次性事件;startup_sync 仍负责 ensure file 模式 + 镜像跟随。
- 前端 load 时 CCApi.getMcpCredentialsStatus(),restoreAvailable>0 → 弹确认
  (in-flight guard 防重入)。确定性、无竞态。
14 单测(+restore_available_count_only_when_live_missing)。Refs MOC-62。
@Cmochance Cmochance force-pushed the feat/moc-62-mcp-portable-store branch from d729c16 to 9daebfe Compare May 29, 2026 12:03
@Cmochance Cmochance changed the base branch from fix/moc-54-misc to main May 29, 2026 12:03
@Cmochance Cmochance closed this May 29, 2026
@Cmochance Cmochance reopened this May 29, 2026
@Cmochance Cmochance merged commit 288ff6e into main May 29, 2026
11 checks passed
@Cmochance Cmochance deleted the feat/moc-62-mcp-portable-store branch May 29, 2026 12:38
Cmochance added a commit that referenced this pull request May 29, 2026
…t 漏跑 (#308)

stacked PR(base=feature 分支)在底 PR 合并后自动 retarget 到 main 时,no-ai-coauthor
因 `branches:[main]` 过滤 + retarget 非触发事件类型而漏跑 → 该 required check 缺失 →
PR BLOCKED,只能手动 close+reopen 补跑(合 MOC-62 #307 时亲历)。去掉 base 过滤、对所有
PR 跑(跟 ci.yml 一致);job 内按 PR_BASE_SHA..PR_HEAD_SHA 扫 commit,任意 base 都正确。

Refs MOC-64
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant