mirror of
https://github.com/openai/codex.git
synced 2026-06-02 19:31:59 +00:00
feat(permissions): introduce effective filesystem permissions
Co-authored-by: Codex noreply@openai.com
This commit is contained in:
187
codex-rs/sandboxing/src/effective_filesystem_permissions.rs
Normal file
187
codex-rs/sandboxing/src/effective_filesystem_permissions.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::ReadDenyMatcher;
|
||||
use codex_protocol::permissions::project_roots_glob_pattern;
|
||||
use codex_protocol::protocol::WritableRoot;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
/// Context needed to evaluate an already-materialized filesystem policy.
|
||||
pub struct FilesystemPermissionsContext<'a> {
|
||||
/// Resolves cwd-sensitive policy mechanics such as filesystem root and
|
||||
/// relative candidate paths. It is not workspace-root authority.
|
||||
pub policy_evaluation_cwd: &'a AbsolutePathBuf,
|
||||
}
|
||||
|
||||
/// The outer filesystem access mode represented by effective permissions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FilesystemPermissionsMode {
|
||||
Restricted,
|
||||
Unrestricted,
|
||||
External,
|
||||
}
|
||||
|
||||
/// A deny-read glob accepted by the existing runtime matcher.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ValidatedDenyGlob {
|
||||
pattern: String,
|
||||
}
|
||||
|
||||
impl ValidatedDenyGlob {
|
||||
pub fn pattern(&self) -> &str {
|
||||
&self.pattern
|
||||
}
|
||||
}
|
||||
|
||||
/// Effective filesystem enforcement facts derived from a permission profile.
|
||||
///
|
||||
/// This internal representation centralizes effective roots, writable carveouts,
|
||||
/// protected metadata, and read-deny matching before platform-specific lowering.
|
||||
pub struct EffectiveFilesystemPermissions {
|
||||
pub mode: FilesystemPermissionsMode,
|
||||
pub readable_roots: Vec<AbsolutePathBuf>,
|
||||
pub writable_roots: Vec<WritableRoot>,
|
||||
pub unreadable_roots: Vec<AbsolutePathBuf>,
|
||||
pub unreadable_globs: Vec<ValidatedDenyGlob>,
|
||||
pub include_platform_defaults: bool,
|
||||
pub glob_scan_max_depth: Option<usize>,
|
||||
file_system_policy: FileSystemSandboxPolicy,
|
||||
policy_evaluation_cwd: AbsolutePathBuf,
|
||||
read_deny_matcher: Option<ReadDenyMatcher>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for EffectiveFilesystemPermissions {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter
|
||||
.debug_struct("EffectiveFilesystemPermissions")
|
||||
.field("mode", &self.mode)
|
||||
.field("readable_roots", &self.readable_roots)
|
||||
.field("writable_roots", &self.writable_roots)
|
||||
.field("unreadable_roots", &self.unreadable_roots)
|
||||
.field("unreadable_globs", &self.unreadable_globs)
|
||||
.field("include_platform_defaults", &self.include_platform_defaults)
|
||||
.field("glob_scan_max_depth", &self.glob_scan_max_depth)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectiveFilesystemPermissions {
|
||||
/// Derives effective filesystem enforcement facts for platform consumers.
|
||||
///
|
||||
/// Callers must pass an effective `PermissionProfile` after runtime grants and
|
||||
/// runtime workspace roots have been applied. Symbolic workspace-root entries
|
||||
/// are rejected at this boundary rather than resolved from a working directory.
|
||||
pub fn from_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
context: FilesystemPermissionsContext<'_>,
|
||||
) -> Result<Self, FilesystemPermissionsError> {
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
if contains_unmaterialized_workspace_roots(&file_system_policy) {
|
||||
return Err(FilesystemPermissionsError::UnmaterializedWorkspaceRoots);
|
||||
}
|
||||
let read_deny_matcher =
|
||||
ReadDenyMatcher::try_new(&file_system_policy, context.policy_evaluation_cwd.as_path())
|
||||
.map_err(FilesystemPermissionsError::InvalidDenyGlob)?;
|
||||
let mode = match file_system_policy.kind {
|
||||
FileSystemSandboxKind::Restricted => FilesystemPermissionsMode::Restricted,
|
||||
FileSystemSandboxKind::Unrestricted => FilesystemPermissionsMode::Unrestricted,
|
||||
FileSystemSandboxKind::ExternalSandbox => FilesystemPermissionsMode::External,
|
||||
};
|
||||
let readable_roots =
|
||||
file_system_policy.get_readable_roots_with_cwd(context.policy_evaluation_cwd.as_path());
|
||||
let writable_roots =
|
||||
file_system_policy.get_writable_roots_with_cwd(context.policy_evaluation_cwd.as_path());
|
||||
let unreadable_roots = file_system_policy
|
||||
.get_unreadable_roots_with_cwd(context.policy_evaluation_cwd.as_path());
|
||||
let unreadable_globs = file_system_policy
|
||||
.get_unreadable_globs_with_cwd(context.policy_evaluation_cwd.as_path())
|
||||
.into_iter()
|
||||
.map(|pattern| ValidatedDenyGlob { pattern })
|
||||
.collect();
|
||||
let include_platform_defaults = file_system_policy.include_platform_defaults();
|
||||
let glob_scan_max_depth = file_system_policy.glob_scan_max_depth;
|
||||
|
||||
Ok(Self {
|
||||
mode,
|
||||
readable_roots,
|
||||
writable_roots,
|
||||
unreadable_roots,
|
||||
unreadable_globs,
|
||||
include_platform_defaults,
|
||||
glob_scan_max_depth,
|
||||
file_system_policy,
|
||||
policy_evaluation_cwd: context.policy_evaluation_cwd.clone(),
|
||||
read_deny_matcher,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether a read is permitted after applying explicit read denies.
|
||||
pub fn can_read(&self, path: &Path) -> bool {
|
||||
self.file_system_policy
|
||||
.can_read_path_with_cwd(path, self.policy_evaluation_cwd.as_path())
|
||||
&& !self.is_read_denied(path)
|
||||
}
|
||||
|
||||
/// Returns whether a write is permitted, including protected metadata rules.
|
||||
pub fn can_write(&self, path: &Path) -> bool {
|
||||
self.file_system_policy
|
||||
.can_write_path_with_cwd(path, self.policy_evaluation_cwd.as_path())
|
||||
}
|
||||
|
||||
/// Returns whether `path` is matched by an explicit deny-read entry.
|
||||
pub fn is_read_denied(&self, path: &Path) -> bool {
|
||||
self.read_deny_matcher
|
||||
.as_ref()
|
||||
.is_some_and(|matcher| matcher.is_read_denied(path))
|
||||
}
|
||||
|
||||
pub fn has_full_disk_read_access(&self) -> bool {
|
||||
self.file_system_policy.has_full_disk_read_access()
|
||||
}
|
||||
|
||||
pub fn has_full_disk_write_access(&self) -> bool {
|
||||
self.file_system_policy.has_full_disk_write_access()
|
||||
}
|
||||
}
|
||||
|
||||
/// An error deriving filesystem enforcement facts from a permission profile.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum FilesystemPermissionsError {
|
||||
UnmaterializedWorkspaceRoots,
|
||||
InvalidDenyGlob(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for FilesystemPermissionsError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UnmaterializedWorkspaceRoots => formatter.write_str(
|
||||
"effective filesystem permissions require workspace roots to be materialized from runtime workspace roots",
|
||||
),
|
||||
Self::InvalidDenyGlob(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FilesystemPermissionsError {}
|
||||
|
||||
fn contains_unmaterialized_workspace_roots(file_system_policy: &FileSystemSandboxPolicy) -> bool {
|
||||
let workspace_glob_prefix = project_roots_glob_pattern(Path::new(""));
|
||||
file_system_policy
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| match &entry.path {
|
||||
FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::ProjectRoots { .. },
|
||||
} => true,
|
||||
FileSystemPath::GlobPattern { pattern } => pattern.starts_with(&workspace_glob_prefix),
|
||||
FileSystemPath::Path { .. } | FileSystemPath::Special { .. } => false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "effective_filesystem_permissions_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,254 @@
|
||||
use super::EffectiveFilesystemPermissions;
|
||||
use super::FilesystemPermissionsContext;
|
||||
use super::FilesystemPermissionsError;
|
||||
use super::FilesystemPermissionsMode;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::permissions::ReadDenyMatcher;
|
||||
use codex_protocol::permissions::project_roots_glob_pattern;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn absolute_path(path: &Path) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::from_absolute_path(path).expect("absolute path")
|
||||
}
|
||||
|
||||
fn derive_effective(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> super::EffectiveFilesystemPermissions {
|
||||
EffectiveFilesystemPermissions::from_profile(
|
||||
permission_profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: cwd,
|
||||
},
|
||||
)
|
||||
.expect("valid effective filesystem permissions")
|
||||
}
|
||||
|
||||
fn assert_projected_fields_match_policy(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &AbsolutePathBuf,
|
||||
) -> super::EffectiveFilesystemPermissions {
|
||||
let effective = derive_effective(permission_profile, cwd);
|
||||
let policy = permission_profile.file_system_sandbox_policy();
|
||||
let expected_mode = match policy.kind {
|
||||
FileSystemSandboxKind::Restricted => FilesystemPermissionsMode::Restricted,
|
||||
FileSystemSandboxKind::Unrestricted => FilesystemPermissionsMode::Unrestricted,
|
||||
FileSystemSandboxKind::ExternalSandbox => FilesystemPermissionsMode::External,
|
||||
};
|
||||
|
||||
assert_eq!(effective.mode, expected_mode);
|
||||
assert_eq!(
|
||||
effective.readable_roots,
|
||||
policy.get_readable_roots_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective.writable_roots,
|
||||
policy.get_writable_roots_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective.unreadable_roots,
|
||||
policy.get_unreadable_roots_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective
|
||||
.unreadable_globs
|
||||
.iter()
|
||||
.map(|glob| glob.pattern().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
policy.get_unreadable_globs_with_cwd(cwd.as_path())
|
||||
);
|
||||
assert_eq!(
|
||||
effective.include_platform_defaults,
|
||||
policy.include_platform_defaults()
|
||||
);
|
||||
assert_eq!(effective.glob_scan_max_depth, policy.glob_scan_max_depth);
|
||||
assert_eq!(
|
||||
effective.has_full_disk_read_access(),
|
||||
policy.has_full_disk_read_access()
|
||||
);
|
||||
assert_eq!(
|
||||
effective.has_full_disk_write_access(),
|
||||
policy.has_full_disk_write_access()
|
||||
);
|
||||
|
||||
effective
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_access_modes_preserve_builtin_profile_semantics() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
|
||||
let read_only = assert_projected_fields_match_policy(&PermissionProfile::read_only(), &cwd);
|
||||
assert_eq!(read_only.mode, FilesystemPermissionsMode::Restricted);
|
||||
assert_eq!(read_only.can_read(cwd.as_path()), true);
|
||||
assert_eq!(read_only.can_write(cwd.as_path()), false);
|
||||
|
||||
let unrestricted = assert_projected_fields_match_policy(&PermissionProfile::Disabled, &cwd);
|
||||
assert_eq!(unrestricted.mode, FilesystemPermissionsMode::Unrestricted);
|
||||
assert_eq!(unrestricted.can_write(cwd.as_path()), true);
|
||||
|
||||
let external = assert_projected_fields_match_policy(
|
||||
&PermissionProfile::External {
|
||||
network: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
&cwd,
|
||||
);
|
||||
assert_eq!(external.mode, FilesystemPermissionsMode::External);
|
||||
assert_eq!(external.has_full_disk_write_access(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_workspace_permissions_reject_unmaterialized_runtime_workspace_roots() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let unresolved_exact = PermissionProfile::workspace_write();
|
||||
let unresolved_glob_policy =
|
||||
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern {
|
||||
pattern: project_roots_glob_pattern(Path::new("**/*.env")),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
}]);
|
||||
let unresolved_glob = PermissionProfile::from_runtime_permissions(
|
||||
&unresolved_glob_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
);
|
||||
|
||||
for profile in [&unresolved_exact, &unresolved_glob] {
|
||||
let error = EffectiveFilesystemPermissions::from_profile(
|
||||
profile,
|
||||
FilesystemPermissionsContext {
|
||||
policy_evaluation_cwd: &cwd,
|
||||
},
|
||||
)
|
||||
.expect_err("unresolved runtime workspace roots should fail");
|
||||
assert_eq!(
|
||||
error,
|
||||
FilesystemPermissionsError::UnmaterializedWorkspaceRoots
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_workspace_permissions_preserve_materialized_workspace_roots() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let first_root = cwd.join("first");
|
||||
let second_root = cwd.join("second");
|
||||
let permission_profile = PermissionProfile::workspace_write()
|
||||
.materialize_project_roots_with_workspace_roots(&[first_root.clone(), second_root.clone()]);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
|
||||
assert_eq!(effective.can_write(first_root.join("src").as_path()), true);
|
||||
assert_eq!(effective.can_write(second_root.join("src").as_path()), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_permissions_preserve_nested_carveouts_and_read_denies() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let workspace = cwd.join("workspace");
|
||||
let read_only_child = workspace.join("generated");
|
||||
let denied_child = workspace.join("private");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: workspace.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: read_only_child.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: denied_child.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
let permission_profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
|
||||
assert_eq!(
|
||||
effective.can_write(workspace.join("file.txt").as_path()),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
effective.can_write(read_only_child.join("file.txt").as_path()),
|
||||
false
|
||||
);
|
||||
assert_eq!(effective.can_write(workspace.join(".git").as_path()), false);
|
||||
assert_eq!(
|
||||
effective.can_read(denied_child.join("secret.txt").as_path()),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn effective_permissions_preserve_symlinked_writable_roots() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let target = cwd.join("target");
|
||||
let link = cwd.join("linked-workspace");
|
||||
std::fs::create_dir_all(target.as_path()).expect("create target");
|
||||
std::os::unix::fs::symlink(target.as_path(), link.as_path()).expect("create symlink");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: link.clone() },
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]);
|
||||
let permission_profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
|
||||
assert_eq!(
|
||||
effective.can_write(link.join("file.txt").as_path()),
|
||||
policy.can_write_path_with_cwd(link.join("file.txt").as_path(), cwd.as_path())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_permissions_preserve_accepted_deny_glob_matching() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = absolute_path(temp_dir.path());
|
||||
let pattern = cwd.join("secret[.txt").to_string_lossy().into_owned();
|
||||
let denied_path = cwd.join("secret[.txt");
|
||||
let policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path: cwd.clone() },
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::GlobPattern { pattern },
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
let permission_profile =
|
||||
PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted);
|
||||
let effective = assert_projected_fields_match_policy(&permission_profile, &cwd);
|
||||
let current_matcher = ReadDenyMatcher::try_new(&policy, cwd.as_path())
|
||||
.expect("accepted pattern")
|
||||
.expect("deny matcher");
|
||||
|
||||
assert_eq!(effective.is_read_denied(denied_path.as_path()), true);
|
||||
assert_eq!(
|
||||
effective.is_read_denied(denied_path.as_path()),
|
||||
current_matcher.is_read_denied(denied_path.as_path())
|
||||
);
|
||||
assert_eq!(effective.can_read(denied_path.as_path()), false);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap;
|
||||
mod effective_filesystem_permissions;
|
||||
pub mod landlock;
|
||||
mod manager;
|
||||
pub mod policy_transforms;
|
||||
@@ -10,6 +11,11 @@ pub mod seatbelt;
|
||||
pub use bwrap::find_system_bwrap_in_path;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use bwrap::system_bwrap_warning;
|
||||
pub use effective_filesystem_permissions::EffectiveFilesystemPermissions;
|
||||
pub use effective_filesystem_permissions::FilesystemPermissionsContext;
|
||||
pub use effective_filesystem_permissions::FilesystemPermissionsError;
|
||||
pub use effective_filesystem_permissions::FilesystemPermissionsMode;
|
||||
pub use effective_filesystem_permissions::ValidatedDenyGlob;
|
||||
pub use manager::SandboxCommand;
|
||||
pub use manager::SandboxExecRequest;
|
||||
pub use manager::SandboxManager;
|
||||
|
||||
Reference in New Issue
Block a user