use std::collections::BTreeMap; use std::io; use std::path::Component; use std::path::Path; use std::path::PathBuf; use codex_network_proxy::NetworkMode; use codex_network_proxy::NetworkProxyConfig; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] pub struct PermissionsToml { #[serde(flatten)] pub entries: BTreeMap, } impl PermissionsToml { pub fn is_empty(&self) -> bool { self.entries.is_empty() } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct PermissionProfileToml { pub filesystem: Option, pub network: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] pub struct FilesystemPermissionsToml { #[serde(flatten)] pub entries: BTreeMap, } impl FilesystemPermissionsToml { pub fn is_empty(&self) -> bool { self.entries.is_empty() } } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(untagged)] pub enum FilesystemPermissionToml { Access(FileSystemAccessMode), Scoped(BTreeMap), } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct PermissionProfileNetworkToml { pub enabled: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[schemars(deny_unknown_fields)] pub struct NetworkToml { pub enabled: Option, pub proxy_url: Option, pub enable_socks5: Option, pub socks_url: Option, pub enable_socks5_udp: Option, pub allow_upstream_proxy: Option, pub dangerously_allow_non_loopback_proxy: Option, pub dangerously_allow_all_unix_sockets: Option, #[schemars(with = "Option")] pub mode: Option, pub allowed_domains: Option>, pub denied_domains: Option>, pub allow_unix_sockets: Option>, pub allow_local_binding: Option, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] enum NetworkModeSchema { Limited, Full, } impl NetworkToml { pub(crate) fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) { if let Some(enabled) = self.enabled { config.network.enabled = enabled; } if let Some(proxy_url) = self.proxy_url.as_ref() { config.network.proxy_url = proxy_url.clone(); } if let Some(enable_socks5) = self.enable_socks5 { config.network.enable_socks5 = enable_socks5; } if let Some(socks_url) = self.socks_url.as_ref() { config.network.socks_url = socks_url.clone(); } if let Some(enable_socks5_udp) = self.enable_socks5_udp { config.network.enable_socks5_udp = enable_socks5_udp; } if let Some(allow_upstream_proxy) = self.allow_upstream_proxy { config.network.allow_upstream_proxy = allow_upstream_proxy; } if let Some(dangerously_allow_non_loopback_proxy) = self.dangerously_allow_non_loopback_proxy { config.network.dangerously_allow_non_loopback_proxy = dangerously_allow_non_loopback_proxy; } if let Some(dangerously_allow_all_unix_sockets) = self.dangerously_allow_all_unix_sockets { config.network.dangerously_allow_all_unix_sockets = dangerously_allow_all_unix_sockets; } if let Some(mode) = self.mode { config.network.mode = mode; } if let Some(allowed_domains) = self.allowed_domains.as_ref() { config.network.allowed_domains = allowed_domains.clone(); } if let Some(denied_domains) = self.denied_domains.as_ref() { config.network.denied_domains = denied_domains.clone(); } if let Some(allow_unix_sockets) = self.allow_unix_sockets.as_ref() { config.network.allow_unix_sockets = allow_unix_sockets.clone(); } if let Some(allow_local_binding) = self.allow_local_binding { config.network.allow_local_binding = allow_local_binding; } } pub(crate) fn to_network_proxy_config(&self) -> NetworkProxyConfig { let mut config = NetworkProxyConfig::default(); self.apply_to_network_proxy_config(&mut config); config } } pub(crate) fn network_proxy_config_from_network( network: Option<&NetworkToml>, ) -> NetworkProxyConfig { network.map_or_else( NetworkProxyConfig::default, NetworkToml::to_network_proxy_config, ) } pub(crate) fn compile_permission_profile( permissions: &PermissionsToml, profile_name: &str, ) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> { let profile = permissions.entries.get(profile_name).ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, format!("default_permissions refers to undefined profile `{profile_name}`"), ) })?; let filesystem = profile.filesystem.as_ref().ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, format!( "permissions profile `{profile_name}` must define a `[permissions.{profile_name}.filesystem]` table" ), ) })?; if filesystem.is_empty() { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!( "permissions profile `{profile_name}` must define at least one filesystem entry" ), )); } let mut entries = Vec::new(); for (path, permission) in &filesystem.entries { compile_filesystem_permission(path, permission, &mut entries)?; } let network_sandbox_policy = compile_network_sandbox_policy(profile.network.as_ref()); Ok(( FileSystemSandboxPolicy::restricted(entries), network_sandbox_policy, )) } fn compile_network_sandbox_policy( network: Option<&PermissionProfileNetworkToml>, ) -> NetworkSandboxPolicy { let Some(network) = network else { return NetworkSandboxPolicy::Restricted; }; match network.enabled { Some(true) => NetworkSandboxPolicy::Enabled, _ => NetworkSandboxPolicy::Restricted, } } fn compile_filesystem_permission( path: &str, permission: &FilesystemPermissionToml, entries: &mut Vec, ) -> io::Result<()> { match permission { FilesystemPermissionToml::Access(access) => entries.push(FileSystemSandboxEntry { path: compile_filesystem_path(path)?, access: *access, }), FilesystemPermissionToml::Scoped(scoped_entries) => { for (subpath, access) in scoped_entries { entries.push(FileSystemSandboxEntry { path: compile_scoped_filesystem_path(path, subpath)?, access: *access, }); } } } Ok(()) } fn compile_filesystem_path(path: &str) -> io::Result { if let Some(special) = parse_special_path(path)? { return Ok(FileSystemPath::Special { value: special }); } let path = parse_absolute_path(path)?; Ok(FileSystemPath::Path { path }) } fn compile_scoped_filesystem_path(path: &str, subpath: &str) -> io::Result { if subpath == "." { return compile_filesystem_path(path); } if let Some(special) = parse_special_path(path)? { if !matches!(special, FileSystemSpecialPath::ProjectRoots { .. }) { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("filesystem path `{path}` does not support nested entries"), )); } return Ok(FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(Some(parse_relative_subpath(subpath)?)), }); } let subpath = parse_relative_subpath(subpath)?; let base = parse_absolute_path(path)?; let path = AbsolutePathBuf::resolve_path_against_base(&subpath, base.as_path())?; Ok(FileSystemPath::Path { path }) } fn parse_special_path(path: &str) -> io::Result> { let special = match path { ":root" => Some(FileSystemSpecialPath::Root), ":minimal" => Some(FileSystemSpecialPath::Minimal), ":project_roots" => Some(FileSystemSpecialPath::project_roots(None)), ":tmpdir" => Some(FileSystemSpecialPath::Tmpdir), _ if path.starts_with(':') => { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("unknown filesystem special path `{path}`"), )); } _ => None, }; Ok(special) } fn parse_absolute_path(path: &str) -> io::Result { let path_ref = Path::new(path); if !path_ref.is_absolute() && path != "~" && !path.starts_with("~/") { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("filesystem path `{path}` must be absolute, use `~/...`, or start with `:`"), )); } AbsolutePathBuf::from_absolute_path(path_ref) } fn parse_relative_subpath(subpath: &str) -> io::Result { let path = Path::new(subpath); if !subpath.is_empty() && path .components() .all(|component| matches!(component, Component::Normal(_))) { return Ok(path.to_path_buf()); } Err(io::Error::new( io::ErrorKind::InvalidInput, format!( "filesystem subpath `{}` must be a descendant path without `.` or `..` components", path.display() ), )) }