mirror of
https://github.com/openai/codex.git
synced 2026-11-09 08:40:33 +00:00
## Why Patch targets can use a different path convention from the Codex host, so host-native path conversion can misclassify writable roots and requested permissions. ## What changed - Evaluate patch targets as `PathUri` values with the active filesystem policy context, including workspace roots and the executor's path convention. - Distinguish executor-managed sandboxing from local platform sandboxing when deciding whether a patch can be auto-approved and how to normalize additional write permissions. - Make full-disk and special-path policy checks honor the selected executor's Windows or POSIX convention. ## Testing Add coverage for Windows executor URIs, full-disk policy aliases, remote patch permission requests, sandbox availability, and owner-provided workspace roots. GitOrigin-RevId: 1a054ea443efd342623c67432762f85c53d20c15
145 lines
5.1 KiB
Rust
145 lines
5.1 KiB
Rust
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::permissions::FileSystemSandboxPolicyContext;
|
|
use codex_protocol::protocol::AskForApproval;
|
|
use codex_sandboxing::get_platform_sandbox;
|
|
use codex_utils_path_uri::PathUri;
|
|
|
|
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,
|
|
AskUser,
|
|
Reject { reason: String },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub(crate) enum PatchSandboxRoute {
|
|
ExecutorManaged,
|
|
Platform(WindowsSandboxLevel),
|
|
}
|
|
|
|
pub fn assess_patch_safety(
|
|
action: &ApplyPatchAction,
|
|
policy: AskForApproval,
|
|
permission_profile: &PermissionProfile,
|
|
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
|
context: &FileSystemSandboxPolicyContext<'_>,
|
|
sandbox_route: PatchSandboxRoute,
|
|
) -> SafetyCheck {
|
|
if action.is_empty() {
|
|
return SafetyCheck::Reject {
|
|
reason: "empty patch".to_string(),
|
|
};
|
|
}
|
|
|
|
match policy {
|
|
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
|
|
);
|
|
let sandbox_available = match sandbox_route {
|
|
PatchSandboxRoute::ExecutorManaged => true,
|
|
PatchSandboxRoute::Platform(windows_sandbox_level) => {
|
|
get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled).is_some()
|
|
}
|
|
};
|
|
|
|
// 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.
|
|
// Disabled and External profiles intentionally do not apply an outer sandbox.
|
|
if is_write_patch_constrained_to_writable_paths(action, file_system_sandbox_policy, context)
|
|
&& (matches!(
|
|
permission_profile,
|
|
PermissionProfile::Disabled | PermissionProfile::External { .. }
|
|
) || sandbox_available)
|
|
{
|
|
SafetyCheck::AutoApprove
|
|
} else if rejects_sandbox_approval {
|
|
SafetyCheck::Reject {
|
|
reason: patch_rejection_reason(permission_profile, file_system_sandbox_policy, context)
|
|
.to_string(),
|
|
}
|
|
} else {
|
|
SafetyCheck::AskUser
|
|
}
|
|
}
|
|
|
|
fn patch_rejection_reason(
|
|
permission_profile: &PermissionProfile,
|
|
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
|
context: &FileSystemSandboxPolicyContext<'_>,
|
|
) -> &'static str {
|
|
let has_no_writable_roots = !file_system_sandbox_policy.has_configured_writable_roots(context);
|
|
match permission_profile {
|
|
PermissionProfile::Managed { .. }
|
|
if !file_system_sandbox_policy.has_full_disk_write_access_with_context(context)
|
|
&& has_no_writable_roots =>
|
|
{
|
|
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,
|
|
context: &FileSystemSandboxPolicyContext<'_>,
|
|
) -> bool {
|
|
// A full-disk policy permits every patch target, so no per-path writable-root check can
|
|
// further constrain the result.
|
|
if file_system_sandbox_policy.has_full_disk_write_access_with_context(context) {
|
|
return true;
|
|
}
|
|
let is_path_writable =
|
|
|path: &PathUri| file_system_sandbox_policy.can_write_path(path, context);
|
|
|
|
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;
|