diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index 0c84800055..ea53e2afab 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -435,7 +435,9 @@ async fn repo_agents_skill_roots( // Discover repo `.agents/skills` folders by walking from the selected environment's project // root to its cwd; this must never consult the local filesystem for remote workspaces. let project_root_markers = project_root_markers_from_stack(config_layer_stack); - let project_root = find_project_root(&env_path.path, &project_root_markers).await; + let Some(project_root) = find_project_root(&env_path.path, &project_root_markers).await else { + return Vec::new(); + }; let dirs = dirs_between_project_root_and_cwd(env_path.path.path(), project_root.path()); let mut roots = Vec::new(); for dir in dirs { @@ -488,28 +490,29 @@ fn project_root_markers_from_stack(config_layer_stack: &ConfigLayerStack) -> Vec async fn find_project_root( cwd: &EnvironmentPathRef, project_root_markers: &[String], -) -> EnvironmentPathRef { +) -> Option { if project_root_markers.is_empty() { - return cwd.clone(); + return Some(cwd.clone()); } for ancestor in cwd.path().ancestors() { for marker in project_root_markers { let marker_path = cwd.with_path(ancestor.join(marker)); match marker_path.metadata(/*sandbox*/ None).await { - Ok(_) => return cwd.with_path(ancestor), + Ok(_) => return Some(cwd.with_path(ancestor)), Err(err) if err.kind() == io::ErrorKind::NotFound => {} Err(err) => { tracing::warn!( "failed to stat project root marker {}: {err:#}", marker_path.path().display() ); + return None; } } } } - cwd.clone() + Some(cwd.clone()) } fn dirs_between_project_root_and_cwd( diff --git a/codex-rs/core-skills/src/loader_tests.rs b/codex-rs/core-skills/src/loader_tests.rs index 00bcff9bb4..a0eec70609 100644 --- a/codex-rs/core-skills/src/loader_tests.rs +++ b/codex-rs/core-skills/src/loader_tests.rs @@ -18,6 +18,8 @@ use std::fs; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; use tempfile::TempDir; use toml::Value as TomlValue; @@ -394,6 +396,35 @@ async fn skill_roots_bind_project_config_roots_to_primary_environment_only() -> Ok(()) } +#[tokio::test] +async fn skill_roots_stop_repo_agents_walk_when_environment_probe_fails() -> anyhow::Result<()> { + let codex_home = tempfile::tempdir()?; + let cfg = make_config(&codex_home).await; + let remote_environment = + codex_exec_server::Environment::create_for_tests(Some("ws://127.0.0.1:1".to_string()))?; + let started = Instant::now(); + + let roots = super::skill_roots( + &[super::SkillEnvironment { + environment_id: "remote".to_string(), + path: EnvironmentPathRef::new(remote_environment.get_filesystem(), cfg.cwd.clone()), + }], + /*local_file_system*/ None, + &cfg.config_layer_stack, + Vec::new(), + Vec::new(), + ) + .await; + + assert!(roots.is_empty()); + assert!( + started.elapsed() < Duration::from_secs(4), + "repo agents discovery should stop after the first unavailable environment probe" + ); + + Ok(()) +} + #[tokio::test] async fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> { let tmp = tempfile::tempdir()?;