mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +00:00
Preserve target-native paths in command approvals (#42577)
## What changed - Pass the selected executor's `PathUri` through user command approval requests instead of converting the working directory to a host-native path. - Make native-path permission helpers explicitly local and require executor-aware permission transforms to receive a `FileSystemSandboxPolicyContext`. - Cover approved remote commands to verify that the approval request reports the selected remote working directory and execution resumes after approval. GitOrigin-RevId: 0aa3042f2d7594af6c7d0abf31d7f99cf67d5556
This commit is contained in:
1 parent
0650d6d1ca
commit
7a7c188682
30 files changed
+304
-368
No files matched your search
@@ -1329,7 +1329,7 @@ impl ThreadRequestProcessor {
|
|||||||
codex_protocol::models::PermissionProfile::Managed { .. } => {
|
codex_protocol::models::PermissionProfile::Managed { .. } => {
|
||||||
effective_permission_profile
|
effective_permission_profile
|
||||||
.file_system_sandbox_policy()
|
.file_system_sandbox_policy()
|
||||||
.can_write_path_with_cwd(config.cwd.as_path(), config.cwd.as_path())
|
.can_write_local_path_with_cwd(config.cwd.as_path(), config.cwd.as_path())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2383,7 +2383,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
|
|||||||
!config
|
!config
|
||||||
.permissions
|
.permissions
|
||||||
.file_system_sandbox_policy()
|
.file_system_sandbox_policy()
|
||||||
.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path())
|
.can_write_local_path_with_cwd(&cwd.path().join(".git"), cwd.path())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.permissions.network_sandbox_policy(),
|
config.permissions.network_sandbox_policy(),
|
||||||
@@ -2828,7 +2828,7 @@ async fn permission_profile_override_keeps_memories_root_out_of_legacy_projectio
|
|||||||
!config
|
!config
|
||||||
.permissions
|
.permissions
|
||||||
.file_system_sandbox_policy()
|
.file_system_sandbox_policy()
|
||||||
.can_write_path_with_cwd(memories_root.as_path(), cwd.path())
|
.can_write_local_path_with_cwd(memories_root.as_path(), cwd.path())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&config.legacy_sandbox_policy(),
|
&config.legacy_sandbox_policy(),
|
||||||
@@ -3064,11 +3064,11 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
|
|||||||
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
|
Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE)
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected :workspace to allow writing the project root, policy: {policy:?}"
|
"expected :workspace to allow writing the project root, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path()),
|
!policy.can_write_local_path_with_cwd(&cwd.path().join(".git"), cwd.path()),
|
||||||
"expected :workspace to protect project metadata, policy: {policy:?}"
|
"expected :workspace to protect project metadata, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -3097,7 +3097,7 @@ async fn default_permissions_read_only_keeps_add_dir_read_only() -> std::io::Res
|
|||||||
|
|
||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
!policy.can_write_local_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||||
"expected :read-only to stay read-only for runtime workspace roots, policy: {policy:?}"
|
"expected :read-only to stay read-only for runtime workspace roots, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3186,15 +3186,15 @@ async fn workspace_profile_applies_rules_to_runtime_and_profile_workspace_roots(
|
|||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
for root in [cwd_abs, runtime_root_abs, profile_root_abs.clone()] {
|
for root in [cwd_abs, runtime_root_abs, profile_root_abs.clone()] {
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_write_path_with_cwd(root.as_path(), cwd.as_path()),
|
policy.can_write_local_path_with_cwd(root.as_path(), cwd.as_path()),
|
||||||
"expected workspace root to be writable, policy: {policy:?}"
|
"expected workspace root to be writable, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(&root.join(".git"), cwd.as_path()),
|
!policy.can_write_local_path_with_cwd(&root.join(".git"), cwd.as_path()),
|
||||||
"expected .git carveout under {root:?}, policy: {policy:?}"
|
"expected .git carveout under {root:?}, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(&root.join(".codex"), cwd.as_path()),
|
!policy.can_write_local_path_with_cwd(&root.join(".codex"), cwd.as_path()),
|
||||||
"expected .codex carveout under {root:?}, policy: {policy:?}"
|
"expected .codex carveout under {root:?}, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3293,11 +3293,11 @@ async fn default_permissions_profile_can_extend_builtin_workspace() -> std::io::
|
|||||||
|
|
||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected profile extending :workspace to keep project-root writes, policy: {policy:?}"
|
"expected profile extending :workspace to keep project-root writes, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path()),
|
!policy.can_write_local_path_with_cwd(&cwd.path().join(".git"), cwd.path()),
|
||||||
"expected profile extending :workspace to keep metadata carveouts, policy: {policy:?}"
|
"expected profile extending :workspace to keep metadata carveouts, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -3388,11 +3388,11 @@ async fn default_permissions_profile_can_extend_builtin_read_only() -> std::io::
|
|||||||
|
|
||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(cwd.path(), cwd.path()),
|
policy.can_read_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected profile extending :read-only to keep read access, policy: {policy:?}"
|
"expected profile extending :read-only to keep read access, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
!policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected profile extending :read-only to stay non-writable, policy: {policy:?}"
|
"expected profile extending :read-only to stay non-writable, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3448,16 +3448,16 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::
|
|||||||
);
|
);
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
!policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected trusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}"
|
"expected trusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected trusted project fallback to use :workspace, policy: {policy:?}"
|
"expected trusted project fallback to use :workspace, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(&cwd.path().join(".codex"), cwd.path()),
|
!policy.can_write_local_path_with_cwd(&cwd.path().join(".codex"), cwd.path()),
|
||||||
"expected :workspace metadata carveouts, policy: {policy:?}"
|
"expected :workspace metadata carveouts, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3502,21 +3502,21 @@ async fn empty_config_defaults_to_builtin_profile_for_untrusted_project() -> std
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(cwd.path(), cwd.path()),
|
policy.can_read_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected untrusted project fallback to allow reads, policy: {policy:?}"
|
"expected untrusted project fallback to allow reads, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
!policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected untrusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}"
|
"expected untrusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected untrusted project fallback to use :workspace, policy: {policy:?}"
|
"expected untrusted project fallback to use :workspace, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(&cwd.path().join(".codex"), cwd.path()),
|
!policy.can_write_local_path_with_cwd(&cwd.path().join(".codex"), cwd.path()),
|
||||||
"expected :workspace metadata carveouts, policy: {policy:?}"
|
"expected :workspace metadata carveouts, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3562,7 +3562,7 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se
|
|||||||
|
|
||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
policy.can_write_local_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||||
"expected implicit :workspace to preserve sandbox_workspace_write.writable_roots, policy: {policy:?}"
|
"expected implicit :workspace to preserve sandbox_workspace_write.writable_roots, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3629,12 +3629,12 @@ async fn implicit_builtin_workspace_profile_preserves_add_dir_metadata_carveouts
|
|||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
let extra_root = extra_root.path().abs();
|
let extra_root = extra_root.path().abs();
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
|
policy.can_write_local_path_with_cwd(extra_root.as_path(), cwd.path()),
|
||||||
"expected implicit :workspace to preserve additional writable roots, policy: {policy:?}"
|
"expected implicit :workspace to preserve additional writable roots, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
for subpath in [".git", ".agents", ".codex"] {
|
for subpath in [".git", ".agents", ".codex"] {
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(&extra_root.join(subpath), cwd.path()),
|
!policy.can_write_local_path_with_cwd(&extra_root.join(subpath), cwd.path()),
|
||||||
"expected implicit :workspace to preserve legacy metadata carveout for {subpath}, \
|
"expected implicit :workspace to preserve legacy metadata carveout for {subpath}, \
|
||||||
policy: {policy:?}"
|
policy: {policy:?}"
|
||||||
);
|
);
|
||||||
@@ -3660,11 +3660,11 @@ async fn empty_config_defaults_to_builtin_read_only_without_trust_decision() ->
|
|||||||
|
|
||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(cwd.path(), cwd.path()),
|
policy.can_read_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected :read-only to allow reads, policy: {policy:?}"
|
"expected :read-only to allow reads, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
|
!policy.can_write_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
"expected :read-only to deny writes, policy: {policy:?}"
|
"expected :read-only to deny writes, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -3842,7 +3842,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
|
|||||||
config
|
config
|
||||||
.permissions
|
.permissions
|
||||||
.file_system_sandbox_policy()
|
.file_system_sandbox_policy()
|
||||||
.can_write_path_with_cwd(external_write_path.as_path(), cwd.path())
|
.can_write_local_path_with_cwd(external_write_path.as_path(), cwd.path())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&config.legacy_sandbox_policy(),
|
&config.legacy_sandbox_policy(),
|
||||||
@@ -6035,8 +6035,12 @@ async fn memory_tool_makes_memories_root_readable_without_creating_or_widening_w
|
|||||||
memories_root.display()
|
memories_root.display()
|
||||||
);
|
);
|
||||||
let file_system_policy = config.permissions.file_system_sandbox_policy();
|
let file_system_policy = config.permissions.file_system_sandbox_policy();
|
||||||
assert!(file_system_policy.can_read_path_with_cwd(memories_root_abs.as_path(), cwd.path()));
|
assert!(
|
||||||
assert!(!file_system_policy.can_write_path_with_cwd(memories_root_abs.as_path(), cwd.path()));
|
file_system_policy.can_read_local_path_with_cwd(memories_root_abs.as_path(), cwd.path())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!file_system_policy.can_write_local_path_with_cwd(memories_root_abs.as_path(), cwd.path())
|
||||||
|
);
|
||||||
|
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
match &config.legacy_sandbox_policy() {
|
match &config.legacy_sandbox_policy() {
|
||||||
@@ -10975,7 +10979,7 @@ async fn permission_profile_override_preserves_split_write_roots() -> std::io::R
|
|||||||
config
|
config
|
||||||
.permissions
|
.permissions
|
||||||
.file_system_sandbox_policy()
|
.file_system_sandbox_policy()
|
||||||
.can_write_path_with_cwd(outside_root.as_path(), config.cwd.as_path())
|
.can_write_local_path_with_cwd(outside_root.as_path(), config.cwd.as_path())
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
&config.legacy_sandbox_policy(),
|
&config.legacy_sandbox_policy(),
|
||||||
|
|||||||
@@ -4091,17 +4091,14 @@ impl Config {
|
|||||||
}) = filesystem_requirements.as_ref()
|
}) = filesystem_requirements.as_ref()
|
||||||
&& let Some(managed_file_system_policy) = managed_deny_read_policy.as_ref()
|
&& let Some(managed_file_system_policy) = managed_deny_read_policy.as_ref()
|
||||||
{
|
{
|
||||||
let _initial_matcher =
|
let managed_deny_matcher =
|
||||||
ReadDenyMatcher::try_new(managed_file_system_policy, resolved_cwd.as_path())
|
ReadDenyMatcher::try_new_for_local_paths(managed_file_system_policy, resolved_cwd.as_path())
|
||||||
.map_err(std::io::Error::other)?;
|
.map_err(std::io::Error::other)?;
|
||||||
let managed_file_system_policy = Arc::clone(managed_file_system_policy);
|
let managed_file_system_policy = Arc::clone(managed_file_system_policy);
|
||||||
let permission_cwd = resolved_cwd.clone();
|
|
||||||
let requirement_source = requirement_source.clone();
|
let requirement_source = requirement_source.clone();
|
||||||
constrained_permission_profile
|
constrained_permission_profile
|
||||||
.value
|
.value
|
||||||
.add_validator(move |permission_profile| {
|
.add_validator(move |permission_profile| {
|
||||||
let managed_deny_matcher =
|
|
||||||
ReadDenyMatcher::new(&managed_file_system_policy, permission_cwd.as_path());
|
|
||||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||||
let missing_required_deny = managed_file_system_policy
|
let missing_required_deny = managed_file_system_policy
|
||||||
.entries
|
.entries
|
||||||
@@ -4118,7 +4115,7 @@ impl Config {
|
|||||||
let path = path.to_abs_path().ok()?;
|
let path = path.to_abs_path().ok()?;
|
||||||
managed_deny_matcher
|
managed_deny_matcher
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|matcher| matcher.is_read_denied(path.as_path()))
|
.is_some_and(|matcher| matcher.is_local_path_read_denied(path.as_path()))
|
||||||
.then_some(path)
|
.then_some(path)
|
||||||
});
|
});
|
||||||
if missing_required_deny || violating_root.is_some() {
|
if missing_required_deny || violating_root.is_some() {
|
||||||
|
|||||||
@@ -96,15 +96,15 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
|
|||||||
let policy = config.permissions.file_system_sandbox_policy();
|
let policy = config.permissions.file_system_sandbox_policy();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(expected_zsh.as_path(), &cwd),
|
policy.can_read_local_path_with_cwd(expected_zsh.as_path(), &cwd),
|
||||||
"expected zsh helper path to be readable, policy: {policy:?}"
|
"expected zsh helper path to be readable, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(expected_allowed_arg0_dir.as_path(), &cwd),
|
policy.can_read_local_path_with_cwd(expected_allowed_arg0_dir.as_path(), &cwd),
|
||||||
"expected active arg0 helper dir to be readable, policy: {policy:?}"
|
"expected active arg0 helper dir to be readable, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_read_path_with_cwd(expected_sibling_arg0_dir.as_path(), &cwd),
|
!policy.can_read_local_path_with_cwd(expected_sibling_arg0_dir.as_path(), &cwd),
|
||||||
"expected sibling arg0 helper dir to remain unreadable, policy: {policy:?}"
|
"expected sibling arg0 helper dir to remain unreadable, policy: {policy:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ docs = "read"
|
|||||||
let (read_deny_policy, _) =
|
let (read_deny_policy, _) =
|
||||||
compile_permission_profile(&permissions, "read_deny", &mut startup_warnings)?;
|
compile_permission_profile(&permissions, "read_deny", &mut startup_warnings)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read_deny_policy.resolve_access_with_cwd(cwd.path(), cwd.path()),
|
read_deny_policy.resolve_access_for_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
FileSystemAccessMode::Deny
|
FileSystemAccessMode::Deny
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -501,7 +501,7 @@ docs = "read"
|
|||||||
compile_permission_profile(&permissions, "write_deny", &mut startup_warnings)?;
|
compile_permission_profile(&permissions, "write_deny", &mut startup_warnings)?;
|
||||||
assert!(!write_deny_policy.has_full_disk_write_access());
|
assert!(!write_deny_policy.has_full_disk_write_access());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
write_deny_policy.resolve_access_with_cwd(cwd.path(), cwd.path()),
|
write_deny_policy.resolve_access_for_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
FileSystemAccessMode::Deny
|
FileSystemAccessMode::Deny
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -509,7 +509,7 @@ docs = "read"
|
|||||||
compile_permission_profile(&permissions, "write_read", &mut startup_warnings)?;
|
compile_permission_profile(&permissions, "write_read", &mut startup_warnings)?;
|
||||||
assert!(!write_read_policy.has_full_disk_write_access());
|
assert!(!write_read_policy.has_full_disk_write_access());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
write_read_policy.resolve_access_with_cwd(&docs, cwd.path()),
|
write_read_policy.resolve_access_for_local_path_with_cwd(&docs, cwd.path()),
|
||||||
FileSystemAccessMode::Read
|
FileSystemAccessMode::Read
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5894,13 +5894,13 @@ async fn session_configuration_apply_preserves_absolute_cwd_write_root_on_cwd_up
|
|||||||
assert!(
|
assert!(
|
||||||
updated
|
updated
|
||||||
.file_system_sandbox_policy(&[])
|
.file_system_sandbox_policy(&[])
|
||||||
.can_write_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()),
|
.can_write_local_path_with_cwd(original_cwd.as_path(), updated.cwd().as_path()),
|
||||||
"absolute grant to the old cwd must remain writable"
|
"absolute grant to the old cwd must remain writable"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!updated
|
!updated
|
||||||
.file_system_sandbox_policy(&[])
|
.file_system_sandbox_policy(&[])
|
||||||
.can_write_path_with_cwd(next_cwd.as_path(), updated.cwd().as_path()),
|
.can_write_local_path_with_cwd(next_cwd.as_path(), updated.cwd().as_path()),
|
||||||
"cwd-only update must not reinterpret an absolute old-cwd grant as :workspace_roots"
|
"cwd-only update must not reinterpret an absolute old-cwd grant as :workspace_roots"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -708,15 +708,6 @@ impl Session {
|
|||||||
proposed_execpolicy_amendment,
|
proposed_execpolicy_amendment,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let cwd = match guardian_cwd(environment_id, cwd.clone()) {
|
|
||||||
Ok(cwd) => cwd,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!(%err, "failed to resolve approval command cwd");
|
|
||||||
return ReviewDecision::denied(format!(
|
|
||||||
"failed to resolve approval command cwd: {err}"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let tool_name = "unified_exec";
|
let tool_name = "unified_exec";
|
||||||
let reason = ctx
|
let reason = ctx
|
||||||
.retry_reason
|
.retry_reason
|
||||||
@@ -743,7 +734,7 @@ impl Session {
|
|||||||
/*approval_id*/ None,
|
/*approval_id*/ None,
|
||||||
Some(environment_id.clone()),
|
Some(environment_id.clone()),
|
||||||
command.clone(),
|
command.clone(),
|
||||||
cwd.into(),
|
cwd.clone(),
|
||||||
reason,
|
reason,
|
||||||
ctx.network_approval_context.clone(),
|
ctx.network_approval_context.clone(),
|
||||||
proposed_execpolicy_amendment.clone(),
|
proposed_execpolicy_amendment.clone(),
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ mod tests {
|
|||||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||||
use codex_protocol::protocol::AskForApproval;
|
use codex_protocol::protocol::AskForApproval;
|
||||||
use codex_protocol::protocol::GranularApprovalConfig;
|
use codex_protocol::protocol::GranularApprovalConfig;
|
||||||
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
|
use codex_sandboxing::policy_transforms::intersect_permission_profiles_with_context;
|
||||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
@@ -538,21 +538,21 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let stored_grant = intersect_permission_profiles(
|
let cwd_uri = PathUri::from_host_native_path(cwd.path()).expect("cwd URI");
|
||||||
|
let stored_grant = intersect_permission_profiles_with_context(
|
||||||
requested_permissions.clone(),
|
requested_permissions.clone(),
|
||||||
requested_permissions.clone(),
|
requested_permissions.clone(),
|
||||||
cwd.path(),
|
&local_context(&cwd_uri),
|
||||||
);
|
);
|
||||||
let effective_permissions =
|
let effective_permissions =
|
||||||
merge_permission_profiles(Some(&requested_permissions), Some(&stored_grant))
|
merge_permission_profiles(Some(&requested_permissions), Some(&stored_grant))
|
||||||
.expect("merged permissions");
|
.expect("merged permissions");
|
||||||
let cwd = PathUri::from_host_native_path(cwd.path()).expect("cwd URI");
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
preapproved_permission_profile(
|
preapproved_permission_profile(
|
||||||
&effective_permissions,
|
&effective_permissions,
|
||||||
&stored_grant,
|
&stored_grant,
|
||||||
&local_context(&cwd),
|
&local_context(&cwd_uri),
|
||||||
),
|
),
|
||||||
Some(stored_grant)
|
Some(stored_grant)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ async fn managed_deny_read_requirements_follow_thread_permission_updates() -> Re
|
|||||||
!snapshot
|
!snapshot
|
||||||
.permission_profile
|
.permission_profile
|
||||||
.file_system_sandbox_policy()
|
.file_system_sandbox_policy()
|
||||||
.can_read_path_with_cwd(nested_root.as_path(), snapshot.cwd().as_path()),
|
.can_read_local_path_with_cwd(nested_root.as_path(), snapshot.cwd().as_path()),
|
||||||
"a live permission change must preserve managed deny-read rules"
|
"a live permission change must preserve managed deny-read rules"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ async fn remote_test_env_exposes_target_shell_and_exec_guidance_to_model() -> Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> {
|
async fn approved_remote_shell_runs_in_remote_cwd() -> Result<()> {
|
||||||
const CALL_ID: &str = "remote-explicit-shell";
|
const CALL_ID: &str = "remote-explicit-shell";
|
||||||
|
|
||||||
skip_if_no_remote_env!(Ok(()));
|
skip_if_no_remote_env!(Ok(()));
|
||||||
@@ -406,9 +406,15 @@ async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> {
|
|||||||
"shell": shell,
|
"shell": shell,
|
||||||
"login": false,
|
"login": false,
|
||||||
"yield_time_ms": 10_000,
|
"yield_time_ms": 10_000,
|
||||||
|
"sandbox_permissions": SandboxPermissions::RequireEscalated,
|
||||||
|
"justification": "Test target-native command approval cwd.",
|
||||||
}))?;
|
}))?;
|
||||||
let mut builder = test_codex();
|
let mut builder = test_codex().with_config(|config| {
|
||||||
|
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||||
|
config.approvals_reviewer = ApprovalsReviewer::User;
|
||||||
|
});
|
||||||
let test = builder.build_with_auto_env(&server).await?;
|
let test = builder.build_with_auto_env(&server).await?;
|
||||||
|
let selection = test.executor_environment().selection().clone();
|
||||||
let response_mock = mount_sse_sequence(
|
let response_mock = mount_sse_sequence(
|
||||||
&server,
|
&server,
|
||||||
vec![
|
vec![
|
||||||
@@ -426,16 +432,39 @@ async fn explicit_remote_shell_runs_in_remote_cwd() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
test.submit_turn_with_environments(
|
submit_turn_with_approval_and_environments(
|
||||||
|
&test,
|
||||||
"run the remote shell in the remote cwd",
|
"run the remote shell in the remote cwd",
|
||||||
Some(vec![TurnEnvironmentSelection {
|
vec![selection.clone()],
|
||||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
AskForApproval::OnRequest,
|
||||||
cwd: test.executor_environment().selection().cwd.clone(),
|
|
||||||
workspace_roots: vec![test.executor_environment().selection().cwd.clone()],
|
|
||||||
config: EnvironmentConfigState::FromThread,
|
|
||||||
}]),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let event = wait_for_event(&test.codex, |event| {
|
||||||
|
matches!(
|
||||||
|
event,
|
||||||
|
EventMsg::ExecApprovalRequest(_) | EventMsg::TurnComplete(_)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let EventMsg::ExecApprovalRequest(approval) = event else {
|
||||||
|
panic!("expected remote command approval before completion: {event:?}");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
approval.cwd.to_inferred_path_uri().as_ref(),
|
||||||
|
Some(&selection.cwd)
|
||||||
|
);
|
||||||
|
test.codex
|
||||||
|
.submit(Op::ExecApproval {
|
||||||
|
id: approval.effective_approval_id(),
|
||||||
|
turn_id: Some(approval.turn_id),
|
||||||
|
decision: ReviewDecision::Approved,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
wait_for_event(&test.codex, |event| {
|
||||||
|
matches!(event, EventMsg::TurnComplete(_))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
let request = response_mock
|
let request = response_mock
|
||||||
.last_request()
|
.last_request()
|
||||||
.context("model should receive the command output")?;
|
.context("model should receive the command output")?;
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ fn add_helper_runtime_permissions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for helper_read_root in helper_read_roots {
|
for helper_read_root in helper_read_roots {
|
||||||
if file_system_policy.can_read_path_with_cwd(helper_read_root.as_path(), cwd) {
|
if file_system_policy.can_read_local_path_with_cwd(helper_read_root.as_path(), cwd) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,8 +571,8 @@ mod tests {
|
|||||||
cwd.as_path(),
|
cwd.as_path(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(policy.can_read_path_with_cwd(readable.as_path(), cwd.as_path()));
|
assert!(policy.can_read_local_path_with_cwd(readable.as_path(), cwd.as_path()));
|
||||||
assert!(policy.can_write_path_with_cwd(writable.as_path(), cwd.as_path()));
|
assert!(policy.can_write_local_path_with_cwd(writable.as_path(), cwd.as_path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -800,10 +800,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(runtime_paths.codex_self_exe.as_path(), cwd.as_path())
|
policy.can_read_local_path_with_cwd(
|
||||||
|
runtime_paths.codex_self_exe.as_path(),
|
||||||
|
cwd.as_path(),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert!(!policy.can_read_path_with_cwd(parent.as_path(), cwd.as_path()));
|
assert!(!policy.can_read_local_path_with_cwd(parent.as_path(), cwd.as_path()));
|
||||||
assert!(!policy.can_read_path_with_cwd(sibling.as_path(), cwd.as_path()));
|
assert!(!policy.can_read_local_path_with_cwd(sibling.as_path(), cwd.as_path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -831,11 +834,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(runtime_paths.codex_self_exe.as_path(), cwd.as_path())
|
policy.can_read_local_path_with_cwd(
|
||||||
|
runtime_paths.codex_self_exe.as_path(),
|
||||||
|
cwd.as_path(),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
assert!(policy.can_read_path_with_cwd(alias.as_path(), cwd.as_path()));
|
assert!(policy.can_read_local_path_with_cwd(alias.as_path(), cwd.as_path()));
|
||||||
assert!(!policy.can_read_path_with_cwd(codex_parent.as_path(), cwd.as_path()));
|
assert!(!policy.can_read_local_path_with_cwd(codex_parent.as_path(), cwd.as_path()));
|
||||||
assert!(!policy.can_read_path_with_cwd(alias_parent.as_path(), cwd.as_path()));
|
assert!(!policy.can_read_local_path_with_cwd(alias_parent.as_path(), cwd.as_path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restricted_policy(entries: Vec<FileSystemSandboxEntry>) -> FileSystemSandboxPolicy {
|
fn restricted_policy(entries: Vec<FileSystemSandboxEntry>) -> FileSystemSandboxPolicy {
|
||||||
|
|||||||
@@ -1263,7 +1263,7 @@ fn sandbox_mode_from_permission_profile(
|
|||||||
.network_sandbox_policy()
|
.network_sandbox_policy()
|
||||||
.is_enabled()
|
.is_enabled()
|
||||||
.then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
|
.then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
|
||||||
} else if file_system_policy.can_write_path_with_cwd(cwd, cwd) {
|
} else if file_system_policy.can_write_local_path_with_cwd(cwd, cwd) {
|
||||||
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
|
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
|
||||||
} else {
|
} else {
|
||||||
Some(codex_app_server_protocol::SandboxMode::ReadOnly)
|
Some(codex_app_server_protocol::SandboxMode::ReadOnly)
|
||||||
|
|||||||
@@ -143,9 +143,9 @@ pub fn intersect_effective_permission_profiles(
|
|||||||
policy.entries.retain(|entry| entry.path != tmpdir);
|
policy.entries.retain(|entry| entry.path != tmpdir);
|
||||||
}
|
}
|
||||||
|
|
||||||
let authority_denies = ReadDenyMatcher::try_new(&authority_policy, cwd)
|
let authority_denies = ReadDenyMatcher::try_new_for_local_paths(&authority_policy, cwd)
|
||||||
.map_err(PermissionIntersectionError::UnsupportedPath)?;
|
.map_err(PermissionIntersectionError::UnsupportedPath)?;
|
||||||
let requested_denies = ReadDenyMatcher::try_new(&requested_policy, cwd)
|
let requested_denies = ReadDenyMatcher::try_new_for_local_paths(&requested_policy, cwd)
|
||||||
.map_err(PermissionIntersectionError::UnsupportedPath)?;
|
.map_err(PermissionIntersectionError::UnsupportedPath)?;
|
||||||
let sources = [
|
let sources = [
|
||||||
(&authority_policy, authority_denies.as_ref()),
|
(&authority_policy, authority_denies.as_ref()),
|
||||||
@@ -214,7 +214,8 @@ pub fn intersect_effective_permission_profiles(
|
|||||||
denies.sort_by_cached_key(|entry| format!("{:?}", entry.path));
|
denies.sort_by_cached_key(|entry| format!("{:?}", entry.path));
|
||||||
let mut intersection = FileSystemSandboxPolicy::restricted(denies);
|
let mut intersection = FileSystemSandboxPolicy::restricted(denies);
|
||||||
intersection.glob_scan_max_depth = glob_depth.flatten();
|
intersection.glob_scan_max_depth = glob_depth.flatten();
|
||||||
let intersection_denies = ReadDenyMatcher::new(&intersection, cwd);
|
let intersection_denies = ReadDenyMatcher::try_new_for_local_paths(&intersection, cwd)
|
||||||
|
.map_err(PermissionIntersectionError::UnsupportedPath)?;
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let [left, right] =
|
let [left, right] =
|
||||||
@@ -395,11 +396,11 @@ fn effective_access(
|
|||||||
path: &Path,
|
path: &Path,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
) -> FileSystemAccessMode {
|
) -> FileSystemAccessMode {
|
||||||
if denies.is_some_and(|matcher| matcher.is_read_denied(path)) {
|
if denies.is_some_and(|matcher| matcher.is_local_path_read_denied(path)) {
|
||||||
FileSystemAccessMode::Deny
|
FileSystemAccessMode::Deny
|
||||||
} else if policy.can_write_path_with_cwd(path, cwd) {
|
} else if policy.can_write_local_path_with_cwd(path, cwd) {
|
||||||
FileSystemAccessMode::Write
|
FileSystemAccessMode::Write
|
||||||
} else if policy.can_read_path_with_cwd(path, cwd) {
|
} else if policy.can_read_local_path_with_cwd(path, cwd) {
|
||||||
FileSystemAccessMode::Read
|
FileSystemAccessMode::Read
|
||||||
} else {
|
} else {
|
||||||
FileSystemAccessMode::Deny
|
FileSystemAccessMode::Deny
|
||||||
|
|||||||
@@ -117,14 +117,15 @@ fn effective_workspace_intersection_preserves_network_metadata_and_temp() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
[&root, &project]
|
[&root, &project]
|
||||||
.map(|path| policy.resolve_access_with_cwd(path.as_path(), root.as_path())),
|
.map(|path| policy
|
||||||
|
.resolve_access_for_local_path_with_cwd(path.as_path(), root.as_path())),
|
||||||
[Read, Write]
|
[Read, Write]
|
||||||
);
|
);
|
||||||
assert_eq!(result.network_sandbox_policy(), Restricted);
|
assert_eq!(result.network_sandbox_policy(), Restricted);
|
||||||
assert!(policy.entries.contains(&special(Tmpdir, Write)));
|
assert!(policy.entries.contains(&special(Tmpdir, Write)));
|
||||||
for name in [".git", ".agents", ".codex"] {
|
for name in [".git", ".agents", ".codex"] {
|
||||||
let protected = project.join(name);
|
let protected = project.join(name);
|
||||||
assert!(!policy.can_write_path_with_cwd(protected.as_path(), root.as_path()));
|
assert!(!policy.can_write_local_path_with_cwd(protected.as_path(), root.as_path()));
|
||||||
assert!(policy.entries.contains(&skipped(protected.into(), Read)));
|
assert!(policy.entries.contains(&skipped(protected.into(), Read)));
|
||||||
}
|
}
|
||||||
assert!(policy.entries.contains(&skipped(gitdir.into(), Read)));
|
assert!(policy.entries.contains(&skipped(gitdir.into(), Read)));
|
||||||
@@ -177,10 +178,13 @@ fn exact_denies_nested_read_carveouts_and_reopened_writes_are_preserved() {
|
|||||||
let right = PermissionProfile::from_runtime_permissions(&right_policy, Restricted);
|
let right = PermissionProfile::from_runtime_permissions(&right_policy, Restricted);
|
||||||
let result = intersection(&left, &right, &root);
|
let result = intersection(&left, &right, &root);
|
||||||
let policy = result.file_system_sandbox_policy();
|
let policy = result.file_system_sandbox_policy();
|
||||||
let denies = ReadDenyMatcher::new(&policy, root.as_path()).expect("merged denies");
|
let denies = ReadDenyMatcher::try_new_for_local_paths(&policy, root.as_path())
|
||||||
|
.expect("valid merged deny globs")
|
||||||
|
.expect("merged denies");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
[&root, &shared, &editable]
|
[&root, &shared, &editable]
|
||||||
.map(|path| policy.resolve_access_with_cwd(path.as_path(), root.as_path())),
|
.map(|path| policy
|
||||||
|
.resolve_access_for_local_path_with_cwd(path.as_path(), root.as_path())),
|
||||||
[Write, Read, Write]
|
[Write, Read, Write]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -190,7 +194,7 @@ fn exact_denies_nested_read_carveouts_and_reopened_writes_are_preserved() {
|
|||||||
&root.join("credentials.env"),
|
&root.join("credentials.env"),
|
||||||
&root.join("credentials.token"),
|
&root.join("credentials.token"),
|
||||||
]
|
]
|
||||||
.map(|path| denies.is_read_denied(path.as_path())),
|
.map(|path| denies.is_local_path_read_denied(path.as_path())),
|
||||||
[true, true, true, true]
|
[true, true, true, true]
|
||||||
);
|
);
|
||||||
assert_eq!(policy.glob_scan_max_depth, Some(4));
|
assert_eq!(policy.glob_scan_max_depth, Some(4));
|
||||||
@@ -317,7 +321,7 @@ fn canonical_grants_cannot_escape_through_readable_or_writable_symlinks() {
|
|||||||
let parent = rooted(&root, Write, []);
|
let parent = rooted(&root, Write, []);
|
||||||
let child = rooted(&escaped, Write, []);
|
let child = rooted(&escaped, Write, []);
|
||||||
let policy = intersection(&parent, &child, &root).file_system_sandbox_policy();
|
let policy = intersection(&parent, &child, &root).file_system_sandbox_policy();
|
||||||
assert!(!policy.can_write_path_with_cwd(outside.as_path(), root.as_path()));
|
assert!(!policy.can_write_local_path_with_cwd(outside.as_path(), root.as_path()));
|
||||||
|
|
||||||
let parent = managed(vec![entry(root.as_path(), Read)]);
|
let parent = managed(vec![entry(root.as_path(), Read)]);
|
||||||
let child = managed(vec![entry(escaped.as_path(), Read)]);
|
let child = managed(vec![entry(escaped.as_path(), Read)]);
|
||||||
@@ -329,7 +333,7 @@ fn canonical_grants_cannot_escape_through_readable_or_writable_symlinks() {
|
|||||||
let parent = rooted(&root, Write, []);
|
let parent = rooted(&root, Write, []);
|
||||||
let child = rooted(&internal_alias, Write, []);
|
let child = rooted(&internal_alias, Write, []);
|
||||||
let policy = intersection(&parent, &child, &root).file_system_sandbox_policy();
|
let policy = intersection(&parent, &child, &root).file_system_sandbox_policy();
|
||||||
assert!(policy.can_write_path_with_cwd(inside.as_path(), root.as_path()));
|
assert!(policy.can_write_local_path_with_cwd(inside.as_path(), root.as_path()));
|
||||||
assert!(
|
assert!(
|
||||||
!policy
|
!policy
|
||||||
.entries
|
.entries
|
||||||
@@ -355,7 +359,7 @@ fn macos_system_path_aliases_share_the_same_physical_permissions() {
|
|||||||
let right = managed(vec![entry(canonical.as_path(), access)]);
|
let right = managed(vec![entry(canonical.as_path(), access)]);
|
||||||
let policy = intersection(&left, &right, &canonical).file_system_sandbox_policy();
|
let policy = intersection(&left, &right, &canonical).file_system_sandbox_policy();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(canonical.as_path(), canonical.as_path()),
|
policy.resolve_access_for_local_path_with_cwd(canonical.as_path(), canonical.as_path()),
|
||||||
access
|
access
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -365,5 +369,5 @@ fn macos_system_path_aliases_share_the_same_physical_permissions() {
|
|||||||
&canonical,
|
&canonical,
|
||||||
)
|
)
|
||||||
.file_system_sandbox_policy();
|
.file_system_sandbox_policy();
|
||||||
assert!(policy.can_write_path_with_cwd(canonical.as_path(), canonical.as_path()));
|
assert!(policy.can_write_local_path_with_cwd(canonical.as_path(), canonical.as_path()));
|
||||||
}
|
}
|
||||||
@@ -325,28 +325,12 @@ impl ReadDenyMatcher {
|
|||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a matcher from exact deny-read roots and deny-read glob entries.
|
/// Builds a local-path matcher for callers that must reject malformed glob patterns.
|
||||||
///
|
|
||||||
/// Returns `None` when the policy has no deny-read restrictions, so callers
|
|
||||||
/// can skip read-deny checks without allocating matcher state. The `cwd`
|
|
||||||
/// resolves cwd-relative policy paths and special paths before matching.
|
|
||||||
pub fn new(file_system_sandbox_policy: &FileSystemSandboxPolicy, cwd: &Path) -> Option<Self> {
|
|
||||||
match Self::build(
|
|
||||||
file_system_sandbox_policy,
|
|
||||||
cwd,
|
|
||||||
InvalidDenyReadGlobBehavior::FailClosed,
|
|
||||||
) {
|
|
||||||
Ok(matcher) => matcher,
|
|
||||||
Err(_) => unreachable!("fail-closed glob handling does not return errors"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a matcher for callers that must reject malformed glob patterns.
|
|
||||||
///
|
///
|
||||||
/// Runtime read checks intentionally fail closed on malformed deny patterns.
|
/// Runtime read checks intentionally fail closed on malformed deny patterns.
|
||||||
/// Host-side expansion work should use this constructor instead so a typo
|
/// Host-side expansion work should use this constructor instead so a typo
|
||||||
/// cannot broaden the set of paths it mutates before execution starts.
|
/// cannot broaden the set of paths it mutates before execution starts.
|
||||||
pub fn try_new(
|
pub fn try_new_for_local_paths(
|
||||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
) -> Result<Option<Self>, String> {
|
) -> Result<Option<Self>, String> {
|
||||||
@@ -386,8 +370,8 @@ impl ReadDenyMatcher {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether `path` is denied by the policy used to build this matcher.
|
/// Returns whether a local native `path` is denied by this matcher.
|
||||||
pub fn is_read_denied(&self, path: &Path) -> bool {
|
pub fn is_local_path_read_denied(&self, path: &Path) -> bool {
|
||||||
let Some(cwd) = self.native_cwd.as_ref() else {
|
let Some(cwd) = self.native_cwd.as_ref() else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -421,8 +405,12 @@ impl ReadDenyMatcher {
|
|||||||
/// parent and a non-symlink directory entry. Symlinks and Windows junctions
|
/// parent and a non-symlink directory entry. Symlinks and Windows junctions
|
||||||
/// must be resolved separately. Do not reuse these locations across walks:
|
/// must be resolved separately. Do not reuse these locations across walks:
|
||||||
/// a later operation must observe newly created files and changed links.
|
/// a later operation must observe newly created files and changed links.
|
||||||
pub fn is_read_denied_with_canonical_path(&self, path: &Path, canonical_path: &Path) -> bool {
|
pub fn is_local_path_read_denied_with_canonical_path(
|
||||||
self.is_read_denied(path) || self.is_read_denied(canonical_path)
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
canonical_path: &Path,
|
||||||
|
) -> bool {
|
||||||
|
self.is_local_path_read_denied(path) || self.is_local_path_read_denied(canonical_path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,7 +629,14 @@ impl FileSystemSandboxPolicy {
|
|||||||
self.entries.retain(|entry| !entry.skips_missing_path());
|
self.entries.retain(|entry| !entry.skips_missing_path());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_explicit_non_write_entry_for_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool {
|
/// Native-path compatibility adapter for local executor boundaries.
|
||||||
|
/// Selected-executor orchestrators must keep paths as [`PathUri`] and use
|
||||||
|
/// context-aware policy APIs instead.
|
||||||
|
pub fn has_explicit_non_write_entry_for_local_path_with_cwd(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
cwd: &Path,
|
||||||
|
) -> bool {
|
||||||
let Some(path) = resolve_candidate_path(path, cwd) else {
|
let Some(path) = resolve_candidate_path(path, cwd) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -930,18 +925,28 @@ impl FileSystemSandboxPolicy {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_access_with_cwd(&self, path: &Path, cwd: &Path) -> FileSystemAccessMode {
|
/// Native-path compatibility adapter for local executor boundaries.
|
||||||
|
/// Selected-executor orchestrators must use [`Self::resolve_access`] with
|
||||||
|
/// a [`PathUri`] and [`FileSystemSandboxPolicyContext`] instead.
|
||||||
|
pub fn resolve_access_for_local_path_with_cwd(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
cwd: &Path,
|
||||||
|
) -> FileSystemAccessMode {
|
||||||
with_local_policy_context(path, cwd, |path, context| {
|
with_local_policy_context(path, cwd, |path, context| {
|
||||||
self.resolve_access(path, context)
|
self.resolve_access(path, context)
|
||||||
})
|
})
|
||||||
.unwrap_or(FileSystemAccessMode::Deny)
|
.unwrap_or(FileSystemAccessMode::Deny)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_read_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool {
|
/// Native-path compatibility adapter for local executor boundaries.
|
||||||
self.resolve_access_with_cwd(path, cwd).can_read()
|
pub fn can_read_local_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool {
|
||||||
|
self.resolve_access_for_local_path_with_cwd(path, cwd)
|
||||||
|
.can_read()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_write_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool {
|
/// Native-path compatibility adapter for local executor boundaries.
|
||||||
|
pub fn can_write_local_path_with_cwd(&self, path: &Path, cwd: &Path) -> bool {
|
||||||
with_local_policy_context(path, cwd, |path, context| {
|
with_local_policy_context(path, cwd, |path, context| {
|
||||||
self.can_write_path(path, context)
|
self.can_write_path(path, context)
|
||||||
})
|
})
|
||||||
@@ -1166,38 +1171,6 @@ impl FileSystemSandboxPolicy {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces symbolic `:workspace_roots` entries with absolute paths resolved
|
|
||||||
/// against `cwd`.
|
|
||||||
///
|
|
||||||
/// Use this when a durable permission profile must survive a cwd-only
|
|
||||||
/// update without rebinding its project-root authority to the new cwd.
|
|
||||||
pub fn materialize_project_roots_with_cwd(mut self, cwd: &Path) -> Self {
|
|
||||||
let cwd = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
|
||||||
for entry in &mut self.entries {
|
|
||||||
match &entry.path {
|
|
||||||
FileSystemPath::Special {
|
|
||||||
value: FileSystemSpecialPath::ProjectRoots { .. },
|
|
||||||
} => {
|
|
||||||
if let Some(path) = resolve_file_system_path(&entry.path, cwd.as_ref()) {
|
|
||||||
entry.path = path.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FileSystemPath::GlobPattern { pattern } => {
|
|
||||||
if let (Some(cwd), Some(subpath)) =
|
|
||||||
(cwd.as_ref(), parse_project_roots_glob_pattern(pattern))
|
|
||||||
{
|
|
||||||
entry.path = FileSystemPath::GlobPattern {
|
|
||||||
pattern: resolve_project_roots_glob_pattern(subpath, cwd),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FileSystemPath::Special { value: _ } => {}
|
|
||||||
FileSystemPath::Path { .. } => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces symbolic `:workspace_roots` entries with concrete entries for
|
/// Replaces symbolic `:workspace_roots` entries with concrete entries for
|
||||||
/// each workspace root.
|
/// each workspace root.
|
||||||
pub fn materialize_project_roots_with_workspace_roots(
|
pub fn materialize_project_roots_with_workspace_roots(
|
||||||
@@ -1344,7 +1317,7 @@ impl FileSystemSandboxPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for path in additional_readable_roots {
|
for path in additional_readable_roots {
|
||||||
if self.can_read_path_with_cwd(path.as_path(), cwd) {
|
if self.can_read_local_path_with_cwd(path.as_path(), cwd) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1363,7 +1336,7 @@ impl FileSystemSandboxPolicy {
|
|||||||
additional_writable_roots: &[AbsolutePathBuf],
|
additional_writable_roots: &[AbsolutePathBuf],
|
||||||
) -> Self {
|
) -> Self {
|
||||||
for path in additional_writable_roots {
|
for path in additional_writable_roots {
|
||||||
if self.can_write_path_with_cwd(path.as_path(), cwd) {
|
if self.can_write_local_path_with_cwd(path.as_path(), cwd) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1452,7 +1425,7 @@ impl FileSystemSandboxPolicy {
|
|||||||
self.resolved_entries_with_cwd(cwd)
|
self.resolved_entries_with_cwd(cwd)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|entry| entry.access.can_read())
|
.filter(|entry| entry.access.can_read())
|
||||||
.filter(|entry| self.can_read_path_with_cwd(entry.path.as_path(), cwd))
|
.filter(|entry| self.can_read_local_path_with_cwd(entry.path.as_path(), cwd))
|
||||||
.map(|entry| entry.path)
|
.map(|entry| entry.path)
|
||||||
.collect(),
|
.collect(),
|
||||||
/*normalize_effective_paths*/ true,
|
/*normalize_effective_paths*/ true,
|
||||||
@@ -1465,18 +1438,6 @@ impl FileSystemSandboxPolicy {
|
|||||||
self.get_writable_roots_with_cwd_impl(cwd, WritableRootPathResolution::Effective)
|
self.get_writable_roots_with_cwd_impl(cwd, WritableRootPathResolution::Effective)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether any effective writable root exists without materializing its carveouts.
|
|
||||||
pub fn has_writable_roots_with_cwd(&self, cwd: &Path) -> bool {
|
|
||||||
!self.has_full_disk_write_access()
|
|
||||||
&& self
|
|
||||||
.resolved_entries_with_cwd(cwd)
|
|
||||||
.into_iter()
|
|
||||||
.any(|entry| {
|
|
||||||
entry.access.can_write()
|
|
||||||
&& self.can_write_path_with_cwd(entry.path.as_path(), cwd)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reports configured writable roots for diagnostics without inspecting the filesystem.
|
/// Reports configured writable roots for diagnostics without inspecting the filesystem.
|
||||||
///
|
///
|
||||||
/// Unlike runtime root resolution, this includes configured roots even if they
|
/// Unlike runtime root resolution, this includes configured roots even if they
|
||||||
@@ -1531,7 +1492,7 @@ impl FileSystemSandboxPolicy {
|
|||||||
let writable_entries: Vec<AbsolutePathBuf> = resolved_entries
|
let writable_entries: Vec<AbsolutePathBuf> = resolved_entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|entry| entry.access.can_write())
|
.filter(|entry| entry.access.can_write())
|
||||||
.filter(|entry| self.can_write_path_with_cwd(entry.path.as_path(), cwd))
|
.filter(|entry| self.can_write_local_path_with_cwd(entry.path.as_path(), cwd))
|
||||||
.map(|entry| entry.path.clone())
|
.map(|entry| entry.path.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -1578,7 +1539,7 @@ impl FileSystemSandboxPolicy {
|
|||||||
resolved_entries
|
resolved_entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|entry| !entry.access.can_write())
|
.filter(|entry| !entry.access.can_write())
|
||||||
.filter(|entry| !self.can_write_path_with_cwd(entry.path.as_path(), cwd))
|
.filter(|entry| !self.can_write_local_path_with_cwd(entry.path.as_path(), cwd))
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
let effective_path = path_resolution.resolve(entry.path.clone());
|
let effective_path = path_resolution.resolve(entry.path.clone());
|
||||||
// Preserve the literal in-root path whenever the
|
// Preserve the literal in-root path whenever the
|
||||||
@@ -1654,7 +1615,7 @@ impl FileSystemSandboxPolicy {
|
|||||||
self.resolved_entries_with_cwd(cwd)
|
self.resolved_entries_with_cwd(cwd)
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|entry| entry.access == FileSystemAccessMode::Deny)
|
.filter(|entry| entry.access == FileSystemAccessMode::Deny)
|
||||||
.filter(|entry| !self.can_read_path_with_cwd(entry.path.as_path(), cwd))
|
.filter(|entry| !self.can_read_local_path_with_cwd(entry.path.as_path(), cwd))
|
||||||
// Restricted policies already deny reads outside explicit allow roots,
|
// Restricted policies already deny reads outside explicit allow roots,
|
||||||
// so materializing the filesystem root here would erase narrower
|
// so materializing the filesystem root here would erase narrower
|
||||||
// readable carveouts when downstream sandboxes apply deny masks last.
|
// readable carveouts when downstream sandboxes apply deny masks last.
|
||||||
@@ -2408,10 +2369,9 @@ fn protected_metadata_names_for_writable_root(
|
|||||||
.map(|raw_root| raw_root.join(*metadata_name)),
|
.map(|raw_root| raw_root.join(*metadata_name)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if metadata_paths
|
if metadata_paths.iter().all(|metadata_path| {
|
||||||
.iter()
|
!policy.can_write_local_path_with_cwd(metadata_path.as_path(), cwd)
|
||||||
.all(|metadata_path| !policy.can_write_path_with_cwd(metadata_path.as_path(), cwd))
|
}) {
|
||||||
{
|
|
||||||
protected_names.push((*metadata_name).to_string());
|
protected_names.push((*metadata_name).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2584,10 +2544,6 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for policy in policies {
|
for policy in policies {
|
||||||
assert_eq!(
|
|
||||||
policy.has_writable_roots_with_cwd(cwd.path()),
|
|
||||||
!policy.get_writable_roots_with_cwd(cwd.path()).is_empty()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.has_configured_writable_roots_with_cwd(cwd.path()),
|
policy.has_configured_writable_roots_with_cwd(cwd.path()),
|
||||||
!policy.get_writable_roots_with_cwd(cwd.path()).is_empty()
|
!policy.get_writable_roots_with_cwd(cwd.path()).is_empty()
|
||||||
@@ -2632,11 +2588,11 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!policy.can_read_path_with_cwd(Path::new(path), cwd.path()),
|
!policy.can_read_local_path_with_cwd(Path::new(path), cwd.path()),
|
||||||
"deny should apply to {path}"
|
"deny should apply to {path}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
policy.can_read_path_with_cwd(&cwd.path().join("ordinary"), cwd.path()),
|
policy.can_read_local_path_with_cwd(&cwd.path().join("ordinary"), cwd.path()),
|
||||||
"opaque deny for {path} should not poison ordinary paths"
|
"opaque deny for {path} should not poison ordinary paths"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3044,7 +3000,7 @@ mod tests {
|
|||||||
/*exclude_tmpdir_env_var*/ true,
|
/*exclude_tmpdir_env_var*/ true,
|
||||||
/*exclude_slash_tmp*/ false,
|
/*exclude_slash_tmp*/ false,
|
||||||
)
|
)
|
||||||
.can_write_path_with_cwd(slash_tmp.as_path(), cwd.path())
|
.can_write_local_path_with_cwd(slash_tmp.as_path(), cwd.path())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3277,12 +3233,10 @@ mod tests {
|
|||||||
.contains(&explicit_dot_codex),
|
.contains(&explicit_dot_codex),
|
||||||
"explicit .codex rule should win over the default protected carveout"
|
"explicit .codex rule should win over the default protected carveout"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(policy.can_write_local_path_with_cwd(
|
||||||
policy.can_write_path_with_cwd(
|
explicit_dot_codex.join("config.toml").as_path(),
|
||||||
explicit_dot_codex.join("config.toml").as_path(),
|
cwd.path()
|
||||||
cwd.path()
|
));
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3299,9 +3253,9 @@ mod tests {
|
|||||||
missing_path_behavior: None,
|
missing_path_behavior: None,
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
assert!(!file_system_policy.can_write_path_with_cwd(&dot_git_config, cwd.path()));
|
assert!(!file_system_policy.can_write_local_path_with_cwd(&dot_git_config, cwd.path()));
|
||||||
assert!(!file_system_policy.can_write_path_with_cwd(&dot_agents_config, cwd.path()));
|
assert!(!file_system_policy.can_write_local_path_with_cwd(&dot_agents_config, cwd.path()));
|
||||||
assert!(!file_system_policy.can_write_path_with_cwd(&dot_codex_config, cwd.path()));
|
assert!(!file_system_policy.can_write_local_path_with_cwd(&dot_codex_config, cwd.path()));
|
||||||
|
|
||||||
let writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd.path());
|
let writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd.path());
|
||||||
assert_eq!(writable_roots.len(), 1);
|
assert_eq!(writable_roots.len(), 1);
|
||||||
@@ -3385,18 +3339,17 @@ mod tests {
|
|||||||
Some(".git")
|
Some(".git")
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
file_system_policy.can_write_path_with_cwd(Path::new("src/main.rs"), relative_cwd,)
|
file_system_policy
|
||||||
|
.can_write_local_path_with_cwd(Path::new("src/main.rs"), relative_cwd,)
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!file_system_policy
|
!file_system_policy
|
||||||
.can_write_path_with_cwd(Path::new(".codex/config.toml"), relative_cwd,)
|
.can_write_local_path_with_cwd(Path::new(".codex/config.toml"), relative_cwd,)
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!file_system_policy.can_write_path_with_cwd(
|
|
||||||
Path::new(".agents/skills/example/SKILL.md"),
|
|
||||||
relative_cwd,
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
assert!(!file_system_policy.can_write_local_path_with_cwd(
|
||||||
|
Path::new(".agents/skills/example/SKILL.md"),
|
||||||
|
relative_cwd,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -3777,7 +3730,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_access_with_cwd_uses_most_specific_entry() {
|
fn resolve_access_for_local_path_with_cwd_uses_most_specific_entry() {
|
||||||
let cwd = TempDir::new().expect("tempdir");
|
let cwd = TempDir::new().expect("tempdir");
|
||||||
let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path());
|
let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path());
|
||||||
let docs_private = AbsolutePathBuf::resolve_path_against_base("docs/private", cwd.path());
|
let docs_private = AbsolutePathBuf::resolve_path_against_base("docs/private", cwd.path());
|
||||||
@@ -3813,19 +3766,20 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(cwd.path(), cwd.path()),
|
policy.resolve_access_for_local_path_with_cwd(cwd.path(), cwd.path()),
|
||||||
FileSystemAccessMode::Write
|
FileSystemAccessMode::Write
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
|
policy.resolve_access_for_local_path_with_cwd(docs.as_path(), cwd.path()),
|
||||||
FileSystemAccessMode::Read
|
FileSystemAccessMode::Read
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(docs_private.as_path(), cwd.path()),
|
policy.resolve_access_for_local_path_with_cwd(docs_private.as_path(), cwd.path()),
|
||||||
FileSystemAccessMode::Deny
|
FileSystemAccessMode::Deny
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(docs_private_public.as_path(), cwd.path()),
|
policy
|
||||||
|
.resolve_access_for_local_path_with_cwd(docs_private_public.as_path(), cwd.path()),
|
||||||
FileSystemAccessMode::Write
|
FileSystemAccessMode::Write
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3939,7 +3893,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!policy.has_full_disk_write_access());
|
assert!(!policy.has_full_disk_write_access());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
|
policy.resolve_access_for_local_path_with_cwd(docs.as_path(), cwd.path()),
|
||||||
FileSystemAccessMode::Read
|
FileSystemAccessMode::Read
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -3978,7 +3932,7 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
|
policy.resolve_access_for_local_path_with_cwd(docs.as_path(), cwd.path()),
|
||||||
FileSystemAccessMode::Read
|
FileSystemAccessMode::Read
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -4013,7 +3967,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(!policy.has_full_disk_write_access());
|
assert!(!policy.has_full_disk_write_access());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(root.as_path(), cwd.path()),
|
policy.resolve_access_for_local_path_with_cwd(root.as_path(), cwd.path()),
|
||||||
FileSystemAccessMode::Deny
|
FileSystemAccessMode::Deny
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4044,7 +3998,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(policy.has_full_disk_write_access());
|
assert!(policy.has_full_disk_write_access());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
policy.resolve_access_with_cwd(docs.as_path(), cwd.path()),
|
policy.resolve_access_for_local_path_with_cwd(docs.as_path(), cwd.path()),
|
||||||
FileSystemAccessMode::Write
|
FileSystemAccessMode::Write
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4215,33 +4169,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn materialize_project_roots_with_cwd_expands_symbolic_glob_entries() {
|
|
||||||
let cwd = TempDir::new().expect("tempdir");
|
|
||||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
|
||||||
path: FileSystemPath::GlobPattern {
|
|
||||||
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
|
|
||||||
},
|
|
||||||
access: FileSystemAccessMode::Deny,
|
|
||||||
missing_path_behavior: None,
|
|
||||||
}]);
|
|
||||||
|
|
||||||
let actual = policy.materialize_project_roots_with_cwd(cwd.path());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
actual,
|
|
||||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
|
||||||
path: FileSystemPath::GlobPattern {
|
|
||||||
pattern: AbsolutePathBuf::resolve_path_against_base("**/*.env", cwd.path())
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned(),
|
|
||||||
},
|
|
||||||
access: FileSystemAccessMode::Deny,
|
|
||||||
missing_path_behavior: None,
|
|
||||||
}])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn with_additional_legacy_workspace_writable_roots_protects_metadata() {
|
fn with_additional_legacy_workspace_writable_roots_protects_metadata() {
|
||||||
let temp_dir = TempDir::new().expect("tempdir");
|
let temp_dir = TempDir::new().expect("tempdir");
|
||||||
@@ -4375,8 +4302,9 @@ mod tests {
|
|||||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
ReadDenyMatcher::new(file_system_sandbox_policy, cwd)
|
ReadDenyMatcher::try_new_for_local_paths(file_system_sandbox_policy, cwd)
|
||||||
.is_some_and(|matcher| matcher.is_read_denied(path))
|
.expect("valid deny-read globs")
|
||||||
|
.is_some_and(|matcher| matcher.is_local_path_read_denied(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -686,11 +686,13 @@ fn compatibility_workspace_write_policy(
|
|||||||
.and_then(|tmpdir| {
|
.and_then(|tmpdir| {
|
||||||
AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()
|
AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()
|
||||||
})
|
})
|
||||||
.is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd));
|
.is_some_and(|tmpdir| {
|
||||||
|
file_system_policy.can_write_local_path_with_cwd(tmpdir.as_path(), cwd)
|
||||||
|
});
|
||||||
let slash_tmp = Path::new("/tmp");
|
let slash_tmp = Path::new("/tmp");
|
||||||
let slash_tmp_writable = slash_tmp.is_absolute()
|
let slash_tmp_writable = slash_tmp.is_absolute()
|
||||||
&& slash_tmp.is_dir()
|
&& slash_tmp.is_dir()
|
||||||
&& file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);
|
&& file_system_policy.can_write_local_path_with_cwd(slash_tmp, cwd);
|
||||||
|
|
||||||
SandboxPolicy::WorkspaceWrite {
|
SandboxPolicy::WorkspaceWrite {
|
||||||
writable_roots,
|
writable_roots,
|
||||||
|
|||||||
@@ -12,11 +12,9 @@ use codex_protocol::permissions::FileSystemSpecialPath;
|
|||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
use codex_protocol::permissions::ReadDenyMatcher;
|
use codex_protocol::permissions::ReadDenyMatcher;
|
||||||
use codex_protocol::permissions::file_system_root;
|
use codex_protocol::permissions::file_system_root;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
||||||
use codex_utils_path_uri::PathConvention;
|
use codex_utils_path_uri::PathConvention;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub fn normalize_additional_permissions(
|
pub fn normalize_additional_permissions(
|
||||||
additional_permissions: AdditionalPermissionProfile,
|
additional_permissions: AdditionalPermissionProfile,
|
||||||
@@ -81,18 +79,6 @@ pub fn normalize_additional_permissions_with_context(
|
|||||||
/// Resolves cwd-dependent permission entries without filtering their authority.
|
/// Resolves cwd-dependent permission entries without filtering their authority.
|
||||||
///
|
///
|
||||||
/// Unlike intersection, this preserves narrower grants beneath denied paths.
|
/// Unlike intersection, this preserves narrower grants beneath denied paths.
|
||||||
pub fn materialize_additional_permissions(
|
|
||||||
additional_permissions: AdditionalPermissionProfile,
|
|
||||||
cwd: &Path,
|
|
||||||
) -> Result<AdditionalPermissionProfile, String> {
|
|
||||||
let cwd = AbsolutePathBuf::from_absolute_path(cwd)
|
|
||||||
.map_err(|err| format!("invalid permission cwd: {err}"))?;
|
|
||||||
let cwd = PathUri::from(cwd);
|
|
||||||
with_local_context_for_cwd(&cwd, |context| {
|
|
||||||
materialize_additional_permissions_with_context(additional_permissions, context)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn materialize_additional_permissions_with_context(
|
pub fn materialize_additional_permissions_with_context(
|
||||||
mut additional_permissions: AdditionalPermissionProfile,
|
mut additional_permissions: AdditionalPermissionProfile,
|
||||||
context: &FileSystemSandboxPolicyContext<'_>,
|
context: &FileSystemSandboxPolicyContext<'_>,
|
||||||
@@ -158,21 +144,6 @@ pub fn merge_permission_profiles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn intersect_permission_profiles(
|
|
||||||
requested: AdditionalPermissionProfile,
|
|
||||||
granted: AdditionalPermissionProfile,
|
|
||||||
cwd: &Path,
|
|
||||||
) -> AdditionalPermissionProfile {
|
|
||||||
AbsolutePathBuf::from_absolute_path(cwd)
|
|
||||||
.ok()
|
|
||||||
.map(PathUri::from)
|
|
||||||
.map_or_else(AdditionalPermissionProfile::default, |cwd| {
|
|
||||||
with_local_context_for_cwd(&cwd, |context| {
|
|
||||||
intersect_permission_profiles_with_context(requested, granted, context)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn intersect_permission_profiles_with_context(
|
pub fn intersect_permission_profiles_with_context(
|
||||||
requested: AdditionalPermissionProfile,
|
requested: AdditionalPermissionProfile,
|
||||||
granted: AdditionalPermissionProfile,
|
granted: AdditionalPermissionProfile,
|
||||||
@@ -248,46 +219,6 @@ pub fn intersect_permission_profiles_with_context(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_local_context_for_cwd<T>(
|
|
||||||
cwd: &PathUri,
|
|
||||||
evaluate: impl FnOnce(&FileSystemSandboxPolicyContext<'_>) -> T,
|
|
||||||
) -> T {
|
|
||||||
let user_home_dir = PathUri::from_host_native_path("~").ok();
|
|
||||||
let local_cwd = std::env::current_dir().ok();
|
|
||||||
let temporary_directory_env_vars: &[&str] = if cfg!(windows) {
|
|
||||||
&["TEMP", "TMP"]
|
|
||||||
} else {
|
|
||||||
&["TMPDIR"]
|
|
||||||
};
|
|
||||||
let normalize_temp_path = |path: std::ffi::OsString| {
|
|
||||||
PathUri::from_host_native_path(&path).ok().or_else(|| {
|
|
||||||
if cfg!(unix) {
|
|
||||||
PathUri::from_host_native_path(local_cwd.as_ref()?.join(path)).ok()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
let mut temporary_directories = Vec::new();
|
|
||||||
for name in temporary_directory_env_vars {
|
|
||||||
if let Some(path) = std::env::var_os(name)
|
|
||||||
.filter(|path| !path.is_empty())
|
|
||||||
.filter(|path| cfg!(unix) || Path::new(path).is_absolute())
|
|
||||||
.and_then(&normalize_temp_path)
|
|
||||||
&& !temporary_directories.contains(&path)
|
|
||||||
{
|
|
||||||
temporary_directories.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let context = FileSystemSandboxPolicyContext {
|
|
||||||
cwd,
|
|
||||||
workspace_roots: std::slice::from_ref(cwd),
|
|
||||||
user_home_dir: user_home_dir.as_ref(),
|
|
||||||
temporary_directories: Some(&temporary_directories),
|
|
||||||
};
|
|
||||||
evaluate(&context)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_glob_scan_max_depth(
|
fn merge_glob_scan_max_depth(
|
||||||
left_entries: &[FileSystemSandboxEntry],
|
left_entries: &[FileSystemSandboxEntry],
|
||||||
left_depth: Option<usize>,
|
left_depth: Option<usize>,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use super::effective_file_system_sandbox_policy;
|
use super::effective_file_system_sandbox_policy;
|
||||||
use super::intersect_permission_profiles;
|
|
||||||
use super::intersect_permission_profiles_with_context;
|
use super::intersect_permission_profiles_with_context;
|
||||||
use super::materialize_additional_permissions;
|
|
||||||
use super::materialize_additional_permissions_with_context;
|
use super::materialize_additional_permissions_with_context;
|
||||||
use super::merge_file_system_policy_with_additional_permissions;
|
use super::merge_file_system_policy_with_additional_permissions;
|
||||||
use super::normalize_additional_permissions;
|
use super::normalize_additional_permissions;
|
||||||
@@ -23,7 +21,6 @@ use codex_utils_path_uri::PathConvention;
|
|||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use dunce::canonicalize;
|
use dunce::canonicalize;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
#[cfg(unix)]
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -32,6 +29,21 @@ fn symlink_dir(original: &Path, link: &Path) -> std::io::Result<()> {
|
|||||||
std::os::unix::fs::symlink(original, link)
|
std::os::unix::fs::symlink(original, link)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn intersect_permission_profiles_with_local_cwd(
|
||||||
|
requested: PermissionProfile,
|
||||||
|
granted: PermissionProfile,
|
||||||
|
cwd: &Path,
|
||||||
|
) -> PermissionProfile {
|
||||||
|
let cwd = PathUri::from_host_native_path(cwd).expect("cwd URI");
|
||||||
|
let context = FileSystemSandboxPolicyContext {
|
||||||
|
cwd: &cwd,
|
||||||
|
workspace_roots: std::slice::from_ref(&cwd),
|
||||||
|
user_home_dir: None,
|
||||||
|
temporary_directories: None,
|
||||||
|
};
|
||||||
|
intersect_permission_profiles_with_context(requested, granted, &context)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn full_access_restricted_policy_skips_platform_sandbox_when_network_is_enabled() {
|
fn full_access_restricted_policy_skips_platform_sandbox_when_network_is_enabled() {
|
||||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||||
@@ -303,6 +315,13 @@ fn materialize_additional_permissions_preserves_authority_and_constraints() {
|
|||||||
)
|
)
|
||||||
.expect("absolute temp dir");
|
.expect("absolute temp dir");
|
||||||
let home = PathUri::from_host_native_path("~").expect("host home");
|
let home = PathUri::from_host_native_path("~").expect("host home");
|
||||||
|
let cwd_uri = PathUri::from(cwd.clone());
|
||||||
|
let context = FileSystemSandboxPolicyContext {
|
||||||
|
cwd: &cwd_uri,
|
||||||
|
workspace_roots: std::slice::from_ref(&cwd_uri),
|
||||||
|
user_home_dir: Some(&home),
|
||||||
|
temporary_directories: None,
|
||||||
|
};
|
||||||
let project_path = |subpath: &str| FileSystemPath::Special {
|
let project_path = |subpath: &str| FileSystemPath::Special {
|
||||||
value: FileSystemSpecialPath::project_roots(Some(subpath.to_owned())),
|
value: FileSystemSpecialPath::project_roots(Some(subpath.to_owned())),
|
||||||
};
|
};
|
||||||
@@ -342,7 +361,7 @@ fn materialize_additional_permissions_preserves_authority_and_constraints() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
materialize_additional_permissions(requested, cwd.as_path())
|
materialize_additional_permissions_with_context(requested, &context)
|
||||||
.expect("materialized permissions"),
|
.expect("materialized permissions"),
|
||||||
expected
|
expected
|
||||||
);
|
);
|
||||||
@@ -409,7 +428,7 @@ fn intersect_permission_profiles_preserves_explicit_empty_requested_reads() {
|
|||||||
let granted = requested.clone();
|
let granted = requested.clone();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested.clone(), granted, temp_dir.path()),
|
intersect_permission_profiles_with_local_cwd(requested.clone(), granted, temp_dir.path()),
|
||||||
requested
|
requested
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -430,7 +449,11 @@ fn intersect_permission_profiles_drops_ungranted_nonempty_path_requests() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, PermissionProfile::default(), temp_dir.path()),
|
intersect_permission_profiles_with_local_cwd(
|
||||||
|
requested,
|
||||||
|
PermissionProfile::default(),
|
||||||
|
temp_dir.path()
|
||||||
|
),
|
||||||
PermissionProfile::default()
|
PermissionProfile::default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -451,7 +474,11 @@ fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, PermissionProfile::default(), temp_dir.path()),
|
intersect_permission_profiles_with_local_cwd(
|
||||||
|
requested,
|
||||||
|
PermissionProfile::default(),
|
||||||
|
temp_dir.path()
|
||||||
|
),
|
||||||
PermissionProfile::default()
|
PermissionProfile::default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -491,7 +518,7 @@ fn intersect_permission_profiles_preserves_parent_relative_project_root_restrict
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted, cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted, cwd.as_path()),
|
||||||
PermissionProfile::default()
|
PermissionProfile::default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -522,11 +549,11 @@ fn intersect_permission_profiles_rejects_symbolic_slash_tmp_grants() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted.clone(), cwd.path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted.clone(), cwd.path()),
|
||||||
PermissionProfile::default()
|
PermissionProfile::default()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(granted.clone(), granted, cwd.path()),
|
intersect_permission_profiles_with_local_cwd(granted.clone(), granted, cwd.path()),
|
||||||
PermissionProfile::default()
|
PermissionProfile::default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -553,7 +580,7 @@ fn intersect_permission_profiles_preserves_deny_across_case_variant_grant() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(
|
intersect_permission_profiles_with_local_cwd(
|
||||||
profile(vec![requested_write, requested_deny.clone()]),
|
profile(vec![requested_write, requested_deny.clone()]),
|
||||||
profile(vec![granted_write.clone()]),
|
profile(vec![granted_write.clone()]),
|
||||||
requested_root.as_path(),
|
requested_root.as_path(),
|
||||||
@@ -698,7 +725,7 @@ fn intersect_permission_profiles_preserves_opaque_child_deny() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(
|
intersect_permission_profiles_with_local_cwd(
|
||||||
profile(vec![requested_write, requested_deny.clone()]),
|
profile(vec![requested_write, requested_deny.clone()]),
|
||||||
profile(vec![granted_write.clone()]),
|
profile(vec![granted_write.clone()]),
|
||||||
root.as_path(),
|
root.as_path(),
|
||||||
@@ -737,7 +764,7 @@ fn intersect_permission_profiles_accepts_child_path_granted_for_requested_cwd()
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted.clone(), cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted.clone(), cwd.as_path()),
|
||||||
granted
|
granted
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -763,7 +790,7 @@ fn intersect_permission_profiles_materializes_cwd_grant_for_reuse() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let intersected = intersect_permission_profiles(
|
let intersected = intersect_permission_profiles_with_local_cwd(
|
||||||
cwd_write_permissions.clone(),
|
cwd_write_permissions.clone(),
|
||||||
cwd_write_permissions,
|
cwd_write_permissions,
|
||||||
request_cwd.as_path(),
|
request_cwd.as_path(),
|
||||||
@@ -780,7 +807,7 @@ fn intersect_permission_profiles_materializes_cwd_grant_for_reuse() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(
|
intersect_permission_profiles_with_local_cwd(
|
||||||
PermissionProfile {
|
PermissionProfile {
|
||||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||||
/*read*/ None,
|
/*read*/ None,
|
||||||
@@ -822,7 +849,11 @@ fn intersect_permission_profiles_deduplicates_materialized_grants() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(permissions.clone(), permissions, cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(
|
||||||
|
permissions.clone(),
|
||||||
|
permissions,
|
||||||
|
cwd.as_path()
|
||||||
|
),
|
||||||
PermissionProfile {
|
PermissionProfile {
|
||||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||||
/*read*/ None,
|
/*read*/ None,
|
||||||
@@ -862,7 +893,11 @@ fn intersect_permission_profiles_materializes_cwd_deny_entries() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(permissions.clone(), permissions, request_cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(
|
||||||
|
permissions.clone(),
|
||||||
|
permissions,
|
||||||
|
request_cwd.as_path()
|
||||||
|
),
|
||||||
PermissionProfile {
|
PermissionProfile {
|
||||||
file_system: Some(FileSystemPermissions {
|
file_system: Some(FileSystemPermissions {
|
||||||
entries: vec![
|
entries: vec![
|
||||||
@@ -924,7 +959,7 @@ fn intersect_permission_profiles_drops_deny_entries_without_filesystem_grants()
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted.clone(), cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted.clone(), cwd.as_path()),
|
||||||
granted
|
granted
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -968,7 +1003,7 @@ fn intersect_permission_profiles_rejects_concrete_grants_matched_by_requested_de
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted, cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted, cwd.as_path()),
|
||||||
PermissionProfile::default()
|
PermissionProfile::default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1002,8 +1037,11 @@ fn intersect_permission_profiles_materializes_relative_deny_globs_for_reuse() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let intersected =
|
let intersected = intersect_permission_profiles_with_local_cwd(
|
||||||
intersect_permission_profiles(permissions.clone(), permissions, request_cwd.as_path());
|
permissions.clone(),
|
||||||
|
permissions,
|
||||||
|
request_cwd.as_path(),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersected,
|
intersected,
|
||||||
@@ -1029,7 +1067,7 @@ fn intersect_permission_profiles_materializes_relative_deny_globs_for_reuse() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(
|
intersect_permission_profiles_with_local_cwd(
|
||||||
PermissionProfile {
|
PermissionProfile {
|
||||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||||
/*read*/ None,
|
/*read*/ None,
|
||||||
@@ -1074,7 +1112,7 @@ fn intersect_permission_profiles_drops_broader_cwd_grant_for_requested_child_pat
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted, cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted, cwd.as_path()),
|
||||||
PermissionProfile::default()
|
PermissionProfile::default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1112,7 +1150,7 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted, cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted, cwd.as_path()),
|
||||||
PermissionProfile {
|
PermissionProfile {
|
||||||
file_system: Some(FileSystemPermissions {
|
file_system: Some(FileSystemPermissions {
|
||||||
entries: vec![
|
entries: vec![
|
||||||
@@ -1170,7 +1208,7 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intersect_permission_profiles(requested, granted, cwd.as_path()),
|
intersect_permission_profiles_with_local_cwd(requested, granted, cwd.as_path()),
|
||||||
PermissionProfile {
|
PermissionProfile {
|
||||||
file_system: Some(FileSystemPermissions {
|
file_system: Some(FileSystemPermissions {
|
||||||
entries: vec![
|
entries: vec![
|
||||||
|
|||||||
@@ -607,7 +607,7 @@ fn protected_metadata_names_for_writable_root(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let path = writable_root.root.join(*name);
|
let path = writable_root.root.join(*name);
|
||||||
if !file_system_sandbox_policy.can_write_path_with_cwd(path.as_path(), cwd) {
|
if !file_system_sandbox_policy.can_write_local_path_with_cwd(path.as_path(), cwd) {
|
||||||
names.push((*name).to_string());
|
names.push((*name).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,8 +254,10 @@ pub fn resolve_windows_elevated_filesystem_overrides(
|
|||||||
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
|
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
|
||||||
let has_explicit_write_carveouts = split_writable_roots.iter().any(|writable_root| {
|
let has_explicit_write_carveouts = split_writable_roots.iter().any(|writable_root| {
|
||||||
writable_root.read_only_subpaths.iter().any(|path| {
|
writable_root.read_only_subpaths.iter().any(|path| {
|
||||||
file_system_sandbox_policy
|
file_system_sandbox_policy.has_explicit_non_write_entry_for_local_path_with_cwd(
|
||||||
.has_explicit_non_write_entry_for_path_with_cwd(path.as_path(), sandbox_policy_cwd)
|
path.as_path(),
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path);
|
let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path);
|
||||||
@@ -322,7 +324,7 @@ pub fn resolve_windows_elevated_filesystem_overrides(
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
let explicitly_configured = file_system_sandbox_policy
|
let explicitly_configured = file_system_sandbox_policy
|
||||||
.has_explicit_non_write_entry_for_path_with_cwd(
|
.has_explicit_non_write_entry_for_local_path_with_cwd(
|
||||||
read_only_subpath.as_path(),
|
read_only_subpath.as_path(),
|
||||||
sandbox_policy_cwd,
|
sandbox_policy_cwd,
|
||||||
);
|
);
|
||||||
@@ -370,7 +372,7 @@ fn windows_policy_has_root_read_access(
|
|||||||
let Some(root) = cwd.as_path().ancestors().last() else {
|
let Some(root) = cwd.as_path().ancestors().last() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
file_system_sandbox_policy.can_read_path_with_cwd(root, cwd.as_path())
|
file_system_sandbox_policy.can_read_local_path_with_cwd(root, cwd.as_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn permission_profile_display_name(permission_profile: &PermissionProfile) -> &'static str {
|
fn permission_profile_display_name(permission_profile: &PermissionProfile) -> &'static str {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub fn add_dir_warning_message(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if file_system_policy.can_write_path_with_cwd(cwd, cwd) {
|
if file_system_policy.can_write_local_path_with_cwd(cwd, cwd) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1830,7 +1830,7 @@ fn sandbox_mode_from_permission_profile(
|
|||||||
.network_sandbox_policy()
|
.network_sandbox_policy()
|
||||||
.is_enabled()
|
.is_enabled()
|
||||||
.then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
|
.then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
|
||||||
} else if file_system_policy.can_write_path_with_cwd(cwd, cwd) {
|
} else if file_system_policy.can_write_local_path_with_cwd(cwd, cwd) {
|
||||||
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
|
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
|
||||||
} else {
|
} else {
|
||||||
Some(codex_app_server_protocol::SandboxMode::ReadOnly)
|
Some(codex_app_server_protocol::SandboxMode::ReadOnly)
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ impl ChatWidget {
|
|||||||
matches!(
|
matches!(
|
||||||
current_permission_profile,
|
current_permission_profile,
|
||||||
PermissionProfile::Managed { .. }
|
PermissionProfile::Managed { .. }
|
||||||
) && file_system_policy.can_write_path_with_cwd(cwd, cwd)
|
) && file_system_policy.can_write_local_path_with_cwd(cwd, cwd)
|
||||||
&& !file_system_policy.has_full_disk_write_access()
|
&& !file_system_policy.has_full_disk_write_access()
|
||||||
}
|
}
|
||||||
_ => current_permission_profile == &preset.permission_profile,
|
_ => current_permission_profile == &preset.permission_profile,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ impl ChatWidget {
|
|||||||
"Full Access mode"
|
"Full Access mode"
|
||||||
} else if profile
|
} else if profile
|
||||||
.file_system_sandbox_policy()
|
.file_system_sandbox_policy()
|
||||||
.can_write_path_with_cwd(self.config.cwd.as_path(), self.config.cwd.as_path())
|
.can_write_local_path_with_cwd(self.config.cwd.as_path(), self.config.cwd.as_path())
|
||||||
{
|
{
|
||||||
"Agent mode"
|
"Agent mode"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -126,8 +126,9 @@ pub(super) fn editor_directory(
|
|||||||
let Some(parent) = directory.parent() else {
|
let Some(parent) = directory.parent() else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
let is_writable = file_system_policy.can_write_path_with_cwd(directory, cwd)
|
let is_writable = file_system_policy
|
||||||
|| file_system_policy.can_write_path_with_cwd(parent, cwd)
|
.can_write_local_path_with_cwd(directory, cwd)
|
||||||
|
|| file_system_policy.can_write_local_path_with_cwd(parent, cwd)
|
||||||
|| writable_roots.iter().any(|root| {
|
|| writable_roots.iter().any(|root| {
|
||||||
root.is_path_writable(directory)
|
root.is_path_writable(directory)
|
||||||
|| root.root.as_path().starts_with(directory)
|
|| root.root.as_path().starts_with(directory)
|
||||||
|
|||||||
@@ -135,8 +135,8 @@ fn editor_directory_rejects_writable_codex_home_alias() {
|
|||||||
symlink(&paths.codex_home, &aliased_home).expect("create Codex home symlink");
|
symlink(&paths.codex_home, &aliased_home).expect("create Codex home symlink");
|
||||||
let policy = workspace_write_policy(&[]);
|
let policy = workspace_write_policy(&[]);
|
||||||
|
|
||||||
assert!(policy.can_write_path_with_cwd(&aliased_home.join("editor"), &paths.cwd));
|
assert!(policy.can_write_local_path_with_cwd(&aliased_home.join("editor"), &paths.cwd));
|
||||||
assert!(!policy.can_write_path_with_cwd(&paths.codex_home.join("editor"), &paths.cwd));
|
assert!(!policy.can_write_local_path_with_cwd(&paths.codex_home.join("editor"), &paths.cwd));
|
||||||
assert!(editor_directory(&[&aliased_home], &policy, &paths.cwd).is_err());
|
assert!(editor_directory(&[&aliased_home], &policy, &paths.cwd).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,8 +156,8 @@ fn editor_directory_rejects_writable_codex_home_alias_target() {
|
|||||||
symlink(&paths.codex_home, &aliased_home).expect("create Codex home symlink");
|
symlink(&paths.codex_home, &aliased_home).expect("create Codex home symlink");
|
||||||
let policy = workspace_write_policy(&[&paths.codex_home]);
|
let policy = workspace_write_policy(&[&paths.codex_home]);
|
||||||
|
|
||||||
assert!(!policy.can_write_path_with_cwd(&aliased_home.join("editor"), &paths.cwd));
|
assert!(!policy.can_write_local_path_with_cwd(&aliased_home.join("editor"), &paths.cwd));
|
||||||
assert!(policy.can_write_path_with_cwd(&paths.codex_home.join("editor"), &paths.cwd));
|
assert!(policy.can_write_local_path_with_cwd(&paths.codex_home.join("editor"), &paths.cwd));
|
||||||
assert!(editor_directory(&[&aliased_home], &policy, &paths.cwd).is_err());
|
assert!(editor_directory(&[&aliased_home], &policy, &paths.cwd).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,9 +177,9 @@ fn editor_directory_uses_protected_workspace_fallback_with_default_temporary_gra
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert!(!workspace_codex_home.exists());
|
assert!(!workspace_codex_home.exists());
|
||||||
assert!(policy.can_write_path_with_cwd(&codex_home, &cwd));
|
assert!(policy.can_write_local_path_with_cwd(&codex_home, &cwd));
|
||||||
assert!(!policy.can_write_path_with_cwd(&workspace_codex_home, &cwd));
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home, &cwd));
|
||||||
assert!(!policy.can_write_path_with_cwd(&workspace_codex_home.join("editor"), &cwd));
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home.join("editor"), &cwd));
|
||||||
|
|
||||||
let directory = editor_directory(&[&codex_home, &workspace_codex_home], &policy, &cwd)
|
let directory = editor_directory(&[&codex_home, &workspace_codex_home], &policy, &cwd)
|
||||||
.expect("use protected workspace metadata directory");
|
.expect("use protected workspace metadata directory");
|
||||||
@@ -210,8 +210,8 @@ fn editor_directory_rejects_explicitly_writable_workspace_fallback() {
|
|||||||
/*exclude_slash_tmp*/ false,
|
/*exclude_slash_tmp*/ false,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(policy.can_write_path_with_cwd(&codex_home, &cwd));
|
assert!(policy.can_write_local_path_with_cwd(&codex_home, &cwd));
|
||||||
assert!(policy.can_write_path_with_cwd(&workspace_codex_home, &cwd));
|
assert!(policy.can_write_local_path_with_cwd(&workspace_codex_home, &cwd));
|
||||||
assert!(
|
assert!(
|
||||||
editor_directory(&[&codex_home, &workspace_codex_home], &policy, &cwd).is_err(),
|
editor_directory(&[&codex_home, &workspace_codex_home], &policy, &cwd).is_err(),
|
||||||
"explicitly writable metadata must not be used for editor buffers"
|
"explicitly writable metadata must not be used for editor buffers"
|
||||||
@@ -229,8 +229,8 @@ fn editor_directory_rejects_workspace_fallback_symlink_to_writable_target() {
|
|||||||
symlink(&paths.codex_home, &workspace_codex_home).expect("create workspace metadata symlink");
|
symlink(&paths.codex_home, &workspace_codex_home).expect("create workspace metadata symlink");
|
||||||
let policy = workspace_write_policy(&[&paths.codex_home]);
|
let policy = workspace_write_policy(&[&paths.codex_home]);
|
||||||
|
|
||||||
assert!(!policy.can_write_path_with_cwd(&workspace_codex_home, &paths.cwd));
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home, &paths.cwd));
|
||||||
assert!(policy.can_write_path_with_cwd(&paths.codex_home, &paths.cwd));
|
assert!(policy.can_write_local_path_with_cwd(&paths.codex_home, &paths.cwd));
|
||||||
assert!(
|
assert!(
|
||||||
editor_directory(
|
editor_directory(
|
||||||
&[&paths.codex_home, &workspace_codex_home],
|
&[&paths.codex_home, &workspace_codex_home],
|
||||||
@@ -347,9 +347,9 @@ async fn editor_process_uses_protected_workspace_fallback_with_default_temporary
|
|||||||
];
|
];
|
||||||
|
|
||||||
assert!(!workspace_codex_home.exists());
|
assert!(!workspace_codex_home.exists());
|
||||||
assert!(policy.can_write_path_with_cwd(&codex_home, &cwd));
|
assert!(policy.can_write_local_path_with_cwd(&codex_home, &cwd));
|
||||||
assert!(policy.can_write_path_with_cwd(&default_codex_home, &cwd));
|
assert!(policy.can_write_local_path_with_cwd(&default_codex_home, &cwd));
|
||||||
assert!(!policy.can_write_path_with_cwd(&workspace_codex_home, &cwd));
|
assert!(!policy.can_write_local_path_with_cwd(&workspace_codex_home, &cwd));
|
||||||
|
|
||||||
let content = run_editor("seed", &editor_command, &codex_home, &policy, &cwd)
|
let content = run_editor("seed", &editor_command, &codex_home, &policy, &cwd)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ impl InlineVisualizationContext {
|
|||||||
context.viewer_dir.parent()?.parent()?.to_path_buf(),
|
context.viewer_dir.parent()?.parent()?.to_path_buf(),
|
||||||
];
|
];
|
||||||
for viewer_cache in viewer_caches {
|
for viewer_cache in viewer_caches {
|
||||||
if file_system_policy.can_write_path_with_cwd(&viewer_cache, config.cwd.as_path())
|
if file_system_policy.can_write_local_path_with_cwd(&viewer_cache, config.cwd.as_path())
|
||||||
|| file_system_policy
|
|| file_system_policy
|
||||||
.can_write_path_with_cwd(viewer_cache.parent()?, config.cwd.as_path())
|
.can_write_local_path_with_cwd(viewer_cache.parent()?, config.cwd.as_path())
|
||||||
|| writable_roots.iter().any(|root| {
|
|| writable_roots.iter().any(|root| {
|
||||||
root.is_path_writable(&viewer_cache)
|
root.is_path_writable(&viewer_cache)
|
||||||
|| root.root.as_path().starts_with(&viewer_cache)
|
|| root.root.as_path().starts_with(&viewer_cache)
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ pub fn resolve_windows_deny_read_paths(
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
let Some(matcher) = ReadDenyMatcher::try_new(&glob_policy, cwd.as_path())? else {
|
let Some(matcher) = ReadDenyMatcher::try_new_for_local_paths(&glob_policy, cwd.as_path())?
|
||||||
|
else {
|
||||||
return Ok(paths);
|
return Ok(paths);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ pub fn resolve_windows_deny_read_paths(
|
|||||||
|
|
||||||
let directory_scan_mode = if let Some(file_paths) = ripgrep_files(&scan_plan)? {
|
let directory_scan_mode = if let Some(file_paths) = ripgrep_files(&scan_plan)? {
|
||||||
for path in file_paths {
|
for path in file_paths {
|
||||||
if matcher.is_read_denied(&path) {
|
if matcher.is_local_path_read_denied(&path) {
|
||||||
push_absolute_path(&mut paths, &mut seen, path)?;
|
push_absolute_path(&mut paths, &mut seen, path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,8 @@ fn ripgrep_files(scan_plan: &GlobScanPlan) -> Result<Option<Vec<PathBuf>>, Strin
|
|||||||
// Ripgrep uses exit 2 for traversal errors, including protected
|
// Ripgrep uses exit 2 for traversal errors, including protected
|
||||||
// Windows directories. Its output may be incomplete. The caller
|
// Windows directories. Its output may be incomplete. The caller
|
||||||
// must enumerate again using the matcher-backed walker; policy
|
// must enumerate again using the matcher-backed walker; policy
|
||||||
// syntax has already been validated by ReadDenyMatcher::try_new.
|
// syntax has already been validated by
|
||||||
|
// ReadDenyMatcher::try_new_for_local_paths.
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,11 +219,11 @@ fn native_walk_preserves_canonical_matches_through_directory_links_and_cycles()
|
|||||||
access: FileSystemAccessMode::Deny,
|
access: FileSystemAccessMode::Deny,
|
||||||
missing_path_behavior: None,
|
missing_path_behavior: None,
|
||||||
}]);
|
}]);
|
||||||
let matcher = ReadDenyMatcher::try_new(&policy, cwd.as_path())
|
let matcher = ReadDenyMatcher::try_new_for_local_paths(&policy, cwd.as_path())
|
||||||
.expect("valid matcher")
|
.expect("valid matcher")
|
||||||
.expect("deny restriction");
|
.expect("deny restriction");
|
||||||
assert!(
|
assert!(
|
||||||
matcher.is_read_denied_with_canonical_path(
|
matcher.is_local_path_read_denied_with_canonical_path(
|
||||||
&alias.join("secret.env"),
|
&alias.join("secret.env"),
|
||||||
&target
|
&target
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ impl GlobWalker<'_> {
|
|||||||
let denied = match canonical {
|
let denied = match canonical {
|
||||||
Some(canonical) => self
|
Some(canonical) => self
|
||||||
.matcher
|
.matcher
|
||||||
.is_read_denied_with_canonical_path(path, canonical),
|
.is_local_path_read_denied_with_canonical_path(path, canonical),
|
||||||
None => self.matcher.is_read_denied(path),
|
None => self.matcher.is_local_path_read_denied(path),
|
||||||
};
|
};
|
||||||
if denied {
|
if denied {
|
||||||
push_absolute_path(self.paths, self.seen_paths, path.to_path_buf())?;
|
push_absolute_path(self.paths, self.seen_paths, path.to_path_buf())?;
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ impl ResolvedWindowsSandboxPermissions {
|
|||||||
}) && cwd
|
}) && cwd
|
||||||
.ancestors()
|
.ancestors()
|
||||||
.last()
|
.last()
|
||||||
.is_some_and(|root| self.file_system.can_read_path_with_cwd(root, cwd))
|
.is_some_and(|root| self.file_system.can_read_local_path_with_cwd(root, cwd))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn include_platform_defaults(&self) -> bool {
|
pub(crate) fn include_platform_defaults(&self) -> bool {
|
||||||
|
|||||||
Reference in new issue
Block a user