sandboxing: materialize cwd-relative permission globs

This commit is contained in:
Michael Bolin
2026-04-21 13:50:53 -07:00
parent 6f6997758a
commit 22370ddb80
3 changed files with 165 additions and 9 deletions
+57 -2
View File
@@ -193,8 +193,7 @@ pub(super) async fn apply_granted_turn_permissions(
);
let permissions_preapproved = match (effective_permissions.as_ref(), granted_permissions) {
(Some(effective_permissions), Some(granted_permissions)) => {
intersect_permission_profiles(effective_permissions.clone(), granted_permissions, cwd)
== *effective_permissions
permissions_are_preapproved(effective_permissions, granted_permissions, cwd)
}
_ => false,
};
@@ -213,17 +212,37 @@ pub(super) async fn apply_granted_turn_permissions(
}
}
fn permissions_are_preapproved(
effective_permissions: &PermissionProfile,
granted_permissions: PermissionProfile,
cwd: &Path,
) -> bool {
let materialized_effective_permissions = intersect_permission_profiles(
effective_permissions.clone(),
effective_permissions.clone(),
cwd,
);
intersect_permission_profiles(effective_permissions.clone(), granted_permissions, cwd)
== materialized_effective_permissions
}
#[cfg(test)]
mod tests {
use super::EffectiveAdditionalPermissions;
use super::implicit_granted_permissions;
use super::normalize_and_validate_additional_permissions;
use super::permissions_are_preapproved;
use crate::sandboxing::SandboxPermissions;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
@@ -326,4 +345,40 @@ mod tests {
assert_eq!(implicit_permissions, None);
}
#[test]
fn relative_deny_glob_grants_remain_preapproved_after_materialization() {
let cwd = tempdir().expect("tempdir");
let requested_permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: FileSystemAccessMode::None,
},
],
glob_scan_max_depth: None,
}),
..Default::default()
};
let stored_grant = intersect_permission_profiles(
requested_permissions.clone(),
requested_permissions.clone(),
cwd.path(),
);
assert!(permissions_are_preapproved(
&requested_permissions,
stored_grant,
cwd.path(),
));
}
}
+9 -3
View File
@@ -383,9 +383,15 @@ fn materialize_cwd_dependent_entry(
access: entry.access,
})
.unwrap_or_else(|| entry.clone()),
FileSystemPath::Path { .. }
| FileSystemPath::GlobPattern { .. }
| FileSystemPath::Special { .. } => entry.clone(),
FileSystemPath::GlobPattern { pattern } => FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(pattern, cwd)
.to_string_lossy()
.into_owned(),
},
access: entry.access,
},
FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => entry.clone(),
}
}
@@ -509,6 +509,75 @@ fn intersect_permission_profiles_rejects_concrete_grants_matched_by_requested_de
);
}
#[test]
fn intersect_permission_profiles_materializes_relative_deny_globs_for_reuse() {
let temp_dir = TempDir::new().expect("create temp dir");
let request_cwd = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("request-cwd"))
.expect("absolute request cwd");
let later_cwd = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("later-cwd"))
.expect("absolute later cwd");
let cwd_write = FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::CurrentWorkingDirectory,
},
access: FileSystemAccessMode::Write,
};
let deny_env_files = FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: "**/*.env".to_string(),
},
access: FileSystemAccessMode::None,
};
let permissions = PermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![cwd_write, deny_env_files],
glob_scan_max_depth: std::num::NonZeroUsize::new(2),
}),
..Default::default()
};
let intersected =
intersect_permission_profiles(permissions.clone(), permissions, request_cwd.as_path());
assert_eq!(
intersected,
PermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: request_cwd.clone(),
},
access: FileSystemAccessMode::Write,
},
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: request_cwd.join("**/*.env").to_string_lossy().into_owned(),
},
access: FileSystemAccessMode::None,
},
],
glob_scan_max_depth: std::num::NonZeroUsize::new(2),
}),
..Default::default()
}
);
assert_eq!(
intersect_permission_profiles(
PermissionProfile {
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![later_cwd.join("token.env")]),
)),
..Default::default()
},
intersected,
later_cwd.as_path(),
),
PermissionProfile::default()
);
}
#[test]
fn intersect_permission_profiles_drops_broader_cwd_grant_for_requested_child_path() {
let temp_dir = TempDir::new().expect("create temp dir");
@@ -567,7 +636,7 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() {
};
let granted = PermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![root_write.clone(), deny_env_files.clone()],
entries: vec![root_write.clone(), deny_env_files],
glob_scan_max_depth: std::num::NonZeroUsize::new(4),
}),
..Default::default()
@@ -577,7 +646,20 @@ fn intersect_permission_profiles_uses_granted_bounded_glob_scan_depth() {
intersect_permission_profiles(requested, granted, cwd.as_path()),
PermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![root_write, deny_env_files],
entries: vec![
root_write,
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
cwd.as_path()
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::None,
},
],
glob_scan_max_depth: std::num::NonZeroUsize::new(4),
}),
..Default::default()
@@ -609,7 +691,7 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() {
};
let granted = PermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![root_write.clone(), deny_env_files.clone()],
entries: vec![root_write.clone(), deny_env_files],
glob_scan_max_depth: None,
}),
..Default::default()
@@ -619,7 +701,20 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() {
intersect_permission_profiles(requested, granted, cwd.as_path()),
PermissionProfile {
file_system: Some(FileSystemPermissions {
entries: vec![root_write, deny_env_files],
entries: vec![
root_write,
FileSystemSandboxEntry {
path: FileSystemPath::GlobPattern {
pattern: AbsolutePathBuf::resolve_path_against_base(
"**/*.env",
cwd.as_path()
)
.to_string_lossy()
.into_owned(),
},
access: FileSystemAccessMode::None,
},
],
glob_scan_max_depth: None,
}),
..Default::default()