From 1a01c1b83ca6f689c552236e794217c62d58c116 Mon Sep 17 00:00:00 2001 From: starr-openai Date: Tue, 5 May 2026 13:58:49 -0700 Subject: [PATCH 01/13] Simplify environment provider defaults Co-authored-by: Codex --- codex-rs/app-server-client/src/lib.rs | 17 +++++++---------- codex-rs/app-server/src/lib.rs | 15 ++++++--------- codex-rs/core/src/connectors.rs | 2 +- codex-rs/core/src/environment_selection.rs | 3 +-- codex-rs/core/src/prompt_debug.rs | 4 +++- codex-rs/core/src/thread_manager_tests.rs | 11 ++++------- codex-rs/core/tests/common/test_codex.rs | 8 +++----- codex-rs/exec/src/lib.rs | 6 +++--- codex-rs/mcp-server/src/lib.rs | 15 ++++++--------- codex-rs/tui/src/lib.rs | 18 +++++++----------- 10 files changed, 41 insertions(+), 58 deletions(-) diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index ebafe351af2f..7f9fdfe43fd5 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -2092,17 +2092,14 @@ mod tests { #[tokio::test] async fn runtime_start_args_forward_environment_manager() { let config = Arc::new(build_test_config().await); - let environment_manager = Arc::new( - EnvironmentManager::create_for_tests( - Some("ws://127.0.0.1:8765".to_string()), - ExecServerRuntimePaths::new( - std::env::current_exe().expect("current exe"), - /*codex_linux_sandbox_exe*/ None, - ) - .expect("runtime paths"), + let environment_manager = Arc::new(EnvironmentManager::create_for_tests( + Some("ws://127.0.0.1:8765".to_string()), + ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe"), + /*codex_linux_sandbox_exe*/ None, ) - .await, - ); + .expect("runtime paths"), + )); let runtime_args = InProcessClientStartArgs { arg0_paths: Arg0DispatchPaths::default(), diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 08aab99f6549..1320f3f14ab5 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -419,15 +419,12 @@ 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 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(), + )?, + ))); let (transport_event_tx, mut transport_event_rx) = mpsc::channel::(CHANNEL_CAPACITY); let (outgoing_tx, mut outgoing_rx) = mpsc::channel::(CHANNEL_CAPACITY); diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 4da588edb6ab..bfa9850d5e22 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -202,7 +202,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::new(EnvironmentManagerArgs::new(local_runtime_paths)); 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 b4bd9cbe8974..f14361bdb54a 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -106,8 +106,7 @@ mod tests { let manager = EnvironmentManager::create_for_tests( Some("ws://127.0.0.1:8765".to_string()), test_runtime_paths(), - ) - .await; + ); assert_eq!( default_thread_environment_selections(&manager, &cwd), diff --git a/codex-rs/core/src/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index 8717427afeb5..a85f4a067a77 100644 --- a/codex-rs/core/src/prompt_debug.rs +++ b/codex-rs/core/src/prompt_debug.rs @@ -44,7 +44,9 @@ 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::new(EnvironmentManagerArgs::new( + local_runtime_paths, + ))), /*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 0834c18e21b9..51e1d726c994 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -304,13 +304,10 @@ async fn start_thread_accepts_explicit_environment_when_default_environment_is_d /*codex_linux_sandbox_exe*/ None, ) .expect("runtime paths"); - let environment_manager = Arc::new( - codex_exec_server::EnvironmentManager::create_for_tests( - Some("none".to_string()), - runtime_paths, - ) - .await, - ); + let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::create_for_tests( + Some("none".to_string()), + runtime_paths, + )); let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index c348d76481ca..2e92649b3c96 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -388,13 +388,11 @@ impl TestCodexBuilder { std::env::current_exe()?, /*codex_linux_sandbox_exe*/ None, )?; - let environment_manager = Arc::new( - codex_exec_server::EnvironmentManager::create_for_tests( + let environment_manager = + Arc::new(codex_exec_server::EnvironmentManager::create_for_tests( exec_server_url, local_runtime_paths, - ) - .await, - ); + )); let file_system = test_env.environment().get_filesystem(); let mut workspace_setups = vec![]; swap(&mut self.workspace_setups, &mut workspace_setups); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index b035a195172b..a8fba18f4a48 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -518,9 +518,9 @@ 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(EnvironmentManager::new( + EnvironmentManagerArgs::new(local_runtime_paths), + )), 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 d86f67522a95..076c698827a1 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -61,15 +61,12 @@ 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, - ); + 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(), + )?, + ))); // 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| { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5622c59f6549..b91c800b8f79 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -761,15 +761,12 @@ 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 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(), + )?, + ))); let cwd = cli.cwd.clone(); let config_cwd = config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target, &environment_manager)?; @@ -2139,8 +2136,7 @@ mod tests { std::env::current_exe().expect("current exe"), /*codex_linux_sandbox_exe*/ None, )?, - ) - .await; + ); let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?; From 7ae808f202ac47d36b1f2df888d7ec38157b93ea Mon Sep 17 00:00:00 2001 From: starr-openai Date: Fri, 1 May 2026 12:09:49 -0700 Subject: [PATCH 02/13] Load configured environments from CODEX_HOME Thread codex_home into EnvironmentManager construction so app entrypoints load environments.toml when present and continue falling back to the legacy CODEX_EXEC_SERVER_URL provider otherwise. Co-authored-by: Codex --- codex-rs/app-server-client/src/lib.rs | 1 - codex-rs/app-server/src/lib.rs | 18 +++++++----- codex-rs/core-api/src/lib.rs | 1 - codex-rs/core/src/connectors.rs | 3 +- codex-rs/core/src/prompt_debug.rs | 10 ++++--- codex-rs/exec-server/src/lib.rs | 1 - codex-rs/exec/src/lib.rs | 10 ++++--- codex-rs/mcp-server/src/lib.rs | 20 ++++++++------ codex-rs/tui/src/lib.rs | 40 ++++++++++++++++++--------- 9 files changed, 63 insertions(+), 41 deletions(-) diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 7f9fdfe43fd5..3a2eb466c185 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 1320f3f14ab5..ba70431820e1 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; @@ -419,12 +418,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(), - )?, - ))); let (transport_event_tx, mut transport_event_rx) = mpsc::channel::(CHANNEL_CAPACITY); let (outgoing_tx, mut outgoing_rx) = mpsc::channel::(CHANNEL_CAPACITY); @@ -440,6 +433,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 f9bdc9b56b4c..790079ec3094 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 bfa9850d5e22..718b2d402a6d 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)); + 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/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index a85f4a067a77..0b077213823e 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,9 +44,11 @@ pub async fn build_prompt_input( &config, Arc::clone(&auth_manager), SessionSource::Exec, - Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::new( - local_runtime_paths, - ))), + 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/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 85de8258f2dc..d8c147127c50 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 a8fba18f4a48..ef33c614f4e6 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), - )), + 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 076c698827a1..f6faf822fbe0 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,12 +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(), - )?, - ))); // 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| { @@ -79,9 +72,20 @@ pub async fn run_main( .await .map_err(|e| { std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}")) - })?; + })?; 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/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index b91c800b8f79..392ff57f0c2d 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,21 +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(), - )?, - ))); + 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 { From 2b97ca8f6358b1f84620d8bad3657ed317b4e281 Mon Sep 17 00:00:00 2001 From: starr-openai Date: Tue, 5 May 2026 15:39:05 -0700 Subject: [PATCH 03/13] Use active environment manager for TUI connectors Pass the app-owned EnvironmentManager into ChatWidget so connector prefetch uses the same environment selection that the session was initialized with, instead of reconstructing it from config. Co-authored-by: Codex --- codex-rs/tui/src/app.rs | 4 ++++ codex-rs/tui/src/app/tests.rs | 2 ++ codex-rs/tui/src/chatwidget.rs | 9 ++++++++- codex-rs/tui/src/chatwidget/tests/plan_mode.rs | 1 + codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs | 1 + codex-rs/tui/src/chatwidget/tests/status_and_layout.rs | 1 + 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index d6d65b04a4c0..dff288493cbc 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 eacb6d505379..5f6ef401e4db 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, @@ -4828,6 +4829,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 b65f8b1c4db9..9452360672cb 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, @@ -4842,6 +4845,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, @@ -4930,6 +4934,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, @@ -7151,12 +7156,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/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index a5dd3d0eb7f9..bd8a1800f1ef 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 cd6fddf5e8c6..bb2bc1a96727 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 89bb715be5b5..44fa38fefd8f 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, From 02af4b378f68f87b9fc2945884d9e1bbf118bcef Mon Sep 17 00:00:00 2001 From: starr-openai Date: Tue, 5 May 2026 15:53:36 -0700 Subject: [PATCH 04/13] Use test environment manager in chat widget helper Keep the direct ChatWidget test constructor aligned with the production initializer after wiring the active environment manager through the TUI. Co-authored-by: Codex --- codex-rs/tui/src/chatwidget/tests/helpers.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 14f856bee53e..08aacd18fdde 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -195,6 +195,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, From 6256029882f0df8c415f06c057ab7f1c56cdddea Mon Sep 17 00:00:00 2001 From: starr-openai Date: Wed, 6 May 2026 14:23:42 -0700 Subject: [PATCH 05/13] Fix prompt debug formatting Apply the rustfmt indentation reported by the PR20667 Format / etc CI job after the stack rebase. Co-authored-by: Codex --- codex-rs/core/src/prompt_debug.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index 0b077213823e..0b9e334e076d 100644 --- a/codex-rs/core/src/prompt_debug.rs +++ b/codex-rs/core/src/prompt_debug.rs @@ -47,7 +47,7 @@ pub async fn build_prompt_input( Arc::new( EnvironmentManager::from_codex_home(config.codex_home.clone(), local_runtime_paths) .await - .map_err(|err| CodexErr::Fatal(err.to_string()))?, + .map_err(|err| CodexErr::Fatal(err.to_string()))?, ), /*analytics_events_client*/ None, thread_store, From 7078cf614171e2a18b3713435e83e26392b29295 Mon Sep 17 00:00:00 2001 From: starr-openai Date: Wed, 6 May 2026 21:12:37 -0700 Subject: [PATCH 06/13] Attach configured environments on thread startup Preserve provider environment order and derive startup selections from the configured default plus remaining environments. This lets multi-environment CODEX_HOME setups attach every configured environment by default while keeping the default first as primary. Co-authored-by: Codex --- codex-rs/core/src/environment_selection.rs | 21 +++-- codex-rs/core/src/thread_manager_tests.rs | 92 +++++++++++++++++++ codex-rs/exec-server/src/environment.rs | 69 ++++++++++---- .../exec-server/src/environment_provider.rs | 62 ++++++++----- codex-rs/exec-server/src/environment_toml.rs | 36 +++++--- 5 files changed, 222 insertions(+), 58 deletions(-) diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index f14361bdb54a..50371e86a0a9 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() } @@ -85,6 +85,7 @@ pub(crate) fn resolve_environment_selections( #[cfg(test)] mod tests { use codex_exec_server::ExecServerRuntimePaths; + use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::REMOTE_ENVIRONMENT_ID; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_utils_absolute_path::AbsolutePathBuf; @@ -110,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_ENVIRONMENT_ID.to_string(), + cwd, + }, + ] ); } diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 51e1d726c994..67c0647bba12 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; @@ -336,6 +337,97 @@ 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 = "local" + +[[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!["local".to_string(), "dev".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, + ) + .await; + + let thread = manager + .start_thread(config) + .await + .expect("thread should start"); + + let selections = thread.thread.environment_selections().await; + assert_eq!( + selections, + vec![ + TurnEnvironmentSelection { + environment_id: "local".to_string(), + cwd: thread.session_configured.cwd.abs(), + }, + TurnEnvironmentSelection { + environment_id: "dev".to_string(), + cwd: thread.session_configured.cwd.abs(), + }, + ] + ); + + 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("")); + assert!(environment_context.contains("")); + assert!(environment_context.contains("")); +} + #[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 d13ba6d3bc90..3c0979455c46 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -40,6 +40,7 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL"; #[derive(Debug)] pub struct EnvironmentManager { default_environment: Option, + configured_environment_ids: Vec, environments: HashMap>, local_environment: Arc, } @@ -65,6 +66,7 @@ impl EnvironmentManager { pub fn default_for_tests() -> Self { Self { default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()), + configured_environment_ids: vec![LOCAL_ENVIRONMENT_ID.to_string()], environments: HashMap::from([( LOCAL_ENVIRONMENT_ID.to_string(), Arc::new(Environment::default_for_tests()), @@ -77,6 +79,7 @@ impl EnvironmentManager { pub fn disabled_for_tests(local_runtime_paths: ExecServerRuntimePaths) -> Self { Self { default_environment: None, + configured_environment_ids: Vec::new(), environments: HashMap::new(), local_environment: Arc::new(Environment::local(local_runtime_paths)), } @@ -147,18 +150,28 @@ impl EnvironmentManager { environments, default, } = snapshot; - for id in environments.keys() { + let mut configured_environment_ids = Vec::with_capacity(environments.len()); + 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" + ))); + } + configured_environment_ids.push(id); } - 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 +180,11 @@ 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, + configured_environment_ids, + environments: environment_map, local_environment, }) } @@ -191,6 +201,26 @@ 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_deref() else { + return Vec::new(); + }; + if self.configured_environment_ids.len() <= 1 { + return vec![default_environment_id.to_string()]; + } + + let mut environment_ids = Vec::with_capacity(self.configured_environment_ids.len()); + environment_ids.push(default_environment_id.to_string()); + environment_ids.extend( + self.configured_environment_ids + .iter() + .filter(|environment_id| environment_id.as_str() != 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 +380,6 @@ impl Environment { #[cfg(test)] mod tests { - use std::collections::HashMap; use std::sync::Arc; use super::Environment; @@ -472,11 +501,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 +531,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 +549,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 +559,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 +568,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 +579,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 +599,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 0e4bcc519162..bced67db55c5 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 99808d7896cc..5ad236c1a0ce 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 { @@ -345,13 +345,15 @@ mod tests { environments, default, } = snapshot; + 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_eq!(provider.environments[1].0, "ssh-dev"); + assert_eq!(provider.environments[1].1, ssh_transport); assert!(environments["ssh-dev"].is_remote()); assert_eq!(environments["ssh-dev"].exec_server_url(), None); assert_eq!( @@ -483,7 +485,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 +688,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 +709,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())); } } From b6f8f6e341b6ca8071816fce3165ba4ab44aa62c Mon Sep 17 00:00:00 2001 From: starr-openai Date: Thu, 7 May 2026 12:50:59 -0700 Subject: [PATCH 07/13] Strengthen multi-environment startup regression Exercise a non-local configured default so the test proves default-first ordering rather than relying on the implicit local environment already being first. Co-authored-by: Codex --- codex-rs/core/src/thread_manager_tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 67c0647bba12..3eb4c7d6de1c 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -347,7 +347,7 @@ async fn start_thread_uses_all_default_environments_from_codex_home() { std::fs::write( config.codex_home.join("environments.toml"), r#" -default = "local" +default = "dev" [[environments]] id = "dev" @@ -372,7 +372,7 @@ args = ["dev", "cd /tmp && true"] ); assert_eq!( environment_manager.default_environment_ids(), - vec!["local".to_string(), "dev".to_string()] + vec!["dev".to_string(), "local".to_string()] ); let manager = ThreadManager::with_models_provider_and_home_for_tests( @@ -393,11 +393,11 @@ args = ["dev", "cd /tmp && true"] selections, vec![ TurnEnvironmentSelection { - environment_id: "local".to_string(), + environment_id: "dev".to_string(), cwd: thread.session_configured.cwd.abs(), }, TurnEnvironmentSelection { - environment_id: "dev".to_string(), + environment_id: "local".to_string(), cwd: thread.session_configured.cwd.abs(), }, ] From 1f0a091a2db4129107c45c8f27752824a57da1cc Mon Sep 17 00:00:00 2001 From: starr-openai Date: Thu, 7 May 2026 15:41:12 -0700 Subject: [PATCH 08/13] Fix environment startup defaults and sample build Co-authored-by: Codex --- codex-rs/core/src/environment_selection.rs | 15 ++---- codex-rs/core/src/thread_manager_tests.rs | 22 ++++++++- codex-rs/exec-server/src/environment.rs | 46 +++++++++++-------- .../exec-server/src/environment_provider.rs | 10 ++++ codex-rs/exec-server/src/environment_toml.rs | 22 ++++----- codex-rs/thread-manager-sample/src/main.rs | 6 +-- 6 files changed, 74 insertions(+), 47 deletions(-) diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index 50371e86a0a9..2c8c5ffb95fc 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -85,7 +85,6 @@ pub(crate) fn resolve_environment_selections( #[cfg(test)] mod tests { use codex_exec_server::ExecServerRuntimePaths; - use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::REMOTE_ENVIRONMENT_ID; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_utils_absolute_path::AbsolutePathBuf; @@ -111,16 +110,10 @@ mod tests { assert_eq!( default_thread_environment_selections(&manager, &cwd), - vec![ - TurnEnvironmentSelection { - environment_id: REMOTE_ENVIRONMENT_ID.to_string(), - cwd: cwd.clone(), - }, - TurnEnvironmentSelection { - environment_id: LOCAL_ENVIRONMENT_ID.to_string(), - cwd, - }, - ] + vec![TurnEnvironmentSelection { + environment_id: REMOTE_ENVIRONMENT_ID.to_string(), + cwd, + }] ); } diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 3eb4c7d6de1c..b195be43d7a1 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -424,8 +424,26 @@ args = ["dev", "cd /tmp && true"] }) .expect("environment context prompt item"); assert!(environment_context.contains("")); - assert!(environment_context.contains("")); - 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] diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 3c0979455c46..74dc1dc80f4e 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -40,7 +40,7 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL"; #[derive(Debug)] pub struct EnvironmentManager { default_environment: Option, - configured_environment_ids: Vec, + startup_environment_ids: Vec, environments: HashMap>, local_environment: Arc, } @@ -66,7 +66,7 @@ impl EnvironmentManager { pub fn default_for_tests() -> Self { Self { default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()), - configured_environment_ids: vec![LOCAL_ENVIRONMENT_ID.to_string()], + startup_environment_ids: vec![LOCAL_ENVIRONMENT_ID.to_string()], environments: HashMap::from([( LOCAL_ENVIRONMENT_ID.to_string(), Arc::new(Environment::default_for_tests()), @@ -79,7 +79,7 @@ impl EnvironmentManager { pub fn disabled_for_tests(local_runtime_paths: ExecServerRuntimePaths) -> Self { Self { default_environment: None, - configured_environment_ids: Vec::new(), + startup_environment_ids: Vec::new(), environments: HashMap::new(), local_environment: Arc::new(Environment::local(local_runtime_paths)), } @@ -149,6 +149,7 @@ impl EnvironmentManager { let EnvironmentProviderSnapshot { environments, default, + include_all_environments_by_default, } = snapshot; let mut configured_environment_ids = Vec::with_capacity(environments.len()); let mut environment_map = HashMap::with_capacity(environments.len()); @@ -180,10 +181,25 @@ impl EnvironmentManager { } }; let local_environment = Arc::new(Environment::local(local_runtime_paths)); + let startup_environment_ids = match default_environment.as_ref() { + None => Vec::new(), + Some(default_environment_id) if include_all_environments_by_default => { + let mut environment_ids = Vec::with_capacity(configured_environment_ids.len()); + environment_ids.push(default_environment_id.clone()); + environment_ids.extend( + configured_environment_ids + .iter() + .filter(|environment_id| environment_id.as_str() != default_environment_id) + .cloned(), + ); + environment_ids + } + Some(default_environment_id) => vec![default_environment_id.clone()], + }; Ok(Self { default_environment, - configured_environment_ids, + startup_environment_ids, environments: environment_map, local_environment, }) @@ -203,22 +219,7 @@ impl EnvironmentManager { /// 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_deref() else { - return Vec::new(); - }; - if self.configured_environment_ids.len() <= 1 { - return vec![default_environment_id.to_string()]; - } - - let mut environment_ids = Vec::with_capacity(self.configured_environment_ids.len()); - environment_ids.push(default_environment_id.to_string()); - environment_ids.extend( - self.configured_environment_ids - .iter() - .filter(|environment_id| environment_id.as_str() != default_environment_id) - .cloned(), - ); - environment_ids + self.startup_environment_ids.clone() } /// Returns the local environment instance used for internal runtime work. @@ -507,6 +508,7 @@ mod tests { .expect("remote environment"), )], default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()), + include_all_environments_by_default: false, }, }; let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -533,6 +535,7 @@ mod tests { snapshot: EnvironmentProviderSnapshot { environments: vec![("".to_string(), Environment::default_for_tests())], default: EnvironmentDefault::Disabled, + include_all_environments_by_default: false, }, }; let err = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -561,6 +564,7 @@ mod tests { ), ], default: EnvironmentDefault::EnvironmentId("devbox".to_string()), + include_all_environments_by_default: true, }, }; let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -584,6 +588,7 @@ mod tests { Environment::default_for_tests(), )], default: EnvironmentDefault::Disabled, + include_all_environments_by_default: true, }, }; let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -604,6 +609,7 @@ mod tests { Environment::default_for_tests(), )], default: EnvironmentDefault::EnvironmentId("missing".to_string()), + include_all_environments_by_default: true, }, }; let err = EnvironmentManager::from_provider(&provider, test_runtime_paths()) diff --git a/codex-rs/exec-server/src/environment_provider.rs b/codex-rs/exec-server/src/environment_provider.rs index bced67db55c5..a337d7e1bcac 100644 --- a/codex-rs/exec-server/src/environment_provider.rs +++ b/codex-rs/exec-server/src/environment_provider.rs @@ -26,6 +26,7 @@ pub trait EnvironmentProvider: Send + Sync { pub struct EnvironmentProviderSnapshot { pub environments: Vec<(String, Environment)>, pub default: EnvironmentDefault, + pub include_all_environments_by_default: bool, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -82,6 +83,7 @@ impl DefaultEnvironmentProvider { EnvironmentProviderSnapshot { environments, default, + include_all_environments_by_default: false, } } } @@ -132,6 +134,7 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, + include_all_environments_by_default, } = snapshot; let environments: HashMap<_, _> = environments.into_iter().collect(); @@ -145,6 +148,7 @@ mod tests { default, EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) ); + assert!(!include_all_environments_by_default); } #[tokio::test] @@ -158,6 +162,7 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, + include_all_environments_by_default, } = snapshot; let environments: HashMap<_, _> = environments.into_iter().collect(); @@ -167,6 +172,7 @@ mod tests { default, EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) ); + assert!(!include_all_environments_by_default); } #[tokio::test] @@ -180,12 +186,14 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, + include_all_environments_by_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!(default, EnvironmentDefault::Disabled); + assert!(!include_all_environments_by_default); } #[tokio::test] @@ -199,6 +207,7 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, + include_all_environments_by_default, } = snapshot; let environments: HashMap<_, _> = environments.into_iter().collect(); @@ -213,6 +222,7 @@ mod tests { default, EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()) ); + assert!(!include_all_environments_by_default); } #[tokio::test] diff --git a/codex-rs/exec-server/src/environment_toml.rs b/codex-rs/exec-server/src/environment_toml.rs index 5ad236c1a0ce..bd45bcf83225 100644 --- a/codex-rs/exec-server/src/environment_toml.rs +++ b/codex-rs/exec-server/src/environment_toml.rs @@ -100,6 +100,7 @@ impl EnvironmentProvider for TomlEnvironmentProvider { Ok(EnvironmentProviderSnapshot { environments, default: self.default.clone(), + include_all_environments_by_default: true, }) } } @@ -302,15 +303,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![ @@ -344,7 +336,16 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, + include_all_environments_by_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()); @@ -352,14 +353,13 @@ mod tests { environments["devbox"].exec_server_url(), Some("ws://127.0.0.1:8765") ); - assert_eq!(provider.environments[1].0, "ssh-dev"); - assert_eq!(provider.environments[1].1, ssh_transport); assert!(environments["ssh-dev"].is_remote()); assert_eq!(environments["ssh-dev"].exec_server_url(), None); assert_eq!( default, EnvironmentDefault::EnvironmentId("ssh-dev".to_string()) ); + assert!(include_all_environments_by_default); } #[tokio::test] diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 6817f677e6b6..fa653ce45922 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, From b94a92aa38f52e9e0b1d661cd22fbf5c28aaccb4 Mon Sep 17 00:00:00 2001 From: starr-openai Date: Thu, 7 May 2026 16:22:37 -0700 Subject: [PATCH 09/13] codex: await test environment manager setup (#20667) Co-authored-by: Codex --- codex-rs/app-server-client/src/lib.rs | 17 ++++++++++------- codex-rs/core/src/environment_selection.rs | 3 ++- codex-rs/core/src/thread_manager_tests.rs | 11 +++++++---- codex-rs/core/tests/common/test_codex.rs | 8 +++++--- codex-rs/mcp-server/src/lib.rs | 2 +- codex-rs/tui/src/lib.rs | 3 ++- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 3a2eb466c185..6dadec3b2468 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -2091,14 +2091,17 @@ mod tests { #[tokio::test] async fn runtime_start_args_forward_environment_manager() { let config = Arc::new(build_test_config().await); - let environment_manager = Arc::new(EnvironmentManager::create_for_tests( - Some("ws://127.0.0.1:8765".to_string()), - ExecServerRuntimePaths::new( - std::env::current_exe().expect("current exe"), - /*codex_linux_sandbox_exe*/ None, + let environment_manager = Arc::new( + EnvironmentManager::create_for_tests( + Some("ws://127.0.0.1:8765".to_string()), + ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe"), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths"), ) - .expect("runtime paths"), - )); + .await, + ); let runtime_args = InProcessClientStartArgs { arg0_paths: Arg0DispatchPaths::default(), diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index 2c8c5ffb95fc..8f3ce7899542 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -106,7 +106,8 @@ mod tests { let manager = EnvironmentManager::create_for_tests( Some("ws://127.0.0.1:8765".to_string()), test_runtime_paths(), - ); + ) + .await; assert_eq!( default_thread_environment_selections(&manager, &cwd), diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index b195be43d7a1..2e121743f16d 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -305,10 +305,13 @@ async fn start_thread_accepts_explicit_environment_when_default_environment_is_d /*codex_linux_sandbox_exe*/ None, ) .expect("runtime paths"); - let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::create_for_tests( - Some("none".to_string()), - runtime_paths, - )); + let environment_manager = Arc::new( + codex_exec_server::EnvironmentManager::create_for_tests( + Some("none".to_string()), + runtime_paths, + ) + .await, + ); let manager = ThreadManager::with_models_provider_and_home_for_tests( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 2e92649b3c96..c348d76481ca 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -388,11 +388,13 @@ impl TestCodexBuilder { std::env::current_exe()?, /*codex_linux_sandbox_exe*/ None, )?; - let environment_manager = - Arc::new(codex_exec_server::EnvironmentManager::create_for_tests( + let environment_manager = Arc::new( + codex_exec_server::EnvironmentManager::create_for_tests( exec_server_url, local_runtime_paths, - )); + ) + .await, + ); let file_system = test_env.environment().get_filesystem(); let mut workspace_setups = vec![]; swap(&mut self.workspace_setups, &mut workspace_setups); diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index f6faf822fbe0..aa560bbe6a5c 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -72,7 +72,7 @@ pub async fn run_main( .await .map_err(|e| { std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}")) - })?; + })?; set_default_client_residency_requirement(config.enforce_residency.value()); let state_db = codex_core::init_state_db(&config).await; let environment_manager = Arc::new( diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 392ff57f0c2d..ac5b489af1f4 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -2150,7 +2150,8 @@ mod tests { std::env::current_exe().expect("current exe"), /*codex_linux_sandbox_exe*/ None, )?, - ); + ) + .await; let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?; From 301ccb8ec89c632cc1bc7e4442210bb0bdff8e7a Mon Sep 17 00:00:00 2001 From: starr-openai Date: Fri, 8 May 2026 10:11:38 -0700 Subject: [PATCH 10/13] codex: restore environment manager env constructor (#20667) Keep the app-server ignore-user-config path compiling after rebasing onto the merged TOML-provider stack. Co-authored-by: Codex --- codex-rs/exec-server/src/environment.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 74dc1dc80f4e..862027ea6293 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -117,6 +117,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, From 165e0e21331fc75a4476f71f312348b8b826443a Mon Sep 17 00:00:00 2001 From: starr-openai Date: Fri, 8 May 2026 10:19:09 -0700 Subject: [PATCH 11/13] codex: fix thread manager test after rebase (#20667) Drop the stale await from the configured-environments startup regression now that the test helper returns ThreadManager synchronously on current main. Co-authored-by: Codex --- codex-rs/core/src/thread_manager_tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 2e121743f16d..7692498442b2 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -383,8 +383,7 @@ args = ["dev", "cd /tmp && true"] config.model_provider.clone(), config.codex_home.to_path_buf(), environment_manager, - ) - .await; + ); let thread = manager .start_thread(config) From 9db4d813c2b799c43a9366dd458a848bf5f67e15 Mon Sep 17 00:00:00 2001 From: starr-openai Date: Fri, 8 May 2026 10:41:00 -0700 Subject: [PATCH 12/13] Simplify environment startup selection Derive initial thread environments from the default environment and the available environment map instead of storing a separate startup selection list. This also removes the provider snapshot boolean that distinguished legacy and TOML startup policies. Co-authored-by: Codex --- codex-rs/core/src/environment_selection.rs | 14 +++++-- codex-rs/exec-server/src/environment.rs | 40 ++++++------------- .../exec-server/src/environment_provider.rs | 10 ----- codex-rs/exec-server/src/environment_toml.rs | 3 -- 4 files changed, 22 insertions(+), 45 deletions(-) diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index 8f3ce7899542..640d813243eb 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -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/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 862027ea6293..be83393bb5dc 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -40,7 +40,6 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL"; #[derive(Debug)] pub struct EnvironmentManager { default_environment: Option, - startup_environment_ids: Vec, environments: HashMap>, local_environment: Arc, } @@ -66,7 +65,6 @@ impl EnvironmentManager { pub fn default_for_tests() -> Self { Self { default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()), - startup_environment_ids: vec![LOCAL_ENVIRONMENT_ID.to_string()], environments: HashMap::from([( LOCAL_ENVIRONMENT_ID.to_string(), Arc::new(Environment::default_for_tests()), @@ -79,7 +77,6 @@ impl EnvironmentManager { pub fn disabled_for_tests(local_runtime_paths: ExecServerRuntimePaths) -> Self { Self { default_environment: None, - startup_environment_ids: Vec::new(), environments: HashMap::new(), local_environment: Arc::new(Environment::local(local_runtime_paths)), } @@ -158,9 +155,7 @@ impl EnvironmentManager { let EnvironmentProviderSnapshot { environments, default, - include_all_environments_by_default, } = snapshot; - let mut configured_environment_ids = Vec::with_capacity(environments.len()); let mut environment_map = HashMap::with_capacity(environments.len()); for (id, environment) in environments { if id.is_empty() { @@ -176,7 +171,6 @@ impl EnvironmentManager { "environment id `{id}` is duplicated" ))); } - configured_environment_ids.push(id); } let default_environment = match default { EnvironmentDefault::Disabled => None, @@ -190,25 +184,9 @@ impl EnvironmentManager { } }; let local_environment = Arc::new(Environment::local(local_runtime_paths)); - let startup_environment_ids = match default_environment.as_ref() { - None => Vec::new(), - Some(default_environment_id) if include_all_environments_by_default => { - let mut environment_ids = Vec::with_capacity(configured_environment_ids.len()); - environment_ids.push(default_environment_id.clone()); - environment_ids.extend( - configured_environment_ids - .iter() - .filter(|environment_id| environment_id.as_str() != default_environment_id) - .cloned(), - ); - environment_ids - } - Some(default_environment_id) => vec![default_environment_id.clone()], - }; Ok(Self { default_environment, - startup_environment_ids, environments: environment_map, local_environment, }) @@ -228,7 +206,18 @@ impl EnvironmentManager { /// Returns the ordered environment ids used for new thread startup. pub fn default_environment_ids(&self) -> Vec { - self.startup_environment_ids.clone() + 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. @@ -517,7 +506,6 @@ mod tests { .expect("remote environment"), )], default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()), - include_all_environments_by_default: false, }, }; let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -544,7 +532,6 @@ mod tests { snapshot: EnvironmentProviderSnapshot { environments: vec![("".to_string(), Environment::default_for_tests())], default: EnvironmentDefault::Disabled, - include_all_environments_by_default: false, }, }; let err = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -573,7 +560,6 @@ mod tests { ), ], default: EnvironmentDefault::EnvironmentId("devbox".to_string()), - include_all_environments_by_default: true, }, }; let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -597,7 +583,6 @@ mod tests { Environment::default_for_tests(), )], default: EnvironmentDefault::Disabled, - include_all_environments_by_default: true, }, }; let manager = EnvironmentManager::from_provider(&provider, test_runtime_paths()) @@ -618,7 +603,6 @@ mod tests { Environment::default_for_tests(), )], default: EnvironmentDefault::EnvironmentId("missing".to_string()), - include_all_environments_by_default: true, }, }; let err = EnvironmentManager::from_provider(&provider, test_runtime_paths()) diff --git a/codex-rs/exec-server/src/environment_provider.rs b/codex-rs/exec-server/src/environment_provider.rs index a337d7e1bcac..bced67db55c5 100644 --- a/codex-rs/exec-server/src/environment_provider.rs +++ b/codex-rs/exec-server/src/environment_provider.rs @@ -26,7 +26,6 @@ pub trait EnvironmentProvider: Send + Sync { pub struct EnvironmentProviderSnapshot { pub environments: Vec<(String, Environment)>, pub default: EnvironmentDefault, - pub include_all_environments_by_default: bool, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -83,7 +82,6 @@ impl DefaultEnvironmentProvider { EnvironmentProviderSnapshot { environments, default, - include_all_environments_by_default: false, } } } @@ -134,7 +132,6 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, - include_all_environments_by_default, } = snapshot; let environments: HashMap<_, _> = environments.into_iter().collect(); @@ -148,7 +145,6 @@ mod tests { default, EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) ); - assert!(!include_all_environments_by_default); } #[tokio::test] @@ -162,7 +158,6 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, - include_all_environments_by_default, } = snapshot; let environments: HashMap<_, _> = environments.into_iter().collect(); @@ -172,7 +167,6 @@ mod tests { default, EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()) ); - assert!(!include_all_environments_by_default); } #[tokio::test] @@ -186,14 +180,12 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, - include_all_environments_by_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!(default, EnvironmentDefault::Disabled); - assert!(!include_all_environments_by_default); } #[tokio::test] @@ -207,7 +199,6 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, - include_all_environments_by_default, } = snapshot; let environments: HashMap<_, _> = environments.into_iter().collect(); @@ -222,7 +213,6 @@ mod tests { default, EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()) ); - assert!(!include_all_environments_by_default); } #[tokio::test] diff --git a/codex-rs/exec-server/src/environment_toml.rs b/codex-rs/exec-server/src/environment_toml.rs index bd45bcf83225..a1f328377a9f 100644 --- a/codex-rs/exec-server/src/environment_toml.rs +++ b/codex-rs/exec-server/src/environment_toml.rs @@ -100,7 +100,6 @@ impl EnvironmentProvider for TomlEnvironmentProvider { Ok(EnvironmentProviderSnapshot { environments, default: self.default.clone(), - include_all_environments_by_default: true, }) } } @@ -336,7 +335,6 @@ mod tests { let EnvironmentProviderSnapshot { environments, default, - include_all_environments_by_default, } = snapshot; let environment_ids: Vec<_> = environments .iter() @@ -359,7 +357,6 @@ mod tests { default, EnvironmentDefault::EnvironmentId("ssh-dev".to_string()) ); - assert!(include_all_environments_by_default); } #[tokio::test] From c7277710ef774bfe6b370ec7b57ade8ab01adf8a Mon Sep 17 00:00:00 2001 From: starr-openai Date: Fri, 8 May 2026 10:46:59 -0700 Subject: [PATCH 13/13] Fix environment startup test compile Remove a test-only call to a non-existent CodexThread helper. The test still verifies startup environment ordering through the manager defaults and generated environment context. Co-authored-by: Codex --- codex-rs/core/src/thread_manager_tests.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index 7692498442b2..683c8e6ab770 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -390,21 +390,6 @@ args = ["dev", "cd /tmp && true"] .await .expect("thread should start"); - let selections = thread.thread.environment_selections().await; - assert_eq!( - selections, - vec![ - TurnEnvironmentSelection { - environment_id: "dev".to_string(), - cwd: thread.session_configured.cwd.abs(), - }, - TurnEnvironmentSelection { - environment_id: "local".to_string(), - cwd: thread.session_configured.cwd.abs(), - }, - ] - ); - let prompt_items = crate::prompt_debug::build_prompt_input_from_session( thread.thread.codex.session.as_ref(), Vec::::new(),