Files
codex/prs/bolinfest/PR-2523.md
2025-09-02 15:17:45 -07:00

9.6 KiB

PR #2523: core: write explicit [projects] tables for trusted projects

Description

all of my trust_level settings in my ~/.codex/config.toml were on one line.

Full Diff

diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index 4b6f8ac9ef..5b6a1ed265 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.
 }

Review Comments

codex-rs/core/src/config.rs

@@ -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)?;

Is there a reason why we cannot assert_eq!() on contents? What is variable in this point of the test?