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
This commit is contained in:
Sean Huang
2026-09-24 19:44:50 +00:00
committed by copyberry
parent 731d4a5ca8
commit 1866e51677
5 changed files with 73 additions and 25 deletions
+8 -4
View File
@@ -866,10 +866,14 @@ impl ConfigToml {
repo_root: Option<&Path>,
) -> Option<ProjectConfig> {
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,
))
})
}
}
+1
View File
@@ -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;
+24 -13
View File
@@ -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<String>,
}
/// 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<Item = (String, Option<String>)>,
cwd: ProjectTrustPath,
repo_root: Option<ProjectTrustPath>,
) -> 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))
+16 -6
View File
@@ -45,10 +45,14 @@ fn assert_lookup_order(lookup: &ProjectTrustLookup, entries: &[(&str, Option<Tru
fn posix_lookup_preserves_cwd_root_alias_order_and_case() {
let lookup = ProjectTrustLookup::from_paths(
PathConvention::Posix,
[
("/alias/src".into(), Some("/repo/src".into())),
("/alias".into(), Some("/repo".into())),
],
ProjectTrustPath {
original: "/alias/src".into(),
canonical: Some("/repo/src".into()),
},
Some(ProjectTrustPath {
original: "/alias".into(),
canonical: Some("/repo".into()),
}),
);
assert_lookup_order(
&lookup,
@@ -68,8 +72,14 @@ fn posix_lookup_preserves_cwd_root_alias_order_and_case() {
#[test]
fn windows_prefers_exact_normalized_key_then_sorted_aliases() {
let lookup =
ProjectTrustLookup::from_paths(PathConvention::Windows, [(r"C:\Repo".into(), None)]);
let lookup = ProjectTrustLookup::from_paths(
PathConvention::Windows,
ProjectTrustPath {
original: r"C:\Repo".into(),
canonical: None,
},
/*repo_root*/ None,
);
assert_lookup_order(
&lookup,
&[
+24 -2
View File
@@ -10425,14 +10425,14 @@ trust_level = "trusted"
#[cfg(unix)]
#[tokio::test]
async fn active_project_does_not_match_configured_alias_for_canonical_cwd() -> 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(())
}