diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index ebafe351a..6dadec3b2 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -49,7 +49,6 @@ use codex_config::RemoteThreadConfigLoader; use codex_config::ThreadConfigLoader; use codex_core::config::Config; pub use codex_exec_server::EnvironmentManager; -pub use codex_exec_server::EnvironmentManagerArgs; pub use codex_exec_server::ExecServerRuntimePaths; use codex_feedback::CodexFeedback; use codex_protocol::protocol::SessionSource; diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 91c7e801a..fcb2335b3 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -8,7 +8,6 @@ use codex_config::RemoteThreadConfigLoader; use codex_config::ThreadConfigLoader; use codex_core::config::Config; use codex_core::resolve_installation_id; -use codex_exec_server::EnvironmentManagerArgs; use codex_features::Feature; use codex_login::AuthManager; use codex_utils_cli::CliConfigOverrides; @@ -420,15 +419,6 @@ pub async fn run_main_with_transport_options( auth: AppServerWebsocketAuthSettings, runtime_options: AppServerRuntimeOptions, ) -> IoResult<()> { - let environment_manager = Arc::new( - EnvironmentManager::new(EnvironmentManagerArgs::new( - ExecServerRuntimePaths::from_optional_paths( - arg0_paths.codex_self_exe.clone(), - arg0_paths.codex_linux_sandbox_exe.clone(), - )?, - )) - .await, - ); let (transport_event_tx, mut transport_event_rx) = mpsc::channel::(CHANNEL_CAPACITY); let (outgoing_tx, mut outgoing_rx) = mpsc::channel::(CHANNEL_CAPACITY); @@ -444,6 +434,17 @@ pub async fn run_main_with_transport_options( ) })?; let codex_home = find_codex_home()?; + let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths( + arg0_paths.codex_self_exe.clone(), + arg0_paths.codex_linux_sandbox_exe.clone(), + )?; + let environment_manager = if loader_overrides.ignore_user_config { + EnvironmentManager::from_env(local_runtime_paths).await + } else { + EnvironmentManager::from_codex_home(codex_home.clone(), local_runtime_paths).await + } + .map(Arc::new) + .map_err(std::io::Error::other)?; let config_manager = ConfigManager::new( codex_home.to_path_buf(), cli_kv_overrides.clone(), diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index f9bdc9b56..790079ec3 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -44,7 +44,6 @@ pub use codex_core::resolve_installation_id; pub use codex_core::skills::SkillsManager; pub use codex_core::thread_store_from_config; pub use codex_exec_server::EnvironmentManager; -pub use codex_exec_server::EnvironmentManagerArgs; pub use codex_exec_server::ExecServerRuntimePaths; pub use codex_features::Feature; pub use codex_features::Features; diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 4da588edb..718b2d402 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -15,7 +15,6 @@ pub use codex_app_server_protocol::AppMetadata; use codex_connectors::AllConnectorsCacheKey; use codex_connectors::DirectoryListResponse; use codex_exec_server::EnvironmentManager; -use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; use codex_protocol::models::PermissionProfile; use codex_tools::DiscoverableTool; @@ -202,7 +201,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( config.codex_linux_sandbox_exe.clone(), )?; let environment_manager = - EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await; + EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?; list_accessible_connectors_from_mcp_tools_with_environment_manager( config, force_refetch, diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index b4bd9cbe8..640d81324 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -15,12 +15,12 @@ pub(crate) fn default_thread_environment_selections( cwd: &AbsolutePathBuf, ) -> Vec { environment_manager - .default_environment_id() + .default_environment_ids() + .into_iter() .map(|environment_id| TurnEnvironmentSelection { - environment_id: environment_id.to_string(), + environment_id, cwd: cwd.clone(), }) - .into_iter() .collect() } @@ -111,10 +111,16 @@ mod tests { assert_eq!( default_thread_environment_selections(&manager, &cwd), - vec![TurnEnvironmentSelection { - environment_id: REMOTE_ENVIRONMENT_ID.to_string(), - cwd, - }] + vec![ + TurnEnvironmentSelection { + environment_id: REMOTE_ENVIRONMENT_ID.to_string(), + cwd: cwd.clone(), + }, + TurnEnvironmentSelection { + environment_id: "local".to_string(), + cwd, + }, + ] ); } diff --git a/codex-rs/core/src/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index 8717427af..0b9e334e0 100644 --- a/codex-rs/core/src/prompt_debug.rs +++ b/codex-rs/core/src/prompt_debug.rs @@ -2,9 +2,9 @@ use std::collections::HashSet; use std::sync::Arc; use codex_exec_server::EnvironmentManager; -use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; use codex_login::AuthManager; +use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; @@ -44,7 +44,11 @@ pub async fn build_prompt_input( &config, Arc::clone(&auth_manager), SessionSource::Exec, - Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await), + Arc::new( + EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths) + .await + .map_err(|err| CodexErr::Fatal(err.to_string()))?, + ), /*analytics_events_client*/ None, thread_store, state_db.clone(), diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 0834c18e2..683c8e6ab 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -21,6 +21,7 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::ThreadSource; use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::protocol::UserMessageEvent; +use codex_protocol::user_input::UserInput; use core_test_support::PathBufExt; use core_test_support::PathExt; use core_test_support::responses::mount_models_once; @@ -339,6 +340,99 @@ async fn start_thread_accepts_explicit_environment_when_default_environment_is_d assert_eq!(manager.list_thread_ids().await, vec![thread.thread_id]); } +#[tokio::test] +async fn start_thread_uses_all_default_environments_from_codex_home() { + let temp_dir = tempdir().expect("tempdir"); + let mut config = test_config().await; + config.codex_home = temp_dir.path().join("codex-home").abs(); + config.cwd = config.codex_home.abs(); + std::fs::create_dir_all(&config.codex_home).expect("create codex home"); + std::fs::write( + config.codex_home.join("environments.toml"), + r#" +default = "dev" + +[[environments]] +id = "dev" +program = "ssh" +args = ["dev", "cd /tmp && true"] +"#, + ) + .expect("write environments.toml"); + + let runtime_paths = codex_exec_server::ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe path"), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths"); + let environment_manager = Arc::new( + codex_exec_server::EnvironmentManager::from_codex_home( + config.codex_home.clone(), + runtime_paths, + ) + .await + .expect("environment manager"), + ); + assert_eq!( + environment_manager.default_environment_ids(), + vec!["dev".to_string(), "local".to_string()] + ); + + let manager = ThreadManager::with_models_provider_and_home_for_tests( + CodexAuth::from_api_key("dummy"), + config.model_provider.clone(), + config.codex_home.to_path_buf(), + environment_manager, + ); + + let thread = manager + .start_thread(config) + .await + .expect("thread should start"); + + let prompt_items = crate::prompt_debug::build_prompt_input_from_session( + thread.thread.codex.session.as_ref(), + Vec::::new(), + ) + .await + .expect("prompt input"); + let environment_context = prompt_items + .iter() + .filter_map(|item| match item { + ResponseItem::Message { content, .. } => Some(content), + _ => None, + }) + .flatten() + .find_map(|content| match content { + ContentItem::InputText { text } if text.contains("") => { + Some(text.as_str()) + } + _ => None, + }) + .expect("environment context prompt item"); + assert!(environment_context.contains("")); + let cwd = thread.session_configured.cwd.display().to_string(); + let dev_entry = format!( + r#" + {cwd} + "# + ); + let local_entry = format!( + r#" + {cwd} + "# + ); + let dev_position = environment_context + .find(&dev_entry) + .expect("dev environment entry"); + let local_position = environment_context + .find(&local_entry) + .expect("local environment entry"); + assert!(dev_position < local_position); + assert!(!environment_context.contains("\n ")); + assert!(!environment_context.contains("\n ")); +} + #[tokio::test] async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() { let temp_dir = tempdir().expect("tempdir"); diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index d13ba6d3b..be83393bb 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -114,6 +114,15 @@ impl EnvironmentManager { Self::from_provider(provider.as_ref(), local_runtime_paths).await } + /// Builds a manager from the legacy environment-variable provider without + /// reading user config files from `CODEX_HOME`. + pub async fn from_env( + local_runtime_paths: ExecServerRuntimePaths, + ) -> Result { + let provider = DefaultEnvironmentProvider::from_env(); + Self::from_provider(&provider, local_runtime_paths).await + } + async fn from_default_provider_url( exec_server_url: Option, local_runtime_paths: ExecServerRuntimePaths, @@ -147,18 +156,26 @@ impl EnvironmentManager { environments, default, } = snapshot; - for id in environments.keys() { + let mut environment_map = HashMap::with_capacity(environments.len()); + for (id, environment) in environments { if id.is_empty() { return Err(ExecServerError::Protocol( "environment id cannot be empty".to_string(), )); } + if environment_map + .insert(id.clone(), Arc::new(environment)) + .is_some() + { + return Err(ExecServerError::Protocol(format!( + "environment id `{id}` is duplicated" + ))); + } } - let default_environment = match default { EnvironmentDefault::Disabled => None, EnvironmentDefault::EnvironmentId(environment_id) => { - if !environments.contains_key(&environment_id) { + if !environment_map.contains_key(&environment_id) { return Err(ExecServerError::Protocol(format!( "default environment `{environment_id}` is not configured" ))); @@ -167,14 +184,10 @@ impl EnvironmentManager { } }; let local_environment = Arc::new(Environment::local(local_runtime_paths)); - let environments = environments - .into_iter() - .map(|(id, environment)| (id, Arc::new(environment))) - .collect(); Ok(Self { default_environment, - environments, + environments: environment_map, local_environment, }) } @@ -191,6 +204,22 @@ impl EnvironmentManager { self.default_environment.as_deref() } + /// Returns the ordered environment ids used for new thread startup. + pub fn default_environment_ids(&self) -> Vec { + let Some(default_environment_id) = self.default_environment.as_ref() else { + return Vec::new(); + }; + let mut environment_ids = Vec::with_capacity(self.environments.len()); + environment_ids.push(default_environment_id.clone()); + environment_ids.extend( + self.environments + .keys() + .filter(|environment_id| *environment_id != default_environment_id) + .cloned(), + ); + environment_ids + } + /// Returns the local environment instance used for internal runtime work. pub fn local_environment(&self) -> Arc { Arc::clone(&self.local_environment) @@ -350,7 +379,6 @@ impl Environment { #[cfg(test)] mod tests { - use std::collections::HashMap; use std::sync::Arc; use super::Environment; @@ -472,11 +500,11 @@ mod tests { async fn environment_manager_builds_from_provider() { let provider = TestEnvironmentProvider { snapshot: EnvironmentProviderSnapshot { - environments: HashMap::from([( + environments: vec![( REMOTE_ENVIRONMENT_ID.to_string(), Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) .expect("remote environment"), - )]), + )], default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()), }, }; @@ -502,7 +530,7 @@ mod tests { async fn environment_manager_rejects_empty_environment_id() { let provider = TestEnvironmentProvider { snapshot: EnvironmentProviderSnapshot { - environments: HashMap::from([("".to_string(), Environment::default_for_tests())]), + environments: vec![("".to_string(), Environment::default_for_tests())], default: EnvironmentDefault::Disabled, }, }; @@ -520,7 +548,7 @@ mod tests { async fn environment_manager_uses_explicit_provider_default() { let provider = TestEnvironmentProvider { snapshot: EnvironmentProviderSnapshot { - environments: HashMap::from([ + environments: vec![ ( LOCAL_ENVIRONMENT_ID.to_string(), Environment::default_for_tests(), @@ -530,7 +558,7 @@ mod tests { Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) .expect("remote environment"), ), - ]), + ], default: EnvironmentDefault::EnvironmentId("devbox".to_string()), }, }; @@ -539,6 +567,10 @@ mod tests { .expect("manager"); assert_eq!(manager.default_environment_id(), Some("devbox")); + assert_eq!( + manager.default_environment_ids(), + vec!["devbox".to_string(), LOCAL_ENVIRONMENT_ID.to_string()] + ); assert!(manager.default_environment().expect("default").is_remote()); } @@ -546,10 +578,10 @@ mod tests { async fn environment_manager_disables_provider_default() { let provider = TestEnvironmentProvider { snapshot: EnvironmentProviderSnapshot { - environments: HashMap::from([( + environments: vec![( LOCAL_ENVIRONMENT_ID.to_string(), Environment::default_for_tests(), - )]), + )], default: EnvironmentDefault::Disabled, }, }; @@ -566,10 +598,10 @@ mod tests { async fn environment_manager_rejects_unknown_provider_default() { let provider = TestEnvironmentProvider { snapshot: EnvironmentProviderSnapshot { - environments: HashMap::from([( + environments: vec![( LOCAL_ENVIRONMENT_ID.to_string(), Environment::default_for_tests(), - )]), + )], default: EnvironmentDefault::EnvironmentId("missing".to_string()), }, }; diff --git a/codex-rs/exec-server/src/environment_provider.rs b/codex-rs/exec-server/src/environment_provider.rs index 0e4bcc519..bced67db5 100644 --- a/codex-rs/exec-server/src/environment_provider.rs +++ b/codex-rs/exec-server/src/environment_provider.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use async_trait::async_trait; use crate::Environment; @@ -12,9 +10,9 @@ use crate::environment::REMOTE_ENVIRONMENT_ID; /// Lists the concrete environments available to Codex. /// /// Implementations own a startup snapshot containing both the available -/// environment list and default environment selection. Providers that want the -/// local environment to be addressable by id should include it explicitly in -/// the returned map. +/// environment list in configured order and the default environment +/// selection. Providers that want the local environment to be addressable by +/// id should include it explicitly in the returned list. #[async_trait] pub trait EnvironmentProvider: Send + Sync { /// Returns the provider-owned environment startup snapshot. @@ -26,7 +24,7 @@ pub trait EnvironmentProvider: Send + Sync { #[derive(Clone, Debug)] pub struct EnvironmentProviderSnapshot { - pub environments: HashMap, + pub environments: Vec<(String, Environment)>, pub default: EnvironmentDefault, } @@ -57,22 +55,25 @@ impl DefaultEnvironmentProvider { &self, local_runtime_paths: &ExecServerRuntimePaths, ) -> EnvironmentProviderSnapshot { - let mut environments = HashMap::from([( + let mut environments = vec![( LOCAL_ENVIRONMENT_ID.to_string(), Environment::local(local_runtime_paths.clone()), - )]); + )]; let (exec_server_url, disabled) = normalize_exec_server_url(self.exec_server_url.clone()); if let Some(exec_server_url) = exec_server_url { - environments.insert( + environments.push(( REMOTE_ENVIRONMENT_ID.to_string(), Environment::remote_inner(exec_server_url, Some(local_runtime_paths.clone())), - ); + )); } + let has_remote = environments + .iter() + .any(|(id, _environment)| id == REMOTE_ENVIRONMENT_ID); let default = if disabled { EnvironmentDefault::Disabled - } else if environments.contains_key(REMOTE_ENVIRONMENT_ID) { + } else if has_remote { EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()) } else { EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) @@ -105,6 +106,8 @@ pub(crate) fn normalize_exec_server_url(exec_server_url: Option) -> (Opt #[cfg(test)] mod tests { + use std::collections::HashMap; + use pretty_assertions::assert_eq; use super::*; @@ -126,7 +129,11 @@ mod tests { .snapshot(&runtime_paths) .await .expect("environments"); - let environments = snapshot.environments; + let EnvironmentProviderSnapshot { + environments, + default, + } = snapshot; + let environments: HashMap<_, _> = environments.into_iter().collect(); assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote()); assert_eq!( @@ -135,7 +142,7 @@ mod tests { ); assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID)); assert_eq!( - snapshot.default, + default, EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) ); } @@ -148,12 +155,16 @@ mod tests { .snapshot(&runtime_paths) .await .expect("environments"); - let environments = snapshot.environments; + let EnvironmentProviderSnapshot { + environments, + default, + } = snapshot; + let environments: HashMap<_, _> = environments.into_iter().collect(); assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote()); assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID)); assert_eq!( - snapshot.default, + default, EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) ); } @@ -166,11 +177,15 @@ mod tests { .snapshot(&runtime_paths) .await .expect("environments"); - let environments = snapshot.environments; + let EnvironmentProviderSnapshot { + environments, + default, + } = snapshot; + let environments: HashMap<_, _> = environments.into_iter().collect(); assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote()); assert!(!environments.contains_key(REMOTE_ENVIRONMENT_ID)); - assert_eq!(snapshot.default, EnvironmentDefault::Disabled); + assert_eq!(default, EnvironmentDefault::Disabled); } #[tokio::test] @@ -181,7 +196,11 @@ mod tests { .snapshot(&runtime_paths) .await .expect("environments"); - let environments = snapshot.environments; + let EnvironmentProviderSnapshot { + environments, + default, + } = snapshot; + let environments: HashMap<_, _> = environments.into_iter().collect(); assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote()); let remote_environment = &environments[REMOTE_ENVIRONMENT_ID]; @@ -191,7 +210,7 @@ mod tests { Some("ws://127.0.0.1:8765") ); assert_eq!( - snapshot.default, + default, EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()) ); } @@ -200,13 +219,14 @@ mod tests { async fn default_provider_normalizes_exec_server_url() { let provider = DefaultEnvironmentProvider::new(Some(" ws://127.0.0.1:8765 ".to_string())); let runtime_paths = test_runtime_paths(); - let environments = provider + let snapshot = provider .snapshot(&runtime_paths) .await .expect("environments"); + let environments: HashMap<_, _> = snapshot.environments.into_iter().collect(); assert_eq!( - environments.environments[REMOTE_ENVIRONMENT_ID].exec_server_url(), + environments[REMOTE_ENVIRONMENT_ID].exec_server_url(), Some("ws://127.0.0.1:8765") ); } diff --git a/codex-rs/exec-server/src/environment_toml.rs b/codex-rs/exec-server/src/environment_toml.rs index 99808d789..a1f328377 100644 --- a/codex-rs/exec-server/src/environment_toml.rs +++ b/codex-rs/exec-server/src/environment_toml.rs @@ -44,7 +44,7 @@ struct EnvironmentToml { #[derive(Clone, Debug, PartialEq, Eq)] struct TomlEnvironmentProvider { default: EnvironmentDefault, - environments: HashMap, + environments: Vec<(String, ExecServerTransportParams)>, } impl TomlEnvironmentProvider { @@ -58,7 +58,7 @@ impl TomlEnvironmentProvider { config_dir: Option<&Path>, ) -> Result { let mut ids = HashSet::from([LOCAL_ENVIRONMENT_ID.to_string()]); - let mut environments = HashMap::with_capacity(config.environments.len()); + let mut environments = Vec::with_capacity(config.environments.len()); for item in config.environments { let (id, transport) = parse_environment_toml(item, config_dir)?; if !ids.insert(id.clone()) { @@ -66,7 +66,7 @@ impl TomlEnvironmentProvider { "environment id `{id}` is duplicated" ))); } - environments.insert(id, transport); + environments.push((id, transport)); } let default = normalize_default_environment_id(config.default.as_deref(), &ids)?; Ok(Self { @@ -82,19 +82,19 @@ impl EnvironmentProvider for TomlEnvironmentProvider { &self, local_runtime_paths: &ExecServerRuntimePaths, ) -> Result { - let mut environments = HashMap::from([( + let mut environments = Vec::with_capacity(self.environments.len() + 1); + environments.push(( LOCAL_ENVIRONMENT_ID.to_string(), Environment::local(local_runtime_paths.clone()), - )]); - + )); for (id, transport_params) in &self.environments { - environments.insert( + environments.push(( id.clone(), Environment::remote_with_transport( transport_params.clone(), Some(local_runtime_paths.clone()), ), - ); + )); } Ok(EnvironmentProviderSnapshot { @@ -302,15 +302,6 @@ mod tests { #[tokio::test] async fn toml_provider_adds_implicit_local_and_configured_environments() { - let ssh_transport = ExecServerTransportParams::StdioCommand(StdioExecServerCommand { - program: "ssh".to_string(), - args: vec![ - "dev".to_string(), - "codex exec-server --listen stdio".to_string(), - ], - env: HashMap::from([("CODEX_LOG".to_string(), "debug".to_string())]), - cwd: None, - }); let provider = TomlEnvironmentProvider::new(EnvironmentsToml { default: Some("ssh-dev".to_string()), environments: vec![ @@ -345,13 +336,21 @@ mod tests { environments, default, } = snapshot; + let environment_ids: Vec<_> = environments + .iter() + .map(|(id, _environment)| id.as_str()) + .collect(); + assert_eq!( + environment_ids, + vec![LOCAL_ENVIRONMENT_ID, "devbox", "ssh-dev"] + ); + let environments: HashMap<_, _> = environments.into_iter().collect(); assert!(!environments[LOCAL_ENVIRONMENT_ID].is_remote()); assert_eq!( environments["devbox"].exec_server_url(), Some("ws://127.0.0.1:8765") ); - assert_eq!(provider.environments["ssh-dev"], ssh_transport); assert!(environments["ssh-dev"].is_remote()); assert_eq!(environments["ssh-dev"].exec_server_url(), None); assert_eq!( @@ -483,7 +482,7 @@ mod tests { .expect("provider"); assert_eq!( - provider.environments["ssh-dev"], + provider.environments[0].1, ExecServerTransportParams::StdioCommand(StdioExecServerCommand { program: "ssh".to_string(), args: Vec::new(), @@ -686,8 +685,13 @@ default = "none" .snapshot(&test_runtime_paths()) .await .expect("environments"); + let environment_ids: Vec<_> = snapshot + .environments + .into_iter() + .map(|(id, _environment)| id) + .collect(); - assert!(snapshot.environments.contains_key(LOCAL_ENVIRONMENT_ID)); + assert!(environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string())); assert_eq!(snapshot.default, EnvironmentDefault::Disabled); } @@ -702,7 +706,12 @@ default = "none" .snapshot(&test_runtime_paths()) .await .expect("environments"); + let environment_ids: Vec<_> = snapshot + .environments + .into_iter() + .map(|(id, _environment)| id) + .collect(); - assert!(snapshot.environments.contains_key(LOCAL_ENVIRONMENT_ID)); + assert!(environment_ids.contains(&LOCAL_ENVIRONMENT_ID.to_string())); } } diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 85de8258f..d8c147127 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -39,7 +39,6 @@ pub use codex_file_system::RemoveOptions; pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR; pub use environment::Environment; pub use environment::EnvironmentManager; -pub use environment::EnvironmentManagerArgs; pub use environment::LOCAL_ENVIRONMENT_ID; pub use environment::REMOTE_ENVIRONMENT_ID; pub use environment_provider::DefaultEnvironmentProvider; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index b035a1951..ef33c614f 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -15,7 +15,6 @@ pub use cli::Command; pub use cli::ReviewArgs; use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY; use codex_app_server_client::EnvironmentManager; -use codex_app_server_client::EnvironmentManagerArgs; use codex_app_server_client::ExecServerRuntimePaths; use codex_app_server_client::InProcessAppServerClient; use codex_app_server_client::InProcessClientStartArgs; @@ -509,6 +508,11 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result arg0_paths.codex_linux_sandbox_exe.clone(), )?; let state_db = codex_core::init_state_db(&config).await; + let environment_manager = if run_loader_overrides.ignore_user_config { + EnvironmentManager::from_env(local_runtime_paths).await? + } else { + EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await? + }; let in_process_start_args = InProcessClientStartArgs { arg0_paths, config: std::sync::Arc::new(config.clone()), @@ -518,9 +522,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result feedback: CodexFeedback::new(), log_db: None, state_db: state_db.clone(), - environment_manager: std::sync::Arc::new( - EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await, - ), + environment_manager: std::sync::Arc::new(environment_manager), config_warnings, session_source: SessionSource::Exec, enable_codex_api_key_env: true, diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index d86f67522..aa560bbe6 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -9,7 +9,6 @@ use codex_arg0::Arg0DispatchPaths; use codex_core::config::Config; use codex_core::resolve_installation_id; use codex_exec_server::EnvironmentManager; -use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; use codex_login::default_client::set_default_client_residency_requirement; use codex_utils_cli::CliConfigOverrides; @@ -61,15 +60,6 @@ pub async fn run_main( arg0_paths: Arg0DispatchPaths, cli_config_overrides: CliConfigOverrides, ) -> IoResult<()> { - let environment_manager = Arc::new( - EnvironmentManager::new(EnvironmentManagerArgs::new( - ExecServerRuntimePaths::from_optional_paths( - arg0_paths.codex_self_exe.clone(), - arg0_paths.codex_linux_sandbox_exe.clone(), - )?, - )) - .await, - ); // Parse CLI overrides once and derive the base Config eagerly so later // components do not need to work with raw TOML values. let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| { @@ -85,6 +75,17 @@ pub async fn run_main( })?; set_default_client_residency_requirement(config.enforce_residency.value()); let state_db = codex_core::init_state_db(&config).await; + let environment_manager = Arc::new( + EnvironmentManager::from_codex_home( + config.codex_home.clone(), + ExecServerRuntimePaths::from_optional_paths( + arg0_paths.codex_self_exe.clone(), + arg0_paths.codex_linux_sandbox_exe.clone(), + )?, + ) + .await + .map_err(std::io::Error::other)?, + ); let otel = codex_core::otel_init::build_provider( &config, diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 6817f677e..fa653ce45 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -20,7 +20,6 @@ use codex_core_api::Config; use codex_core_api::ConfigLayerStack; use codex_core_api::Constrained; use codex_core_api::EnvironmentManager; -use codex_core_api::EnvironmentManagerArgs; use codex_core_api::EventMsg; use codex_core_api::ExecServerRuntimePaths; use codex_core_api::Features; @@ -114,8 +113,9 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { config.codex_linux_sandbox_exe.clone(), )?; let thread_store = thread_store_from_config(&config, state_db.clone()); - let environment_manager = - Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new(local_runtime_paths)).await); + let environment_manager = Arc::new( + EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths).await?, + ); let installation_id = resolve_installation_id(&config.codex_home).await?; let thread_manager = ThreadManager::new( &config, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index d6d65b04a..dff288493 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -575,6 +575,7 @@ impl App { ) -> crate::chatwidget::ChatWidgetInit { crate::chatwidget::ChatWidgetInit { config: cfg, + environment_manager: self.environment_manager.clone(), frame_requester: tui.frame_requester(), app_event_tx: self.app_event_tx.clone(), workspace_command_runner: self.workspace_command_runner.clone(), @@ -739,6 +740,7 @@ impl App { .await; let init = crate::chatwidget::ChatWidgetInit { config: config.clone(), + environment_manager: environment_manager.clone(), frame_requester: tui.frame_requester(), app_event_tx: app_event_tx.clone(), workspace_command_runner: Some(workspace_command_runner.clone()), @@ -775,6 +777,7 @@ impl App { })?; let init = crate::chatwidget::ChatWidgetInit { config: config.clone(), + environment_manager: environment_manager.clone(), frame_requester: tui.frame_requester(), app_event_tx: app_event_tx.clone(), workspace_command_runner: Some(workspace_command_runner.clone()), @@ -816,6 +819,7 @@ impl App { })?; let init = crate::chatwidget::ChatWidgetInit { config: config.clone(), + environment_manager: environment_manager.clone(), frame_requester: tui.frame_requester(), app_event_tx: app_event_tx.clone(), workspace_command_runner: Some(workspace_command_runner.clone()), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 301808d15..b74bfab8f 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -435,6 +435,7 @@ async fn enqueue_primary_thread_session_replays_turns_before_initial_prompt_subm let model = crate::legacy_core::test_support::get_model_offline(config.model.as_deref()); app.chat_widget = ChatWidget::new_with_app_event(ChatWidgetInit { config, + environment_manager: app.environment_manager.clone(), frame_requester: crate::tui::FrameRequester::test_dummy(), app_event_tx: app.app_event_tx.clone(), workspace_command_runner: None, @@ -4834,6 +4835,7 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() { let replacement = ChatWidget::new_with_app_event(ChatWidgetInit { config: app.config.clone(), + environment_manager: app.environment_manager.clone(), frame_requester: crate::tui::FrameRequester::test_dummy(), app_event_tx: app.app_event_tx.clone(), workspace_command_runner: None, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 68e59d940..0e601e02c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -129,6 +129,7 @@ use codex_config::types::ApprovalsReviewer; use codex_config::types::Notifications; use codex_config::types::WindowsSandboxModeToml; use codex_core_skills::model::SkillMetadata; +use codex_exec_server::EnvironmentManager; use codex_features::FEATURES; use codex_features::Feature; #[cfg(test)] @@ -558,6 +559,7 @@ pub(crate) fn get_limits_duration(windows_minutes: i64) -> String { /// Common initialization parameters shared by all `ChatWidget` constructors. pub(crate) struct ChatWidgetInit { pub(crate) config: Config, + pub(crate) environment_manager: Arc, pub(crate) frame_requester: FrameRequester, pub(crate) app_event_tx: AppEventSender, /// App-server-backed runner used by status surfaces for workspace metadata probes. @@ -759,6 +761,7 @@ pub(crate) struct ChatWidget { /// where the overlay may briefly treat new tail content as already cached. active_cell_revision: u64, config: Config, + environment_manager: Arc, raw_output_mode: bool, /// Runtime value resolved by core. `config.service_tier` remains the explicit user choice. effective_service_tier: Option, @@ -4839,6 +4842,7 @@ impl ChatWidget { fn new_with_op_target(common: ChatWidgetInit, codex_op_target: CodexOpTarget) -> Self { let ChatWidgetInit { config, + environment_manager, frame_requester, app_event_tx, workspace_command_runner, @@ -4924,6 +4928,7 @@ impl ChatWidget { active_cell_revision: 0, raw_output_mode: config.tui_raw_output_mode, config, + environment_manager, effective_service_tier, skills_all: Vec::new(), skills_initial_state: None, @@ -7148,12 +7153,14 @@ impl ChatWidget { } let config = self.config.clone(); + let environment_manager = Arc::clone(&self.environment_manager); let app_event_tx = self.app_event_tx.clone(); tokio::spawn(async move { let accessible_result = - match connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status( + match connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( &config, force_refetch, + &environment_manager, ) .await { diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index e03eda919..05f967b5a 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -192,6 +192,7 @@ pub(super) async fn make_chatwidget_manual( raw_output_mode: cfg.tui_raw_output_mode, config: cfg, effective_service_tier, + environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), current_collaboration_mode, active_collaboration_mask, has_chatgpt_account: false, diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index a5dd3d0eb..bd8a1800f 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -1536,6 +1536,7 @@ async fn make_startup_chat_with_cli_overrides( let session_telemetry = test_session_telemetry(&cfg, resolved_model.as_str()); let init = ChatWidgetInit { config: cfg.clone(), + environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), frame_requester: FrameRequester::test_dummy(), app_event_tx: AppEventSender::new(unbounded_channel::().0), workspace_command_runner: None, diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index cd6fddf5e..bb2bc1a96 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -70,6 +70,7 @@ async fn experimental_mode_plan_is_ignored_on_startup() { let session_telemetry = test_session_telemetry(&cfg, resolved_model.as_str()); let init = ChatWidgetInit { config: cfg.clone(), + environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), frame_requester: FrameRequester::test_dummy(), app_event_tx: AppEventSender::new(unbounded_channel::().0), workspace_command_runner: None, diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 93e3222a5..df88bcec9 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -246,6 +246,7 @@ async fn helpers_are_available_and_do_not_panic() { let session_telemetry = test_session_telemetry(&cfg, resolved_model.as_str()); let init = ChatWidgetInit { config: cfg.clone(), + environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), frame_requester: FrameRequester::test_dummy(), app_event_tx: tx, workspace_command_runner: None, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5622c59f6..ac5b489af 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -8,7 +8,7 @@ use crate::legacy_core::config::Config; use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::ConfigOverrides; use crate::legacy_core::config::find_codex_home; -use crate::legacy_core::config::load_config_as_toml_with_cli_overrides; +use crate::legacy_core::config::load_config_as_toml_with_cli_and_loader_overrides; use crate::legacy_core::config::resolve_oss_provider; use crate::legacy_core::format_exec_policy_error_with_source; use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; @@ -40,7 +40,6 @@ use codex_config::ConfigLoadError; use codex_config::LoaderOverrides; use codex_config::format_config_error_with_source; use codex_exec_server::EnvironmentManager; -use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; use codex_login::AuthConfig; use codex_login::default_client::set_default_client_residency_requirement; @@ -661,10 +660,10 @@ fn config_cwd_for_app_server_target( app_server_target: &AppServerTarget, environment_manager: &EnvironmentManager, ) -> std::io::Result> { - if environment_manager - .default_environment() - .is_some_and(|environment| environment.is_remote()) - || matches!(app_server_target, AppServerTarget::Remote { .. }) + if matches!(app_server_target, AppServerTarget::Remote { .. }) + || environment_manager + .default_environment() + .is_some_and(|environment| environment.is_remote()) { return Ok(None); } @@ -678,6 +677,14 @@ fn config_cwd_for_app_server_target( Ok(Some(cwd)) } +fn should_load_configured_environments( + loader_overrides: &LoaderOverrides, + app_server_target: &AppServerTarget, +) -> bool { + !loader_overrides.ignore_user_config + && !matches!(app_server_target, AppServerTarget::Remote { .. }) +} + fn latest_session_cwd_filter<'a>( remote_mode: bool, remote_cwd_override: Option<&'a Path>, @@ -761,24 +768,28 @@ pub async fn run_main( } }; - let environment_manager = Arc::new( - EnvironmentManager::new(EnvironmentManagerArgs::new( - ExecServerRuntimePaths::from_optional_paths( - arg0_paths.codex_self_exe.clone(), - arg0_paths.codex_linux_sandbox_exe.clone(), - )?, - )) - .await, - ); + let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths( + arg0_paths.codex_self_exe.clone(), + arg0_paths.codex_linux_sandbox_exe.clone(), + )?; + let environment_manager = + if should_load_configured_environments(&loader_overrides, &app_server_target) { + EnvironmentManager::from_codex_home(codex_home.clone(), local_runtime_paths).await + } else { + EnvironmentManager::from_env(local_runtime_paths).await + } + .map(Arc::new) + .map_err(std::io::Error::other)?; let cwd = cli.cwd.clone(); let config_cwd = config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target, &environment_manager)?; #[allow(clippy::print_stderr)] - let config_toml = match load_config_as_toml_with_cli_overrides( + let config_toml = match load_config_as_toml_with_cli_and_loader_overrides( &codex_home, config_cwd.as_ref(), cli_kv_overrides.clone(), + loader_overrides.clone(), ) .await {