diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index dce76bf3d2bb..18d689a52a16 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -828,6 +828,9 @@ pub(crate) struct Session { agent_status: watch::Sender, out_of_band_elicitation_paused: watch::Sender, state: Mutex, + /// Serializes rebuild/apply cycles for the running proxy; each cycle + /// rebuilds from the current SessionState while holding this lock. + managed_network_proxy_refresh_lock: Mutex<()>, /// The set of enabled features should be invariant for the lifetime of the /// session. features: ManagedFeatures, @@ -1344,6 +1347,48 @@ impl Session { Ok((network_proxy, session_network_proxy)) } + async fn refresh_managed_network_proxy_for_current_sandbox_policy(&self) { + let Some(started_proxy) = self.services.network_proxy.as_ref() else { + return; + }; + let _refresh_guard = self.managed_network_proxy_refresh_lock.lock().await; + let session_configuration = { + let state = self.state.lock().await; + state.session_configuration.clone() + }; + let Some(spec) = session_configuration + .original_config_do_not_use + .permissions + .network + .as_ref() + else { + return; + }; + + let spec = match spec + .recompute_for_sandbox_policy(session_configuration.sandbox_policy.get()) + { + Ok(spec) => spec, + Err(err) => { + warn!("failed to rebuild managed network proxy policy for sandbox change: {err}"); + return; + } + }; + let current_exec_policy = self.services.exec_policy.current(); + let spec = match spec.with_exec_policy_network_rules(current_exec_policy.as_ref()) { + Ok(spec) => spec, + Err(err) => { + warn!( + "failed to apply execpolicy network rules while refreshing managed network proxy: {err}" + ); + spec + } + }; + if let Err(err) = spec.apply_to_started_proxy(started_proxy).await { + warn!("failed to refresh managed network proxy for sandbox change: {err}"); + } + } + /// Don't expand the number of mutated arguments on config. We are in the process of getting rid of it. pub(crate) fn build_per_turn_config(session_configuration: &SessionConfiguration) -> Config { // todo(aibrahim): store this state somewhere else so we don't need to mut config @@ -2002,6 +2047,7 @@ impl Session { agent_status, out_of_band_elicitation_paused, state: Mutex::new(state), + managed_network_proxy_refresh_lock: Mutex::new(()), features: config.features.clone(), pending_mcp_server_refresh_config: Mutex::new(None), conversation: Arc::new(RealtimeConversationManager::new()), @@ -2421,6 +2467,8 @@ impl Session { match state.session_configuration.apply(&updates) { Ok(updated) => { let previous_cwd = state.session_configuration.cwd.clone(); + let sandbox_policy_changed = + state.session_configuration.sandbox_policy != updated.sandbox_policy; let next_cwd = updated.cwd.clone(); let codex_home = updated.codex_home.clone(); let session_source = updated.session_source.clone(); @@ -2433,6 +2481,10 @@ impl Session { &codex_home, &session_source, ); + if sandbox_policy_changed { + self.refresh_managed_network_proxy_for_current_sandbox_policy() + .await; + } Ok(()) } @@ -2519,6 +2571,8 @@ impl Session { .set_approval_policy(&session_configuration.approval_policy); if sandbox_policy_changed { + self.refresh_managed_network_proxy_for_current_sandbox_policy() + .await; let sandbox_state = SandboxState { sandbox_policy: per_turn_config.permissions.sandbox_policy.get().clone(), codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(), @@ -2954,6 +3008,7 @@ impl Session { amendment: &NetworkPolicyAmendment, network_approval_context: &NetworkApprovalContext, ) -> anyhow::Result<()> { + let _refresh_guard = self.managed_network_proxy_refresh_lock.lock().await; let host = Self::validated_network_policy_amendment_host(amendment, network_approval_context)?; let codex_home = self diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 6e11dd825a12..55b7eb9c0431 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -544,6 +544,139 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() Ok(()) } +#[tokio::test] +async fn managed_network_proxy_refreshes_when_sandbox_policy_changes() -> anyhow::Result<()> { + let spec = crate::config::NetworkProxySpec::from_config_and_constraints( + NetworkProxyConfig::default(), + Some(NetworkConstraints { + domains: Some(NetworkDomainPermissionsToml { + entries: std::collections::BTreeMap::from([( + "blocked.example.com".to_string(), + NetworkDomainPermissionToml::Deny, + )]), + }), + danger_full_access_denylist_only: Some(true), + allow_local_binding: Some(false), + ..Default::default() + }), + &SandboxPolicy::new_workspace_write_policy(), + )?; + let exec_policy = Policy::empty(); + + let (started_proxy, _) = Session::start_managed_network_proxy( + &spec, + &exec_policy, + &SandboxPolicy::new_workspace_write_policy(), + /*network_policy_decider*/ None, + /*blocked_request_observer*/ None, + /*managed_network_requirements_enabled*/ false, + crate::config::NetworkProxyAuditMetadata::default(), + ) + .await?; + + assert!(!started_proxy.proxy().allow_local_binding()); + let current_cfg = started_proxy.proxy().current_cfg().await?; + assert_eq!(current_cfg.network.allowed_domains(), None); + assert_eq!( + current_cfg.network.denied_domains(), + Some(vec!["blocked.example.com".to_string()]) + ); + + let spec = spec.recompute_for_sandbox_policy(&SandboxPolicy::DangerFullAccess)?; + spec.apply_to_started_proxy(&started_proxy).await?; + + assert!(started_proxy.proxy().allow_local_binding()); + let current_cfg = started_proxy.proxy().current_cfg().await?; + assert_eq!( + current_cfg.network.allowed_domains(), + Some(vec!["*".to_string()]) + ); + assert_eq!( + current_cfg.network.denied_domains(), + Some(vec!["blocked.example.com".to_string()]) + ); + + let spec = spec.recompute_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy())?; + spec.apply_to_started_proxy(&started_proxy).await?; + + assert!(!started_proxy.proxy().allow_local_binding()); + let current_cfg = started_proxy.proxy().current_cfg().await?; + assert_eq!(current_cfg.network.allowed_domains(), None); + assert_eq!( + current_cfg.network.denied_domains(), + Some(vec!["blocked.example.com".to_string()]) + ); + Ok(()) +} + +#[tokio::test] +async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::Result<()> { + let spec = crate::config::NetworkProxySpec::from_config_and_constraints( + NetworkProxyConfig::default(), + Some(NetworkConstraints { + enabled: Some(true), + danger_full_access_denylist_only: Some(true), + ..Default::default() + }), + &SandboxPolicy::DangerFullAccess, + )?; + let exec_policy = Policy::empty(); + let decider_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let network_policy_decider: Arc = Arc::new({ + let decider_calls = Arc::clone(&decider_calls); + move |_request| { + decider_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + async { codex_network_proxy::NetworkDecision::ask("not_allowed") } + } + }); + + let (started_proxy, _) = Session::start_managed_network_proxy( + &spec, + &exec_policy, + &SandboxPolicy::DangerFullAccess, + Some(network_policy_decider), + /*blocked_request_observer*/ None, + /*managed_network_requirements_enabled*/ true, + crate::config::NetworkProxyAuditMetadata::default(), + ) + .await?; + + let spec = spec.recompute_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy())?; + spec.apply_to_started_proxy(&started_proxy).await?; + let current_cfg = started_proxy.proxy().current_cfg().await?; + assert_eq!(current_cfg.network.allowed_domains(), None); + + use tokio::io::AsyncReadExt as _; + use tokio::io::AsyncWriteExt as _; + + let mut stream = tokio::net::TcpStream::connect(started_proxy.proxy().http_addr()).await?; + stream + .write_all( + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n", + ) + .await?; + let mut buffer = [0_u8; 4096]; + let bytes_read = tokio::time::timeout(StdDuration::from_secs(2), stream.read(&mut buffer)) + .await + .expect("timed out waiting for proxy response")?; + let response = String::from_utf8_lossy(&buffer[..bytes_read]); + + assert!( + response.starts_with("HTTP/1.1 403 Forbidden"), + "unexpected proxy response: {response}" + ); + assert!( + response.contains("x-proxy-error: blocked-by-allowlist"), + "unexpected proxy response: {response}" + ); + assert_eq!( + decider_calls.load(std::sync::atomic::Ordering::SeqCst), + 1, + "unexpected proxy response: {response}" + ); + Ok(()) +} + #[tokio::test] async fn get_base_instructions_no_user_content() { let prompt_with_apply_patch_instructions = @@ -2824,6 +2957,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { agent_status: agent_status_tx, out_of_band_elicitation_paused: watch::channel(false).0, state: Mutex::new(state), + managed_network_proxy_refresh_lock: Mutex::new(()), features: config.features.clone(), pending_mcp_server_refresh_config: Mutex::new(None), conversation: Arc::new(RealtimeConversationManager::new()), @@ -3666,6 +3800,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( agent_status: agent_status_tx, out_of_band_elicitation_paused: watch::channel(false).0, state: Mutex::new(state), + managed_network_proxy_refresh_lock: Mutex::new(()), features: config.features.clone(), pending_mcp_server_refresh_config: Mutex::new(None), conversation: Arc::new(RealtimeConversationManager::new()), diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index 0a37f46cdfb8..b67c41a44279 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -24,6 +24,8 @@ const GLOBAL_ALLOWLIST_PATTERN: &str = "*"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct NetworkProxySpec { + base_config: NetworkProxyConfig, + requirements: Option, config: NetworkProxyConfig, constraints: NetworkProxyConstraints, hard_deny_allowlist_misses: bool, @@ -91,13 +93,14 @@ impl NetworkProxySpec { requirements: Option, sandbox_policy: &SandboxPolicy, ) -> std::io::Result { + let base_config = config.clone(); let hard_deny_allowlist_misses = requirements .as_ref() .is_some_and(Self::managed_allowed_domains_only); - let (config, constraints) = if let Some(requirements) = requirements { + let (config, constraints) = if let Some(requirements) = requirements.as_ref() { Self::apply_requirements( config, - &requirements, + requirements, sandbox_policy, hard_deny_allowlist_misses, ) @@ -111,6 +114,8 @@ impl NetworkProxySpec { ) })?; Ok(Self { + base_config, + requirements, config, constraints, hard_deny_allowlist_misses, @@ -127,21 +132,16 @@ impl NetworkProxySpec { ) -> std::io::Result { let state = self.build_state_with_audit_metadata(audit_metadata)?; let mut builder = NetworkProxy::builder().state(Arc::new(state)); - if enable_network_approval_flow - && !self.hard_deny_allowlist_misses - && matches!( + if enable_network_approval_flow && !self.hard_deny_allowlist_misses { + if let Some(policy_decider) = policy_decider { + builder = builder.policy_decider_arc(policy_decider); + } else if matches!( sandbox_policy, SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } - ) - { - builder = match policy_decider { - Some(policy_decider) => builder.policy_decider_arc(policy_decider), - None => builder.policy_decider(|_request| async { - // In restricted sandbox modes, allowlist misses should ask for - // explicit network approval instead of hard-denying. - NetworkDecision::ask("not_allowed") - }), - }; + ) { + builder = builder + .policy_decider(|_request| async { NetworkDecision::ask("not_allowed") }); + } } if let Some(blocked_request_observer) = blocked_request_observer { builder = builder.blocked_request_observer_arc(blocked_request_observer); @@ -156,6 +156,17 @@ impl NetworkProxySpec { Ok(StartedNetworkProxy::new(proxy, handle)) } + pub(crate) fn recompute_for_sandbox_policy( + &self, + sandbox_policy: &SandboxPolicy, + ) -> std::io::Result { + Self::from_config_and_constraints( + self.base_config.clone(), + self.requirements.clone(), + sandbox_policy, + ) + } + pub(crate) fn with_exec_policy_network_rules( &self, exec_policy: &Policy, @@ -171,14 +182,25 @@ impl NetworkProxySpec { Ok(spec) } + pub(crate) async fn apply_to_started_proxy( + &self, + started_proxy: &StartedNetworkProxy, + ) -> std::io::Result<()> { + let state = self.build_config_state_for_spec()?; + started_proxy + .proxy() + .replace_config_state(state) + .await + .map_err(|err| { + std::io::Error::other(format!("failed to update network proxy state: {err}")) + }) + } + fn build_state_with_audit_metadata( &self, audit_metadata: NetworkProxyAuditMetadata, ) -> std::io::Result { - let state = - build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| { - std::io::Error::other(format!("failed to build network proxy state: {err}")) - })?; + let state = self.build_config_state_for_spec()?; let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone())); Ok(NetworkProxyState::with_reloader_and_audit_metadata( state, @@ -187,6 +209,12 @@ impl NetworkProxySpec { )) } + fn build_config_state_for_spec(&self) -> std::io::Result { + build_config_state(self.config.clone(), self.constraints.clone()).map_err(|err| { + std::io::Error::other(format!("failed to build network proxy state: {err}")) + }) + } + fn apply_requirements( mut config: NetworkProxyConfig, requirements: &NetworkConstraints, diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index 742512d600b6..ff351d1e7399 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -21,6 +21,8 @@ fn domain_permissions( #[test] fn build_state_with_audit_metadata_threads_metadata_to_state() { let spec = NetworkProxySpec { + base_config: NetworkProxyConfig::default(), + requirements: None, config: NetworkProxyConfig::default(), constraints: NetworkProxyConstraints::default(), hard_deny_allowlist_misses: false, @@ -322,6 +324,55 @@ fn danger_full_access_denylist_only_does_not_change_workspace_write_behavior() { assert_eq!(spec.constraints.denylist_expansion_enabled, Some(true)); } +#[test] +fn recompute_for_sandbox_policy_rebuilds_denylist_only_full_access_policy() { + let requirements = NetworkConstraints { + domains: Some(domain_permissions([( + "blocked.example.com", + NetworkDomainPermissionToml::Deny, + )])), + danger_full_access_denylist_only: Some(true), + ..Default::default() + }; + let spec = NetworkProxySpec::from_config_and_constraints( + NetworkProxyConfig::default(), + Some(requirements), + &SandboxPolicy::new_workspace_write_policy(), + ) + .expect("workspace-write policy should load"); + + assert_eq!(spec.config.network.allowed_domains(), None); + assert_eq!( + spec.config.network.denied_domains(), + Some(vec!["blocked.example.com".to_string()]) + ); + + let spec = spec + .recompute_for_sandbox_policy(&SandboxPolicy::DangerFullAccess) + .expect("full-access policy should load"); + + assert_eq!( + spec.config.network.allowed_domains(), + Some(vec!["*".to_string()]) + ); + assert_eq!( + spec.config.network.denied_domains(), + Some(vec!["blocked.example.com".to_string()]) + ); + assert!(spec.config.network.allow_local_binding); + + let spec = spec + .recompute_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()) + .expect("workspace-write policy should reload"); + + assert_eq!(spec.config.network.allowed_domains(), None); + assert_eq!( + spec.config.network.denied_domains(), + Some(vec!["blocked.example.com".to_string()]) + ); + assert!(!spec.config.network.allow_local_binding); +} + #[test] fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() { let mut config = NetworkProxyConfig::default(); diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index d3c526e99e93..ecc5fb6daae5 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -19,6 +19,7 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ReviewDecision; +use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::WarningEvent; use indexmap::IndexMap; use std::collections::HashMap; @@ -118,6 +119,13 @@ fn allows_network_approval_flow(policy: AskForApproval) -> bool { !matches!(policy, AskForApproval::Never) } +fn sandbox_policy_allows_network_approval_flow(policy: &SandboxPolicy) -> bool { + matches!( + policy, + SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } + ) +} + impl PendingApprovalDecision { fn to_network_decision(self) -> NetworkDecision { match self { @@ -334,6 +342,16 @@ impl NetworkApprovalService { .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); }; + if !sandbox_policy_allows_network_approval_flow(turn_context.sandbox_policy.get()) { + pending.set_decision(PendingApprovalDecision::Deny).await; + let mut pending_approvals = self.pending_host_approvals.lock().await; + pending_approvals.remove(&key); + self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( + policy_denial_message, + )) + .await; + return NetworkDecision::deny(REASON_NOT_ALLOWED); + } if !allows_network_approval_flow(turn_context.approval_policy.value()) { pending.set_decision(PendingApprovalDecision::Deny).await; let mut pending_approvals = self.pending_host_approvals.lock().await; diff --git a/codex-rs/core/src/tools/network_approval_tests.rs b/codex-rs/core/src/tools/network_approval_tests.rs index ad01a45bbd38..ca777e7cd462 100644 --- a/codex-rs/core/src/tools/network_approval_tests.rs +++ b/codex-rs/core/src/tools/network_approval_tests.rs @@ -1,6 +1,7 @@ use super::*; use codex_network_proxy::BlockedRequestArgs; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::SandboxPolicy; use pretty_assertions::assert_eq; #[tokio::test] @@ -179,6 +180,19 @@ fn only_never_policy_disables_network_approval_flow() { assert!(allows_network_approval_flow(AskForApproval::UnlessTrusted)); } +#[test] +fn network_approval_flow_is_limited_to_restricted_sandbox_modes() { + assert!(sandbox_policy_allows_network_approval_flow( + &SandboxPolicy::new_read_only_policy() + )); + assert!(sandbox_policy_allows_network_approval_flow( + &SandboxPolicy::new_workspace_write_policy() + )); + assert!(!sandbox_policy_allows_network_approval_flow( + &SandboxPolicy::DangerFullAccess + )); +} + fn denied_blocked_request(host: &str) -> BlockedRequest { BlockedRequest::new(BlockedRequestArgs { host: host.to_string(), diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index c334e817d081..0270966d4414 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -2,6 +2,7 @@ use crate::config; use crate::http_proxy; use crate::network_policy::NetworkPolicyDecider; use crate::runtime::BlockedRequestObserver; +use crate::runtime::ConfigState; use crate::runtime::unix_socket_permissions_supported; use crate::socks5; use crate::state::NetworkProxyState; @@ -13,6 +14,7 @@ use std::net::SocketAddr; use std::net::TcpListener as StdTcpListener; use std::sync::Arc; use std::sync::Mutex; +use std::sync::RwLock; use tokio::task::JoinHandle; use tracing::warn; @@ -219,11 +221,9 @@ impl NetworkProxyBuilder { http_addr, socks_addr, socks_enabled: current_cfg.network.enable_socks5, - allow_local_binding: current_cfg.network.allow_local_binding, - allow_unix_sockets: current_cfg.network.allow_unix_sockets(), - dangerously_allow_all_unix_sockets: current_cfg - .network - .dangerously_allow_all_unix_sockets, + runtime_settings: Arc::new(RwLock::new(NetworkProxyRuntimeSettings::from_config( + ¤t_cfg, + ))), reserved_listeners, policy_decider: self.policy_decider, }) @@ -294,15 +294,30 @@ fn reserve_loopback_ephemeral_listener() -> Result { .context("bind loopback ephemeral port") } +#[derive(Debug, Clone, PartialEq, Eq)] +struct NetworkProxyRuntimeSettings { + allow_local_binding: bool, + allow_unix_sockets: Arc<[String]>, + dangerously_allow_all_unix_sockets: bool, +} + +impl NetworkProxyRuntimeSettings { + fn from_config(config: &config::NetworkProxyConfig) -> Self { + Self { + allow_local_binding: config.network.allow_local_binding, + allow_unix_sockets: config.network.allow_unix_sockets().into(), + dangerously_allow_all_unix_sockets: config.network.dangerously_allow_all_unix_sockets, + } + } +} + #[derive(Clone)] pub struct NetworkProxy { state: Arc, http_addr: SocketAddr, socks_addr: SocketAddr, socks_enabled: bool, - allow_local_binding: bool, - allow_unix_sockets: Vec, - dangerously_allow_all_unix_sockets: bool, + runtime_settings: Arc>, reserved_listeners: Option>, policy_decider: Option>, } @@ -322,7 +337,7 @@ impl PartialEq for NetworkProxy { fn eq(&self, other: &Self) -> bool { self.http_addr == other.http_addr && self.socks_addr == other.socks_addr - && self.allow_local_binding == other.allow_local_binding + && self.runtime_settings() == other.runtime_settings() } } @@ -488,18 +503,19 @@ impl NetworkProxy { } pub fn allow_local_binding(&self) -> bool { - self.allow_local_binding + self.runtime_settings().allow_local_binding } - pub fn allow_unix_sockets(&self) -> &[String] { - &self.allow_unix_sockets + pub fn allow_unix_sockets(&self) -> Arc<[String]> { + self.runtime_settings().allow_unix_sockets } pub fn dangerously_allow_all_unix_sockets(&self) -> bool { - self.dangerously_allow_all_unix_sockets + self.runtime_settings().dangerously_allow_all_unix_sockets } pub fn apply_to_env(&self, env: &mut HashMap) { + let allow_local_binding = self.allow_local_binding(); // Enforce proxying for child processes. We intentionally override existing values so // command-level environment cannot bypass the managed proxy endpoint. apply_proxy_env_overrides( @@ -507,10 +523,50 @@ impl NetworkProxy { self.http_addr, self.socks_addr, self.socks_enabled, - self.allow_local_binding, + allow_local_binding, ); } + pub async fn replace_config_state(&self, new_state: ConfigState) -> Result<()> { + let current_cfg = self.state.current_cfg().await?; + anyhow::ensure!( + new_state.config.network.enabled == current_cfg.network.enabled, + "cannot update network.enabled on a running proxy" + ); + anyhow::ensure!( + new_state.config.network.proxy_url == current_cfg.network.proxy_url, + "cannot update network.proxy_url on a running proxy" + ); + anyhow::ensure!( + new_state.config.network.socks_url == current_cfg.network.socks_url, + "cannot update network.socks_url on a running proxy" + ); + anyhow::ensure!( + new_state.config.network.enable_socks5 == current_cfg.network.enable_socks5, + "cannot update network.enable_socks5 on a running proxy" + ); + anyhow::ensure!( + new_state.config.network.enable_socks5_udp == current_cfg.network.enable_socks5_udp, + "cannot update network.enable_socks5_udp on a running proxy" + ); + + let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config); + self.state.replace_config_state(new_state).await?; + let mut guard = self + .runtime_settings + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *guard = settings; + Ok(()) + } + + fn runtime_settings(&self) -> NetworkProxyRuntimeSettings { + self.runtime_settings + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } + pub async fn run(&self) -> Result { let current_cfg = self.state.current_cfg().await?; if !current_cfg.network.enabled { diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index e96783256498..da090a69d40d 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -335,6 +335,17 @@ impl NetworkProxyState { } } + pub async fn replace_config_state(&self, mut new_state: ConfigState) -> Result<()> { + self.reload_if_needed().await?; + let mut guard = self.state.write().await; + log_policy_changes(&guard.config, &new_state.config); + new_state.blocked = guard.blocked.clone(); + new_state.blocked_total = guard.blocked_total; + *guard = new_state; + info!("updated network proxy config state"); + Ok(()) + } + pub async fn host_blocked(&self, host: &str, port: u16) -> Result { self.reload_if_needed().await?; let host = match Host::parse(host) {