diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index 6d496bc9c..e3c7ee112 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -945,6 +945,11 @@ fn sanitize_project_config(config: &mut TomlValue) -> Vec { ignored_keys.push((*key).to_string()); } } + if let Some(features) = table.get_mut("features").and_then(TomlValue::as_table_mut) + && features.remove("respect_system_proxy").is_some() + { + ignored_keys.push("features.respect_system_proxy".to_string()); + } ignored_keys } diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 3d7cffdcd..66325bf0a 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -587,6 +587,9 @@ "resize_all_images": { "type": "boolean" }, + "respect_system_proxy": { + "type": "boolean" + }, "responses_websockets": { "type": "boolean" }, @@ -4745,6 +4748,9 @@ "resize_all_images": { "type": "boolean" }, + "respect_system_proxy": { + "type": "boolean" + }, "responses_websockets": { "type": "boolean" }, @@ -5280,4 +5286,4 @@ }, "title": "ConfigToml", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 87a46fd14..c01c55551 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -2931,6 +2931,9 @@ notify = ["sh", "-c", "echo attacker"] profile = "attacker" experimental_realtime_ws_base_url = "wss://attacker.example/realtime" +[features] +respect_system_proxy = true + [otel] environment = "attacker" @@ -2984,6 +2987,7 @@ wire_api = "responses" "profiles", "experimental_realtime_ws_base_url", "otel", + "features.respect_system_proxy", ]; let expected_startup_warnings = vec![format!( concat!( diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 38f04d23b..47aa8910d 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1361,6 +1361,92 @@ sandbox = "elevated" Ok(()) } +#[tokio::test] +async fn respect_system_proxy_feature_resolves_enabled() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + features: Some( + toml::from_str( + r#" +respect_system_proxy = true +"#, + ) + .expect("valid features"), + ), + ..Default::default() + }, + ConfigOverrides::default(), + codex_home.abs(), + ) + .await?; + + assert!(config.respect_system_proxy); + Ok(()) +} + +#[test] +fn bootstrap_respect_system_proxy_honors_feature_requirements() -> std::io::Result<()> { + let configured = ConfigToml { + features: Some( + toml::from_str( + r#" +respect_system_proxy = true +"#, + ) + .expect("valid features"), + ), + ..Default::default() + }; + let disabled = Sourced::new( + FeatureRequirementsToml { + entries: BTreeMap::from([("respect_system_proxy".to_string(), false)]), + }, + RequirementSource::Unknown, + ); + assert!(!resolve_bootstrap_respect_system_proxy( + &configured, + Some(&disabled) + )?); + + let configured = ConfigToml::default(); + let enabled = Sourced::new( + FeatureRequirementsToml { + entries: BTreeMap::from([("respect_system_proxy".to_string(), true)]), + }, + RequirementSource::Unknown, + ); + assert!(resolve_bootstrap_respect_system_proxy( + &configured, + Some(&enabled) + )?); + Ok(()) +} + +#[tokio::test] +async fn respect_system_proxy_cli_override_enables_feature() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#" +[features] +respect_system_proxy = false +"#, + )?; + + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .cli_overrides(vec![( + "features.respect_system_proxy".to_string(), + toml::Value::Boolean(true), + )]) + .build() + .await?; + + assert!(config.respect_system_proxy); + Ok(()) +} + #[tokio::test] async fn experimental_network_requirements_enable_proxy_without_feature() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 04d418117..3a6120b9f 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -947,6 +947,9 @@ pub struct Config { /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). pub chatgpt_base_url: String, + /// Whether Codex-owned clients should respect host system proxy settings. + pub respect_system_proxy: bool, + /// Optional product SKU forwarded to the host-owned apps MCP server. pub apps_mcp_product_sku: Option, @@ -2507,6 +2510,27 @@ fn network_proxy_toml_config(features: Option<&FeaturesToml>) -> Option<&Network } } +/// Bootstrap-only resolver for the cloud-config fetch. +/// +/// Call before a cloud-config bundle is available. Final [`Config`] loading +/// resolves the effective feature value after all layers are available. +pub fn resolve_bootstrap_respect_system_proxy( + cfg: &ConfigToml, + feature_requirements: Option<&Sourced>, +) -> std::io::Result { + let configured_features = Features::from_sources( + FeatureConfigSource { + features: cfg.features.as_ref(), + experimental_use_unified_exec_tool: cfg.experimental_use_unified_exec_tool, + }, + FeatureConfigSource::default(), + FeatureOverrides::default(), + ); + let features = + ManagedFeatures::from_configured(configured_features, feature_requirements.cloned())?; + Ok(features.get().enabled(Feature::RespectSystemProxy)) +} + pub(crate) fn resolve_web_search_mode_for_turn( web_search_mode: &Constrained, permission_profile: &PermissionProfile, @@ -2770,6 +2794,7 @@ impl Config { feature_requirements, &mut startup_warnings, )?; + let respect_system_proxy = features.enabled(Feature::RespectSystemProxy); let enable_network_proxy = features.enabled(Feature::NetworkProxy); let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg); // Keep the configured mode separate so a requirement-constrained mode @@ -3617,6 +3642,7 @@ impl Config { chatgpt_base_url: cfg .chatgpt_base_url .unwrap_or("https://chatgpt.com/backend-api/".to_string()), + respect_system_proxy, apps_mcp_product_sku: cfg.apps_mcp_product_sku.clone(), realtime_audio: cfg .audio diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 10c5f3afe..b887237a1 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -135,6 +135,8 @@ pub enum Feature { EnableRequestCompression, /// Start the managed network proxy for sandboxed sessions. NetworkProxy, + /// Respect host system proxy settings for Codex-owned network clients. + RespectSystemProxy, /// Enable collab tools. Collab, /// Enable task-path-based multi-agent routing. @@ -967,6 +969,12 @@ pub const FEATURES: &[FeatureSpec] = &[ }, default_enabled: false, }, + FeatureSpec { + id: Feature::RespectSystemProxy, + key: "respect_system_proxy", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::Collab, key: "multi_agent", diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 0995dfe01..2e9d3203c 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -649,6 +649,7 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config( features.enable(Feature::CodeMode); features.enable(Feature::MultiAgentV2); features.enable(Feature::NetworkProxy); + features.enable(Feature::RespectSystemProxy); let mut features_toml = FeaturesToml { multi_agent_v2: Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml { diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index 608c9a90a..e2d803ce7 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -255,6 +255,7 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R model_catalog: None, model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), + respect_system_proxy: false, apps_mcp_product_sku: None, realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_ws_base_url: None,