diff --git a/codex-rs/config/src/permissions_toml.rs b/codex-rs/config/src/permissions_toml.rs index 92bdcfcbb353..75f28185b515 100644 --- a/codex-rs/config/src/permissions_toml.rs +++ b/codex-rs/config/src/permissions_toml.rs @@ -453,29 +453,6 @@ impl NetworkMitmToml { Ok(()) } - pub fn validate_action_references( - &self, - actions_by_name: &IndexMap, - ) -> Result<(), String> { - self.validate_action_definitions()?; - - let Some(hooks) = self.hooks.as_ref() else { - return Ok(()); - }; - - for (hook_name, hook) in hooks { - for action_name in &hook.action { - if !actions_by_name.contains_key(action_name) { - return Err(format!( - "network.mitm.hooks.{hook_name}.action references undefined action `{action_name}`" - )); - } - } - } - - Ok(()) - } - pub fn to_runtime_hooks( &self, actions_by_name: Option<&IndexMap>, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 8b9548a1b15e..336b7464dbb0 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -173,8 +173,6 @@ use permission_profile_catalog::permission_profile_catalog_from_permissions; use permission_profile_catalog::permission_profile_is_allowed; use permission_profile_catalog::validate_permission_profile_for_deny_read; pub(crate) use permissions::is_builtin_permission_profile_name; -pub(crate) use permissions::reject_unknown_builtin_permission_profile; -pub(crate) use permissions::resolve_permission_profile; pub use resolved_permission_profile::PermissionProfileSnapshot; pub(crate) use resolved_permission_profile::PermissionProfileState; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 25560e46c773..0341090b20be 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -59,11 +59,7 @@ mod mcp_skill_dependencies; mod mcp_tool_approval_templates; mod mcp_tool_exposure; mod network_policy_decision; -pub(crate) mod network_proxy_loader; pub use mcp::McpManager; -pub use network_proxy_loader::MtimeConfigReloader; -pub use network_proxy_loader::build_network_proxy_state; -pub use network_proxy_loader::build_network_proxy_state_and_reloader; mod original_image_detail; pub use codex_mcp::CodexAppsToolsCache; pub use codex_mcp::SandboxState; diff --git a/codex-rs/core/src/network_proxy_loader.rs b/codex-rs/core/src/network_proxy_loader.rs deleted file mode 100644 index 049f55c40da4..000000000000 --- a/codex-rs/core/src/network_proxy_loader.rs +++ /dev/null @@ -1,397 +0,0 @@ -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::format_exec_policy_error_with_source; -use crate::exec_policy::load_exec_policy_with_warning; -use anyhow::Context; -use anyhow::Result; -use codex_config::CONFIG_TOML_FILE; -use codex_config::ConfigLayerSource; -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 { - 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)> { - 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 layer_mtimes = collect_layer_mtimes(&config_layer_stack); - let state = build_config_state_from_layers(&config_layer_stack).await?; - Ok((state, layer_mtimes)) -} - -async fn build_config_state_from_layers( - config_layer_stack: &ConfigLayerStack, -) -> Result { - let (exec_policy, warning) = load_exec_policy_with_warning(config_layer_stack).await?; - 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)?; - build_config_state(config, constraints) -} - -fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec { - 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 { - 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 { - 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.set_allowed_domains(allowed_domains); - } - if let Some(denied_domains) = constraints.denied_domains.take() { - config.set_denied_domains(denied_domains); - } - overlay_network_domain_permissions(&mut config, domains); - constraints.allowed_domains = config.allowed_domains(); - constraints.denied_domains = config.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, - permissions: Option, -} - -fn network_tables_from_toml(value: &toml::Value) -> Result { - value - .clone() - .try_into() - .context("failed to deserialize network tables from config") -} - -fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result> { - 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, - mitm_actions: IndexMap, -} - -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 { - 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.mitm_hooks = mitm.to_runtime_hooks(Some(&actions)); - } - - self.config.mitm = - self.config.mode == NetworkMode::Limited || !self.config.mitm_hooks.is_empty(); - Ok(self.config) - } -} - -fn config_from_layers( - layers: &ConfigLayerStack, - exec_policy: &codex_execpolicy::Policy, -) -> Result { - 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.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, -} - -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>, -} - -impl MtimeConfigReloader { - fn new(layer_mtimes: Vec) -> 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> { - 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 { - 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> { - 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; diff --git a/codex-rs/core/src/network_proxy_loader_tests.rs b/codex-rs/core/src/network_proxy_loader_tests.rs deleted file mode 100644 index f2758e894613..000000000000 --- a/codex-rs/core/src/network_proxy_loader_tests.rs +++ /dev/null @@ -1,704 +0,0 @@ -use super::*; - -use codex_config::ConfigLayerEntry; -use codex_config::ConfigLayerSource; -use codex_config::ConfigLayerStack; -use codex_config::ConfigRequirements; -use codex_config::ConfigRequirementsToml; -use codex_config::RequirementSource; -use codex_config::RequirementsExecPolicy; -use codex_config::Sourced; -use codex_config::permissions_toml::NetworkDomainPermissionToml; -use codex_config::permissions_toml::NetworkDomainPermissionsToml; -use codex_execpolicy::Decision; -use codex_execpolicy::NetworkRuleProtocol; -use codex_execpolicy::Policy; -use pretty_assertions::assert_eq; -use std::collections::BTreeMap; -use std::fs; -use tempfile::tempdir; - -#[test] -fn higher_precedence_profile_network_overlays_domain_entries() { - let lower_network: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] - -[permissions.dev.network.domains] -"lower.example.com" = "allow" -"blocked.example.com" = "deny" -"#, - ) - .expect("lower layer should parse"); - let higher_network: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] - -[permissions.dev.network.domains] -"higher.example.com" = "allow" -"#, - ) - .expect("higher layer should parse"); - - let mut config = NetworkProxyConfig::default(); - apply_network_tables( - &mut config, - network_tables_from_toml(&lower_network).expect("lower layer should deserialize"), - ) - .expect("lower layer should apply"); - apply_network_tables( - &mut config, - network_tables_from_toml(&higher_network).expect("higher layer should deserialize"), - ) - .expect("higher layer should apply"); - - assert_eq!( - config.allowed_domains(), - Some(vec![ - "lower.example.com".to_string(), - "higher.example.com".to_string() - ]) - ); - assert_eq!( - config.denied_domains(), - Some(vec!["blocked.example.com".to_string()]) - ); -} - -#[test] -fn higher_precedence_profile_network_overrides_matching_domain_entries() { - let lower_network: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] - -[permissions.dev.network.domains] -"shared.example.com" = "deny" -"other.example.com" = "allow" -"#, - ) - .expect("lower layer should parse"); - let higher_network: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] - -[permissions.dev.network.domains] -"shared.example.com" = "allow" -"#, - ) - .expect("higher layer should parse"); - - let mut config = NetworkProxyConfig::default(); - apply_network_tables( - &mut config, - network_tables_from_toml(&lower_network).expect("lower layer should deserialize"), - ) - .expect("lower layer should apply"); - apply_network_tables( - &mut config, - network_tables_from_toml(&higher_network).expect("higher layer should deserialize"), - ) - .expect("higher layer should apply"); - - assert_eq!( - config.allowed_domains(), - Some(vec![ - "other.example.com".to_string(), - "shared.example.com".to_string() - ]) - ); - assert_eq!(config.denied_domains(), None); -} - -#[test] -fn higher_precedence_profile_network_overrides_named_mitm_actions() { - let lower_network: toml::Value = toml::from_str( - r#" -default_permissions = "workspace" - -[permissions.workspace.network] -mode = "full" - -[permissions.workspace.network.domains] -"lower.example.com" = "allow" - -[permissions.workspace.network.mitm.hooks.github_write] -host = "api.github.com" -methods = ["POST"] -path_prefixes = ["/repos/openai/"] -action = ["strip_auth"] - -[permissions.workspace.network.mitm.actions.strip_auth] -strip_request_headers = ["authorization"] -"#, - ) - .expect("lower layer should parse"); - let higher_network: toml::Value = toml::from_str( - r#" -default_permissions = "workspace" - -[permissions.workspace.network] -mode = "full" - -[permissions.workspace.network.domains] -"higher.example.com" = "allow" - -[permissions.workspace.network.mitm.actions.strip_auth] -strip_request_headers = ["x-api-key"] -"#, - ) - .expect("higher layer should parse"); - - let mut accumulator = NetworkConfigAccumulator::default(); - accumulator - .apply_network_tables( - network_tables_from_toml(&lower_network).expect("lower layer should deserialize"), - ) - .expect("lower layer should apply"); - accumulator - .apply_network_tables( - network_tables_from_toml(&higher_network).expect("higher layer should deserialize"), - ) - .expect("higher layer should apply"); - let config = accumulator.finish().expect("merged config should build"); - - assert_eq!(config.mode, codex_network_proxy::NetworkMode::Full); - assert!(config.mitm); - assert_eq!( - config.allowed_domains(), - Some(vec![ - "lower.example.com".to_string(), - "higher.example.com".to_string() - ]) - ); - assert_eq!(config.mitm_hooks.len(), 1); - assert_eq!(config.mitm_hooks[0].host, "api.github.com"); - assert_eq!(config.mitm_hooks[0].matcher.methods, vec!["POST"]); - assert_eq!( - config.mitm_hooks[0].actions.strip_request_headers, - vec!["x-api-key"] - ); -} - -#[test] -fn execpolicy_network_rules_overlay_network_lists() { - let mut config = NetworkProxyConfig::default(); - config.set_allowed_domains(vec!["config.example.com".to_string()]); - config.set_denied_domains(vec!["blocked.example.com".to_string()]); - - let mut exec_policy = Policy::empty(); - exec_policy - .add_network_rule( - "blocked.example.com", - NetworkRuleProtocol::Https, - Decision::Allow, - /*justification*/ None, - ) - .expect("allow rule should be valid"); - exec_policy - .add_network_rule( - "api.example.com", - NetworkRuleProtocol::Http, - Decision::Forbidden, - /*justification*/ None, - ) - .expect("deny rule should be valid"); - - apply_exec_policy_network_rules(&mut config, &exec_policy); - - assert_eq!( - config.allowed_domains(), - Some(vec![ - "config.example.com".to_string(), - "blocked.example.com".to_string() - ]) - ); - assert_eq!( - config.denied_domains(), - Some(vec!["api.example.com".to_string()]) - ); -} - -#[tokio::test] -async fn malformed_custom_rules_preserve_managed_denied_domain() { - let temp_dir = tempdir().expect("create temp dir"); - let policy_dir = temp_dir.path().join("rules"); - fs::create_dir_all(&policy_dir).expect("create policy dir"); - fs::write(policy_dir.join("broken.rules"), "prefix_rule(").expect("write malformed policy"); - - let mut requirements_exec_policy = Policy::empty(); - requirements_exec_policy - .add_network_rule( - "blocked.example.com", - NetworkRuleProtocol::Https, - Decision::Forbidden, - /*justification*/ None, - ) - .expect("managed network rule should be valid"); - let requirements = ConfigRequirements { - exec_policy: Some(Sourced::new( - RequirementsExecPolicy::new(requirements_exec_policy), - RequirementSource::Unknown, - )), - ..ConfigRequirements::default() - }; - let dot_codex_folder = AbsolutePathBuf::from_absolute_path(temp_dir.path()) - .expect("dot codex folder should be absolute"); - let layers = ConfigLayerStack::new( - vec![ConfigLayerEntry::new( - ConfigLayerSource::Project { dot_codex_folder }, - toml::Value::Table(Default::default()), - )], - requirements, - ConfigRequirementsToml::default(), - ) - .expect("layer stack should be valid"); - - let state = build_config_state_from_layers(&layers) - .await - .expect("proxy state should tolerate malformed custom rules"); - - assert_eq!( - state.config.denied_domains(), - Some(vec!["blocked.example.com".to_string()]) - ); -} - -#[test] -fn apply_network_constraints_includes_allow_all_unix_sockets_flag() { - let config: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] -dangerously_allow_all_unix_sockets = true -"#, - ) - .expect("permissions profile should parse"); - let network = selected_network_from_tables( - network_tables_from_toml(&config).expect("permissions profile should deserialize"), - ) - .expect("permissions profile should select a network table") - .expect("network table should be present"); - - let mut constraints = NetworkProxyConstraints::default(); - apply_network_constraints(network, &mut constraints); - - assert_eq!(constraints.dangerously_allow_all_unix_sockets, Some(true)); -} - -#[test] -fn selected_network_from_tables_ignores_builtin_profile_without_permissions_table() { - let config: toml::Value = toml::from_str( - r#" -default_permissions = ":workspace" -"#, - ) - .expect("built-in profile config should parse"); - - let network = selected_network_from_tables( - network_tables_from_toml(&config).expect("built-in profile config should deserialize"), - ) - .expect("built-in profile selection should not require permissions tables"); - - assert_eq!(network, None); -} - -#[test] -fn selected_network_from_tables_rejects_unknown_builtin_profile_without_permissions_table() { - let config: toml::Value = toml::from_str( - r#" -default_permissions = ":unknown" -"#, - ) - .expect("unknown built-in config should parse"); - - let err = selected_network_from_tables( - network_tables_from_toml(&config).expect("unknown built-in config should deserialize"), - ) - .expect_err("unknown built-in profile should be rejected"); - - assert_eq!( - err.to_string(), - "default_permissions refers to unknown built-in profile `:unknown`" - ); -} - -#[test] -fn selected_network_from_tables_resolves_builtin_workspace_parent() { - let config: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev] -extends = ":workspace" - -[permissions.dev.network] -enabled = true - -[permissions.dev.network.domains] -"child.example.com" = "allow" -"#, - ) - .expect("dev extension config should parse"); - - let network = selected_network_from_tables( - network_tables_from_toml(&config).expect("dev extension config should deserialize"), - ) - .expect("dev extension should resolve") - .expect("dev extension should expose child network config"); - - assert_eq!( - network, - NetworkToml { - enabled: Some(true), - domains: Some(NetworkDomainPermissionsToml { - entries: BTreeMap::from([( - "child.example.com".to_string(), - NetworkDomainPermissionToml::Allow, - )]), - }), - ..Default::default() - } - ); -} - -#[test] -fn selected_network_from_tables_resolves_permission_profile_inheritance() { - let config: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.base.network] -enabled = true -dangerously_allow_all_unix_sockets = true - -[permissions.base.network.domains] -"base.example.com" = "allow" -"shared.example.com" = "deny" - -[permissions.dev] -extends = "base" - -[permissions.dev.network] -allow_local_binding = true - -[permissions.dev.network.domains] -"child.example.com" = "allow" -"shared.example.com" = "allow" -"#, - ) - .expect("permissions profiles should parse"); - - let network = selected_network_from_tables( - network_tables_from_toml(&config).expect("permissions profiles should deserialize"), - ) - .expect("permissions profiles should select a network table") - .expect("network table should be present"); - - assert_eq!( - network, - NetworkToml { - enabled: Some(true), - dangerously_allow_all_unix_sockets: Some(true), - allow_local_binding: Some(true), - domains: Some(NetworkDomainPermissionsToml { - entries: BTreeMap::from([ - ( - "base.example.com".to_string(), - NetworkDomainPermissionToml::Allow, - ), - ( - "child.example.com".to_string(), - NetworkDomainPermissionToml::Allow, - ), - ( - "shared.example.com".to_string(), - NetworkDomainPermissionToml::Allow, - ), - ]), - }), - ..Default::default() - } - ); -} - -#[test] -fn config_from_layers_resolves_inherited_profiles_across_layers() { - let lower_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::toml! { - [permissions.base.network.domains] - "base.example.com" = "allow" - } - .into(), - ); - let higher_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::toml! { - default_permissions = "dev" - - [permissions.dev] - extends = "base" - - [permissions.dev.network.domains] - "child.example.com" = "allow" - } - .into(), - ); - let layers = ConfigLayerStack::new( - vec![lower_layer, higher_layer], - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - ) - .expect("layer stack should be valid"); - - let config = - config_from_layers(&layers, &Policy::empty()).expect("inherited profiles should load"); - - assert_eq!( - config.allowed_domains(), - Some(vec![ - "base.example.com".to_string(), - "child.example.com".to_string(), - ]) - ); -} - -#[test] -fn config_from_layers_normalizes_profile_network_domains_before_merging_layers() { - let lower_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::toml! { - default_permissions = "dev" - - [permissions.dev.network.domains] - "example.com" = "deny" - } - .into(), - ); - let higher_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::toml! { - [permissions.dev.network.domains] - "EXAMPLE.COM" = "allow" - } - .into(), - ); - let layers = ConfigLayerStack::new( - vec![lower_layer, higher_layer], - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - ) - .expect("layer stack should be valid"); - - let config = config_from_layers(&layers, &Policy::empty()) - .expect("network domain layer precedence should load"); - - assert_eq!( - config.allowed_domains(), - Some(vec!["example.com".to_string()]) - ); - assert_eq!(config.denied_domains(), None); -} - -#[test] -fn config_from_layers_uses_only_the_final_selected_profile_network() { - let lower_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::toml! { - default_permissions = "dev" - - [permissions.dev.network.domains] - "lower.example.com" = "allow" - } - .into(), - ); - let higher_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::toml! { - default_permissions = ":workspace" - } - .into(), - ); - let layers = ConfigLayerStack::new( - vec![lower_layer, higher_layer], - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - ) - .expect("layer stack should be valid"); - - let config = config_from_layers(&layers, &Policy::empty()) - .expect("final built-in profile selection should load"); - - assert_eq!(config.allowed_domains(), None); - assert_eq!(config.denied_domains(), None); -} - -#[test] -fn trusted_constraints_use_only_the_final_selected_profile_network() { - let lower_layer = ConfigLayerEntry::new( - ConfigLayerSource::System { - file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/system.toml")) - .expect("system config path should be absolute"), - }, - toml::toml! { - default_permissions = "dev" - - [permissions.dev.network.domains] - "managed.example.com" = "allow" - } - .into(), - ); - let higher_layer = ConfigLayerEntry::new( - ConfigLayerSource::LegacyManagedConfigTomlFromFile { - file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/managed.toml")) - .expect("managed config path should be absolute"), - }, - toml::toml! { - default_permissions = ":workspace" - } - .into(), - ); - let layers = ConfigLayerStack::new( - vec![lower_layer, higher_layer], - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - ) - .expect("layer stack should be valid"); - - let constraints = network_constraints_from_trusted_layers(&layers) - .expect("final built-in trusted selection should load"); - - assert_eq!(constraints.allowed_domains, None); - assert_eq!(constraints.denied_domains, None); -} - -#[test] -fn trusted_constraints_normalize_profile_network_domains_before_merging_layers() { - let lower_layer = ConfigLayerEntry::new( - ConfigLayerSource::System { - file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/system.toml")) - .expect("system config path should be absolute"), - }, - toml::toml! { - default_permissions = "dev" - - [permissions.dev.network.domains] - "example.com" = "deny" - } - .into(), - ); - let higher_layer = ConfigLayerEntry::new( - ConfigLayerSource::LegacyManagedConfigTomlFromFile { - file: AbsolutePathBuf::try_from(std::path::PathBuf::from("/tmp/managed.toml")) - .expect("managed config path should be absolute"), - }, - toml::toml! { - [permissions.dev.network.domains] - "EXAMPLE.COM" = "allow" - } - .into(), - ); - let layers = ConfigLayerStack::new( - vec![lower_layer, higher_layer], - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - ) - .expect("layer stack should be valid"); - - let constraints = network_constraints_from_trusted_layers(&layers) - .expect("trusted network domain layer precedence should load"); - - assert_eq!( - constraints.allowed_domains, - Some(vec!["example.com".to_string()]) - ); - assert_eq!(constraints.denied_domains, None); -} - -#[test] -fn apply_network_constraints_skips_empty_domain_sides() { - let config: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] - -[permissions.dev.network.domains] -"managed.example.com" = "allow" -"#, - ) - .expect("permissions profile should parse"); - let network = selected_network_from_tables( - network_tables_from_toml(&config).expect("permissions profile should deserialize"), - ) - .expect("permissions profile should select a network table") - .expect("network table should be present"); - - let mut constraints = NetworkProxyConstraints::default(); - apply_network_constraints(network, &mut constraints); - - assert_eq!( - constraints.allowed_domains, - Some(vec!["managed.example.com".to_string()]) - ); - assert_eq!(constraints.denied_domains, None); -} - -#[test] -fn apply_network_constraints_overlay_domain_entries() { - let lower_network: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] - -[permissions.dev.network.domains] -"blocked.example.com" = "deny" -"#, - ) - .expect("lower layer should parse"); - let higher_network: toml::Value = toml::from_str( - r#" -default_permissions = "dev" - -[permissions.dev.network] - -[permissions.dev.network.domains] -"api.example.com" = "allow" -"#, - ) - .expect("higher layer should parse"); - - let lower_network = selected_network_from_tables( - network_tables_from_toml(&lower_network).expect("lower layer should deserialize"), - ) - .expect("lower layer should select a network table") - .expect("lower network table should be present"); - let higher_network = selected_network_from_tables( - network_tables_from_toml(&higher_network).expect("higher layer should deserialize"), - ) - .expect("higher layer should select a network table") - .expect("higher network table should be present"); - - let mut constraints = NetworkProxyConstraints::default(); - apply_network_constraints(lower_network, &mut constraints); - apply_network_constraints(higher_network, &mut constraints); - - assert_eq!( - constraints.allowed_domains, - Some(vec!["api.example.com".to_string()]) - ); - assert_eq!( - constraints.denied_domains, - Some(vec!["blocked.example.com".to_string()]) - ); -}