From 1866e516779012d3ff20541d21074f00be010b32 Mon Sep 17 00:00:00 2001 From: Sean Huang Date: Thu, 24 Sep 2026 19:34:54 +0000 Subject: [PATCH] Make project trust lookup paths explicit and defer root resolution (#47924) ## What changed - Export `ProjectTrustPath` and make `ProjectTrustLookup::from_paths` accept an explicit working directory and optional repository root instead of an arbitrary sequence of path pairs. - Defer repository-root canonicalization until neither working-directory key matches, avoiding unnecessary filesystem access while preserving lookup precedence. ## Testing Extend the active-project regression test to cover repository-root fallback and ensure a working-directory entry without a trust level takes precedence over a trusted root. Adapt the existing POSIX and Windows lookup tests to the new API. GitOrigin-RevId: ba5dc0df1ef9d5109933df00c055eef04434601f --- codex-rs/config/src/config_toml.rs | 12 ++++--- codex-rs/config/src/lib.rs | 1 + codex-rs/config/src/project_trust.rs | 37 ++++++++++++++-------- codex-rs/config/src/project_trust_tests.rs | 22 +++++++++---- codex-rs/core/src/config/config_tests.rs | 26 +++++++++++++-- 5 files changed, 73 insertions(+), 25 deletions(-) diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 7d966891a3..3369f4613c 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -866,10 +866,14 @@ impl ConfigToml { repo_root: Option<&Path>, ) -> Option { self.projects.as_ref()?; - self.get_active_project_for_lookup(&crate::ProjectTrustLookup::from_native( - resolved_cwd, - repo_root, - )) + // Only canonicalize the repository root when neither cwd key matches. + std::iter::once(resolved_cwd) + .chain(repo_root) + .find_map(|path| { + self.get_active_project_for_lookup(&crate::ProjectTrustLookup::from_native_path( + path, + )) + }) } } diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index a999fe64f2..fac2652e47 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -190,6 +190,7 @@ pub use plugin_edit::set_user_plugin_enabled; pub use project_root_markers::default_project_root_markers; pub use project_root_markers::project_root_markers_from_config; pub use project_trust::ProjectTrustLookup; +pub use project_trust::ProjectTrustPath; pub use requirements_exec_policy::RequirementsExecPolicyDecisionToml; pub use requirements_exec_policy::RequirementsExecPolicyParseError; pub use requirements_exec_policy::RequirementsExecPolicyPatternTokenToml; diff --git a/codex-rs/config/src/project_trust.rs b/codex-rs/config/src/project_trust.rs index d12e5702ab..82678afa53 100644 --- a/codex-rs/config/src/project_trust.rs +++ b/codex-rs/config/src/project_trust.rs @@ -6,6 +6,13 @@ use codex_utils_path::normalize_for_path_comparison; use codex_utils_path_uri::PathConvention; use std::path::Path; +/// Original and executor-normalized canonical spellings of one native path. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProjectTrustPath { + pub original: String, + pub canonical: Option, +} + /// Ordered project lookup keys for one executor working directory. /// /// Resolve this independently of configuration layers, then apply it after all @@ -18,23 +25,22 @@ pub struct ProjectTrustLookup { } impl ProjectTrustLookup { - pub(crate) fn from_native(cwd: &Path, repo_root: Option<&Path>) -> Self { + pub(crate) fn from_native_path(path: &Path) -> Self { Self::from_paths( PathConvention::native(), - std::iter::once(cwd).chain(repo_root).map(|path| { - ( - path.to_string_lossy().into_owned(), - normalize_for_path_comparison(path) - .ok() - .map(|path| path.to_string_lossy().into_owned()), - ) - }), + ProjectTrustPath { + original: path.to_string_lossy().into_owned(), + canonical: normalize_for_path_comparison(path) + .ok() + .map(|path| path.to_string_lossy().into_owned()), + }, + /*repo_root*/ None, ) } - /// Build lookup keys from `(original, canonical)` native path spellings. + /// Build lookup keys for a cwd and optional repository root. /// - /// Supply the cwd first, followed by the optional repository root. Each + /// Cwd keys always take precedence over repository-root keys. Each /// canonical spelling is tried before its original spelling; `None` uses /// only the original. This method performs no filesystem access or path /// resolution. Callers must supply executor-normalized canonical spellings, @@ -43,10 +49,15 @@ impl ProjectTrustLookup { /// matched without ASCII case distinctions; POSIX keys remain case-sensitive. pub fn from_paths( convention: PathConvention, - paths: impl IntoIterator)>, + cwd: ProjectTrustPath, + repo_root: Option, ) -> Self { let mut keys = Vec::new(); - for (original, canonical) in paths { + for ProjectTrustPath { + original, + canonical, + } in std::iter::once(cwd).chain(repo_root) + { let original = normalize_lookup_key(&original, convention); let canonical = canonical .map(|key| normalize_lookup_key(&key, convention)) diff --git a/codex-rs/config/src/project_trust_tests.rs b/codex-rs/config/src/project_trust_tests.rs index 672209bf6a..f935b0e6dd 100644 --- a/codex-rs/config/src/project_trust_tests.rs +++ b/codex-rs/config/src/project_trust_tests.rs @@ -45,10 +45,14 @@ fn assert_lookup_order(lookup: &ProjectTrustLookup, entries: &[(&str, Option anyhow::Result<()> { +async fn active_project_preserves_cwd_alias_and_repo_root_precedence() -> anyhow::Result<()> { let tmp = tempdir()?; let project_root = tmp.path().join("project"); let alias_root = tmp.path().join("project_alias"); std::fs::create_dir_all(&project_root)?; std::os::unix::fs::symlink(&project_root, &alias_root)?; - let config = ConfigToml { + let mut config = ConfigToml { projects: Some(HashMap::from([( alias_root.to_string_lossy().to_string(), ProjectConfig { @@ -10447,6 +10447,28 @@ async fn active_project_does_not_match_configured_alias_for_canonical_cwd() -> a None ); + let trusted_root = ProjectConfig { + trust_level: Some(TrustLevel::Trusted), + }; + config.projects.as_mut().unwrap().insert( + tmp.path().to_string_lossy().into_owned(), + trusted_root.clone(), + ); + assert_eq!( + config.get_active_project(&project_root, Some(tmp.path())), + Some(trusted_root) + ); + + let empty_cwd = ProjectConfig { trust_level: None }; + config.projects.as_mut().unwrap().insert( + project_root.to_string_lossy().into_owned(), + empty_cwd.clone(), + ); + assert_eq!( + config.get_active_project(&project_root, Some(tmp.path())), + Some(empty_cwd) + ); + Ok(()) }