Codex Handbook
core/src/config/network_proxy_spec.rs 377 lines
use codex_config::NetworkConstraints;use codex_execpolicy::Policy;use codex_network_proxy::BlockedRequestObserver;use codex_network_proxy::ConfigReloader;use codex_network_proxy::ConfigReloaderFuture;use codex_network_proxy::ConfigState;use codex_network_proxy::NetworkDecision;use codex_network_proxy::NetworkPolicyDecider;use codex_network_proxy::NetworkProxy;use codex_network_proxy::NetworkProxyAuditMetadata;use codex_network_proxy::NetworkProxyConfig;use codex_network_proxy::NetworkProxyConstraints;use codex_network_proxy::NetworkProxyHandle;use codex_network_proxy::NetworkProxyState;use codex_network_proxy::build_config_state;use codex_network_proxy::host_and_port_from_network_addr;use codex_network_proxy::normalize_host;use codex_network_proxy::validate_policy_against_constraints;use codex_protocol::models::PermissionProfile;use std::collections::HashSet;use std::sync::Arc;#[derive(Debug, Clone, PartialEq, Eq)]pub struct NetworkProxySpec {    base_config: NetworkProxyConfig,    requirements: Option<NetworkConstraints>,    config: NetworkProxyConfig,    constraints: NetworkProxyConstraints,    hard_deny_allowlist_misses: bool,}pub struct StartedNetworkProxy {    proxy: NetworkProxy,    _handle: NetworkProxyHandle,}impl StartedNetworkProxy {    fn new(proxy: NetworkProxy, handle: NetworkProxyHandle) -> Self {        Self {            proxy,            _handle: handle,        }    }    pub fn proxy(&self) -> NetworkProxy {        self.proxy.clone()    }}#[derive(Clone)]struct StaticNetworkProxyReloader {    state: ConfigState,}impl StaticNetworkProxyReloader {    fn new(state: ConfigState) -> Self {        Self { state }    }}impl ConfigReloader for StaticNetworkProxyReloader {    fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {        Box::pin(async { Ok(None) })    }    fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {        Box::pin(async { Ok(self.state.clone()) })    }    fn source_label(&self) -> String {        "StaticNetworkProxyReloader".to_string()    }}impl NetworkProxySpec {    pub(crate) fn enabled(&self) -> bool {        self.config.network.enabled    }    pub fn proxy_host_and_port(&self) -> String {        host_and_port_from_network_addr(&self.config.network.proxy_url, /*default_port*/ 3128)    }    pub fn socks_enabled(&self) -> bool {        self.config.network.enable_socks5    }    pub(crate) fn from_config_and_constraints(        config: NetworkProxyConfig,        requirements: Option<NetworkConstraints>,        permission_profile: &PermissionProfile,    ) -> std::io::Result<Self> {        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.as_ref() {            Self::apply_requirements(                config,                requirements,                permission_profile,                hard_deny_allowlist_misses,            )        } else {            (config, NetworkProxyConstraints::default())        };        validate_policy_against_constraints(&config, &constraints).map_err(|err| {            std::io::Error::new(                std::io::ErrorKind::InvalidInput,                format!("network proxy constraints are invalid: {err}"),            )        })?;        Ok(Self {            base_config,            requirements,            config,            constraints,            hard_deny_allowlist_misses,        })    }    pub async fn start_proxy(        &self,        permission_profile: &PermissionProfile,        policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,        blocked_request_observer: Option<Arc<dyn BlockedRequestObserver>>,        enable_network_approval_flow: bool,        audit_metadata: NetworkProxyAuditMetadata,    ) -> std::io::Result<StartedNetworkProxy> {        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 {            if let Some(policy_decider) = policy_decider {                builder = builder.policy_decider_arc(policy_decider);            } else if Self::managed_sandbox_active(permission_profile) {                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);        }        let proxy = builder.build().await.map_err(|err| {            std::io::Error::other(format!("failed to build network proxy: {err}"))        })?;        let handle = proxy            .run()            .await            .map_err(|err| std::io::Error::other(format!("failed to run network proxy: {err}")))?;        Ok(StartedNetworkProxy::new(proxy, handle))    }    pub(crate) fn recompute_for_permission_profile(        &self,        permission_profile: &PermissionProfile,    ) -> std::io::Result<Self> {        Self::from_config_and_constraints(            self.base_config.clone(),            self.requirements.clone(),            permission_profile,        )    }    pub(crate) fn with_exec_policy_network_rules(        &self,        exec_policy: &Policy,    ) -> std::io::Result<Self> {        let mut spec = self.clone();        apply_exec_policy_network_rules(&mut spec.config, exec_policy);        validate_policy_against_constraints(&spec.config, &spec.constraints).map_err(|err| {            std::io::Error::new(                std::io::ErrorKind::InvalidInput,                format!("network proxy constraints are invalid: {err}"),            )        })?;        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<NetworkProxyState> {        let state = self.build_config_state_for_spec()?;        let reloader = Arc::new(StaticNetworkProxyReloader::new(state.clone()));        Ok(NetworkProxyState::with_reloader_and_audit_metadata(            state,            reloader,            audit_metadata,        ))    }    fn build_config_state_for_spec(&self) -> std::io::Result<ConfigState> {        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,        permission_profile: &PermissionProfile,        hard_deny_allowlist_misses: bool,    ) -> (NetworkProxyConfig, NetworkProxyConstraints) {        let mut constraints = NetworkProxyConstraints::default();        let allowlist_expansion_enabled =            Self::allowlist_expansion_enabled(permission_profile, hard_deny_allowlist_misses);        let denylist_expansion_enabled = Self::denylist_expansion_enabled(permission_profile);        if let Some(enabled) = requirements.enabled {            config.network.enabled = enabled;            constraints.enabled = Some(enabled);        }        if let Some(http_port) = requirements.http_port {            config.network.proxy_url = format!("http://127.0.0.1:{http_port}");        }        if let Some(socks_port) = requirements.socks_port {            config.network.socks_url = format!("http://127.0.0.1:{socks_port}");        }        if let Some(allow_upstream_proxy) = requirements.allow_upstream_proxy {            config.network.allow_upstream_proxy = allow_upstream_proxy;            constraints.allow_upstream_proxy = Some(allow_upstream_proxy);        }        if let Some(dangerously_allow_non_loopback_proxy) =            requirements.dangerously_allow_non_loopback_proxy        {            config.network.dangerously_allow_non_loopback_proxy =                dangerously_allow_non_loopback_proxy;            constraints.dangerously_allow_non_loopback_proxy =                Some(dangerously_allow_non_loopback_proxy);        }        if let Some(dangerously_allow_all_unix_sockets) =            requirements.dangerously_allow_all_unix_sockets        {            config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets;            constraints.dangerously_allow_all_unix_sockets =                Some(dangerously_allow_all_unix_sockets);        }        let managed_allowed_domains = if hard_deny_allowlist_misses {            Some(                requirements                    .domains                    .as_ref()                    .and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)                    .unwrap_or_default(),            )        } else {            requirements                .domains                .as_ref()                .and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains)        };        if let Some(managed_allowed_domains) = managed_allowed_domains {            // Managed requirements seed the baseline allowlist. User additions            // can extend that baseline unless managed-only mode pins the            // effective allowlist to the managed set.            let effective_allowed_domains = if allowlist_expansion_enabled {                Self::merge_domain_lists(                    managed_allowed_domains.clone(),                    config.network.allowed_domains().as_deref().unwrap_or(&[]),                )            } else {                managed_allowed_domains.clone()            };            config                .network                .set_allowed_domains(effective_allowed_domains);            constraints.allowed_domains = Some(managed_allowed_domains);            constraints.allowlist_expansion_enabled = Some(allowlist_expansion_enabled);        }        let managed_denied_domains = requirements            .domains            .as_ref()            .and_then(codex_config::NetworkDomainPermissionsToml::denied_domains);        if let Some(managed_denied_domains) = managed_denied_domains {            let effective_denied_domains = if denylist_expansion_enabled {                Self::merge_domain_lists(                    managed_denied_domains.clone(),                    config.network.denied_domains().as_deref().unwrap_or(&[]),                )            } else {                managed_denied_domains.clone()            };            config.network.set_denied_domains(effective_denied_domains);            constraints.denied_domains = Some(managed_denied_domains);            constraints.denylist_expansion_enabled = Some(denylist_expansion_enabled);        }        if requirements.unix_sockets.is_some() {            let allow_unix_sockets = requirements                .unix_sockets                .as_ref()                .map(codex_config::NetworkUnixSocketPermissionsToml::allow_unix_sockets)                .unwrap_or_default();            config                .network                .set_allow_unix_sockets(allow_unix_sockets.clone());            constraints.allow_unix_sockets = Some(allow_unix_sockets);        }        if let Some(allow_local_binding) = requirements.allow_local_binding {            config.network.allow_local_binding = allow_local_binding;            constraints.allow_local_binding = Some(allow_local_binding);        }        (config, constraints)    }    fn allowlist_expansion_enabled(        permission_profile: &PermissionProfile,        hard_deny_allowlist_misses: bool,    ) -> bool {        Self::managed_sandbox_active(permission_profile) && !hard_deny_allowlist_misses    }    fn managed_allowed_domains_only(requirements: &NetworkConstraints) -> bool {        requirements.managed_allowed_domains_only.unwrap_or(false)    }    fn denylist_expansion_enabled(permission_profile: &PermissionProfile) -> bool {        Self::managed_sandbox_active(permission_profile)    }    fn managed_sandbox_active(permission_profile: &PermissionProfile) -> bool {        matches!(permission_profile, PermissionProfile::Managed { .. })    }    fn merge_domain_lists(mut managed: Vec<String>, user_entries: &[String]) -> Vec<String> {        for entry in user_entries {            if !managed                .iter()                .any(|managed_entry| managed_entry.eq_ignore_ascii_case(entry))            {                managed.push(entry.clone());            }        }        managed    }}fn apply_exec_policy_network_rules(config: &mut NetworkProxyConfig, exec_policy: &Policy) {    let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();    upsert_network_domains(config, allowed_domains, /*allow*/ true);    upsert_network_domains(config, denied_domains, /*allow*/ false);}fn upsert_network_domains(config: &mut NetworkProxyConfig, hosts: Vec<String>, allow: bool) {    let mut incoming = HashSet::new();    for host in hosts {        if incoming.insert(host.clone()) {            config.network.upsert_domain_permission(                host,                if allow {                    codex_network_proxy::NetworkDomainPermission::Allow                } else {                    codex_network_proxy::NetworkDomainPermission::Deny                },                normalize_host,            );        }    }}#[cfg(test)]#[path = "network_proxy_spec_tests.rs"]mod tests;