From 8d4dde8b2afaee52f729133458d30e271535b44f Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 28 Apr 2026 22:54:45 -0700 Subject: [PATCH] feat: add network proxy feature flag Co-authored-by: Codex noreply@openai.com --- codex-rs/cli/src/main.rs | 37 +++-- codex-rs/config/src/schema.rs | 9 ++ codex-rs/core/config.schema.json | 85 ++++++++++++ codex-rs/core/src/config/config_tests.rs | 166 +++++++++++++++++++++++ codex-rs/core/src/config/mod.rs | 21 ++- codex-rs/core/src/config/permissions.rs | 68 ++++++++++ codex-rs/features/src/feature_configs.rs | 57 ++++++++ codex-rs/features/src/lib.rs | 36 +++++ codex-rs/features/src/tests.rs | 13 ++ 9 files changed, 481 insertions(+), 11 deletions(-) diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index dbe6b7605c..04bec06721 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -58,7 +58,7 @@ use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::find_codex_home; use codex_features::FEATURES; use codex_features::Stage; -use codex_features::is_known_feature_key; +use codex_features::feature_toggle_override_key; use codex_login::AuthManager; use codex_memories_write::clear_memory_roots_contents; use codex_models_manager::bundled_models_response; @@ -687,22 +687,23 @@ impl FeatureToggles { fn to_overrides(&self) -> anyhow::Result> { let mut v = Vec::new(); for feature in &self.enable { - Self::validate_feature(feature)?; - v.push(format!("features.{feature}=true")); + let key = Self::feature_override_key(feature)?; + v.push(format!("{key}=true")); } for feature in &self.disable { - Self::validate_feature(feature)?; - v.push(format!("features.{feature}=false")); + let key = Self::feature_override_key(feature)?; + v.push(format!("{key}=false")); } Ok(v) } + fn feature_override_key(feature: &str) -> anyhow::Result { + feature_toggle_override_key(feature) + .ok_or_else(|| anyhow::anyhow!("Unknown feature flag: {feature}")) + } + fn validate_feature(feature: &str) -> anyhow::Result<()> { - if is_known_feature_key(feature) { - Ok(()) - } else { - anyhow::bail!("Unknown feature flag: {feature}") - } + Self::feature_override_key(feature).map(|_| ()) } } @@ -2659,6 +2660,22 @@ mod tests { ); } + #[test] + fn feature_toggles_preserve_configurable_feature_tables() { + let toggles = FeatureToggles { + enable: vec!["network_proxy".to_string()], + disable: vec!["multi_agent_v2".to_string()], + }; + let overrides = toggles.to_overrides().expect("valid features"); + assert_eq!( + overrides, + vec![ + "features.network_proxy.enabled=true".to_string(), + "features.multi_agent_v2.enabled=false".to_string(), + ] + ); + } + #[test] fn feature_toggles_accept_legacy_linux_sandbox_flag() { let toggles = FeatureToggles { diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs index 715822fbfe..df0de3b1cd 100644 --- a/codex-rs/config/src/schema.rs +++ b/codex-rs/config/src/schema.rs @@ -43,6 +43,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { ); continue; } + if feature.id == codex_features::Feature::NetworkProxy { + validation.properties.insert( + feature.key.to_string(), + schema_gen.subschema_for::>(), + ); + continue; + } validation .properties .insert(feature.key.to_string(), schema_gen.subschema_for::()); diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index edbf45694f..dee56cd6bf 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -487,6 +487,9 @@ "multi_agent_v2": { "$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml" }, + "network_proxy": { + "$ref": "#/definitions/FeatureToml_for_NetworkProxyConfigToml" + }, "personality": { "type": "boolean" }, @@ -802,6 +805,16 @@ } ] }, + "FeatureToml_for_NetworkProxyConfigToml": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/NetworkProxyConfigToml" + } + ] + }, "FeedbackConfigToml": { "additionalProperties": false, "properties": { @@ -1494,6 +1507,75 @@ ], "type": "string" }, + "NetworkProxyConfigToml": { + "additionalProperties": false, + "properties": { + "allow_local_binding": { + "type": "boolean" + }, + "allow_upstream_proxy": { + "type": "boolean" + }, + "dangerously_allow_all_unix_sockets": { + "type": "boolean" + }, + "dangerously_allow_non_loopback_proxy": { + "type": "boolean" + }, + "domains": { + "additionalProperties": { + "$ref": "#/definitions/NetworkProxyDomainPermissionToml" + }, + "type": "object" + }, + "enable_socks5": { + "type": "boolean" + }, + "enable_socks5_udp": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "mode": { + "$ref": "#/definitions/NetworkProxyModeToml" + }, + "proxy_url": { + "type": "string" + }, + "socks_url": { + "type": "string" + }, + "unix_sockets": { + "additionalProperties": { + "$ref": "#/definitions/NetworkProxyUnixSocketPermissionToml" + }, + "type": "object" + } + }, + "type": "object" + }, + "NetworkProxyDomainPermissionToml": { + "enum": [ + "allow", + "deny" + ], + "type": "string" + }, + "NetworkProxyModeToml": { + "enum": [ + "limited", + "full" + ], + "type": "string" + }, + "NetworkProxyUnixSocketPermissionToml": { + "enum": [ + "allow", + "none" + ], + "type": "string" + }, "NetworkToml": { "additionalProperties": false, "properties": { @@ -4043,6 +4125,9 @@ "multi_agent_v2": { "$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml" }, + "network_proxy": { + "$ref": "#/definitions/FeatureToml_for_NetworkProxyConfigToml" + }, "personality": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 9bda54a9a7..b0fbf21d3a 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -891,6 +891,172 @@ async fn permissions_profiles_proxy_policy_starts_managed_network_proxy() -> std Ok(()) } +#[tokio::test] +async fn network_proxy_feature_starts_proxy_without_enabling_sandbox_network() -> std::io::Result<()> +{ + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + features: Some( + toml::from_str( + r#" +[network_proxy] +enabled = true +proxy_url = "http://127.0.0.1:43128" +enable_socks5 = false +"#, + ) + .expect("valid features"), + ), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!( + config.permissions.network_sandbox_policy(), + NetworkSandboxPolicy::Restricted + ); + let network = config + .permissions + .network + .as_ref() + .expect("network_proxy should start the managed network proxy"); + assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128"); + assert!(!network.socks_enabled()); + Ok(()) +} + +#[tokio::test] +async fn network_proxy_cli_overrides_merge_toggle_with_proxy_config() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .cli_overrides(vec![ + ( + "features.network_proxy.enabled".to_string(), + toml::Value::Boolean(true), + ), + ( + "features.network_proxy.proxy_url".to_string(), + toml::Value::String("http://127.0.0.1:43128".to_string()), + ), + ( + "features.network_proxy.enable_socks5".to_string(), + toml::Value::Boolean(false), + ), + ]) + .harness_overrides(ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }) + .build() + .await?; + + assert_eq!( + config.permissions.network_sandbox_policy(), + NetworkSandboxPolicy::Restricted + ); + let network = config + .permissions + .network + .as_ref() + .expect("network_proxy should start the managed network proxy"); + assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128"); + assert!(!network.socks_enabled()); + Ok(()) +} + +#[tokio::test] +async fn experimental_network_requirements_enable_proxy_without_feature() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cloud_requirements(CloudRequirementsLoader::new(async { + Ok(Some(codex_config::ConfigRequirementsToml { + network: Some(codex_config::NetworkRequirementsToml { + enabled: Some(true), + ..Default::default() + }), + ..Default::default() + })) + })) + .build() + .await?; + + assert!(!config.features.enabled(Feature::NetworkProxy)); + assert!(config.managed_network_requirements_enabled()); + assert!( + config + .permissions + .network + .as_ref() + .expect("experimental_network should configure the managed proxy") + .enabled() + ); + Ok(()) +} + +#[tokio::test] +async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + features: Some(toml::from_str("network_proxy = true").expect("valid features")), + default_permissions: Some("workspace".to_string()), + permissions: Some(PermissionsToml { + entries: BTreeMap::from([( + "workspace".to_string(), + PermissionProfileToml { + filesystem: Some(FilesystemPermissionsToml { + glob_scan_max_depth: None, + entries: BTreeMap::from([( + ":minimal".to_string(), + FilesystemPermissionToml::Access(FileSystemAccessMode::Read), + )]), + }), + network: Some(NetworkToml { + enabled: Some(true), + proxy_url: Some("http://127.0.0.1:43128".to_string()), + enable_socks5: Some(false), + ..Default::default() + }), + }, + )]), + }), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!( + config.permissions.network_sandbox_policy(), + NetworkSandboxPolicy::Enabled + ); + let network = config + .permissions + .network + .as_ref() + .expect("network_proxy should start the managed network proxy"); + assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128"); + assert!(!network.socks_enabled()); + Ok(()) +} + #[tokio::test] async fn permissions_profiles_network_disabled_by_default_does_not_start_proxy() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b1277eda48..be2a6029e2 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -64,6 +64,7 @@ use codex_features::FeatureToml; use codex_features::Features; use codex_features::FeaturesToml; use codex_features::MultiAgentV2ConfigToml; +use codex_features::NetworkProxyConfigToml; use codex_git_utils::resolve_root_git_project_for_trust; use codex_login::AuthManagerConfig; use codex_mcp::BuiltinMcpServerOptions; @@ -111,6 +112,7 @@ use std::path::PathBuf; use std::sync::Arc; use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE; +use crate::config::permissions::apply_network_proxy_feature_config; use crate::config::permissions::builtin_permission_profile; use crate::config::permissions::compile_permission_profile_selection; use crate::config::permissions::default_builtin_permission_profile_name; @@ -2025,6 +2027,13 @@ fn apps_mcp_path_override_toml_config( } } +fn network_proxy_toml_config(features: Option<&FeaturesToml>) -> Option<&NetworkProxyConfigToml> { + match features?.network_proxy.as_ref()? { + FeatureToml::Enabled(_) => None, + FeatureToml::Config(config) => Some(config), + } +} + pub(crate) fn resolve_web_search_mode_for_turn( web_search_mode: &Constrained, permission_profile: &PermissionProfile, @@ -2215,6 +2224,7 @@ impl Config { feature_requirements, &mut startup_warnings, )?; + let enable_network_proxy = features.enabled(Feature::NetworkProxy); let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile); let windows_sandbox_private_desktop = resolve_windows_sandbox_private_desktop(&cfg, &config_profile); @@ -2298,7 +2308,7 @@ impl Config { let using_implicit_builtin_profile = permission_config_syntax.is_none() && default_permissions.is_none(); let ( - configured_network_proxy_config, + mut configured_network_proxy_config, permission_profile, file_system_sandbox_policy, mut active_permission_profile, @@ -2511,6 +2521,15 @@ impl Config { None, ) }; + if let Some(network_proxy) = network_proxy_toml_config(cfg.features.as_ref()) { + apply_network_proxy_feature_config(&mut configured_network_proxy_config, network_proxy); + } + if let Some(network_proxy) = network_proxy_toml_config(config_profile.features.as_ref()) { + apply_network_proxy_feature_config(&mut configured_network_proxy_config, network_proxy); + } + if enable_network_proxy { + configured_network_proxy_config.network.enabled = true; + } let approval_policy_was_explicit = approval_policy_override.is_some() || config_profile.approval_policy.is_some() || cfg.approval_policy.is_some(); diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index b51a8973a3..274a6ff116 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -6,10 +6,19 @@ use std::path::PathBuf; use codex_config::permissions_toml::FilesystemPermissionToml; use codex_config::permissions_toml::FilesystemPermissionsToml; +use codex_config::permissions_toml::NetworkDomainPermissionToml; +use codex_config::permissions_toml::NetworkDomainPermissionsToml; use codex_config::permissions_toml::NetworkToml; +use codex_config::permissions_toml::NetworkUnixSocketPermissionToml; +use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml; use codex_config::permissions_toml::PermissionProfileToml; use codex_config::permissions_toml::PermissionsToml; use codex_config::types::SandboxWorkspaceWrite; +use codex_features::NetworkProxyConfigToml; +use codex_features::NetworkProxyDomainPermissionToml; +use codex_features::NetworkProxyModeToml; +use codex_features::NetworkProxyUnixSocketPermissionToml; +use codex_network_proxy::NetworkMode; use codex_network_proxy::NetworkProxyConfig; #[cfg(test)] use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission; @@ -139,6 +148,65 @@ fn profile_network_requires_proxy(network: &NetworkToml) -> bool { || network.allow_local_binding == Some(true) } +pub(crate) fn apply_network_proxy_feature_config( + config: &mut NetworkProxyConfig, + feature_config: &NetworkProxyConfigToml, +) { + NetworkToml { + enabled: feature_config.enabled, + proxy_url: feature_config.proxy_url.clone(), + enable_socks5: feature_config.enable_socks5, + socks_url: feature_config.socks_url.clone(), + enable_socks5_udp: feature_config.enable_socks5_udp, + allow_upstream_proxy: feature_config.allow_upstream_proxy, + dangerously_allow_non_loopback_proxy: feature_config.dangerously_allow_non_loopback_proxy, + dangerously_allow_all_unix_sockets: feature_config.dangerously_allow_all_unix_sockets, + mode: feature_config.mode.map(|mode| match mode { + NetworkProxyModeToml::Limited => NetworkMode::Limited, + NetworkProxyModeToml::Full => NetworkMode::Full, + }), + domains: feature_config + .domains + .as_ref() + .map(|domains| NetworkDomainPermissionsToml { + entries: domains + .iter() + .map(|(pattern, permission)| { + let permission = match permission { + NetworkProxyDomainPermissionToml::Allow => { + NetworkDomainPermissionToml::Allow + } + NetworkProxyDomainPermissionToml::Deny => { + NetworkDomainPermissionToml::Deny + } + }; + (pattern.clone(), permission) + }) + .collect(), + }), + unix_sockets: feature_config.unix_sockets.as_ref().map(|unix_sockets| { + NetworkUnixSocketPermissionsToml { + entries: unix_sockets + .iter() + .map(|(path, permission)| { + let permission = match permission { + NetworkProxyUnixSocketPermissionToml::Allow => { + NetworkUnixSocketPermissionToml::Allow + } + NetworkProxyUnixSocketPermissionToml::None => { + NetworkUnixSocketPermissionToml::None + } + }; + (path.clone(), permission) + }) + .collect(), + } + }), + allow_local_binding: feature_config.allow_local_binding, + } + .apply_to_network_proxy_config(config); +} + pub(crate) fn resolve_permission_profile<'a>( permissions: &'a PermissionsToml, profile_name: &str, diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index 4f3eb5b11c..b7992fefd4 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -2,6 +2,7 @@ use crate::FeatureConfig; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use std::collections::BTreeMap; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] @@ -54,3 +55,59 @@ impl FeatureConfig for AppsMcpPathOverrideConfigToml { self.enabled = Some(enabled); } } + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct NetworkProxyConfigToml { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub proxy_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_socks5: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub socks_url: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_socks5_udp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_upstream_proxy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dangerously_allow_non_loopback_proxy: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub dangerously_allow_all_unix_sockets: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub domains: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub unix_sockets: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_local_binding: Option, +} + +impl FeatureConfig for NetworkProxyConfigToml { + fn enabled(&self) -> Option { + self.enabled + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum NetworkProxyModeToml { + Limited, + Full, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum NetworkProxyDomainPermissionToml { + Allow, + Deny, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum NetworkProxyUnixSocketPermissionToml { + Allow, + None, +} diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 721069a5bd..95bc297f1f 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -18,6 +18,10 @@ mod feature_configs; mod legacy; pub use feature_configs::AppsMcpPathOverrideConfigToml; pub use feature_configs::MultiAgentV2ConfigToml; +pub use feature_configs::NetworkProxyConfigToml; +pub use feature_configs::NetworkProxyDomainPermissionToml; +pub use feature_configs::NetworkProxyModeToml; +pub use feature_configs::NetworkProxyUnixSocketPermissionToml; use legacy::LegacyFeatureToggles; pub use legacy::legacy_feature_keys; @@ -142,6 +146,8 @@ pub enum Feature { ChildAgentsMd, /// Compress request bodies (zstd) when sending streaming requests to codex-backend. EnableRequestCompression, + /// Start the managed network proxy for sandboxed sessions. + NetworkProxy, /// Enable collab tools. Collab, /// Enable task-path-based multi-agent routing. @@ -252,6 +258,13 @@ impl Feature { self.info().default_enabled } + pub fn uses_config_table(self) -> bool { + matches!( + self, + Feature::MultiAgentV2 | Feature::AppsMcpPathOverride | Feature::NetworkProxy + ) + } + fn info(self) -> &'static FeatureSpec { FEATURES .iter() @@ -567,6 +580,15 @@ pub fn is_known_feature_key(key: &str) -> bool { feature_for_key(key).is_some() } +pub fn feature_toggle_override_key(key: &str) -> Option { + let feature = feature_for_key(key)?; + if feature.uses_config_table() { + Some(format!("features.{}.enabled", feature.key())) + } else { + Some(format!("features.{key}")) + } +} + /// Deserializable features table for TOML. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] pub struct FeaturesToml { @@ -574,6 +596,7 @@ pub struct FeaturesToml { pub multi_agent_v2: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub apps_mcp_path_override: Option>, + pub network_proxy: Option>, /// Boolean feature toggles keyed by canonical or legacy feature name. #[serde(flatten)] entries: BTreeMap, @@ -599,6 +622,9 @@ impl FeaturesToml { { entries.insert(Feature::AppsMcpPathOverride.key().to_string(), enabled); } + if let Some(enabled) = self.network_proxy.as_ref().and_then(FeatureToml::enabled) { + entries.insert(Feature::NetworkProxy.key().to_string(), enabled); + } entries } @@ -887,6 +913,16 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::NetworkProxy, + key: "network_proxy", + stage: Stage::Experimental { + name: "Network proxy", + menu_description: "Start Codex's managed network proxy for sandboxed sessions. The active permissions profile still controls direct network access.", + announcement: "NEW: Network proxy can now be enabled from /experimental. Restart Codex after enabling it.", + }, + 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 55d2d8ba27..d1d7d3cae9 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -171,6 +171,19 @@ fn tool_suggest_is_stable_and_enabled_by_default() { assert_eq!(Feature::ToolSuggest.default_enabled(), true); } +#[test] +fn network_proxy_is_experimental_and_disabled_by_default() { + assert_eq!( + feature_for_key("network_proxy"), + Some(Feature::NetworkProxy) + ); + assert!(matches!( + Feature::NetworkProxy.stage(), + Stage::Experimental { .. } + )); + assert_eq!(Feature::NetworkProxy.default_enabled(), false); +} + #[test] fn tool_search_is_stable_and_enabled_by_default() { assert_eq!(Feature::ToolSearch.stage(), Stage::Stable);