From 750ca9e21d269fc065923319a3b57a6677ecbf97 Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:20:36 -0700 Subject: [PATCH] core: write explicit [projects] tables for trusted projects (#2523) all of my trust_level settings in my ~/.codex/config.toml were on one line. --- codex-rs/core/src/config.rs | 141 +++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 4b6f8ac9..5b6a1ed2 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -259,10 +259,53 @@ pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Re Err(e) => return Err(e.into()), }; - // Mark the project as trusted. toml_edit is very good at handling - // missing properties + // Ensure we render a human-friendly structure: + // + // [projects] + // [projects."/path/to/project"] + // trust_level = "trusted" + // + // rather than inline tables like: + // + // [projects] + // "/path/to/project" = { trust_level = "trusted" } let project_key = project_path.to_string_lossy().to_string(); - doc["projects"][project_key.as_str()]["trust_level"] = toml_edit::value("trusted"); + + // Ensure top-level `projects` exists as a non-inline, explicit table. If it + // exists but was previously represented as a non-table (e.g., inline), + // replace it with an explicit table. + { + let root = doc.as_table_mut(); + let needs_table = !root.contains_key("projects") + || root.get("projects").and_then(|i| i.as_table()).is_none(); + if needs_table { + root.insert("projects", toml_edit::table()); + } + } + let Some(projects_tbl) = doc["projects"].as_table_mut() else { + return Err(anyhow::anyhow!( + "projects table missing after initialization" + )); + }; + + // Ensure the per-project entry is its own explicit table. If it exists but + // is not a table (e.g., an inline table), replace it with an explicit table. + let needs_proj_table = !projects_tbl.contains_key(project_key.as_str()) + || projects_tbl + .get(project_key.as_str()) + .and_then(|i| i.as_table()) + .is_none(); + if needs_proj_table { + projects_tbl.insert(project_key.as_str(), toml_edit::table()); + } + let Some(proj_tbl) = projects_tbl + .get_mut(project_key.as_str()) + .and_then(|i| i.as_table_mut()) + else { + return Err(anyhow::anyhow!("project table missing for {}", project_key)); + }; + proj_tbl.set_implicit(false); + proj_tbl["trust_level"] = toml_edit::value("trusted"); // ensure codex_home exists std::fs::create_dir_all(codex_home)?; @@ -1178,4 +1221,96 @@ disable_response_storage = true Ok(()) } + + #[test] + fn test_set_project_trusted_writes_explicit_tables() -> anyhow::Result<()> { + let codex_home = TempDir::new().unwrap(); + let project_dir = TempDir::new().unwrap(); + + // Call the function under test + set_project_trusted(codex_home.path(), project_dir.path())?; + + // Read back the generated config.toml + let config_path = codex_home.path().join(CONFIG_TOML_FILE); + let contents = std::fs::read_to_string(&config_path)?; + + // Verify it does not use inline tables for the project entry + assert!( + !contents.contains("{ trust_level"), + "config.toml should not use inline tables:\n{}", + contents + ); + + // Verify the explicit table for the project exists. toml_edit may choose + // either basic (double-quoted) or literal (single-quoted) strings for keys + // containing backslashes (e.g., on Windows). Accept both forms. + let path_str = project_dir.path().to_string_lossy(); + let project_key_double = format!("[projects.\"{}\"]", path_str); + let project_key_single = format!("[projects.'{}']", path_str); + assert!( + contents.contains(&project_key_double) || contents.contains(&project_key_single), + "missing explicit project table header: expected to find `{}` or `{}` in:\n{}", + project_key_double, + project_key_single, + contents + ); + + // Verify the trust_level entry + assert!( + contents.contains("trust_level = \"trusted\""), + "missing trust_level entry in:\n{}", + contents + ); + + Ok(()) + } + + #[test] + fn test_set_project_trusted_converts_inline_to_explicit() -> anyhow::Result<()> { + let codex_home = TempDir::new().unwrap(); + let project_dir = TempDir::new().unwrap(); + + // Seed config.toml with an inline project entry under [projects] + let config_path = codex_home.path().join(CONFIG_TOML_FILE); + let path_str = project_dir.path().to_string_lossy(); + // Use a literal-quoted key so Windows backslashes don't require escaping + let initial = format!( + "[projects]\n'{}' = {{ trust_level = \"untrusted\" }}\n", + path_str + ); + std::fs::create_dir_all(codex_home.path())?; + std::fs::write(&config_path, initial)?; + + // Run the function; it should convert to explicit tables and set trusted + set_project_trusted(codex_home.path(), project_dir.path())?; + + let contents = std::fs::read_to_string(&config_path)?; + + // Should not contain inline table representation anymore (accept both quote styles) + let inline_double = format!("\"{}\" = {{ trust_level = \"trusted\" }}", path_str); + let inline_single = format!("'{}' = {{ trust_level = \"trusted\" }}", path_str); + assert!( + !contents.contains(&inline_double) && !contents.contains(&inline_single), + "config.toml should not contain inline project table anymore:\n{}", + contents + ); + + // And explicit child table header for the project + let project_key_double = format!("[projects.\"{}\"]", path_str); + let project_key_single = format!("[projects.'{}']", path_str); + assert!( + contents.contains(&project_key_double) || contents.contains(&project_key_single), + "missing explicit project table header: expected to find `{}` or `{}` in:\n{}", + project_key_double, + project_key_single, + contents + ); + + // And the trust level value + assert!(contents.contains("trust_level = \"trusted\"")); + + Ok(()) + } + + // No test enforcing the presence of a standalone [projects] header. }