Codex Handbook
sandboxing/src/policy_transforms.rs 533 lines
use codex_protocol::models::AdditionalPermissionProfile;use codex_protocol::models::FileSystemPermissions;use codex_protocol::models::NetworkPermissions;use codex_protocol::models::PermissionProfile;use codex_protocol::permissions::FileSystemAccessMode;use codex_protocol::permissions::FileSystemPath;use codex_protocol::permissions::FileSystemSandboxEntry;use codex_protocol::permissions::FileSystemSandboxKind;use codex_protocol::permissions::FileSystemSandboxPolicy;use codex_protocol::permissions::FileSystemSpecialPath;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 std::num::NonZeroUsize;use std::path::Path;use std::path::PathBuf;pub fn normalize_additional_permissions(    additional_permissions: AdditionalPermissionProfile,) -> Result<AdditionalPermissionProfile, String> {    let network = additional_permissions        .network        .filter(|network| !network.is_empty());    let file_system = match additional_permissions.file_system {        Some(file_system) => {            let mut entries = Vec::with_capacity(file_system.entries.len());            let glob_scan_max_depth = file_system.glob_scan_max_depth;            for entry in file_system.entries {                if matches!(&entry.path, FileSystemPath::GlobPattern { .. })                    && entry.access != FileSystemAccessMode::Deny                {                    return Err(                        "glob file system permissions only support deny-read entries".to_string(),                    );                }                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),                    },                    FileSystemPath::GlobPattern { pattern } => {                        FileSystemPath::GlobPattern { pattern }                    }                    FileSystemPath::Special { value } => FileSystemPath::Special { value },                };                let normalized_entry = FileSystemSandboxEntry {                    path,                    access: entry.access,                };                if !entries.contains(&normalized_entry) {                    entries.push(normalized_entry);                }            }            let file_system = FileSystemPermissions {                entries,                glob_scan_max_depth,            };            (!file_system.is_empty()).then_some(file_system)        }        None => None,    };    Ok(AdditionalPermissionProfile {        network,        file_system,    })}pub fn merge_permission_profiles(    base: Option<&AdditionalPermissionProfile>,    permissions: Option<&AdditionalPermissionProfile>,) -> Option<AdditionalPermissionProfile> {    let Some(permissions) = permissions else {        return base.cloned();    };    match base {        Some(base) => {            let network = match (base.network.as_ref(), permissions.network.as_ref()) {                (                    Some(NetworkPermissions {                        enabled: Some(true),                    }),                    _,                )                | (                    _,                    Some(NetworkPermissions {                        enabled: Some(true),                    }),                ) => Some(NetworkPermissions {                    enabled: Some(true),                }),                _ => None,            };            let file_system = match (base.file_system.as_ref(), permissions.file_system.as_ref()) {                (Some(base), Some(permissions)) => Some(FileSystemPermissions {                    entries: merge_permission_entries(&base.entries, &permissions.entries),                    glob_scan_max_depth: merge_glob_scan_max_depth(                        &base.entries,                        base.glob_scan_max_depth.map(usize::from),                        &permissions.entries,                        permissions.glob_scan_max_depth.map(usize::from),                    )                    .and_then(NonZeroUsize::new),                })                .filter(|file_system| !file_system.is_empty()),                (Some(base), None) => Some(base.clone()),                (None, Some(permissions)) => Some(permissions.clone()),                (None, None) => None,            };            Some(AdditionalPermissionProfile {                network,                file_system,            })            .filter(|permissions| !permissions.is_empty())        }        None => Some(permissions.clone()).filter(|permissions| !permissions.is_empty()),    }}pub fn intersect_permission_profiles(    requested: AdditionalPermissionProfile,    granted: AdditionalPermissionProfile,    cwd: &Path,) -> AdditionalPermissionProfile {    let file_system = requested        .file_system        .map(|requested_file_system| {            let granted_file_system = granted.file_system.unwrap_or_default();            let requested_policy =                FileSystemSandboxPolicy::restricted(requested_file_system.entries.clone());            let requested_read_deny_matcher = ReadDenyMatcher::new(&requested_policy, cwd);            let mut accepted_entries = Vec::new();            for entry in granted_file_system.entries.iter().filter(|entry| {                granted_file_system_entry_within_request(                    &requested_file_system,                    &requested_policy,                    requested_read_deny_matcher.as_ref(),                    entry,                    cwd,                )            }) {                let entry = materialize_cwd_dependent_entry(entry, cwd);                if !accepted_entries.contains(&entry) {                    accepted_entries.push(entry);                }            }            let mut entries = accepted_entries.clone();            let requested_retained_deny_entries = retain_constraining_deny_entries(                &requested_file_system.entries,                &accepted_entries,                cwd,                &mut entries,            );            let granted_retained_deny_entries = retain_constraining_deny_entries(                &granted_file_system.entries,                &accepted_entries,                cwd,                &mut entries,            );            FileSystemPermissions {                glob_scan_max_depth: merge_glob_scan_max_depth(                    &requested_retained_deny_entries,                    requested_file_system.glob_scan_max_depth.map(usize::from),                    &granted_retained_deny_entries,                    granted_file_system.glob_scan_max_depth.map(usize::from),                )                .and_then(NonZeroUsize::new),                entries,            }        })        .filter(|file_system| !file_system.is_empty());    let network = match (requested.network, granted.network) {        (            Some(NetworkPermissions {                enabled: Some(true),            }),            Some(NetworkPermissions {                enabled: Some(true),            }),        ) => Some(NetworkPermissions {            enabled: Some(true),        }),        _ => None,    };    AdditionalPermissionProfile {        network,        file_system,    }}fn merge_glob_scan_max_depth(    left_entries: &[FileSystemSandboxEntry],    left_depth: Option<usize>,    right_entries: &[FileSystemSandboxEntry],    right_depth: Option<usize>,) -> Option<usize> {    let left_depth = effective_glob_scan_depth(left_entries, left_depth);    let right_depth = effective_glob_scan_depth(right_entries, right_depth);    match (left_depth, right_depth) {        (Some(GlobScanDepth::Unbounded), _) | (_, Some(GlobScanDepth::Unbounded)) => None,        (Some(GlobScanDepth::Bounded(left)), Some(GlobScanDepth::Bounded(right))) => {            Some(left.max(right))        }        (Some(GlobScanDepth::Bounded(depth)), None)        | (None, Some(GlobScanDepth::Bounded(depth))) => Some(depth),        (None, None) => None,    }}fn effective_glob_scan_depth(    entries: &[FileSystemSandboxEntry],    depth: Option<usize>,) -> Option<GlobScanDepth> {    entries        .iter()        .any(|entry| {            entry.access == FileSystemAccessMode::Deny                && matches!(&entry.path, FileSystemPath::GlobPattern { .. })        })        .then_some(match depth {            Some(depth) => GlobScanDepth::Bounded(depth),            None => GlobScanDepth::Unbounded,        })}#[derive(Debug, Clone, Copy, PartialEq, Eq)]enum GlobScanDepth {    Bounded(usize),    Unbounded,}fn granted_file_system_entry_within_request(    requested: &FileSystemPermissions,    requested_policy: &FileSystemSandboxPolicy,    requested_read_deny_matcher: Option<&ReadDenyMatcher>,    granted_entry: &FileSystemSandboxEntry,    cwd: &Path,) -> bool {    if !granted_entry.access.can_read() {        return false;    }    if let Some(path) = resolve_permission_path(&granted_entry.path, cwd) {        if requested_read_deny_matcher.is_some_and(|matcher| matcher.is_read_denied(path.as_path()))        {            return false;        }        return access_covers(            requested_policy.resolve_access_with_cwd(path.as_path(), cwd),            granted_entry.access,        );    }    requested.entries.iter().any(|requested_entry| {        access_covers(requested_entry.access, granted_entry.access)            && requested_entry.path == granted_entry.path    })}fn retain_constraining_deny_entries(    source_entries: &[FileSystemSandboxEntry],    accepted_entries: &[FileSystemSandboxEntry],    cwd: &Path,    output_entries: &mut Vec<FileSystemSandboxEntry>,) -> Vec<FileSystemSandboxEntry> {    let mut retained_entries = Vec::new();    for entry in source_entries        .iter()        .filter(|entry| entry.access == FileSystemAccessMode::Deny)    {        if !deny_entry_constrains_accepted_grant(entry, accepted_entries, cwd) {            continue;        }        let entry = materialize_cwd_dependent_entry(entry, cwd);        if !output_entries.contains(&entry) {            output_entries.push(entry.clone());        }        retained_entries.push(entry);    }    retained_entries}fn deny_entry_constrains_accepted_grant(    deny_entry: &FileSystemSandboxEntry,    accepted_entries: &[FileSystemSandboxEntry],    cwd: &Path,) -> bool {    accepted_entries        .iter()        .filter(|entry| entry.access.can_read())        .any(|entry| {            let Some(grant_path) = resolve_permission_path(&entry.path, cwd) else {                return false;            };            match &deny_entry.path {                FileSystemPath::GlobPattern { pattern } => glob_static_prefix_path(pattern, cwd)                    .is_some_and(|prefix| paths_overlap(prefix.as_path(), grant_path.as_path())),                FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => {                    resolve_permission_path(&deny_entry.path, cwd).is_some_and(|deny_path| {                        paths_overlap(deny_path.as_path(), grant_path.as_path())                    })                }            }        })}fn glob_static_prefix_path(pattern: &str, cwd: &Path) -> Option<AbsolutePathBuf> {    let resolved_pattern = AbsolutePathBuf::resolve_path_against_base(pattern, cwd);    let resolved_pattern = resolved_pattern.as_path().to_string_lossy();    let prefix = match resolved_pattern.find(['*', '?', '[', ']']) {        Some(0) => return None,        Some(index) => {            let prefix = &resolved_pattern[..index];            if prefix.ends_with(std::path::MAIN_SEPARATOR)                || prefix.ends_with('/')                || prefix.ends_with('\\')            {                Path::new(prefix)            } else {                Path::new(prefix).parent()?            }        }        None => Path::new(resolved_pattern.as_ref()),    };    AbsolutePathBuf::from_absolute_path(prefix).ok()}fn paths_overlap(left: &Path, right: &Path) -> bool {    left.starts_with(right) || right.starts_with(left)}fn access_covers(requested: FileSystemAccessMode, granted: FileSystemAccessMode) -> bool {    match granted {        FileSystemAccessMode::Read => requested.can_read(),        FileSystemAccessMode::Write => requested.can_write(),        FileSystemAccessMode::Deny => false,    }}fn materialize_cwd_dependent_entry(    entry: &FileSystemSandboxEntry,    cwd: &Path,) -> FileSystemSandboxEntry {    match &entry.path {        FileSystemPath::Special {            value: FileSystemSpecialPath::ProjectRoots { .. },        } => resolve_permission_path(&entry.path, cwd)            .map(|path| FileSystemSandboxEntry {                path: FileSystemPath::Path { path },                access: entry.access,            })            .unwrap_or_else(|| entry.clone()),        FileSystemPath::GlobPattern { pattern } => FileSystemSandboxEntry {            path: FileSystemPath::GlobPattern {                pattern: AbsolutePathBuf::resolve_path_against_base(pattern, cwd)                    .to_string_lossy()                    .into_owned(),            },            access: entry.access,        },        FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => entry.clone(),    }}fn resolve_permission_path(path: &FileSystemPath, cwd: &Path) -> Option<AbsolutePathBuf> {    match path {        FileSystemPath::Path { path } => Some(path.clone()),        FileSystemPath::GlobPattern { .. } => None,        FileSystemPath::Special { value } => match value {            FileSystemSpecialPath::Root => {                let root = cwd.ancestors().last()?;                AbsolutePathBuf::from_absolute_path(root).ok()            }            FileSystemSpecialPath::ProjectRoots { subpath } => {                let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok()?;                Some(match subpath {                    Some(subpath) => {                        AbsolutePathBuf::resolve_path_against_base(subpath, cwd.as_path())                    }                    None => cwd,                })            }            FileSystemSpecialPath::Tmpdir => {                let tmpdir = std::env::var_os("TMPDIR")?;                if tmpdir.is_empty() {                    None                } else {                    AbsolutePathBuf::from_absolute_path(PathBuf::from(tmpdir)).ok()                }            }            FileSystemSpecialPath::SlashTmp => AbsolutePathBuf::from_absolute_path("/tmp")                .ok()                .filter(|path| path.as_path().is_dir()),            FileSystemSpecialPath::Minimal | FileSystemSpecialPath::Unknown { .. } => None,        },    }}fn merge_permission_entries(    base: &[FileSystemSandboxEntry],    permissions: &[FileSystemSandboxEntry],) -> Vec<FileSystemSandboxEntry> {    let mut merged = Vec::with_capacity(base.len() + permissions.len());    for entry in base.iter().chain(permissions.iter()) {        if !merged.contains(entry) {            merged.push(entry.clone());        }    }    merged}fn merge_file_system_policy_with_additional_permissions(    file_system_policy: &FileSystemSandboxPolicy,    additional_permissions: &FileSystemPermissions,) -> FileSystemSandboxPolicy {    match file_system_policy.kind {        FileSystemSandboxKind::Restricted => {            let mut merged_policy = file_system_policy.clone();            for entry in &additional_permissions.entries {                if !merged_policy.entries.contains(entry) {                    merged_policy.entries.push(entry.clone());                }            }            merged_policy.glob_scan_max_depth = merge_glob_scan_max_depth(                &file_system_policy.entries,                file_system_policy.glob_scan_max_depth,                &additional_permissions.entries,                additional_permissions.glob_scan_max_depth.map(usize::from),            );            merged_policy        }        FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => {            file_system_policy.clone()        }    }}pub fn effective_file_system_sandbox_policy(    file_system_policy: &FileSystemSandboxPolicy,    additional_permissions: Option<&AdditionalPermissionProfile>,) -> FileSystemSandboxPolicy {    let Some(additional_permissions) = additional_permissions else {        return file_system_policy.clone();    };    let Some(file_system_permissions) = additional_permissions.file_system.as_ref() else {        return file_system_policy.clone();    };    if file_system_permissions.is_empty() {        file_system_policy.clone()    } else {        merge_file_system_policy_with_additional_permissions(            file_system_policy,            file_system_permissions,        )    }}fn merge_network_access(    base_network_access: bool,    additional_permissions: &AdditionalPermissionProfile,) -> bool {    base_network_access        || additional_permissions            .network            .as_ref()            .and_then(|network| network.enabled)            .unwrap_or(false)}pub fn effective_network_sandbox_policy(    network_policy: NetworkSandboxPolicy,    additional_permissions: Option<&AdditionalPermissionProfile>,) -> NetworkSandboxPolicy {    if additional_permissions        .is_some_and(|permissions| merge_network_access(network_policy.is_enabled(), permissions))    {        NetworkSandboxPolicy::Enabled    } else if additional_permissions.is_some() {        NetworkSandboxPolicy::Restricted    } else {        network_policy    }}pub fn effective_permission_profile(    permission_profile: &PermissionProfile,    additional_permissions: Option<&AdditionalPermissionProfile>,) -> PermissionProfile {    let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions();    let effective_file_system_policy =        effective_file_system_sandbox_policy(&file_system_policy, additional_permissions);    let effective_network_policy =        effective_network_sandbox_policy(network_policy, additional_permissions);    PermissionProfile::from_runtime_permissions_with_enforcement(        permission_profile.enforcement(),        &effective_file_system_policy,        effective_network_policy,    )}pub fn should_require_platform_sandbox(    file_system_policy: &FileSystemSandboxPolicy,    network_policy: NetworkSandboxPolicy,    has_managed_network_requirements: bool,) -> bool {    if has_managed_network_requirements {        return true;    }    if !network_policy.is_enabled() {        return !matches!(            file_system_policy.kind,            FileSystemSandboxKind::ExternalSandbox        );    }    match file_system_policy.kind {        FileSystemSandboxKind::Restricted => !file_system_policy.has_full_disk_write_access(),        FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => false,    }}#[cfg(test)]#[path = "policy_transforms_tests.rs"]mod tests;