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.<id>] 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.
This commit is contained in:
Jason
2026-07-08 16:28:36 +08:00
parent fad5b4c094
commit 8b1ce764f1
2 changed files with 41 additions and 8 deletions
+5 -8
View File
@@ -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::<toml_edit::DocumentMut>() {
Ok(doc) => doc,
Err(e) => {
log::warn!("解析 Codex config.toml 失败: {e},将创建新配置");
toml_edit::DocumentMut::new()
}
}
// 解析失败必须报错而不是用空文档顶替:写回空文档会把用户
// config.toml 里的其它段落(model/model_providers/注释等)整体清空
content
.parse::<toml_edit::DocumentMut>()
.map_err(|e| AppError::McpValidation(format!("解析 config.toml 失败: {e}")))?
} else {
toml_edit::DocumentMut::new()
};
+36
View File
@@ -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");