From aa5fa65f85ee6f11dd7df5ce7f7960fc2f88ca5b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 15 May 2026 13:08:38 -0700 Subject: [PATCH] tui: pass active permission profiles through app commands --- codex-rs/tui/src/app.rs | 6 ++++ codex-rs/tui/src/app/config_persistence.rs | 23 +++++++++---- codex-rs/tui/src/app/event_dispatch.rs | 26 ++++++++++----- codex-rs/tui/src/app/tests.rs | 23 +++++++++---- codex-rs/tui/src/app/thread_routing.rs | 28 +++++++--------- codex-rs/tui/src/app_command.rs | 14 ++++---- codex-rs/tui/src/app_event.rs | 6 +++- codex-rs/tui/src/chatwidget.rs | 1 + .../tui/src/chatwidget/input_submission.rs | 4 +-- .../tui/src/chatwidget/permission_popups.rs | 14 ++++++-- codex-rs/tui/src/chatwidget/rate_limits.rs | 2 +- codex-rs/tui/src/chatwidget/service_tiers.rs | 2 +- codex-rs/tui/src/chatwidget/settings.rs | 13 +++++++- .../tui/src/chatwidget/settings_popups.rs | 2 +- codex-rs/tui/src/chatwidget/tests.rs | 2 ++ .../chatwidget/tests/composer_submission.rs | 32 +++++++++++-------- .../tui/src/chatwidget/tests/permissions.rs | 24 +++++++++++++- .../src/chatwidget/windows_sandbox_prompts.rs | 17 +++++++--- codex-rs/utils/approval-presets/src/lib.rs | 15 +++++++++ 19 files changed, 184 insertions(+), 70 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index fcaf4d2290..5f34eb9045 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -140,6 +140,8 @@ use codex_protocol::ThreadId; use codex_protocol::config_types::Personality; #[cfg(target_os = "windows")] use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::ActivePermissionProfile; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ModelAvailabilityNux; use codex_protocol::openai_models::ModelPreset; @@ -327,6 +329,7 @@ fn default_exec_approval_decisions( struct AutoReviewMode { approval_policy: AskForApproval, approvals_reviewer: ApprovalsReviewer, + active_permission_profile: ActivePermissionProfile, permission_profile: PermissionProfile, } @@ -338,6 +341,9 @@ fn auto_review_mode() -> AutoReviewMode { AutoReviewMode { approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, + active_permission_profile: ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + ), permission_profile: PermissionProfile::workspace_write(), } } diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 8d02e6f1e4..a04bb74111 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -105,16 +105,20 @@ impl App { true } - pub(super) fn try_set_permission_profile_on_config( + pub(super) fn try_set_permission_profile_snapshot_on_config( &mut self, config: &mut Config, permission_profile: PermissionProfile, + active_permission_profile: Option, user_message_prefix: &str, log_message: &str, ) -> bool { if let Err(err) = config .permissions - .set_permission_profile(permission_profile) + .set_permission_profile_from_session_snapshot( + permission_profile, + active_permission_profile, + ) { tracing::warn!(error = %err, "{log_message}"); self.chat_widget @@ -149,6 +153,7 @@ impl App { let mut approval_policy_override = None; let mut approvals_reviewer_override = None; let mut permission_profile_override = None; + let mut active_permission_profile_override = None; let mut feature_updates_to_apply = Vec::with_capacity(updates.len()); // Auto-Review owns `approvals_reviewer`, but disabling the feature // from inside a profile should not silently clear a value configured at @@ -240,9 +245,10 @@ impl App { ) { continue; } - if !self.try_set_permission_profile_on_config( + if !self.try_set_permission_profile_snapshot_on_config( &mut feature_config, auto_review_preset.permission_profile.clone(), + Some(auto_review_preset.active_permission_profile.clone()), "Failed to enable Auto-review", "failed to set auto-review permission profile on staged config", ) { @@ -260,6 +266,8 @@ impl App { ]); approval_policy_override = Some(auto_review_preset.approval_policy); permission_profile_override = Some(auto_review_preset.permission_profile.clone()); + active_permission_profile_override = + Some(auto_review_preset.active_permission_profile.clone()); } next_config = feature_config; feature_updates_to_apply.push((feature, effective_enabled)); @@ -305,7 +313,10 @@ impl App { if let Some(permission_profile) = permission_profile_override_value.as_ref() && let Err(err) = self .chat_widget - .set_permission_profile(permission_profile.clone()) + .set_permission_profile_from_session_snapshot( + permission_profile.clone(), + active_permission_profile_override.clone(), + ) { tracing::error!( error = %err, @@ -333,7 +344,7 @@ impl App { /*cwd*/ None, approval_policy_override, approvals_reviewer_override, - permission_profile_override, + active_permission_profile_override, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, @@ -360,7 +371,7 @@ impl App { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*permission_profile*/ None, + /*active_permission_profile*/ None, #[cfg(target_os = "windows")] Some(windows_sandbox_level), /*model*/ None, diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 811c24cc4d..c373b2ab37 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1111,7 +1111,7 @@ impl App { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*permission_profile*/ None, + /*active_permission_profile*/ None, #[cfg(target_os = "windows")] Some(windows_sandbox_level), /*model*/ None, @@ -1136,7 +1136,7 @@ impl App { /*cwd*/ None, Some(AskForApproval::from(preset.approval)), Some(self.config.approvals_reviewer), - Some(preset.permission_profile.clone()), + Some(preset.active_permission_profile.clone()), #[cfg(target_os = "windows")] Some(windows_sandbox_level), /*model*/ None, @@ -1150,9 +1150,12 @@ impl App { self.app_event_tx.send(AppEvent::UpdateAskForApprovalPolicy( AskForApproval::from(preset.approval), )); - self.app_event_tx.send(AppEvent::UpdatePermissionProfile( - preset.permission_profile.clone(), - )); + self.app_event_tx.send(AppEvent::UpdatePermissionProfile { + permission_profile: preset.permission_profile.clone(), + active_permission_profile: Some( + preset.active_permission_profile.clone(), + ), + }); let _ = mode; self.chat_widget.add_plain_history_lines(vec![ Line::from(vec!["• ".dim(), "Sandbox ready".into()]), @@ -1400,16 +1403,20 @@ impl App { self.sync_active_thread_permission_settings_to_cached_session() .await; } - AppEvent::UpdatePermissionProfile(permission_profile) => { + AppEvent::UpdatePermissionProfile { + permission_profile, + active_permission_profile, + } => { #[cfg(target_os = "windows")] let permission_profile_is_managed_restricted = managed_filesystem_sandbox_is_restricted(&permission_profile); let permission_profile_for_chat = permission_profile.clone(); let mut config = self.config.clone(); - if !self.try_set_permission_profile_on_config( + if !self.try_set_permission_profile_snapshot_on_config( &mut config, permission_profile, + active_permission_profile.clone(), "Failed to set permission profile", "failed to set permission profile on app config", ) { @@ -1418,7 +1425,10 @@ impl App { self.config = config; if let Err(err) = self .chat_widget - .set_permission_profile(permission_profile_for_chat) + .set_permission_profile_from_session_snapshot( + permission_profile_for_chat, + active_permission_profile, + ) { tracing::warn!(%err, "failed to set permission profile on chat config"); self.chat_widget diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index bbc1802d17..c9c8d24f82 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -1642,6 +1642,17 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< .permission_profile(), &auto_review.permission_profile ); + assert_eq!( + app.config.permissions.active_permission_profile(), + Some(auto_review.active_permission_profile.clone()) + ); + assert_eq!( + app.chat_widget + .config_ref() + .permissions + .active_permission_profile(), + Some(auto_review.active_permission_profile.clone()) + ); assert_eq!( app.chat_widget.config_ref().approvals_reviewer, auto_review.approvals_reviewer @@ -1657,7 +1668,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< cwd: None, approval_policy: Some(auto_review.approval_policy), approvals_reviewer: Some(auto_review.approvals_reviewer), - permission_profile: Some(auto_review.permission_profile.clone()), + active_permission_profile: Some(auto_review.active_permission_profile.clone()), windows_sandbox_level: None, model: None, effort: None, @@ -1747,7 +1758,7 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor cwd: None, approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), - permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1825,7 +1836,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review cwd: None, approval_policy: Some(auto_review.approval_policy), approvals_reviewer: Some(auto_review.approvals_reviewer), - permission_profile: Some(auto_review.permission_profile.clone()), + active_permission_profile: Some(auto_review.active_permission_profile.clone()), windows_sandbox_level: None, model: None, effort: None, @@ -1882,7 +1893,7 @@ async fn update_feature_flags_disabling_guardian_clears_manual_review_policy_wit cwd: None, approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), - permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -1941,7 +1952,7 @@ async fn update_feature_flags_enabling_guardian_in_profile_sets_profile_auto_rev cwd: None, approval_policy: Some(auto_review.approval_policy), approvals_reviewer: Some(auto_review.approvals_reviewer), - permission_profile: Some(auto_review.permission_profile.clone()), + active_permission_profile: Some(auto_review.active_permission_profile.clone()), windows_sandbox_level: None, model: None, effort: None, @@ -2028,7 +2039,7 @@ guardian_approval = true cwd: None, approval_policy: None, approvals_reviewer: Some(ApprovalsReviewer::User), - permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 4f858f10e6..d270bb53ce 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -510,7 +510,7 @@ impl App { cwd, approval_policy, approvals_reviewer, - permission_profile, + active_permission_profile, model, effort, summary, @@ -590,7 +590,7 @@ impl App { approvals_reviewer.unwrap_or(config.approvals_reviewer); let permissions_override = Self::turn_permissions_override_from_config( config, - permission_profile, + active_permission_profile.as_ref(), self.runtime_permission_profile_override.as_ref(), ); app_server @@ -698,16 +698,14 @@ impl App { fn turn_permissions_override_from_config( config: &Config, - permission_profile: &PermissionProfile, + active_permission_profile: Option<&ActivePermissionProfile>, runtime_permission_profile_override: Option<&PermissionProfile>, ) -> TurnPermissionsOverride { - let effective_permission_profile = config.permissions.effective_permission_profile(); - if &effective_permission_profile == permission_profile - && let Some(active_permission_profile) = config.permissions.active_permission_profile() - { - return TurnPermissionsOverride::ActiveProfile(active_permission_profile); + if let Some(active_permission_profile) = active_permission_profile { + return TurnPermissionsOverride::ActiveProfile(active_permission_profile.clone()); } + let effective_permission_profile = config.permissions.effective_permission_profile(); let runtime_permission_profile_override = runtime_permission_profile_override.map(|profile| { profile @@ -718,9 +716,9 @@ impl App { }); if runtime_permission_profile_override .as_ref() - .is_some_and(|profile| profile == permission_profile) + .is_some_and(|profile| profile == &effective_permission_profile) { - return TurnPermissionsOverride::LegacySandbox(permission_profile.clone()); + return TurnPermissionsOverride::LegacySandbox(effective_permission_profile); } TurnPermissionsOverride::Preserve @@ -1506,12 +1504,12 @@ mod tests { #[tokio::test] async fn turn_permissions_use_active_profile_when_available() { let config = config_with_workspace_profile().await; - let permission_profile = config.permissions.effective_permission_profile(); + let active_permission_profile = config.permissions.active_permission_profile(); assert_eq!( App::turn_permissions_override_from_config( &config, - &permission_profile, + active_permission_profile.as_ref(), /*runtime_permission_profile_override*/ None, ), TurnPermissionsOverride::ActiveProfile(ActivePermissionProfile::new( @@ -1527,12 +1525,10 @@ mod tests { .permissions .set_permission_profile(PermissionProfile::read_only()) .expect("read-only profile should be allowed"); - let permission_profile = config.permissions.effective_permission_profile(); assert_eq!( App::turn_permissions_override_from_config( - &config, - &permission_profile, + &config, /*active_permission_profile*/ None, /*runtime_permission_profile_override*/ None, ), TurnPermissionsOverride::Preserve @@ -1552,7 +1548,7 @@ mod tests { assert_eq!( App::turn_permissions_override_from_config( &config, - &effective_permission_profile, + /*active_permission_profile*/ None, Some(&permission_profile), ), TurnPermissionsOverride::LegacySandbox(effective_permission_profile) diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 89fd2600f8..0b6484d015 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -16,7 +16,7 @@ use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::ActivePermissionProfile; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::request_permissions::RequestPermissionsResponse; use serde::Serialize; @@ -41,7 +41,7 @@ pub(crate) enum AppCommand { cwd: PathBuf, approval_policy: AskForApproval, approvals_reviewer: Option, - permission_profile: PermissionProfile, + active_permission_profile: Option, model: String, effort: Option, summary: Option, @@ -54,7 +54,7 @@ pub(crate) enum AppCommand { cwd: Option, approval_policy: Option, approvals_reviewer: Option, - permission_profile: Option, + active_permission_profile: Option, windows_sandbox_level: Option, model: Option, effort: Option>, @@ -142,7 +142,7 @@ impl AppCommand { items: Vec, cwd: PathBuf, approval_policy: AskForApproval, - permission_profile: PermissionProfile, + active_permission_profile: Option, model: String, effort: Option, summary: Option, @@ -156,7 +156,7 @@ impl AppCommand { cwd, approval_policy, approvals_reviewer: None, - permission_profile, + active_permission_profile, model, effort, summary, @@ -172,7 +172,7 @@ impl AppCommand { cwd: Option, approval_policy: Option, approvals_reviewer: Option, - permission_profile: Option, + active_permission_profile: Option, windows_sandbox_level: Option, model: Option, effort: Option>, @@ -185,7 +185,7 @@ impl AppCommand { cwd, approval_policy, approvals_reviewer, - permission_profile, + active_permission_profile, windows_sandbox_level, model, effort, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index ab9055197b..1317e48f77 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -43,6 +43,7 @@ use codex_features::Feature; use codex_plugin::PluginCapabilitySummary; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::Personality; +use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_realtime_webrtc::RealtimeWebrtcEvent; @@ -743,7 +744,10 @@ pub(crate) enum AppEvent { UpdateAskForApprovalPolicy(AskForApproval), /// Update the current permission profile in the running app and widget. - UpdatePermissionProfile(PermissionProfile), + UpdatePermissionProfile { + permission_profile: PermissionProfile, + active_permission_profile: Option, + }, /// Update the current approvals reviewer in the running app and widget. UpdateApprovalsReviewer(ApprovalsReviewer), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 5c7d0194ef..207c547562 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -442,6 +442,7 @@ use crate::workspace_command::WorkspaceCommandRunner; use chrono::Local; use codex_app_server_protocol::AskForApproval; use codex_file_search::FileMatch; +use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelPreset; diff --git a/codex-rs/tui/src/chatwidget/input_submission.rs b/codex-rs/tui/src/chatwidget/input_submission.rs index 6e9860bb77..acf1962170 100644 --- a/codex-rs/tui/src/chatwidget/input_submission.rs +++ b/codex-rs/tui/src/chatwidget/input_submission.rs @@ -334,12 +334,12 @@ impl ChatWidget { None if self.config.notices.fast_default_opt_out == Some(true) => Some(None), None => None, }; - let permission_profile = self.config.permissions.effective_permission_profile(); + let active_permission_profile = self.config.permissions.active_permission_profile(); let op = AppCommand::user_turn( items, self.config.cwd.to_path_buf(), AskForApproval::from(self.config.permissions.approval_policy.value()), - permission_profile, + active_permission_profile, effective_mode.model().to_string(), effective_mode.reasoning_effort(), /*summary*/ None, diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs index 6f2e1c4dbd..bf1c0160c5 100644 --- a/codex-rs/tui/src/chatwidget/permission_popups.rs +++ b/codex-rs/tui/src/chatwidget/permission_popups.rs @@ -125,6 +125,7 @@ impl ChatWidget { Self::approval_preset_actions( preset_approval, preset.permission_profile.clone(), + preset.active_permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -135,6 +136,7 @@ impl ChatWidget { Self::approval_preset_actions( preset_approval, preset.permission_profile.clone(), + preset.active_permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -143,6 +145,7 @@ impl ChatWidget { Self::approval_preset_actions( preset_approval, preset.permission_profile.clone(), + preset.active_permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -181,6 +184,7 @@ impl ChatWidget { actions: Self::approval_preset_actions( preset_approval, preset.permission_profile.clone(), + preset.active_permission_profile.clone(), "Auto-review".to_string(), ApprovalsReviewer::AutoReview, ), @@ -309,6 +313,7 @@ impl ChatWidget { pub(super) fn approval_preset_actions( approval: AskForApproval, permission_profile: PermissionProfile, + active_permission_profile: ActivePermissionProfile, label: String, approvals_reviewer: ApprovalsReviewer, ) -> Vec { @@ -318,7 +323,7 @@ impl ChatWidget { /*cwd*/ None, Some(approval), Some(approvals_reviewer), - Some(permission_profile_clone.clone()), + Some(active_permission_profile.clone()), /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, @@ -328,7 +333,10 @@ impl ChatWidget { /*personality*/ None, ))); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); - tx.send(AppEvent::UpdatePermissionProfile(permission_profile_clone)); + tx.send(AppEvent::UpdatePermissionProfile { + permission_profile: permission_profile_clone, + active_permission_profile: Some(active_permission_profile.clone()), + }); tx.send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer)); tx.send(AppEvent::InsertHistoryCell(Box::new( history_cell::new_info_event( @@ -403,6 +411,7 @@ impl ChatWidget { let mut accept_actions = Self::approval_preset_actions( approval, permission_profile.clone(), + preset.active_permission_profile.clone(), selected_name.clone(), ApprovalsReviewer::User, ); @@ -413,6 +422,7 @@ impl ChatWidget { let mut accept_and_remember_actions = Self::approval_preset_actions( approval, permission_profile, + preset.active_permission_profile, selected_name, ApprovalsReviewer::User, ); diff --git a/codex-rs/tui/src/chatwidget/rate_limits.rs b/codex-rs/tui/src/chatwidget/rate_limits.rs index 6b5414536d..7eaca908c6 100644 --- a/codex-rs/tui/src/chatwidget/rate_limits.rs +++ b/codex-rs/tui/src/chatwidget/rate_limits.rs @@ -285,7 +285,7 @@ impl ChatWidget { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*permission_profile*/ None, + /*active_permission_profile*/ None, /*windows_sandbox_level*/ None, Some(switch_model_for_events.clone()), Some(Some(default_effort)), diff --git a/codex-rs/tui/src/chatwidget/service_tiers.rs b/codex-rs/tui/src/chatwidget/service_tiers.rs index 1cdb52f224..a60af0a69e 100644 --- a/codex-rs/tui/src/chatwidget/service_tiers.rs +++ b/codex-rs/tui/src/chatwidget/service_tiers.rs @@ -107,7 +107,7 @@ impl ChatWidget { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*permission_profile*/ None, + /*active_permission_profile*/ None, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index 419070ef9e..cf6ac6db21 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -23,7 +23,18 @@ impl ChatWidget { &mut self, profile: PermissionProfile, ) -> ConstraintResult<()> { - self.config.permissions.set_permission_profile(profile)?; + self.set_permission_profile_from_session_snapshot(profile, None) + } + + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + pub(crate) fn set_permission_profile_from_session_snapshot( + &mut self, + profile: PermissionProfile, + active_profile: Option, + ) -> ConstraintResult<()> { + self.config + .permissions + .set_permission_profile_from_session_snapshot(profile, active_profile)?; self.refresh_status_surfaces(); Ok(()) } diff --git a/codex-rs/tui/src/chatwidget/settings_popups.rs b/codex-rs/tui/src/chatwidget/settings_popups.rs index d718e70d79..2dcfefb73a 100644 --- a/codex-rs/tui/src/chatwidget/settings_popups.rs +++ b/codex-rs/tui/src/chatwidget/settings_popups.rs @@ -53,7 +53,7 @@ impl ChatWidget { /*cwd*/ None, /*approval_policy*/ None, /*approvals_reviewer*/ None, - /*permission_profile*/ None, + /*active_permission_profile*/ None, /*windows_sandbox_level*/ None, /*model*/ None, /*effort*/ None, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f86b896e5b..e0dda9eb5c 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -149,6 +149,8 @@ pub(super) use codex_protocol::config_types::CollaborationMode; pub(super) use codex_protocol::config_types::ModeKind; pub(super) use codex_protocol::config_types::Personality; pub(super) use codex_protocol::config_types::ServiceTier; +pub(super) use codex_protocol::models::ActivePermissionProfile; +pub(super) use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; pub(super) use codex_protocol::models::FileSystemPermissions; pub(super) use codex_protocol::models::MessagePhase; pub(super) use codex_protocol::models::NetworkPermissions; diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index 96477c8d38..3a226114af 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -93,7 +93,7 @@ async fn submission_preserves_text_elements_and_local_images() { } #[tokio::test] -async fn submission_includes_configured_permission_profile() { +async fn submission_includes_configured_active_permission_profile() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let thread_id = ThreadId::new(); @@ -119,6 +119,7 @@ async fn submission_includes_configured_permission_profile() { }, } .into(); + let expected_active_permission_profile = ActivePermissionProfile::new("custom"); let configured = crate::session_state::ThreadSessionState { thread_id, forked_from_id: None, @@ -129,8 +130,8 @@ async fn submission_includes_configured_permission_profile() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: expected_permission_profile.clone(), - active_permission_profile: None, + permission_profile: expected_permission_profile, + active_permission_profile: Some(expected_active_permission_profile.clone()), cwd: test_path_buf("/home/user/project").abs(), runtime_workspace_roots: Vec::new(), instruction_source_paths: Vec::new(), @@ -149,17 +150,21 @@ async fn submission_includes_configured_permission_profile() { ); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - let permission_profile = match next_submit_op(&mut op_rx) { + let active_permission_profile = match next_submit_op(&mut op_rx) { Op::UserTurn { - permission_profile, .. - } => permission_profile, + active_permission_profile, + .. + } => active_permission_profile, other => panic!("expected Op::UserTurn, got {other:?}"), }; - assert_eq!(permission_profile, expected_permission_profile); + assert_eq!( + active_permission_profile, + Some(expected_active_permission_profile) + ); } #[tokio::test] -async fn submission_keeps_profile_when_legacy_projection_is_external() { +async fn submission_omits_active_permission_profile_for_legacy_snapshot() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let thread_id = ThreadId::new(); @@ -179,7 +184,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() { service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, - permission_profile: expected_permission_profile.clone(), + permission_profile: expected_permission_profile, active_permission_profile: None, cwd: test_path_buf("/home/user/project").abs(), runtime_workspace_roots: Vec::new(), @@ -196,13 +201,14 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() { .set_composer_text("submit".to_string(), Vec::new(), Vec::new()); chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - let permission_profile = match next_submit_op(&mut op_rx) { + let active_permission_profile = match next_submit_op(&mut op_rx) { Op::UserTurn { - permission_profile, .. - } => permission_profile, + active_permission_profile, + .. + } => active_permission_profile, other => panic!("expected Op::UserTurn, got {other:?}"), }; - assert_eq!(permission_profile, expected_permission_profile); + assert_eq!(active_permission_profile, None); } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 4c66041271..7f136f135f 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -742,7 +742,9 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context cwd: None, approval_policy: Some(AskForApproval::OnRequest), approvals_reviewer: Some(ApprovalsReviewer::AutoReview), - permission_profile: Some(PermissionProfile::workspace_write()), + active_permission_profile: Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + )), windows_sandbox_level: None, model: None, effort: None, @@ -752,6 +754,26 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context personality: None, } ); + + let permission_profile_update = std::iter::from_fn(|| rx.try_recv().ok()) + .find_map(|event| match event { + AppEvent::UpdatePermissionProfile { + permission_profile, + active_permission_profile, + } => Some((permission_profile, active_permission_profile)), + _ => None, + }) + .expect("expected UpdatePermissionProfile event"); + + assert_eq!( + permission_profile_update, + ( + PermissionProfile::workspace_write(), + Some(ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + )) + ) + ); } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index 401e2a787d..3b71d467bf 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -42,12 +42,13 @@ impl ChatWidget { extra_count: usize, failed_scan: bool, ) { - let (approval, permission_profile) = match &preset { + let (approval, permission_profile, active_permission_profile) = match &preset { Some(p) => ( Some(AskForApproval::from(p.approval)), Some(p.permission_profile.clone()), + Some(p.active_permission_profile.clone()), ), - None => (None, None), + None => (None, None, None), }; let mut header_children: Vec> = Vec::new(); let describe_profile = |profile: &PermissionProfile| { @@ -110,10 +111,15 @@ impl ChatWidget { tx.send(AppEvent::SkipNextWorldWritableScan); })); } - if let (Some(approval), Some(permission_profile)) = (approval, permission_profile.clone()) { + if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) = ( + approval, + permission_profile.clone(), + active_permission_profile.clone(), + ) { accept_actions.extend(Self::approval_preset_actions( approval, permission_profile, + active_permission_profile, mode_label.to_string(), ApprovalsReviewer::User, )); @@ -124,10 +130,13 @@ impl ChatWidget { tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true)); tx.send(AppEvent::PersistWorldWritableWarningAcknowledged); })); - if let (Some(approval), Some(permission_profile)) = (approval, permission_profile) { + if let (Some(approval), Some(permission_profile), Some(active_permission_profile)) = + (approval, permission_profile, active_permission_profile) + { accept_and_remember_actions.extend(Self::approval_preset_actions( approval, permission_profile, + active_permission_profile, mode_label.to_string(), ApprovalsReviewer::User, )); diff --git a/codex-rs/utils/approval-presets/src/lib.rs b/codex-rs/utils/approval-presets/src/lib.rs index b254950500..2a3fc92419 100644 --- a/codex-rs/utils/approval-presets/src/lib.rs +++ b/codex-rs/utils/approval-presets/src/lib.rs @@ -1,3 +1,7 @@ +use codex_protocol::models::ActivePermissionProfile; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; +use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; @@ -12,6 +16,8 @@ pub struct ApprovalPreset { pub description: &'static str, /// Approval policy to apply. pub approval: AskForApproval, + /// Built-in permission profile selected by this preset. + pub active_permission_profile: ActivePermissionProfile, /// Permission profile to apply. pub permission_profile: PermissionProfile, } @@ -26,6 +32,9 @@ pub fn builtin_approval_presets() -> Vec { label: "Read Only", description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.", approval: AskForApproval::OnRequest, + active_permission_profile: ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_READ_ONLY, + ), permission_profile: PermissionProfile::read_only(), }, ApprovalPreset { @@ -33,6 +42,9 @@ pub fn builtin_approval_presets() -> Vec { label: "Default", description: "Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or edit other files. (Identical to Agent mode)", approval: AskForApproval::OnRequest, + active_permission_profile: ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + ), permission_profile: PermissionProfile::workspace_write(), }, ApprovalPreset { @@ -40,6 +52,9 @@ pub fn builtin_approval_presets() -> Vec { label: "Full Access", description: "Codex can edit files outside this workspace and access the internet without asking for approval. Exercise caution when using.", approval: AskForApproval::Never, + active_permission_profile: ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS, + ), permission_profile: PermissionProfile::Disabled, }, ]