From 265cd2e100ff091bee0e691a7e28b42e4eb56837 Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Thu, 23 Jul 2026 19:14:49 +0000 Subject: [PATCH] Initialize execution environments with the final HTTP policy (#34995) ## Why The TUI must inspect the default execution environment before loading its final configuration. Initializing the environment manager at that point can give startup services the bootstrap HTTP policy instead of the effective policy after managed requirements are applied. ## What changed - Split environment discovery from manager construction so callers can inspect the default environment without starting remote connections. - Build the environment manager after final configuration loading and pass its resolved `HttpClientFactory` through all construction paths. - Add shared test support for managers that use the legacy default HTTP policy. ## Testing - Cover connection-free environment discovery and explicit HTTP policy propagation. - Verify TUI startup services use the final managed `respect_system_proxy` value. GitOrigin-RevId: 928fa31e6b4bcfbe1a121cade2f351427fdfa0f4 --- codex-rs/Cargo.lock | 12 ++ codex-rs/Cargo.toml | 2 + codex-rs/app-server/src/lib.rs | 20 +- codex-rs/cli/src/mcp_cmd.rs | 4 +- codex-rs/codex-mcp/Cargo.toml | 1 + .../codex-mcp/src/connection_manager_tests.rs | 10 +- codex-rs/codex-mcp/src/runtime.rs | 7 +- codex-rs/core-plugins/Cargo.toml | 1 + codex-rs/core-plugins/src/provider_tests.rs | 3 +- codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/config/mod.rs | 11 +- codex-rs/core/src/connectors.rs | 9 +- codex-rs/core/src/environment_selection.rs | 18 +- codex-rs/core/src/prompt_debug.rs | 1 + codex-rs/core/tests/suite/mcp_auth_refresh.rs | 3 +- codex-rs/exec-server/Cargo.toml | 1 + codex-rs/exec-server/src/environment.rs | 204 ++++++++++++------ .../exec-server/src/environment_bootstrap.rs | 67 ++++++ .../src/environment_bootstrap_tests.rs | 87 ++++++++ codex-rs/exec-server/src/lib.rs | 2 + .../exec-server/tests/deferred_environment.rs | 12 +- codex-rs/exec-server/tests/relay.rs | 4 +- .../tests/selected_capability_roots.rs | 4 +- .../exec-server/tests/support/BUILD.bazel | 7 + codex-rs/exec-server/tests/support/Cargo.toml | 17 ++ codex-rs/exec-server/tests/support/lib.rs | 10 + codex-rs/exec/src/lib.rs | 11 +- codex-rs/mcp-server/src/lib.rs | 1 + codex-rs/thread-manager-sample/src/main.rs | 8 +- codex-rs/tui/src/lib.rs | 157 ++++++++++++-- codex-rs/tui/src/session_archive_commands.rs | 18 +- 31 files changed, 582 insertions(+), 131 deletions(-) create mode 100644 codex-rs/exec-server/src/environment_bootstrap.rs create mode 100644 codex-rs/exec-server/src/environment_bootstrap_tests.rs create mode 100644 codex-rs/exec-server/tests/support/BUILD.bazel create mode 100644 codex-rs/exec-server/tests/support/Cargo.toml create mode 100644 codex-rs/exec-server/tests/support/lib.rs 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,