Files
Sean Huang 13869ca39e Add portable project trust lookup APIs (#47620)
## What changed

Add `resolve_root_git_project_uri_for_trust` to resolve repository trust roots through `ExecutorFileSystem` using executor path conventions. Share repository and linked-worktree validation with native lookup while preserving native ancestry for paths with opaque URI representations.

Extract `ProjectTrustLookup` to match merged project settings using supplied path spellings and an explicit path convention. Preserve canonical-before-original and cwd-before-repository-root precedence, including cwd entries without a trust level, with ASCII case-insensitive matching for Windows.

## Testing

Add coverage for POSIX, Windows drive and UNC trust roots, project-key precedence and case handling, and native/URI lookup parity. Extend environment-config coverage to verify returned `projects` entries include entries without a trust level.

GitOrigin-RevId: 315434802e86cceee85b14bc1c3767a45e3139f5
2026-09-23 18:40:12 +00:00

161 lines
5.8 KiB
Rust

mod common;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerSource;
use codex_config::format_config_layer_source;
use codex_config::loader::project_trust_key;
use codex_exec_server::Environment;
use codex_exec_server::EnvironmentConfigLayer;
use codex_exec_server::EnvironmentConfigLayerStack;
use codex_exec_server::EnvironmentConfigReadParams;
use codex_exec_server::EnvironmentConfigReadResponse;
use codex_exec_server::ExecServerError;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use common::exec_server::exec_server;
use pretty_assertions::assert_eq;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_environment_reads_projected_executor_config() -> anyhow::Result<()> {
let mut server = exec_server().await?;
let codex_home =
AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(server.codex_home())?)?;
let config_file = codex_home.join(CONFIG_TOML_FILE);
let project = codex_home.join("project");
let nested = project.join("nested");
let dot_codex = project.join(".codex");
tokio::fs::create_dir_all(dot_codex.as_path()).await?;
tokio::fs::write(project.join(".project-root").as_path(), "").await?;
let project_key = toml::Value::String(project_trust_key(project.as_path())).to_string();
let nested_key = toml::Value::String(project_trust_key(nested.as_path())).to_string();
let projects_toml =
format!("[projects.{project_key}]\ntrust_level = \"trusted\"\n[projects.{nested_key}]\n");
tokio::fs::write(
&config_file,
format!("project_root_markers = [\".project-root\"]\n{projects_toml}"),
)
.await?;
tokio::fs::write(
dot_codex.join(CONFIG_TOML_FILE).as_path(),
r#"
[future_environment]
relative_path = "./executor-relative"
unselected = "do not return"
"#,
)
.await?;
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
let environment_info = environment.info().await?;
assert!(environment_info.capabilities.environment_config_read);
assert_eq!(
environment_info.user_home_dir,
dirs::home_dir().and_then(|home_dir| PathUri::from_host_native_path(home_dir).ok()),
);
let response = environment
.read_environment_config(EnvironmentConfigReadParams {
cwd: PathUri::from_abs_path(&project),
config_paths: vec![vec![
"future_environment".to_string(),
"relative_path".to_string(),
]],
requirements_paths: Vec::new(),
})
.await?;
let projected_toml = toml::toml! {
[future_environment]
relative_path = "./executor-relative"
};
assert_eq!(
response,
EnvironmentConfigReadResponse {
user_home_dir: dirs::home_dir()
.and_then(|home_dir| PathUri::from_host_native_path(home_dir).ok()),
codex_home_dir: PathUri::from_abs_path(&codex_home),
hostname: codex_config::host_name(),
config: EnvironmentConfigLayerStack {
layers: vec![EnvironmentConfigLayer {
source: format_config_layer_source(
&ConfigLayerSource::Project {
dot_codex_folder: dot_codex.clone(),
},
CONFIG_TOML_FILE,
),
base_dir: PathUri::from_abs_path(&dot_codex),
toml: toml::to_string(&projected_toml)?,
}],
cloud_insertion_index: 0,
},
requirements: EnvironmentConfigLayerStack {
layers: Vec::new(),
cloud_insertion_index: 0,
},
}
);
let mut expected_projects = response;
expected_projects.config.layers[0] = EnvironmentConfigLayer {
source: format_config_layer_source(
&ConfigLayerSource::User {
file: config_file,
profile: None,
},
CONFIG_TOML_FILE,
),
base_dir: PathUri::from_abs_path(&codex_home),
toml: toml::to_string(&toml::from_str::<toml::Value>(&projects_toml)?)?,
};
let projects = environment
.read_environment_config(EnvironmentConfigReadParams {
cwd: PathUri::from_abs_path(&project),
config_paths: vec![vec!["projects".to_string()]],
requirements_paths: Vec::new(),
})
.await?;
assert_eq!(projects, expected_projects);
server.shutdown().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn environment_config_read_rejects_empty_selectors() -> anyhow::Result<()> {
let mut server = exec_server().await?;
let codex_home =
AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(server.codex_home())?)?;
let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?;
for (config_paths, expected_message) in [
(
Vec::new(),
"at least one config or requirements path is required",
),
(
vec![Vec::new()],
"TOML paths must contain at least one key segment",
),
] {
let error = environment
.read_environment_config(EnvironmentConfigReadParams {
cwd: PathUri::from_abs_path(&codex_home),
config_paths,
requirements_paths: Vec::new(),
})
.await
.expect_err("invalid selectors should fail");
assert!(
matches!(
error,
ExecServerError::Server { code: -32602, ref message }
if message == expected_message
),
"unexpected error: {error:?}"
);
}
server.shutdown().await?;
Ok(())
}