diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index fcaf4d229..00e45f979 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; @@ -150,6 +152,7 @@ use codex_protocol::permissions::FileSystemSandboxKind; use codex_rollout::StateDbHandle; use codex_terminal_detection::user_agent; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_approval_presets::builtin_permission_profile_for_active_permission_profile; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; use crossterm::event::KeyCode; @@ -327,7 +330,7 @@ fn default_exec_approval_decisions( struct AutoReviewMode { approval_policy: AskForApproval, approvals_reviewer: ApprovalsReviewer, - permission_profile: PermissionProfile, + active_permission_profile: ActivePermissionProfile, } /// Enabling the Auto-review experiment in the TUI should also switch the @@ -338,7 +341,17 @@ fn auto_review_mode() -> AutoReviewMode { AutoReviewMode { approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, - permission_profile: PermissionProfile::workspace_write(), + active_permission_profile: ActivePermissionProfile::new( + BUILT_IN_PERMISSION_PROFILE_WORKSPACE, + ), + } +} + +#[cfg(test)] +impl AutoReviewMode { + fn permission_profile(&self) -> PermissionProfile { + builtin_permission_profile_for_active_permission_profile(&self.active_permission_profile) + .expect("auto-review mode should use a built-in permission profile") } } diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 8d02e6f1e..4a410f813 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -105,24 +105,41 @@ impl App { true } - pub(super) fn try_set_permission_profile_on_config( + pub(super) fn try_set_builtin_active_permission_profile_on_config( &mut self, config: &mut Config, - permission_profile: PermissionProfile, + active_permission_profile: ActivePermissionProfile, user_message_prefix: &str, log_message: &str, - ) -> bool { + ) -> Option { + let Some(permission_profile) = + builtin_permission_profile_for_active_permission_profile(&active_permission_profile) + else { + tracing::warn!( + id = %active_permission_profile.id, + "{log_message}: unsupported active permission profile" + ); + self.chat_widget.add_error_message(format!( + "{user_message_prefix}: unsupported active permission profile `{}`", + active_permission_profile.id + )); + return None; + }; + if let Err(err) = config .permissions - .set_permission_profile(permission_profile) + .set_permission_profile_from_session_snapshot( + permission_profile.clone(), + Some(active_permission_profile), + ) { tracing::warn!(error = %err, "{log_message}"); self.chat_widget .add_error_message(format!("{user_message_prefix}: {err}")); - return false; + return None; } - true + Some(permission_profile) } pub(super) async fn update_feature_flags(&mut self, updates: Vec<(Feature, bool)>) { @@ -149,6 +166,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,14 +258,16 @@ impl App { ) { continue; } - if !self.try_set_permission_profile_on_config( - &mut feature_config, - auto_review_preset.permission_profile.clone(), - "Failed to enable Auto-review", - "failed to set auto-review permission profile on staged config", - ) { + let Some(permission_profile) = self + .try_set_builtin_active_permission_profile_on_config( + &mut feature_config, + auto_review_preset.active_permission_profile.clone(), + "Failed to enable Auto-review", + "failed to set auto-review permission profile on staged config", + ) + else { continue; - } + }; feature_edits.extend([ ConfigEdit::SetPath { segments: scoped_segments("approval_policy"), @@ -259,7 +279,9 @@ impl App { }, ]); approval_policy_override = Some(auto_review_preset.approval_policy); - permission_profile_override = Some(auto_review_preset.permission_profile.clone()); + permission_profile_override = Some(permission_profile); + 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 +327,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 +358,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 +385,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 811c24cc4..16e25e4d1 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,10 @@ 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::UpdateActivePermissionProfile( + preset.active_permission_profile.clone(), + )); let _ = mode; self.chat_widget.add_plain_history_lines(vec![ Line::from(vec!["• ".dim(), "Sandbox ready".into()]), @@ -1400,25 +1401,30 @@ impl App { self.sync_active_thread_permission_settings_to_cached_session() .await; } - AppEvent::UpdatePermissionProfile(permission_profile) => { + AppEvent::UpdateActivePermissionProfile(active_permission_profile) => { + let mut config = self.config.clone(); + let Some(permission_profile) = self + .try_set_builtin_active_permission_profile_on_config( + &mut config, + active_permission_profile.clone(), + "Failed to set permission profile", + "failed to set active permission profile on app config", + ) + else { + return Ok(AppRunControl::Continue); + }; #[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( - &mut config, - permission_profile, - "Failed to set permission profile", - "failed to set permission profile on app config", - ) { - return Ok(AppRunControl::Continue); - } 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, + Some(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 c107bc777..a925b4d48 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -1640,7 +1640,18 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< .config_ref() .permissions .permission_profile(), - &auto_review.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, @@ -1649,7 +1660,7 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result< assert_eq!(app.runtime_approval_policy_override, None); assert_eq!( app.runtime_permission_profile_override, - Some(auto_review.permission_profile.clone()) + Some(auto_review.permission_profile()) ); assert_eq!( op_rx.try_recv(), @@ -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, @@ -1719,7 +1730,10 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor app.chat_widget .set_approval_policy(AskForApproval::OnRequest); app.chat_widget - .set_permission_profile(PermissionProfile::workspace_write())?; + .set_permission_profile_from_session_snapshot( + PermissionProfile::workspace_write(), + /*active_profile*/ None, + )?; app.update_feature_flags(vec![(Feature::GuardianApproval, false)]) .await; @@ -1747,7 +1761,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, @@ -1817,7 +1831,7 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review .config_ref() .permissions .permission_profile(), - &auto_review.permission_profile + &auto_review.permission_profile() ); assert_eq!( op_rx.try_recv(), @@ -1825,7 +1839,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 +1896,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 +1955,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 +2042,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, @@ -3139,7 +3153,10 @@ async fn side_fork_config_inherits_parent_thread_runtime_settings() { app.chat_widget .set_approval_policy(AskForApproval::OnRequest); app.chat_widget - .set_permission_profile(parent_permission_profile.clone()) + .set_permission_profile_from_session_snapshot( + parent_permission_profile.clone(), + /*active_profile*/ None, + ) .expect("test permission profile should be accepted"); app.chat_widget .set_approvals_reviewer(ApprovalsReviewer::AutoReview); diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 4f858f10e..d270bb53c 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/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index a8d421563..eb5632566 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -131,6 +131,7 @@ mod tests { use codex_app_server_protocol::PermissionProfileFileSystemPermissions; use codex_app_server_protocol::PermissionProfileNetworkPermissions; use codex_config::types::ApprovalsReviewer; + use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; use codex_protocol::models::PermissionProfile; use pretty_assertions::assert_eq; use std::path::PathBuf; @@ -197,9 +198,14 @@ mod tests { codex_config::Constrained::allow_any(AskForApproval::OnRequest.to_core()); app.config.approvals_reviewer = ApprovalsReviewer::AutoReview; let expected_permission_profile = PermissionProfile::workspace_write(); + let expected_active_permission_profile = + ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE); app.chat_widget.handle_thread_session(main_session.clone()); app.chat_widget - .set_permission_profile(expected_permission_profile.clone()) + .set_permission_profile_from_session_snapshot( + expected_permission_profile.clone(), + Some(expected_active_permission_profile.clone()), + ) .expect("set widget permission profile"); app.config .permissions @@ -213,6 +219,7 @@ mod tests { approval_policy: AskForApproval::OnRequest, approvals_reviewer: ApprovalsReviewer::AutoReview, permission_profile: expected_permission_profile, + active_permission_profile: Some(expected_active_permission_profile), ..main_session }; assert_eq!( diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 89fd2600f..0b6484d01 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 ab9055197..57ec37174 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -43,7 +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::PermissionProfile; +use codex_protocol::models::ActivePermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_realtime_webrtc::RealtimeWebrtcEvent; use codex_realtime_webrtc::RealtimeWebrtcSessionHandle; @@ -742,8 +742,8 @@ pub(crate) enum AppEvent { /// Update the current approval policy in the running app and widget. UpdateAskForApprovalPolicy(AskForApproval), - /// Update the current permission profile in the running app and widget. - UpdatePermissionProfile(PermissionProfile), + /// Update the current built-in active permission profile in the running app and widget. + UpdateActivePermissionProfile(ActivePermissionProfile), /// 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 5c7d0194e..207c54756 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 2ead61f3d..2fd376f13 100644 --- a/codex-rs/tui/src/chatwidget/input_submission.rs +++ b/codex-rs/tui/src/chatwidget/input_submission.rs @@ -336,12 +336,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 6f2e1c4db..a82f3a6cd 100644 --- a/codex-rs/tui/src/chatwidget/permission_popups.rs +++ b/codex-rs/tui/src/chatwidget/permission_popups.rs @@ -124,7 +124,7 @@ impl ChatWidget { } else { Self::approval_preset_actions( preset_approval, - preset.permission_profile.clone(), + preset.active_permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -134,7 +134,7 @@ impl ChatWidget { { Self::approval_preset_actions( preset_approval, - preset.permission_profile.clone(), + preset.active_permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -142,7 +142,7 @@ impl ChatWidget { } else { Self::approval_preset_actions( preset_approval, - preset.permission_profile.clone(), + preset.active_permission_profile.clone(), base_name.clone(), ApprovalsReviewer::User, ) @@ -180,7 +180,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, ), @@ -308,17 +308,16 @@ impl ChatWidget { pub(super) fn approval_preset_actions( approval: AskForApproval, - permission_profile: PermissionProfile, + active_permission_profile: ActivePermissionProfile, label: String, approvals_reviewer: ApprovalsReviewer, ) -> Vec { vec![Box::new(move |tx| { - let permission_profile_clone = permission_profile.clone(); tx.send(AppEvent::CodexOp(AppCommand::override_turn_context( /*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 +327,9 @@ impl ChatWidget { /*personality*/ None, ))); tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); - tx.send(AppEvent::UpdatePermissionProfile(permission_profile_clone)); + tx.send(AppEvent::UpdateActivePermissionProfile( + active_permission_profile.clone(), + )); tx.send(AppEvent::UpdateApprovalsReviewer(approvals_reviewer)); tx.send(AppEvent::InsertHistoryCell(Box::new( history_cell::new_info_event( @@ -385,7 +386,6 @@ impl ChatWidget { ) { let selected_name = preset.label.to_string(); let approval = AskForApproval::from(preset.approval); - let permission_profile = preset.permission_profile; let mut header_children: Vec> = Vec::new(); let title_line = Line::from("Enable full access?").bold(); let info_line = Line::from(vec![ @@ -402,7 +402,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, ); @@ -412,7 +412,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 6b5414536..7eaca908c 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 1cdb52f22..a60af0a69 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 419070ef9..a2125c3f4 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -17,13 +17,15 @@ impl ChatWidget { } } - /// Set the permission profile in the widget's config copy. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] - pub(crate) fn set_permission_profile( + pub(crate) fn set_permission_profile_from_session_snapshot( &mut self, profile: PermissionProfile, + active_profile: Option, ) -> ConstraintResult<()> { - self.config.permissions.set_permission_profile(profile)?; + 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 d718e70d7..2dcfefb73 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 f86b896e5..e0dda9eb5 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 3e8635c46..381c5430a 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -94,7 +94,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(); @@ -120,6 +120,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, @@ -130,8 +131,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(), @@ -150,17 +151,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(); @@ -180,7 +185,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(), @@ -197,13 +202,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/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 1d1b19fdf..7dca12844 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -296,8 +296,11 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { assert_eq!(&chat.config_ref().cwd, &expected_cwd); let updated_profile = PermissionProfile::workspace_write(); - chat.set_permission_profile(updated_profile.clone()) - .expect("set permission profile"); + chat.set_permission_profile_from_session_snapshot( + updated_profile.clone(), + /*active_profile*/ None, + ) + .expect("set permission profile"); assert_eq!( chat.config_ref().permissions.permission_profile(), &updated_profile, diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 4c6604127..9928676e4 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,20 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context personality: None, } ); + + let active_permission_profile_update = std::iter::from_fn(|| rx.try_recv().ok()) + .find_map(|event| match event { + AppEvent::UpdateActivePermissionProfile(active_permission_profile) => { + Some(active_permission_profile) + } + _ => None, + }) + .expect("expected UpdateActivePermissionProfile event"); + + assert_eq!( + active_permission_profile_update, + 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 401e2a787..d8ff59da4 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -42,10 +42,10 @@ impl ChatWidget { extra_count: usize, failed_scan: bool, ) { - let (approval, permission_profile) = match &preset { + let (approval, 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), }; @@ -110,10 +110,12 @@ impl ChatWidget { tx.send(AppEvent::SkipNextWorldWritableScan); })); } - if let (Some(approval), Some(permission_profile)) = (approval, permission_profile.clone()) { + if let (Some(approval), Some(active_permission_profile)) = + (approval, 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 +126,12 @@ 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(active_permission_profile)) = + (approval, 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 b25495050..c7ce9e578 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,7 +52,26 @@ 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, }, ] } + +/// Return the concrete profile for one of the built-in active profile ids. +pub fn builtin_permission_profile_for_active_permission_profile( + active_permission_profile: &ActivePermissionProfile, +) -> Option { + if active_permission_profile.extends.is_some() { + return None; + } + + match active_permission_profile.id.as_str() { + BUILT_IN_PERMISSION_PROFILE_READ_ONLY => Some(PermissionProfile::read_only()), + BUILT_IN_PERMISSION_PROFILE_WORKSPACE => Some(PermissionProfile::workspace_write()), + BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS => Some(PermissionProfile::Disabled), + _ => None, + } +}