diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 00982ce276..105fb5ffce 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -531,9 +531,13 @@ impl CatalogRequestProcessor { } None => environment_manager.try_local_environment(), }; + let local_file_system = environment_manager + .try_local_environment() + .map(|environment| environment.get_filesystem()); let mut data = futures::stream::iter(cwds.into_iter().enumerate()) .map(|(index, cwd)| { let config = &config; + let local_file_system = local_file_system.clone(); let selected_environment = selected_environment.clone(); let plugins_manager = &plugins_manager; let skills_manager = &skills_manager; @@ -555,22 +559,6 @@ impl CatalogRequestProcessor { ); } }; - let Some(environment) = selected_environment.as_ref() else { - // Legacy callers without `environmentId` remain local-only; if local is - // unavailable, do not fabricate an unbound skill path. - return ( - index, - codex_app_server_protocol::SkillsListEntry { - cwd, - skills: Vec::new(), - errors: Vec::new(), - }, - ); - }; - let path_ref = codex_core::skills::EnvironmentPathRef::new( - environment.get_filesystem(), - cwd_abs.clone(), - ); let effective_skill_roots = if workspace_codex_plugins_enabled { let plugins_input = config.plugins_config_input(); plugins_manager @@ -582,8 +570,25 @@ impl CatalogRequestProcessor { } else { Vec::new() }; + let Some(environment) = selected_environment.as_ref() else { + // Omitted `environmentId` uses the legacy implicit local target. When + // local exec is disabled there is no valid implicit target, so return an + // empty catalog rather than silently switching to the default environment. + return ( + index, + codex_app_server_protocol::SkillsListEntry { + cwd, + skills: Vec::new(), + errors: Vec::new(), + }, + ); + }; let skills_input = codex_core::skills::SkillsLoadInput::new( - path_ref, + codex_core::skills::EnvironmentPathRef::new( + environment.get_filesystem(), + cwd_abs.clone(), + ), + local_file_system, effective_skill_roots, config_layer_stack, config.bundled_skills_enabled(), diff --git a/codex-rs/app-server/src/skills_watcher.rs b/codex-rs/app-server/src/skills_watcher.rs index 7bd940963a..0c648cbc00 100644 --- a/codex-rs/app-server/src/skills_watcher.rs +++ b/codex-rs/app-server/src/skills_watcher.rs @@ -103,11 +103,16 @@ impl SkillsWatcher { let root_path_ref = EnvironmentPathRef::new(environment.get_filesystem(), config.cwd.clone()); + let local_file_system = thread_manager + .environment_manager() + .try_local_environment() + .map(|environment| environment.get_filesystem()); let plugins_input = config.plugins_config_input(); let plugins_manager = thread_manager.plugins_manager(); let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; let skills_input = SkillsLoadInput::new( root_path_ref, + local_file_system, plugin_outcome.effective_plugin_skill_roots(), config.config_layer_stack.clone(), config.bundled_skills_enabled(), diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index d5feedc3dd..895ca367eb 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -230,7 +230,8 @@ where } pub(crate) async fn skill_roots( - path_ref: &EnvironmentPathRef, + env_path: &EnvironmentPathRef, + local_file_system: Option<&Arc>, config_layer_stack: &ConfigLayerStack, plugin_skill_roots: Vec, extra_skill_roots: Vec, @@ -238,7 +239,8 @@ pub(crate) async fn skill_roots( let home_dir = home_dir().and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok()); skill_roots_with_home_dir( - path_ref, + env_path, + local_file_system, config_layer_stack, home_dir.as_ref(), plugin_skill_roots, @@ -248,35 +250,38 @@ pub(crate) async fn skill_roots( } async fn skill_roots_with_home_dir( - path_ref: &EnvironmentPathRef, + env_path: &EnvironmentPathRef, + local_file_system: Option<&Arc>, config_layer_stack: &ConfigLayerStack, home_dir: Option<&AbsolutePathBuf>, plugin_skill_roots: Vec, extra_skill_roots: Vec, ) -> Vec { - let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir, path_ref); - roots.extend(plugin_skill_roots.into_iter().map(|root| SkillRoot { - path: path_ref.with_path(root.path), - scope: SkillScope::User, - plugin_id: Some(root.plugin_id), - plugin_root: Some(path_ref.with_path(root.plugin_root)), - })); - roots.extend(extra_skill_roots.into_iter().map(|path| SkillRoot { - path: path_ref.with_path(path), - scope: SkillScope::User, - plugin_id: None, - plugin_root: None, - })); - roots.extend(repo_agents_skill_roots(path_ref, config_layer_stack).await); + let mut roots = repo_config_skill_roots(env_path, config_layer_stack); + roots.extend(local_skill_roots( + local_file_system, + config_layer_stack, + home_dir, + plugin_skill_roots, + extra_skill_roots, + )); + roots.extend(repo_agents_skill_roots(env_path, config_layer_stack).await); dedupe_skill_roots_by_path(&mut roots); roots } -fn skill_roots_from_layer_stack_inner( +fn local_skill_roots( + local_file_system: Option<&Arc>, config_layer_stack: &ConfigLayerStack, home_dir: Option<&AbsolutePathBuf>, - path_ref: &EnvironmentPathRef, + plugin_skill_roots: Vec, + extra_skill_roots: Vec, ) -> Vec { + let Some(local_file_system) = local_file_system else { + // Local exec can be disabled. In that case, keep repo/env skill loading intact but skip + // local absolute roots instead of falling back to the process-global local filesystem. + return Vec::new(); + }; let mut roots = Vec::new(); for layer in config_layer_stack.get_layers( @@ -288,19 +293,13 @@ fn skill_roots_from_layer_stack_inner( }; match &layer.name { - ConfigLayerSource::Project { .. } => { - roots.push(SkillRoot { - path: path_ref.with_path(config_folder.join(SKILLS_DIR_NAME)), - scope: SkillScope::Repo, - plugin_id: None, - plugin_root: None, - }); - } + ConfigLayerSource::Project { .. } => {} ConfigLayerSource::User { .. } => { // Deprecated user skills location (`$CODEX_HOME/skills`), kept for backward - // compatibility. + // compatibility. These are client-local absolute paths even when the active repo + // environment is remote, so bind them to the available local filesystem only. roots.push(SkillRoot { - path: path_ref.with_path(config_folder.join(SKILLS_DIR_NAME)), + path: local_path_ref(local_file_system, config_folder.join(SKILLS_DIR_NAME)), scope: SkillScope::User, plugin_id: None, plugin_root: None, @@ -309,8 +308,10 @@ fn skill_roots_from_layer_stack_inner( // `$HOME/.agents/skills` (user-installed skills). if let Some(home_dir) = home_dir { roots.push(SkillRoot { - path: path_ref - .with_path(home_dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME)), + path: local_path_ref( + local_file_system, + home_dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), + ), scope: SkillScope::User, plugin_id: None, plugin_root: None, @@ -320,7 +321,7 @@ fn skill_roots_from_layer_stack_inner( // Embedded system skills are cached under `$CODEX_HOME/skills/.system` and are a // special case (not a config layer). roots.push(SkillRoot { - path: path_ref.with_path(system_cache_root_dir(&config_folder)), + path: local_path_ref(local_file_system, system_cache_root_dir(&config_folder)), scope: SkillScope::System, plugin_id: None, plugin_root: None, @@ -330,7 +331,7 @@ fn skill_roots_from_layer_stack_inner( // The system config layer lives under `/etc/codex/` on Unix, so treat // `/etc/codex/skills` as admin-scoped skills. roots.push(SkillRoot { - path: path_ref.with_path(config_folder.join(SKILLS_DIR_NAME)), + path: local_path_ref(local_file_system, config_folder.join(SKILLS_DIR_NAME)), scope: SkillScope::Admin, plugin_id: None, plugin_root: None, @@ -343,19 +344,64 @@ fn skill_roots_from_layer_stack_inner( } } + roots.extend(plugin_skill_roots.into_iter().map(|root| SkillRoot { + // Plugin discovery is currently local-only; multi-env plugin skill loading should be an + // explicit future change rather than inheriting the selected repo environment here. + path: local_path_ref(local_file_system, root.path), + scope: SkillScope::User, + plugin_id: Some(root.plugin_id), + plugin_root: Some(local_path_ref(local_file_system, root.plugin_root)), + })); + roots.extend(extra_skill_roots.into_iter().map(|path| SkillRoot { + path: local_path_ref(local_file_system, path), + scope: SkillScope::User, + plugin_id: None, + plugin_root: None, + })); + roots } +fn repo_config_skill_roots( + env_path: &EnvironmentPathRef, + config_layer_stack: &ConfigLayerStack, +) -> Vec { + config_layer_stack + .get_layers( + ConfigLayerStackOrdering::HighestPrecedenceFirst, + /*include_disabled*/ true, + ) + .filter_map(|layer| match &layer.name { + ConfigLayerSource::Project { .. } => layer.config_folder().map(|config_folder| { + SkillRoot { + // Project and repo `.agents` roots belong to the selected environment because + // their absolute paths are only meaningful within that environment's cwd/repo. + path: env_path.with_path(config_folder.join(SKILLS_DIR_NAME)), + scope: SkillScope::Repo, + plugin_id: None, + plugin_root: None, + } + }), + ConfigLayerSource::User { .. } + | ConfigLayerSource::System { .. } + | ConfigLayerSource::Mdm { .. } + | ConfigLayerSource::SessionFlags + | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } + | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => None, + }) + .collect() +} + async fn repo_agents_skill_roots( - path_ref: &EnvironmentPathRef, + env_path: &EnvironmentPathRef, config_layer_stack: &ConfigLayerStack, ) -> Vec { let project_root_markers = project_root_markers_from_stack(config_layer_stack); - let project_root = find_project_root(path_ref, &project_root_markers).await; - let dirs = dirs_between_project_root_and_cwd(path_ref.path(), project_root.path()); + let project_root = find_project_root(env_path, &project_root_markers).await; + let dirs = dirs_between_project_root_and_cwd(env_path.path(), project_root.path()); let mut roots = Vec::new(); for dir in dirs { - let agents_skills = path_ref.with_path(dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME)); + let agents_skills = env_path.with_path(dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME)); match agents_skills.metadata(/*sandbox*/ None).await { Ok(metadata) if metadata.is_directory => roots.push(SkillRoot { path: agents_skills, @@ -376,6 +422,13 @@ async fn repo_agents_skill_roots( roots } +fn local_path_ref( + file_system: &Arc, + path: AbsolutePathBuf, +) -> EnvironmentPathRef { + EnvironmentPathRef::new(Arc::clone(file_system), path) +} + fn project_root_markers_from_stack(config_layer_stack: &ConfigLayerStack) -> Vec { let mut merged = TomlValue::Table(toml::map::Map::new()); for layer in config_layer_stack.get_layers( @@ -1061,8 +1114,16 @@ pub(crate) async fn skill_roots_from_layer_stack( home_dir: Option<&AbsolutePathBuf>, ) -> Vec { let path_ref = EnvironmentPathRef::new(fs, cwd.clone()); - skill_roots_with_home_dir(&path_ref, config_layer_stack, home_dir, Vec::new(), Vec::new()) - .await + let local_file_system = path_ref.file_system(); + skill_roots_with_home_dir( + &path_ref, + Some(&local_file_system), + config_layer_stack, + home_dir, + Vec::new(), + Vec::new(), + ) + .await } #[cfg(test)] diff --git a/codex-rs/core-skills/src/loader_tests.rs b/codex-rs/core-skills/src/loader_tests.rs index 92b7ec05bd..184ac4c370 100644 --- a/codex-rs/core-skills/src/loader_tests.rs +++ b/codex-rs/core-skills/src/loader_tests.rs @@ -4,7 +4,9 @@ use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerStack; use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; +use codex_exec_server::ExecutorFileSystem; use codex_exec_server::LOCAL_FS; +use codex_exec_server::LocalFileSystem; use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; @@ -291,6 +293,55 @@ async fn skill_roots_from_layer_stack_includes_disabled_project_layers() -> anyh Ok(()) } +#[tokio::test] +async fn skill_roots_bind_repo_and_local_roots_to_their_own_file_systems() -> anyhow::Result<()> { + let codex_home = tempfile::tempdir()?; + let project_root = codex_home.path().join("workspace"); + fs::create_dir_all(project_root.join(".git"))?; + fs::create_dir_all(project_root.join(REPO_ROOT_CONFIG_DIR_NAME))?; + let cfg = make_config_for_cwd(&codex_home, project_root.clone()).await; + + let env_file_system: Arc = Arc::new(LocalFileSystem::unsandboxed()); + let local_file_system: Arc = Arc::new(LocalFileSystem::unsandboxed()); + let env_path = EnvironmentPathRef::new(env_file_system.clone(), cfg.cwd.clone()); + let roots = super::skill_roots( + &env_path, + Some(&local_file_system), + &cfg.config_layer_stack, + Vec::new(), + ) + .await; + + let repo_root = roots + .iter() + .find(|root| root.scope == SkillScope::Repo) + .expect("repo root"); + let user_root = roots + .iter() + .find(|root| root.scope == SkillScope::User) + .expect("user root"); + assert!(Arc::ptr_eq(&repo_root.path.file_system(), &env_file_system)); + assert!(Arc::ptr_eq( + &user_root.path.file_system(), + &local_file_system + )); + + let roots_without_local = super::skill_roots( + &env_path, + /*local_file_system*/ None, + &cfg.config_layer_stack, + Vec::new(), + ) + .await; + assert!( + roots_without_local + .iter() + .all(|root| root.scope == SkillScope::Repo) + ); + + Ok(()) +} + #[tokio::test] async fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> { let tmp = tempfile::tempdir()?; @@ -1887,12 +1938,18 @@ async fn skill_roots_include_admin_with_lowest_priority() { let cfg = make_config(&codex_home).await; let cwd = local_env_path(cfg.cwd.clone()); - let scopes: Vec = - super::skill_roots(&cwd, &cfg.config_layer_stack, Vec::new(), Vec::new()) - .await - .into_iter() - .map(|root| root.scope) - .collect(); + let local_file_system = cwd.file_system(); + let scopes: Vec = super::skill_roots( + &cwd, + Some(&local_file_system), + &cfg.config_layer_stack, + Vec::new(), + Vec::new(), + ) + .await + .into_iter() + .map(|root| root.scope) + .collect(); let mut expected = vec![SkillScope::User, SkillScope::System]; if home_dir().is_some() { expected.insert(1, SkillScope::User); diff --git a/codex-rs/core-skills/src/manager.rs b/codex-rs/core-skills/src/manager.rs index 71739c65b8..a4f0eb0a43 100644 --- a/codex-rs/core-skills/src/manager.rs +++ b/codex-rs/core-skills/src/manager.rs @@ -23,10 +23,15 @@ use crate::system::install_system_skills; use crate::system::uninstall_system_skills; use codex_config::SkillsConfig; use codex_exec_server::EnvironmentPathRef; +use codex_exec_server::ExecutorFileSystem; #[derive(Debug, Clone)] pub struct SkillsLoadInput { - pub path_ref: EnvironmentPathRef, + pub env_path: EnvironmentPathRef, + /// Local absolute skill roots use this filesystem when local exec is available. + /// Repo-relative roots still use `env_path`, so remote turns do not accidentally + /// read local user/system/plugin skill paths from the remote filesystem. + pub local_file_system: Option>, pub effective_skill_roots: Vec, pub config_layer_stack: ConfigLayerStack, pub bundled_skills_enabled: bool, @@ -34,13 +39,15 @@ pub struct SkillsLoadInput { impl SkillsLoadInput { pub fn new( - path_ref: EnvironmentPathRef, + env_path: EnvironmentPathRef, + local_file_system: Option>, effective_skill_roots: Vec, config_layer_stack: ConfigLayerStack, bundled_skills_enabled: bool, ) -> Self { Self { - path_ref, + env_path, + local_file_system, effective_skill_roots, config_layer_stack, bundled_skills_enabled, @@ -52,7 +59,7 @@ pub struct SkillsManager { codex_home: AbsolutePathBuf, restriction_product: Option, extra_roots: RwLock>, - cache_by_path_ref: RwLock>, + cache_by_path_ref: RwLock>, cache_by_config: RwLock>, } @@ -119,7 +126,8 @@ impl SkillsManager { pub async fn skill_roots_for_config(&self, input: &SkillsLoadInput) -> Vec { let mut roots = skill_roots( - &input.path_ref, + &input.env_path, + input.local_file_system.as_ref(), &input.config_layer_stack, input.effective_skill_roots.clone(), self.extra_roots(), @@ -136,7 +144,13 @@ impl SkillsManager { input: &SkillsLoadInput, force_reload: bool, ) -> SkillLoadOutcome { - let path_ref_cache_key = input.path_ref.clone(); + let path_ref_cache_key = SkillsPathCacheKey { + env_path: input.env_path.clone(), + local_file_system: input + .local_file_system + .as_ref() + .map(ExecutorFileSystemRef::new), + }; if !force_reload && let Some(outcome) = self.cached_outcome_for_path_ref(&path_ref_cache_key) { @@ -144,7 +158,8 @@ impl SkillsManager { } let mut roots = skill_roots( - &input.path_ref, + &input.env_path, + input.local_file_system.as_ref(), &input.config_layer_stack, input.effective_skill_roots.clone(), self.extra_roots(), @@ -201,11 +216,11 @@ impl SkillsManager { fn cached_outcome_for_path_ref( &self, - path_ref: &EnvironmentPathRef, + cache_key: &SkillsPathCacheKey, ) -> Option { match self.cache_by_path_ref.read() { - Ok(cache) => cache.get(path_ref).cloned(), - Err(err) => err.into_inner().get(path_ref).cloned(), + Ok(cache) => cache.get(cache_key).cloned(), + Err(err) => err.into_inner().get(cache_key).cloned(), } } @@ -227,6 +242,41 @@ impl SkillsManager { } } +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct SkillsPathCacheKey { + env_path: EnvironmentPathRef, + local_file_system: Option, +} + +#[derive(Clone)] +struct ExecutorFileSystemRef(Arc); + +impl ExecutorFileSystemRef { + fn new(file_system: &Arc) -> Self { + Self(Arc::clone(file_system)) + } +} + +impl PartialEq for ExecutorFileSystemRef { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for ExecutorFileSystemRef {} + +impl std::hash::Hash for ExecutorFileSystemRef { + fn hash(&self, state: &mut H) { + std::hash::Hash::hash(&(Arc::as_ptr(&self.0) as *const () as usize), state); + } +} + +impl std::fmt::Debug for ExecutorFileSystemRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExecutorFileSystemRef").finish() + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct ConfigSkillsCacheKey { roots: Vec<(EnvironmentPathRef, u8, Option)>, diff --git a/codex-rs/core-skills/src/manager_tests.rs b/codex-rs/core-skills/src/manager_tests.rs index f2caded837..4314d5b43b 100644 --- a/codex-rs/core-skills/src/manager_tests.rs +++ b/codex-rs/core-skills/src/manager_tests.rs @@ -97,6 +97,7 @@ fn local_skills_input( let path_ref = skill_root_path_ref(cwd); SkillsLoadInput::new( path_ref, + Some(Arc::clone(&LOCAL_FS)), effective_skill_roots, config_layer_stack.clone(), bundled_skills_enabled_from_stack(&config_layer_stack), diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index 2f64124b83..2c9ec62d6f 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -425,7 +425,12 @@ enabled = false Arc::clone(&codex_exec_server::LOCAL_FS), config.cwd.clone(), ); - let skills_input = skills_load_input_from_config(&config, cwd, effective_skill_roots); + let skills_input = skills_load_input_from_config( + &config, + cwd, + Some(Arc::clone(&codex_exec_server::LOCAL_FS)), + effective_skill_roots, + ); let outcome = skills_manager.skills_for_config(&skills_input).await; let skill = outcome .skills diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 559d1f3d7c..0296f10a28 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -464,11 +464,18 @@ async fn warm_plugins_and_skills_for_session_init( let Some(path_ref) = path_ref else { return Vec::new(); }; + let local_file_system = environment_manager + .try_local_environment() + .map(|environment| environment.get_filesystem()); let plugins_input = config.plugins_config_input(); let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); - let skills_input = - skills_load_input_from_config(config.as_ref(), path_ref, effective_skill_roots); + let skills_input = skills_load_input_from_config( + config.as_ref(), + path_ref, + local_file_system, + effective_skill_roots, + ); skills_manager.skills_for_config(&skills_input).await.errors } diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index a8648fc2f6..718983c0c2 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -4133,6 +4133,11 @@ async fn new_default_turn_uses_config_aware_skills_for_role_overrides() { Arc::clone(&skill_fs), parent_config.cwd.clone(), ), + session + .services + .environment_manager + .try_local_environment() + .map(|environment| environment.get_filesystem()), Vec::new(), ), /*force_reload*/ true, @@ -4702,8 +4707,15 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { environment.get_filesystem(), per_turn_config.cwd.clone(), ); - let skills_input = - crate::skills_load_input_from_config(&per_turn_config, cwd, effective_skill_roots); + let skills_input = crate::skills_load_input_from_config( + &per_turn_config, + cwd, + services + .environment_manager + .try_local_environment() + .map(|environment| environment.get_filesystem()), + effective_skill_roots, + ); let skills_outcome = Arc::new( services .skills_manager @@ -6548,8 +6560,15 @@ where environment.get_filesystem(), per_turn_config.cwd.clone(), ); - let skills_input = - crate::skills_load_input_from_config(&per_turn_config, cwd, effective_skill_roots); + let skills_input = crate::skills_load_input_from_config( + &per_turn_config, + cwd, + services + .environment_manager + .try_local_environment() + .map(|environment| environment.get_filesystem()), + effective_skill_roots, + ); let skills_outcome = Arc::new( services .skills_manager diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index b07492ee21..b76723c34a 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -685,6 +685,11 @@ impl Session { ) }); let skills_outcome = if let Some(path_ref) = path_ref { + let local_file_system = self + .services + .environment_manager + .try_local_environment() + .map(|environment| environment.get_filesystem()); let plugins_input = per_turn_config.plugins_config_input(); let plugin_outcome = self .services @@ -692,8 +697,12 @@ impl Session { .plugins_for_config(&plugins_input) .await; let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots(); - let skills_input = - skills_load_input_from_config(&per_turn_config, path_ref, effective_skill_roots); + let skills_input = skills_load_input_from_config( + &per_turn_config, + path_ref, + local_file_system, + effective_skill_roots, + ); Arc::new( self.services .skills_manager diff --git a/codex-rs/core/src/skills.rs b/codex-rs/core/src/skills.rs index 6b3bbce309..03e9180b9a 100644 --- a/codex-rs/core/src/skills.rs +++ b/codex-rs/core/src/skills.rs @@ -5,9 +5,11 @@ use codex_analytics::InvocationType; use codex_analytics::SkillInvocation; use codex_analytics::build_track_events_context; pub use codex_exec_server::EnvironmentPathRef; +use codex_exec_server::ExecutorFileSystem; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_plugins::PluginSkillRoot; +use std::sync::Arc; pub use codex_core_skills::SkillError; pub use codex_core_skills::SkillLoadOutcome; @@ -36,11 +38,13 @@ pub use codex_core_skills::system; pub(crate) fn skills_load_input_from_config( config: &Config, - path_ref: EnvironmentPathRef, + env_path: EnvironmentPathRef, + local_file_system: Option>, effective_skill_roots: Vec, ) -> SkillsLoadInput { SkillsLoadInput::new( - path_ref, + env_path, + local_file_system, effective_skill_roots, config.config_layer_stack.clone(), config.bundled_skills_enabled(),