From b55d9504feb5d1f8e9c5983bf9d646ff4965c4a1 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 24 Apr 2026 20:37:16 -0700 Subject: [PATCH 1/3] permissions: migrate approval and sandbox consumers to profiles --- .../analytics/src/analytics_client_tests.rs | 5 +- codex-rs/analytics/src/facts.rs | 5 +- codex-rs/analytics/src/reducer.rs | 28 ++++++--- .../app-server/src/codex_message_processor.rs | 32 ++++++---- codex-rs/cli/src/debug_sandbox.rs | 2 +- codex-rs/core/src/apply_patch.rs | 3 +- codex-rs/core/src/config/config_tests.rs | 57 +++++++++++++++++ codex-rs/core/src/config/mod.rs | 4 +- .../core/src/config/network_proxy_spec.rs | 43 ++++++------- .../src/config/network_proxy_spec_tests.rs | 30 +++++---- codex-rs/core/src/guardian/review_session.rs | 2 +- codex-rs/core/src/guardian/tests.rs | 9 ++- codex-rs/core/src/landlock.rs | 26 ++++---- codex-rs/core/src/safety.rs | 44 +++++++++---- codex-rs/core/src/safety_tests.rs | 22 ++++--- codex-rs/core/src/session/mod.rs | 30 +++++---- codex-rs/core/src/session/session.rs | 6 +- codex-rs/core/src/session/tests.rs | 62 ++++++++++++------- codex-rs/core/src/session/turn.rs | 3 +- codex-rs/core/src/session/turn_context.rs | 20 +++--- codex-rs/core/src/tasks/user_shell.rs | 20 +++++- codex-rs/core/src/tools/network_approval.rs | 12 ++-- .../core/src/tools/network_approval_tests.rs | 19 ++++-- codex-rs/core/tests/suite/approvals.rs | 8 ++- codex-rs/exec/tests/suite/sandbox.rs | 5 +- 25 files changed, 330 insertions(+), 167 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index c0465ca7d7..352acbe1fc 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -315,7 +315,10 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { session_source: SessionSource::Exec, model: "gpt-5".to_string(), model_provider: "openai".to_string(), - sandbox_policy: SandboxPolicy::new_read_only_policy(), + permission_profile: CorePermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), + permission_profile_cwd: PathBuf::from("/tmp"), reasoning_effort: None, reasoning_summary: None, service_tier: None, diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 1d371acb1c..8ebff278c4 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -13,12 +13,12 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ServiceTier; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::HookEventName; use codex_protocol::protocol::HookRunStatus; use codex_protocol::protocol::HookSource; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::SubAgentSource; @@ -62,7 +62,8 @@ pub struct TurnResolvedConfigFact { pub session_source: SessionSource, pub model: String, pub model_provider: String, - pub sandbox_policy: SandboxPolicy, + pub permission_profile: PermissionProfile, + pub permission_profile_cwd: PathBuf, pub reasoning_effort: Option, pub reasoning_summary: Option, pub service_tier: Option, diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index a6ce3fc831..681c25483a 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -61,6 +61,7 @@ use codex_login::default_client::originator; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillScope; @@ -884,7 +885,8 @@ fn codex_turn_event_params( session_source: _session_source, model, model_provider, - sandbox_policy, + permission_profile, + permission_profile_cwd, reasoning_effort, reasoning_summary, service_tier, @@ -909,7 +911,10 @@ fn codex_turn_event_params( parent_thread_id: thread_metadata.parent_thread_id.clone(), model: Some(model), model_provider, - sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)), + sandbox_policy: Some(sandbox_policy_mode( + &permission_profile, + permission_profile_cwd.as_path(), + )), reasoning_effort: reasoning_effort.map(|value| value.to_string()), reasoning_summary: reasoning_summary_mode(reasoning_summary), service_tier: service_tier @@ -954,12 +959,19 @@ fn codex_turn_event_params( } } -fn sandbox_policy_mode(sandbox_policy: &SandboxPolicy) -> &'static str { - match sandbox_policy { - SandboxPolicy::DangerFullAccess => "full_access", - SandboxPolicy::ReadOnly { .. } => "read_only", - SandboxPolicy::WorkspaceWrite { .. } => "workspace_write", - SandboxPolicy::ExternalSandbox { .. } => "external_sandbox", +fn sandbox_policy_mode(permission_profile: &PermissionProfile, cwd: &Path) -> &'static str { + match permission_profile { + PermissionProfile::Disabled => "full_access", + PermissionProfile::External { .. } => "external_sandbox", + PermissionProfile::Managed { .. } => { + match permission_profile.to_legacy_sandbox_policy(cwd) { + Ok(SandboxPolicy::DangerFullAccess) => "full_access", + Ok(SandboxPolicy::ReadOnly { .. }) => "read_only", + Ok(SandboxPolicy::WorkspaceWrite { .. }) => "workspace_write", + Ok(SandboxPolicy::ExternalSandbox { .. }) => "external_sandbox", + Err(_) => "workspace_write", + } + } } } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 6694c3db4d..bd3dfd84c4 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2184,7 +2184,7 @@ impl CodexMessageProcessor { let started_network_proxy = match self.config.permissions.network.as_ref() { Some(spec) => match spec .start_proxy( - self.config.permissions.sandbox_policy.get(), + self.config.permissions.permission_profile.get(), /*policy_decider*/ None, /*blocked_request_observer*/ None, managed_network_requirements_enabled, @@ -2265,17 +2265,11 @@ impl CodexMessageProcessor { &file_system_sandbox_policy, network_sandbox_policy, ); - let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &effective_permission_profile, - &file_system_sandbox_policy, - network_sandbox_policy, - sandbox_cwd.as_path(), - ); match self .config .permissions - .sandbox_policy - .can_set(&sandbox_policy) + .permission_profile + .can_set(&effective_permission_profile) { Ok(()) => effective_permission_profile, Err(err) => { @@ -2295,13 +2289,29 @@ impl CodexMessageProcessor { codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd); let network_sandbox_policy = codex_protocol::permissions::NetworkSandboxPolicy::from(&policy); - codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( + let permission_profile = + codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement( codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy( &policy, ), &file_system_sandbox_policy, network_sandbox_policy, - ) + ); + if let Err(err) = self + .config + .permissions + .permission_profile + .can_set(&permission_profile) + { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("invalid sandbox policy: {err}"), + data: None, + }; + self.outgoing.send_error(request, error).await; + return; + } + permission_profile } Err(err) => { let error = JSONRPCErrorError { diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index a59ce31d55..a6cd07699e 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -171,7 +171,7 @@ async fn run_command_under_sandbox( let network_proxy = match config.permissions.network.as_ref() { Some(spec) => Some( spec.start_proxy( - config.permissions.sandbox_policy.get(), + config.permissions.permission_profile.get(), /*policy_decider*/ None, /*blocked_request_observer*/ None, managed_network_requirements_enabled, diff --git a/codex-rs/core/src/apply_patch.rs b/codex-rs/core/src/apply_patch.rs index d31b4f0343..d5ebe4fe1f 100644 --- a/codex-rs/core/src/apply_patch.rs +++ b/codex-rs/core/src/apply_patch.rs @@ -35,11 +35,10 @@ pub(crate) async fn apply_patch( file_system_sandbox_policy: &FileSystemSandboxPolicy, action: ApplyPatchAction, ) -> InternalApplyPatchInvocation { - let sandbox_policy = turn_context.sandbox_policy(); match assess_patch_safety( &action, turn_context.approval_policy.value(), - &sandbox_policy, + &turn_context.permission_profile(), file_system_sandbox_policy, &turn_context.cwd, turn_context.windows_sandbox_level, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index c973ce9ee2..e32abc6a89 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -57,6 +57,7 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -6610,6 +6611,62 @@ async fn permission_profile_override_falls_back_when_disallowed_by_requirements( Ok(()) } +#[tokio::test] +async fn permission_profile_override_preserves_split_write_roots() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = codex_home.path().join("workspace"); + let outside_root = codex_home.path().join("outside-write"); + std::fs::create_dir_all(&cwd)?; + std::fs::create_dir_all(&outside_root)?; + let outside_root = + AbsolutePathBuf::from_absolute_path(outside_root).expect("outside root is absolute"); + let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: outside_root.clone(), + }, + access: FileSystemAccessMode::Write, + }, + ]); + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::Managed, + &file_system_sandbox_policy, + NetworkSandboxPolicy::Restricted, + ); + + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(cwd)) + .harness_overrides(ConfigOverrides { + permission_profile: Some(permission_profile), + ..Default::default() + }) + .build() + .await?; + + assert!( + config + .permissions + .file_system_sandbox_policy() + .can_write_path_with_cwd(outside_root.as_path(), config.cwd.as_path()) + ); + assert!(matches!( + config.permissions.sandbox_policy.get(), + SandboxPolicy::WorkspaceWrite { .. } + )); + assert_eq!( + config.permissions.network_sandbox_policy(), + NetworkSandboxPolicy::Restricted + ); + Ok(()) +} + #[tokio::test] async fn requirements_web_search_mode_overrides_danger_full_access_default() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f92414a019..d8fe67b838 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2350,10 +2350,12 @@ impl Config { None => (None, None), }; let has_network_requirements = network_requirements.is_some(); + let network_permission_profile = + PermissionProfile::from_legacy_sandbox_policy(constrained_sandbox_policy.get()); let network = NetworkProxySpec::from_config_and_constraints( configured_network_proxy_config, network_requirements, - constrained_sandbox_policy.get(), + &network_permission_profile, ) .map_err(|err| { if let Some(source) = network_requirements_source.as_ref() { diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index acabe24f20..252eb6e6d8 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -16,7 +16,7 @@ use codex_network_proxy::build_config_state; use codex_network_proxy::host_and_port_from_network_addr; use codex_network_proxy::normalize_host; use codex_network_proxy::validate_policy_against_constraints; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use std::collections::HashSet; use std::sync::Arc; @@ -89,7 +89,7 @@ impl NetworkProxySpec { pub(crate) fn from_config_and_constraints( config: NetworkProxyConfig, requirements: Option, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, ) -> std::io::Result { let base_config = config.clone(); let hard_deny_allowlist_misses = requirements @@ -99,7 +99,7 @@ impl NetworkProxySpec { Self::apply_requirements( config, requirements, - sandbox_policy, + permission_profile, hard_deny_allowlist_misses, ) } else { @@ -122,7 +122,7 @@ impl NetworkProxySpec { pub async fn start_proxy( &self, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, policy_decider: Option>, blocked_request_observer: Option>, enable_network_approval_flow: bool, @@ -133,10 +133,7 @@ impl NetworkProxySpec { if enable_network_approval_flow && !self.hard_deny_allowlist_misses { if let Some(policy_decider) = policy_decider { builder = builder.policy_decider_arc(policy_decider); - } else if matches!( - sandbox_policy, - SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } - ) { + } else if Self::managed_sandbox_active(permission_profile) { builder = builder .policy_decider(|_request| async { NetworkDecision::ask("not_allowed") }); } @@ -154,14 +151,14 @@ impl NetworkProxySpec { Ok(StartedNetworkProxy::new(proxy, handle)) } - pub(crate) fn recompute_for_sandbox_policy( + pub(crate) fn recompute_for_permission_profile( &self, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, ) -> std::io::Result { Self::from_config_and_constraints( self.base_config.clone(), self.requirements.clone(), - sandbox_policy, + permission_profile, ) } @@ -216,13 +213,13 @@ impl NetworkProxySpec { fn apply_requirements( mut config: NetworkProxyConfig, requirements: &NetworkConstraints, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, hard_deny_allowlist_misses: bool, ) -> (NetworkProxyConfig, NetworkProxyConstraints) { let mut constraints = NetworkProxyConstraints::default(); let allowlist_expansion_enabled = - Self::allowlist_expansion_enabled(sandbox_policy, hard_deny_allowlist_misses); - let denylist_expansion_enabled = Self::denylist_expansion_enabled(sandbox_policy); + Self::allowlist_expansion_enabled(permission_profile, hard_deny_allowlist_misses); + let denylist_expansion_enabled = Self::denylist_expansion_enabled(permission_profile); if let Some(enabled) = requirements.enabled { config.network.enabled = enabled; @@ -322,24 +319,22 @@ impl NetworkProxySpec { } fn allowlist_expansion_enabled( - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, hard_deny_allowlist_misses: bool, ) -> bool { - matches!( - sandbox_policy, - SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } - ) && !hard_deny_allowlist_misses + Self::managed_sandbox_active(permission_profile) && !hard_deny_allowlist_misses } fn managed_allowed_domains_only(requirements: &NetworkConstraints) -> bool { requirements.managed_allowed_domains_only.unwrap_or(false) } - fn denylist_expansion_enabled(sandbox_policy: &SandboxPolicy) -> bool { - matches!( - sandbox_policy, - SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } - ) + fn denylist_expansion_enabled(permission_profile: &PermissionProfile) -> bool { + Self::managed_sandbox_active(permission_profile) + } + + fn managed_sandbox_active(permission_profile: &PermissionProfile) -> bool { + matches!(permission_profile, PermissionProfile::Managed { .. }) } fn merge_domain_lists(mut managed: Vec, user_entries: &[String]) -> Vec { diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index 5ba4bd1536..2600fc90b3 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -2,8 +2,14 @@ use super::*; use crate::config_loader::NetworkDomainPermissionToml; use crate::config_loader::NetworkDomainPermissionsToml; use codex_network_proxy::NetworkDomainPermission; +use codex_protocol::models::PermissionProfile; +use codex_protocol::protocol::SandboxPolicy; use pretty_assertions::assert_eq; +fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { + PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) +} + fn domain_permissions( entries: impl IntoIterator, ) -> NetworkDomainPermissionsToml { @@ -54,7 +60,7 @@ fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_read_only_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_read_only_policy()), ) .expect("config should stay within the managed allowlist"); @@ -89,7 +95,7 @@ fn requirements_allowed_domains_do_not_override_user_denies_for_same_pattern() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), ) .expect("managed allowlist should not erase a user deny"); @@ -121,7 +127,7 @@ fn requirements_allowlist_expansion_keeps_user_entries_mutable() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), ) .expect("managed baseline should still allow user edits"); @@ -164,7 +170,7 @@ fn danger_full_access_keeps_managed_allowlist_and_denylist_fixed() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), ) .expect("yolo mode should pin the effective policy to the managed baseline"); @@ -198,7 +204,7 @@ fn managed_allowed_domains_only_disables_default_mode_allowlist_expansion() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), ) .expect("managed baseline should still load"); @@ -227,7 +233,7 @@ fn managed_allowed_domains_only_ignores_user_allowlist_and_hard_denies_misses() let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), ) .expect("managed-only allowlist should still load"); @@ -257,7 +263,7 @@ fn managed_allowed_domains_only_without_managed_allowlist_blocks_all_user_domain let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), ) .expect("managed-only mode should treat missing managed allowlist as empty"); @@ -281,7 +287,7 @@ fn managed_allowed_domains_only_blocks_all_user_domains_in_full_access_without_m let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), ) .expect("managed-only mode should treat missing managed allowlist as empty"); @@ -308,7 +314,7 @@ fn deny_only_requirements_do_not_create_allow_constraints_in_full_access() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), ) .expect("deny-only requirements should not constrain the allowlist"); @@ -341,7 +347,7 @@ fn allow_only_requirements_do_not_create_deny_constraints_in_full_access() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), ) .expect("allow-only requirements should not constrain the denylist"); @@ -374,7 +380,7 @@ fn requirements_denied_domains_are_a_baseline_for_default_mode() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), ) .expect("default mode should merge managed and user deny entries"); @@ -409,7 +415,7 @@ fn requirements_denylist_expansion_keeps_user_entries_mutable() { let spec = NetworkProxySpec::from_config_and_constraints( config, Some(requirements), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), ) .expect("managed baseline should still allow user edits"); diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 754cb43af6..fac589c58b 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -874,7 +874,7 @@ pub(crate) fn build_guardian_review_session_config( guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints( live_network_config, network_constraints, - &SandboxPolicy::new_read_only_policy(), + guardian_config.permissions.permission_profile.get(), )?); } for feature in [ diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index c78884bcea..77401a6d0e 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -27,6 +27,7 @@ use codex_protocol::ThreadId; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::models::ContentItem; +use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -1942,7 +1943,7 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() { }), ..Default::default() }), - parent_config.permissions.sandbox_policy.get(), + parent_config.permissions.permission_profile.get(), ) .expect("network proxy spec"); parent_config.permissions.network = Some(network.clone()); @@ -2007,7 +2008,7 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() { NetworkProxySpec::from_config_and_constraints( parent_network, /*requirements*/ None, - parent_config.permissions.sandbox_policy.get(), + parent_config.permissions.permission_profile.get(), ) .expect("parent network proxy spec"), ); @@ -2032,7 +2033,9 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() { NetworkProxySpec::from_config_and_constraints( live_network, /*requirements*/ None, - &SandboxPolicy::new_read_only_policy(), + &PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), ) .expect("live network proxy spec") ) diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 7e2de35e89..56059f8eee 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -2,9 +2,8 @@ use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; use codex_network_proxy::NetworkProxy; -use codex_protocol::permissions::FileSystemSandboxPolicy; -use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; +use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_sandboxing::landlock::allow_network_for_proxy; use codex_sandboxing::landlock::create_linux_sandbox_command_args_for_policies; @@ -18,15 +17,15 @@ use tokio::process::Child; /// isolation plus seccomp for network restrictions. /// /// Unlike macOS Seatbelt where we directly embed the policy text, the Linux -/// helper is a separate executable. We pass the legacy [`SandboxPolicy`] plus -/// split filesystem/network policies as JSON so the helper can migrate -/// incrementally without breaking older call sites. +/// helper is a separate executable. We pass both the canonical split +/// filesystem/network policies and a compatibility legacy projection as JSON +/// until the helper protocol no longer needs the legacy field. #[allow(clippy::too_many_arguments)] pub async fn spawn_command_under_linux_sandbox

( codex_linux_sandbox_exe: P, command: Vec, command_cwd: AbsolutePathBuf, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, sandbox_policy_cwd: &AbsolutePathBuf, use_legacy_landlock: bool, stdio_policy: StdioPolicy, @@ -36,15 +35,18 @@ pub async fn spawn_command_under_linux_sandbox

( where P: AsRef, { - let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( - sandbox_policy, - sandbox_policy_cwd, + let (file_system_sandbox_policy, network_sandbox_policy) = + permission_profile.to_runtime_permissions(); + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + permission_profile, + &file_system_sandbox_policy, + network_sandbox_policy, + sandbox_policy_cwd.as_path(), ); - let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy); let args = create_linux_sandbox_command_args_for_policies( command, command_cwd.as_path(), - sandbox_policy, + &sandbox_policy, &file_system_sandbox_policy, network_sandbox_policy, sandbox_policy_cwd, diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 843145de3e..c8c85a6814 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -6,9 +6,9 @@ use crate::util::resolve_path; use codex_apply_patch::ApplyPatchAction; use codex_apply_patch::ApplyPatchFileChange; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; use codex_utils_absolute_path::AbsolutePathBuf; @@ -33,7 +33,7 @@ pub enum SafetyCheck { pub fn assess_patch_safety( action: &ApplyPatchAction, policy: AskForApproval, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, file_system_sandbox_policy: &FileSystemSandboxPolicy, cwd: &AbsolutePathBuf, windows_sandbox_level: WindowsSandboxLevel, @@ -71,10 +71,11 @@ pub fn assess_patch_safety( || matches!(policy, AskForApproval::OnFailure) { if matches!( - sandbox_policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } + permission_profile, + PermissionProfile::Disabled | PermissionProfile::External { .. } ) { - // DangerFullAccess is intended to bypass sandboxing entirely. + // Disabled and External profiles intentionally do not apply an + // outer Codex filesystem sandbox. SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, user_explicitly_approved: false, @@ -91,7 +92,12 @@ pub fn assess_patch_safety( None => { if rejects_sandbox_approval { SafetyCheck::Reject { - reason: patch_rejection_reason(sandbox_policy).to_string(), + reason: patch_rejection_reason( + permission_profile, + file_system_sandbox_policy, + cwd, + ) + .to_string(), } } else { SafetyCheck::AskUser @@ -101,19 +107,31 @@ pub fn assess_patch_safety( } } else if rejects_sandbox_approval { SafetyCheck::Reject { - reason: patch_rejection_reason(sandbox_policy).to_string(), + reason: patch_rejection_reason(permission_profile, file_system_sandbox_policy, cwd) + .to_string(), } } else { SafetyCheck::AskUser } } -fn patch_rejection_reason(sandbox_policy: &SandboxPolicy) -> &'static str { - match sandbox_policy { - SandboxPolicy::ReadOnly { .. } => PATCH_REJECTED_READ_ONLY_REASON, - SandboxPolicy::WorkspaceWrite { .. } - | SandboxPolicy::DangerFullAccess - | SandboxPolicy::ExternalSandbox { .. } => PATCH_REJECTED_OUTSIDE_PROJECT_REASON, +fn patch_rejection_reason( + permission_profile: &PermissionProfile, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + cwd: &AbsolutePathBuf, +) -> &'static str { + match permission_profile { + PermissionProfile::Managed { .. } + if !file_system_sandbox_policy.has_full_disk_write_access() + && file_system_sandbox_policy + .get_writable_roots_with_cwd(cwd.as_path()) + .is_empty() => + { + PATCH_REJECTED_READ_ONLY_REASON + } + PermissionProfile::Managed { .. } + | PermissionProfile::Disabled + | PermissionProfile::External { .. } => PATCH_REJECTED_OUTSIDE_PROJECT_REASON, } } diff --git a/codex-rs/core/src/safety_tests.rs b/codex-rs/core/src/safety_tests.rs index 774673f887..0ca10e66e6 100644 --- a/codex-rs/core/src/safety_tests.rs +++ b/codex-rs/core/src/safety_tests.rs @@ -1,14 +1,20 @@ use super::*; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::FileSystemAccessMode; use codex_protocol::protocol::FileSystemPath; use codex_protocol::protocol::FileSystemSandboxEntry; use codex_protocol::protocol::FileSystemSpecialPath; use codex_protocol::protocol::GranularApprovalConfig; +use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::PathExt; use pretty_assertions::assert_eq; use tempfile::TempDir; +fn permission_profile_for_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { + PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) +} + #[test] fn test_writable_roots_constraint() { // Use a temporary directory as our workspace to avoid touching @@ -75,7 +81,7 @@ fn external_sandbox_auto_approves_in_on_request() { assess_patch_safety( &add_inside, AskForApproval::OnRequest, - &policy, + &permission_profile_for_policy(&policy), &FileSystemSandboxPolicy::from(&policy), &cwd, WindowsSandboxLevel::Disabled @@ -105,7 +111,7 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() { assess_patch_safety( &add_outside, AskForApproval::OnRequest, - &policy_workspace_only, + &permission_profile_for_policy(&policy_workspace_only), &FileSystemSandboxPolicy::from(&policy_workspace_only), &cwd, WindowsSandboxLevel::Disabled, @@ -122,7 +128,7 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() { request_permissions: true, mcp_elicitations: true, }), - &policy_workspace_only, + &permission_profile_for_policy(&policy_workspace_only), &FileSystemSandboxPolicy::from(&policy_workspace_only), &cwd, WindowsSandboxLevel::Disabled, @@ -155,7 +161,7 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() { request_permissions: true, mcp_elicitations: true, }), - &policy_workspace_only, + &permission_profile_for_policy(&policy_workspace_only), &FileSystemSandboxPolicy::from(&policy_workspace_only), &cwd, WindowsSandboxLevel::Disabled, @@ -185,7 +191,7 @@ fn read_only_policy_rejects_patch_with_read_only_reason() { assess_patch_safety( &action, AskForApproval::Never, - &sandbox_policy, + &permission_profile_for_policy(&sandbox_policy), &file_system_sandbox_policy, &cwd, WindowsSandboxLevel::Disabled, @@ -229,7 +235,7 @@ fn explicit_unreadable_paths_prevent_auto_approval_for_external_sandbox() { assess_patch_safety( &action, AskForApproval::OnRequest, - &sandbox_policy, + &permission_profile_for_policy(&sandbox_policy), &file_system_sandbox_policy, &cwd, WindowsSandboxLevel::Disabled, @@ -273,7 +279,7 @@ fn explicit_read_only_subpaths_prevent_auto_approval_for_external_sandbox() { assess_patch_safety( &action, AskForApproval::OnRequest, - &sandbox_policy, + &permission_profile_for_policy(&sandbox_policy), &file_system_sandbox_policy, &cwd, WindowsSandboxLevel::Disabled, @@ -306,7 +312,7 @@ fn missing_project_dot_codex_config_requires_approval() { assess_patch_safety( &action, AskForApproval::OnRequest, - &sandbox_policy, + &permission_profile_for_policy(&sandbox_policy), &file_system_sandbox_policy, &cwd, WindowsSandboxLevel::Disabled, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index fc0c830662..ce3a96e583 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -845,8 +845,10 @@ impl Session { } } - fn managed_network_proxy_active_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> bool { - !matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) + fn managed_network_proxy_active_for_permission_profile( + permission_profile: &PermissionProfile, + ) -> bool { + !matches!(permission_profile, PermissionProfile::Disabled) } /// Builds the `x-codex-beta-features` header value for this session. @@ -879,7 +881,7 @@ impl Session { async fn start_managed_network_proxy( spec: &crate::config::NetworkProxySpec, exec_policy: &codex_execpolicy::Policy, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, network_policy_decider: Option>, blocked_request_observer: Option>, managed_network_requirements_enabled: bool, @@ -896,7 +898,7 @@ impl Session { .unwrap_or_else(|_| spec.clone()); let network_proxy = spec .start_proxy( - sandbox_policy, + permission_profile, network_policy_decider, blocked_request_observer, managed_network_requirements_enabled, @@ -914,7 +916,7 @@ impl Session { Ok((network_proxy, session_network_proxy)) } - async fn refresh_managed_network_proxy_for_current_sandbox_policy(&self) { + async fn refresh_managed_network_proxy_for_current_permission_profile(&self) { let Some(started_proxy) = self.services.network_proxy.as_ref() else { return; }; @@ -935,7 +937,8 @@ impl Session { return; }; - let spec = match spec.recompute_for_sandbox_policy(&session_configuration.sandbox_policy()) + let spec = match spec + .recompute_for_permission_profile(&session_configuration.permission_profile()) { Ok(spec) => spec, Err(err) => { @@ -1285,7 +1288,7 @@ impl Session { &self, updates: SessionSettingsUpdate, ) -> ConstraintResult<()> { - let (previous_cwd, sandbox_policy_changed, next_cwd, codex_home, session_source) = { + let (previous_cwd, permission_profile_changed, next_cwd, codex_home, session_source) = { let mut state = self.state.lock().await; let updated = match state.session_configuration.apply(&updates) { Ok(updated) => updated, @@ -1296,16 +1299,17 @@ impl Session { }; let previous_cwd = state.session_configuration.cwd.clone(); - let previous_sandbox_policy = state.session_configuration.sandbox_policy(); - let updated_sandbox_policy = updated.sandbox_policy(); - let sandbox_policy_changed = previous_sandbox_policy != updated_sandbox_policy; + let previous_permission_profile = state.session_configuration.permission_profile(); + let updated_permission_profile = updated.permission_profile(); + let permission_profile_changed = + previous_permission_profile != updated_permission_profile; let next_cwd = updated.cwd.clone(); let codex_home = updated.codex_home.clone(); let session_source = updated.session_source.clone(); state.session_configuration = updated; ( previous_cwd, - sandbox_policy_changed, + permission_profile_changed, next_cwd, codex_home, session_source, @@ -1318,8 +1322,8 @@ impl Session { &codex_home, &session_source, ); - if sandbox_policy_changed { - self.refresh_managed_network_proxy_for_current_sandbox_policy() + if permission_profile_changed { + self.refresh_managed_network_proxy_for_current_permission_profile() .await; } diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 406c60a025..789b00699e 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -709,7 +709,7 @@ impl Session { let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy( spec, current_exec_policy.as_ref(), - config.permissions.sandbox_policy.get(), + config.permissions.permission_profile.get(), network_policy_decider.as_ref().map(Arc::clone), blocked_request_observer.as_ref().map(Arc::clone), managed_network_requirements_configured, @@ -863,8 +863,8 @@ impl Session { history_entry_count, initial_messages, network_proxy: session_network_proxy.filter(|_| { - Self::managed_network_proxy_active_for_sandbox_policy( - &session_sandbox_policy, + Self::managed_network_proxy_active_for_permission_profile( + session_configuration.permission_profile.get(), ) }), rollout_path, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 707ede3ba8..1958be1515 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -150,6 +150,10 @@ use std::time::Duration as StdDuration; mod guardian_tests; +fn permission_profile_for_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { + PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) +} + struct InstructionsTestCase { slug: &'static str, expects_apply_patch_description: bool, @@ -584,7 +588,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho let spec = crate::config::NetworkProxySpec::from_config_and_constraints( NetworkProxyConfig::default(), /*requirements*/ None, - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), )?; let mut exec_policy = Policy::empty(); exec_policy.add_network_rule( @@ -597,7 +601,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &exec_policy, - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), /*network_policy_decider*/ None, /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ false, @@ -628,7 +632,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() managed_allowed_domains_only: Some(true), ..Default::default() }), - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), )?; let mut exec_policy = Policy::empty(); exec_policy.add_network_rule( @@ -641,7 +645,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &exec_policy, - &SandboxPolicy::new_workspace_write_policy(), + &permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), /*network_policy_decider*/ None, /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ false, @@ -665,7 +669,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R enabled: Some(true), ..Default::default() }), - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), )?; let exec_policy = Policy::empty(); let decider_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0)); @@ -680,7 +684,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &exec_policy, - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), Some(network_policy_decider), /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ true, @@ -688,7 +692,9 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R ) .await?; - let spec = spec.recompute_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy())?; + let spec = spec.recompute_for_permission_profile(&permission_profile_for_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + ))?; spec.apply_to_started_proxy(&started_proxy).await?; let current_cfg = started_proxy.proxy().current_cfg().await?; assert_eq!(current_cfg.network.allowed_domains(), None); @@ -745,12 +751,12 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow let spec = crate::config::NetworkProxySpec::from_config_and_constraints( network_config, Some(requirements), - &initial_policy, + &permission_profile_for_sandbox_policy(&initial_policy), )?; let (started_proxy, _) = Session::start_managed_network_proxy( &spec, &Policy::empty(), - &initial_policy, + &permission_profile_for_sandbox_policy(&initial_policy), /*network_policy_decider*/ None, /*blocked_request_observer*/ None, /*managed_network_requirements_enabled*/ false, @@ -823,14 +829,15 @@ async fn danger_full_access_turns_do_not_expose_managed_network_proxy() -> anyho enabled: Some(true), ..Default::default() }), - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), )?; let session = make_session_with_config(move |config| { - config.permissions.sandbox_policy = - codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess); - config.permissions.permission_profile = - codex_config::Constrained::allow_any(PermissionProfile::Disabled); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path()) + .expect("test setup should allow sandbox policy"); config.permissions.network = Some(network_spec); }) .await?; @@ -888,14 +895,15 @@ async fn danger_full_access_tool_attempts_do_not_enforce_managed_network() -> an enabled: Some(true), ..Default::default() }), - &SandboxPolicy::DangerFullAccess, + &permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess), )?; let session = make_session_with_config(move |config| { - config.permissions.sandbox_policy = - codex_config::Constrained::allow_any(SandboxPolicy::DangerFullAccess); - config.permissions.permission_profile = - codex_config::Constrained::allow_any(PermissionProfile::Disabled); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path()) + .expect("test setup should allow sandbox policy"); config.permissions.network = Some(network_spec); let layers = config @@ -962,11 +970,15 @@ async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> any enabled: Some(true), ..Default::default() }), - &sandbox_policy, + &permission_profile_for_sandbox_policy(&sandbox_policy), )?; let session = make_session_with_config(move |config| { - config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(sandbox_policy, cwd.as_path()) + .expect("test setup should allow sandbox policy"); config.permissions.network = Some(network_spec); }) .await?; @@ -985,11 +997,15 @@ async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::R enabled: Some(true), ..Default::default() }), - &sandbox_policy, + &permission_profile_for_sandbox_policy(&sandbox_policy), )?; let (session, rx) = make_session_with_config_and_rx(move |config| { - config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(sandbox_policy, cwd.as_path()) + .expect("test setup should allow sandbox policy"); config.permissions.network = Some(network_spec); }) .await?; diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 52b579151f..eda8f42093 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -692,7 +692,8 @@ async fn track_turn_resolved_config_analytics( session_source: thread_config.session_source, model: turn_context.model_info.slug.clone(), model_provider: turn_context.config.model_provider_id.clone(), - sandbox_policy: turn_context.sandbox_policy(), + permission_profile: turn_context.permission_profile(), + permission_profile_cwd: turn_context.cwd.to_path_buf(), reasoning_effort: turn_context.reasoning_effort, reasoning_summary: Some(turn_context.reasoning_summary), service_tier: turn_context.config.service_tier, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 3096b495d6..857b9a88f6 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -534,16 +534,18 @@ impl Session { let turn_environments = self.resolve_turn_environments(&effective_environments)?; let previous_cwd = state.session_configuration.cwd.clone(); - let previous_sandbox_policy = state.session_configuration.sandbox_policy(); - let next_sandbox_policy = next.sandbox_policy(); - let sandbox_policy_changed = previous_sandbox_policy != next_sandbox_policy; + let previous_permission_profile = + state.session_configuration.permission_profile(); + let next_permission_profile = next.permission_profile(); + let permission_profile_changed = + previous_permission_profile != next_permission_profile; let codex_home = next.codex_home.clone(); let session_source = next.session_source.clone(); state.session_configuration = next.clone(); Ok(( next, turn_environments, - sandbox_policy_changed, + permission_profile_changed, previous_cwd, codex_home, session_source, @@ -556,7 +558,7 @@ impl Session { let ( session_configuration, turn_environments, - sandbox_policy_changed, + permission_profile_changed, previous_cwd, codex_home, session_source, @@ -583,8 +585,8 @@ impl Session { &session_source, ); - if sandbox_policy_changed { - self.refresh_managed_network_proxy_for_current_sandbox_policy() + if permission_profile_changed { + self.refresh_managed_network_proxy_for_current_permission_profile() .await; } @@ -686,8 +688,8 @@ impl Session { .network_proxy .as_ref() .and_then(|started_proxy| { - Self::managed_network_proxy_active_for_sandbox_policy( - &session_configuration.sandbox_policy(), + Self::managed_network_proxy_active_for_permission_profile( + &session_configuration.permission_profile(), ) .then(|| started_proxy.proxy()) }), diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index f12200e54f..61e7bc15ae 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -3,6 +3,10 @@ use std::time::Duration; use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; +use codex_network_proxy::PROXY_ACTIVE_ENV_KEY; +use codex_network_proxy::PROXY_ENV_KEYS; +#[cfg(target_os = "macos")] +use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; use codex_protocol::user_input::UserInput; use tokio_util::sync::CancellationToken; use tracing::error; @@ -123,10 +127,24 @@ pub(crate) async fn execute_user_shell_command( let use_login_shell = true; let session_shell = session.user_shell(); let display_command = session_shell.derive_exec_args(&command, use_login_shell); - let exec_env_map = create_env( + let mut exec_env_map = create_env( &turn_context.shell_environment_policy, Some(session.conversation_id), ); + if exec_env_map.contains_key(PROXY_ACTIVE_ENV_KEY) { + for key in PROXY_ENV_KEYS { + exec_env_map.remove(*key); + } + #[cfg(target_os = "macos")] + if exec_env_map + .get(PROXY_GIT_SSH_COMMAND_ENV_KEY) + .is_some_and(|value| { + value.starts_with(codex_network_proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER) + }) + { + exec_env_map.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY); + } + } let exec_command = maybe_wrap_shell_lc_with_snapshot( &display_command, session_shell.as_ref(), diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index af0331700b..1264e809b5 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -21,11 +21,11 @@ use codex_network_proxy::NetworkProxy; use codex_protocol::approvals::NetworkApprovalContext; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::approvals::NetworkPolicyRuleAction; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ReviewDecision; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::WarningEvent; use indexmap::IndexMap; use std::collections::HashMap; @@ -127,11 +127,8 @@ fn allows_network_approval_flow(policy: AskForApproval) -> bool { !matches!(policy, AskForApproval::Never) } -fn sandbox_policy_allows_network_approval_flow(policy: &SandboxPolicy) -> bool { - matches!( - policy, - SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } - ) +fn permission_profile_allows_network_approval_flow(permission_profile: &PermissionProfile) -> bool { + matches!(permission_profile, PermissionProfile::Managed { .. }) } impl PendingApprovalDecision { @@ -359,8 +356,7 @@ impl NetworkApprovalService { .await; return NetworkDecision::deny(REASON_NOT_ALLOWED); }; - let sandbox_policy = turn_context.sandbox_policy(); - if !sandbox_policy_allows_network_approval_flow(&sandbox_policy) { + if !permission_profile_allows_network_approval_flow(&turn_context.permission_profile()) { pending.set_decision(PendingApprovalDecision::Deny).await; self.pending_host_approvals.lock().await.remove(&key); self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy( diff --git a/codex-rs/core/src/tools/network_approval_tests.rs b/codex-rs/core/src/tools/network_approval_tests.rs index ac0046228b..683c8c7539 100644 --- a/codex-rs/core/src/tools/network_approval_tests.rs +++ b/codex-rs/core/src/tools/network_approval_tests.rs @@ -1,6 +1,8 @@ use super::*; use crate::sandboxing::SandboxPermissions; use codex_network_proxy::BlockedRequestArgs; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use core_test_support::PathBufExt; @@ -185,14 +187,19 @@ fn only_never_policy_disables_network_approval_flow() { #[test] fn network_approval_flow_is_limited_to_restricted_sandbox_modes() { - assert!(sandbox_policy_allows_network_approval_flow( - &SandboxPolicy::new_read_only_policy() + assert!(permission_profile_allows_network_approval_flow( + &PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_read_only_policy()) )); - assert!(sandbox_policy_allows_network_approval_flow( - &SandboxPolicy::new_workspace_write_policy() + assert!(permission_profile_allows_network_approval_flow( + &PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()) )); - assert!(!sandbox_policy_allows_network_approval_flow( - &SandboxPolicy::DangerFullAccess + assert!(!permission_profile_allows_network_approval_flow( + &PermissionProfile::Disabled + )); + assert!(!permission_profile_allows_network_approval_flow( + &PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + } )); } diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 050f5bea81..5eecea2562 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -15,7 +15,6 @@ use codex_features::Feature; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction; -use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -2835,8 +2834,11 @@ allow_local_binding = true }; let mut builder = test_codex().with_home(home).with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(SandboxPolicy::DangerFullAccess); - config.permissions.permission_profile = Constrained::allow_any(PermissionProfile::Disabled); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path()) + .expect("test setup should allow sandbox policy"); let layers = config .config_layer_stack .get_layers( diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index 84d4a6beb8..feb1a7b8c8 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -89,13 +89,16 @@ async fn spawn_command_under_sandbox( env: HashMap, ) -> std::io::Result { use codex_core::spawn_command_under_linux_sandbox; + use codex_protocol::models::PermissionProfile; + let codex_linux_sandbox_exe = core_test_support::find_codex_linux_sandbox_exe() .map_err(|err| io::Error::new(io::ErrorKind::NotFound, err))?; + let permission_profile = PermissionProfile::from_legacy_sandbox_policy(sandbox_policy); spawn_command_under_linux_sandbox( codex_linux_sandbox_exe, command, command_cwd, - sandbox_policy, + &permission_profile, sandbox_cwd, /*use_legacy_landlock*/ false, stdio_policy, From 2a018fc6fe7f5fb5e82be6d7ef371bc2c2791dd7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 24 Apr 2026 20:37:16 -0700 Subject: [PATCH 2/3] permissions: remove core legacy policy round trips --- codex-rs/core/src/config/config_tests.rs | 17 +-- codex-rs/core/src/config/mod.rs | 4 +- codex-rs/core/src/exec_policy.rs | 32 +++-- codex-rs/core/src/exec_policy_tests.rs | 52 +++++-- codex-rs/core/src/session/tests.rs | 3 +- codex-rs/core/src/session/turn_context.rs | 6 +- codex-rs/core/src/tools/handlers/shell.rs | 3 +- .../tools/runtimes/shell/unix_escalation.rs | 38 ++--- .../runtimes/shell/unix_escalation_tests.rs | 61 ++++---- .../core/src/unified_exec/process_manager.rs | 3 +- codex-rs/protocol/src/approvals.rs | 5 - codex-rs/sandboxing/src/policy_transforms.rs | 134 ------------------ .../sandboxing/src/policy_transforms_tests.rs | 63 -------- 13 files changed, 118 insertions(+), 303 deletions(-) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index e32abc6a89..85e3d3e185 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -2094,24 +2094,25 @@ fn web_search_mode_disabled_overrides_legacy_request() { #[test] fn web_search_mode_for_turn_uses_preference_for_read_only() { let web_search_mode = Constrained::allow_any(WebSearchMode::Cached); - let mode = - resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::new_read_only_policy()); + let permission_profile = + PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_read_only_policy()); + let mode = resolve_web_search_mode_for_turn(&web_search_mode, &permission_profile); assert_eq!(mode, WebSearchMode::Cached); } #[test] -fn web_search_mode_for_turn_prefers_live_for_danger_full_access() { +fn web_search_mode_for_turn_prefers_live_for_disabled_permissions() { let web_search_mode = Constrained::allow_any(WebSearchMode::Cached); - let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess); + let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled); assert_eq!(mode, WebSearchMode::Live); } #[test] -fn web_search_mode_for_turn_respects_disabled_for_danger_full_access() { +fn web_search_mode_for_turn_respects_disabled_for_disabled_permissions() { let web_search_mode = Constrained::allow_any(WebSearchMode::Disabled); - let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess); + let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled); assert_eq!(mode, WebSearchMode::Disabled); } @@ -2131,7 +2132,7 @@ fn web_search_mode_for_turn_falls_back_when_live_is_disallowed() -> anyhow::Resu }) } })?; - let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::DangerFullAccess); + let mode = resolve_web_search_mode_for_turn(&web_search_mode, &PermissionProfile::Disabled); assert_eq!(mode, WebSearchMode::Cached); Ok(()) @@ -6695,7 +6696,7 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() -> assert_eq!( resolve_web_search_mode_for_turn( &config.web_search_mode, - config.permissions.sandbox_policy.get(), + &config.permissions.permission_profile(), ), WebSearchMode::Cached, ); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d8fe67b838..dee7216347 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1606,11 +1606,11 @@ fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiA pub(crate) fn resolve_web_search_mode_for_turn( web_search_mode: &Constrained, - sandbox_policy: &SandboxPolicy, + permission_profile: &PermissionProfile, ) -> WebSearchMode { let preferred = web_search_mode.value(); - if matches!(sandbox_policy, SandboxPolicy::DangerFullAccess) + if matches!(permission_profile, PermissionProfile::Disabled) && preferred != WebSearchMode::Disabled { for mode in [ diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 54ad8058d0..4ea827cb5a 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -20,10 +20,10 @@ use codex_execpolicy::RuleMatch; use codex_execpolicy::blocking_append_allow_prefix_rule; use codex_execpolicy::blocking_append_network_rule; use codex_protocol::approvals::ExecPolicyAmendment; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_shell_command::is_dangerous_command::command_might_be_dangerous; use codex_shell_command::is_safe_command::is_known_safe_command; use thiserror::Error; @@ -204,7 +204,7 @@ pub(crate) struct ExecPolicyManager { pub(crate) struct ExecApprovalRequest<'a> { pub(crate) command: &'a [String], pub(crate) approval_policy: AskForApproval, - pub(crate) sandbox_policy: &'a SandboxPolicy, + pub(crate) permission_profile: PermissionProfile, pub(crate) file_system_sandbox_policy: &'a FileSystemSandboxPolicy, pub(crate) sandbox_permissions: SandboxPermissions, pub(crate) prefix_rule: Option>, @@ -238,7 +238,7 @@ impl ExecPolicyManager { let ExecApprovalRequest { command, approval_policy, - sandbox_policy, + permission_profile, file_system_sandbox_policy, sandbox_permissions, prefix_rule, @@ -252,7 +252,7 @@ impl ExecPolicyManager { let exec_policy_fallback = |cmd: &[String]| { render_decision_for_unmatched_command( approval_policy, - sandbox_policy, + &permission_profile, file_system_sandbox_policy, cmd, sandbox_permissions, @@ -580,7 +580,7 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result { let sandbox_is_explicitly_disabled = matches!( - sandbox_policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } + permission_profile, + PermissionProfile::Disabled | PermissionProfile::External { .. } ); if sandbox_is_explicitly_disabled { // If the sandbox is explicitly disabled, we should allow the command to run @@ -670,6 +670,20 @@ pub fn render_decision_for_unmatched_command( } } +fn profile_is_managed_read_only( + permission_profile: &PermissionProfile, + file_system_sandbox_policy: &FileSystemSandboxPolicy, +) -> bool { + matches!(permission_profile, PermissionProfile::Managed { .. }) + && matches!( + file_system_sandbox_policy.kind, + FileSystemSandboxKind::Restricted + ) + && file_system_sandbox_policy + .get_writable_roots_with_cwd(Path::new("/")) + .is_empty() +} + fn default_policy_path(codex_home: &Path) -> PathBuf { codex_home.join(RULES_DIR_NAME).join(DEFAULT_POLICY_FILE) } diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index fe4560a781..cf7eac8da8 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -15,6 +15,7 @@ use codex_config::RequirementsExecPolicy; use codex_config::config_toml::ConfigToml; use codex_config::config_toml::ProjectConfig; use codex_protocol::config_types::TrustLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -116,6 +117,10 @@ fn external_file_system_sandbox_policy() -> FileSystemSandboxPolicy { FileSystemSandboxPolicy::external_sandbox() } +fn permission_profile_from_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { + PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) +} + async fn test_config() -> (TempDir, Config) { let home = TempDir::new().expect("create temp dir"); let config = ConfigBuilder::without_managed_config_for_tests() @@ -954,7 +959,7 @@ fn unmatched_granular_policy_still_prompts_for_restricted_sandbox_escalation() { request_permissions: true, mcp_elicitations: true, }), - &SandboxPolicy::new_read_only_policy(), + &permission_profile_from_sandbox_policy(&SandboxPolicy::new_read_only_policy()), &read_only_file_system_sandbox_policy(), &command, SandboxPermissions::RequireEscalated, @@ -972,7 +977,7 @@ fn unmatched_on_request_uses_split_filesystem_policy_for_escalation_prompts() { Decision::Prompt, render_decision_for_unmatched_command( AskForApproval::OnRequest, - &SandboxPolicy::DangerFullAccess, + &PermissionProfile::Disabled, &restricted_file_system_policy, &command, SandboxPermissions::RequireEscalated, @@ -1058,7 +1063,9 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision() request_permissions: true, mcp_elicitations: true, }), - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::RequireEscalated, prefix_rule: None, @@ -1095,7 +1102,9 @@ async fn mixed_rule_and_sandbox_prompt_rejects_when_granular_rules_are_disabled( request_permissions: true, mcp_elicitations: true, }), - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::RequireEscalated, prefix_rule: None, @@ -1119,7 +1128,9 @@ async fn exec_approval_requirement_falls_back_to_heuristics() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, @@ -1144,7 +1155,9 @@ async fn empty_bash_lc_script_falls_back_to_original_command() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, @@ -1173,7 +1186,9 @@ async fn whitespace_bash_lc_script_falls_back_to_original_command() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, @@ -1202,7 +1217,9 @@ async fn request_rule_uses_prefix_rule() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::RequireEscalated, prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]), @@ -1234,7 +1251,7 @@ async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: PermissionProfile::Disabled, file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::RequireEscalated, prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]), @@ -1273,7 +1290,7 @@ async fn heuristics_apply_when_other_commands_match_policy() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::DangerFullAccess, + permission_profile: PermissionProfile::Disabled, file_system_sandbox_policy: &unrestricted_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, @@ -1759,7 +1776,9 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &sneaky_command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: permissions, prefix_rule: None, @@ -1783,7 +1802,9 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &dangerous_command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: permissions, prefix_rule: None, @@ -1803,7 +1824,9 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &dangerous_command, approval_policy: AskForApproval::Never, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: permissions, prefix_rule: None, @@ -1897,11 +1920,12 @@ async fn assert_exec_approval_requirement_for_command( None => Arc::new(Policy::empty()), }; + let permission_profile = permission_profile_from_sandbox_policy(&sandbox_policy); let requirement = ExecPolicyManager::new(policy) .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy, - sandbox_policy: &sandbox_policy, + permission_profile, file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions, prefix_rule, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 1958be1515..f9b3110bc2 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -7036,14 +7036,13 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { turn_context_mut.permission_profile = PermissionProfile::Disabled; let file_system_sandbox_policy = turn_context.file_system_sandbox_policy(); - let sandbox_policy = turn_context.sandbox_policy(); let exec_approval_requirement = session .services .exec_policy .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: ¶ms.command, approval_policy: turn_context.approval_policy.value(), - sandbox_policy: &sandbox_policy, + permission_profile: turn_context.permission_profile(), file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 857b9a88f6..4bcc091bb0 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -383,10 +383,10 @@ impl Session { per_turn_config.permissions.permission_profile = session_configuration.permission_profile.clone(); let sandbox_policy = session_configuration.sandbox_policy(); - per_turn_config.permissions.sandbox_policy = - Constrained::allow_only(sandbox_policy.clone()); + per_turn_config.permissions.sandbox_policy = Constrained::allow_only(sandbox_policy); + let permission_profile = session_configuration.permission_profile(); let resolved_web_search_mode = - resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &sandbox_policy); + resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile); if let Err(err) = per_turn_config .web_search_mode .set(resolved_web_search_mode) diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index b43fab30b4..835b6170f8 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -514,14 +514,13 @@ impl ShellHandler { emitter.begin(event_ctx).await; let file_system_sandbox_policy = turn.file_system_sandbox_policy(); - let sandbox_policy = turn.sandbox_policy(); let exec_approval_requirement = session .services .exec_policy .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &exec_params.command, approval_policy: turn.approval_policy.value(), - sandbox_policy: &sandbox_policy, + permission_profile: turn.permission_profile(), file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: if effective_additional_permissions.permissions_preapproved { codex_protocol::models::SandboxPermissions::UseDefault diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 81c3071dda..3c67a995ce 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -32,7 +32,6 @@ use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::PermissionProfile; -use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; @@ -203,9 +202,8 @@ pub(super) async fn try_run_zsh_fork( call_id: ctx.call_id.clone(), tool_name: GuardianCommandSource::Shell, approval_policy: ctx.turn.approval_policy.value(), - sandbox_policy: command_executor.sandbox_policy.clone(), + permission_profile: command_executor.permission_profile.clone(), file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(), - network_sandbox_policy: command_executor.network_sandbox_policy, sandbox_permissions: req.sandbox_permissions, approval_sandbox_permissions, prompt_permissions: req.additional_permissions.clone(), @@ -276,9 +274,8 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( call_id: ctx.call_id.clone(), tool_name: GuardianCommandSource::UnifiedExec, approval_policy: ctx.turn.approval_policy.value(), - sandbox_policy: exec_request.sandbox_policy.clone(), + permission_profile: exec_request.permission_profile.clone(), file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(), - network_sandbox_policy: exec_request.network_sandbox_policy, sandbox_permissions: req.sandbox_permissions, approval_sandbox_permissions: approval_sandbox_permissions( req.sandbox_permissions, @@ -311,9 +308,8 @@ struct CoreShellActionProvider { call_id: String, tool_name: GuardianCommandSource, approval_policy: AskForApproval, - sandbox_policy: SandboxPolicy, + permission_profile: PermissionProfile, file_system_sandbox_policy: FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, sandbox_permissions: SandboxPermissions, approval_sandbox_permissions: SandboxPermissions, prompt_permissions: Option, @@ -363,9 +359,7 @@ impl CoreShellActionProvider { fn shell_request_escalation_execution( sandbox_permissions: SandboxPermissions, - sandbox_policy: &SandboxPolicy, - file_system_sandbox_policy: &FileSystemSandboxPolicy, - network_sandbox_policy: NetworkSandboxPolicy, + permission_profile: &PermissionProfile, additional_permissions: Option<&AdditionalPermissionProfile>, ) -> EscalationExecution { match sandbox_permissions { @@ -378,15 +372,7 @@ impl CoreShellActionProvider { EscalationExecution::Permissions( EscalationPermissions::ResolvedPermissionProfile( ResolvedPermissionProfile { - permission_profile: - PermissionProfile::from_runtime_permissions_with_enforcement( - SandboxEnforcement::from_legacy_sandbox_policy( - sandbox_policy, - ), - file_system_sandbox_policy, - network_sandbox_policy, - ), - sandbox_policy: sandbox_policy.clone(), + permission_profile: permission_profile.clone(), }, ), ) @@ -605,7 +591,7 @@ impl EscalationPolicy for CoreShellActionProvider { argv, InterceptedExecPolicyContext { approval_policy: self.approval_policy, - sandbox_policy: &self.sandbox_policy, + permission_profile: self.permission_profile.clone(), file_system_sandbox_policy: &self.file_system_sandbox_policy, sandbox_permissions: self.approval_sandbox_permissions, enable_shell_wrapper_parsing: @@ -629,9 +615,7 @@ impl EscalationPolicy for CoreShellActionProvider { DecisionSource::PrefixRule => EscalationExecution::Unsandboxed, DecisionSource::UnmatchedCommandFallback => Self::shell_request_escalation_execution( self.sandbox_permissions, - &self.sandbox_policy, - &self.file_system_sandbox_policy, - self.network_sandbox_policy, + &self.permission_profile, self.prompt_permissions.as_ref(), ), }; @@ -657,7 +641,7 @@ fn evaluate_intercepted_exec_policy( ) -> Evaluation { let InterceptedExecPolicyContext { approval_policy, - sandbox_policy, + permission_profile, file_system_sandbox_policy, sandbox_permissions, enable_shell_wrapper_parsing, @@ -682,7 +666,7 @@ fn evaluate_intercepted_exec_policy( let fallback = |cmd: &[String]| { crate::exec_policy::render_decision_for_unmatched_command( approval_policy, - sandbox_policy, + &permission_profile, file_system_sandbox_policy, cmd, sandbox_permissions, @@ -699,10 +683,10 @@ fn evaluate_intercepted_exec_policy( ) } -#[derive(Clone, Copy)] +#[derive(Clone)] struct InterceptedExecPolicyContext<'a> { approval_policy: AskForApproval, - sandbox_policy: &'a SandboxPolicy, + permission_profile: PermissionProfile, file_system_sandbox_policy: &'a FileSystemSandboxPolicy, sandbox_permissions: SandboxPermissions, enable_shell_wrapper_parsing: bool, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index b02ad08775..192ae46031 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -66,6 +66,10 @@ fn read_only_file_system_sandbox_policy() -> FileSystemSandboxPolicy { }]) } +fn permission_profile_from_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { + PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) +} + #[test] fn execve_prompt_rejection_keeps_prefix_rules_on_rules_flag() { assert_eq!( @@ -266,12 +270,6 @@ fn shell_request_escalation_execution_is_explicit() { )), ..Default::default() }; - let sandbox_policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap()], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { path: FileSystemPath::Path { @@ -287,13 +285,15 @@ fn shell_request_escalation_execution_is_explicit() { }, ]); let network_sandbox_policy = NetworkSandboxPolicy::Restricted; + let permission_profile = PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + network_sandbox_policy, + ); assert_eq!( CoreShellActionProvider::shell_request_escalation_execution( crate::sandboxing::SandboxPermissions::UseDefault, - &sandbox_policy, - &file_system_sandbox_policy, - network_sandbox_policy, + &permission_profile, /*additional_permissions*/ None, ), EscalationExecution::TurnDefault, @@ -301,9 +301,7 @@ fn shell_request_escalation_execution_is_explicit() { assert_eq!( CoreShellActionProvider::shell_request_escalation_execution( crate::sandboxing::SandboxPermissions::RequireEscalated, - &sandbox_policy, - &file_system_sandbox_policy, - network_sandbox_policy, + &permission_profile, /*additional_permissions*/ None, ), EscalationExecution::Unsandboxed, @@ -311,19 +309,11 @@ fn shell_request_escalation_execution_is_explicit() { assert_eq!( CoreShellActionProvider::shell_request_escalation_execution( crate::sandboxing::SandboxPermissions::WithAdditionalPermissions, - &sandbox_policy, - &file_system_sandbox_policy, - network_sandbox_policy, + &permission_profile, Some(&requested_permissions), ), EscalationExecution::Permissions(EscalationPermissions::ResolvedPermissionProfile( - ResolvedPermissionProfile { - permission_profile: PermissionProfile::from_runtime_permissions( - &file_system_sandbox_policy, - network_sandbox_policy, - ), - sandbox_policy, - }, + ResolvedPermissionProfile { permission_profile }, )), ); } @@ -395,8 +385,6 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul &read_only_file_system_sandbox_policy(), NetworkSandboxPolicy::Restricted, ); - let sandbox_policy = SandboxPolicy::new_read_only_policy(); - let workdir = AbsolutePathBuf::try_from(std::env::current_dir()?)?; let target = std::env::temp_dir().join("execve-hook-short-circuit.txt"); let target_str = target.display().to_string(); @@ -410,9 +398,10 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul call_id: "execve-hook-call".to_string(), tool_name: GuardianCommandSource::Shell, approval_policy: AskForApproval::OnRequest, - sandbox_policy, + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: read_only_file_system_sandbox_policy(), - network_sandbox_policy: NetworkSandboxPolicy::Restricted, sandbox_permissions: SandboxPermissions::RequireEscalated, approval_sandbox_permissions: SandboxPermissions::RequireEscalated, prompt_permissions: None, @@ -476,7 +465,9 @@ fn evaluate_intercepted_exec_policy_uses_wrapper_command_when_shell_wrapper_pars ], InterceptedExecPolicyContext { approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing, @@ -527,7 +518,9 @@ fn evaluate_intercepted_exec_policy_matches_inner_shell_commands_when_enabled() ], InterceptedExecPolicyContext { approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing, @@ -569,7 +562,9 @@ host_executable(name = "git", paths = ["{git_path_literal}"]) &["git".to_string(), "status".to_string()], InterceptedExecPolicyContext { approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, enable_shell_wrapper_parsing: false, @@ -609,7 +604,7 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default( &argv, InterceptedExecPolicyContext { approval_policy, - sandbox_policy: &sandbox_policy, + permission_profile: permission_profile_from_sandbox_policy(&sandbox_policy), file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: super::approval_sandbox_permissions( SandboxPermissions::WithAdditionalPermissions, @@ -624,7 +619,7 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default( &argv, InterceptedExecPolicyContext { approval_policy, - sandbox_policy: &sandbox_policy, + permission_profile: permission_profile_from_sandbox_policy(&sandbox_policy), file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, enable_shell_wrapper_parsing: false, @@ -657,7 +652,9 @@ host_executable(name = "git", paths = ["{allowed_git_literal}"]) &["git".to_string(), "status".to_string()], InterceptedExecPolicyContext { approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::new_read_only_policy(), + permission_profile: permission_profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + ), file_system_sandbox_policy: &read_only_file_system_sandbox_policy(), sandbox_permissions: SandboxPermissions::UseDefault, enable_shell_wrapper_parsing: false, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index bd4452ce1a..b2c83a363f 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -789,7 +789,6 @@ impl UnifiedExecProcessManager { context.turn.tools_config.unified_exec_shell_mode.clone(), ); let file_system_sandbox_policy = context.turn.file_system_sandbox_policy(); - let sandbox_policy = context.turn.sandbox_policy(); let exec_approval_requirement = context .session .services @@ -797,7 +796,7 @@ impl UnifiedExecProcessManager { .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &request.command, approval_policy: context.turn.approval_policy.value(), - sandbox_policy: &sandbox_policy, + permission_profile: context.turn.permission_profile(), file_system_sandbox_policy: &file_system_sandbox_policy, sandbox_permissions: if request.additional_permissions_preapproved { crate::sandboxing::SandboxPermissions::UseDefault diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 6fc5e49b49..73283e3eb6 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -4,7 +4,6 @@ use crate::models::PermissionProfile; use crate::parse_command::ParsedCommand; use crate::protocol::FileChange; use crate::protocol::ReviewDecision; -use crate::protocol::SandboxPolicy; use crate::request_permissions::RequestPermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; @@ -16,13 +15,9 @@ use std::path::PathBuf; use ts_rs::TS; /// Fully resolved permissions for rerunning an intercepted child process. -/// -/// `permission_profile` is the canonical permission model. `sandbox_policy` -/// remains as the legacy adapter for sandbox backends that still require it. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedPermissionProfile { pub permission_profile: PermissionProfile, - pub sandbox_policy: SandboxPolicy, } #[allow(clippy::large_enum_variant)] diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 20efb4b900..fc352865a8 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -10,37 +10,12 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::ReadDenyMatcher; -use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; -use std::collections::HashSet; use std::num::NonZeroUsize; use std::path::Path; use std::path::PathBuf; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EffectiveSandboxPermissions { - pub sandbox_policy: SandboxPolicy, -} - -impl EffectiveSandboxPermissions { - pub fn new( - sandbox_policy: &SandboxPolicy, - additional_permissions: Option<&AdditionalPermissionProfile>, - ) -> Self { - let Some(additional_permissions) = additional_permissions else { - return Self { - sandbox_policy: sandbox_policy.clone(), - }; - }; - - Self { - sandbox_policy: effective_sandbox_policy(sandbox_policy, Some(additional_permissions)), - } - } -} - pub fn normalize_additional_permissions( additional_permissions: AdditionalPermissionProfile, ) -> Result { @@ -446,48 +421,6 @@ fn merge_permission_entries( merged } -fn dedup_absolute_paths(paths: Vec) -> Vec { - let mut out = Vec::with_capacity(paths.len()); - let mut seen = HashSet::new(); - for path in paths { - if seen.insert(path.to_path_buf()) { - out.push(path); - } - } - out -} - -fn additional_permission_roots( - additional_permissions: &AdditionalPermissionProfile, -) -> (Vec, Vec) { - ( - dedup_absolute_paths( - additional_permissions - .file_system - .as_ref() - .map(|file_system| { - file_system - .explicit_path_entries() - .filter_map(|(path, access)| access.can_read().then_some(path.clone())) - .collect() - }) - .unwrap_or_default(), - ), - dedup_absolute_paths( - additional_permissions - .file_system - .as_ref() - .map(|file_system| { - file_system - .explicit_path_entries() - .filter_map(|(path, access)| access.can_write().then_some(path.clone())) - .collect() - }) - .unwrap_or_default(), - ), - ) -} - fn merge_file_system_policy_with_additional_permissions( file_system_policy: &FileSystemSandboxPolicy, additional_permissions: &FileSystemPermissions, @@ -578,73 +511,6 @@ pub fn effective_permission_profile( ) } -fn sandbox_policy_with_additional_permissions( - sandbox_policy: &SandboxPolicy, - additional_permissions: &AdditionalPermissionProfile, -) -> SandboxPolicy { - if additional_permissions.is_empty() { - return sandbox_policy.clone(); - } - - let (_extra_reads, extra_writes) = additional_permission_roots(additional_permissions); - - match sandbox_policy { - SandboxPolicy::DangerFullAccess => SandboxPolicy::DangerFullAccess, - SandboxPolicy::ExternalSandbox { network_access } => SandboxPolicy::ExternalSandbox { - network_access: if merge_network_access( - network_access.is_enabled(), - additional_permissions, - ) { - NetworkAccess::Enabled - } else { - NetworkAccess::Restricted - }, - }, - SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - } => { - let mut merged_writes = writable_roots.clone(); - merged_writes.extend(extra_writes); - SandboxPolicy::WorkspaceWrite { - writable_roots: dedup_absolute_paths(merged_writes), - network_access: merge_network_access(*network_access, additional_permissions), - exclude_tmpdir_env_var: *exclude_tmpdir_env_var, - exclude_slash_tmp: *exclude_slash_tmp, - } - } - SandboxPolicy::ReadOnly { network_access } => { - if extra_writes.is_empty() { - SandboxPolicy::ReadOnly { - network_access: merge_network_access(*network_access, additional_permissions), - } - } else { - // todo(dylan) - for now, this grants more access than the request. We should restrict this, - // but we should add a new SandboxPolicy variant to handle this. While the feature is still - // UnderDevelopment, it's a useful approximation of the desired behavior. - SandboxPolicy::WorkspaceWrite { - writable_roots: dedup_absolute_paths(extra_writes), - network_access: merge_network_access(*network_access, additional_permissions), - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - } - } - } - } -} - -fn effective_sandbox_policy( - sandbox_policy: &SandboxPolicy, - additional_permissions: Option<&AdditionalPermissionProfile>, -) -> SandboxPolicy { - additional_permissions.map_or_else( - || sandbox_policy.clone(), - |permissions| sandbox_policy_with_additional_permissions(sandbox_policy, permissions), - ) -} - pub fn should_require_platform_sandbox( file_system_policy: &FileSystemSandboxPolicy, network_policy: NetworkSandboxPolicy, diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 2894b29bb1..9b41205735 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -2,7 +2,6 @@ use super::effective_file_system_sandbox_policy; use super::intersect_permission_profiles; use super::merge_file_system_policy_with_additional_permissions; use super::normalize_additional_permissions; -use super::sandbox_policy_with_additional_permissions; use super::should_require_platform_sandbox; use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; @@ -13,8 +12,6 @@ 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::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize; use pretty_assertions::assert_eq; @@ -757,66 +754,6 @@ fn intersect_permission_profiles_uses_granted_unbounded_glob_scan_depth() { ); } -#[test] -fn read_only_additional_permissions_can_enable_network_without_writes() { - 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 policy = sandbox_policy_with_additional_permissions( - &SandboxPolicy::ReadOnly { - network_access: false, - }, - &PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions::from_read_write_roots( - Some(vec![path]), - Some(Vec::new()), - )), - }, - ); - - assert_eq!( - policy, - SandboxPolicy::ReadOnly { - network_access: true, - } - ); -} - -#[test] -fn external_sandbox_additional_permissions_can_enable_network() { - 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 policy = sandbox_policy_with_additional_permissions( - &SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Restricted, - }, - &PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions::from_read_write_roots( - Some(vec![path]), - Some(Vec::new()), - )), - }, - ); - - assert_eq!( - policy, - SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Enabled, - } - ); -} - #[test] fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roots() { let temp_dir = TempDir::new().expect("create temp dir"); From 1c3b8fc1d1cf810e0c7972999296380b7d5e02f1 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 24 Apr 2026 20:37:16 -0700 Subject: [PATCH 3/3] permissions: finish profile-backed app surfaces --- codex-rs/analytics/src/reducer.rs | 17 +-- .../app-server/src/codex_message_processor.rs | 62 +++++----- codex-rs/app-server/src/lib.rs | 2 +- .../src/event_processor_with_human_output.rs | 110 +++++++++++------- ...event_processor_with_human_output_tests.rs | 72 ++++++++++++ codex-rs/exec/src/lib.rs | 58 ++------- codex-rs/sandboxing/src/bwrap.rs | 17 +-- codex-rs/sandboxing/src/lib.rs | 2 +- codex-rs/tui/src/app/startup_prompts.rs | 6 +- 9 files changed, 205 insertions(+), 141 deletions(-) diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index 681c25483a..d15002b957 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -62,7 +62,6 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::models::PermissionProfile; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::TokenUsage; @@ -964,12 +963,16 @@ fn sandbox_policy_mode(permission_profile: &PermissionProfile, cwd: &Path) -> &' PermissionProfile::Disabled => "full_access", PermissionProfile::External { .. } => "external_sandbox", PermissionProfile::Managed { .. } => { - match permission_profile.to_legacy_sandbox_policy(cwd) { - Ok(SandboxPolicy::DangerFullAccess) => "full_access", - Ok(SandboxPolicy::ReadOnly { .. }) => "read_only", - Ok(SandboxPolicy::WorkspaceWrite { .. }) => "workspace_write", - Ok(SandboxPolicy::ExternalSandbox { .. }) => "external_sandbox", - Err(_) => "workspace_write", + let file_system_policy = permission_profile.file_system_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + "full_access" + } else if file_system_policy + .get_writable_roots_with_cwd(cwd) + .is_empty() + { + "read_only" + } else { + "workspace_write" } } } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index bd3dfd84c4..44eae02db1 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -349,7 +349,6 @@ use codex_rmcp_client::perform_oauth_login_return_url; use codex_rollout::state_db::StateDbHandle; use codex_rollout::state_db::get_state_db; use codex_rollout::state_db::reconcile_rollout; -use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile; use codex_state::StateRuntime; use codex_state::ThreadMetadata; use codex_state::ThreadMetadataBuilder; @@ -2630,16 +2629,14 @@ impl CodexMessageProcessor { // should still be considered "trusted" in this case. let requested_permissions_trust_project = requested_permissions_trust_project(&typesafe_overrides, config.cwd.as_path()); + let effective_permissions_trust_project = permission_profile_trusts_project( + &config.permissions.permission_profile(), + config.cwd.as_path(), + ); if requested_cwd.is_some() && config.active_project.trust_level.is_none() - && (requested_permissions_trust_project - || matches!( - config.permissions.sandbox_policy.get(), - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } - | codex_protocol::protocol::SandboxPolicy::DangerFullAccess - | codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } - )) + && (requested_permissions_trust_project || effective_permissions_trust_project) { let trust_target = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &config.cwd) .await @@ -10013,22 +10010,20 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) overrides .permission_profile .as_ref() - .is_some_and(|profile| { - let (file_system_sandbox_policy, network_sandbox_policy) = - profile.to_runtime_permissions(); - let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - profile, - &file_system_sandbox_policy, - network_sandbox_policy, - cwd, - ); - matches!( - sandbox_policy, - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } - | codex_protocol::protocol::SandboxPolicy::DangerFullAccess - | codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } - ) - }) + .is_some_and(|profile| permission_profile_trusts_project(profile, cwd)) +} + +fn permission_profile_trusts_project( + profile: &codex_protocol::models::PermissionProfile, + cwd: &Path, +) -> bool { + match profile { + codex_protocol::models::PermissionProfile::Disabled + | codex_protocol::models::PermissionProfile::External { .. } => true, + codex_protocol::models::PermissionProfile::Managed { .. } => profile + .file_system_sandbox_policy() + .can_write_path_with_cwd(cwd, cwd), + } } fn parse_datetime(timestamp: Option<&str>) -> Option> { @@ -10325,6 +10320,7 @@ mod tests { use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; @@ -10550,17 +10546,21 @@ mod tests { let full_access_profile = codex_protocol::models::PermissionProfile::Disabled; let workspace_write_profile = codex_protocol::models::PermissionProfile::workspace_write(); let read_only_profile = codex_protocol::models::PermissionProfile::read_only(); - let direct_write_profile = + let split_write_profile = codex_protocol::models::PermissionProfile::from_runtime_permissions( - &codex_protocol::permissions::FileSystemSandboxPolicy::restricted(vec![ + &FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { - path: FileSystemPath::Path { - path: test_path_buf("/tmp/other").abs(), - }, + path: FileSystemPath::Path { path: cwd.clone() }, access: FileSystemAccessMode::Write, }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: "/tmp/project/**/*.env".to_string(), + }, + access: FileSystemAccessMode::None, + }, ]), - codex_protocol::permissions::NetworkSandboxPolicy::Restricted, + NetworkSandboxPolicy::Restricted, ); assert!(requested_permissions_trust_project( @@ -10579,7 +10579,7 @@ mod tests { )); assert!(requested_permissions_trust_project( &ConfigOverrides { - permission_profile: Some(direct_write_profile), + permission_profile: Some(split_write_profile), ..Default::default() }, cwd.as_path() diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index d9b403165c..bdbb363d55 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -475,7 +475,7 @@ pub async fn run_main_with_transport( }); } if let Some(warning) = - codex_core::config::system_bwrap_warning(config.permissions.sandbox_policy.get()) + codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get()) { config_warnings.push(ConfigWarningNotification { summary: warning, diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 4dab204493..64f47ef5ef 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -1,4 +1,5 @@ use std::io::IsTerminal; +use std::path::Path; use std::path::PathBuf; use codex_app_server_protocol::CommandExecutionStatus; @@ -10,9 +11,11 @@ use codex_app_server_protocol::ThreadTokenUsage; use codex_app_server_protocol::TurnStatus; use codex_core::config::Config; use codex_model_provider_info::WireApi; +use codex_protocol::models::PermissionProfile; use codex_protocol::num_format::format_with_separators; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SessionConfiguredEvent; +use codex_utils_absolute_path::canonicalize_preserving_symlinks; use owo_colors::OwoColorize; use owo_colors::Style; @@ -433,7 +436,10 @@ fn config_summary_entries( ), ( "sandbox", - summarize_sandbox_policy(config.permissions.sandbox_policy.get()), + summarize_permission_profile( + config.permissions.permission_profile.get(), + config.cwd.as_path(), + ), ), ]; if config.model_provider.wire_api == WireApi::Responses { @@ -459,54 +465,78 @@ fn config_summary_entries( entries } -fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { - match sandbox_policy { - SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(), - SandboxPolicy::ReadOnly { network_access, .. } => { - let mut summary = "read-only".to_string(); - if *network_access { - summary.push_str(" (network access enabled)"); - } - summary - } - SandboxPolicy::ExternalSandbox { network_access } => { +fn summarize_permission_profile(permission_profile: &PermissionProfile, cwd: &Path) -> String { + match permission_profile { + PermissionProfile::Disabled => "danger-full-access".to_string(), + PermissionProfile::External { network } => { let mut summary = "external-sandbox".to_string(); - if matches!( - network_access, - codex_protocol::protocol::NetworkAccess::Enabled - ) { - summary.push_str(" (network access enabled)"); - } + append_network_summary(&mut summary, *network); summary } - SandboxPolicy::WorkspaceWrite { - writable_roots, - network_access, - exclude_tmpdir_env_var, - exclude_slash_tmp, - } => { + PermissionProfile::Managed { .. } => { + let file_system_policy = permission_profile.file_system_sandbox_policy(); + let network_policy = permission_profile.network_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + let mut summary = "workspace-write [/]".to_string(); + append_network_summary(&mut summary, network_policy); + return summary; + } + + let writable_roots = file_system_policy.get_writable_roots_with_cwd(cwd); + if writable_roots.is_empty() { + let mut summary = "read-only".to_string(); + append_network_summary(&mut summary, network_policy); + return summary; + } + let mut summary = "workspace-write".to_string(); - let mut writable_entries = vec!["workdir".to_string()]; - if !*exclude_slash_tmp { - writable_entries.push("/tmp".to_string()); - } - if !*exclude_tmpdir_env_var { - writable_entries.push("$TMPDIR".to_string()); - } - writable_entries.extend( - writable_roots - .iter() - .map(|path| path.to_string_lossy().to_string()), - ); + let writable_entries = writable_roots + .iter() + .map(|root| writable_root_label(root.root.as_path(), cwd)) + .collect::>(); summary.push_str(&format!(" [{}]", writable_entries.join(", "))); - if *network_access { - summary.push_str(" (network access enabled)"); - } + append_network_summary(&mut summary, network_policy); summary } } } +fn append_network_summary(summary: &mut String, network_policy: NetworkSandboxPolicy) { + if network_policy.is_enabled() { + summary.push_str(" (network access enabled)"); + } +} + +fn writable_root_label(root: &Path, cwd: &Path) -> String { + if paths_match_after_canonicalization(root, cwd) { + return "workdir".to_string(); + } + if paths_match_after_canonicalization(root, Path::new("/tmp")) { + return "/tmp".to_string(); + } + if std::env::var_os("TMPDIR") + .filter(|tmpdir| !tmpdir.is_empty()) + .is_some_and(|tmpdir| paths_match_after_canonicalization(root, Path::new(&tmpdir))) + { + return "$TMPDIR".to_string(); + } + display_path_label(root) +} + +fn paths_match_after_canonicalization(left: &Path, right: &Path) -> bool { + canonicalize_preserving_symlinks(left).ok() == canonicalize_preserving_symlinks(right).ok() + || display_path_label(left) == display_path_label(right) +} + +fn display_path_label(path: &Path) -> String { + path.strip_prefix("/private/tmp") + .ok() + .map(|suffix| Path::new("/tmp").join(suffix)) + .unwrap_or_else(|| path.to_path_buf()) + .to_string_lossy() + .to_string() +} + fn reasoning_text( summary: &[String], content: &[String], diff --git a/codex-rs/exec/src/event_processor_with_human_output_tests.rs b/codex-rs/exec/src/event_processor_with_human_output_tests.rs index 232be7f02c..5db731afd6 100644 --- a/codex-rs/exec/src/event_processor_with_human_output_tests.rs +++ b/codex-rs/exec/src/event_processor_with_human_output_tests.rs @@ -2,6 +2,14 @@ use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnStatus; +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::NetworkSandboxPolicy; +use codex_utils_absolute_path::test_support::PathBufExt; +use codex_utils_absolute_path::test_support::test_path_buf; use owo_colors::Style; use pretty_assertions::assert_eq; @@ -10,6 +18,7 @@ use super::final_message_from_turn_items; use super::reasoning_text; use super::should_print_final_message_to_stdout; use super::should_print_final_message_to_tty; +use super::summarize_permission_profile; use crate::event_processor::EventProcessor; #[test] @@ -89,6 +98,69 @@ fn reasoning_text_uses_raw_content_when_enabled() { assert_eq!(text.as_deref(), Some("raw")); } +#[test] +fn summarizes_disabled_permission_profile_as_danger_full_access() { + assert_eq!( + summarize_permission_profile( + &PermissionProfile::Disabled, + test_path_buf("/tmp").as_path() + ), + "danger-full-access" + ); +} + +#[test] +fn summarizes_external_permission_profile() { + assert_eq!( + summarize_permission_profile( + &PermissionProfile::External { + network: NetworkSandboxPolicy::Enabled, + }, + test_path_buf("/tmp").as_path(), + ), + "external-sandbox (network access enabled)" + ); +} + +#[test] +fn summarizes_managed_workspace_write_permission_profile() { + let cwd = test_path_buf("/tmp/project").abs(); + let cache_root = test_path_buf("/tmp/cache").abs(); + let profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Path { path: cwd.clone() }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: cache_root.clone(), + }, + access: FileSystemAccessMode::Write, + }, + ]), + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + summarize_permission_profile(&profile, cwd.as_path()), + format!("workspace-write [workdir, {}]", cache_root.display()) + ); +} + +#[test] +fn summarizes_managed_read_only_permission_profile() { + let profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(Vec::new()), + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + summarize_permission_profile(&profile, test_path_buf("/tmp/project").as_path()), + "read-only" + ); +} + #[test] fn final_message_from_turn_items_uses_latest_agent_message() { let message = final_message_from_turn_items(&[ diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index c96e06279b..020867fff3 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -575,7 +575,6 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { let default_cwd = config.cwd.to_path_buf(); let default_approval_policy = config.permissions.approval_policy.value(); - let default_sandbox_policy = config.permissions.sandbox_policy.get(); let default_effort = config.model_reasoning_effort; let (initial_operation, prompt_summary) = match (command.as_ref(), prompt, images) { @@ -717,7 +716,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { event_processor.print_config_summary(&config, &prompt_summary, &session_configured); if !json_mode && let Some(message) = - codex_core::config::system_bwrap_warning(config.permissions.sandbox_policy.get()) + codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get()) { event_processor.process_warning(message); } @@ -737,10 +736,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { items, output_schema, } => { - let permission_profile = permission_profile_override_from_config(&config); - let sandbox_policy = permission_profile - .is_none() - .then(|| default_sandbox_policy.clone().into()); + let permission_profile = Some(config.permissions.permission_profile().into()); let response: TurnStartResponse = send_request_with_response( &client, ClientRequest::TurnStart { @@ -753,7 +749,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { cwd: Some(default_cwd), approval_policy: Some(default_approval_policy.into()), approvals_reviewer: None, - sandbox_policy, + sandbox_policy: None, permission_profile, model: None, service_tier: None, @@ -910,37 +906,15 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { Ok(()) } -fn sandbox_mode_from_policy( - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, -) -> Option { - match sandbox_policy { - codex_protocol::protocol::SandboxPolicy::DangerFullAccess => { - Some(codex_app_server_protocol::SandboxMode::DangerFullAccess) - } - codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } => { - Some(codex_app_server_protocol::SandboxMode::ReadOnly) - } - codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } => { - Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite) - } - codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } => None, - } -} - fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { - let permission_profile = permission_profile_override_from_config(config); - let sandbox = permission_profile - .is_none() - .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get())) - .flatten(); ThreadStartParams { model: config.model.clone(), model_provider: Some(config.model_provider_id.clone()), cwd: Some(config.cwd.to_string_lossy().to_string()), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox, - permission_profile, + sandbox: None, + permission_profile: Some(config.permissions.permission_profile().into()), config: config_request_overrides_from_config(config), ephemeral: Some(config.ephemeral), ..ThreadStartParams::default() @@ -948,11 +922,6 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams { } fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams { - let permission_profile = permission_profile_override_from_config(config); - let sandbox = permission_profile - .is_none() - .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get())) - .flatten(); ThreadResumeParams { thread_id, model: config.model.clone(), @@ -960,26 +929,13 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa cwd: Some(config.cwd.to_string_lossy().to_string()), approval_policy: Some(config.permissions.approval_policy.value().into()), approvals_reviewer: approvals_reviewer_override_from_config(config), - sandbox, - permission_profile, + sandbox: None, + permission_profile: Some(config.permissions.permission_profile().into()), config: config_request_overrides_from_config(config), ..ThreadResumeParams::default() } } -fn permission_profile_override_from_config( - config: &Config, -) -> Option { - if matches!( - config.permissions.sandbox_policy.get(), - SandboxPolicy::ExternalSandbox { .. } - ) { - None - } else { - Some(config.permissions.permission_profile().into()) - } -} - fn config_request_overrides_from_config(config: &Config) -> Option> { config .active_profile diff --git a/codex-rs/sandboxing/src/bwrap.rs b/codex-rs/sandboxing/src/bwrap.rs index 069807f986..3435c6d193 100644 --- a/codex-rs/sandboxing/src/bwrap.rs +++ b/codex-rs/sandboxing/src/bwrap.rs @@ -1,4 +1,5 @@ -use codex_protocol::protocol::SandboxPolicy; +use crate::policy_transforms::should_require_platform_sandbox; +use codex_protocol::models::PermissionProfile; use std::path::Path; use std::path::PathBuf; use std::process::Command; @@ -26,8 +27,8 @@ const USER_NAMESPACE_FAILURES: [&str; 4] = [ "No permissions to create a new namespace", ]; -pub fn system_bwrap_warning(sandbox_policy: &SandboxPolicy) -> Option { - if !should_warn_about_system_bwrap(sandbox_policy) { +pub fn system_bwrap_warning(permission_profile: &PermissionProfile) -> Option { + if !should_warn_about_system_bwrap(permission_profile) { return None; } @@ -35,10 +36,12 @@ pub fn system_bwrap_warning(sandbox_policy: &SandboxPolicy) -> Option { system_bwrap_warning_for_path(system_bwrap_path.as_deref()) } -fn should_warn_about_system_bwrap(sandbox_policy: &SandboxPolicy) -> bool { - !matches!( - sandbox_policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } +fn should_warn_about_system_bwrap(permission_profile: &PermissionProfile) -> bool { + let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions(); + should_require_platform_sandbox( + &file_system_policy, + network_policy, + /*has_managed_network_requirements*/ false, ) } diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index f4263fdfd4..c70393db8a 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -24,7 +24,7 @@ use codex_protocol::error::CodexErr; #[cfg(not(target_os = "linux"))] pub fn system_bwrap_warning( - _sandbox_policy: &codex_protocol::protocol::SandboxPolicy, + _permission_profile: &codex_protocol::models::PermissionProfile, ) -> Option { None } diff --git a/codex-rs/tui/src/app/startup_prompts.rs b/codex-rs/tui/src/app/startup_prompts.rs index 284f94fbcb..41972e6751 100644 --- a/codex-rs/tui/src/app/startup_prompts.rs +++ b/codex-rs/tui/src/app/startup_prompts.rs @@ -66,9 +66,9 @@ pub(super) fn emit_project_config_warnings(app_event_tx: &AppEventSender, config } pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config) { - let Some(message) = - crate::legacy_core::config::system_bwrap_warning(config.permissions.sandbox_policy.get()) - else { + let Some(message) = crate::legacy_core::config::system_bwrap_warning( + config.permissions.permission_profile.get(), + ) else { return; };