From b80c55b5e02ede263ec8c6bd53ade26a13aba5e5 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 26 Apr 2026 18:53:02 -0700 Subject: [PATCH 1/4] permissions: centralize legacy sandbox projection --- .../app-server/src/codex_message_processor.rs | 10 ++++- codex-rs/cli/src/debug_sandbox.rs | 9 ++++- codex-rs/core/src/config/mod.rs | 34 ++++++++++++++++- codex-rs/core/src/session/session.rs | 4 +- codex-rs/tui/src/app.rs | 8 +++- codex-rs/tui/src/app/config_persistence.rs | 15 +++++--- codex-rs/tui/src/app/event_dispatch.rs | 17 +++++++-- codex-rs/tui/src/app/thread_session_state.rs | 16 ++++++-- codex-rs/tui/src/app_server_session.rs | 38 +++++++++++++++---- codex-rs/tui/src/chatwidget.rs | 33 ++++++++++++---- .../tui/src/chatwidget/tests/permissions.rs | 34 ++++++----------- codex-rs/tui/src/history_cell.rs | 4 +- codex-rs/tui/src/lib.rs | 15 ++++++-- codex-rs/tui/src/status/card.rs | 16 +++++--- codex-rs/tui/src/status/tests.rs | 37 +++++++++--------- .../sandbox-summary/src/config_summary.rs | 6 ++- 16 files changed, 210 insertions(+), 86 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 0216a2a520..c86d414359 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2307,7 +2307,11 @@ impl CodexMessageProcessor { } } } else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) { - match self.config.permissions.sandbox_policy.can_set(&policy) { + match self + .config + .permissions + .can_set_legacy_sandbox_policy(&policy, &sandbox_cwd) + { Ok(()) => { let file_system_sandbox_policy = codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd); @@ -8705,7 +8709,9 @@ impl CodexMessageProcessor { Ok(config) => { let setup_request = WindowsSandboxSetupRequest { mode, - policy: config.permissions.sandbox_policy.get().clone(), + policy: config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), policy_cwd: config.cwd.to_path_buf(), command_cwd, env_map: std::env::vars().collect(), diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index a6cd07699e..c85da0f5f2 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -227,7 +227,9 @@ async fn run_command_under_sandbox( let args = create_linux_sandbox_command_args_for_policies( command, cwd.as_path(), - config.permissions.sandbox_policy.get(), + &config + .permissions + .legacy_sandbox_policy(sandbox_policy_cwd.as_path()), &file_system_sandbox_policy, network_sandbox_policy, sandbox_policy_cwd.as_path(), @@ -290,7 +292,10 @@ async fn run_command_under_windows_session( use codex_windows_sandbox::spawn_windows_sandbox_session_elevated; use codex_windows_sandbox::spawn_windows_sandbox_session_legacy; - let policy_str = match serde_json::to_string(config.permissions.sandbox_policy.get()) { + let sandbox_policy = config + .permissions + .legacy_sandbox_policy(sandbox_policy_cwd.as_path()); + let policy_str = match serde_json::to_string(&sandbox_policy) { Ok(policy_str) => policy_str, Err(err) => { eprintln!("windows sandbox failed to serialize policy: {err}"); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 741472959e..14eecbb842 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -237,6 +237,37 @@ impl Permissions { self.permission_profile.get().network_sandbox_policy() } + /// Legacy compatibility projection derived from the canonical profile. + pub fn legacy_sandbox_policy(&self, cwd: &Path) -> SandboxPolicy { + let permission_profile = self.permission_profile.get(); + let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy(); + compatibility_sandbox_policy_for_permission_profile( + permission_profile, + &file_system_sandbox_policy, + permission_profile.network_sandbox_policy(), + cwd, + ) + } + + /// Check whether a legacy sandbox policy can be applied to this permission + /// set under both legacy and canonical profile constraints. + pub fn can_set_legacy_sandbox_policy( + &self, + sandbox_policy: &SandboxPolicy, + cwd: &Path, + ) -> ConstraintResult<()> { + self.sandbox_policy.can_set(sandbox_policy)?; + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd); + let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy); + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ); + self.permission_profile.can_set(&permission_profile) + } + /// Replace permissions from a legacy sandbox policy and keep every /// permission projection in sync. pub fn set_legacy_sandbox_policy( @@ -244,7 +275,7 @@ impl Permissions { sandbox_policy: SandboxPolicy, cwd: &Path, ) -> ConstraintResult<()> { - self.sandbox_policy.can_set(&sandbox_policy)?; + self.can_set_legacy_sandbox_policy(&sandbox_policy, cwd)?; let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd); let network_sandbox_policy = NetworkSandboxPolicy::from(&sandbox_policy); @@ -253,7 +284,6 @@ impl Permissions { &file_system_sandbox_policy, network_sandbox_policy, ); - self.permission_profile.can_set(&permission_profile)?; self.sandbox_policy.set(sandbox_policy)?; self.permission_profile.set(permission_profile)?; diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index bf2e36a277..dcadac70a8 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -634,7 +634,9 @@ impl Session { config.model_context_window, config.model_auto_compact_token_limit, config.permissions.approval_policy.value(), - config.permissions.sandbox_policy.get().clone(), + config + .permissions + .legacy_sandbox_policy(session_configuration.cwd.as_path()), mcp_servers.keys().map(String::as_str).collect(), config.active_profile.clone(), ); diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 77c1f52775..bffa46e478 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -939,10 +939,14 @@ impl App { // On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows. #[cfg(target_os = "windows")] { + let startup_sandbox_policy = app + .config + .permissions + .legacy_sandbox_policy(app.config.cwd.as_path()); let should_check = WindowsSandboxLevel::from_config(&app.config) != WindowsSandboxLevel::Disabled && matches!( - app.config.permissions.sandbox_policy.get(), + &startup_sandbox_policy, codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } | codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } ) @@ -956,7 +960,7 @@ impl App { let env_map: std::collections::HashMap = std::env::vars().collect(); let tx = app.app_event_tx.clone(); let logs_base_dir = app.config.codex_home.clone(); - let sandbox_policy = app.config.permissions.sandbox_policy.get().clone(); + let sandbox_policy = startup_sandbox_policy; Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, sandbox_policy, tx); } } diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 44ef5f664d..9f6b631226 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -300,9 +300,11 @@ impl App { .set_approval_policy(self.config.permissions.approval_policy.value()); } if sandbox_policy_override.is_some() - && let Err(err) = self - .chat_widget - .set_sandbox_policy(self.config.permissions.sandbox_policy.get().clone()) + && let Err(err) = self.chat_widget.set_sandbox_policy( + self.config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()), + ) { tracing::error!( error = %err, @@ -312,8 +314,11 @@ impl App { .add_error_message(format!("Failed to enable Auto-review: {err}")); } if sandbox_policy_override.is_some() { - self.runtime_sandbox_policy_override = - Some(self.config.permissions.sandbox_policy.get().clone()); + self.runtime_sandbox_policy_override = Some( + self.config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()), + ); } if approval_policy_override.is_some() diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 7e096c6b92..4c3b038424 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -834,7 +834,10 @@ impl App { /*hint*/ None, )); - let policy = self.config.permissions.sandbox_policy.get().clone(); + let policy = self + .config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()); let policy_cwd = self.config.cwd.clone(); let command_cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = @@ -1245,8 +1248,11 @@ impl App { .add_error_message(format!("Failed to set sandbox policy: {err}")); return Ok(AppRunControl::Continue); } - self.runtime_sandbox_policy_override = - Some(self.config.permissions.sandbox_policy.get().clone()); + self.runtime_sandbox_policy_override = Some( + self.config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()), + ); self.sync_active_thread_permission_settings_to_cached_session() .await; @@ -1269,7 +1275,10 @@ impl App { std::env::vars().collect(); let tx = self.app_event_tx.clone(); let logs_base_dir = self.config.codex_home.clone(); - let sandbox_policy = self.config.permissions.sandbox_policy.get().clone(); + let sandbox_policy = self + .config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()); Self::spawn_world_writable_scan( cwd, env_map, diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 3743073449..2b242890d3 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -12,7 +12,10 @@ impl App { let approval_policy = self.config.permissions.approval_policy.value(); let approvals_reviewer = self.config.approvals_reviewer; - let sandbox_policy = self.config.permissions.sandbox_policy.get().clone(); + let sandbox_policy = self + .config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()); let permission_profile = Some( self.chat_widget .config_ref() @@ -45,7 +48,10 @@ impl App { thread_id: ThreadId, thread: &Thread, ) -> ThreadSessionState { - let sandbox_policy = self.config.permissions.sandbox_policy.get().clone(); + let sandbox_policy = self + .config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()); let mut session = self .primary_session_configured .clone() @@ -185,8 +191,10 @@ mod tests { app.chat_widget .set_sandbox_policy(expected_sandbox_policy.clone()) .expect("set widget sandbox policy"); - app.config.permissions.sandbox_policy = - codex_config::Constrained::allow_any(expected_sandbox_policy.clone()); + app.config + .permissions + .set_legacy_sandbox_policy(expected_sandbox_policy.clone(), app.config.cwd.as_path()) + .expect("set app sandbox policy"); app.sync_active_thread_permission_settings_to_cached_session() .await; diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 8fa7630212..366e8912ae 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -1143,7 +1143,13 @@ fn thread_start_params_from_config( let permission_profile = permission_profile_override_from_config(config, thread_params_mode); let sandbox = permission_profile .is_none() - .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .then(|| { + sandbox_mode_from_policy( + config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ) + }) .flatten(); ThreadStartParams { model: config.model.clone(), @@ -1170,7 +1176,13 @@ fn thread_resume_params_from_config( let permission_profile = permission_profile_override_from_config(&config, thread_params_mode); let sandbox = permission_profile .is_none() - .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .then(|| { + sandbox_mode_from_policy( + config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ) + }) .flatten(); ThreadResumeParams { thread_id: thread_id.to_string(), @@ -1196,7 +1208,13 @@ fn thread_fork_params_from_config( let permission_profile = permission_profile_override_from_config(&config, thread_params_mode); let sandbox = permission_profile .is_none() - .then(|| sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone())) + .then(|| { + sandbox_mode_from_policy( + config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ) + }) .flatten(); ThreadForkParams { thread_id: thread_id.to_string(), @@ -1522,8 +1540,11 @@ mod tests { let temp_dir = tempfile::tempdir().expect("tempdir"); let config = build_config(&temp_dir).await; let thread_id = ThreadId::new(); - let expected_sandbox = - sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()); + let expected_sandbox = sandbox_mode_from_policy( + config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ); let start = thread_start_params_from_config( &config, @@ -1564,8 +1585,11 @@ mod tests { let config = build_config(&temp_dir).await; let thread_id = ThreadId::new(); let remote_cwd = PathBuf::from("repo/on/server"); - let expected_sandbox = - sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()); + let expected_sandbox = sandbox_mode_from_policy( + config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ); let start = thread_start_params_from_config( &config, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1fbe122b6e..dd48f030cb 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6415,7 +6415,9 @@ impl ChatWidget { items, self.config.cwd.to_path_buf(), self.config.permissions.approval_policy.value(), - self.config.permissions.sandbox_policy.get().clone(), + self.config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()), permission_profile, effective_mode.model().to_string(), effective_mode.reasoning_effort(), @@ -9466,7 +9468,10 @@ impl ChatWidget { pub(crate) fn open_permissions_popup(&mut self) { let include_read_only = cfg!(target_os = "windows"); let current_approval = self.config.permissions.approval_policy.value(); - let current_sandbox = self.config.permissions.sandbox_policy.get(); + let current_sandbox = self + .config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()); let guardian_approval_enabled = self.config.features.enabled(Feature::GuardianApproval); let current_review_policy = self.config.approvals_reviewer; let mut items: Vec = Vec::new(); @@ -9600,7 +9605,11 @@ impl ChatWidget { name: base_name.clone(), description: base_description.clone(), is_current: current_review_policy == ApprovalsReviewer::User - && Self::preset_matches_current(current_approval, current_sandbox, &preset), + && Self::preset_matches_current( + current_approval, + ¤t_sandbox, + &preset, + ), actions: default_actions, dismiss_on_select: true, disabled_reason: default_disabled_reason, @@ -9617,7 +9626,7 @@ impl ChatWidget { is_current: current_review_policy == ApprovalsReviewer::AutoReview && Self::preset_matches_current( current_approval, - current_sandbox, + ¤t_sandbox, &preset, ), actions: Self::approval_preset_actions( @@ -9638,7 +9647,7 @@ impl ChatWidget { description: base_description, is_current: Self::preset_matches_current( current_approval, - current_sandbox, + ¤t_sandbox, &preset, ), actions: default_actions, @@ -9774,7 +9783,10 @@ impl ChatWidget { self.config.codex_home.as_path(), cwd.as_path(), &env_map, - self.config.permissions.sandbox_policy.get(), + &self + .config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()), Some(self.config.codex_home.as_path()), ) { Ok(_) => None, @@ -9892,7 +9904,14 @@ impl ChatWidget { let mode_label = preset .as_ref() .map(|p| describe_policy(&p.sandbox)) - .unwrap_or_else(|| describe_policy(self.config.permissions.sandbox_policy.get())); + .unwrap_or_else(|| { + describe_policy( + &self + .config + .permissions + .legacy_sandbox_policy(self.config.cwd.as_path()), + ) + }); let info_line = if failed_scan { Line::from(vec![ "We couldn't complete the world-writable scan, so protections cannot be verified. " diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 73263c6871..ccab18bfcb 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -1,6 +1,13 @@ use super::*; use pretty_assertions::assert_eq; +fn set_legacy_sandbox_policy(chat: &mut ChatWidget, sandbox_policy: SandboxPolicy) { + chat.config + .permissions + .set_legacy_sandbox_policy(sandbox_policy, chat.config.cwd.as_path()) + .expect("set sandbox policy"); +} + #[tokio::test] async fn approvals_selection_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -347,8 +354,7 @@ async fn permissions_selection_history_snapshot_full_access_to_default() { .approval_policy .set(AskForApproval::Never) .expect("set approval policy"); - chat.config.permissions.sandbox_policy = - Constrained::allow_any(SandboxPolicy::DangerFullAccess); + set_legacy_sandbox_policy(&mut chat, SandboxPolicy::DangerFullAccess); chat.open_permissions_popup(); let popup = render_bottom_popup(&chat, /*width*/ 120); @@ -387,11 +393,7 @@ async fn permissions_selection_emits_history_cell_when_current_is_selected() { .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - chat.config - .permissions - .sandbox_policy - .set(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); chat.open_permissions_popup(); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); @@ -446,11 +448,7 @@ async fn permissions_selection_hides_auto_review_when_feature_disabled_even_if_a .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - chat.config - .permissions - .sandbox_policy - .set(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); chat.open_permissions_popup(); let popup = render_bottom_popup(&chat, /*width*/ 120); @@ -575,11 +573,7 @@ async fn permissions_selection_can_disable_auto_review() { .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - chat.config - .permissions - .sandbox_policy - .set(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); chat.open_permissions_popup(); chat.handle_key_event(KeyEvent::from(KeyCode::Up)); @@ -616,11 +610,7 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - chat.config - .permissions - .sandbox_policy - .set(SandboxPolicy::new_workspace_write_policy()) - .expect("set sandbox policy"); + set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); chat.set_approvals_reviewer(ApprovalsReviewer::User); chat.open_permissions_popup(); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 16c2440de4..33e2f25c63 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1313,7 +1313,9 @@ pub(crate) fn new_session_info( pub(crate) fn is_yolo_mode(config: &Config) -> bool { has_yolo_permissions( config.permissions.approval_policy.value(), - config.permissions.sandbox_policy.get(), + &config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), ) } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 7f65e3b049..ef4dd6276c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -874,9 +874,12 @@ pub async fn run_main( set_default_client_residency_requirement(config.enforce_residency.value()); - if let Some(warning) = - add_dir_warning_message(&cli.add_dir, config.permissions.sandbox_policy.get()) - { + if let Some(warning) = add_dir_warning_message( + &cli.add_dir, + &config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ) { #[allow(clippy::print_stderr)] { eprintln!("Error adding directories: {warning}"); @@ -2205,7 +2208,9 @@ mod tests { current_date: None, timezone: None, approval_policy: config.permissions.approval_policy.value(), - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), permission_profile: None, network: None, file_system_sandbox_policy: None, @@ -2328,6 +2333,7 @@ trust_level = "untrusted" ..Default::default() }; let trusted_config = ConfigBuilder::default() + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) .codex_home(codex_home.clone()) .harness_overrides(trusted_overrides.clone()) .build() @@ -2342,6 +2348,7 @@ trust_level = "untrusted" ..trusted_overrides }; let untrusted_config = ConfigBuilder::default() + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) .codex_home(codex_home) .harness_overrides(untrusted_overrides) .build() diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 2a05bb888d..06594587b6 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -254,7 +254,11 @@ impl StatusHistoryCell { ), ( "sandbox", - summarize_sandbox_policy(config.permissions.sandbox_policy.get()), + summarize_sandbox_policy( + &config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ), ), ]; if config.model_provider.wire_api == WireApi::Responses { @@ -277,7 +281,10 @@ impl StatusHistoryCell { .find(|(k, _)| *k == "approval") .map(|(_, v)| v.clone()) .unwrap_or_else(|| "".to_string()); - let sandbox = match config.permissions.sandbox_policy.get() { + let sandbox_policy = config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()); + let sandbox = match &sandbox_policy { SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(), SandboxPolicy::ReadOnly { .. } => "read-only".to_string(), SandboxPolicy::WorkspaceWrite { @@ -294,12 +301,11 @@ impl StatusHistoryCell { } }; let permissions = if config.permissions.approval_policy.value() == AskForApproval::OnRequest - && *config.permissions.sandbox_policy.get() - == SandboxPolicy::new_workspace_write_policy() + && sandbox_policy == SandboxPolicy::new_workspace_write_policy() { "Default".to_string() } else if config.permissions.approval_policy.value() == AskForApproval::Never - && *config.permissions.sandbox_policy.get() == SandboxPolicy::DangerFullAccess + && sandbox_policy == SandboxPolicy::DangerFullAccess { "Full Access".to_string() } else { diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 44611deee3..569f093a11 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -97,19 +97,20 @@ async fn status_snapshot_includes_reasoning_details() { config.model = Some("gpt-5.1-codex-max".to_string()); config.model_provider_id = "openai".to_string(); config.model_reasoning_summary = Some(ReasoningSummary::Detailed); + config.cwd = test_path_buf("/workspace/tests").abs(); config .permissions - .sandbox_policy - .set(SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) + .set_legacy_sandbox_policy( + SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + config.cwd.as_path(), + ) .expect("set sandbox policy"); - config.cwd = test_path_buf("/workspace/tests").abs(); - let account_display = test_status_account_display(); let usage = TokenUsage { input_tokens: 1_200, @@ -182,17 +183,19 @@ async fn status_permissions_non_default_workspace_write_is_custom() { .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); + config.cwd = test_path_buf("/workspace/tests").abs(); config .permissions - .sandbox_policy - .set(SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) + .set_legacy_sandbox_policy( + SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + config.cwd.as_path(), + ) .expect("set sandbox policy"); - config.cwd = test_path_buf("/workspace/tests").abs(); let account_display = test_status_account_display(); let usage = TokenUsage::default(); diff --git a/codex-rs/utils/sandbox-summary/src/config_summary.rs b/codex-rs/utils/sandbox-summary/src/config_summary.rs index 47f4ca770b..b3de5b6382 100644 --- a/codex-rs/utils/sandbox-summary/src/config_summary.rs +++ b/codex-rs/utils/sandbox-summary/src/config_summary.rs @@ -15,7 +15,11 @@ pub fn create_config_summary_entries(config: &Config, model: &str) -> Vec<(&'sta ), ( "sandbox", - summarize_sandbox_policy(config.permissions.sandbox_policy.get()), + summarize_sandbox_policy( + &config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + ), ), ]; if config.model_provider.wire_api == WireApi::Responses { From be50c0360a621bcf395a91f825775d05eac874f7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 26 Apr 2026 19:27:31 -0700 Subject: [PATCH 2/4] permissions: store only constrained permission profiles --- codex-rs/core/src/agent/role_tests.rs | 2 +- .../core/src/config/config_loader_tests.rs | 2 +- codex-rs/core/src/config/config_tests.rs | 59 ++++++++----------- codex-rs/core/src/config/mod.rs | 36 +++++------ codex-rs/core/src/guardian/review_session.rs | 1 - codex-rs/core/src/guardian/tests.rs | 6 +- codex-rs/core/src/memories/tests.rs | 7 ++- codex-rs/core/src/session/tests.rs | 6 +- codex-rs/core/src/session/turn_context.rs | 2 - .../src/tools/handlers/multi_agents_common.rs | 2 +- .../src/tools/handlers/multi_agents_tests.rs | 33 ++++++++--- codex-rs/core/tests/common/zsh_fork.rs | 4 +- codex-rs/core/tests/suite/agent_websocket.rs | 28 +++------ codex-rs/core/tests/suite/approvals.rs | 32 +++++++--- codex-rs/core/tests/suite/client.rs | 4 +- codex-rs/core/tests/suite/codex_delegate.rs | 10 ++-- .../tests/suite/collaboration_instructions.rs | 4 +- codex-rs/core/tests/suite/hooks.rs | 4 +- codex-rs/core/tests/suite/otel.rs | 5 +- codex-rs/core/tests/suite/prompt_caching.rs | 4 +- codex-rs/core/tests/suite/remote_models.rs | 10 ++-- .../core/tests/suite/request_permissions.rs | 56 +++++++++++++----- .../tests/suite/request_permissions_tool.rs | 8 ++- codex-rs/core/tests/suite/resume_warning.rs | 2 +- codex-rs/core/tests/suite/tools.rs | 12 ++-- codex-rs/core/tests/suite/unified_exec.rs | 4 +- codex-rs/tui/src/app/tests.rs | 23 ++------ codex-rs/tui/src/app/thread_session_state.rs | 5 +- codex-rs/tui/src/chatwidget.rs | 19 ++++-- .../src/chatwidget/tests/history_replay.rs | 8 +-- .../tui/src/chatwidget/tests/permissions.rs | 27 +++++---- codex-rs/tui/src/status/tests.rs | 32 ++++------ 32 files changed, 242 insertions(+), 215 deletions(-) diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index d8b277db99..eceaaa9200 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -574,7 +574,7 @@ writable_roots = ["./sandbox-root"] false ); - match &*config.permissions.sandbox_policy { + match &config.legacy_sandbox_policy() { SandboxPolicy::WorkspaceWrite { network_access, .. } => { assert_eq!(*network_access, true); } diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 00d67ae1e3..63d80d0480 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -533,7 +533,7 @@ writable_roots = ["~/code"] .await?; let expected_root = AbsolutePathBuf::from_absolute_path(home.join("code"))?; - match config.permissions.sandbox_policy.get() { + match &config.legacy_sandbox_policy() { SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { assert_eq!( writable_roots diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index ce6cf03125..10ca2d6187 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -806,7 +806,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: ]), ); assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { writable_roots: vec![memories_root], network_access: false, @@ -840,7 +840,7 @@ async fn permission_profile_override_populates_runtime_permissions() -> std::io: assert_eq!(config.permissions.permission_profile(), permission_profile); assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::DangerFullAccess ); Ok(()) @@ -869,7 +869,7 @@ async fn permission_profile_override_preserves_managed_unrestricted_filesystem() assert_eq!(config.permissions.permission_profile(), permission_profile); assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, } @@ -898,7 +898,7 @@ async fn managed_unrestricted_permission_profile_still_enables_network_requireme ) .await?; assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::DangerFullAccess, "the legacy projection is intentionally lossy for managed unrestricted profiles" ); @@ -974,7 +974,7 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection( .can_write_path_with_cwd(memories_root.as_path(), cwd.path()) ); assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { writable_roots: vec![memories_root], network_access: false, @@ -1209,7 +1209,7 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() .can_write_path_with_cwd(external_write_path.as_path(), cwd.path()) ); assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::WorkspaceWrite { writable_roots: vec![external_write_path, memories_root], network_access: false, @@ -1317,7 +1317,7 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<( }]), ); assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::ReadOnly { network_access: false, } @@ -1382,7 +1382,7 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io FileSystemSandboxPolicy::restricted(Vec::new()) ); assert_eq!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::ReadOnly { network_access: false, } @@ -1509,13 +1509,7 @@ async fn permissions_profiles_allow_network_enablement() -> std::io::Result<()> config.permissions.network_sandbox_policy().is_enabled(), "expected network sandbox policy to be enabled", ); - assert!( - config - .permissions - .sandbox_policy - .get() - .has_full_network_access() - ); + assert!(config.legacy_sandbox_policy().has_full_network_access()); Ok(()) } @@ -1799,7 +1793,7 @@ exclude_slash_tmp = true ) .await?; - let sandbox_policy = config.permissions.sandbox_policy.get(); + let sandbox_policy = &config.legacy_sandbox_policy(); assert_eq!( config.permissions.file_system_sandbox_policy(), FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd.path()), @@ -1982,12 +1976,12 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result< let expected_backend = backend.abs(); if cfg!(target_os = "windows") { - match config.permissions.sandbox_policy.get() { + match &config.legacy_sandbox_policy() { SandboxPolicy::ReadOnly { .. } => {} other => panic!("expected read-only policy on Windows, got {other:?}"), } } else { - match config.permissions.sandbox_policy.get() { + match &config.legacy_sandbox_policy() { SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { assert_eq!( writable_roots @@ -2045,7 +2039,7 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result .await?; if cfg!(target_os = "windows") { - match config.permissions.sandbox_policy.get() { + match &config.legacy_sandbox_policy() { SandboxPolicy::ReadOnly { .. } => {} other => panic!("expected read-only policy on Windows, got {other:?}"), } @@ -2056,7 +2050,7 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result memories_root.display() ); let expected_memories_root = memories_root.abs(); - match config.permissions.sandbox_policy.get() { + match &config.legacy_sandbox_policy() { SandboxPolicy::WorkspaceWrite { writable_roots, .. } => { assert_eq!( writable_roots @@ -2375,7 +2369,7 @@ async fn profile_sandbox_mode_overrides_base() -> std::io::Result<()> { .await?; assert!(matches!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), &SandboxPolicy::DangerFullAccess )); @@ -2409,12 +2403,12 @@ async fn cli_override_takes_precedence_over_profile_sandbox_mode() -> std::io::R if cfg!(target_os = "windows") { assert!(matches!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), SandboxPolicy::ReadOnly { .. } )); } else { assert!(matches!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), SandboxPolicy::WorkspaceWrite { .. } )); } @@ -5450,7 +5444,6 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::Never), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -5644,7 +5637,6 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -5792,7 +5784,6 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::OnFailure), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -5925,7 +5916,6 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { permissions: Permissions { approval_policy: Constrained::allow_any(AskForApproval::OnFailure), permission_profile: Constrained::allow_any(PermissionProfile::read_only()), - sandbox_policy: Constrained::allow_any(SandboxPolicy::new_read_only_policy()), network: None, allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), @@ -6662,7 +6652,7 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow: if cfg!(target_os = "windows") { assert!( matches!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), SandboxPolicy::ReadOnly { .. } ), "Expected ReadOnly on Windows" @@ -6670,7 +6660,7 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow: } else { assert!( matches!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), SandboxPolicy::WorkspaceWrite { .. } ), "Expected WorkspaceWrite sandbox for untrusted project" @@ -6696,7 +6686,7 @@ async fn requirements_disallowing_default_sandbox_falls_back_to_required_default .build() .await?; assert_eq!( - *config.permissions.sandbox_policy.get(), + config.legacy_sandbox_policy(), SandboxPolicy::new_read_only_policy() ); Ok(()) @@ -6737,7 +6727,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s .build() .await?; assert_eq!( - *config.permissions.sandbox_policy.get(), + config.legacy_sandbox_policy(), SandboxPolicy::new_read_only_policy() ); Ok(()) @@ -6766,10 +6756,7 @@ async fn permission_profile_override_falls_back_when_disallowed_by_requirements( .await?; let expected_sandbox_policy = SandboxPolicy::new_read_only_policy(); - assert_eq!( - *config.permissions.sandbox_policy.get(), - expected_sandbox_policy - ); + assert_eq!(config.legacy_sandbox_policy(), expected_sandbox_policy); assert_eq!( config.permissions.permission_profile(), PermissionProfile::read_only() @@ -6823,7 +6810,7 @@ async fn permission_profile_override_preserves_split_write_roots() -> std::io::R .can_write_path_with_cwd(outside_root.as_path(), config.cwd.as_path()) ); assert!(matches!( - config.permissions.sandbox_policy.get(), + &config.legacy_sandbox_policy(), SandboxPolicy::WorkspaceWrite { .. } )); assert_eq!( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 14eecbb842..192abd3c9b 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -195,11 +195,6 @@ pub struct Permissions { /// Canonical effective runtime permissions after config requirements and /// runtime readable-root additions have been applied. pub permission_profile: Constrained, - /// Effective sandbox policy used for shell/unified exec. - /// - /// Legacy projection retained while runtime call sites migrate to - /// `permission_profile`. - pub sandbox_policy: Constrained, /// Effective network configuration applied to all spawned processes. pub network: Option, /// Whether the model may request a login shell for shell-based tools. @@ -250,13 +245,12 @@ impl Permissions { } /// Check whether a legacy sandbox policy can be applied to this permission - /// set under both legacy and canonical profile constraints. + /// set after projecting it into the canonical permission profile. pub fn can_set_legacy_sandbox_policy( &self, sandbox_policy: &SandboxPolicy, cwd: &Path, ) -> ConstraintResult<()> { - self.sandbox_policy.can_set(sandbox_policy)?; let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd); let network_sandbox_policy = NetworkSandboxPolicy::from(sandbox_policy); @@ -285,31 +279,18 @@ impl Permissions { network_sandbox_policy, ); - self.sandbox_policy.set(sandbox_policy)?; self.permission_profile.set(permission_profile)?; Ok(()) } - /// Replace permissions from the canonical profile and update compatibility - /// projections for legacy consumers. + /// Replace permissions from the canonical profile. pub fn set_permission_profile( &mut self, permission_profile: PermissionProfile, - cwd: &Path, ) -> ConstraintResult<()> { - 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, - cwd, - ); self.permission_profile.can_set(&permission_profile)?; - self.sandbox_policy.can_set(&sandbox_policy)?; self.permission_profile.set(permission_profile)?; - self.sandbox_policy.set(sandbox_policy)?; Ok(()) } } @@ -938,6 +919,18 @@ impl ConfigBuilder { } impl Config { + pub fn legacy_sandbox_policy(&self) -> SandboxPolicy { + self.permissions.legacy_sandbox_policy(self.cwd.as_path()) + } + + pub fn set_legacy_sandbox_policy( + &mut self, + sandbox_policy: SandboxPolicy, + ) -> ConstraintResult<()> { + self.permissions + .set_legacy_sandbox_policy(sandbox_policy, self.cwd.as_path()) + } + pub fn to_models_manager_config(&self) -> ModelsManagerConfig { ModelsManagerConfig { model_context_window: self.model_context_window, @@ -2516,7 +2509,6 @@ impl Config { permissions: Permissions { approval_policy: constrained_approval_policy.value, permission_profile: constrained_permission_profile, - sandbox_policy: constrained_sandbox_policy.value, network, allow_login_shell, shell_environment_policy, diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index fac589c58b..22651c23d8 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -848,7 +848,6 @@ pub(crate) fn build_guardian_review_session_config( guardian_config.permissions.permission_profile = Constrained::allow_only( PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy), ); - guardian_config.permissions.sandbox_policy = Constrained::allow_only(sandbox_policy.clone()); guardian_config .permissions .set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path()) diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 7b0f7904b0..19bee54e4c 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -1950,8 +1950,10 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() { Constrained::allow_only(AskForApproval::Never) ); assert_eq!( - guardian_config.permissions.sandbox_policy, - Constrained::allow_only(SandboxPolicy::new_read_only_policy()) + guardian_config.permissions.permission_profile, + Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_read_only_policy(), + )) ); } diff --git a/codex-rs/core/src/memories/tests.rs b/codex-rs/core/src/memories/tests.rs index f718c309a2..08ebcd802a 100644 --- a/codex-rs/core/src/memories/tests.rs +++ b/codex-rs/core/src/memories/tests.rs @@ -489,7 +489,7 @@ mod phase2 { ); config .permissions - .set_permission_profile(permission_profile, config.cwd.as_path()) + .set_permission_profile(permission_profile) .expect("permissions are configurable"); configure(&mut config); let config = Arc::new(config); @@ -935,8 +935,9 @@ mod phase2 { .await .expect("enqueue global consolidation"); let mut constrained_config = harness.config.as_ref().clone(); - constrained_config.permissions.sandbox_policy = - Constrained::allow_only(SandboxPolicy::DangerFullAccess); + constrained_config.permissions.permission_profile = Constrained::allow_only( + PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::DangerFullAccess), + ); phase2::run(&harness.session, Arc::new(constrained_config)).await; diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 290b90036f..9515755323 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1515,7 +1515,9 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() -> }; let expected_sandbox_policy = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { - config.permissions.sandbox_policy = codex_config::Constrained::allow_any(sandbox_policy); + config + .set_legacy_sandbox_policy(sandbox_policy) + .expect("set sandbox policy"); config.permissions.permission_profile = codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions( &FileSystemSandboxPolicy::external_sandbox(), @@ -4187,7 +4189,7 @@ async fn user_turn_updates_approvals_reviewer() { cwd: config.cwd.to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: Some(codex_config::types::ApprovalsReviewer::AutoReview), - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), permission_profile: None, model: turn_context.model_info.slug.clone(), effort: config.model_reasoning_effort, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 45b41e601e..383d80292c 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -383,8 +383,6 @@ impl Session { per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer; 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); let permission_profile = session_configuration.permission_profile(); let resolved_web_search_mode = resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 2666660229..c722ddb8d3 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -269,7 +269,7 @@ pub(crate) fn apply_spawn_agent_runtime_overrides( config.cwd = turn.cwd.clone(); config .permissions - .set_permission_profile(turn.permission_profile(), turn.cwd.as_path()) + .set_permission_profile(turn.permission_profile()) .map_err(|err| { FunctionCallError::RespondToModel(format!("permission_profile is invalid: {err}")) })?; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index e9c9406ae8..64cc9db032 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -2086,7 +2086,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() { let (mut session, mut turn) = make_session_and_context().await; let manager = thread_manager(); session.services.agent_control = manager.agent_control(); - let expected_sandbox = turn.config.permissions.sandbox_policy.get().clone(); + let expected_sandbox = turn.config.legacy_sandbox_policy(); let mut expected_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&expected_sandbox, &turn.cwd); expected_file_system_sandbox_policy @@ -3585,8 +3585,9 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr #[tokio::test] async fn build_agent_spawn_config_uses_turn_context_values() { fn pick_allowed_sandbox_policy( - constraint: &crate::config::Constrained, + constraint: &crate::config::Constrained, base: SandboxPolicy, + cwd: &std::path::Path, ) -> SandboxPolicy { let candidates = [ SandboxPolicy::new_read_only_policy(), @@ -3595,7 +3596,21 @@ async fn build_agent_spawn_config_uses_turn_context_values() { ]; candidates .into_iter() - .find(|candidate| *candidate != base && constraint.can_set(candidate).is_ok()) + .find(|candidate| { + if *candidate == base { + return false; + } + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(candidate, cwd); + let network_sandbox_policy = NetworkSandboxPolicy::from(candidate); + let permission_profile = + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(candidate), + &file_system_sandbox_policy, + network_sandbox_policy, + ); + constraint.can_set(&permission_profile).is_ok() + }) .unwrap_or(base) } @@ -3613,8 +3628,9 @@ async fn build_agent_spawn_config_uses_turn_context_values() { turn.cwd = temp_dir.abs(); turn.codex_linux_sandbox_exe = Some(PathBuf::from("/bin/echo")); let sandbox_policy = pick_allowed_sandbox_policy( - &turn.config.permissions.sandbox_policy, - turn.config.permissions.sandbox_policy.get().clone(), + &turn.config.permissions.permission_profile, + turn.config.legacy_sandbox_policy(), + turn.cwd.as_path(), ); let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, &turn.cwd); @@ -3648,7 +3664,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() { .expect("approval policy set"); expected .permissions - .set_permission_profile(permission_profile, turn.cwd.as_path()) + .set_permission_profile(permission_profile) .expect("permission profile set"); assert_eq!(config, expected); } @@ -3699,8 +3715,7 @@ async fn build_agent_resume_config_clears_base_instructions() { .expect("approval policy set"); expected .permissions - .sandbox_policy - .set(turn.sandbox_policy()) - .expect("sandbox policy set"); + .set_permission_profile(turn.permission_profile()) + .expect("permission profile set"); assert_eq!(config, expected); } diff --git a/codex-rs/core/tests/common/zsh_fork.rs b/codex-rs/core/tests/common/zsh_fork.rs index bc87c9ea93..448693e069 100644 --- a/codex-rs/core/tests/common/zsh_fork.rs +++ b/codex-rs/core/tests/common/zsh_fork.rs @@ -36,7 +36,9 @@ impl ZshForkRuntime { config.main_execve_wrapper_exe = Some(self.main_execve_wrapper_exe.clone()); config.permissions.allow_login_shell = false; config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy); + config + .set_legacy_sandbox_policy(sandbox_policy) + .expect("set sandbox policy"); } } diff --git a/codex-rs/core/tests/suite/agent_websocket.rs b/codex-rs/core/tests/suite/agent_websocket.rs index fb0cd84120..305346afac 100644 --- a/codex-rs/core/tests/suite/agent_websocket.rs +++ b/codex-rs/core/tests/suite/agent_websocket.rs @@ -38,11 +38,8 @@ async fn websocket_test_codex_shell_chain() -> Result<()> { let mut builder = test_codex().with_windows_cmd_shell(); let test = builder.build_with_websocket_server(&server).await?; - test.submit_turn_with_policy( - "run the echo command", - test.config.permissions.sandbox_policy.get().clone(), - ) - .await?; + test.submit_turn_with_policy("run the echo command", test.config.legacy_sandbox_policy()) + .await?; let connection = server.single_connection(); assert_eq!(connection.len(), 2); @@ -85,11 +82,8 @@ async fn websocket_first_turn_uses_startup_prewarm_and_create() -> Result<()> { let mut builder = test_codex(); let test = builder.build_with_websocket_server(&server).await?; - test.submit_turn_with_policy( - "hello", - test.config.permissions.sandbox_policy.get().clone(), - ) - .await?; + test.submit_turn_with_policy("hello", test.config.legacy_sandbox_policy()) + .await?; assert_eq!(server.handshakes().len(), 1); let connection = server.single_connection(); @@ -135,11 +129,8 @@ async fn websocket_first_turn_handles_handshake_delay_with_startup_prewarm() -> let mut builder = test_codex(); let test = builder.build_with_websocket_server(&server).await?; - test.submit_turn_with_policy( - "hello", - test.config.permissions.sandbox_policy.get().clone(), - ) - .await?; + test.submit_turn_with_policy("hello", test.config.legacy_sandbox_policy()) + .await?; assert_eq!(server.handshakes().len(), 1); let connection = server.single_connection(); @@ -191,11 +182,8 @@ async fn websocket_v2_test_codex_shell_chain() -> Result<()> { }); let test = builder.build_with_websocket_server(&server).await?; - test.submit_turn_with_policy( - "run the echo command", - test.config.permissions.sandbox_policy.get().clone(), - ) - .await?; + test.submit_turn_with_policy("run the echo command", test.config.legacy_sandbox_policy()) + .await?; let connection = server.single_connection(); assert_eq!(connection.len(), 3); diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 0888c91c47..4209fc2100 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -1727,7 +1727,9 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> { let mut builder = test_codex().with_model(model).with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy.clone()); + config + .set_legacy_sandbox_policy(sandbox_policy.clone()) + .expect("set sandbox policy"); for feature in features { config .features @@ -1854,7 +1856,9 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file() .with_model("gpt-5.4") .with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config.approvals_reviewer = ApprovalsReviewer::User; }); let test = builder.build(&server).await?; @@ -1962,7 +1966,9 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); }); let test = builder.build(&server).await?; let allow_prefix_path = test.cwd.path().join("allow-prefix.txt"); @@ -2133,7 +2139,9 @@ async fn spawned_subagent_execpolicy_amendment_propagates_to_parent_session() -> let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::Collab) @@ -2394,7 +2402,9 @@ async fn invalid_requested_prefix_rule_falls_back_for_compound_command() -> Resu let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); }); let test = builder.build(&server).await?; @@ -2445,7 +2455,9 @@ async fn approving_fallback_rule_for_compound_command_works() -> Result<()> { let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); }); let test = builder.build(&server).await?; @@ -2580,7 +2592,9 @@ allow_local_binding = true let sandbox_policy_for_config = sandbox_policy.clone(); 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(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); let layers = config .config_layer_stack .get_layers( @@ -3030,7 +3044,9 @@ async fn compound_command_with_one_safe_command_still_requires_approval() -> Res let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); }); let test = builder.build(&server).await?; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 13cdf38674..8886568435 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1745,7 +1745,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re cwd: config.cwd.to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), permission_profile: None, model: session_configured.model.clone(), effort: Some(ReasoningEffort::Low), @@ -1867,7 +1867,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() cwd: config.cwd.to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), permission_profile: None, model: session_configured.model, effort: None, diff --git a/codex-rs/core/tests/suite/codex_delegate.rs b/codex-rs/core/tests/suite/codex_delegate.rs index 0b96c6e5ab..461b07284c 100644 --- a/codex-rs/core/tests/suite/codex_delegate.rs +++ b/codex-rs/core/tests/suite/codex_delegate.rs @@ -64,8 +64,9 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() { // routes ExecApprovalRequest via the parent. let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.permissions.sandbox_policy = - Constrained::allow_any(SandboxPolicy::new_read_only_policy()); + config + .set_legacy_sandbox_policy(SandboxPolicy::new_read_only_policy()) + .expect("set sandbox policy"); }); let test = builder.build(&server).await.expect("build test codex"); @@ -147,8 +148,9 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() { let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); // Use a restricted sandbox so patch approval is required - config.permissions.sandbox_policy = - Constrained::allow_any(SandboxPolicy::new_read_only_policy()); + config + .set_legacy_sandbox_policy(SandboxPolicy::new_read_only_policy()) + .expect("set sandbox policy"); config.include_apply_patch_tool = true; }); let test = builder.build(&server).await.expect("build test codex"); diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 26d8d6aacc..e3ea0669ca 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -185,7 +185,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> { cwd: test.config.cwd.to_path_buf(), approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), + sandbox_policy: test.config.legacy_sandbox_policy(), permission_profile: None, model: test.session_configured.model.clone(), effort: None, @@ -307,7 +307,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu cwd: test.config.cwd.to_path_buf(), approval_policy: test.config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: test.config.permissions.sandbox_policy.get().clone(), + sandbox_policy: test.config.legacy_sandbox_policy(), permission_profile: None, model: test.session_configured.model.clone(), effort: None, diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 851980c42f..74e9a7a682 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -1583,7 +1583,9 @@ allow_local_binding = true .enable(Feature::CodexHooks) .expect("test config should allow feature update"); config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); let layers = config .config_layer_stack .get_layers( diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 6407ec2702..3d2f5102e2 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -1110,8 +1110,9 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.permissions.sandbox_policy = - Constrained::allow_any(SandboxPolicy::DangerFullAccess); + config + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) + .expect("set sandbox policy"); }) .build(&server) .await diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 2e168bd729..3852918c53 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -825,7 +825,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a let default_cwd = config.cwd.clone(); let default_approval_policy = config.permissions.approval_policy.value(); - let default_sandbox_policy = config.permissions.sandbox_policy.get(); + let default_sandbox_policy = &config.legacy_sandbox_policy(); let default_model = session_configured.model; let default_effort = config.model_reasoning_effort; let default_summary = config.model_reasoning_summary; @@ -955,7 +955,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu let default_cwd = config.cwd.clone(); let default_approval_policy = config.permissions.approval_policy.value(); - let default_sandbox_policy = config.permissions.sandbox_policy.get(); + let default_sandbox_policy = &config.legacy_sandbox_policy(); let default_model = session_configured.model; let default_effort = config.model_reasoning_effort; let default_summary = config.model_reasoning_summary; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 07a1bc404d..a69dae4a5b 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -162,7 +162,7 @@ async fn remote_models_config_context_window_override_clamps_to_max_context_wind cwd: cwd.path().to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), model: requested_model.to_string(), effort: None, summary: None, @@ -240,7 +240,7 @@ async fn remote_models_config_override_above_max_uses_max_context_window() -> Re cwd: cwd.path().to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), model: requested_model.to_string(), effort: None, summary: None, @@ -317,7 +317,7 @@ async fn remote_models_use_context_window_when_config_override_is_absent() -> Re cwd: cwd.path().to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), model: requested_model.to_string(), effort: None, summary: None, @@ -407,7 +407,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( cwd: cwd.path().to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), permission_profile: None, model: requested_model.to_string(), effort: None, @@ -468,7 +468,7 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning() cwd: cwd.path().to_path_buf(), approval_policy: config.permissions.approval_policy.value(), approvals_reviewer: None, - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), permission_profile: None, model: requested_model.to_string(), effort: None, diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index 8719bba9ff..455c1fabb9 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -324,7 +324,9 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -419,7 +421,9 @@ async fn request_permissions_tool_is_auto_denied_when_granular_request_permissio let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::RequestPermissionsTool) @@ -502,7 +506,9 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -603,7 +609,9 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_cwd let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -703,7 +711,9 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_tmp let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -802,7 +812,9 @@ async fn workspace_write_with_additional_permissions_can_write_outside_cwd() -> let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -906,7 +918,9 @@ async fn with_additional_permissions_denied_approval_blocks_execution() -> Resul let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -1011,7 +1025,9 @@ async fn request_permissions_grants_apply_to_later_exec_command_calls() -> Resul let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -1135,7 +1151,9 @@ async fn request_permissions_preapprove_explicit_exec_permissions_outside_on_req let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -1253,7 +1271,9 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls() -> Resu let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -1365,7 +1385,9 @@ async fn request_permissions_grants_apply_to_later_shell_command_calls_without_i let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::RequestPermissionsTool) @@ -1477,7 +1499,9 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions() let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -1641,7 +1665,9 @@ async fn request_permissions_grants_do_not_carry_across_turns() -> Result<()> { let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -1754,7 +1780,9 @@ async fn request_permissions_session_grants_carry_across_turns() -> Result<()> { let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) diff --git a/codex-rs/core/tests/suite/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 8bd83f58b5..8baf14293c 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -204,7 +204,9 @@ async fn approved_folder_write_request_permissions_unblocks_later_exec_without_s let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) @@ -334,7 +336,9 @@ async fn apply_patch_after_request_permissions(strict_auto_review: bool) -> Resu let mut builder = test_codex().with_config(move |config| { config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config.permissions.sandbox_policy = Constrained::allow_any(sandbox_policy_for_config); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); config .features .enable(Feature::ExecPermissionApprovals) diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index ee3c7bbf33..f5810956ab 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -32,7 +32,7 @@ fn resume_history( current_date: None, timezone: None, approval_policy: config.permissions.approval_policy.value(), - sandbox_policy: config.permissions.sandbox_policy.get().clone(), + sandbox_policy: config.legacy_sandbox_policy(), permission_profile: None, network: None, file_system_sandbox_policy: None, diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index 46bedff36e..aff8755b11 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -552,7 +552,9 @@ async fn shell_enforces_glob_deny_read_policy() -> Result<()> { let mut builder = test_codex() .with_model("gpt-5.4") .with_config(move |config| { - config.permissions.sandbox_policy = Constrained::allow_any(read_only_policy_for_config); + config + .set_legacy_sandbox_policy(read_only_policy_for_config) + .expect("set sandbox policy"); let mut file_system_sandbox_policy = FileSystemSandboxPolicy::default(); file_system_sandbox_policy .entries @@ -789,9 +791,7 @@ async fn shell_timeout_handles_background_grandchild_stdout() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_model("gpt-5.4").with_config(|config| { config - .permissions - .sandbox_policy - .set(SandboxPolicy::DangerFullAccess) + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) .expect("set sandbox policy"); }); let test = builder.build(&server).await?; @@ -885,9 +885,7 @@ async fn shell_spawn_failure_truncates_exec_error() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_config(|cfg| { - cfg.permissions - .sandbox_policy - .set(SandboxPolicy::DangerFullAccess) + cfg.set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) .expect("set sandbox policy"); }); let test = builder.build(&server).await?; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 67226ef20e..9c3272882e 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -2545,7 +2545,9 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> { .features .enable(Feature::UnifiedExec) .expect("test config should allow feature update"); - config.permissions.sandbox_policy = Constrained::allow_any(read_only_policy_for_config); + config + .set_legacy_sandbox_policy(read_only_policy_for_config) + .expect("set sandbox policy"); let mut file_system_sandbox_policy = FileSystemSandboxPolicy::default(); file_system_sandbox_policy .entries diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index da83a72c40..e81f4476f4 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -1634,11 +1634,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< auto_review.approval_policy ); assert_eq!( - app.chat_widget - .config_ref() - .permissions - .sandbox_policy - .get(), + &app.chat_widget.config_ref().legacy_sandbox_policy(), &auto_review.sandbox_policy ); assert_eq!( @@ -1714,9 +1710,7 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor .approval_policy .set(AskForApproval::OnRequest)?; app.config - .permissions - .sandbox_policy - .set(SandboxPolicy::new_workspace_write_policy())?; + .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy())?; app.chat_widget .set_approval_policy(AskForApproval::OnRequest); app.chat_widget @@ -1815,11 +1809,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review auto_review.approval_policy ); assert_eq!( - app.chat_widget - .config_ref() - .permissions - .sandbox_policy - .get(), + &app.chat_widget.config_ref().legacy_sandbox_policy(), &auto_review.sandbox_policy ); assert_eq!( @@ -2933,7 +2923,7 @@ async fn side_fork_config_is_ephemeral_and_appends_developer_guardrails() { let mut app = make_test_app().await; app.config.developer_instructions = Some("Existing developer policy.".to_string()); let original_approval_policy = app.config.permissions.approval_policy.value(); - let original_sandbox_policy = app.config.permissions.sandbox_policy.get().clone(); + let original_sandbox_policy = app.config.legacy_sandbox_policy(); let fork_config = app.side_fork_config(); @@ -2942,10 +2932,7 @@ async fn side_fork_config_is_ephemeral_and_appends_developer_guardrails() { fork_config.permissions.approval_policy.value(), original_approval_policy ); - assert_eq!( - fork_config.permissions.sandbox_policy.get(), - &original_sandbox_policy - ); + assert_eq!(fork_config.legacy_sandbox_policy(), original_sandbox_policy); let developer_instructions = fork_config .developer_instructions .as_deref() diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 2b242890d3..b4d0fb2684 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -192,9 +192,8 @@ mod tests { .set_sandbox_policy(expected_sandbox_policy.clone()) .expect("set widget sandbox policy"); app.config - .permissions - .set_legacy_sandbox_policy(expected_sandbox_policy.clone(), app.config.cwd.as_path()) - .expect("set app sandbox policy"); + .set_legacy_sandbox_policy(expected_sandbox_policy.clone()) + .expect("set sandbox policy"); app.sync_active_thread_permission_settings_to_cached_session() .await; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index dd48f030cb..a1035fac72 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -141,8 +141,12 @@ use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::UserMessageItem; use codex_protocol::models::MessagePhase; +use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::models::local_image_label_text; use codex_protocol::parse_command::ParsedCommand; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg; use codex_protocol::plan_tool::StepStatus as UpdatePlanItemStatus; #[cfg(test)] @@ -2369,7 +2373,7 @@ impl ChatWidget { Some(permission_profile) => self .config .permissions - .set_permission_profile(permission_profile, event.cwd.as_path()), + .set_permission_profile(permission_profile), None => self .config .permissions @@ -2377,11 +2381,16 @@ impl ChatWidget { }; if let Err(err) = permission_sync { tracing::warn!(%err, "failed to sync permissions from SessionConfigured"); - self.config.permissions.sandbox_policy = - Constrained::allow_only(event.sandbox_policy.clone()); let permission_profile = event.permission_profile.clone().unwrap_or_else(|| { - codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &event.sandbox_policy, + let file_system_sandbox_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd( + &event.sandbox_policy, + event.cwd.as_path(), + ); + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&event.sandbox_policy), + &file_system_sandbox_policy, + NetworkSandboxPolicy::from(&event.sandbox_policy), ) }); self.config.permissions.permission_profile = diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index be0fd03a11..77f52524f4 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -252,9 +252,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { .set(AskForApproval::OnRequest) .expect("set approval policy"); chat.config - .permissions - .sandbox_policy - .set(SandboxPolicy::new_workspace_write_policy()) + .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) .expect("set sandbox policy"); chat.config.cwd = test_path_buf("/home/user/main").abs(); @@ -312,7 +310,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { AskForApproval::Never ); assert_eq!( - chat.config_ref().permissions.sandbox_policy.get(), + &chat.config_ref().legacy_sandbox_policy(), &expected_sandbox ); assert_eq!( @@ -374,7 +372,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { }); assert_eq!( - chat.config_ref().permissions.sandbox_policy.get(), + &chat.config_ref().legacy_sandbox_policy(), &expected_sandbox ); assert_eq!( diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index ccab18bfcb..388bc67f81 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -1,13 +1,6 @@ use super::*; use pretty_assertions::assert_eq; -fn set_legacy_sandbox_policy(chat: &mut ChatWidget, sandbox_policy: SandboxPolicy) { - chat.config - .permissions - .set_legacy_sandbox_policy(sandbox_policy, chat.config.cwd.as_path()) - .expect("set sandbox policy"); -} - #[tokio::test] async fn approvals_selection_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -354,7 +347,9 @@ async fn permissions_selection_history_snapshot_full_access_to_default() { .approval_policy .set(AskForApproval::Never) .expect("set approval policy"); - set_legacy_sandbox_policy(&mut chat, SandboxPolicy::DangerFullAccess); + chat.config + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess) + .expect("set sandbox policy"); chat.open_permissions_popup(); let popup = render_bottom_popup(&chat, /*width*/ 120); @@ -393,7 +388,9 @@ async fn permissions_selection_emits_history_cell_when_current_is_selected() { .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); + chat.config + .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) + .expect("set sandbox policy"); chat.open_permissions_popup(); chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); @@ -448,7 +445,9 @@ async fn permissions_selection_hides_auto_review_when_feature_disabled_even_if_a .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); + chat.config + .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) + .expect("set sandbox policy"); chat.open_permissions_popup(); let popup = render_bottom_popup(&chat, /*width*/ 120); @@ -573,7 +572,9 @@ async fn permissions_selection_can_disable_auto_review() { .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); + chat.config + .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) + .expect("set sandbox policy"); chat.open_permissions_popup(); chat.handle_key_event(KeyEvent::from(KeyCode::Up)); @@ -610,7 +611,9 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context .approval_policy .set(AskForApproval::OnRequest) .expect("set approval policy"); - set_legacy_sandbox_policy(&mut chat, SandboxPolicy::new_workspace_write_policy()); + chat.config + .set_legacy_sandbox_policy(SandboxPolicy::new_workspace_write_policy()) + .expect("set sandbox policy"); chat.set_approvals_reviewer(ApprovalsReviewer::User); chat.open_permissions_popup(); diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 569f093a11..03a988c793 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -99,16 +99,12 @@ async fn status_snapshot_includes_reasoning_details() { config.model_reasoning_summary = Some(ReasoningSummary::Detailed); config.cwd = test_path_buf("/workspace/tests").abs(); config - .permissions - .set_legacy_sandbox_policy( - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }, - config.cwd.as_path(), - ) + .set_legacy_sandbox_policy(SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }) .expect("set sandbox policy"); let account_display = test_status_account_display(); @@ -185,16 +181,12 @@ async fn status_permissions_non_default_workspace_write_is_custom() { .expect("set approval policy"); config.cwd = test_path_buf("/workspace/tests").abs(); config - .permissions - .set_legacy_sandbox_policy( - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }, - config.cwd.as_path(), - ) + .set_legacy_sandbox_policy(SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: true, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }) .expect("set sandbox policy"); let account_display = test_status_account_display(); From fc3c6bf4fb0cc929e71e5b81d886be2ddc3d35d5 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 26 Apr 2026 19:27:31 -0700 Subject: [PATCH 3/4] permissions: constrain requirements as profiles --- codex-rs/config/src/config_requirements.rs | 159 +++++++++++------- codex-rs/config/src/config_toml.rs | 11 +- codex-rs/config/src/lib.rs | 1 + .../core/src/config/config_loader_tests.rs | 35 ++-- codex-rs/core/src/config/config_tests.rs | 57 ++++--- codex-rs/core/src/config/mod.rs | 134 +++++---------- codex-rs/tui/src/debug_config.rs | 8 +- 7 files changed, 200 insertions(+), 205 deletions(-) diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index ef0602ae24..51cc9baa25 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -1,8 +1,8 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; @@ -84,7 +84,7 @@ impl std::ops::DerefMut for ConstrainedWithSource { pub struct ConfigRequirements { pub approval_policy: ConstrainedWithSource, pub approvals_reviewer: ConstrainedWithSource, - pub sandbox_policy: ConstrainedWithSource, + pub permission_profile: ConstrainedWithSource, pub web_search_mode: ConstrainedWithSource, pub feature_requirements: Option>, pub managed_hooks: Option>, @@ -110,8 +110,8 @@ impl Default for ConfigRequirements { Constrained::allow_any_from_default(), /*source*/ None, ), - sandbox_policy: ConstrainedWithSource::new( - Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permission_profile: ConstrainedWithSource::new( + Constrained::allow_any(PermissionProfile::read_only()), /*source*/ None, ), web_search_mode: ConstrainedWithSource::new( @@ -967,15 +967,8 @@ impl TryFrom for ConfigRequirements { ), }; - // TODO(gt): `ConfigRequirementsToml` should let the author specify the - // default `SandboxPolicy`? Should do this for `AskForApproval` too? - // - // Currently, we force ReadOnly as the default policy because two of - // the other variants (WorkspaceWrite, ExternalSandbox) require - // additional parameters. Ultimately, we should expand the config - // format to allow specifying those parameters. - let default_sandbox_policy = SandboxPolicy::new_read_only_policy(); - let sandbox_policy = match allowed_sandbox_modes { + let default_permission_profile = PermissionProfile::read_only(); + let permission_profile = match allowed_sandbox_modes { Some(Sourced { value: modes, source: requirement_source, @@ -984,23 +977,15 @@ impl TryFrom for ConfigRequirements { return Err(ConstraintError::InvalidValue { field_name: "allowed_sandbox_modes", candidate: format!("{modes:?}"), - allowed: "must include 'read-only' to allow any SandboxPolicy".to_string(), + allowed: "must include 'read-only' to allow any PermissionProfile" + .to_string(), requirement_source, }); }; let requirement_source_for_error = requirement_source.clone(); - let constrained = Constrained::new(default_sandbox_policy, move |candidate| { - let mode = match candidate { - SandboxPolicy::ReadOnly { .. } => SandboxModeRequirement::ReadOnly, - SandboxPolicy::WorkspaceWrite { .. } => { - SandboxModeRequirement::WorkspaceWrite - } - SandboxPolicy::DangerFullAccess => SandboxModeRequirement::DangerFullAccess, - SandboxPolicy::ExternalSandbox { .. } => { - SandboxModeRequirement::ExternalSandbox - } - }; + let constrained = Constrained::new(default_permission_profile, move |candidate| { + let mode = sandbox_mode_requirement_for_permission_profile(candidate); if modes.contains(&mode) { Ok(()) } else { @@ -1014,12 +999,10 @@ impl TryFrom for ConfigRequirements { })?; ConstrainedWithSource::new(constrained, Some(requirement_source)) } - None => { - ConstrainedWithSource::new( - Constrained::allow_any(default_sandbox_policy), - /*source*/ None, - ) - } + None => ConstrainedWithSource::new( + Constrained::allow_any(default_permission_profile), + /*source*/ None, + ), }; let exec_policy = match rules { Some(Sourced { value, source }) => { @@ -1145,7 +1128,7 @@ impl TryFrom for ConfigRequirements { Ok(ConfigRequirements { approval_policy, approvals_reviewer, - sandbox_policy, + permission_profile, web_search_mode, feature_requirements, managed_hooks, @@ -1159,6 +1142,29 @@ impl TryFrom for ConfigRequirements { } } +pub fn sandbox_mode_requirement_for_permission_profile( + permission_profile: &PermissionProfile, +) -> SandboxModeRequirement { + match permission_profile { + PermissionProfile::Disabled => SandboxModeRequirement::DangerFullAccess, + PermissionProfile::External { .. } => SandboxModeRequirement::ExternalSandbox, + PermissionProfile::Managed { .. } => { + let file_system_policy = permission_profile.file_system_sandbox_policy(); + if file_system_policy.has_full_disk_write_access() { + SandboxModeRequirement::DangerFullAccess + } else if file_system_policy + .entries + .iter() + .any(|entry| entry.access.can_write()) + { + SandboxModeRequirement::WorkspaceWrite + } else { + SandboxModeRequirement::ReadOnly + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -1168,6 +1174,7 @@ mod tests { use codex_execpolicy::Evaluation; use codex_execpolicy::RuleMatch; use codex_protocol::protocol::NetworkAccess; + use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use pretty_assertions::assert_eq; @@ -1183,6 +1190,10 @@ mod tests { )?) } + fn profile_from_sandbox_policy(sandbox_policy: &SandboxPolicy) -> PermissionProfile { + PermissionProfile::from_legacy_sandbox_policy(sandbox_policy) + } + fn with_unknown_source(toml: ConfigRequirementsToml) -> ConfigRequirementsWithSources { let ConfigRequirementsToml { allowed_approval_policies, @@ -1724,8 +1735,10 @@ allowed_approvals_reviewers = ["user"] ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -1803,7 +1816,7 @@ allowed_approvals_reviewers = ["user"] Some(source_location.clone()) ); assert_eq!( - requirements.sandbox_policy.source, + requirements.permission_profile.source, Some(source_location.clone()) ); assert_eq!( @@ -1869,8 +1882,10 @@ allowed_approvals_reviewers = ["user"] ); assert!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::new_read_only_policy()) + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy() + )) .is_ok() ); @@ -1952,25 +1967,30 @@ allowed_approvals_reviewers = ["user"] let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; assert!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::new_read_only_policy()) + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::new_read_only_policy() + )) .is_ok() ); + let workspace_write_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; assert!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) + .permission_profile + .can_set(&profile_from_sandbox_policy(&workspace_write_policy)) .is_ok() ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -1980,10 +2000,12 @@ allowed_approvals_reviewers = ["user"] ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Restricted, - }), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::ExternalSandbox { + network_access: NetworkAccess::Restricted, + } + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "ExternalSandbox".into(), @@ -2064,21 +2086,24 @@ allowed_approvals_reviewers = ["user"] let requirements = ConfigRequirements::try_from(requirements_with_sources)?; let root = if cfg!(windows) { "C:\\repo" } else { "/repo" }; + let workspace_write_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }; assert!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) + .permission_profile + .can_set(&profile_from_sandbox_policy(&workspace_write_policy)) .is_ok() ); assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -2108,8 +2133,10 @@ allowed_approvals_reviewers = ["user"] assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::DangerFullAccess), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "DangerFullAccess".into(), @@ -2147,8 +2174,10 @@ allowed_approvals_reviewers = ["user"] assert_eq!( requirements - .sandbox_policy - .can_set(&SandboxPolicy::new_workspace_write_policy()), + .permission_profile + .can_set(&profile_from_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy(), + )), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "WorkspaceWrite".into(), diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 92ff18b45a..2821de5a8b 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -47,6 +47,7 @@ use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; @@ -647,7 +648,7 @@ impl ConfigToml { profile_sandbox_mode: Option, windows_sandbox_level: WindowsSandboxLevel, active_project: Option<&ProjectConfig>, - sandbox_policy_constraint: Option<&crate::Constrained>, + permission_profile_constraint: Option<&crate::Constrained>, ) -> SandboxPolicy { let sandbox_mode_was_explicit = sandbox_mode_override.is_some() || profile_sandbox_mode.is_some() @@ -707,14 +708,16 @@ impl ConfigToml { downgrade_workspace_write_if_unsupported(&mut sandbox_policy); } if !sandbox_mode_was_explicit - && let Some(constraint) = sandbox_policy_constraint - && let Err(err) = constraint.can_set(&sandbox_policy) + && let Some(constraint) = permission_profile_constraint + && let Err(err) = constraint.can_set(&PermissionProfile::from_legacy_sandbox_policy( + &sandbox_policy, + )) { tracing::warn!( error = %err, "default sandbox policy is disallowed by requirements; falling back to required default" ); - sandbox_policy = constraint.get().clone(); + sandbox_policy = SandboxPolicy::new_read_only_policy(); downgrade_workspace_write_if_unsupported(&mut sandbox_policy); } sandbox_policy diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index eb0e7713fb..d628fbf04f 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -53,6 +53,7 @@ pub use config_requirements::ResidencyRequirement; pub use config_requirements::SandboxModeRequirement; pub use config_requirements::Sourced; pub use config_requirements::WebSearchModeRequirement; +pub use config_requirements::sandbox_mode_requirement_for_permission_profile; pub use constraint::Constrained; pub use constraint::ConstraintError; pub use constraint::ConstraintResult; diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 63d80d0480..e07447edfe 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -27,6 +27,7 @@ use codex_config::version_for_toml; use codex_exec_server::LOCAL_FS; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; @@ -585,8 +586,8 @@ allowed_sandbox_modes = ["read-only"] AskForApproval::Never ); assert_eq!( - *state.requirements().sandbox_policy.get(), - SandboxPolicy::new_read_only_policy() + state.requirements().permission_profile.get(), + &PermissionProfile::read_only() ); assert!( state @@ -598,13 +599,15 @@ allowed_sandbox_modes = ["read-only"] assert!( state .requirements() - .sandbox_policy - .can_set(&SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }) + .permission_profile + .can_set(&PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::WorkspaceWrite { + writable_roots: Vec::new(), + network_access: false, + exclude_tmpdir_env_var: false, + exclude_slash_tmp: false, + }, + )) .is_err() ); @@ -913,9 +916,11 @@ allowed_sandbox_modes = ["read-only"] let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?; assert_eq!( - config_requirements - .sandbox_policy - .can_set(&SandboxPolicy::new_workspace_write_policy()), + config_requirements.permission_profile.can_set( + &PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy() + ) + ), Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: "WorkspaceWrite".into(), @@ -1191,8 +1196,10 @@ async fn load_config_layers_applies_matching_remote_sandbox_config() -> anyhow:: assert!( layers .requirements() - .sandbox_policy - .can_set(&SandboxPolicy::new_workspace_write_policy()) + .permission_profile + .can_set(&PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::new_workspace_write_policy() + )) .is_ok() ); diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 10ca2d6187..9fa11fdaa8 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -1636,7 +1636,7 @@ network_access = false # This should be ignored. /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; assert_eq!(resolution, SandboxPolicy::DangerFullAccess); @@ -1657,7 +1657,7 @@ network_access = true # This should be ignored. /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); @@ -1689,7 +1689,7 @@ trust_level = "trusted" /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; if cfg!(target_os = "windows") { @@ -1729,7 +1729,7 @@ exclude_slash_tmp = true /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, /*active_project*/ None, - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; if cfg!(target_os = "windows") { @@ -6318,7 +6318,7 @@ trust_level = "untrusted" /*profile_sandbox_mode*/ None, WindowsSandboxLevel::Disabled, Some(&active_project), - /*sandbox_policy_constraint*/ None, + /*permission_profile_constraint*/ None, ) .await; @@ -6339,8 +6339,8 @@ trust_level = "untrusted" } #[tokio::test] -async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults() --> anyhow::Result<()> { +async fn derive_sandbox_policy_falls_back_to_read_only_for_implicit_defaults() -> anyhow::Result<()> +{ let project_dir = TempDir::new()?; let project_path = project_dir.path().to_path_buf(); let project_key = project_path.to_string_lossy().to_string(); @@ -6356,14 +6356,14 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau let active_project = ProjectConfig { trust_level: Some(TrustLevel::Trusted), }; - let constrained = Constrained::new(SandboxPolicy::DangerFullAccess, |candidate| { - if matches!(candidate, SandboxPolicy::DangerFullAccess) { + let constrained = Constrained::new(PermissionProfile::read_only(), |candidate| { + if candidate == &PermissionProfile::read_only() { Ok(()) } else { Err(ConstraintError::InvalidValue { field_name: "sandbox_mode", candidate: format!("{candidate:?}"), - allowed: "[DangerFullAccess]".to_string(), + allowed: "[ReadOnly]".to_string(), requirement_source: RequirementSource::Unknown, }) } @@ -6379,7 +6379,7 @@ async fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defau ) .await; - assert_eq!(resolution, SandboxPolicy::DangerFullAccess); + assert_eq!(resolution, SandboxPolicy::new_read_only_policy()); Ok(()) } @@ -6401,18 +6401,29 @@ async fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallb let active_project = ProjectConfig { trust_level: Some(TrustLevel::Trusted), }; - let constrained = Constrained::new(SandboxPolicy::new_workspace_write_policy(), |candidate| { - if matches!(candidate, SandboxPolicy::WorkspaceWrite { .. }) { - Ok(()) - } else { - Err(ConstraintError::InvalidValue { - field_name: "sandbox_mode", - candidate: format!("{candidate:?}"), - allowed: "[WorkspaceWrite]".to_string(), - requirement_source: RequirementSource::Unknown, - }) - } - })?; + let constrained = Constrained::new( + PermissionProfile::from_legacy_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()), + |candidate| { + if matches!( + candidate, + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { entries, .. }, + .. + } if entries + .iter() + .any(|entry| entry.access.can_write()) + ) { + Ok(()) + } else { + Err(ConstraintError::InvalidValue { + field_name: "sandbox_mode", + candidate: format!("{candidate:?}"), + allowed: "[WorkspaceWrite]".to_string(), + requirement_source: RequirementSource::Unknown, + }) + } + }, + )?; let resolution = cfg .derive_sandbox_policy( diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 192abd3c9b..36bc6a1182 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -19,6 +19,7 @@ use codex_config::LoaderOverrides; use codex_config::McpServerIdentity; use codex_config::McpServerRequirement; use codex_config::ResidencyRequirement; +use codex_config::SandboxModeRequirement; use codex_config::Sourced; use codex_config::ThreadConfigLoader; use codex_config::config_toml::ConfigToml; @@ -30,6 +31,7 @@ use codex_config::config_toml::validate_model_providers; use codex_config::loader::load_config_layers_state; use codex_config::loader::project_trust_key; use codex_config::profile_toml::ConfigProfile; +use codex_config::sandbox_mode_requirement_for_permission_profile; use codex_config::types::ApprovalsReviewer; use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::DEFAULT_OTEL_ENVIRONMENT; @@ -295,25 +297,6 @@ impl Permissions { } } -fn constrained_permission_profile_from_sandbox_projection( - initial_value: PermissionProfile, - sandbox_constraint: Constrained, - cwd: AbsolutePathBuf, -) -> std::io::Result> { - Constrained::new(initial_value, move |candidate| { - let (file_system_sandbox_policy, network_sandbox_policy) = - candidate.to_runtime_permissions(); - let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - candidate, - &file_system_sandbox_policy, - network_sandbox_policy, - cwd.as_path(), - ); - sandbox_constraint.can_set(&sandbox_policy) - }) - .map_err(std::io::Error::from) -} - /// Configured thread persistence backend. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum ThreadStoreConfig { @@ -1734,7 +1717,7 @@ impl Config { let ConfigRequirements { approval_policy: mut constrained_approval_policy, approvals_reviewer: mut constrained_approvals_reviewer, - sandbox_policy: mut constrained_sandbox_policy, + permission_profile: mut constrained_permission_profile, web_search_mode: mut constrained_web_search_mode, feature_requirements, managed_hooks: _, @@ -1906,9 +1889,7 @@ impl Config { let ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) = if let Some(mut permission_profile) = permission_profile { let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); @@ -1935,7 +1916,7 @@ impl Config { } else { NetworkProxyConfig::default() }; - let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &permission_profile, &file_system_sandbox_policy, network_sandbox_policy, @@ -1952,19 +1933,11 @@ impl Config { &file_system_sandbox_policy, network_sandbox_policy, ); - sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &permission_profile, - &file_system_sandbox_policy, - network_sandbox_policy, - resolved_cwd.as_path(), - ); } ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) } else if profiles_are_active { let permissions = cfg.permissions.as_ref().ok_or_else(|| { @@ -1993,7 +1966,7 @@ impl Config { &file_system_sandbox_policy, network_sandbox_policy, ); - let mut sandbox_policy = compatibility_sandbox_policy_for_permission_profile( + let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &permission_profile, &file_system_sandbox_policy, network_sandbox_policy, @@ -2009,19 +1982,11 @@ impl Config { &file_system_sandbox_policy, network_sandbox_policy, ); - sandbox_policy = compatibility_sandbox_policy_for_permission_profile( - &permission_profile, - &file_system_sandbox_policy, - network_sandbox_policy, - resolved_cwd.as_path(), - ); } ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) } else { let configured_network_proxy_config = NetworkProxyConfig::default(); @@ -2031,7 +1996,7 @@ impl Config { config_profile.sandbox_mode, windows_sandbox_level, Some(&active_project), - Some(&constrained_sandbox_policy), + Some(&constrained_permission_profile), ) .await; if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy { @@ -2055,9 +2020,7 @@ impl Config { ( configured_network_proxy_config, permission_profile, - sandbox_policy, file_system_sandbox_policy, - network_sandbox_policy, ) }; let approval_policy_was_explicit = approval_policy_override.is_some() @@ -2356,8 +2319,7 @@ impl Config { .map(AbsolutePathBuf::to_path_buf) .or_else(|| resolve_sqlite_home_env(&resolved_cwd)) .unwrap_or_else(|| codex_home.to_path_buf()); - let original_sandbox_policy = sandbox_policy.clone(); - + let original_permission_profile = permission_profile.clone(); apply_requirement_constrained_value( "approval_policy", approval_policy, @@ -2371,17 +2333,22 @@ impl Config { && !filesystem_requirements.deny_read.is_empty() { let requirement_source = filesystem_requirements_source.clone(); - constrained_sandbox_policy + constrained_permission_profile .value - .add_validator(move |policy| match policy { - SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Ok(()), - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - Err(ConstraintError::InvalidValue { - field_name: "sandbox_mode", - candidate: policy.to_string(), - allowed: "[read-only, workspace-write]".to_string(), - requirement_source: requirement_source.clone(), - }) + .add_validator(move |permission_profile| { + let mode = sandbox_mode_requirement_for_permission_profile(permission_profile); + match mode { + SandboxModeRequirement::ReadOnly + | SandboxModeRequirement::WorkspaceWrite => Ok(()), + SandboxModeRequirement::DangerFullAccess + | SandboxModeRequirement::ExternalSandbox => { + Err(ConstraintError::InvalidValue { + field_name: "sandbox_mode", + candidate: format!("{mode:?}"), + allowed: "[read-only, workspace-write]".to_string(), + requirement_source: requirement_source.clone(), + }) + } } }) .map_err(std::io::Error::from)?; @@ -2399,9 +2366,9 @@ impl Config { &mut startup_warnings, )?; apply_requirement_constrained_value( - "sandbox_mode", - sandbox_policy, - &mut constrained_sandbox_policy, + "permission_profile", + permission_profile, + &mut constrained_permission_profile, &mut startup_warnings, )?; apply_requirement_constrained_value( @@ -2419,13 +2386,7 @@ impl Config { None => (None, None), }; let has_network_requirements = network_requirements.is_some(); - let network_permission_profile = if *constrained_sandbox_policy.get() - == original_sandbox_policy - { - permission_profile.clone() - } else { - PermissionProfile::from_legacy_sandbox_policy(constrained_sandbox_policy.get()) - }; + let network_permission_profile = constrained_permission_profile.get().clone(); let network = NetworkProxySpec::from_config_and_constraints( configured_network_proxy_config, network_requirements, @@ -2451,17 +2412,13 @@ impl Config { zsh_path.as_ref(), main_execve_wrapper_exe.as_ref(), ); - let effective_sandbox_policy = constrained_sandbox_policy.value.get().clone(); - let mut effective_file_system_sandbox_policy = - if effective_sandbox_policy == original_sandbox_policy { - file_system_sandbox_policy - } else { - FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries( - &effective_sandbox_policy, - resolved_cwd.as_path(), - &file_system_sandbox_policy, - ) - }; + let effective_permission_profile = constrained_permission_profile.value.get().clone(); + let (mut effective_file_system_sandbox_policy, effective_network_sandbox_policy) = + effective_permission_profile.to_runtime_permissions(); + if effective_permission_profile != original_permission_profile { + effective_file_system_sandbox_policy + .preserve_deny_read_restrictions_from(&file_system_sandbox_policy); + } if let Some(Sourced { value: filesystem_requirements, .. @@ -2474,28 +2431,15 @@ impl Config { } let effective_file_system_sandbox_policy = effective_file_system_sandbox_policy .with_additional_readable_roots(resolved_cwd.as_path(), &helper_readable_roots); - let effective_network_sandbox_policy = - if effective_sandbox_policy == original_sandbox_policy { - network_sandbox_policy - } else { - NetworkSandboxPolicy::from(&effective_sandbox_policy) - }; - let effective_enforcement = if effective_sandbox_policy == original_sandbox_policy { - permission_profile.enforcement() - } else { - SandboxEnforcement::from_legacy_sandbox_policy(&effective_sandbox_policy) - }; let effective_permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( - effective_enforcement, + effective_permission_profile.enforcement(), &effective_file_system_sandbox_policy, effective_network_sandbox_policy, ); - let constrained_permission_profile = - constrained_permission_profile_from_sandbox_projection( - effective_permission_profile, - constrained_sandbox_policy.value.clone(), - resolved_cwd.clone(), - )?; + constrained_permission_profile + .value + .set(effective_permission_profile) + .map_err(std::io::Error::from)?; let config = Self { model, service_tier, @@ -2508,7 +2452,7 @@ impl Config { startup_warnings, permissions: Permissions { approval_policy: constrained_approval_policy.value, - permission_profile: constrained_permission_profile, + permission_profile: constrained_permission_profile.value, network, allow_login_shell, shell_environment_policy, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 1c48c44918..dde85d9392 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -126,7 +126,7 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec> { requirement_lines.push(requirement_line( "allowed_sandbox_modes", value, - requirements.sandbox_policy.source.as_ref(), + requirements.permission_profile.source.as_ref(), )); } @@ -531,8 +531,8 @@ mod tests { use codex_config::WebSearchModeRequirement; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::WebSearchMode; + use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; - use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use ratatui::text::Line; use std::collections::BTreeMap; @@ -622,8 +622,8 @@ mod tests { Constrained::allow_any(ApprovalsReviewer::AutoReview), Some(RequirementSource::LegacyManagedConfigTomlFromMdm), ), - sandbox_policy: ConstrainedWithSource::new( - Constrained::allow_any(SandboxPolicy::new_read_only_policy()), + permission_profile: ConstrainedWithSource::new( + Constrained::allow_any(PermissionProfile::read_only()), Some(RequirementSource::SystemRequirementsToml { file: requirements_file.clone(), }), From 14009b1a3be9c5bdcdbb4deed90717143d79324d Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sun, 26 Apr 2026 19:27:31 -0700 Subject: [PATCH 4/4] permissions: derive legacy exec policies at boundaries --- codex-rs/core/src/exec.rs | 8 ++++---- codex-rs/core/src/sandboxing/mod.rs | 19 +++++++++---------- codex-rs/core/src/tasks/user_shell.rs | 2 -- .../tools/runtimes/shell/unix_escalation.rs | 6 ------ .../core/src/unified_exec/process_manager.rs | 3 ++- .../src/unified_exec/process_manager_tests.rs | 1 - codex-rs/sandboxing/src/manager.rs | 2 -- codex-rs/sandboxing/src/manager_tests.rs | 8 +++----- 8 files changed, 18 insertions(+), 31 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index aee6b14c77..c261fd3355 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -321,10 +321,11 @@ pub fn build_exec_request( exec_req.windows_sandbox_level, exec_req.network.is_some(), ); + let sandbox_policy = exec_req.compatibility_sandbox_policy(); exec_req.windows_sandbox_filesystem_overrides = if use_windows_elevated_backend { resolve_windows_elevated_filesystem_overrides( exec_req.sandbox, - &exec_req.sandbox_policy, + &sandbox_policy, &exec_req.file_system_sandbox_policy, exec_req.network_sandbox_policy, sandbox_cwd, @@ -333,7 +334,7 @@ pub fn build_exec_request( } else { resolve_windows_restricted_token_filesystem_overrides( exec_req.sandbox, - &exec_req.sandbox_policy, + &sandbox_policy, &exec_req.file_system_sandbox_policy, exec_req.network_sandbox_policy, sandbox_cwd, @@ -349,6 +350,7 @@ pub(crate) async fn execute_exec_request( stdout_stream: Option, after_spawn: Option>, ) -> Result { + let sandbox_policy = exec_request.compatibility_sandbox_policy(); let ExecRequest { command, cwd, @@ -362,8 +364,6 @@ pub(crate) async fn execute_exec_request( windows_sandbox_level, windows_sandbox_private_desktop, permission_profile: _, - sandbox_policy, - // TODO(mbolin): Use file_system_sandbox_policy instead of sandbox_policy. file_system_sandbox_policy: _, network_sandbox_policy, windows_sandbox_filesystem_overrides, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index e7b9925198..5070d8da3a 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -55,7 +55,6 @@ pub struct ExecRequest { pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, pub permission_profile: PermissionProfile, - pub sandbox_policy: SandboxPolicy, pub file_system_sandbox_policy: FileSystemSandboxPolicy, pub network_sandbox_policy: NetworkSandboxPolicy, pub(crate) windows_sandbox_filesystem_overrides: Option, @@ -80,12 +79,6 @@ impl ExecRequest { let windows_sandbox_policy_cwd = cwd.clone(); 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, - cwd.as_path(), - ); Self { command, cwd, @@ -99,7 +92,6 @@ impl ExecRequest { windows_sandbox_level, windows_sandbox_private_desktop, permission_profile, - sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, windows_sandbox_filesystem_overrides: None, @@ -107,6 +99,15 @@ impl ExecRequest { } } + pub(crate) fn compatibility_sandbox_policy(&self) -> SandboxPolicy { + compatibility_sandbox_policy_for_permission_profile( + &self.permission_profile, + &self.file_system_sandbox_policy, + self.network_sandbox_policy, + self.windows_sandbox_policy_cwd.as_path(), + ) + } + pub(crate) fn from_sandbox_exec_request( request: SandboxExecRequest, options: ExecOptions, @@ -121,7 +122,6 @@ impl ExecRequest { windows_sandbox_level, windows_sandbox_private_desktop, permission_profile, - sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, arg0, @@ -153,7 +153,6 @@ impl ExecRequest { windows_sandbox_level, windows_sandbox_private_desktop, permission_profile, - sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, windows_sandbox_filesystem_overrides: None, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 61e7bc15ae..444b0c3ec2 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -29,7 +29,6 @@ use codex_protocol::protocol::ExecCommandBeginEvent; use codex_protocol::protocol::ExecCommandEndEvent; use codex_protocol::protocol::ExecCommandSource; use codex_protocol::protocol::ExecCommandStatus; -use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::TurnStartedEvent; use codex_sandboxing::SandboxType; use codex_shell_command::parse_command::parse_command; @@ -195,7 +194,6 @@ pub(crate) async fn execute_user_shell_command( .permissions .windows_sandbox_private_desktop, permission_profile: permission_profile.clone(), - sandbox_policy: SandboxPolicy::DangerFullAccess, file_system_sandbox_policy: permission_profile.file_system_sandbox_policy(), network_sandbox_policy: permission_profile.network_sandbox_policy(), windows_sandbox_filesystem_overrides: None, 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 cdd309f61b..e61c78359d 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -40,7 +40,6 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GuardianCommandSource; use codex_protocol::protocol::NetworkPolicyRuleAction; use codex_protocol::protocol::ReviewDecision; -use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; @@ -143,7 +142,6 @@ pub(super) async fn try_run_zsh_fork( windows_sandbox_level, windows_sandbox_private_desktop: _windows_sandbox_private_desktop, permission_profile, - sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, windows_sandbox_filesystem_overrides: _windows_sandbox_filesystem_overrides, @@ -161,7 +159,6 @@ pub(super) async fn try_run_zsh_fork( command, cwd: sandbox_cwd, permission_profile, - sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, sandbox, @@ -260,7 +257,6 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( command: exec_request.command.clone(), cwd: exec_request.cwd.clone(), permission_profile: exec_request.permission_profile.clone(), - sandbox_policy: exec_request.sandbox_policy.clone(), file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(), network_sandbox_policy: exec_request.network_sandbox_policy, sandbox: exec_request.sandbox, @@ -742,7 +738,6 @@ struct CoreShellCommandExecutor { command: Vec, cwd: AbsolutePathBuf, permission_profile: PermissionProfile, - sandbox_policy: SandboxPolicy, file_system_sandbox_policy: FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, sandbox: SandboxType, @@ -796,7 +791,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, permission_profile: self.permission_profile.clone(), - sandbox_policy: self.sandbox_policy.clone(), file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), network_sandbox_policy: self.network_sandbox_policy, windows_sandbox_filesystem_overrides: None, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 24af1391fe..76d8021d3d 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -664,7 +664,8 @@ impl UnifiedExecProcessManager { #[cfg(target_os = "windows")] if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken { - let policy_json = serde_json::to_string(&request.sandbox_policy).map_err(|err| { + let sandbox_policy = request.compatibility_sandbox_policy(); + let policy_json = serde_json::to_string(&sandbox_policy).map_err(|err| { UnifiedExecError::create_process(format!( "failed to serialize Windows sandbox policy: {err}" )) diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index 78b0047951..18930afb61 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -110,7 +110,6 @@ fn exec_server_params_use_env_policy_overlay_contract() { windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, permission_profile, - sandbox_policy, file_system_sandbox_policy, network_sandbox_policy, windows_sandbox_filesystem_overrides: None, diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 5115edb6db..900130ee68 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -80,7 +80,6 @@ pub struct SandboxExecRequest { pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, pub permission_profile: PermissionProfile, - pub sandbox_policy: SandboxPolicy, pub file_system_sandbox_policy: FileSystemSandboxPolicy, pub network_sandbox_policy: NetworkSandboxPolicy, pub arg0: Option, @@ -262,7 +261,6 @@ impl SandboxManager { windows_sandbox_level, windows_sandbox_private_desktop, permission_profile: effective_permission_profile, - sandbox_policy: effective_policy, file_system_sandbox_policy: effective_file_system_policy, network_sandbox_policy: effective_network_policy, arg0: arg0_override, diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 7b8bc8579d..31f74b9c0a 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -15,8 +15,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; @@ -152,9 +150,9 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { .expect("transform"); assert_eq!( - exec_request.sandbox_policy, - SandboxPolicy::ExternalSandbox { - network_access: NetworkAccess::Enabled, + exec_request.permission_profile, + PermissionProfile::External { + network: NetworkSandboxPolicy::Enabled, } ); assert_eq!(