diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index c6f678c2aa22..1c3de1208b15 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -46,6 +46,7 @@ use codex_core::config::Config; use codex_core::config_loader::CloudRequirementsLoader; use codex_core::config_loader::LoaderOverrides; 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; @@ -968,7 +969,7 @@ mod tests { cloud_requirements: CloudRequirementsLoader::default(), feedback: CodexFeedback::new(), log_db: None, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), config_warnings: Vec::new(), session_source, enable_codex_api_key_env: false, @@ -1969,9 +1970,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::new(Some( - "ws://127.0.0.1:8765".to_string(), - ))); + let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: Some("ws://127.0.0.1:8765".to_string()), + local_runtime_paths: ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe"), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths"), + })); let runtime_args = InProcessClientStartArgs { arg0_paths: Arg0DispatchPaths::default(), @@ -1998,7 +2004,13 @@ mod tests { &runtime_args.environment_manager, &environment_manager )); - assert!(runtime_args.environment_manager.is_remote()); + assert!( + runtime_args + .environment_manager + .default_environment() + .expect("default environment") + .is_remote() + ); } #[tokio::test] diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index a2ea77900aa9..56cd688d2a4e 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -3497,9 +3497,7 @@ mod tests { CodexAuth::create_dummy_chatgpt_auth_for_testing(), config.model_provider.clone(), config.codex_home.to_path_buf(), - Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ), ); let codex_core::NewThread { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 4669c0cb1a31..8f31288cd54d 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -260,6 +260,7 @@ use codex_core_plugins::loader::load_plugin_mcp_servers; use codex_core_plugins::manifest::PluginManifestInterface; use codex_core_plugins::marketplace::MarketplaceError; use codex_core_plugins::marketplace::MarketplacePluginSource; +use codex_exec_server::EnvironmentManager; use codex_exec_server::LOCAL_FS; use codex_features::FEATURES; use codex_features::Feature; @@ -5677,27 +5678,17 @@ impl CodexMessageProcessor { .to_mcp_config(self.thread_manager.plugins_manager().as_ref()) .await; let auth = self.auth_manager.auth().await; - let runtime_environment = match self.thread_manager.environment_manager().current().await { - Ok(Some(environment)) => { + let environment_manager = self.thread_manager.environment_manager(); + let runtime_environment = match environment_manager.default_environment() { + Some(environment) => { // Status listing has no turn cwd. This fallback is used only // by executor-backed stdio MCPs whose config omits `cwd`. McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()) } - Ok(None) => McpRuntimeEnvironment::new( - Arc::new(codex_exec_server::Environment::default()), + None => McpRuntimeEnvironment::new( + environment_manager.local_environment(), config.cwd.to_path_buf(), ), - Err(err) => { - // TODO(aibrahim): Investigate degrading MCP status listing when - // executor environment creation fails. - let error = JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to create environment: {err}"), - data: None, - }; - self.outgoing.send_error(request, error).await; - return; - } }; tokio::spawn(async move { @@ -5856,25 +5847,14 @@ impl CodexMessageProcessor { .to_mcp_config(self.thread_manager.plugins_manager().as_ref()) .await; let auth = self.auth_manager.auth().await; - let runtime_environment = match self.thread_manager.environment_manager().current().await { - Ok(Some(environment)) => { - // Resource reads without a thread have no turn cwd. This fallback - // is used only by executor-backed stdio MCPs whose config omits `cwd`. - McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()) - } - Ok(None) => McpRuntimeEnvironment::new( - Arc::new(codex_exec_server::Environment::default()), - config.cwd.to_path_buf(), - ), - Err(err) => { - let error = JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to create environment: {err}"), - data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; - } + let runtime_environment = { + let environment_manager = self.thread_manager.environment_manager(); + let environment = environment_manager + .default_environment() + .unwrap_or_else(|| environment_manager.local_environment()); + // Resource reads without a thread have no turn cwd. This fallback + // is used only by executor-backed stdio MCPs whose config omits `cwd`. + McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()) }; tokio::spawn(async move { @@ -6222,8 +6202,9 @@ impl CodexMessageProcessor { let request = request_id.clone(); let outgoing = Arc::clone(&self.outgoing); + let environment_manager = self.thread_manager.environment_manager(); tokio::spawn(async move { - Self::apps_list_task(outgoing, request, params, config).await; + Self::apps_list_task(outgoing, request, params, config, environment_manager).await; }); } @@ -6232,6 +6213,7 @@ impl CodexMessageProcessor { request_id: ConnectionRequestId, params: AppsListParams, config: Config, + environment_manager: Arc, ) { let AppsListParams { cursor, @@ -6266,12 +6248,15 @@ impl CodexMessageProcessor { let accessible_config = config.clone(); let accessible_tx = tx.clone(); tokio::spawn(async move { - let result = connectors::list_accessible_connectors_from_mcp_tools_with_options( - &accessible_config, - force_refetch, - ) - .await - .map_err(|err| format!("failed to load accessible apps: {err}")); + let result = + connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( + &accessible_config, + force_refetch, + &environment_manager, + ) + .await + .map(|status| status.connectors) + .map_err(|err| format!("failed to load accessible apps: {err}")); let _ = accessible_tx.send(AppListLoadResult::Accessible(result)); }); @@ -6461,23 +6446,11 @@ impl CodexMessageProcessor { }; let skills_manager = self.thread_manager.skills_manager(); let plugins_manager = self.thread_manager.plugins_manager(); - let fs = match self.thread_manager.environment_manager().current().await { - Ok(Some(environment)) => Some(environment.get_filesystem()), - Ok(None) => None, - Err(err) => { - self.outgoing - .send_error( - request_id, - JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to create environment: {err}"), - data: None, - }, - ) - .await; - return; - } - }; + let fs = self + .thread_manager + .environment_manager() + .default_environment() + .map(|environment| environment.get_filesystem()); let mut data = Vec::new(); for cwd in cwds { let extra_roots = extra_roots_by_cwd @@ -6793,8 +6766,13 @@ impl CodexMessageProcessor { return; } }; - let app_summaries = - plugin_app_helpers::load_plugin_app_summaries(&config, &outcome.plugin.apps).await; + let environment_manager = self.thread_manager.environment_manager(); + let app_summaries = plugin_app_helpers::load_plugin_app_summaries( + &config, + &outcome.plugin.apps, + &environment_manager, + ) + .await; let visible_skills = outcome .plugin .skills @@ -6971,10 +6949,11 @@ impl CodexMessageProcessor { ) { Vec::new() } else { + let environment_manager = self.thread_manager.environment_manager(); let (all_connectors_result, accessible_connectors_result) = tokio::join!( connectors::list_all_connectors_with_options(&config, /*force_refetch*/ true), - connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status( - &config, /*force_refetch*/ true + connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( + &config, /*force_refetch*/ true, &environment_manager ), ); diff --git a/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs b/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs index f2ba96d43acf..ad5875608b0f 100644 --- a/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs +++ b/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs @@ -5,11 +5,13 @@ use codex_app_server_protocol::AppSummary; use codex_chatgpt::connectors; use codex_core::config::Config; use codex_core::plugins::AppConnectorId; +use codex_exec_server::EnvironmentManager; use tracing::warn; pub(super) async fn load_plugin_app_summaries( config: &Config, plugin_apps: &[AppConnectorId], + environment_manager: &EnvironmentManager, ) -> Vec { if plugin_apps.is_empty() { return Vec::new(); @@ -29,8 +31,10 @@ pub(super) async fn load_plugin_app_summaries( let plugin_connectors = connectors::connectors_for_plugin_apps(connectors, plugin_apps); let accessible_connectors = - match connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status( - config, /*force_refetch*/ false, + match connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( + config, + /*force_refetch*/ false, + environment_manager, ) .await { diff --git a/codex-rs/app-server/src/fs_api.rs b/codex-rs/app-server/src/fs_api.rs index a2c71871db70..93b4f21c2b3b 100644 --- a/codex-rs/app-server/src/fs_api.rs +++ b/codex-rs/app-server/src/fs_api.rs @@ -20,7 +20,6 @@ use codex_app_server_protocol::FsWriteFileResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_exec_server::CopyOptions; use codex_exec_server::CreateDirectoryOptions; -use codex_exec_server::Environment; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::RemoveOptions; use std::io; @@ -31,15 +30,11 @@ pub(crate) struct FsApi { file_system: Arc, } -impl Default for FsApi { - fn default() -> Self { - Self { - file_system: Environment::default().get_filesystem(), - } +impl FsApi { + pub(crate) fn new(file_system: Arc) -> Self { + Self { file_system } } -} -impl FsApi { pub(crate) async fn read_file( &self, params: FsReadFileParams, diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 4fe88fa277ac..729f6d04af0b 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -750,7 +750,7 @@ mod tests { thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader), feedback: CodexFeedback::new(), log_db: None, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), config_warnings: Vec::new(), session_source, enable_codex_api_key_env: false, diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index ca80ad2c99dc..a2f35305ae75 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -6,6 +6,7 @@ use codex_config::ThreadConfigLoader; use codex_core::config::Config; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::config_loader::LoaderOverrides; +use codex_exec_server::EnvironmentManagerArgs; use codex_features::Feature; use codex_login::AuthManager; use codex_utils_cli::CliConfigOverrides; @@ -360,7 +361,7 @@ pub async fn run_main_with_transport( session_source: SessionSource, auth: AppServerWebsocketAuthSettings, ) -> IoResult<()> { - let environment_manager = Arc::new(EnvironmentManager::from_env_with_runtime_paths(Some( + let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env( ExecServerRuntimePaths::from_optional_paths( arg0_paths.codex_self_exe.clone(), arg0_paths.codex_linux_sandbox_exe.clone(), diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 2b7097c26238..57fa6e21e0c7 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -328,7 +328,12 @@ impl MessageProcessor { let device_key_api = DeviceKeyApi::default(); let external_agent_config_api = ExternalAgentConfigApi::new(config.codex_home.to_path_buf()); - let fs_api = FsApi::default(); + let fs_api = FsApi::new( + thread_manager + .environment_manager() + .local_environment() + .get_filesystem(), + ); let fs_watch_manager = FsWatchManager::new(outgoing.clone()); Self { @@ -1079,11 +1084,14 @@ impl MessageProcessor { } let outgoing = Arc::clone(&self.outgoing); + let environment_manager = self.thread_manager.environment_manager(); tokio::spawn(async move { let (all_connectors_result, accessible_connectors_result) = tokio::join!( connectors::list_all_connectors_with_options(&config, /*force_refetch*/ true), - connectors::list_accessible_connectors_from_mcp_tools_with_options( - &config, /*force_refetch*/ true, + connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager( + &config, + /*force_refetch*/ true, + &environment_manager, ), ); let all_connectors = match all_connectors_result { @@ -1096,7 +1104,7 @@ impl MessageProcessor { } }; let accessible_connectors = match accessible_connectors_result { - Ok(connectors) => connectors, + Ok(status) => status.connectors, Err(err) => { tracing::warn!( "failed to force-refresh accessible apps after experimental feature enablement: {err:#}" diff --git a/codex-rs/app-server/src/message_processor/tracing_tests.rs b/codex-rs/app-server/src/message_processor/tracing_tests.rs index bf5986f6de69..e42dd5afbc0a 100644 --- a/codex-rs/app-server/src/message_processor/tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor/tracing_tests.rs @@ -279,7 +279,7 @@ fn build_test_processor( arg0_paths: Arg0DispatchPaths::default(), config, config_manager, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), feedback: CodexFeedback::new(), log_db: None, config_warnings: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs index c26b456fa91c..a347d87fc763 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_resource.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_resource.rs @@ -204,7 +204,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> { thread_config_loader: Arc::new(codex_config::NoopThreadConfigLoader), feedback: CodexFeedback::new(), log_db: None, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), config_warnings: Vec::new(), session_source: SessionSource::Cli, enable_codex_api_key_env: false, diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 5f6efbc124c1..c054d1b8df82 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -13,6 +13,7 @@ use codex_connectors::merge::merge_connectors; use codex_connectors::merge::merge_plugin_connectors; use codex_core::config::Config; pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools; +pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager; pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options; pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status; pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools; diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 10850ef8c74e..1a5c38723f8b 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -95,9 +95,7 @@ impl AgentControlHarness { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let control = manager.agent_control(); Self { @@ -911,9 +909,7 @@ async fn spawn_agent_respects_max_threads_limit() { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let control = manager.agent_control(); @@ -965,9 +961,7 @@ async fn spawn_agent_releases_slot_after_shutdown() { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let control = manager.agent_control(); @@ -1010,9 +1004,7 @@ async fn spawn_agent_limit_shared_across_clones() { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let control = manager.agent_control(); let cloned = control.clone(); @@ -1057,9 +1049,7 @@ async fn resume_agent_respects_max_threads_limit() { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let control = manager.agent_control(); @@ -1115,9 +1105,7 @@ async fn resume_agent_releases_slot_after_resume_failure() { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let control = manager.agent_control(); @@ -1512,9 +1500,7 @@ async fn resume_thread_subagent_restores_stored_nickname_and_role() { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let control = manager.agent_control(); let harness = AgentControlHarness { diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 7247c601f46e..4f4ced4101f4 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use async_channel::Receiver; use async_channel::Sender; use codex_async_utils::OrCancelExt; -use codex_exec_server::EnvironmentManager; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -78,9 +77,7 @@ pub(crate) async fn run_codex_thread_interactive( config, auth_manager, models_manager, - environment_manager: Arc::new(EnvironmentManager::from_environment( - parent_ctx.environment.as_deref(), - )), + environment_manager: Arc::clone(&parent_session.services.environment_manager), skills_manager: Arc::clone(&parent_session.services.skills_manager), plugins_manager: Arc::clone(&parent_session.services.plugins_manager), mcp_manager: Arc::clone(&parent_session.services.mcp_manager), diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 933ce0ac764e..7641b4cb620e 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -13,7 +13,9 @@ pub use codex_app_server_protocol::AppInfo; pub use codex_app_server_protocol::AppMetadata; use codex_connectors::AllConnectorsCacheKey; use codex_connectors::DirectoryListResponse; -use codex_exec_server::Environment; +use codex_exec_server::EnvironmentManager; +use codex_exec_server::EnvironmentManagerArgs; +use codex_exec_server::ExecServerRuntimePaths; use codex_login::token_data::TokenData; use codex_protocol::protocol::SandboxPolicy; use codex_tools::DiscoverableTool; @@ -190,6 +192,28 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options( pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( config: &Config, force_refetch: bool, +) -> anyhow::Result { + // TODO: Wire callers that already own an EnvironmentManager into + // list_accessible_connectors_from_mcp_tools_with_environment_manager instead + // of constructing a temporary manager here. + let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths( + config.codex_self_exe.clone(), + config.codex_linux_sandbox_exe.clone(), + )?; + let environment_manager = + EnvironmentManager::new(EnvironmentManagerArgs::from_env(local_runtime_paths)); + list_accessible_connectors_from_mcp_tools_with_environment_manager( + config, + force_refetch, + &environment_manager, + ) + .await +} + +pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( + config: &Config, + force_refetch: bool, + environment_manager: &EnvironmentManager, ) -> anyhow::Result { let auth_manager = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false); @@ -235,6 +259,10 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( let (tx_event, rx_event) = unbounded(); drop(rx_event); + let environment = environment_manager + .default_environment() + .unwrap_or_else(|| environment_manager.local_environment()); + let (mcp_connection_manager, cancel_token) = McpConnectionManager::new( &mcp_servers, config.mcp_oauth_credentials_store_mode, @@ -243,7 +271,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( INITIAL_SUBMIT_ID.to_owned(), tx_event, SandboxPolicy::new_read_only_policy(), - McpRuntimeEnvironment::new(Arc::new(Environment::default()), config.cwd.to_path_buf()), + McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), ToolPluginProvenance::default(), diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index af698e1eaaa3..1b5614d31451 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -491,9 +491,7 @@ mod phase2 { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - std::sync::Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + std::sync::Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let (mut session, _turn_context) = make_session_and_context().await; session.services.state_db = Some(Arc::clone(&state_db)); diff --git a/codex-rs/core/src/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index 9717163df2db..1f62c2b08845 100644 --- a/codex-rs/core/src/prompt_debug.rs +++ b/codex-rs/core/src/prompt_debug.rs @@ -2,6 +2,8 @@ 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_features::Feature; use codex_login::AuthManager; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; @@ -29,6 +31,11 @@ pub async fn build_prompt_input( let auth_manager = AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false); + let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths( + config.codex_self_exe.clone(), + config.codex_linux_sandbox_exe.clone(), + )?; + let thread_manager = ThreadManager::new( &config, Arc::clone(&auth_manager), @@ -38,7 +45,9 @@ pub async fn build_prompt_input( .features .enabled(Feature::DefaultModeRequestUserInput), }, - Arc::new(EnvironmentManager::from_env()), + Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env( + local_runtime_paths, + ))), /*analytics_events_client*/ None, ); let thread = thread_manager.start_thread(config).await?; diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 958b98b2f168..921d528cd3ae 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -507,8 +507,8 @@ pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec, for let plugins_manager = &sess.services.plugins_manager; let fs = sess .services - .environment - .as_ref() + .environment_manager + .default_environment() .map(|environment| environment.get_filesystem()); let config = sess.get_config().await; let codex_home = sess.codex_home().await; diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 6fcc5336754a..2e4a3301eae7 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -237,7 +237,7 @@ impl Session { turn_context .environment .clone() - .unwrap_or_else(|| Arc::new(Environment::default())), + .unwrap_or_else(|| self.services.environment_manager.local_environment()), turn_context.cwd.to_path_buf(), ), config.codex_home.to_path_buf(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 80084f197930..1aa15a2cad8c 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -458,10 +458,7 @@ impl Codex { let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY); let (tx_event, rx_event) = async_channel::unbounded(); - let environment = environment_manager - .current() - .await - .map_err(|err| CodexErr::Fatal(format!("failed to create environment: {err}")))?; + let environment = environment_manager.default_environment(); let fs = environment .as_ref() .map(|environment| environment.get_filesystem()); @@ -648,7 +645,7 @@ impl Codex { mcp_manager.clone(), skills_watcher, agent_control, - environment, + environment_manager, analytics_events_client, ) .await diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 94de4617d5a4..af1028686d2f 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -46,7 +46,7 @@ pub(super) async fn spawn_review_thread( ) .with_web_search_config(/*web_search_config*/ None) .with_allow_login_shell(config.permissions.allow_login_shell) - .with_has_environment(parent_turn_context.environment.is_some()) + .with_has_environment(parent_turn_context.tools_config.has_environment) .with_spawn_agent_usage_hint(config.multi_agent_v2.usage_hint_enabled) .with_spawn_agent_usage_hint_text(config.multi_agent_v2.usage_hint_text.clone()) .with_hide_spawn_agent_metadata(config.multi_agent_v2.hide_spawn_agent_metadata) diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 640e59b5b712..b3c9b8d6a693 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -227,7 +227,7 @@ impl Session { mcp_manager: Arc, skills_watcher: Arc, agent_control: AgentControl, - environment: Option>, + environment_manager: Arc, analytics_events_client: Option, ) -> anyhow::Result> { debug!( @@ -676,7 +676,7 @@ impl Session { code_mode_service: crate::tools::code_mode::CodeModeService::new( config.js_repl_node_path.clone(), ), - environment: environment.clone(), + environment_manager, }; services .model_client @@ -770,9 +770,10 @@ impl Session { tx_event.clone(), session_configuration.sandbox_policy.get().clone(), McpRuntimeEnvironment::new( - environment - .clone() - .unwrap_or_else(|| Arc::new(Environment::default())), + sess.services + .environment_manager + .default_environment() + .unwrap_or_else(|| sess.services.environment_manager.local_environment()), session_configuration.cwd.to_path_buf(), ), config.codex_home.to_path_buf(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index ee72769bda6a..0b936195fd32 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2718,8 +2718,8 @@ async fn new_default_turn_uses_config_aware_skills_for_role_overrides() { let skill_fs = session .services - .environment - .as_ref() + .environment_manager + .default_environment() .map(|environment| environment.get_filesystem()) .unwrap_or_else(|| std::sync::Arc::clone(&codex_exec_server::LOCAL_FS)); let parent_outcome = session @@ -2945,11 +2945,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { mcp_manager, Arc::new(SkillsWatcher::noop()), AgentControl::default(), - Some(Arc::new( - codex_exec_server::Environment::create(/*exec_server_url*/ None) - .await - .expect("create environment"), - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, ) .await; @@ -3046,8 +3042,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { )); let network_approval = Arc::new(NetworkApprovalService::default()); let environment = Arc::new( - codex_exec_server::Environment::create(/*exec_server_url*/ None) - .await + codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None) .expect("create environment"), ); @@ -3107,7 +3102,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { code_mode_service: crate::tools::code_mode::CodeModeService::new( config.js_repl_node_path.clone(), ), - environment: Some(Arc::clone(&environment)), + environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), }; let js_repl = Arc::new(JsReplHandle::with_node_path( config.js_repl_node_path.clone(), @@ -3262,11 +3257,7 @@ async fn make_session_with_config_and_rx( mcp_manager, Arc::new(SkillsWatcher::noop()), AgentControl::default(), - Some(Arc::new( - codex_exec_server::Environment::create(/*exec_server_url*/ None) - .await - .expect("create environment"), - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, ) .await?; @@ -4137,8 +4128,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( )); let network_approval = Arc::new(NetworkApprovalService::default()); let environment = Arc::new( - codex_exec_server::Environment::create(/*exec_server_url*/ None) - .await + codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None) .expect("create environment"), ); @@ -4198,7 +4188,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( code_mode_service: crate::tools::code_mode::CodeModeService::new( config.js_repl_node_path.clone(), ), - environment: Some(Arc::clone(&environment)), + environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), }; let js_repl = Arc::new(JsReplHandle::with_node_path( config.js_repl_node_path.clone(), diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 73a0de285a0f..d655802ed174 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -635,7 +635,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { config, auth_manager, models_manager, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), skills_manager, plugins_manager, mcp_manager, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index dd86804ee5d6..ce6758c442ba 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -544,9 +544,8 @@ impl Session { .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots); - let fs = self - .services - .environment + let environment = self.services.environment_manager.default_environment(); + let fs = environment .as_ref() .map(|environment| environment.get_filesystem()); let skills_outcome = Arc::new( @@ -576,7 +575,7 @@ impl Session { ) .then(|| started_proxy.proxy()) }), - self.services.environment.clone(), + environment, sub_id, Arc::clone(&self.js_repl), skills_outcome, diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index aae02d61bdc6..3fbc0361e42f 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -16,7 +16,7 @@ use crate::tools::network_approval::NetworkApprovalService; use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; use codex_analytics::AnalyticsEventsClient; -use codex_exec_server::Environment; +use codex_exec_server::EnvironmentManager; use codex_hooks::Hooks; use codex_login::AuthManager; use codex_mcp::McpConnectionManager; @@ -62,5 +62,7 @@ pub(crate) struct SessionServices { /// Session-scoped model client shared across turns. pub(crate) model_client: ModelClient, pub(crate) code_mode_service: CodeModeService, - pub(crate) environment: Option>, + /// Shared process-level environment registry. Sessions carry an `Arc` handle so they can pass + /// the same manager through child-thread spawn paths without reconstructing it. + pub(crate) environment_manager: Arc, } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index e4da99bb55a0..066bfe316525 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -301,7 +301,7 @@ impl ThreadManager { auth, provider, codex_home.clone(), - Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + Arc::new(EnvironmentManager::default_for_tests()), ); manager._test_codex_home_guard = Some(TempCodexHomeGuard { path: codex_home }); manager @@ -920,11 +920,7 @@ impl ThreadManagerState { parent_trace: Option, user_shell_override: Option, ) -> CodexResult { - let environment = self - .environment_manager - .current() - .await - .map_err(|err| CodexErr::Fatal(format!("failed to create environment: {err}")))?; + let environment = self.environment_manager.default_environment(); let watch_registration = match environment.as_ref() { Some(environment) if !environment.is_remote() => { self.skills_watcher diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index fe2039e89bc4..4dcc29f562fd 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -246,9 +246,7 @@ async fn shutdown_all_threads_bounded_submits_shutdown_to_every_thread() { CodexAuth::from_api_key("dummy"), config.model_provider.clone(), config.codex_home.to_path_buf(), - Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), ); let thread_1 = manager .start_thread(config.clone()) @@ -297,9 +295,7 @@ async fn new_uses_configured_openai_provider_for_model_refresh() { auth_manager, SessionSource::Exec, CollaborationModesConfig::default(), - Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, ); @@ -434,9 +430,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor auth_manager.clone(), SessionSource::Exec, CollaborationModesConfig::default(), - Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, ); @@ -537,9 +531,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() { auth_manager.clone(), SessionSource::Exec, CollaborationModesConfig::default(), - Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, ); @@ -630,9 +622,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_ auth_manager.clone(), SessionSource::Exec, CollaborationModesConfig::default(), - Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, ); diff --git a/codex-rs/core/src/unified_exec/mod_tests.rs b/codex-rs/core/src/unified_exec/mod_tests.rs index 9877b2cb9fcd..1865188d3e7d 100644 --- a/codex-rs/core/src/unified_exec/mod_tests.rs +++ b/codex-rs/core/src/unified_exec/mod_tests.rs @@ -508,7 +508,7 @@ async fn completed_pipe_commands_preserve_exit_code() -> anyhow::Result<()> { shell_env(), ); - let environment = codex_exec_server::Environment::default(); + let environment = codex_exec_server::Environment::default_for_tests(); let process = UnifiedExecProcessManager::default() .open_session_with_exec_env( /*process_id*/ 1234, diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 5075f91620e5..73219423b5a5 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -76,7 +76,8 @@ impl TestEnv { pub async fn local() -> Result { let local_cwd_temp_dir = Arc::new(TempDir::new()?); let cwd = local_cwd_temp_dir.abs(); - let environment = codex_exec_server::Environment::create(/*exec_server_url*/ None).await?; + let environment = + codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None)?; Ok(Self { environment, cwd, @@ -115,7 +116,8 @@ pub async fn test_env() -> Result { match get_remote_test_env() { Some(remote_env) => { let websocket_url = remote_exec_server_url()?; - let environment = codex_exec_server::Environment::create(Some(websocket_url)).await?; + let environment = + codex_exec_server::Environment::create_for_tests(Some(websocket_url))?; let cwd = remote_aware_cwd_path(); environment .get_filesystem() @@ -204,6 +206,7 @@ pub struct TestCodexBuilder { workspace_setups: Vec>, home: Option>, user_shell_override: Option, + exec_server_url: Option, } impl TestCodexBuilder { @@ -255,6 +258,11 @@ impl TestCodexBuilder { self } + pub fn with_exec_server_url(mut self, exec_server_url: impl Into) -> Self { + self.exec_server_url = Some(exec_server_url.into()); + self + } + pub fn with_windows_cmd_shell(self) -> Self { if cfg!(windows) { self.with_user_shell(get_shell_by_model_provided_path(&PathBuf::from("cmd.exe"))) @@ -350,8 +358,18 @@ impl TestCodexBuilder { let (config, fallback_cwd) = self .prepare_config(base_url, &home, test_env.cwd().clone()) .await?; + let exec_server_url = self + .exec_server_url + .clone() + .or_else(|| test_env.exec_server_url().map(str::to_owned)); let environment_manager = Arc::new(codex_exec_server::EnvironmentManager::new( - test_env.exec_server_url().map(str::to_owned), + codex_exec_server::EnvironmentManagerArgs { + exec_server_url, + local_runtime_paths: codex_exec_server::ExecServerRuntimePaths::new( + std::env::current_exe()?, + /*codex_linux_sandbox_exe*/ None, + )?, + }, )); let file_system = test_env.environment().get_filesystem(); let mut workspace_setups = vec![]; @@ -885,6 +903,7 @@ pub fn test_codex() -> TestCodexBuilder { workspace_setups: vec![], home: None, user_shell_override: None, + exec_server_url: None, } } diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 2086367b21e7..2ebd49d53e11 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1103,9 +1103,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { .features .enabled(Feature::DefaultModeRequestUserInput), }, - Arc::new(codex_exec_server::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, ); let NewThread { thread: codex, .. } = thread_manager diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 1b2ac71643b2..59d28b61fc62 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -5,6 +5,7 @@ use anyhow::Result; use codex_core::ThreadManager; use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::EnvironmentManager; +use codex_exec_server::ExecServerRuntimePaths; use codex_exec_server::ExecutorFileSystem; use codex_login::CodexAuth; use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig; @@ -234,7 +235,15 @@ async fn list_skills_skips_cwd_roots_when_environment_disabled() -> Result<()> { codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("dummy")), SessionSource::Exec, CollaborationModesConfig::default(), - Arc::new(EnvironmentManager::new(Some("none".to_string()))), + Arc::new(EnvironmentManager::new( + codex_exec_server::EnvironmentManagerArgs { + exec_server_url: Some("none".to_string()), + local_runtime_paths: ExecServerRuntimePaths::new( + std::env::current_exe()?, + /*codex_linux_sandbox_exe*/ None, + )?, + }, + )), /*analytics_events_client*/ None, ); let new_thread = thread_manager.start_thread(config.clone()).await?; diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index 4e282c8fd3fb..375571b77930 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -10,6 +10,7 @@ use arc_swap::ArcSwap; use codex_app_server_protocol::JSONRPCNotification; use serde_json::Value; use tokio::sync::Mutex; +use tokio::sync::OnceCell; use tokio::sync::mpsc; use tokio::sync::watch; @@ -174,6 +175,37 @@ pub struct ExecServerClient { inner: Arc, } +#[derive(Clone)] +pub(crate) struct LazyRemoteExecServerClient { + websocket_url: String, + client: Arc>, +} + +impl LazyRemoteExecServerClient { + pub(crate) fn new(websocket_url: String) -> Self { + Self { + websocket_url, + client: Arc::new(OnceCell::new()), + } + } + + pub(crate) async fn get(&self) -> Result { + self.client + .get_or_try_init(|| async { + ExecServerClient::connect_websocket(RemoteExecServerConnectArgs { + websocket_url: self.websocket_url.clone(), + client_name: "codex-environment".to_string(), + connect_timeout: Duration::from_secs(5), + initialize_timeout: Duration::from_secs(5), + resume_session_id: None, + }) + .await + }) + .await + .cloned() + } +} + #[derive(Debug, thiserror::Error)] pub enum ExecServerError { #[error("failed to spawn exec-server: {0}")] diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index afe072019600..8e10c4a34ebb 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -1,11 +1,9 @@ +use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::OnceCell; - -use crate::ExecServerClient; use crate::ExecServerError; use crate::ExecServerRuntimePaths; -use crate::RemoteExecServerConnectArgs; +use crate::client::LazyRemoteExecServerClient; use crate::file_system::ExecutorFileSystem; use crate::local_file_system::LocalFileSystem; use crate::local_process::LocalProcess; @@ -15,130 +13,139 @@ use crate::remote_process::RemoteProcess; pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL"; -/// Lazily creates and caches the active environment for a session. +/// Owns the execution/filesystem environments available to the Codex runtime. +/// +/// `EnvironmentManager` is a shared registry for concrete environments. It +/// always creates a local environment under [`LOCAL_ENVIRONMENT_ID`]. When +/// `CODEX_EXEC_SERVER_URL` is set to a websocket URL, it also creates a remote +/// environment under [`REMOTE_ENVIRONMENT_ID`] and makes that the default +/// environment. Otherwise the local environment is the default. /// -/// The manager keeps the session's environment selection stable so subagents -/// and follow-up turns preserve an explicit disabled state. +/// Setting `CODEX_EXEC_SERVER_URL=none` disables environment access by leaving +/// the default environment unset while still keeping the local environment +/// available for internal callers by id. Callers use +/// `default_environment().is_some()` as the signal for model-facing +/// shell/filesystem tool availability. +/// +/// Remote environments create remote filesystem and execution backends that +/// lazy-connect to the configured exec-server on first use. The websocket is +/// not opened when the manager or environment is constructed. #[derive(Debug)] pub struct EnvironmentManager { - exec_server_url: Option, - local_runtime_paths: Option, - disabled: bool, - current_environment: OnceCell>>, + default_environment: Option, + environments: HashMap>, } -impl Default for EnvironmentManager { - fn default() -> Self { - Self::new(/*exec_server_url*/ None) - } +pub const LOCAL_ENVIRONMENT_ID: &str = "local"; +pub const REMOTE_ENVIRONMENT_ID: &str = "remote"; + +#[derive(Clone, Debug)] +pub struct EnvironmentManagerArgs { + pub exec_server_url: Option, + pub local_runtime_paths: ExecServerRuntimePaths, } -impl EnvironmentManager { - /// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value. - pub fn new(exec_server_url: Option) -> Self { - Self::new_with_runtime_paths(exec_server_url, /*local_runtime_paths*/ None) +impl EnvironmentManagerArgs { + pub fn new(local_runtime_paths: ExecServerRuntimePaths) -> Self { + Self { + exec_server_url: None, + local_runtime_paths, + } } - /// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value and local - /// runtime paths used when creating local filesystem helpers. - pub fn new_with_runtime_paths( - exec_server_url: Option, - local_runtime_paths: Option, - ) -> Self { - let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url); + pub fn from_env(local_runtime_paths: ExecServerRuntimePaths) -> Self { Self { - exec_server_url, + exec_server_url: std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok(), local_runtime_paths, - disabled, - current_environment: OnceCell::new(), } } +} - /// Builds a manager from process environment variables. - pub fn from_env() -> Self { - Self::from_env_with_runtime_paths(/*local_runtime_paths*/ None) +impl EnvironmentManager { + /// Builds a test-only manager without configured sandbox helper paths. + pub fn default_for_tests() -> Self { + Self { + default_environment: Some(LOCAL_ENVIRONMENT_ID.to_string()), + environments: HashMap::from([( + LOCAL_ENVIRONMENT_ID.to_string(), + Arc::new(Environment::default_for_tests()), + )]), + } } - /// Builds a manager from process environment variables and local runtime - /// paths used when creating local filesystem helpers. - pub fn from_env_with_runtime_paths( - local_runtime_paths: Option, - ) -> Self { - Self::new_with_runtime_paths( - std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok(), + /// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value and local + /// runtime paths used when creating local filesystem helpers. + pub fn new(args: EnvironmentManagerArgs) -> Self { + let EnvironmentManagerArgs { + exec_server_url, local_runtime_paths, - ) - } + } = args; + let (exec_server_url, environment_disabled) = normalize_exec_server_url(exec_server_url); + let mut environments = HashMap::from([( + LOCAL_ENVIRONMENT_ID.to_string(), + Arc::new(Environment::local(local_runtime_paths.clone())), + )]); + let default_environment = if environment_disabled { + None + } else { + match exec_server_url { + Some(exec_server_url) => { + environments.insert( + REMOTE_ENVIRONMENT_ID.to_string(), + Arc::new(Environment::remote(exec_server_url, local_runtime_paths)), + ); + Some(REMOTE_ENVIRONMENT_ID.to_string()) + } + None => Some(LOCAL_ENVIRONMENT_ID.to_string()), + } + }; - /// Builds a manager from the currently selected environment, or from the - /// disabled mode when no environment is available. - pub fn from_environment(environment: Option<&Environment>) -> Self { - match environment { - Some(environment) => Self { - exec_server_url: environment.exec_server_url().map(str::to_owned), - local_runtime_paths: environment.local_runtime_paths().cloned(), - disabled: false, - current_environment: OnceCell::new(), - }, - None => Self { - exec_server_url: None, - local_runtime_paths: None, - disabled: true, - current_environment: OnceCell::new(), - }, + Self { + default_environment, + environments, } } - /// Returns the remote exec-server URL when one is configured. - pub fn exec_server_url(&self) -> Option<&str> { - self.exec_server_url.as_deref() + /// Returns the default environment instance. + pub fn default_environment(&self) -> Option> { + self.default_environment + .as_deref() + .and_then(|environment_id| self.get_environment(environment_id)) } - /// Returns true when this manager is configured to use a remote exec server. - pub fn is_remote(&self) -> bool { - self.exec_server_url.is_some() + /// Returns the local environment instance used for internal runtime work. + pub fn local_environment(&self) -> Arc { + match self.get_environment(LOCAL_ENVIRONMENT_ID) { + Some(environment) => environment, + None => unreachable!("EnvironmentManager always has a local environment"), + } } - /// Returns the cached environment, creating it on first access. - pub async fn current(&self) -> Result>, ExecServerError> { - self.current_environment - .get_or_try_init(|| async { - if self.disabled { - Ok(None) - } else { - Ok(Some(Arc::new( - Environment::create_with_runtime_paths( - self.exec_server_url.clone(), - self.local_runtime_paths.clone(), - ) - .await?, - ))) - } - }) - .await - .map(Option::as_ref) - .map(std::option::Option::<&Arc>::cloned) + /// Returns a named environment instance. + pub fn get_environment(&self, environment_id: &str) -> Option> { + self.environments.get(environment_id).cloned() } } /// Concrete execution/filesystem environment selected for a session. /// -/// This bundles the selected backend together with the corresponding remote -/// client, if any. +/// This bundles the selected backend metadata together with the local runtime +/// paths used by filesystem helpers. #[derive(Clone)] pub struct Environment { exec_server_url: Option, - remote_exec_server_client: Option, exec_backend: Arc, + filesystem: Arc, local_runtime_paths: Option, } -impl Default for Environment { - fn default() -> Self { +impl Environment { + /// Builds a test-only local environment without configured sandbox helper paths. + pub fn default_for_tests() -> Self { Self { exec_server_url: None, - remote_exec_server_client: None, exec_backend: Arc::new(LocalProcess::default()), + filesystem: Arc::new(LocalFileSystem::unsandboxed()), local_runtime_paths: None, } } @@ -154,13 +161,21 @@ impl std::fmt::Debug for Environment { impl Environment { /// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value. - pub async fn create(exec_server_url: Option) -> Result { - Self::create_with_runtime_paths(exec_server_url, /*local_runtime_paths*/ None).await + pub fn create( + exec_server_url: Option, + local_runtime_paths: ExecServerRuntimePaths, + ) -> Result { + Self::create_inner(exec_server_url, Some(local_runtime_paths)) + } + + /// Builds a test-only environment without configured sandbox helper paths. + pub fn create_for_tests(exec_server_url: Option) -> Result { + Self::create_inner(exec_server_url, /*local_runtime_paths*/ None) } /// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value and /// local runtime paths used when creating local filesystem helpers. - pub async fn create_with_runtime_paths( + fn create_inner( exec_server_url: Option, local_runtime_paths: Option, ) -> Result { @@ -171,34 +186,44 @@ impl Environment { )); } - let remote_exec_server_client = if let Some(exec_server_url) = &exec_server_url { - Some( - ExecServerClient::connect_websocket(RemoteExecServerConnectArgs { - websocket_url: exec_server_url.clone(), - client_name: "codex-environment".to_string(), - connect_timeout: std::time::Duration::from_secs(5), - initialize_timeout: std::time::Duration::from_secs(5), - resume_session_id: None, - }) - .await?, - ) - } else { - None - }; + Ok(match exec_server_url { + Some(exec_server_url) => Self::remote_inner(exec_server_url, local_runtime_paths), + None => match local_runtime_paths { + Some(local_runtime_paths) => Self::local(local_runtime_paths), + None => Self::default_for_tests(), + }, + }) + } - let exec_backend: Arc = - if let Some(client) = remote_exec_server_client.clone() { - Arc::new(RemoteProcess::new(client)) - } else { - Arc::new(LocalProcess::default()) - }; + fn local(local_runtime_paths: ExecServerRuntimePaths) -> Self { + Self { + exec_server_url: None, + exec_backend: Arc::new(LocalProcess::default()), + filesystem: Arc::new(LocalFileSystem::with_runtime_paths( + local_runtime_paths.clone(), + )), + local_runtime_paths: Some(local_runtime_paths), + } + } - Ok(Self { - exec_server_url, - remote_exec_server_client, + fn remote(exec_server_url: String, local_runtime_paths: ExecServerRuntimePaths) -> Self { + Self::remote_inner(exec_server_url, Some(local_runtime_paths)) + } + + fn remote_inner( + exec_server_url: String, + local_runtime_paths: Option, + ) -> Self { + let client = LazyRemoteExecServerClient::new(exec_server_url.clone()); + let exec_backend: Arc = Arc::new(RemoteProcess::new(client.clone())); + let filesystem: Arc = Arc::new(RemoteFileSystem::new(client)); + + Self { + exec_server_url: Some(exec_server_url), exec_backend, + filesystem, local_runtime_paths, - }) + } } pub fn is_remote(&self) -> bool { @@ -219,13 +244,7 @@ impl Environment { } pub fn get_filesystem(&self) -> Arc { - match self.remote_exec_server_client.clone() { - Some(client) => Arc::new(RemoteFileSystem::new(client)), - None => match self.local_runtime_paths.clone() { - Some(runtime_paths) => Arc::new(LocalFileSystem::with_runtime_paths(runtime_paths)), - None => Arc::new(LocalFileSystem::unsandboxed()), - }, - } + Arc::clone(&self.filesystem) } } @@ -242,100 +261,162 @@ mod tests { use super::Environment; use super::EnvironmentManager; + use super::EnvironmentManagerArgs; + use super::LOCAL_ENVIRONMENT_ID; + use super::REMOTE_ENVIRONMENT_ID; use crate::ExecServerRuntimePaths; use crate::ProcessId; use pretty_assertions::assert_eq; + fn test_runtime_paths() -> ExecServerRuntimePaths { + ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe"), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths") + } + #[tokio::test] async fn create_local_environment_does_not_connect() { - let environment = Environment::create(/*exec_server_url*/ None) - .await + let environment = Environment::create(/*exec_server_url*/ None, test_runtime_paths()) .expect("create environment"); assert_eq!(environment.exec_server_url(), None); - assert!(environment.remote_exec_server_client.is_none()); + assert!(!environment.is_remote()); } - #[test] - fn environment_manager_normalizes_empty_url() { - let manager = EnvironmentManager::new(Some(String::new())); - - assert!(!manager.disabled); - assert_eq!(manager.exec_server_url(), None); - assert!(!manager.is_remote()); + #[tokio::test] + async fn environment_manager_normalizes_empty_url() { + let manager = EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: Some(String::new()), + local_runtime_paths: test_runtime_paths(), + }); + + let environment = manager.default_environment().expect("default environment"); + assert!(!environment.is_remote()); + assert!( + !manager + .get_environment(LOCAL_ENVIRONMENT_ID) + .expect("local environment") + .is_remote() + ); + assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none()); } - #[test] - fn environment_manager_treats_none_value_as_disabled() { - let manager = EnvironmentManager::new(Some("none".to_string())); + #[tokio::test] + async fn environment_manager_treats_none_value_as_disabled() { + let manager = EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: Some("none".to_string()), + local_runtime_paths: test_runtime_paths(), + }); - assert!(manager.disabled); - assert_eq!(manager.exec_server_url(), None); - assert!(!manager.is_remote()); + assert!(manager.default_environment().is_none()); + assert!( + !manager + .get_environment(LOCAL_ENVIRONMENT_ID) + .expect("local environment") + .is_remote() + ); + assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none()); } - #[test] - fn environment_manager_reports_remote_url() { - let manager = EnvironmentManager::new(Some("ws://127.0.0.1:8765".to_string())); - - assert!(manager.is_remote()); - assert_eq!(manager.exec_server_url(), Some("ws://127.0.0.1:8765")); + #[tokio::test] + async fn environment_manager_reports_remote_url() { + let manager = EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: Some("ws://127.0.0.1:8765".to_string()), + local_runtime_paths: test_runtime_paths(), + }); + + let environment = manager.default_environment().expect("default environment"); + assert!(environment.is_remote()); + assert_eq!(environment.exec_server_url(), Some("ws://127.0.0.1:8765")); + assert!(Arc::ptr_eq( + &environment, + &manager + .get_environment(REMOTE_ENVIRONMENT_ID) + .expect("remote environment") + )); + assert!( + !manager + .get_environment(LOCAL_ENVIRONMENT_ID) + .expect("local environment") + .is_remote() + ); } #[tokio::test] - async fn environment_manager_current_caches_environment() { - let manager = EnvironmentManager::new(/*exec_server_url*/ None); - - let first = manager.current().await.expect("get current environment"); - let second = manager.current().await.expect("get current environment"); + async fn environment_manager_default_environment_caches_environment() { + let manager = EnvironmentManager::default_for_tests(); - let first = first.expect("local environment"); - let second = second.expect("local environment"); + let first = manager.default_environment().expect("default environment"); + let second = manager.default_environment().expect("default environment"); assert!(Arc::ptr_eq(&first, &second)); + assert!(Arc::ptr_eq( + &first.get_filesystem(), + &second.get_filesystem() + )); } #[tokio::test] async fn environment_manager_carries_local_runtime_paths() { - let runtime_paths = ExecServerRuntimePaths::new( - std::env::current_exe().expect("current exe"), - /*codex_linux_sandbox_exe*/ None, - ) - .expect("runtime paths"); - let manager = EnvironmentManager::new_with_runtime_paths( - /*exec_server_url*/ None, - Some(runtime_paths.clone()), - ); + let runtime_paths = test_runtime_paths(); + let manager = EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: None, + local_runtime_paths: runtime_paths.clone(), + }); - let environment = manager - .current() - .await - .expect("get current environment") - .expect("local environment"); + let environment = manager.default_environment().expect("default environment"); assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths)); - assert_eq!( - EnvironmentManager::from_environment(Some(&environment)).local_runtime_paths, - Some(runtime_paths) - ); + let manager = EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: environment.exec_server_url().map(str::to_owned), + local_runtime_paths: environment + .local_runtime_paths() + .expect("local runtime paths") + .clone(), + }); + let environment = manager.default_environment().expect("default environment"); + assert_eq!(environment.local_runtime_paths(), Some(&runtime_paths)); + } + + #[tokio::test] + async fn disabled_environment_manager_has_no_default_environment() { + let manager = EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: Some("none".to_string()), + local_runtime_paths: test_runtime_paths(), + }); + + assert!(manager.default_environment().is_none()); } #[tokio::test] - async fn disabled_environment_manager_has_no_current_environment() { - let manager = EnvironmentManager::new(Some("none".to_string())); + async fn environment_manager_keeps_local_lookup_when_default_disabled() { + let manager = EnvironmentManager::new(EnvironmentManagerArgs { + exec_server_url: Some("none".to_string()), + local_runtime_paths: test_runtime_paths(), + }); + assert!(manager.default_environment().is_none()); assert!( - manager - .current() - .await - .expect("get current environment") - .is_none() + !manager + .get_environment(LOCAL_ENVIRONMENT_ID) + .expect("local environment") + .is_remote() ); + assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none()); + } + + #[tokio::test] + async fn get_environment_returns_none_for_unknown_id() { + let manager = EnvironmentManager::default_for_tests(); + + assert!(manager.get_environment("does-not-exist").is_none()); } #[tokio::test] async fn default_environment_has_ready_local_executor() { - let environment = Environment::default(); + let environment = Environment::default_for_tests(); let response = environment .get_exec_backend() @@ -354,4 +435,27 @@ mod tests { assert_eq!(response.process.process_id().as_str(), "default-env-proc"); } + + #[tokio::test] + async fn test_environment_rejects_sandboxed_filesystem_without_runtime_paths() { + let environment = Environment::default_for_tests(); + let path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path( + std::env::current_exe().expect("current exe").as_path(), + ) + .expect("absolute current exe"); + let sandbox = crate::FileSystemSandboxContext::new( + codex_protocol::protocol::SandboxPolicy::new_read_only_policy(), + ); + + let err = environment + .get_filesystem() + .read_file(&path, Some(&sandbox)) + .await + .expect_err("sandboxed read should require runtime paths"); + + assert_eq!( + err.to_string(), + "sandboxed filesystem operations require configured runtime paths" + ); + } } diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 067fa0a7c147..fc6a86f50836 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -25,6 +25,7 @@ pub use client_api::RemoteExecServerConnectArgs; pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR; pub use environment::Environment; pub use environment::EnvironmentManager; +pub use environment::EnvironmentManagerArgs; pub use file_system::CopyOptions; pub use file_system::CreateDirectoryOptions; pub use file_system::ExecutorFileSystem; diff --git a/codex-rs/exec-server/src/remote_file_system.rs b/codex-rs/exec-server/src/remote_file_system.rs index d6a32ba4d532..dc269505a1d4 100644 --- a/codex-rs/exec-server/src/remote_file_system.rs +++ b/codex-rs/exec-server/src/remote_file_system.rs @@ -7,7 +7,6 @@ use tracing::trace; use crate::CopyOptions; use crate::CreateDirectoryOptions; -use crate::ExecServerClient; use crate::ExecServerError; use crate::ExecutorFileSystem; use crate::FileMetadata; @@ -15,6 +14,7 @@ use crate::FileSystemResult; use crate::FileSystemSandboxContext; use crate::ReadDirectoryEntry; use crate::RemoveOptions; +use crate::client::LazyRemoteExecServerClient; use crate::protocol::FsCopyParams; use crate::protocol::FsCreateDirectoryParams; use crate::protocol::FsGetMetadataParams; @@ -28,11 +28,11 @@ const NOT_FOUND_ERROR_CODE: i64 = -32004; #[derive(Clone)] pub(crate) struct RemoteFileSystem { - client: ExecServerClient, + client: LazyRemoteExecServerClient, } impl RemoteFileSystem { - pub(crate) fn new(client: ExecServerClient) -> Self { + pub(crate) fn new(client: LazyRemoteExecServerClient) -> Self { trace!("remote fs new"); Self { client } } @@ -46,8 +46,8 @@ impl ExecutorFileSystem for RemoteFileSystem { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult> { trace!("remote fs read_file"); - let response = self - .client + let client = self.client.get().await.map_err(map_remote_error)?; + let response = client .fs_read_file(FsReadFileParams { path: path.clone(), sandbox: sandbox.cloned(), @@ -69,7 +69,8 @@ impl ExecutorFileSystem for RemoteFileSystem { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { trace!("remote fs write_file"); - self.client + let client = self.client.get().await.map_err(map_remote_error)?; + client .fs_write_file(FsWriteFileParams { path: path.clone(), data_base64: STANDARD.encode(contents), @@ -87,7 +88,8 @@ impl ExecutorFileSystem for RemoteFileSystem { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { trace!("remote fs create_directory"); - self.client + let client = self.client.get().await.map_err(map_remote_error)?; + client .fs_create_directory(FsCreateDirectoryParams { path: path.clone(), recursive: Some(options.recursive), @@ -104,8 +106,8 @@ impl ExecutorFileSystem for RemoteFileSystem { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult { trace!("remote fs get_metadata"); - let response = self - .client + let client = self.client.get().await.map_err(map_remote_error)?; + let response = client .fs_get_metadata(FsGetMetadataParams { path: path.clone(), sandbox: sandbox.cloned(), @@ -127,8 +129,8 @@ impl ExecutorFileSystem for RemoteFileSystem { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult> { trace!("remote fs read_directory"); - let response = self - .client + let client = self.client.get().await.map_err(map_remote_error)?; + let response = client .fs_read_directory(FsReadDirectoryParams { path: path.clone(), sandbox: sandbox.cloned(), @@ -153,7 +155,8 @@ impl ExecutorFileSystem for RemoteFileSystem { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { trace!("remote fs remove"); - self.client + let client = self.client.get().await.map_err(map_remote_error)?; + client .fs_remove(FsRemoveParams { path: path.clone(), recursive: Some(options.recursive), @@ -173,7 +176,8 @@ impl ExecutorFileSystem for RemoteFileSystem { sandbox: Option<&FileSystemSandboxContext>, ) -> FileSystemResult<()> { trace!("remote fs copy"); - self.client + let client = self.client.get().await.map_err(map_remote_error)?; + client .fs_copy(FsCopyParams { source_path: source_path.clone(), destination_path: destination_path.clone(), diff --git a/codex-rs/exec-server/src/remote_process.rs b/codex-rs/exec-server/src/remote_process.rs index 86786a54f743..d8d06735cdb9 100644 --- a/codex-rs/exec-server/src/remote_process.rs +++ b/codex-rs/exec-server/src/remote_process.rs @@ -9,7 +9,7 @@ use crate::ExecProcess; use crate::ExecProcessEventReceiver; use crate::ExecServerError; use crate::StartedExecProcess; -use crate::client::ExecServerClient; +use crate::client::LazyRemoteExecServerClient; use crate::client::Session; use crate::protocol::ExecParams; use crate::protocol::ReadResponse; @@ -17,7 +17,7 @@ use crate::protocol::WriteResponse; #[derive(Clone)] pub(crate) struct RemoteProcess { - client: ExecServerClient, + client: LazyRemoteExecServerClient, } struct RemoteExecProcess { @@ -25,7 +25,7 @@ struct RemoteExecProcess { } impl RemoteProcess { - pub(crate) fn new(client: ExecServerClient) -> Self { + pub(crate) fn new(client: LazyRemoteExecServerClient) -> Self { trace!("remote process new"); Self { client } } @@ -35,8 +35,9 @@ impl RemoteProcess { impl ExecBackend for RemoteProcess { async fn start(&self, params: ExecParams) -> Result { let process_id = params.process_id.clone(); - let session = self.client.register_session(&process_id).await?; - if let Err(err) = self.client.exec(params).await { + let client = self.client.get().await?; + let session = client.register_session(&process_id).await?; + if let Err(err) = client.exec(params).await { session.unregister().await; return Err(err); } diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index d449315c8d6e..9972cc004a78 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -49,13 +49,13 @@ enum ProcessEventSnapshot { async fn create_process_context(use_remote: bool) -> Result { if use_remote { let server = exec_server().await?; - let environment = Environment::create(Some(server.websocket_url().to_string())).await?; + let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?; Ok(ProcessContext { backend: environment.get_exec_backend(), server: Some(server), }) } else { - let environment = Environment::create(/*exec_server_url*/ None).await?; + let environment = Environment::create_for_tests(/*exec_server_url*/ None)?; Ok(ProcessContext { backend: environment.get_exec_backend(), server: None, diff --git a/codex-rs/exec-server/tests/file_system.rs b/codex-rs/exec-server/tests/file_system.rs index d4f94c7e44c1..f137be969580 100644 --- a/codex-rs/exec-server/tests/file_system.rs +++ b/codex-rs/exec-server/tests/file_system.rs @@ -46,7 +46,7 @@ struct FileSystemContext { async fn create_file_system_context(use_remote: bool) -> Result { if use_remote { let server = exec_server().await?; - let environment = Environment::create(Some(server.websocket_url().to_string())).await?; + let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?; Ok(FileSystemContext { file_system: environment.get_filesystem(), _helper_paths: None, @@ -214,7 +214,7 @@ async fn sandboxed_file_system_helper_finds_bwrap_on_preserved_path() -> Result< let helper_path = std::env::join_paths(path_entries)?; let server = exec_server_with_env([("PATH", helper_path.as_os_str())]).await?; - let environment = Environment::create(Some(server.websocket_url().to_string())).await?; + let environment = Environment::create_for_tests(Some(server.websocket_url().to_string()))?; let file_system = environment.get_filesystem(); let workspace = tmp.path().join("workspace"); std::fs::create_dir_all(&workspace)?; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 781e423fde45..1279532daf04 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -15,6 +15,7 @@ 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; @@ -497,8 +498,8 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result cloud_requirements: run_cloud_requirements, feedback: CodexFeedback::new(), log_db: None, - environment_manager: std::sync::Arc::new(EnvironmentManager::from_env_with_runtime_paths( - Some(local_runtime_paths), + environment_manager: std::sync::Arc::new(EnvironmentManager::new( + EnvironmentManagerArgs::from_env(local_runtime_paths), )), config_warnings, session_source: SessionSource::Exec, diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index 1320fd1b67e2..1d904e4577a0 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use codex_arg0::Arg0DispatchPaths; use codex_core::config::Config; 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; @@ -59,7 +60,7 @@ pub async fn run_main( arg0_paths: Arg0DispatchPaths, cli_config_overrides: CliConfigOverrides, ) -> IoResult<()> { - let environment_manager = Arc::new(EnvironmentManager::from_env_with_runtime_paths(Some( + let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env( ExecServerRuntimePaths::from_optional_paths( arg0_paths.codex_self_exe.clone(), arg0_paths.codex_linux_sandbox_exe.clone(), diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index 8b2dfe8d47f8..4dc724ee5e1f 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -38,7 +38,7 @@ pub(super) async fn make_test_app() -> App { backtrack_render_pending: false, feedback: codex_feedback::CodexFeedback::new(), feedback_audience: FeedbackAudience::External, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), remote_app_server_url: None, remote_app_server_auth_token: None, pending_update_action: None, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 1749ee9f1eb1..c498d3d41d78 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3576,7 +3576,7 @@ async fn make_test_app() -> App { backtrack_render_pending: false, feedback: codex_feedback::CodexFeedback::new(), feedback_audience: FeedbackAudience::External, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), remote_app_server_url: None, remote_app_server_auth_token: None, pending_update_action: None, @@ -3633,7 +3633,7 @@ async fn make_test_app_with_channels() -> ( backtrack_render_pending: false, feedback: codex_feedback::CodexFeedback::new(), feedback_audience: FeedbackAudience::External, - environment_manager: Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + environment_manager: Arc::new(EnvironmentManager::default_for_tests()), remote_app_server_url: None, remote_app_server_auth_token: None, pending_update_action: None, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 7c60b2e38a28..7e33f2e8a3f6 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -36,6 +36,7 @@ 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; @@ -425,7 +426,7 @@ pub(crate) async fn start_embedded_app_server_for_picker( start_app_server_for_picker( config, &AppServerTarget::Embedded, - Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + Arc::new(EnvironmentManager::default_for_tests()), ) .await } @@ -623,7 +624,9 @@ fn config_cwd_for_app_server_target( app_server_target: &AppServerTarget, environment_manager: &EnvironmentManager, ) -> std::io::Result> { - if environment_manager.is_remote() + if environment_manager + .default_environment() + .is_some_and(|environment| environment.is_remote()) || matches!(app_server_target, AppServerTarget::Remote { .. }) { return Ok(None); @@ -726,7 +729,7 @@ pub async fn run_main( } }; - let environment_manager = Arc::new(EnvironmentManager::from_env_with_runtime_paths(Some( + let environment_manager = Arc::new(EnvironmentManager::new(EnvironmentManagerArgs::from_env( ExecServerRuntimePaths::from_optional_paths( arg0_paths.codex_self_exe.clone(), arg0_paths.codex_linux_sandbox_exe.clone(), @@ -1771,7 +1774,7 @@ mod tests { CloudRequirementsLoader::default(), codex_feedback::CodexFeedback::new(), /*log_db*/ None, - Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + Arc::new(EnvironmentManager::default_for_tests()), ) .await } @@ -1919,8 +1922,9 @@ mod tests { Ok(()) } - #[test] - fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()> { + #[tokio::test] + async fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()> + { let remote_only_cwd = if cfg!(windows) { Path::new(r"C:\definitely\not\local\to\this\test") } else { @@ -1930,7 +1934,7 @@ mod tests { websocket_url: "ws://127.0.0.1:1234/".to_string(), auth_token: None, }; - let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None); + let environment_manager = EnvironmentManager::default_for_tests(); let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?; @@ -1939,11 +1943,12 @@ mod tests { Ok(()) } - #[test] - fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()> { + #[tokio::test] + async fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()> + { let temp_dir = TempDir::new()?; let target = AppServerTarget::Embedded; - let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None); + let environment_manager = EnvironmentManager::default_for_tests(); let config_cwd = config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?; @@ -1957,13 +1962,13 @@ mod tests { Ok(()) } - #[test] - fn config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd() -> std::io::Result<()> - { + #[tokio::test] + async fn config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd() + -> std::io::Result<()> { let temp_dir = TempDir::new()?; let missing = temp_dir.path().join("missing"); let target = AppServerTarget::Embedded; - let environment_manager = EnvironmentManager::new(/*exec_server_url*/ None); + let environment_manager = EnvironmentManager::default_for_tests(); let err = config_cwd_for_app_server_target(Some(&missing), &target, &environment_manager) .expect_err("missing embedded cwd should fail"); @@ -1972,15 +1977,23 @@ mod tests { Ok(()) } - #[test] - fn config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server() -> std::io::Result<()> { + #[tokio::test] + async fn config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server() + -> std::io::Result<()> { let remote_only_cwd = if cfg!(windows) { Path::new(r"C:\definitely\not\local\to\this\test") } else { Path::new("/definitely/not/local/to/this/test") }; let target = AppServerTarget::Embedded; - let environment_manager = EnvironmentManager::new(Some("ws://127.0.0.1:8765".to_string())); + let environment_manager = + EnvironmentManager::new(codex_exec_server::EnvironmentManagerArgs { + exec_server_url: Some("ws://127.0.0.1:8765".to_string()), + local_runtime_paths: ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe"), + /*codex_linux_sandbox_exe*/ None, + )?, + }); let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?; @@ -2107,7 +2120,7 @@ mod tests { CloudRequirementsLoader::default(), codex_feedback::CodexFeedback::new(), /*log_db*/ None, - Arc::new(EnvironmentManager::new(/*exec_server_url*/ None)), + Arc::new(EnvironmentManager::default_for_tests()), |_args| async { Err(std::io::Error::other("boom")) }, ) .await; diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index aee909cea1fc..1e55b5c5d2e1 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -989,9 +989,9 @@ mod tests { ), feedback: codex_feedback::CodexFeedback::new(), log_db: None, - environment_manager: Arc::new(codex_app_server_client::EnvironmentManager::new( - /*exec_server_url*/ None, - )), + environment_manager: Arc::new( + codex_app_server_client::EnvironmentManager::default_for_tests(), + ), config_warnings: Vec::new(), session_source: SessionSource::Cli, enable_codex_api_key_env: false,