Codex Handbook
core/src/network_proxy_loader.rs 400 lines
use crate::config::find_codex_home;use crate::config::is_builtin_permission_profile_name;use crate::config::reject_unknown_builtin_permission_profile;use crate::config::resolve_permission_profile;use crate::exec_policy::ExecPolicyError;use crate::exec_policy::format_exec_policy_error_with_source;use crate::exec_policy::load_exec_policy;use anyhow::Context;use anyhow::Result;use codex_app_server_protocol::ConfigLayerSource;use codex_config::CONFIG_TOML_FILE;use codex_config::ConfigLayerStack;use codex_config::ConfigLayerStackOrdering;use codex_config::LoaderOverrides;use codex_config::loader::load_config_layers_state;use codex_config::merge_toml_values;use codex_config::permissions_toml::NetworkMitmActionToml;use codex_config::permissions_toml::NetworkMitmHookToml;use codex_config::permissions_toml::NetworkMitmToml;use codex_config::permissions_toml::NetworkToml;use codex_config::permissions_toml::PermissionsToml;use codex_config::permissions_toml::overlay_network_domain_permissions;use codex_exec_server::LOCAL_FS;use codex_network_proxy::ConfigReloader;use codex_network_proxy::ConfigReloaderFuture;use codex_network_proxy::ConfigState;use codex_network_proxy::NetworkMode;use codex_network_proxy::NetworkProxyConfig;use codex_network_proxy::NetworkProxyConstraintError;use codex_network_proxy::NetworkProxyConstraints;use codex_network_proxy::NetworkProxyState;use codex_network_proxy::build_config_state;use codex_network_proxy::normalize_host;use codex_network_proxy::validate_policy_against_constraints;use codex_utils_absolute_path::AbsolutePathBuf;use indexmap::IndexMap;use serde::Deserialize;use std::sync::Arc;use tokio::sync::RwLock;pub async fn build_network_proxy_state() -> Result<NetworkProxyState> {    let (state, reloader) = build_network_proxy_state_and_reloader().await?;    Ok(NetworkProxyState::with_reloader(state, Arc::new(reloader)))}pub async fn build_network_proxy_state_and_reloader() -> Result<(ConfigState, MtimeConfigReloader)>{    let (state, layer_mtimes) = build_config_state_with_mtimes().await?;    Ok((state, MtimeConfigReloader::new(layer_mtimes)))}async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime>)> {    let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;    let cli_overrides = Vec::new();    let overrides = LoaderOverrides::default();    let config_layer_stack = load_config_layers_state(        LOCAL_FS.as_ref(),        &codex_home,        /*cwd*/ None,        &cli_overrides,        overrides,        &codex_config::NoopThreadConfigLoader,    )    .await    .context("failed to load Codex config")?;    let (exec_policy, warning) = match load_exec_policy(&config_layer_stack).await {        Ok(policy) => (policy, None),        Err(err @ ExecPolicyError::ParsePolicy { .. }) => {            (codex_execpolicy::Policy::empty(), Some(err))        }        Err(err) => return Err(err.into()),    };    if let Some(err) = warning.as_ref() {        tracing::warn!(            "failed to parse execpolicy while building network proxy state: {}",            format_exec_policy_error_with_source(err)        );    }    let config = config_from_layers(&config_layer_stack, &exec_policy)?;    let constraints = enforce_trusted_constraints(&config_layer_stack, &config)?;    let layer_mtimes = collect_layer_mtimes(&config_layer_stack);    let state = build_config_state(config, constraints)?;    Ok((state, layer_mtimes))}fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec<LayerMtime> {    stack        .get_layers(            ConfigLayerStackOrdering::LowestPrecedenceFirst,            /*include_disabled*/ false,        )        .iter()        .filter_map(|layer| {            let path = match &layer.name {                ConfigLayerSource::System { file } => Some(file.clone()),                ConfigLayerSource::User { file, .. } => Some(file.clone()),                ConfigLayerSource::Project { dot_codex_folder } => {                    Some(dot_codex_folder.join(CONFIG_TOML_FILE))                }                ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.clone()),                _ => None,            };            path.map(LayerMtime::new)        })        .collect()}fn enforce_trusted_constraints(    layers: &ConfigLayerStack,    config: &NetworkProxyConfig,) -> Result<NetworkProxyConstraints> {    let constraints = network_constraints_from_trusted_layers(layers)?;    validate_policy_against_constraints(config, &constraints)        .map_err(NetworkProxyConstraintError::into_anyhow)        .context("network proxy constraints")?;    Ok(constraints)}fn network_constraints_from_trusted_layers(    layers: &ConfigLayerStack,) -> Result<NetworkProxyConstraints> {    let mut constraints = NetworkProxyConstraints::default();    let mut merged = toml::Value::Table(toml::map::Map::new());    for layer in layers.get_layers(        ConfigLayerStackOrdering::LowestPrecedenceFirst,        /*include_disabled*/ false,    ) {        if is_user_controlled_layer(&layer.name) {            continue;        }        merge_toml_values(&mut merged, &layer.config);    }    let parsed = network_tables_from_toml(&merged)?;    if let Some(network) = selected_network_from_tables(parsed)? {        apply_network_constraints(network, &mut constraints);    }    Ok(constraints)}fn apply_network_constraints(network: NetworkToml, constraints: &mut NetworkProxyConstraints) {    if let Some(enabled) = network.enabled {        constraints.enabled = Some(enabled);    }    if let Some(mode) = network.mode {        constraints.mode = Some(mode);    }    if let Some(allow_upstream_proxy) = network.allow_upstream_proxy {        constraints.allow_upstream_proxy = Some(allow_upstream_proxy);    }    if let Some(dangerously_allow_non_loopback_proxy) = network.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) = network.dangerously_allow_all_unix_sockets {        constraints.dangerously_allow_all_unix_sockets = Some(dangerously_allow_all_unix_sockets);    }    if let Some(domains) = network.domains.as_ref() {        let mut config = NetworkProxyConfig::default();        if let Some(allowed_domains) = constraints.allowed_domains.take() {            config.network.set_allowed_domains(allowed_domains);        }        if let Some(denied_domains) = constraints.denied_domains.take() {            config.network.set_denied_domains(denied_domains);        }        overlay_network_domain_permissions(&mut config, domains);        constraints.allowed_domains = config.network.allowed_domains();        constraints.denied_domains = config.network.denied_domains();    }    if let Some(unix_sockets) = network.unix_sockets.as_ref() {        let allow_unix_sockets = unix_sockets.allow_unix_sockets();        constraints.allow_unix_sockets = Some(allow_unix_sockets);    }    if let Some(allow_local_binding) = network.allow_local_binding {        constraints.allow_local_binding = Some(allow_local_binding);    }}#[derive(Debug, Clone, Default, Deserialize)]struct NetworkTablesToml {    default_permissions: Option<String>,    permissions: Option<PermissionsToml>,}fn network_tables_from_toml(value: &toml::Value) -> Result<NetworkTablesToml> {    value        .clone()        .try_into()        .context("failed to deserialize network tables from config")}fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result<Option<NetworkToml>> {    let Some(default_permissions) = parsed.default_permissions else {        return Ok(None);    };    if is_builtin_permission_profile_name(&default_permissions) {        return Ok(None);    }    reject_unknown_builtin_permission_profile(&default_permissions)?;    let permissions = parsed        .permissions        .context("default_permissions requires a `[permissions]` table for network settings")?;    let profile = resolve_permission_profile(&permissions, &default_permissions)        .map_err(anyhow::Error::from)?;    Ok(profile.network)}#[cfg(test)]fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesToml) -> Result<()> {    if let Some(network) = selected_network_from_tables(parsed)? {        network.apply_to_network_proxy_config(config);    }    Ok(())}#[derive(Default)]struct NetworkConfigAccumulator {    config: NetworkProxyConfig,    mitm_hooks: IndexMap<String, NetworkMitmHookToml>,    mitm_actions: IndexMap<String, NetworkMitmActionToml>,}impl NetworkConfigAccumulator {    fn apply_network_tables(&mut self, parsed: NetworkTablesToml) -> Result<()> {        if let Some(network) = selected_network_from_tables(parsed)? {            self.apply_network(network);        }        Ok(())    }    fn apply_network(&mut self, mut network: NetworkToml) {        let mitm = network.mitm.take();        network.apply_to_network_proxy_config(&mut self.config);        if let Some(mitm) = mitm {            if let Some(actions) = mitm.actions {                self.mitm_actions.extend(actions);            }            if let Some(hooks) = mitm.hooks {                self.mitm_hooks.extend(hooks);            }        }    }    fn finish(mut self) -> Result<NetworkProxyConfig> {        if !self.mitm_hooks.is_empty() {            let actions = self.mitm_actions;            let mitm = NetworkMitmToml {                hooks: Some(self.mitm_hooks),                actions: Some(actions.clone()),            };            mitm.validate_action_references(&actions)                .map_err(anyhow::Error::msg)?;            self.config.network.mitm_hooks = mitm.to_runtime_hooks(Some(&actions));        }        self.config.network.mitm = self.config.network.mode == NetworkMode::Limited            || !self.config.network.mitm_hooks.is_empty();        Ok(self.config)    }}fn config_from_layers(    layers: &ConfigLayerStack,    exec_policy: &codex_execpolicy::Policy,) -> Result<NetworkProxyConfig> {    let mut merged = toml::Value::Table(toml::map::Map::new());    for layer in layers.get_layers(        ConfigLayerStackOrdering::LowestPrecedenceFirst,        /*include_disabled*/ false,    ) {        merge_toml_values(&mut merged, &layer.config);    }    let parsed = network_tables_from_toml(&merged)?;    let mut accumulator = NetworkConfigAccumulator::default();    accumulator.apply_network_tables(parsed)?;    let mut config = accumulator.finish()?;    apply_exec_policy_network_rules(&mut config, exec_policy);    Ok(config)}fn apply_exec_policy_network_rules(    config: &mut NetworkProxyConfig,    exec_policy: &codex_execpolicy::Policy,) {    let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();    for host in allowed_domains {        upsert_network_domain(            config,            host,            codex_network_proxy::NetworkDomainPermission::Allow,        );    }    for host in denied_domains {        upsert_network_domain(            config,            host,            codex_network_proxy::NetworkDomainPermission::Deny,        );    }}fn upsert_network_domain(    config: &mut NetworkProxyConfig,    host: String,    permission: codex_network_proxy::NetworkDomainPermission,) {    config        .network        .upsert_domain_permission(host, permission, normalize_host);}fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {    matches!(        layer,        ConfigLayerSource::User { .. }            | ConfigLayerSource::Project { .. }            | ConfigLayerSource::SessionFlags    )}#[derive(Clone)]struct LayerMtime {    path: AbsolutePathBuf,    mtime: Option<std::time::SystemTime>,}impl LayerMtime {    fn new(path: AbsolutePathBuf) -> Self {        let mtime = path.metadata().and_then(|m| m.modified()).ok();        Self { path, mtime }    }}pub struct MtimeConfigReloader {    layer_mtimes: RwLock<Vec<LayerMtime>>,}impl MtimeConfigReloader {    fn new(layer_mtimes: Vec<LayerMtime>) -> Self {        Self {            layer_mtimes: RwLock::new(layer_mtimes),        }    }    async fn needs_reload(&self) -> bool {        let guard = self.layer_mtimes.read().await;        guard.iter().any(|layer| {            let metadata = std::fs::metadata(&layer.path).ok();            match (metadata.and_then(|m| m.modified().ok()), layer.mtime) {                (Some(new_mtime), Some(old_mtime)) => new_mtime > old_mtime,                (Some(_), None) => true,                (None, Some(_)) => true,                (None, None) => false,            }        })    }    async fn maybe_reload(&self) -> Result<Option<ConfigState>> {        if !self.needs_reload().await {            return Ok(None);        }        let (state, layer_mtimes) = build_config_state_with_mtimes().await?;        let mut guard = self.layer_mtimes.write().await;        *guard = layer_mtimes;        Ok(Some(state))    }    async fn reload_now(&self) -> Result<ConfigState> {        let (state, layer_mtimes) = build_config_state_with_mtimes().await?;        let mut guard = self.layer_mtimes.write().await;        *guard = layer_mtimes;        Ok(state)    }}impl ConfigReloader for MtimeConfigReloader {    fn source_label(&self) -> String {        "config layers".to_string()    }    fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {        Box::pin(MtimeConfigReloader::maybe_reload(self))    }    fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {        Box::pin(MtimeConfigReloader::reload_now(self))    }}#[cfg(test)]#[path = "network_proxy_loader_tests.rs"]mod tests;