diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 958a259182..59702ad656 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -1,5 +1,6 @@ use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; +use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; @@ -12,6 +13,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::ReadDenyMatcher; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; +use codex_utils_path_uri::PathUri; use std::num::NonZeroUsize; use std::path::Path; use std::path::PathBuf; @@ -19,6 +21,24 @@ use std::path::PathBuf; pub fn normalize_additional_permissions( additional_permissions: AdditionalPermissionProfile, ) -> Result { + normalize_additional_permissions_with(additional_permissions, |path| { + canonicalize_preserving_symlinks(path.as_path()) + .ok() + .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) + .unwrap_or(path) + }) +} + +pub fn normalize_additional_permissions_for_uri( + additional_permissions: AdditionalPermissionProfile, +) -> Result, String> { + normalize_additional_permissions_with(additional_permissions, std::convert::identity) +} + +fn normalize_additional_permissions_with( + additional_permissions: AdditionalPermissionProfile, + mut normalize_path: impl FnMut(PathType) -> PathType, +) -> Result, String> { let network = additional_permissions .network .filter(|network| !network.is_empty()); @@ -36,10 +56,7 @@ pub fn normalize_additional_permissions( } let path = match entry.path { FileSystemPath::Path { path } => FileSystemPath::Path { - path: canonicalize_preserving_symlinks(path.as_path()) - .ok() - .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) - .unwrap_or(path), + path: normalize_path(path), }, FileSystemPath::GlobPattern { pattern } => { FileSystemPath::GlobPattern { pattern } @@ -68,10 +85,53 @@ pub fn normalize_additional_permissions( }) } -pub fn merge_permission_profiles( - base: Option<&AdditionalPermissionProfile>, - permissions: Option<&AdditionalPermissionProfile>, -) -> Option { +pub fn resolve_additional_permission_paths( + additional_permissions: AdditionalPermissionProfile, + cwd: &PathUri, +) -> Result, String> { + let network = additional_permissions.network; + let file_system = additional_permissions + .file_system + .map(|file_system| -> Result, String> { + let entries = file_system + .entries + .into_iter() + .map(|entry| { + let path = match entry.path { + FileSystemPath::Path { path } => FileSystemPath::Path { + path: cwd.join(&path).map_err(|err| { + format!( + "failed to resolve permission path `{path}` against cwd URI `{cwd}`: {err}" + ) + })?, + }, + FileSystemPath::GlobPattern { pattern } => { + FileSystemPath::GlobPattern { pattern } + } + FileSystemPath::Special { value } => FileSystemPath::Special { value }, + }; + Ok(FileSystemSandboxEntry { + path, + access: entry.access, + }) + }) + .collect::, String>>()?; + Ok(FileSystemPermissions { + entries, + glob_scan_max_depth: file_system.glob_scan_max_depth, + }) + }) + .transpose()?; + Ok(AdditionalPermissionProfile { + network, + file_system, + }) +} + +pub fn merge_permission_profiles( + base: Option<&AdditionalPermissionProfile>, + permissions: Option<&AdditionalPermissionProfile>, +) -> Option> { let Some(permissions) = permissions else { return base.cloned(); }; @@ -194,10 +254,10 @@ pub fn intersect_permission_profiles( } } -fn merge_glob_scan_max_depth( - left_entries: &[FileSystemSandboxEntry], +fn merge_glob_scan_max_depth( + left_entries: &[FileSystemSandboxEntry], left_depth: Option, - right_entries: &[FileSystemSandboxEntry], + right_entries: &[FileSystemSandboxEntry], right_depth: Option, ) -> Option { let left_depth = effective_glob_scan_depth(left_entries, left_depth); @@ -214,8 +274,8 @@ fn merge_glob_scan_max_depth( } } -fn effective_glob_scan_depth( - entries: &[FileSystemSandboxEntry], +fn effective_glob_scan_depth( + entries: &[FileSystemSandboxEntry], depth: Option, ) -> Option { entries @@ -403,10 +463,10 @@ fn resolve_permission_path(path: &FileSystemPath, cwd: &Path) -> Option Vec { +fn merge_permission_entries( + base: &[FileSystemSandboxEntry], + permissions: &[FileSystemSandboxEntry], +) -> Vec> { let mut merged = Vec::with_capacity(base.len() + permissions.len()); for entry in base.iter().chain(permissions.iter()) { if !merged.contains(entry) { @@ -463,9 +523,9 @@ pub fn effective_file_system_sandbox_policy( } } -fn merge_network_access( +fn merge_network_access( base_network_access: bool, - additional_permissions: &AdditionalPermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> bool { base_network_access || additional_permissions @@ -475,9 +535,9 @@ fn merge_network_access( .unwrap_or(false) } -pub fn effective_network_sandbox_policy( +pub fn effective_network_sandbox_policy( network_policy: NetworkSandboxPolicy, - additional_permissions: Option<&AdditionalPermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> NetworkSandboxPolicy { if additional_permissions .is_some_and(|permissions| merge_network_access(network_policy.is_enabled(), permissions)) @@ -506,6 +566,59 @@ pub fn effective_permission_profile( ) } +pub fn effective_permission_profile_for_uri( + permission_profile: &PermissionProfile, + additional_permissions: Option<&AdditionalPermissionProfile>, +) -> PermissionProfile { + let permission_profile = PermissionProfile::::from(permission_profile.clone()); + let Some(additional_permissions) = additional_permissions else { + return permission_profile; + }; + let network = |network| effective_network_sandbox_policy(network, Some(additional_permissions)); + + match permission_profile { + PermissionProfile::Managed { + file_system, + network: base_network, + } => { + let file_system = match (file_system, additional_permissions.file_system.as_ref()) { + ( + ManagedFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + }, + Some(additional_file_system), + ) if !additional_file_system.is_empty() => { + ManagedFileSystemPermissions::Restricted { + entries: merge_permission_entries( + &entries, + &additional_file_system.entries, + ), + glob_scan_max_depth: merge_glob_scan_max_depth( + &entries, + glob_scan_max_depth.map(usize::from), + &additional_file_system.entries, + additional_file_system.glob_scan_max_depth.map(usize::from), + ) + .and_then(NonZeroUsize::new), + } + } + (file_system, _) => file_system, + }; + PermissionProfile::Managed { + file_system, + network: network(base_network), + } + } + PermissionProfile::Disabled => PermissionProfile::Disabled, + PermissionProfile::External { + network: base_network, + } => PermissionProfile::External { + network: network(base_network), + }, + } +} + pub fn should_require_platform_sandbox( file_system_policy: &FileSystemSandboxPolicy, network_policy: NetworkSandboxPolicy,