diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index dd019946f2..5117d7cf82 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1747,12 +1747,8 @@ impl App { AppEvent::OpenAllModelsPopup { models } => { self.chat_widget.open_all_models_popup(models); } - AppEvent::OpenFullAccessConfirmation { - preset, - return_to_permissions, - } => { - self.chat_widget - .open_full_access_confirmation(preset, return_to_permissions); + AppEvent::OpenFullAccessConfirmation { preset } => { + self.chat_widget.open_full_access_confirmation(preset); } AppEvent::OpenWorldWritableWarningConfirmation { preset, diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 1c9e8ce88e..a95844c8bb 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -16,11 +16,11 @@ use codex_core::protocol::RateLimitSnapshot; use codex_file_search::FileMatch; use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelPreset; -use codex_utils_approval_presets::ApprovalPreset; use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::StatusLineItem; use crate::history_cell::HistoryCell; +use crate::permissions::PermissionsPreset; use codex_core::features::Feature; use codex_core::protocol::AskForApproval; @@ -171,8 +171,7 @@ pub(crate) enum AppEvent { /// Open the confirmation prompt before enabling full access mode. OpenFullAccessConfirmation { - preset: ApprovalPreset, - return_to_permissions: bool, + preset: PermissionsPreset, }, /// Open the Windows world-writable directories warning. @@ -181,7 +180,7 @@ pub(crate) enum AppEvent { /// policy change and only acknowledges/dismisses the warning. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] OpenWorldWritableWarningConfirmation { - preset: Option, + preset: Option, /// Up to 3 sample world-writable directories to display in the warning. sample_paths: Vec, /// If there are more than `sample_paths`, this carries the remaining count. @@ -193,25 +192,25 @@ pub(crate) enum AppEvent { /// Prompt to enable the Windows sandbox feature before using Agent mode. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] OpenWindowsSandboxEnablePrompt { - preset: ApprovalPreset, + preset: PermissionsPreset, }, /// Open the Windows sandbox fallback prompt after declining or failing elevation. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] OpenWindowsSandboxFallbackPrompt { - preset: ApprovalPreset, + preset: PermissionsPreset, }, /// Begin the elevated Windows sandbox setup flow. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] BeginWindowsSandboxElevatedSetup { - preset: ApprovalPreset, + preset: PermissionsPreset, }, /// Begin the non-elevated Windows sandbox setup flow. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] BeginWindowsSandboxLegacySetup { - preset: ApprovalPreset, + preset: PermissionsPreset, }, /// Begin a non-elevated grant of read access for an additional directory. @@ -230,7 +229,7 @@ pub(crate) enum AppEvent { /// Enable the Windows sandbox feature and switch to Agent mode. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] EnableWindowsSandboxForAgentMode { - preset: ApprovalPreset, + preset: PermissionsPreset, mode: WindowsSandboxEnableMode, }, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0a6da6a5ae..a57a9abc55 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -39,6 +39,11 @@ use std::time::Instant; use crate::bottom_pane::StatusLineItem; use crate::bottom_pane::StatusLineSetupView; +use crate::permissions::PermissionsPreset; +#[cfg(target_os = "windows")] +use crate::permissions::builtin_permissions_presets; +use crate::permissions::visible_permissions_options; +use crate::permissions::windows_degraded_sandbox_enabled; use crate::status::RateLimitWindowDisplay; use crate::status::format_directory_display; use crate::status::format_tokens_compact; @@ -242,8 +247,6 @@ use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::plan_tool::UpdatePlanArgs; -use codex_utils_approval_presets::ApprovalPreset; -use codex_utils_approval_presets::builtin_approval_presets; use strum::IntoEnumIterator; const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally"; @@ -3338,7 +3341,7 @@ impl ChatWidget { return; } - let Some(preset) = builtin_approval_presets() + let Some(preset) = builtin_permissions_presets() .into_iter() .find(|preset| preset.id == "auto") else { @@ -5335,125 +5338,14 @@ impl ChatWidget { /// Open a popup to choose the permissions mode (approval policy + sandbox policy). 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 mut items: Vec = Vec::new(); - let presets: Vec = builtin_approval_presets(); - - #[cfg(target_os = "windows")] - let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); - #[cfg(target_os = "windows")] - let windows_degraded_sandbox_enabled = - matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); - #[cfg(not(target_os = "windows"))] - let windows_degraded_sandbox_enabled = false; + let items: Vec = visible_permissions_options(&self.config); + let windows_degraded_sandbox_enabled = windows_degraded_sandbox_enabled(&self.config); let show_elevate_sandbox_hint = codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED && windows_degraded_sandbox_enabled - && presets.iter().any(|preset| preset.id == "auto"); - - for preset in presets.into_iter() { - if !include_read_only && preset.id == "read-only" { - continue; - } - let is_current = - Self::preset_matches_current(current_approval, current_sandbox, &preset); - let name = if preset.id == "auto" && windows_degraded_sandbox_enabled { - "Default (non-admin sandbox)".to_string() - } else { - preset.label.to_string() - }; - let description = Some(preset.description.replace(" (Identical to Agent mode)", "")); - let disabled_reason = match self - .config - .permissions - .approval_policy - .can_set(&preset.approval) - { - Ok(()) => None, - Err(err) => Some(err.to_string()), - }; - let requires_confirmation = preset.id == "full-access" - && !self - .config - .notices - .hide_full_access_warning - .unwrap_or(false); - let actions: Vec = if requires_confirmation { - let preset_clone = preset.clone(); - vec![Box::new(move |tx| { - tx.send(AppEvent::OpenFullAccessConfirmation { - preset: preset_clone.clone(), - return_to_permissions: !include_read_only, - }); - })] - } else if preset.id == "auto" { - #[cfg(target_os = "windows")] - { - if WindowsSandboxLevel::from_config(&self.config) - == WindowsSandboxLevel::Disabled - { - let preset_clone = preset.clone(); - if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && codex_core::windows_sandbox::sandbox_setup_is_complete( - self.config.codex_home.as_path(), - ) - { - vec![Box::new(move |tx| { - tx.send(AppEvent::EnableWindowsSandboxForAgentMode { - preset: preset_clone.clone(), - mode: WindowsSandboxEnableMode::Elevated, - }); - })] - } else { - vec![Box::new(move |tx| { - tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { - preset: preset_clone.clone(), - }); - })] - } - } else if let Some((sample_paths, extra_count, failed_scan)) = - self.world_writable_warning_details() - { - let preset_clone = preset.clone(); - vec![Box::new(move |tx| { - tx.send(AppEvent::OpenWorldWritableWarningConfirmation { - preset: Some(preset_clone.clone()), - sample_paths: sample_paths.clone(), - extra_count, - failed_scan, - }); - })] - } else { - Self::approval_preset_actions( - preset.approval, - preset.sandbox.clone(), - name.clone(), - ) - } - } - #[cfg(not(target_os = "windows"))] - { - Self::approval_preset_actions( - preset.approval, - preset.sandbox.clone(), - name.clone(), - ) - } - } else { - Self::approval_preset_actions(preset.approval, preset.sandbox.clone(), name.clone()) - }; - items.push(SelectionItem { - name, - description, - is_current, - actions, - dismiss_on_select: true, - disabled_reason, - ..Default::default() - }); - } + && items + .iter() + .any(|item| item.name == "Default (non-admin sandbox)"); let footer_note = show_elevate_sandbox_hint.then(|| { vec![ @@ -5519,14 +5411,6 @@ impl ChatWidget { })] } - fn preset_matches_current( - current_approval: AskForApproval, - current_sandbox: &SandboxPolicy, - preset: &ApprovalPreset, - ) -> bool { - current_approval == preset.approval && *current_sandbox == preset.sandbox - } - #[cfg(target_os = "windows")] pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec, usize, bool)> { if self @@ -5557,14 +5441,7 @@ impl ChatWidget { None } - pub(crate) fn open_full_access_confirmation( - &mut self, - preset: ApprovalPreset, - return_to_permissions: bool, - ) { - let selected_name = preset.label.to_string(); - let approval = preset.approval; - let sandbox = preset.sandbox; + pub(crate) fn open_full_access_confirmation(&mut self, preset: PermissionsPreset) { let mut header_children: Vec> = Vec::new(); let title_line = Line::from("Enable full access?").bold(); let info_line = Line::from(vec![ @@ -5579,25 +5456,27 @@ impl ChatWidget { )); let header = ColumnRenderable::with(header_children); - let mut accept_actions = - Self::approval_preset_actions(approval, sandbox.clone(), selected_name.clone()); + let mut accept_actions = Self::approval_preset_actions( + preset.approval, + preset.sandbox.clone(), + preset.label.to_string(), + ); accept_actions.push(Box::new(|tx| { tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true)); })); - let mut accept_and_remember_actions = - Self::approval_preset_actions(approval, sandbox, selected_name); + let mut accept_and_remember_actions = Self::approval_preset_actions( + preset.approval, + preset.sandbox.clone(), + preset.label.to_string(), + ); accept_and_remember_actions.push(Box::new(|tx| { tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true)); tx.send(AppEvent::PersistFullAccessWarningAcknowledged); })); let deny_actions: Vec = vec![Box::new(move |tx| { - if return_to_permissions { - tx.send(AppEvent::OpenPermissionsPopup); - } else { - tx.send(AppEvent::OpenApprovalsPopup); - } + tx.send(AppEvent::OpenPermissionsPopup); })]; let items = vec![ @@ -5635,7 +5514,7 @@ impl ChatWidget { #[cfg(target_os = "windows")] pub(crate) fn open_world_writable_warning_confirmation( &mut self, - preset: Option, + preset: Option, sample_paths: Vec, extra_count: usize, failed_scan: bool, @@ -5744,7 +5623,7 @@ impl ChatWidget { #[cfg(not(target_os = "windows"))] pub(crate) fn open_world_writable_warning_confirmation( &mut self, - _preset: Option, + _preset: Option, _sample_paths: Vec, _extra_count: usize, _failed_scan: bool, @@ -5752,7 +5631,7 @@ impl ChatWidget { } #[cfg(target_os = "windows")] - pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) { + pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: PermissionsPreset) { use ratatui_macros::line; if !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED { @@ -5864,10 +5743,10 @@ impl ChatWidget { } #[cfg(not(target_os = "windows"))] - pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {} + pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: PermissionsPreset) {} #[cfg(target_os = "windows")] - pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, preset: ApprovalPreset) { + pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, preset: PermissionsPreset) { use ratatui_macros::line; let mut lines = Vec::new(); @@ -5943,13 +5822,13 @@ impl ChatWidget { } #[cfg(not(target_os = "windows"))] - pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, _preset: ApprovalPreset) {} + pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, _preset: PermissionsPreset) {} #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) { if show_now && WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled - && let Some(preset) = builtin_approval_presets() + && let Some(preset) = builtin_permissions_presets() .into_iter() .find(|preset| preset.id == "auto") { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap index e6b067b4e5..291ab22928 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup.snap @@ -1,13 +1,12 @@ --- source: tui/src/chatwidget/tests.rs -assertion_line: 3092 expression: popup --- Update Model Permissions › 1. Default Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or - edit other files. + edit other files. (Identical to Agent mode) 2. Full Access Codex can edit files outside this workspace and access the internet without asking for approval. Exercise caution when using. diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index bfae9581b2..02bcff82d9 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -12,6 +12,7 @@ use crate::bottom_pane::FeedbackAudience; use crate::bottom_pane::LocalImageAttachment; use crate::bottom_pane::MentionBinding; use crate::history_cell::UserHistoryCell; +use crate::permissions::builtin_permissions_presets; use crate::test_backend::VT100Backend; use crate::tui::FrameRequester; use assert_matches::assert_matches; @@ -91,7 +92,6 @@ use codex_protocol::protocol::SkillScope; use codex_protocol::user_input::TextElement; use codex_protocol::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_approval_presets::builtin_approval_presets; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -5064,10 +5064,11 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { #[tokio::test] async fn preset_matching_requires_exact_workspace_write_settings() { - let preset = builtin_approval_presets() + let preset = builtin_permissions_presets() .into_iter() .find(|p| p.id == "auto") .expect("auto preset exists"); + let mut config = test_config().await; let current_sandbox = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from("C:\\extra").unwrap()], read_only_access: Default::default(), @@ -5075,13 +5076,28 @@ async fn preset_matching_requires_exact_workspace_write_settings() { exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }; + config + .permissions + .approval_policy + .set(AskForApproval::OnRequest) + .unwrap(); + config + .permissions + .sandbox_policy + .set(current_sandbox) + .unwrap(); assert!( - !ChatWidget::preset_matches_current(AskForApproval::OnRequest, ¤t_sandbox, &preset), + !preset.to_selection_item(&config).is_current, "WorkspaceWrite with extra roots should not match the Default preset" ); + config + .permissions + .approval_policy + .set(AskForApproval::Never) + .unwrap(); assert!( - !ChatWidget::preset_matches_current(AskForApproval::Never, ¤t_sandbox, &preset), + !preset.to_selection_item(&config).is_current, "approval mismatch should prevent matching the preset" ); } @@ -5090,11 +5106,11 @@ async fn preset_matching_requires_exact_workspace_write_settings() { async fn full_access_confirmation_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - let preset = builtin_approval_presets() + let preset = builtin_permissions_presets() .into_iter() - .find(|preset| preset.id == "full-access") + .find(|selection_item| selection_item.id == "full-access") .expect("full access preset"); - chat.open_full_access_confirmation(preset, false); + chat.open_full_access_confirmation(preset); let popup = render_bottom_popup(&chat, 80); assert_snapshot!("full_access_confirmation_popup", popup); @@ -5105,7 +5121,7 @@ async fn full_access_confirmation_popup_snapshot() { async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - let preset = builtin_approval_presets() + let preset = builtin_permissions_presets() .into_iter() .find(|preset| preset.id == "auto") .expect("auto preset"); @@ -5624,11 +5640,8 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation() AppEvent::InsertHistoryCell(cell) => { cells_before_confirmation.push(cell.display_lines(80)); } - AppEvent::OpenFullAccessConfirmation { - preset, - return_to_permissions, - } => { - open_confirmation_event = Some((preset, return_to_permissions)); + AppEvent::OpenFullAccessConfirmation { preset } => { + open_confirmation_event = Some(preset); } _ => {} } @@ -5639,9 +5652,8 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation() "did not expect history cell before confirming full access" ); } - let (preset, return_to_permissions) = - open_confirmation_event.expect("expected full access confirmation event"); - chat.open_full_access_confirmation(preset, return_to_permissions); + let preset = open_confirmation_event.expect("expected full access confirmation event"); + chat.open_full_access_confirmation(preset.clone()); let popup = render_bottom_popup(&chat, 80); assert!( diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index f41421a98d..25b82a7103 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -91,6 +91,7 @@ mod notifications; pub mod onboarding; mod oss_selection; mod pager_overlay; +mod permissions; pub mod public_widgets; mod render; mod resume_picker; diff --git a/codex-rs/tui/src/permissions/mod.rs b/codex-rs/tui/src/permissions/mod.rs new file mode 100644 index 0000000000..dcb04edd11 --- /dev/null +++ b/codex-rs/tui/src/permissions/mod.rs @@ -0,0 +1,243 @@ +use crate::app_event::AppEvent; +#[cfg(target_os = "windows")] +use crate::app_event::WindowsSandboxEnableMode; +use crate::app_event_sender::AppEventSender; +use crate::bottom_pane::SelectionAction; +use crate::bottom_pane::SelectionItem; +use crate::history_cell; +use codex_core::config::Config; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::Op; +use codex_core::protocol::SandboxPolicy; +use codex_core::protocol_config_types::WindowsSandboxLevel; + +/// A simple preset pairing an approval policy with a sandbox policy. +#[derive(Debug, Clone)] +pub struct PermissionsPreset { + /// Stable identifier for the preset. + pub id: &'static str, + /// Display label shown in UIs. + pub label: &'static str, + /// Short human description shown next to the label in UIs. + pub description: &'static str, + /// Approval policy to apply. + pub approval: AskForApproval, + /// Sandbox policy to apply. + pub sandbox: SandboxPolicy, +} + +/// Built-in list of approval presets that pair approval and sandbox policy. +pub fn builtin_permissions_presets() -> Vec { + vec![ + PermissionsPreset { + id: "read-only", + 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, + sandbox: SandboxPolicy::new_read_only_policy(), + }, + PermissionsPreset { + id: "auto", + 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, + sandbox: SandboxPolicy::new_workspace_write_policy(), + }, + PermissionsPreset { + id: "full-access", + 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, + sandbox: SandboxPolicy::DangerFullAccess, + }, + ] +} + +pub(crate) fn visible_permissions_options(config: &Config) -> Vec { + builtin_permissions_presets() + .into_iter() + .filter(|preset| preset.is_visible(config)) + .map(|preset| preset.to_selection_item(config)) + .collect() +} + +impl PermissionsPreset { + pub(crate) fn is_visible(&self, config: &Config) -> bool { + match self.id { + "read-only" => cfg!(target_os = "windows"), + "auto" => { + !cfg!(target_os = "windows") + || codex_core::windows_sandbox::windows_sandbox_level_from_config(config) + == WindowsSandboxLevel::Disabled + } + "full-access" => true, + _ => false, + } + } + + pub(crate) fn to_selection_item(&self, config: &Config) -> SelectionItem { + let name = if self.id == "auto" && windows_degraded_sandbox_enabled(config) { + "Default (non-admin sandbox)".to_string() + } else { + self.label.to_string() + }; + + SelectionItem { + name, + description: Some(self.description.to_string()), + is_current: self.is_current(config), + actions: self.actions(config), + dismiss_on_select: true, + disabled_reason: self.disabled_reason(config), + ..Default::default() + } + } + + fn is_current(&self, config: &Config) -> bool { + self.approval == config.permissions.approval_policy.value() + && self.sandbox == *config.permissions.sandbox_policy.get() + } + + fn disabled_reason(&self, config: &Config) -> Option { + let disabled_sandbox_reason = match config.permissions.sandbox_policy.can_set(&self.sandbox) + { + Ok(()) => None, + Err(err) => Some(err.to_string()), + }; + if disabled_sandbox_reason.is_some() { + return disabled_sandbox_reason; + } + + let disabled_approval_reason = + match config.permissions.approval_policy.can_set(&self.approval) { + Ok(()) => None, + Err(err) => Some(err.to_string()), + }; + if disabled_approval_reason.is_some() { + return disabled_approval_reason; + } + + None + } + + pub(crate) fn actions(&self, config: &Config) -> Vec { + let requires_full_access_confirmation = + self.id == "full-access" && !config.notices.hide_full_access_warning.unwrap_or(false); + if requires_full_access_confirmation { + let preset = self.clone(); + return vec![Box::new(move |tx: &AppEventSender| { + tx.send(AppEvent::OpenFullAccessConfirmation { + preset: preset.clone(), + }); + })]; + } + + #[cfg(target_os = "windows")] + { + if let Some(actions) = windows_permissions_actions(self, config) { + return actions; + } + } + + let approval = self.approval; + let sandbox = self.sandbox.clone(); + let label = self.label.to_string(); + vec![Box::new(move |tx: &AppEventSender| { + let sandbox_clone = sandbox.clone(); + tx.send(AppEvent::CodexOp(Op::OverrideTurnContext { + cwd: None, + approval_policy: Some(approval), + sandbox_policy: Some(sandbox_clone.clone()), + windows_sandbox_level: None, + model: None, + effort: None, + summary: None, + collaboration_mode: None, + personality: None, + })); + tx.send(AppEvent::UpdateAskForApprovalPolicy(approval)); + tx.send(AppEvent::UpdateSandboxPolicy(sandbox_clone)); + tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_info_event(format!("Permissions updated to {label}"), None), + ))); + })] + } +} + +/// Handle windows-specific actions for auto preset. Returns Some when it should take precedence over the approval preset actions. +#[cfg(target_os = "windows")] +fn windows_permissions_actions( + preset: &PermissionsPreset, + config: &Config, +) -> Option> { + if preset.id != "auto" { + return None; + } + + if codex_core::windows_sandbox::windows_sandbox_level_from_config(config) + == WindowsSandboxLevel::Disabled + { + let preset_clone = preset.clone(); + if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && codex_core::windows_sandbox::sandbox_setup_is_complete(config.codex_home.as_path()) + { + Some(vec![Box::new(move |tx| { + tx.send(AppEvent::EnableWindowsSandboxForAgentMode { + preset: preset_clone.clone(), + mode: WindowsSandboxEnableMode::Elevated, + }); + })]) + } else { + Some(vec![Box::new(move |tx| { + tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { + preset: preset_clone.clone(), + }); + })]) + } + } else if let Some((sample_paths, extra_count, failed_scan)) = + world_writable_warning_details(config) + { + let preset_clone = preset.clone(); + Some(vec![Box::new(move |tx| { + tx.send(AppEvent::OpenWorldWritableWarningConfirmation { + preset: Some(preset_clone.clone()), + sample_paths: sample_paths.clone(), + extra_count, + failed_scan, + }); + })]) + } else { + None + } +} + +#[cfg(target_os = "windows")] +pub(crate) fn windows_degraded_sandbox_enabled(config: &Config) -> bool { + let windows_sandbox_level = + codex_core::windows_sandbox::windows_sandbox_level_from_config(config); + matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); +} + +#[cfg(target_os = "windows")] +fn world_writable_warning_details(config: &Config) -> Option<(Vec, usize, bool)> { + if config.notices.hide_world_writable_warning.unwrap_or(false) { + return None; + } + let cwd = config.cwd.clone(); + let env_map: std::collections::HashMap = std::env::vars().collect(); + match codex_windows_sandbox::apply_world_writable_scan_and_denies( + config.codex_home.as_path(), + cwd.as_path(), + &env_map, + config.permissions.sandbox_policy.get(), + Some(config.codex_home.as_path()), + ) { + Ok(_) => None, + Err(_) => Some((Vec::new(), 0, true)), + } +} + +#[cfg(not(target_os = "windows"))] +pub(crate) fn windows_degraded_sandbox_enabled(_config: &Config) -> bool { + false +}