Codex Handbook
core/src/safety.rs 197 lines
use std::path::Component;use std::path::Path;use std::path::PathBuf;use crate::util::resolve_path;use codex_apply_patch::ApplyPatchAction;use codex_apply_patch::ApplyPatchFileChange;use codex_protocol::config_types::WindowsSandboxLevel;use codex_protocol::models::PermissionProfile;use codex_protocol::permissions::FileSystemSandboxPolicy;use codex_protocol::protocol::AskForApproval;use codex_sandboxing::SandboxType;use codex_sandboxing::get_platform_sandbox;use codex_utils_absolute_path::AbsolutePathBuf;const PATCH_REJECTED_OUTSIDE_PROJECT_REASON: &str =    "writing outside of the project; rejected by user approval settings";const PATCH_REJECTED_READ_ONLY_REASON: &str =    "writing is blocked by read-only sandbox; rejected by user approval settings";#[derive(Debug, PartialEq)]pub enum SafetyCheck {    AutoApprove {        sandbox_type: SandboxType,        user_explicitly_approved: bool,    },    AskUser,    Reject {        reason: String,    },}pub fn assess_patch_safety(    action: &ApplyPatchAction,    policy: AskForApproval,    permission_profile: &PermissionProfile,    file_system_sandbox_policy: &FileSystemSandboxPolicy,    cwd: &AbsolutePathBuf,    windows_sandbox_level: WindowsSandboxLevel,) -> SafetyCheck {    if action.is_empty() {        return SafetyCheck::Reject {            reason: "empty patch".to_string(),        };    }    match policy {        AskForApproval::OnFailure        | AskForApproval::Never        | AskForApproval::OnRequest        | AskForApproval::Granular(_) => {            // Continue to see if this can be auto-approved.        }        // TODO(ragona): I'm not sure this is actually correct? I believe in this case        // we want to continue to the writable paths check before asking the user.        AskForApproval::UnlessTrusted => {            return SafetyCheck::AskUser;        }    }    let rejects_sandbox_approval = matches!(policy, AskForApproval::Never)        || matches!(            policy,            AskForApproval::Granular(granular_config) if !granular_config.sandbox_approval        );    // Even though the patch appears to be constrained to writable paths, it is    // possible that paths in the patch are hard links to files outside the    // writable roots, so we should still run `apply_patch` in a sandbox in that case.    if is_write_patch_constrained_to_writable_paths(action, file_system_sandbox_policy, cwd)        || matches!(policy, AskForApproval::OnFailure)    {        if matches!(            permission_profile,            PermissionProfile::Disabled | PermissionProfile::External { .. }        ) {            // Disabled and External profiles intentionally do not apply an            // outer Codex filesystem sandbox.            SafetyCheck::AutoApprove {                sandbox_type: SandboxType::None,                user_explicitly_approved: false,            }        } else {            // Only auto‑approve when we can actually enforce a sandbox. Otherwise            // fall back to asking the user because the patch may touch arbitrary            // paths outside the project.            match get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) {                Some(sandbox_type) => SafetyCheck::AutoApprove {                    sandbox_type,                    user_explicitly_approved: false,                },                None => {                    if rejects_sandbox_approval {                        SafetyCheck::Reject {                            reason: patch_rejection_reason(                                permission_profile,                                file_system_sandbox_policy,                                cwd,                            )                            .to_string(),                        }                    } else {                        SafetyCheck::AskUser                    }                }            }        }    } else if rejects_sandbox_approval {        SafetyCheck::Reject {            reason: patch_rejection_reason(permission_profile, file_system_sandbox_policy, cwd)                .to_string(),        }    } else {        SafetyCheck::AskUser    }}fn patch_rejection_reason(    permission_profile: &PermissionProfile,    file_system_sandbox_policy: &FileSystemSandboxPolicy,    cwd: &AbsolutePathBuf,) -> &'static str {    match permission_profile {        PermissionProfile::Managed { .. }            if !file_system_sandbox_policy.has_full_disk_write_access()                && file_system_sandbox_policy                    .get_writable_roots_with_cwd(cwd.as_path())                    .is_empty() =>        {            PATCH_REJECTED_READ_ONLY_REASON        }        PermissionProfile::Managed { .. }        | PermissionProfile::Disabled        | PermissionProfile::External { .. } => PATCH_REJECTED_OUTSIDE_PROJECT_REASON,    }}fn is_write_patch_constrained_to_writable_paths(    action: &ApplyPatchAction,    file_system_sandbox_policy: &FileSystemSandboxPolicy,    cwd: &AbsolutePathBuf,) -> bool {    // Normalize a path by removing `.` and resolving `..` without touching the    // filesystem (works even if the file does not exist).    fn normalize(path: &Path) -> Option<PathBuf> {        let mut out = PathBuf::new();        for comp in path.components() {            match comp {                Component::ParentDir => {                    out.pop();                }                Component::CurDir => { /* skip */ }                other => out.push(other.as_os_str()),            }        }        Some(out)    }    // Determine whether `path` is inside **any** writable root. Both `path`    // and roots are converted to absolute, normalized forms before the    // prefix check.    let is_path_writable = |p: &Path| {        let abs = resolve_path(cwd, &p.to_path_buf());        let abs = match normalize(&abs) {            Some(v) => v,            None => return false,        };        file_system_sandbox_policy.can_write_path_with_cwd(&abs, cwd)    };    for (path, change) in action.changes() {        match change {            ApplyPatchFileChange::Add { .. } | ApplyPatchFileChange::Delete { .. } => {                if !is_path_writable(path) {                    return false;                }            }            ApplyPatchFileChange::Update { move_path, .. } => {                if !is_path_writable(path) {                    return false;                }                if let Some(dest) = move_path                    && !is_path_writable(dest)                {                    return false;                }            }        }    }    true}#[cfg(test)]#[path = "safety_tests.rs"]mod tests;