diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9461a61fe9..d9fb5d2804 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2646,6 +2646,7 @@ dependencies = [ "codex-core-plugins", "codex-core-skills", "codex-exec-server", + "codex-exec-server-test-support", "codex-execpolicy", "codex-extension-api", "codex-extension-items", @@ -2783,6 +2784,7 @@ dependencies = [ "codex-connectors", "codex-core-skills", "codex-exec-server", + "codex-exec-server-test-support", "codex-git-utils", "codex-hooks", "codex-http-client", @@ -2914,6 +2916,7 @@ dependencies = [ "clatter", "codex-api", "codex-exec-server-protocol", + "codex-exec-server-test-support", "codex-file-system", "codex-http-client", "codex-network-proxy", @@ -2969,6 +2972,14 @@ dependencies = [ "serde_json", ] +[[package]] +name = "codex-exec-server-test-support" +version = "0.0.0" +dependencies = [ + "codex-exec-server", + "codex-http-client", +] + [[package]] name = "codex-execpolicy" version = "0.0.0" @@ -3391,6 +3402,7 @@ dependencies = [ "codex-config", "codex-connectors", "codex-exec-server", + "codex-exec-server-test-support", "codex-login", "codex-model-provider", "codex-otel", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 8aa7d7f0b2..3cd3498b53 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -47,6 +47,7 @@ members = [ "file-system", "exec-server-protocol", "exec-server", + "exec-server/tests/support", "execpolicy", "ext/agent", "ext/connectors", @@ -181,6 +182,7 @@ codex-exec = { path = "exec" } codex-file-system = { path = "file-system" } codex-exec-server-protocol = { path = "exec-server-protocol" } codex-exec-server = { path = "exec-server" } +codex-exec-server-test-support = { path = "exec-server/tests/support" } codex-execpolicy = { path = "execpolicy" } codex-extension-api = { path = "ext/extension-api" } codex-extension-items = { path = "ext/items" } diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 24d0f94a56..efd01055b5 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -480,13 +480,7 @@ pub async fn run_main_with_transport_options( 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(Some(local_runtime_paths)).await - } else { - EnvironmentManager::from_codex_home(codex_home.clone(), Some(local_runtime_paths)).await - } - .map(Arc::new) - .map_err(std::io::Error::other)?; + let ignore_user_config = loader_overrides.ignore_user_config; let config_manager = ConfigManager::new( codex_home.to_path_buf(), cli_kv_overrides.clone(), @@ -540,6 +534,18 @@ pub async fn run_main_with_transport_options( })? } }; + let environment_manager = if ignore_user_config { + EnvironmentManager::from_env(Some(local_runtime_paths), config.http_client_factory()).await + } else { + EnvironmentManager::from_codex_home( + codex_home.clone(), + Some(local_runtime_paths), + config.http_client_factory(), + ) + .await + } + .map(Arc::new) + .map_err(std::io::Error::other)?; let otel = codex_core::otel_init::build_provider( &config, diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 848180a517..cd39176b2e 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -561,7 +561,9 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> config.auth_keyring_backend_kind(), auth.as_ref(), &McpRuntimeContext::new( - Arc::new(EnvironmentManager::without_environments()), + Arc::new(EnvironmentManager::without_environments( + config.http_client_factory(), + )), config.cwd.to_path_buf(), ), ) diff --git a/codex-rs/codex-mcp/Cargo.toml b/codex-rs/codex-mcp/Cargo.toml index e1da7b22fc..a61e71c51c 100644 --- a/codex-rs/codex-mcp/Cargo.toml +++ b/codex-rs/codex-mcp/Cargo.toml @@ -42,6 +42,7 @@ tracing = { workspace = true } url = { workspace = true } [dev-dependencies] +codex-exec-server-test-support = { workspace = true } codex-plugin = { workspace = true } pretty_assertions = { workspace = true } rmcp = { workspace = true, default-features = false, features = ["base64", "macros", "schemars", "server"] } diff --git a/codex-rs/codex-mcp/src/connection_manager_tests.rs b/codex-rs/codex-mcp/src/connection_manager_tests.rs index b79f8130b3..25b3dc870d 100644 --- a/codex-rs/codex-mcp/src/connection_manager_tests.rs +++ b/codex-rs/codex-mcp/src/connection_manager_tests.rs @@ -31,7 +31,7 @@ use codex_connectors::ConnectorRuntimeContext; use codex_connectors::ConnectorRuntimeContextKey; use codex_connectors::ConnectorRuntimeFetchSource; use codex_connectors::ConnectorRuntimeManager; -use codex_exec_server::EnvironmentManager; +use codex_exec_server_test_support::environment_manager_without_environments; use codex_login::CodexAuth; use codex_protocol::ToolName; use codex_protocol::mcp::McpServerInfo; @@ -1344,7 +1344,7 @@ async fn hard_refresh_keeps_binding_override_local_when_shared_cache_loses_race( #[tokio::test(start_paused = true)] async fn tool_catalog_cache_sanitizes_tools_and_tracks_environment_generation() { let cache = McpToolCatalogCache::default(); - let environment_manager = Arc::new(EnvironmentManager::without_environments()); + let environment_manager = Arc::new(environment_manager_without_environments()); let replace_environment = |url: &str| { environment_manager .upsert_environment( @@ -1416,7 +1416,7 @@ async fn tool_catalog_cache_sanitizes_tools_and_tracks_environment_generation() fn tool_catalog_cache_bypasses_remote_sourced_environment_variables() { let cache = McpToolCatalogCache::default(); let runtime_context = McpRuntimeContext::new( - Arc::new(EnvironmentManager::without_environments()), + Arc::new(environment_manager_without_environments()), PathBuf::from("/tmp"), ); let config: McpServerConfig = serde_json::from_value(serde_json::json!({ @@ -2292,7 +2292,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() { tx_event: None, startup_cancellation_token: cancel_token.clone(), runtime_context: McpRuntimeContext::new( - Arc::new(EnvironmentManager::without_environments()), + Arc::new(environment_manager_without_environments()), PathBuf::from("/tmp"), ), codex_apps_tools_cache: ConnectorRuntimeManager::::default(), @@ -2533,7 +2533,7 @@ fn reusable_server_config(url: &str) -> McpServerConfig { fn reusable_server_runtime_context() -> McpRuntimeContext { McpRuntimeContext::new( - Arc::new(EnvironmentManager::without_environments()), + Arc::new(environment_manager_without_environments()), PathBuf::from("/tmp"), ) } diff --git a/codex-rs/codex-mcp/src/runtime.rs b/codex-rs/codex-mcp/src/runtime.rs index 1f27fd4020..67abb8423c 100644 --- a/codex-rs/codex-mcp/src/runtime.rs +++ b/codex-rs/codex-mcp/src/runtime.rs @@ -354,6 +354,7 @@ mod tests { use codex_config::McpServerConfig; use codex_config::McpServerTransportConfig; use codex_exec_server::EnvironmentManager; + use codex_exec_server_test_support::environment_manager_without_environments; use codex_utils_path_uri::LegacyAppPathString; use pretty_assertions::assert_eq; use serde_json::Value; @@ -475,7 +476,7 @@ mod tests { #[test] fn local_stdio_requires_local_stdio_availability() { let runtime_context = McpRuntimeContext::new( - Arc::new(EnvironmentManager::without_environments()), + Arc::new(environment_manager_without_environments()), PathBuf::from("/tmp"), ); @@ -494,7 +495,7 @@ mod tests { #[test] fn local_http_does_not_require_local_stdio_availability() { let runtime_context = McpRuntimeContext::new( - Arc::new(EnvironmentManager::without_environments()), + Arc::new(environment_manager_without_environments()), PathBuf::from("/tmp"), ); @@ -510,7 +511,7 @@ mod tests { #[test] fn unknown_explicit_environment_is_rejected() { let runtime_context = McpRuntimeContext::new( - Arc::new(EnvironmentManager::without_environments()), + Arc::new(environment_manager_without_environments()), PathBuf::from("/tmp"), ); diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index ade250db06..200174562b 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -57,6 +57,7 @@ zip = { workspace = true } which = { workspace = true } [dev-dependencies] +codex-exec-server-test-support = { workspace = true } libc = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/core-plugins/src/provider_tests.rs b/codex-rs/core-plugins/src/provider_tests.rs index e35a094940..25531a5fa2 100644 --- a/codex-rs/core-plugins/src/provider_tests.rs +++ b/codex-rs/core-plugins/src/provider_tests.rs @@ -14,6 +14,7 @@ use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::ReadDirectoryEntry; use codex_exec_server::RemoveOptions; +use codex_exec_server_test_support::environment_manager_without_environments; use codex_plugin::PluginProvider; use codex_plugin::ResolvedPlugin; use codex_protocol::capabilities::CapabilityRootLocation; @@ -318,7 +319,7 @@ async fn unavailable_environment_does_not_fall_back_to_host_filesystem() { let plugin_root = temp_dir.path().join("host-plugin"); write_manifest(&plugin_root, ".codex-plugin/plugin.json", MANIFEST_CONTENTS); let provider = - ExecutorPluginProvider::new(Arc::new(EnvironmentManager::without_environments())); + ExecutorPluginProvider::new(Arc::new(environment_manager_without_environments())); let err = provider .resolve(&selected_root("host-plugin", "missing", &plugin_root)) diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 7e4066b1d3..3eac8f03e6 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -138,6 +138,7 @@ codex-shell-escalation = { workspace = true } [dev-dependencies] assert_cmd = { workspace = true } assert_matches = { workspace = true } +codex-exec-server-test-support = { workspace = true } codex-image-generation-extension = { workspace = true } codex-home = { workspace = true } codex-otel = { workspace = true } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index e7422d5a5b..e310bf7c42 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2927,13 +2927,22 @@ pub fn resolve_bootstrap_auth_route_config( cfg: &ConfigToml, feature_requirements: Option<&Sourced>, ) -> std::io::Result { + resolve_bootstrap_http_client_factory(cfg, feature_requirements) + .map(AuthRouteConfig::from_http_client_factory) +} + +/// Resolves shared HTTP routing for startup work that runs before final [`Config`] loading. +pub fn resolve_bootstrap_http_client_factory( + cfg: &ConfigToml, + feature_requirements: Option<&Sourced>, +) -> std::io::Result { resolve_bootstrap_respect_system_proxy(cfg, feature_requirements).map(|respect_system_proxy| { let outbound_proxy_policy = if respect_system_proxy { OutboundProxyPolicy::RespectSystemProxy } else { OutboundProxyPolicy::ReqwestDefault }; - AuthRouteConfig::from_http_client_factory(HttpClientFactory::new(outbound_proxy_policy)) + HttpClientFactory::new(outbound_proxy_policy) }) } diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 2b62270051..8b1a01d61b 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -166,9 +166,12 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( config.codex_self_exe.clone(), config.codex_linux_sandbox_exe.clone(), )?; - let environment_manager = - EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths)) - .await?; + let environment_manager = EnvironmentManager::from_codex_home( + config.codex_home.clone(), + Some(local_runtime_paths), + config.http_client_factory(), + ) + .await?; list_accessible_connectors_from_mcp_tools_with_environment_manager( config, force_refetch, diff --git a/codex-rs/core/src/environment_selection.rs b/codex-rs/core/src/environment_selection.rs index 97c9c0d3bd..433e50819a 100644 --- a/codex-rs/core/src/environment_selection.rs +++ b/codex-rs/core/src/environment_selection.rs @@ -480,6 +480,9 @@ mod tests { use codex_exec_server::ExecServerRuntimePaths; use codex_exec_server::LOCAL_ENVIRONMENT_ID; use codex_exec_server::REMOTE_ENVIRONMENT_ID; + use codex_exec_server_test_support::environment_manager_without_environments; + use codex_http_client::HttpClientFactory; + use codex_http_client::OutboundProxyPolicy; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; @@ -609,10 +612,13 @@ url = "ws://127.0.0.1:8765" .expect("write environments.toml"); let cwd = AbsolutePathBuf::current_dir().expect("cwd"); let cwd_uri = PathUri::from_abs_path(&cwd); - let manager = - EnvironmentManager::from_codex_home(temp_dir.path(), Some(test_runtime_paths())) - .await - .expect("environment manager"); + let manager = EnvironmentManager::from_codex_home( + temp_dir.path(), + Some(test_runtime_paths()), + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + ) + .await + .expect("environment manager"); assert_eq!( default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)), @@ -634,7 +640,7 @@ url = "ws://127.0.0.1:8765" #[tokio::test] async fn default_thread_environment_selections_empty_when_default_disabled() { let cwd = AbsolutePathBuf::current_dir().expect("cwd"); - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); assert_eq!( default_thread_environment_selections(&manager, &cwd, std::slice::from_ref(&cwd)), @@ -1062,7 +1068,7 @@ url = "ws://127.0.0.1:8765" Vec::new(), /*shell*/ None, ); - let manager = Arc::new(EnvironmentManager::without_environments()); + let manager = Arc::new(environment_manager_without_environments()); manager .upsert_environment( REMOTE_ENVIRONMENT_ID.to_string(), diff --git a/codex-rs/core/src/prompt_debug.rs b/codex-rs/core/src/prompt_debug.rs index 5397286e61..3fbaf1d683 100644 --- a/codex-rs/core/src/prompt_debug.rs +++ b/codex-rs/core/src/prompt_debug.rs @@ -52,6 +52,7 @@ pub async fn build_prompt_input( EnvironmentManager::from_codex_home( config.codex_home.clone(), Some(local_runtime_paths), + config.http_client_factory(), ) .await .map_err(|err| CodexErr::Fatal(err.to_string()))?, diff --git a/codex-rs/core/tests/suite/mcp_auth_refresh.rs b/codex-rs/core/tests/suite/mcp_auth_refresh.rs index 602654619b..931407f9aa 100644 --- a/codex-rs/core/tests/suite/mcp_auth_refresh.rs +++ b/codex-rs/core/tests/suite/mcp_auth_refresh.rs @@ -4,6 +4,7 @@ use anyhow::Result; use codex_config::McpServerTransportConfig; use codex_core::config::ConfigBuilder; use codex_core::config::Constrained; +use codex_exec_server_test_support::environment_manager_without_environments; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::ExternalAuth; @@ -95,7 +96,7 @@ async fn hosted_plugin_runtime_ps_mcp_tool_calls_use_current_auth_manager_token( tx_event: None, startup_cancellation_token: CancellationToken::new(), runtime_context: McpRuntimeContext::new( - Arc::new(codex_exec_server::EnvironmentManager::without_environments()), + Arc::new(environment_manager_without_environments()), home.path().to_path_buf(), ), codex_apps_tools_cache: CodexAppsToolsCache::default(), diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index f303ac70ee..076689bc77 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -63,6 +63,7 @@ windows-sys = { version = "0.52", features = [ [dev-dependencies] anyhow = { workspace = true } +codex-exec-server-test-support = { workspace = true } codex-test-binary-support = { workspace = true } ctor = { workspace = true } http = { workspace = true } diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 4ac437c728..30c2e4445f 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -5,6 +5,8 @@ use std::sync::Mutex; use std::sync::OnceLock; use std::sync::RwLock; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; use futures::FutureExt; @@ -21,6 +23,8 @@ use crate::client::LazyRemoteExecServerClient; use crate::client::http_client::ReqwestHttpClient; use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT; use crate::client_api::ExecServerTransportParams; +use crate::environment_bootstrap::PreparedEnvironmentManager; +use crate::environment_bootstrap::PreparedEnvironmentSource; use crate::environment_provider::DefaultEnvironmentProvider; use crate::environment_provider::EnvironmentDefault; use crate::environment_provider::EnvironmentProvider; @@ -76,6 +80,7 @@ pub struct EnvironmentManager { pub(super) environments: RwLock>>, local_environment: Option>, local_runtime_paths: Option, + http_client_factory: HttpClientFactory, } /// Information supplied by the environment owner when a deferred environment is ready. @@ -134,16 +139,18 @@ impl EnvironmentManager { )])), local_environment: Some(Arc::new(Environment::default_for_tests())), local_runtime_paths: None, + http_client_factory: HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), } } /// Builds a manager with no configured execution environments. - pub fn without_environments() -> Self { + pub fn without_environments(http_client_factory: HttpClientFactory) -> Self { Self { default_environment: None, environments: RwLock::new(HashMap::new()), local_environment: None, local_runtime_paths: None, + http_client_factory, } } @@ -151,59 +158,78 @@ impl EnvironmentManager { pub async fn create_for_tests( exec_server_url: Option, local_runtime_paths: Option, - ) -> Self { - Self::from_default_provider_url(exec_server_url, local_runtime_paths).await - } - - /// Builds a manager from `CODEX_HOME` and local runtime paths used when - /// creating local filesystem helpers. - /// - /// If `CODEX_HOME/environments.toml` is present, it defines the configured - /// environments. Otherwise this preserves the legacy - /// `CODEX_EXEC_SERVER_URL` behavior. - pub async fn from_codex_home( - codex_home: impl AsRef, - local_runtime_paths: Option, - ) -> Result { - if let Some(config) = noise_environment_config_from_env()? { - return Self::from_noise_environment_config(config, local_runtime_paths); - } - let provider = environment_provider_from_codex_home(codex_home.as_ref())?; - Self::from_snapshot(provider.snapshot().await?, local_runtime_paths) - } - - /// 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: Option, - ) -> Result { - if let Some(config) = noise_environment_config_from_env()? { - return Self::from_noise_environment_config(config, local_runtime_paths); - } - let provider = DefaultEnvironmentProvider::from_env(); - Self::from_snapshot(provider.snapshot().await?, local_runtime_paths) - } - - async fn from_default_provider_url( - exec_server_url: Option, - local_runtime_paths: Option, ) -> Self { let provider = DefaultEnvironmentProvider::new(exec_server_url); - match Self::from_snapshot(provider.snapshot_inner(), local_runtime_paths) { + match Self::from_snapshot( + provider.snapshot_inner(), + local_runtime_paths, + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + ) { Ok(manager) => manager, Err(err) => panic!("default provider should create valid environments: {err}"), } } - fn from_noise_environment_config( + /// Discovers configured environments without starting remote connections. + /// + /// If `CODEX_HOME/environments.toml` is present, it defines the configured + /// environments. Otherwise this preserves the legacy + /// `CODEX_EXEC_SERVER_URL` behavior. + pub async fn prepare_from_codex_home( + codex_home: impl AsRef, + ) -> Result { + let source = if let Some(config) = noise_environment_config_from_env()? { + PreparedEnvironmentSource::Noise(config) + } else { + let provider = environment_provider_from_codex_home(codex_home.as_ref())?; + PreparedEnvironmentSource::Snapshot(provider.snapshot().await?) + }; + Ok(PreparedEnvironmentManager { source }) + } + + /// Builds a manager from `CODEX_HOME` with an explicit outbound HTTP policy. + pub async fn from_codex_home( + codex_home: impl AsRef, + local_runtime_paths: Option, + http_client_factory: HttpClientFactory, + ) -> Result { + Self::prepare_from_codex_home(codex_home) + .await? + .build(local_runtime_paths, http_client_factory) + } + + /// Discovers environment-variable environments without starting connections. + pub async fn prepare_from_env() -> Result { + let source = if let Some(config) = noise_environment_config_from_env()? { + PreparedEnvironmentSource::Noise(config) + } else { + let provider = DefaultEnvironmentProvider::from_env(); + PreparedEnvironmentSource::Snapshot(provider.snapshot().await?) + }; + Ok(PreparedEnvironmentManager { source }) + } + + /// Builds a manager from environment variables with an explicit outbound HTTP policy. + pub async fn from_env( + local_runtime_paths: Option, + http_client_factory: HttpClientFactory, + ) -> Result { + Self::prepare_from_env() + .await? + .build(local_runtime_paths, http_client_factory) + } + + pub(crate) fn from_noise_environment_config( config: NoiseRendezvousEnvironmentConfig, local_runtime_paths: Option, + http_client_factory: HttpClientFactory, ) -> Result { let manager = Self { default_environment: Some(REMOTE_ENVIRONMENT_ID.to_string()), environments: RwLock::new(HashMap::new()), local_environment: None, local_runtime_paths, + http_client_factory, }; manager.upsert_noise_environment( REMOTE_ENVIRONMENT_ID.to_string(), @@ -220,15 +246,20 @@ impl EnvironmentManager { ) -> Self { let mut snapshot = DefaultEnvironmentProvider::new(exec_server_url).snapshot_inner(); snapshot.include_local = true; - match Self::from_snapshot(snapshot, Some(local_runtime_paths)) { + match Self::from_snapshot( + snapshot, + Some(local_runtime_paths), + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + ) { Ok(manager) => manager, Err(err) => panic!("test provider with local should create valid environments: {err}"), } } - fn from_snapshot( + pub(crate) fn from_snapshot( snapshot: EnvironmentProviderSnapshot, local_runtime_paths: Option, + http_client_factory: HttpClientFactory, ) -> Result { let EnvironmentProviderSnapshot { environments, @@ -292,6 +323,7 @@ impl EnvironmentManager { environments: RwLock::new(environment_map), local_environment, local_runtime_paths, + http_client_factory, }) } @@ -341,6 +373,11 @@ impl EnvironmentManager { .cloned() } + /// Returns the outbound HTTP policy carried by this manager. + pub fn http_client_factory(&self) -> &HttpClientFactory { + &self.http_client_factory + } + /// Returns the current status of one named environment when it is configured. pub async fn get_environment_status( &self, @@ -836,11 +873,17 @@ mod tests { use crate::client_api::StdioExecServerCommand; use crate::environment_provider::EnvironmentDefault; use crate::environment_provider::EnvironmentProviderSnapshot; + use codex_http_client::HttpClientFactory; + use codex_http_client::OutboundProxyPolicy; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; use tokio::net::TcpListener; use tokio::time::timeout; + fn legacy_http_client_factory() -> HttpClientFactory { + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault) + } + fn test_runtime_paths() -> ExecServerRuntimePaths { ExecServerRuntimePaths::new( std::env::current_exe().expect("current exe"), @@ -878,7 +921,9 @@ mod tests { .expect("noise environment configuration"); let manager = EnvironmentManager::from_noise_environment_config( - config, /*local_runtime_paths*/ None, + config, + /*local_runtime_paths*/ None, + legacy_http_client_factory(), ) .expect("build environment manager"); @@ -929,13 +974,19 @@ mod tests { #[tokio::test] async fn disabled_environment_manager_has_no_default_or_local_environment() { - let manager = EnvironmentManager::without_environments(); + let manager = EnvironmentManager::without_environments(HttpClientFactory::new( + OutboundProxyPolicy::RespectSystemProxy, + )); assert!(manager.default_environment().is_none()); assert_eq!(manager.default_environment_id(), None); assert_local_environment_unavailable(&manager); assert!(manager.get_environment(LOCAL_ENVIRONMENT_ID).is_none()); assert!(manager.get_environment(REMOTE_ENVIRONMENT_ID).is_none()); + assert_eq!( + manager.http_client_factory().outbound_proxy_policy(), + OutboundProxyPolicy::RespectSystemProxy + ); } #[tokio::test] @@ -987,8 +1038,12 @@ mod tests { default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()), include_local: false, }; - let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) - .expect("environment manager"); + let manager = EnvironmentManager::from_snapshot( + snapshot, + Some(test_runtime_paths()), + legacy_http_client_factory(), + ) + .expect("environment manager"); assert_eq!( manager.default_environment_id(), @@ -1011,8 +1066,12 @@ mod tests { default: EnvironmentDefault::Disabled, include_local: false, }; - let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) - .expect_err("empty id should fail"); + let err = EnvironmentManager::from_snapshot( + snapshot, + Some(test_runtime_paths()), + legacy_http_client_factory(), + ) + .expect_err("empty id should fail"); assert_eq!( err.to_string(), @@ -1030,8 +1089,12 @@ mod tests { default: EnvironmentDefault::Disabled, include_local: false, }; - let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) - .expect_err("local id should fail"); + let err = EnvironmentManager::from_snapshot( + snapshot, + Some(test_runtime_paths()), + legacy_http_client_factory(), + ) + .expect_err("local id should fail"); assert_eq!( err.to_string(), @@ -1050,8 +1113,12 @@ mod tests { default: EnvironmentDefault::EnvironmentId("devbox".to_string()), include_local: true, }; - let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) - .expect("manager"); + let manager = EnvironmentManager::from_snapshot( + snapshot, + Some(test_runtime_paths()), + legacy_http_client_factory(), + ) + .expect("manager"); assert_eq!(manager.default_environment_id(), Some("devbox")); assert_eq!( @@ -1072,8 +1139,12 @@ mod tests { default: EnvironmentDefault::Disabled, include_local: true, }; - let manager = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) - .expect("manager"); + let manager = EnvironmentManager::from_snapshot( + snapshot, + Some(test_runtime_paths()), + legacy_http_client_factory(), + ) + .expect("manager"); assert_eq!(manager.default_environment_id(), None); assert!(manager.default_environment().is_none()); @@ -1096,8 +1167,12 @@ mod tests { default: EnvironmentDefault::EnvironmentId("missing".to_string()), include_local: true, }; - let err = EnvironmentManager::from_snapshot(snapshot, Some(test_runtime_paths())) - .expect_err("unknown default should fail"); + let err = EnvironmentManager::from_snapshot( + snapshot, + Some(test_runtime_paths()), + legacy_http_client_factory(), + ) + .expect_err("unknown default should fail"); assert_eq!( err.to_string(), @@ -1178,9 +1253,12 @@ mod tests { }; snapshot.include_local = false; snapshot.default = EnvironmentDefault::Disabled; - let manager = - EnvironmentManager::from_snapshot(snapshot, /*local_runtime_paths*/ None) - .expect("environment manager"); + let manager = EnvironmentManager::from_snapshot( + snapshot, + /*local_runtime_paths*/ None, + legacy_http_client_factory(), + ) + .expect("environment manager"); assert!(manager.default_environment().is_none()); assert_eq!(manager.default_environment_id(), None); @@ -1197,7 +1275,7 @@ mod tests { #[tokio::test] async fn environment_manager_upserts_named_remote_environment() { - let manager = EnvironmentManager::without_environments(); + let manager = EnvironmentManager::without_environments(legacy_http_client_factory()); manager .upsert_environment( @@ -1231,7 +1309,7 @@ mod tests { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("bind websocket listener"); - let manager = EnvironmentManager::without_environments(); + let manager = EnvironmentManager::without_environments(legacy_http_client_factory()); manager .upsert_environment( @@ -1290,6 +1368,7 @@ mod tests { include_local: false, }, /*local_runtime_paths*/ None, + legacy_http_client_factory(), ) .expect("environment manager"); let environment = manager.get_environment("stdio").expect("stdio environment"); @@ -1323,6 +1402,7 @@ mod tests { include_local: false, }, /*local_runtime_paths*/ None, + legacy_http_client_factory(), ) .expect("environment manager"); let environment = manager.get_environment("stdio").expect("stdio environment"); @@ -1373,7 +1453,7 @@ mod tests { let second_listener = TcpListener::bind("127.0.0.1:0") .await .expect("bind second websocket listener"); - let manager = EnvironmentManager::without_environments(); + let manager = EnvironmentManager::without_environments(legacy_http_client_factory()); manager .upsert_environment( "executor-a".to_string(), @@ -1421,7 +1501,7 @@ mod tests { #[tokio::test] async fn environment_manager_rejects_empty_remote_environment_url() { - let manager = EnvironmentManager::without_environments(); + let manager = EnvironmentManager::without_environments(legacy_http_client_factory()); let err = manager .upsert_environment( diff --git a/codex-rs/exec-server/src/environment_bootstrap.rs b/codex-rs/exec-server/src/environment_bootstrap.rs new file mode 100644 index 0000000000..3427321cac --- /dev/null +++ b/codex-rs/exec-server/src/environment_bootstrap.rs @@ -0,0 +1,67 @@ +use codex_http_client::HttpClientFactory; + +use crate::EnvironmentManager; +use crate::ExecServerError; +use crate::ExecServerRuntimePaths; +use crate::environment_provider::EnvironmentDefault; +use crate::environment_provider::EnvironmentProviderSnapshot; +use crate::remote::NoiseRendezvousEnvironmentConfig; + +#[derive(Debug)] +pub(crate) enum PreparedEnvironmentSource { + Noise(NoiseRendezvousEnvironmentConfig), + Snapshot(EnvironmentProviderSnapshot), +} + +/// Holds discovered execution environments before their HTTP policy is resolved. +/// +/// Preparing environments does not start remote connections. Callers can inspect +/// the default environment to choose config-loading behavior and then build the +/// manager with the effective outbound HTTP policy. +#[derive(Debug)] +pub struct PreparedEnvironmentManager { + pub(crate) source: PreparedEnvironmentSource, +} + +impl PreparedEnvironmentManager { + /// Returns whether the discovered default environment is remote. + pub fn default_environment_is_remote(&self) -> bool { + match &self.source { + PreparedEnvironmentSource::Noise(_) => true, + PreparedEnvironmentSource::Snapshot(snapshot) => match &snapshot.default { + EnvironmentDefault::Disabled => false, + EnvironmentDefault::EnvironmentId(default_id) => snapshot + .environments + .iter() + .find(|(environment_id, _)| environment_id == default_id) + .is_some_and(|(_, environment)| environment.is_remote()), + }, + } + } + + /// Builds the manager and starts remote connections using the supplied policy. + pub fn build( + self, + local_runtime_paths: Option, + http_client_factory: HttpClientFactory, + ) -> Result { + match self.source { + PreparedEnvironmentSource::Noise(config) => { + EnvironmentManager::from_noise_environment_config( + config, + local_runtime_paths, + http_client_factory, + ) + } + PreparedEnvironmentSource::Snapshot(snapshot) => EnvironmentManager::from_snapshot( + snapshot, + local_runtime_paths, + http_client_factory, + ), + } + } +} + +#[cfg(test)] +#[path = "environment_bootstrap_tests.rs"] +mod tests; diff --git a/codex-rs/exec-server/src/environment_bootstrap_tests.rs b/codex-rs/exec-server/src/environment_bootstrap_tests.rs new file mode 100644 index 0000000000..70fae51c9a --- /dev/null +++ b/codex-rs/exec-server/src/environment_bootstrap_tests.rs @@ -0,0 +1,87 @@ +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; +use pretty_assertions::assert_eq; + +use super::PreparedEnvironmentManager; +use super::PreparedEnvironmentSource; +use crate::DefaultEnvironmentProvider; +use crate::Environment; +use crate::EnvironmentConnectionState; +use crate::ExecServerRuntimePaths; +use crate::LOCAL_ENVIRONMENT_ID; +use crate::REMOTE_ENVIRONMENT_ID; +use crate::environment_provider::EnvironmentDefault; +use crate::environment_provider::EnvironmentProviderSnapshot; + +#[test] +fn prepared_remote_environment_is_detected_without_starting_a_connection() { + let environment = Environment::create_for_tests(Some("ws://127.0.0.1:8765".to_string())) + .expect("remote environment"); + let connection_state = environment + .subscribe_connection_state() + .expect("remote connection state"); + let prepared = PreparedEnvironmentManager { + source: PreparedEnvironmentSource::Snapshot(EnvironmentProviderSnapshot { + environments: vec![(REMOTE_ENVIRONMENT_ID.to_string(), environment)], + default: EnvironmentDefault::EnvironmentId(REMOTE_ENVIRONMENT_ID.to_string()), + include_local: false, + }), + }; + + assert!(prepared.default_environment_is_remote()); + assert_eq!( + *connection_state.borrow(), + EnvironmentConnectionState::Disconnected + ); +} + +#[test] +fn prepared_local_and_disabled_environments_are_not_remote() { + let local = PreparedEnvironmentManager { + source: PreparedEnvironmentSource::Snapshot(EnvironmentProviderSnapshot { + environments: Vec::new(), + default: EnvironmentDefault::EnvironmentId(LOCAL_ENVIRONMENT_ID.to_string()), + include_local: true, + }), + }; + let disabled = PreparedEnvironmentManager { + source: PreparedEnvironmentSource::Snapshot(EnvironmentProviderSnapshot { + environments: Vec::new(), + default: EnvironmentDefault::Disabled, + include_local: false, + }), + }; + + assert_eq!( + [ + local.default_environment_is_remote(), + disabled.default_environment_is_remote() + ], + [false, false] + ); +} + +#[tokio::test] +async fn prepared_environment_manager_builds_with_the_explicit_http_policy() { + let prepared = PreparedEnvironmentManager { + source: PreparedEnvironmentSource::Snapshot( + DefaultEnvironmentProvider::new(/*exec_server_url*/ None).snapshot_inner(), + ), + }; + let runtime_paths = ExecServerRuntimePaths::new( + std::env::current_exe().expect("current exe"), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths"); + let manager = prepared + .build( + Some(runtime_paths), + HttpClientFactory::new(OutboundProxyPolicy::RespectSystemProxy), + ) + .expect("environment manager"); + + assert_eq!( + manager.http_client_factory().outbound_proxy_policy(), + OutboundProxyPolicy::RespectSystemProxy + ); +} diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 803302b373..5069caaafe 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -6,6 +6,7 @@ mod client_api; mod client_transport; mod connection; mod environment; +mod environment_bootstrap; mod environment_provider; mod environment_registry; mod environment_toml; @@ -85,6 +86,7 @@ pub use environment::EnvironmentReadyInfo; pub use environment::LOCAL_ENVIRONMENT_ID; pub use environment::MAX_SELECTED_CAPABILITY_ROOTS; pub use environment::REMOTE_ENVIRONMENT_ID; +pub use environment_bootstrap::PreparedEnvironmentManager; pub use environment_provider::DefaultEnvironmentProvider; pub use environment_provider::EnvironmentProvider; pub use environment_provider::EnvironmentProviderFuture; diff --git a/codex-rs/exec-server/tests/deferred_environment.rs b/codex-rs/exec-server/tests/deferred_environment.rs index 11de320b3c..899a7605a6 100644 --- a/codex-rs/exec-server/tests/deferred_environment.rs +++ b/codex-rs/exec-server/tests/deferred_environment.rs @@ -2,12 +2,12 @@ use std::sync::Arc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; -use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentReadyInfo; use codex_exec_server::ExecServerError; use codex_exec_server::NoiseChannelPublicKey; use codex_exec_server::NoiseRendezvousConnectBundle; use codex_exec_server::NoiseRendezvousConnectProvider; +use codex_exec_server_test_support::environment_manager_without_environments; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_utils_path_uri::PathUri; @@ -56,7 +56,7 @@ fn ready_info(root_id: &str, environment_id: &str) -> anyhow::Result anyhow::Result<()> { - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); let provider = Arc::new(FailingNoiseConnectProvider::default()); let registration = manager.register_deferred_noise_environment("tools".to_string(), provider.clone())?; @@ -85,7 +85,7 @@ async fn deferred_environment_waits_before_connecting() -> anyhow::Result<()> { #[tokio::test] async fn failure_and_dropped_registration_are_terminal() -> anyhow::Result<()> { - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); let failed_provider = Arc::new(FailingNoiseConnectProvider::default()); let failed = manager .register_deferred_noise_environment("failed".to_string(), failed_provider.clone())?; @@ -118,7 +118,7 @@ async fn failure_and_dropped_registration_are_terminal() -> anyhow::Result<()> { #[tokio::test] async fn invalid_ready_info_is_terminal() -> anyhow::Result<()> { - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); let provider = Arc::new(FailingNoiseConnectProvider::default()); let registration = manager.register_deferred_noise_environment("tools".to_string(), provider.clone())?; @@ -141,7 +141,7 @@ async fn invalid_ready_info_is_terminal() -> anyhow::Result<()> { #[tokio::test] async fn late_completion_is_isolated_from_replacement() -> anyhow::Result<()> { - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); let old_provider = Arc::new(FailingNoiseConnectProvider::default()); let old_registration = manager.register_deferred_noise_environment("tools".to_string(), old_provider.clone())?; @@ -183,7 +183,7 @@ async fn late_completion_is_isolated_from_replacement() -> anyhow::Result<()> { #[tokio::test] async fn eager_noise_environment_connects_without_registration() -> anyhow::Result<()> { - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); let provider = Arc::new(FailingNoiseConnectProvider::default()); manager.upsert_noise_environment("tools".to_string(), provider.clone())?; let environment = manager.get_environment("tools").expect("environment"); diff --git a/codex-rs/exec-server/tests/relay.rs b/codex-rs/exec-server/tests/relay.rs index 692bee2b42..41b84e4f5b 100644 --- a/codex-rs/exec-server/tests/relay.rs +++ b/codex-rs/exec-server/tests/relay.rs @@ -16,7 +16,6 @@ use base64::Engine as _; use base64::engine::general_purpose::STANDARD; use codex_api::AuthProvider; use codex_exec_server::EnvironmentConnectionState; -use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentReadyInfo; use codex_exec_server::ExecParams; use codex_exec_server::ExecResponse; @@ -31,6 +30,7 @@ use codex_exec_server::NoiseRendezvousConnectBundle; use codex_exec_server::NoiseRendezvousConnectProvider; use codex_exec_server::ProcessId; use codex_exec_server::RemoteEnvironmentConfig; +use codex_exec_server_test_support::environment_manager_without_environments; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_utils_path_uri::PathUri; @@ -156,7 +156,7 @@ async fn deferred_noise_environment_connects_and_reconnects_with_fresh_bundle() executor_public_key: registered_executor_public_key(®istry).await?, calls: AtomicUsize::new(0), }); - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); let registration = manager .register_deferred_noise_environment(ENVIRONMENT_ID.to_string(), provider.clone())?; let environment = manager diff --git a/codex-rs/exec-server/tests/selected_capability_roots.rs b/codex-rs/exec-server/tests/selected_capability_roots.rs index 639d652ca8..3c621e1c82 100644 --- a/codex-rs/exec-server/tests/selected_capability_roots.rs +++ b/codex-rs/exec-server/tests/selected_capability_roots.rs @@ -5,7 +5,7 @@ mod common; use std::collections::HashMap; use std::sync::Arc; -use codex_exec_server::EnvironmentManager; +use codex_exec_server_test_support::environment_manager_without_environments; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_utils_path_uri::PathUri; @@ -15,7 +15,7 @@ use pretty_assertions::assert_eq; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn selected_capability_roots_use_captured_handle_after_replacement() -> anyhow::Result<()> { let mut executor = exec_server().await?; - let manager = EnvironmentManager::without_environments(); + let manager = environment_manager_without_environments(); let selected_root = SelectedCapabilityRoot { id: "demo@1".to_string(), location: CapabilityRootLocation::Environment { diff --git a/codex-rs/exec-server/tests/support/BUILD.bazel b/codex-rs/exec-server/tests/support/BUILD.bazel new file mode 100644 index 0000000000..8e09d7dafd --- /dev/null +++ b/codex-rs/exec-server/tests/support/BUILD.bazel @@ -0,0 +1,7 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "support", + crate_name = "codex_exec_server_test_support", + crate_srcs = glob(["*.rs"]), +) diff --git a/codex-rs/exec-server/tests/support/Cargo.toml b/codex-rs/exec-server/tests/support/Cargo.toml new file mode 100644 index 0000000000..85ad347b6c --- /dev/null +++ b/codex-rs/exec-server/tests/support/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "codex-exec-server-test-support" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +path = "lib.rs" +test = false +doctest = false + +[lints] +workspace = true + +[dependencies] +codex-exec-server = { workspace = true } +codex-http-client = { workspace = true } diff --git a/codex-rs/exec-server/tests/support/lib.rs b/codex-rs/exec-server/tests/support/lib.rs new file mode 100644 index 0000000000..5234363564 --- /dev/null +++ b/codex-rs/exec-server/tests/support/lib.rs @@ -0,0 +1,10 @@ +use codex_exec_server::EnvironmentManager; +use codex_http_client::HttpClientFactory; +use codex_http_client::OutboundProxyPolicy; + +/// Builds a manager without environments using the legacy outbound HTTP policy. +pub fn environment_manager_without_environments() -> EnvironmentManager { + EnvironmentManager::without_environments(HttpClientFactory::new( + OutboundProxyPolicy::ReqwestDefault, + )) +} diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index a13c83124b..b022e6c467 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -550,10 +550,15 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result )?; let state_db = codex_core::init_state_db(&config).await; let environment_manager = if run_loader_overrides.ignore_user_config { - EnvironmentManager::from_env(Some(local_runtime_paths)).await? - } else { - EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths)) + EnvironmentManager::from_env(Some(local_runtime_paths), config.http_client_factory()) .await? + } else { + EnvironmentManager::from_codex_home( + config.codex_home.clone(), + Some(local_runtime_paths), + config.http_client_factory(), + ) + .await? }; let in_process_start_args = InProcessClientStartArgs { arg0_paths, diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index 4a4ff054be..c462229716 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -100,6 +100,7 @@ pub async fn run_main( arg0_paths.codex_self_exe.clone(), arg0_paths.codex_linux_sandbox_exe.clone(), )?), + config.http_client_factory(), ) .await .map_err(std::io::Error::other)?, diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 81db5ca57f..ede1a8ef4b 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -123,8 +123,12 @@ async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { )?; let thread_store = thread_store_from_config(&config, state_db.clone()); let environment_manager = Arc::new( - EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths)) - .await?, + EnvironmentManager::from_codex_home( + config.codex_home.clone(), + Some(local_runtime_paths), + config.http_client_factory(), + ) + .await?, ); let installation_id = resolve_installation_id(&config.codex_home).await?; let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new( diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index ac7b57bb48..71dcf0b96e 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -9,7 +9,7 @@ use crate::legacy_core::config::ConfigOverrides; use crate::legacy_core::config::ConfigTomlLoadResult; use crate::legacy_core::config::load_config_toml_with_layer_stack; use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind; -use crate::legacy_core::config::resolve_bootstrap_auth_route_config; +use crate::legacy_core::config::resolve_bootstrap_http_client_factory; use crate::legacy_core::config::resolve_oss_provider; use crate::legacy_core::config::resolve_profile_v2_config_path; use crate::session_resume::ResolveCwdOutcome; @@ -47,6 +47,7 @@ use codex_config::types::ResumeCwdMode; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecServerRuntimePaths; use codex_login::AuthConfig; +use codex_login::AuthRouteConfig; use codex_login::default_client::originator; use codex_login::default_client::set_default_client_residency_requirement; use codex_login::enforce_login_restrictions; @@ -760,9 +761,9 @@ fn latest_session_lookup_params( fn config_cwd_for_app_server_target( cwd: Option<&Path>, app_server_target: &AppServerTarget, - environment_manager: &EnvironmentManager, + default_environment_is_remote: bool, ) -> std::io::Result> { - if uses_remote_workspace_or_environment(app_server_target, environment_manager) { + if app_server_target.uses_remote_workspace() || default_environment_is_remote { return Ok(None); } @@ -989,17 +990,19 @@ pub async fn run_main( arg0_paths.codex_self_exe.clone(), arg0_paths.codex_linux_sandbox_exe.clone(), )?; - let environment_manager = + let prepared_environment_manager = if should_load_configured_environments(&loader_overrides, &app_server_target) { - EnvironmentManager::from_codex_home(codex_home.clone(), Some(local_runtime_paths)).await + EnvironmentManager::prepare_from_codex_home(&codex_home).await } else { - EnvironmentManager::from_env(Some(local_runtime_paths)).await + EnvironmentManager::prepare_from_env().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)?; + let config_cwd = config_cwd_for_app_server_target( + cwd.as_deref(), + &app_server_target, + prepared_environment_manager.default_environment_is_remote(), + )?; let mut loader_overrides = loader_overrides; if let Some(profile_v2) = cli.config_profile_v2.as_ref() { let user_config_path = resolve_profile_v2_config_path(&codex_home, profile_v2); @@ -1022,7 +1025,7 @@ pub async fn run_main( .chatgpt_base_url .clone() .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()); - let auth_route_config = resolve_bootstrap_auth_route_config( + let bootstrap_http_client_factory = resolve_bootstrap_http_client_factory( bootstrap_config_toml, bootstrap_config .config_layer_stack @@ -1030,6 +1033,8 @@ pub async fn run_main( .feature_requirements .as_ref(), )?; + let auth_route_config = + AuthRouteConfig::from_http_client_factory(bootstrap_http_client_factory); let cloud_config_bundle = cloud_config_bundle_loader_for_storage( codex_home.to_path_buf(), /*enable_codex_api_key_env*/ false, @@ -1130,6 +1135,21 @@ pub async fn run_main( ) .await; + let cloud_config_bundle = cloud_config_bundle_loader_for_storage( + config.codex_home.to_path_buf(), + /*enable_codex_api_key_env*/ false, + config.cli_auth_credentials_store_mode, + config.auth_keyring_backend_kind(), + config.chatgpt_base_url.clone(), + config.auth_route_config(), + ) + .await; + let environment_manager = Arc::new( + prepared_environment_manager + .build(Some(local_runtime_paths), config.http_client_factory()) + .map_err(std::io::Error::other)?, + ); + remove_legacy_tui_log_file(config.codex_home.as_path()); let otel_originator = originator().value; @@ -2135,6 +2155,77 @@ mod tests { Ok(()) } + #[tokio::test] + async fn startup_services_use_final_cloud_managed_http_policy() -> color_eyre::Result<()> { + for (configured_respect_system_proxy, managed_respect_system_proxy) in + [(true, false), (false, true)] + { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + format!("[features]\nrespect_system_proxy = {configured_respect_system_proxy}\n"), + )?; + let prepared_environment_manager = + EnvironmentManager::prepare_from_codex_home(codex_home.path()).await?; + let loader_overrides = LoaderOverrides::without_managed_config_for_tests(); + let bootstrap_config = load_config_toml_with_layer_stack( + codex_home.path(), + /*cwd*/ None, + Vec::new(), + codex_config::ConfigLoadOptions { + loader_overrides: loader_overrides.clone(), + ..Default::default() + }, + ) + .await?; + let bootstrap_http_client_factory = resolve_bootstrap_http_client_factory( + &bootstrap_config.config_toml, + bootstrap_config + .config_layer_stack + .requirements() + .feature_requirements + .as_ref(), + )?; + let cloud_config_bundle = + codex_config::test_support::CloudConfigBundleFixture::loader_with_enterprise_requirement( + format!("[features]\nrespect_system_proxy = {managed_respect_system_proxy}\n"), + ); + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .loader_overrides(loader_overrides) + .cloud_config_bundle(cloud_config_bundle) + .build() + .await?; + let runtime_paths = ExecServerRuntimePaths::new( + std::env::current_exe()?, + /*codex_linux_sandbox_exe*/ None, + )?; + let environment_manager = prepared_environment_manager + .build(Some(runtime_paths), config.http_client_factory())?; + + assert_ne!( + bootstrap_http_client_factory.outbound_proxy_policy(), + config.http_client_factory().outbound_proxy_policy() + ); + assert_eq!( + environment_manager + .http_client_factory() + .outbound_proxy_policy(), + config.http_client_factory().outbound_proxy_policy() + ); + assert_eq!( + config + .auth_route_config() + .http_client_factory() + .outbound_proxy_policy(), + config.http_client_factory().outbound_proxy_policy() + ); + assert_eq!(config.respect_system_proxy, managed_respect_system_proxy); + } + + Ok(()) + } + async fn start_test_embedded_app_server( config: Config, ) -> color_eyre::Result { @@ -2875,8 +2966,13 @@ mod tests { }; let environment_manager = EnvironmentManager::default_for_tests(); - let config_cwd = - config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?; + let config_cwd = config_cwd_for_app_server_target( + Some(remote_only_cwd), + &target, + environment_manager + .default_environment() + .is_some_and(|environment| environment.is_remote()), + )?; assert_eq!(config_cwd, None); Ok(()) @@ -2889,8 +2985,13 @@ mod tests { let target = AppServerTarget::Embedded; let environment_manager = EnvironmentManager::default_for_tests(); - let config_cwd = - config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?; + let config_cwd = config_cwd_for_app_server_target( + Some(temp_dir.path()), + &target, + environment_manager + .default_environment() + .is_some_and(|environment| environment.is_remote()), + )?; assert_eq!( config_cwd, @@ -2912,8 +3013,13 @@ mod tests { }; let environment_manager = EnvironmentManager::default_for_tests(); - let config_cwd = - config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?; + let config_cwd = config_cwd_for_app_server_target( + Some(temp_dir.path()), + &target, + environment_manager + .default_environment() + .is_some_and(|environment| environment.is_remote()), + )?; assert_eq!( config_cwd, @@ -2932,8 +3038,14 @@ mod tests { let target = AppServerTarget::Embedded; 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"); + let err = config_cwd_for_app_server_target( + Some(&missing), + &target, + environment_manager + .default_environment() + .is_some_and(|environment| environment.is_remote()), + ) + .expect_err("missing embedded cwd should fail"); assert_eq!(err.kind(), std::io::ErrorKind::NotFound); Ok(()) @@ -2957,8 +3069,13 @@ mod tests { ) .await; - let config_cwd = - config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?; + let config_cwd = config_cwd_for_app_server_target( + Some(remote_only_cwd), + &target, + environment_manager + .default_environment() + .is_some_and(|environment| environment.is_remote()), + )?; assert_eq!(config_cwd, None); Ok(()) diff --git a/codex-rs/tui/src/session_archive_commands.rs b/codex-rs/tui/src/session_archive_commands.rs index 24e1a16c25..6b94655bcf 100644 --- a/codex-rs/tui/src/session_archive_commands.rs +++ b/codex-rs/tui/src/session_archive_commands.rs @@ -13,7 +13,7 @@ use crate::legacy_core::config::ConfigBuilder; use crate::legacy_core::config::ConfigOverrides; use crate::legacy_core::config::load_config_toml_with_layer_stack; use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind; -use crate::legacy_core::config::resolve_bootstrap_auth_route_config; +use crate::legacy_core::config::resolve_bootstrap_http_client_factory; use crate::legacy_core::config::resolve_oss_provider; use crate::legacy_core::config::resolve_profile_v2_config_path; use codex_app_server_protocol::Thread as AppServerThread; @@ -26,6 +26,7 @@ use codex_config::ConfigLoadOptions; use codex_config::LoaderOverrides; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecServerRuntimePaths; +use codex_login::AuthRouteConfig; use codex_protocol::ThreadId; use codex_utils_cli::CliConfigOverrides; use codex_utils_home_dir::find_codex_home; @@ -295,14 +296,13 @@ async fn start_app_server_for_archive_command( arg0_paths.codex_linux_sandbox_exe.clone(), ) .wrap_err("failed to resolve local runtime paths")?; - let environment_manager = EnvironmentManager::from_env(Some(local_runtime_paths)) + let prepared_environment_manager = EnvironmentManager::prepare_from_env() .await - .map(Arc::new) - .wrap_err("failed to initialize environment manager")?; + .wrap_err("failed to discover execution environments")?; let config_cwd = super::config_cwd_for_app_server_target( cli.cwd.as_deref(), &app_server_target, - &environment_manager, + prepared_environment_manager.default_environment_is_remote(), ) .wrap_err("failed to resolve config cwd")?; @@ -332,7 +332,7 @@ async fn start_app_server_for_archive_command( .chatgpt_base_url .clone() .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()); - let auth_route_config = resolve_bootstrap_auth_route_config( + let http_client_factory = resolve_bootstrap_http_client_factory( config_toml, bootstrap_config .config_layer_stack @@ -340,6 +340,12 @@ async fn start_app_server_for_archive_command( .feature_requirements .as_ref(), )?; + let environment_manager = Arc::new( + prepared_environment_manager + .build(Some(local_runtime_paths), http_client_factory.clone()) + .wrap_err("failed to initialize environment manager")?, + ); + let auth_route_config = AuthRouteConfig::from_http_client_factory(http_client_factory); let cloud_config_bundle = cloud_config_bundle_loader_for_storage( codex_home.to_path_buf(), /*enable_codex_api_key_env*/ false,