Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/app-server/tests/suite/v2/config_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ async fn config_read_respects_managed_project_root_markers() -> Result<()> {
] {
std::fs::create_dir_all(dir)?;
}
std::fs::write(workspace.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
std::fs::write(
ancestor_config.join("config.toml"),
"model_context_window = 32768\n",
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/app-server/tests/suite/v2/thread_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1640,8 +1640,9 @@ async fn thread_start_with_nested_git_cwd_respects_effective_permissions_for_pro

let repo_root = TempDir::new()?;
std::fs::create_dir(repo_root.path().join(".git"))?;
std::fs::write(repo_root.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
let nested = repo_root.path().join("nested/project");
std::fs::create_dir_all(&nested)?;
std::fs::create_dir_all(nested.join(".git"))?;

let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
Expand Down
30 changes: 30 additions & 0 deletions codex-rs/config/src/loader/managed_project_discovery_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ impl Fixture {
let cwd = project.join("child");
std::fs::create_dir_all(&home)?;
std::fs::create_dir_all(repo.join(".git"))?;
std::fs::write(repo.join(".git/HEAD"), "ref: refs/heads/main\n")?;
for (dir, model) in [(&repo, "ancestor"), (&project, "project"), (&cwd, "child")] {
std::fs::create_dir_all(dir.join(".codex"))?;
std::fs::write(
Expand Down Expand Up @@ -181,6 +182,35 @@ async fn managed_project_discovery_uses_file_markers_without_changing_precedence
Ok(())
}

#[tokio::test]
async fn managed_project_discovery_ignores_incomplete_nested_git_directory() -> anyhow::Result<()> {
let fixture = Fixture::new()?;
std::fs::create_dir(fixture.cwd.join(".git"))?;
std::fs::write(&fixture.managed, "project_root_markers = [\".git\"]\n")?;

let (canonical, local) = fixture.load().await?;
assert_discovery(
&canonical,
&local,
&[&fixture.repo, &fixture.project, &fixture.cwd],
&[".git"],
)?;

std::fs::write(fixture.home.join("config.toml"), fixture.trust("untrusted"))?;
let (canonical, local) = fixture.load().await?;
assert_discovery(&canonical, &local, &[], &[".git"])?;
let trust_key = project_trust_key(fixture.repo.as_path());
assert!(
canonical
.all_layers_low_to_high()
.filter(|layer| matches!(layer.name, ConfigLayerSource::Project { .. }))
.all(|layer| layer.disabled_reason.as_deref().is_some_and(
|reason| reason.starts_with(&format!("{trust_key} is marked as untrusted"))
))
);
Ok(())
}

#[tokio::test]
async fn managed_project_discovery_uses_managed_project_trust() -> anyhow::Result<()> {
let fixture = Fixture::new()?;
Expand Down
37 changes: 27 additions & 10 deletions codex-rs/config/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1315,13 +1315,22 @@ async fn find_project_root(
for marker in project_root_markers {
let marker_path = ancestor.join(marker);
let marker_path_uri = PathUri::from_abs_path(&marker_path);
if fs
.get_metadata(&marker_path_uri, /*sandbox*/ None)
.await
.is_ok()
let Ok(metadata) = fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await else {
continue;
};
if marker == ".git"
&& metadata.is_directory
&& fs
.get_metadata(
&PathUri::from_abs_path(&marker_path.join("HEAD")),
/*sandbox*/ None,
)
.await
.is_err()
{
return Ok(ancestor);
continue;
}
return Ok(ancestor);
}
}
Ok(cwd.clone())
Expand All @@ -1340,13 +1349,21 @@ async fn find_git_checkout_root(
for dir in base.ancestors() {
let dot_git = dir.join(".git");
let dot_git_uri = PathUri::from_abs_path(&dot_git);
if fs
.get_metadata(&dot_git_uri, /*sandbox*/ None)
.await
.is_ok()
let Ok(metadata) = fs.get_metadata(&dot_git_uri, /*sandbox*/ None).await else {
continue;
};
if metadata.is_directory
&& fs
.get_metadata(
&PathUri::from_abs_path(&dot_git.join("HEAD")),
/*sandbox*/ None,
)
.await
.is_err()
{
return Some(dir);
continue;
}
return Some(dir);
}
None
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/config/config_loader_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3295,6 +3295,7 @@ async fn codex_home_within_project_tree_is_not_double_loaded() -> std::io::Resul

tokio::fs::create_dir_all(&nested_dot_codex).await?;
tokio::fs::create_dir_all(project_root.join(".git")).await?;
tokio::fs::write(project_root.join(".git/HEAD"), "ref: refs/heads/main\n").await?;
tokio::fs::write(
nested_dot_codex.join(CONFIG_TOML_FILE),
r#"foo = "child"
Expand Down
40 changes: 40 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8285,6 +8285,7 @@ async fn agent_role_file_without_developer_instructions_is_dropped_with_warning(
let repo_root = TempDir::new()?;
let nested_cwd = repo_root.path().join("packages").join("app");
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::write(repo_root.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
std::fs::create_dir_all(&nested_cwd)?;

let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
Expand Down Expand Up @@ -8456,6 +8457,7 @@ async fn discovered_agent_role_file_without_name_is_dropped_with_warning() -> st
let repo_root = TempDir::new()?;
let nested_cwd = repo_root.path().join("packages").join("app");
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::write(repo_root.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
std::fs::create_dir_all(&nested_cwd)?;

let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
Expand Down Expand Up @@ -8656,6 +8658,7 @@ async fn discovers_multiple_standalone_agent_role_files() -> std::io::Result<()>
let repo_root = TempDir::new()?;
let nested_cwd = repo_root.path().join("packages").join("app");
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::write(repo_root.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
std::fs::create_dir_all(&nested_cwd)?;

let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
Expand Down Expand Up @@ -8787,6 +8790,7 @@ async fn mixed_legacy_and_standalone_agent_role_sources_merge_with_precedence()
let repo_root = TempDir::new()?;
let nested_cwd = repo_root.path().join("packages").join("app");
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::write(repo_root.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
std::fs::create_dir_all(&nested_cwd)?;

let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
Expand Down Expand Up @@ -8933,6 +8937,7 @@ async fn higher_precedence_agent_role_can_inherit_description_from_lower_layer()
let repo_root = TempDir::new()?;
let nested_cwd = repo_root.path().join("packages").join("app");
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::write(repo_root.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
std::fs::create_dir_all(&nested_cwd)?;

let workspace_key = repo_root.path().to_string_lossy().replace('\\', "\\\\");
Expand Down Expand Up @@ -10341,6 +10346,41 @@ mcp_oauth_callback_url = "https://example.com/callback"
Ok(())
}

#[tokio::test]
async fn untrusted_parent_repo_with_incomplete_child_git_keeps_unless_trusted_approval_policy()
-> anyhow::Result<()> {
let codex_home = TempDir::new()?;
let repo = TempDir::new()?;
let cwd = repo.path().join("sub");
std::fs::create_dir_all(repo.path().join(".git"))?;
std::fs::write(repo.path().join(".git/HEAD"), "ref: refs/heads/main\n")?;
std::fs::create_dir_all(cwd.join(".git"))?;

let config = Config::load_from_base_config_with_overrides(
ConfigToml {
projects: Some(HashMap::from([(
repo.path().to_string_lossy().to_string(),
ProjectConfig {
trust_level: Some(TrustLevel::Untrusted),
},
)])),
..Default::default()
},
ConfigOverrides {
cwd: Some(cwd),
..Default::default()
},
codex_home.abs(),
)
.await?;

assert_eq!(
config.permissions.approval_policy.value(),
AskForApproval::UnlessTrusted
);
Ok(())
}

#[tokio::test]
async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
Expand Down
10 changes: 9 additions & 1 deletion codex-rs/core/src/git_info_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use codex_exec_server::RemoveOptions;
use codex_git_utils::GitInfo;
use codex_git_utils::GitSha;
use codex_git_utils::collect_git_info;
use codex_git_utils::get_git_repo_root;
use codex_git_utils::get_has_changes_in_repo;
use codex_git_utils::git_diff_to_remote;
use codex_git_utils::recent_commits;
Expand Down Expand Up @@ -643,6 +644,7 @@ async fn resolve_root_git_project_for_trust_starts_at_parent_for_file() {
let proj = tmp.path().join("proj");
let nested = proj.join("nested");
std::fs::create_dir_all(proj.join(".git")).unwrap();
std::fs::write(proj.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::create_dir_all(&nested).unwrap();
let file = nested.join("file.txt");
std::fs::write(&file, "contents").unwrap();
Expand All @@ -659,6 +661,7 @@ async fn resolve_root_git_project_for_trust_ignores_metadata_errors() {
let proj = tmp.path().join("proj");
let nested = proj.join("nested");
std::fs::create_dir_all(proj.join(".git")).unwrap();
std::fs::write(proj.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::create_dir_all(&nested).unwrap();
let fs = MetadataOverrideFileSystem {
path: PathUri::from_abs_path(&nested.join(".git").abs()),
Expand All @@ -678,6 +681,7 @@ async fn resolve_root_git_project_for_trust_supports_windows_namespace_paths() {
let tmp = TempDir::new().expect("tempdir");
let repo = tmp.path().join("repo");
std::fs::create_dir_all(repo.join(".git")).unwrap();
std::fs::write(repo.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
std::fs::create_dir_all(repo.join("nested")).unwrap();

let namespace_repo = PathBuf::from(format!(r"\\?\{}", repo.display()));
Expand All @@ -699,7 +703,11 @@ async fn resolve_root_git_project_for_trust_regular_repo_returns_repo_root() {
Some(repo_path.clone())
);
let nested = repo_path.join("sub/dir");
std::fs::create_dir_all(nested.as_path()).unwrap();
std::fs::create_dir_all(nested.join(".git")).unwrap();
assert_eq!(
get_git_repo_root(nested.as_path()),
Some(repo_path.as_path().to_path_buf())
);
assert_eq!(
resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested).await,
Some(repo_path)
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/turn_metadata_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,11 @@ async fn turn_metadata_state_git_enrichment_cancellation_is_retryable_and_errors

let invalid_repo = TempDir::new().expect("invalid repo");
std::fs::create_dir(invalid_repo.path().join(".git")).expect("invalid git directory");
std::fs::write(
invalid_repo.path().join(".git/HEAD"),
"ref: refs/heads/main\n",
)
.expect("invalid git HEAD");
let invalid_state = Arc::new(TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/git-utils/src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ fn find_ancestor_git_entry(base_dir: &Path) -> Option<(PathBuf, PathBuf)> {

loop {
let dot_git = dir.join(".git");
if dot_git.exists() {
if dot_git.exists() && (!dot_git.is_dir() || dot_git.join("HEAD").exists()) {
return Some((dir, dot_git));
}

Expand Down
38 changes: 29 additions & 9 deletions codex-rs/git-utils/src/trust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,35 @@ pub async fn resolve_root_git_project_for_trust(
Ok(metadata) if metadata.is_directory => cwd.clone(),
_ => cwd.parent()?,
};
let repo_root = find_nearest_native_ancestor_with_markers(
fs,
&base,
vec![".git".to_string()],
FindUpErrorPolicy::Ignore,
/*sandbox*/ None,
)
.await
.ok()??;
let mut base = base;
let repo_root = loop {
let candidate = find_nearest_native_ancestor_with_markers(
fs,
&base,
vec![".git".to_string()],
FindUpErrorPolicy::Ignore,
/*sandbox*/ None,
)
.await
.ok()??;
let dot_git = candidate.join(".git");
let metadata = fs
.get_metadata(&PathUri::from_abs_path(&dot_git), /*sandbox*/ None)
.await
.ok()?;
if !metadata.is_directory
|| fs
.get_metadata(
&PathUri::from_abs_path(&dot_git.join("HEAD")),
/*sandbox*/ None,
)
.await
.is_ok()
{
break candidate;
}
base = candidate.parent()?;
};
let dot_git = repo_root.join(".git");
let dot_git_uri = PathUri::from_abs_path(&dot_git);
let dot_git_metadata = fs.get_metadata(&dot_git_uri, /*sandbox*/ None).await.ok()?;
Expand Down
Loading
Loading