Files
codex/codex-rs/core/src/safety_tests.rs
T
Adam Perry @ OpenAI 0f89dd768c apply-patch: carry paths as PathUri (#28854)
## Why

Allows the model to edit files that are hosted on a different OS than
where app-server is running.

## What

* Use `PathUri` for apply_patch-internal data structures
* Limit `PathUri` -> `AbsolutePathBuf` conversion to cases where the
inferred path convention matches the host OS, allows requiring valid
paths to pass to perms check
* Adds `PathConvention::path_segments()` for iterating over path
segments regardless of OS
* Handle cross-platform relative paths in path filename parsing for
sniffing a shell
* Ensure we can apply patches in the wine e2e test
2026-06-18 19:31:19 +00:00

349 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::*;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::FileSystemAccessMode;
use codex_protocol::protocol::FileSystemPath;
use codex_protocol::protocol::FileSystemSandboxEntry;
use codex_protocol::protocol::FileSystemSpecialPath;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use core_test_support::PathExt;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn test_writable_roots_constraint() {
// Use a temporary directory as our workspace to avoid touching
// the real current working directory.
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let parent = cwd.parent().unwrap();
// Helper to build a singleentry patch that adds a file at `p`.
let make_add_change = |p: AbsolutePathBuf| {
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&p), "".to_string())
};
let add_inside = make_add_change(cwd.join("inner.txt"));
let add_outside = make_add_change(parent.join("outside.txt"));
// Policy limited to the workspace only; exclude system temp roots so
// only `cwd` is writable by default.
let workspace_only_file_system_policy = FileSystemSandboxPolicy::workspace_write(
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
assert!(is_write_patch_constrained_to_writable_paths(
&add_inside,
&workspace_only_file_system_policy,
&cwd_uri,
));
assert!(!is_write_patch_constrained_to_writable_paths(
&add_outside,
&workspace_only_file_system_policy,
&cwd_uri,
));
// With the parent dir explicitly added as a writable root, the
// outside write should be permitted.
let file_system_policy_with_parent = FileSystemSandboxPolicy::workspace_write(
std::slice::from_ref(&parent),
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
assert!(is_write_patch_constrained_to_writable_paths(
&add_outside,
&file_system_policy_with_parent,
&cwd_uri,
));
}
#[test]
fn external_sandbox_auto_approves_in_on_request() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let add_inside_path = cwd.join("inner.txt");
let add_inside = ApplyPatchAction::new_add_for_test(
&PathUri::from_abs_path(&add_inside_path),
"".to_string(),
);
let permission_profile = PermissionProfile::External {
network: NetworkSandboxPolicy::Enabled,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::external_sandbox();
assert_eq!(
assess_patch_safety(
&add_inside,
AskForApproval::OnRequest,
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled
),
SafetyCheck::AutoApprove {
sandbox_type: SandboxType::None,
user_explicitly_approved: false,
}
);
}
#[test]
fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let parent = cwd.parent().unwrap();
let outside_path = parent.join("outside.txt");
let add_outside =
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&outside_path), "".to_string());
let permission_profile = PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
assert_eq!(
assess_patch_safety(
&add_outside,
AskForApproval::OnRequest,
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled,
),
SafetyCheck::AskUser,
);
assert_eq!(
assess_patch_safety(
&add_outside,
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled,
),
SafetyCheck::AskUser,
);
}
#[test]
fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let parent = cwd.parent().unwrap();
let outside_path = parent.join("outside.txt");
let add_outside =
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&outside_path), "".to_string());
let permission_profile = PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
assert_eq!(
assess_patch_safety(
&add_outside,
AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: false,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}),
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled,
),
SafetyCheck::Reject {
reason: PATCH_REJECTED_OUTSIDE_PROJECT_REASON.to_string(),
},
);
}
#[test]
fn read_only_policy_rejects_patch_with_read_only_reason() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let inside_path = cwd.join("inside.txt");
let action =
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&inside_path), "".to_string());
let permission_profile = PermissionProfile::read_only();
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
assert!(!is_write_patch_constrained_to_writable_paths(
&action,
&file_system_sandbox_policy,
&cwd_uri,
));
assert_eq!(
assess_patch_safety(
&action,
AskForApproval::Never,
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled,
),
SafetyCheck::Reject {
reason: PATCH_REJECTED_READ_ONLY_REASON.to_string(),
},
);
}
#[test]
fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let blocked_path = cwd.join("blocked.txt");
let blocked_absolute = blocked_path;
let action = ApplyPatchAction::new_add_for_test(
&PathUri::from_abs_path(&blocked_absolute),
"".to_string(),
);
let permission_profile = PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: blocked_absolute,
},
access: FileSystemAccessMode::Deny,
},
]);
assert!(!is_write_patch_constrained_to_writable_paths(
&action,
&file_system_sandbox_policy,
&cwd_uri,
));
assert_eq!(
assess_patch_safety(
&action,
AskForApproval::OnRequest,
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled,
),
SafetyCheck::AskUser,
);
}
#[test]
fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let blocked_path = cwd.join("docs").join("blocked.txt");
let blocked_absolute = blocked_path;
let docs_absolute = AbsolutePathBuf::resolve_path_against_base("docs", &cwd);
let action = ApplyPatchAction::new_add_for_test(
&PathUri::from_abs_path(&blocked_absolute),
"".to_string(),
);
let permission_profile = PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: docs_absolute,
},
access: FileSystemAccessMode::Read,
},
]);
assert!(!is_write_patch_constrained_to_writable_paths(
&action,
&file_system_sandbox_policy,
&cwd_uri,
));
assert_eq!(
assess_patch_safety(
&action,
AskForApproval::OnRequest,
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled,
),
SafetyCheck::AskUser,
);
}
#[test]
fn missing_project_dot_codex_config_requires_approval() {
let tmp = TempDir::new().unwrap();
let cwd = tmp.path().abs();
let cwd_uri = PathUri::from_abs_path(&cwd);
let config_path = cwd.join(".codex").join("config.toml");
let action =
ApplyPatchAction::new_add_for_test(&PathUri::from_abs_path(&config_path), "".to_string());
let permission_profile = PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let mut file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
file_system_sandbox_policy
.entries
.push(FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: cwd.join(".codex"),
},
access: FileSystemAccessMode::Read,
});
assert!(!is_write_patch_constrained_to_writable_paths(
&action,
&file_system_sandbox_policy,
&cwd_uri,
));
assert_eq!(
assess_patch_safety(
&action,
AskForApproval::OnRequest,
&permission_profile,
&file_system_sandbox_policy,
&cwd_uri,
WindowsSandboxLevel::Disabled,
),
SafetyCheck::AskUser,
);
}