mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
dcec516313
## Why `PermissionProfile` needs stable, canonical file-system semantics before it can become the primary runtime permissions abstraction. Without a canonical form, callers have to keep re-deriving legacy sandbox maps and profile comparisons remain lossy or order-dependent. ## What changed This adds canonicalization helpers for `FileSystemPermissions` and `PermissionProfile`, expands special paths into explicit sandbox entries, and updates permission request/conversion paths to consume those canonical entries. It also tightens the legacy bridge so root-wide write profiles with narrower carveouts are not silently projected as full-disk legacy access. ## Verification - `cargo test -p codex-protocol root_write_with_read_only_child_is_not_full_disk_write -- --nocapture` - `cargo test -p codex-sandboxing permission -- --nocapture` - `cargo test -p codex-tui permissions -- --nocapture`
358 lines
13 KiB
Rust
358 lines
13 KiB
Rust
use super::SandboxCommand;
|
|
use super::SandboxManager;
|
|
use super::SandboxTransformRequest;
|
|
use super::SandboxType;
|
|
use super::SandboxablePreference;
|
|
use super::get_platform_sandbox;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
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::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use codex_protocol::protocol::NetworkAccess;
|
|
use codex_protocol::protocol::ReadOnlyAccess;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use dunce::canonicalize;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::HashMap;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() {
|
|
let manager = SandboxManager::new();
|
|
let sandbox = manager.select_initial(
|
|
&FileSystemSandboxPolicy::unrestricted(),
|
|
NetworkSandboxPolicy::Enabled,
|
|
SandboxablePreference::Auto,
|
|
WindowsSandboxLevel::Disabled,
|
|
/*has_managed_network_requirements*/ false,
|
|
);
|
|
assert_eq!(sandbox, SandboxType::None);
|
|
}
|
|
|
|
#[test]
|
|
fn danger_full_access_uses_platform_sandbox_with_network_requirements() {
|
|
let manager = SandboxManager::new();
|
|
let expected =
|
|
get_platform_sandbox(/*windows_sandbox_enabled*/ false).unwrap_or(SandboxType::None);
|
|
let sandbox = manager.select_initial(
|
|
&FileSystemSandboxPolicy::unrestricted(),
|
|
NetworkSandboxPolicy::Enabled,
|
|
SandboxablePreference::Auto,
|
|
WindowsSandboxLevel::Disabled,
|
|
/*has_managed_network_requirements*/ true,
|
|
);
|
|
assert_eq!(sandbox, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn restricted_file_system_uses_platform_sandbox_without_managed_network() {
|
|
let manager = SandboxManager::new();
|
|
let expected =
|
|
get_platform_sandbox(/*windows_sandbox_enabled*/ false).unwrap_or(SandboxType::None);
|
|
let sandbox = manager.select_initial(
|
|
&FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
access: FileSystemAccessMode::Read,
|
|
}]),
|
|
NetworkSandboxPolicy::Enabled,
|
|
SandboxablePreference::Auto,
|
|
WindowsSandboxLevel::Disabled,
|
|
/*has_managed_network_requirements*/ false,
|
|
);
|
|
assert_eq!(sandbox, expected);
|
|
}
|
|
|
|
#[test]
|
|
fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() {
|
|
let manager = SandboxManager::new();
|
|
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
|
let exec_request = manager
|
|
.transform(SandboxTransformRequest {
|
|
command: SandboxCommand {
|
|
program: "true".into(),
|
|
args: Vec::new(),
|
|
cwd: cwd.clone(),
|
|
env: HashMap::new(),
|
|
additional_permissions: None,
|
|
},
|
|
policy: &SandboxPolicy::ExternalSandbox {
|
|
network_access: NetworkAccess::Restricted,
|
|
},
|
|
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
|
network_policy: NetworkSandboxPolicy::Restricted,
|
|
sandbox: SandboxType::None,
|
|
enforce_managed_network: false,
|
|
network: None,
|
|
sandbox_policy_cwd: cwd.as_path(),
|
|
codex_linux_sandbox_exe: None,
|
|
use_legacy_landlock: false,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
windows_sandbox_private_desktop: false,
|
|
})
|
|
.expect("transform");
|
|
|
|
assert_eq!(
|
|
exec_request.file_system_sandbox_policy,
|
|
FileSystemSandboxPolicy::unrestricted()
|
|
);
|
|
assert_eq!(
|
|
exec_request.network_sandbox_policy,
|
|
NetworkSandboxPolicy::Restricted
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
|
let manager = SandboxManager::new();
|
|
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
|
let temp_dir = TempDir::new().expect("create temp dir");
|
|
let path = AbsolutePathBuf::from_absolute_path(
|
|
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
|
)
|
|
.expect("absolute temp dir");
|
|
let exec_request = manager
|
|
.transform(SandboxTransformRequest {
|
|
command: SandboxCommand {
|
|
program: "true".into(),
|
|
args: Vec::new(),
|
|
cwd: cwd.clone(),
|
|
env: HashMap::new(),
|
|
additional_permissions: Some(PermissionProfile {
|
|
network: Some(NetworkPermissions {
|
|
enabled: Some(true),
|
|
}),
|
|
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
|
Some(vec![path]),
|
|
Some(Vec::new()),
|
|
)),
|
|
}),
|
|
},
|
|
policy: &SandboxPolicy::ExternalSandbox {
|
|
network_access: NetworkAccess::Restricted,
|
|
},
|
|
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
|
network_policy: NetworkSandboxPolicy::Restricted,
|
|
sandbox: SandboxType::None,
|
|
enforce_managed_network: false,
|
|
network: None,
|
|
sandbox_policy_cwd: cwd.as_path(),
|
|
codex_linux_sandbox_exe: None,
|
|
use_legacy_landlock: false,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
windows_sandbox_private_desktop: false,
|
|
})
|
|
.expect("transform");
|
|
|
|
assert_eq!(
|
|
exec_request.sandbox_policy,
|
|
SandboxPolicy::ExternalSandbox {
|
|
network_access: NetworkAccess::Enabled,
|
|
}
|
|
);
|
|
assert_eq!(
|
|
exec_request.network_sandbox_policy,
|
|
NetworkSandboxPolicy::Enabled
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn transform_additional_permissions_preserves_denied_entries() {
|
|
let manager = SandboxManager::new();
|
|
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
|
let temp_dir = TempDir::new().expect("create temp dir");
|
|
let workspace_root = AbsolutePathBuf::from_absolute_path(
|
|
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
|
)
|
|
.expect("absolute temp dir");
|
|
let allowed_path = workspace_root.join("allowed");
|
|
let denied_path = workspace_root.join("denied");
|
|
let exec_request = manager
|
|
.transform(SandboxTransformRequest {
|
|
command: SandboxCommand {
|
|
program: "true".into(),
|
|
args: Vec::new(),
|
|
cwd: cwd.clone(),
|
|
env: HashMap::new(),
|
|
additional_permissions: Some(PermissionProfile {
|
|
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
|
/*read*/ None,
|
|
Some(vec![allowed_path.clone()]),
|
|
)),
|
|
..Default::default()
|
|
}),
|
|
},
|
|
policy: &SandboxPolicy::ReadOnly {
|
|
access: ReadOnlyAccess::FullAccess,
|
|
network_access: false,
|
|
},
|
|
file_system_policy: &FileSystemSandboxPolicy::restricted(vec![
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
access: FileSystemAccessMode::Read,
|
|
},
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Path {
|
|
path: denied_path.clone(),
|
|
},
|
|
access: FileSystemAccessMode::None,
|
|
},
|
|
]),
|
|
network_policy: NetworkSandboxPolicy::Restricted,
|
|
sandbox: SandboxType::None,
|
|
enforce_managed_network: false,
|
|
network: None,
|
|
sandbox_policy_cwd: cwd.as_path(),
|
|
codex_linux_sandbox_exe: None,
|
|
use_legacy_landlock: false,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
windows_sandbox_private_desktop: false,
|
|
})
|
|
.expect("transform");
|
|
|
|
assert_eq!(
|
|
exec_request.file_system_sandbox_policy,
|
|
FileSystemSandboxPolicy::restricted(vec![
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
access: FileSystemAccessMode::Read,
|
|
},
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Path { path: denied_path },
|
|
access: FileSystemAccessMode::None,
|
|
},
|
|
FileSystemSandboxEntry {
|
|
path: FileSystemPath::Path { path: allowed_path },
|
|
access: FileSystemAccessMode::Write,
|
|
},
|
|
])
|
|
);
|
|
assert_eq!(
|
|
exec_request.network_sandbox_policy,
|
|
NetworkSandboxPolicy::Restricted
|
|
);
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn transform_linux_seccomp_request(
|
|
codex_linux_sandbox_exe: &std::path::Path,
|
|
) -> super::SandboxExecRequest {
|
|
let manager = SandboxManager::new();
|
|
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
|
manager
|
|
.transform(SandboxTransformRequest {
|
|
command: SandboxCommand {
|
|
program: "true".into(),
|
|
args: Vec::new(),
|
|
cwd: cwd.clone(),
|
|
env: HashMap::new(),
|
|
additional_permissions: None,
|
|
},
|
|
policy: &SandboxPolicy::DangerFullAccess,
|
|
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
|
network_policy: NetworkSandboxPolicy::Enabled,
|
|
sandbox: SandboxType::LinuxSeccomp,
|
|
enforce_managed_network: false,
|
|
network: None,
|
|
sandbox_policy_cwd: cwd.as_path(),
|
|
codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe),
|
|
use_legacy_landlock: false,
|
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
|
windows_sandbox_private_desktop: false,
|
|
})
|
|
.expect("transform")
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn wsl1_rejects_linux_bubblewrap_path() {
|
|
let restricted_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
access: FileSystemAccessMode::Read,
|
|
}]);
|
|
|
|
assert!(matches!(
|
|
super::ensure_linux_bubblewrap_is_supported(
|
|
&restricted_policy,
|
|
/*use_legacy_landlock*/ false,
|
|
/*allow_network_for_proxy*/ false,
|
|
/*is_wsl1*/ true,
|
|
),
|
|
Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap)
|
|
));
|
|
assert!(matches!(
|
|
super::ensure_linux_bubblewrap_is_supported(
|
|
&FileSystemSandboxPolicy::unrestricted(),
|
|
/*use_legacy_landlock*/ false,
|
|
/*allow_network_for_proxy*/ true,
|
|
/*is_wsl1*/ true,
|
|
),
|
|
Err(super::SandboxTransformError::Wsl1UnsupportedForBubblewrap)
|
|
));
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn wsl1_allows_non_bubblewrap_linux_paths() {
|
|
assert!(
|
|
super::ensure_linux_bubblewrap_is_supported(
|
|
&FileSystemSandboxPolicy::unrestricted(),
|
|
/*use_legacy_landlock*/ false,
|
|
/*allow_network_for_proxy*/ false,
|
|
/*is_wsl1*/ true,
|
|
)
|
|
.is_ok()
|
|
);
|
|
|
|
let restricted_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
|
|
path: FileSystemPath::Special {
|
|
value: FileSystemSpecialPath::Root,
|
|
},
|
|
access: FileSystemAccessMode::Read,
|
|
}]);
|
|
assert!(
|
|
super::ensure_linux_bubblewrap_is_supported(
|
|
&restricted_policy,
|
|
/*use_legacy_landlock*/ true,
|
|
/*allow_network_for_proxy*/ false,
|
|
/*is_wsl1*/ true,
|
|
)
|
|
.is_ok()
|
|
);
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn transform_linux_seccomp_preserves_helper_path_in_arg0_when_available() {
|
|
let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex-linux-sandbox");
|
|
let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe);
|
|
|
|
assert_eq!(
|
|
exec_request.arg0,
|
|
Some(codex_linux_sandbox_exe.to_string_lossy().into_owned())
|
|
);
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[test]
|
|
fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path() {
|
|
let codex_linux_sandbox_exe = std::path::PathBuf::from("/tmp/codex");
|
|
let exec_request = transform_linux_seccomp_request(&codex_linux_sandbox_exe);
|
|
|
|
assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string()));
|
|
}
|