From 90354ab9aa356039c8e39853c22914830e13fce3 Mon Sep 17 00:00:00 2001 From: Yuri Trukhin <1245379+trukhinyuri@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:53:21 +0100 Subject: [PATCH] fix(agents): parse JSON configuration with serde_json before json5 Every start validates the agent configuration files, and every file that is not TOML or YAML went through json5. The json5 (pest) parser needs about 80 times the input size in memory, so the generated Codex model catalog (~/.codex/cpa-gui-model-catalog.json, 64.6 MB with 733 models) pushed the app to a 5.6 GiB peak at every launch. Try strict JSON first and fall back to json5 only when that fails, so comments and trailing commas in hand-edited files keep working. --- src-tauri/src/agents/discovery.rs | 5 +++-- src-tauri/src/agents/transactions.rs | 11 ++++++++++- src-tauri/src/agents/transactions/tests.rs | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/agents/discovery.rs b/src-tauri/src/agents/discovery.rs index 2359932aa..94ea2ca0d 100644 --- a/src-tauri/src/agents/discovery.rs +++ b/src-tauri/src/agents/discovery.rs @@ -1577,7 +1577,7 @@ pub(crate) fn agent_has_connection_evidence( ) .map_err(|e| e.to_string())?, Some("yaml" | "yml") => serde_yaml::from_str(content).map_err(|e| e.to_string())?, - _ => json5::from_str(content).map_err(|e| e.to_string())?, + _ => super::transactions::parse_json_or_json5(content)?, }; let provider_present = client != AgentClient::Codex && [ @@ -3587,7 +3587,8 @@ pub(crate) fn read_agent_json5_or_empty( return Ok(serde_json::json!({})); } let value: serde_json::Value = - json5::from_str(&content).map_err(|error| format!("Failed to parse {label}: {error}"))?; + super::transactions::parse_json_or_json5(&content) + .map_err(|error| format!("Failed to parse {label}: {error}"))?; if !value.is_object() { return Err(format!("{label} root must be an object")); } diff --git a/src-tauri/src/agents/transactions.rs b/src-tauri/src/agents/transactions.rs index 1ec76a17b..66988e1ca 100644 --- a/src-tauri/src/agents/transactions.rs +++ b/src-tauri/src/agents/transactions.rs @@ -1171,6 +1171,15 @@ pub(crate) fn text(bytes: Option<&[u8]>) -> Result, String> { .transpose() } +/// Parses JSON configuration, accepting JSON5 syntax (comments, trailing commas) as well. +/// Strict JSON is tried first: the json5 (pest) parser needs about 80x the input size in memory, +/// so a 64 MB generated Codex model catalog took more than 5 GB at every start. +pub(crate) fn parse_json_or_json5(content: &str) -> Result { + serde_json::from_str::(content) + .or_else(|_| json5::from_str::(content)) + .map_err(|e| e.to_string()) +} + pub(crate) fn parse(path: &Path, content: Option<&str>) -> Result { let Some(content) = content else { return Ok(serde_json::json!({})); @@ -1181,7 +1190,7 @@ pub(crate) fn parse(path: &Path, content: Option<&str>) -> Result .map_err(|e| e.to_string()) .and_then(|v| serde_json::to_value(v).map_err(|e| e.to_string())), Some("yaml" | "yml") => serde_norway::from_str::(content).map_err(|e| e.to_string()), - _ => json5::from_str::(content).map_err(|e| e.to_string()), + _ => parse_json_or_json5(content), }; let value = result.map_err(|_| { format!( diff --git a/src-tauri/src/agents/transactions/tests.rs b/src-tauri/src/agents/transactions/tests.rs index e457040c1..93890d972 100644 --- a/src-tauri/src/agents/transactions/tests.rs +++ b/src-tauri/src/agents/transactions/tests.rs @@ -122,3 +122,24 @@ fn relocated_configuration_root_accepts_writes_but_nested_links_do_not() { "a nested configuration link targeting the temp root must stay rejected" ); } + +#[test] +fn parse_json_or_json5_reads_strict_json_and_json5() { + let strict = parse_json_or_json5(r#"{"models":[{"slug":"a","n":1}]}"#).unwrap(); + assert_eq!(strict["models"][0]["slug"], "a"); + + let json5 = parse_json_or_json5("{\n // comment\n models: [{slug: 'b',},],\n}").unwrap(); + assert_eq!(json5["models"][0]["slug"], "b"); + + assert!(parse_json_or_json5("{\"models\":").is_err()); +} + +#[test] +fn parse_keeps_json5_config_files_working() { + let value = parse( + Path::new("opencode.jsonc"), + Some("{\n // keep comments working\n \"provider\": {\"cpa-gui\": {},},\n}"), + ) + .unwrap(); + assert!(value["provider"]["cpa-gui"].is_object()); +}