From 8b1ce764f1ee47e94488b0c7f9f6f239abfff882 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 8 Jul 2026 16:28:36 +0800 Subject: [PATCH] fix(mcp): fail closed when Codex config.toml is unparseable during MCP sync sync_single_server_to_codex silently fell back to an empty DocumentMut when the existing config.toml failed to parse, then wrote the whole file back -- wiping every other section (model, model_providers, comments) and leaving only the single synced [mcp_servers.] entry. Introduced by 3a548152, which fixed the removal path but left the destructive fallback on the sync path. Now the sync path returns an McpValidation error and leaves the file untouched, matching the fail-closed behavior of the read/validate path. --- src-tauri/src/mcp/codex.rs | 13 ++++------ src-tauri/tests/import_export_sync.rs | 36 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/mcp/codex.rs b/src-tauri/src/mcp/codex.rs index 7ab7eef8f..73100beda 100644 --- a/src-tauri/src/mcp/codex.rs +++ b/src-tauri/src/mcp/codex.rs @@ -361,14 +361,11 @@ pub fn sync_single_server_to_codex( let mut doc = if config_path.exists() { let content = std::fs::read_to_string(&config_path).map_err(|e| AppError::io(&config_path, e))?; - // 尝试解析现有配置,如果失败则创建新文档(容错处理) - match content.parse::() { - Ok(doc) => doc, - Err(e) => { - log::warn!("解析 Codex config.toml 失败: {e},将创建新配置"); - toml_edit::DocumentMut::new() - } - } + // 解析失败必须报错而不是用空文档顶替:写回空文档会把用户 + // config.toml 里的其它段落(model/model_providers/注释等)整体清空 + content + .parse::() + .map_err(|e| AppError::McpValidation(format!("解析 config.toml 失败: {e}")))? } else { toml_edit::DocumentMut::new() }; diff --git a/src-tauri/tests/import_export_sync.rs b/src-tauri/tests/import_export_sync.rs index e1e81d24e..f6df65876 100644 --- a/src-tauri/tests/import_export_sync.rs +++ b/src-tauri/tests/import_export_sync.rs @@ -497,6 +497,42 @@ fn sync_enabled_to_codex_returns_error_on_invalid_toml() { } } +#[test] +fn sync_single_server_to_codex_fails_closed_on_invalid_toml() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let path = cc_switch_lib::get_codex_config_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).expect("create codex dir"); + } + // 含用户内容 + 语法错误的 config.toml:同步必须报错且不得覆盖文件 + let broken = "model = \"gpt-5.5\"\ninvalid = [\n"; + fs::write(&path, broken).expect("write invalid config"); + + let config = MultiAppConfig::default(); + let err = cc_switch_lib::sync_single_server_to_codex( + &config, + "srv", + &json!({ "type": "stdio", "command": "echo" }), + ) + .expect_err("sync should fail instead of wiping the file"); + match err { + cc_switch_lib::AppError::McpValidation(msg) => { + assert!( + msg.contains("config.toml"), + "error message should mention config.toml" + ); + } + other => panic!("unexpected error: {other:?}"), + } + + let text = fs::read_to_string(&path).expect("read config.toml"); + assert_eq!( + text, broken, + "invalid config.toml must be left untouched on sync failure" + ); +} + #[test] fn sync_codex_provider_missing_auth_returns_error() { let _guard = test_mutex().lock().expect("acquire test mutex");