use crate::NetworkDomainPermissions; use crate::NetworkProxyConfig; use crate::NetworkUnixSocketPermission; use crate::NetworkUnixSocketPermissions; use serde::Deserialize; use serde::Serialize; /// Managed proxy requirements supplied by the owner of one execution environment. /// /// The owner can require proxy routing without granting direct network access. /// Listeners, network mode, MITM, and credentials remain controller-owned. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct EnvironmentNetworkPolicy { /// Requires managed proxy routing even when direct network access is restricted. /// False or omission leaves activation to the controller and command permissions. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub requires_proxy: bool, pub domains: Option, /// Managed host selectors inherited by every execution in this environment. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub review_domains: Vec, pub unix_sockets: Option, pub allow_upstream_proxy: bool, pub dangerously_allow_all_unix_sockets: bool, /// Omission inherits local-binding policy; an explicit false remains a restriction. pub allow_local_binding: Option, pub managed_allowed_domains_only: bool, } impl EnvironmentNetworkPolicy { /// Captures proxy activation and traffic restrictions without controller runtime settings. pub fn from_config(config: &NetworkProxyConfig, managed_allowed_domains_only: bool) -> Self { Self { requires_proxy: config.enabled, domains: config.domains.clone(), review_domains: config.review_domains.clone(), unix_sockets: config.unix_sockets.clone(), allow_upstream_proxy: config.allow_upstream_proxy, dangerously_allow_all_unix_sockets: config .dangerously_allow_all_unix_sockets .unwrap_or(false), allow_local_binding: config.allow_local_binding, managed_allowed_domains_only, } } /// Applies attachment-owned traffic settings while preserving inherited denials and proxy setup. pub fn apply_to(&self, config: &mut NetworkProxyConfig) { // Neither owner nor controller may remove the other's review selectors. for pattern in &self.review_domains { if !config .review_domains .iter() .any(|existing| existing.eq_ignore_ascii_case(pattern)) { config.review_domains.push(pattern.clone()); } } // Use the owner's domain rules without dropping controller denials. let inherited_denials = config.denied_domains().unwrap_or_default(); config.domains.clone_from(&self.domains); for domain in inherited_denials { config.upsert_domain_permission( domain, crate::NetworkDomainPermission::Deny, crate::normalize_host, ); } // An omitted controller policy leaves socket permissions to the attachment; // an explicitly empty socket map still supplies a restrictive ceiling. let inherited_allow_all = config .dangerously_allow_all_unix_sockets .unwrap_or(config.unix_sockets.is_none()); let inherited_sockets = config.unix_sockets.take().unwrap_or_default(); let mut effective_sockets = self.unix_sockets.clone().unwrap_or_default(); // "Allow all" cannot override a socket denied by either policy. let inherited_permits_all = inherited_allow_all && !inherited_sockets .entries .values() .any(|permission| matches!(permission, NetworkUnixSocketPermission::Deny)); let owner_permits_all = self.dangerously_allow_all_unix_sockets && !effective_sockets .entries .values() .any(|permission| matches!(permission, NetworkUnixSocketPermission::Deny)); // Keep shared socket grants; controller denials always take priority. effective_sockets.entries.retain(|path, permission| { matches!(permission, NetworkUnixSocketPermission::Deny) || inherited_permits_all || matches!( inherited_sockets.entries.get(path), Some(NetworkUnixSocketPermission::Allow) ) }); for (path, permission) in inherited_sockets.entries { if owner_permits_all || matches!(permission, NetworkUnixSocketPermission::Deny) { effective_sockets.entries.insert(path, permission); } } // Socket and upstream-proxy permissions require both controller and owner grants. config.unix_sockets = (!effective_sockets.entries.is_empty()).then_some(effective_sockets); config.dangerously_allow_all_unix_sockets = Some(inherited_permits_all && owner_permits_all); config.allow_upstream_proxy &= self.allow_upstream_proxy; // Either explicit denial wins; defer unresolved defaults to the executor. config.allow_local_binding = match (config.allow_local_binding, self.allow_local_binding) { (Some(controller), Some(owner)) => Some(controller && owner), (controller, owner) => controller.or(owner), }; } }