Codex Handbook
tui/src/chatwidget/permission_popups.rs 501 lines
//! Permission and approval popup flows for `ChatWidget`.//!//! This module owns the generic permission pickers and confirmation surfaces;//! Windows-specific sandbox prompting lives beside it in//! `windows_sandbox_prompts`.use super::*;impl ChatWidget {    /// Open the permissions popup.    pub(crate) fn open_approvals_popup(&mut self) {        self.open_permissions_popup();    }    /// Open a popup to choose the permissions mode.    pub(crate) fn open_permissions_popup(&mut self) {        if self.config.explicit_permission_profile_mode {            self.open_permission_profiles_popup();            return;        }        let include_read_only = cfg!(target_os = "windows");        let current_approval =            AskForApproval::from(self.config.permissions.approval_policy.value());        let current_permission_profile = self.config.permissions.permission_profile().clone();        let guardian_approval_enabled = self.config.features.enabled(Feature::GuardianApproval);        let current_review_policy = self.config.approvals_reviewer;        let mut items: Vec<SelectionItem> = Vec::new();        let presets: Vec<ApprovalPreset> = builtin_approval_presets();        #[cfg(target_os = "windows")]        let windows_sandbox_level = crate::windows_sandbox::level_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 show_elevate_sandbox_hint =            windows_degraded_sandbox_enabled && presets.iter().any(|preset| preset.id == "auto");        let guardian_disabled_reason = |enabled: bool| {            let mut next_features = self.config.features.get().clone();            next_features.set_enabled(Feature::GuardianApproval, enabled);            self.config                .features                .can_set(&next_features)                .err()                .map(|err| err.to_string())        };        for preset in presets.into_iter() {            if !include_read_only && preset.id == "read-only" {                continue;            }            let base_name = if preset.id == "auto" && windows_degraded_sandbox_enabled {                format!("{ASK_FOR_APPROVAL_LABEL} (non-admin sandbox)")            } else if preset.id == "auto" {                ASK_FOR_APPROVAL_LABEL.to_string()            } else {                preset.label.to_string()            };            let base_description =                Some(preset.description.replace(" (Identical to Agent mode)", ""));            let approval_disabled_reason = match self                .config                .permissions                .approval_policy                .can_set(&preset.approval)            {                Ok(()) => None,                Err(err) => Some(err.to_string()),            };            let default_disabled_reason = approval_disabled_reason                .clone()                .or_else(|| guardian_disabled_reason(false));            let default_actions = self.permission_mode_actions(                &preset,                base_name.clone(),                ApprovalsReviewer::User,                /*profile_selection*/ None,                /*return_to_permissions*/ !include_read_only,            );            if preset.id == "auto" {                items.push(SelectionItem {                    name: base_name.clone(),                    description: base_description.clone(),                    is_current: current_review_policy == ApprovalsReviewer::User                        && Self::preset_matches_current(                            current_approval,                            &current_permission_profile,                            self.config.cwd.as_path(),                            &preset,                        ),                    actions: default_actions,                    dismiss_on_select: true,                    disabled_reason: default_disabled_reason,                    ..Default::default()                });                if guardian_approval_enabled {                    items.push(SelectionItem {                        name: APPROVE_FOR_ME_LABEL.to_string(),                        description: Some(AUTO_REVIEW_DESCRIPTION.to_string()),                        is_current: current_review_policy == ApprovalsReviewer::AutoReview                            && Self::preset_matches_current(                                current_approval,                                &current_permission_profile,                                self.config.cwd.as_path(),                                &preset,                            ),                        actions: self.permission_mode_actions(                            &preset,                            APPROVE_FOR_ME_LABEL.to_string(),                            ApprovalsReviewer::AutoReview,                            /*profile_selection*/ None,                            /*return_to_permissions*/ !include_read_only,                        ),                        dismiss_on_select: true,                        disabled_reason: approval_disabled_reason                            .or_else(|| guardian_disabled_reason(true)),                        ..Default::default()                    });                }            } else {                items.push(SelectionItem {                    name: base_name,                    description: base_description,                    is_current: Self::preset_matches_current(                        current_approval,                        &current_permission_profile,                        self.config.cwd.as_path(),                        &preset,                    ),                    actions: default_actions,                    dismiss_on_select: true,                    disabled_reason: default_disabled_reason,                    ..Default::default()                });            }        }        let footer_note = show_elevate_sandbox_hint.then(|| {            vec![                "The non-admin sandbox protects your files and prevents network access under most circumstances. However, it carries greater risk if prompt injected. To upgrade to the default sandbox, run ".dim(),                "/setup-default-sandbox".cyan(),                ".".dim(),            ]            .into()        });        self.bottom_pane.show_selection_view(SelectionViewParams {            title: Some("Update Model Permissions".to_string()),            footer_note,            footer_hint: Some(standard_popup_hint_line()),            items,            header: Box::new(()),            ..Default::default()        });    }    pub(crate) fn open_auto_review_denials_popup(&mut self) {        if self.review.recent_auto_review_denials.is_empty() {            self.add_info_message(                "No recent auto-review denials in this thread.".to_string(),                Some("Denials are recorded after auto-review rejects an action.".to_string()),            );            return;        }        let Some(thread_id) = self.thread_id() else {            self.add_error_message("That thread is no longer available.".to_string());            return;        };        let mut items = vec![SelectionItem {            name: "Command".to_string(),            description: Some("Rationale".to_string()),            is_disabled: true,            search_value: Some(String::new()),            ..Default::default()        }];        items.extend(            self.review                .recent_auto_review_denials                .entries()                .map(|event| {                    let id = event.id.clone();                    let summary = auto_review_denials::action_summary(&event.action);                    let rationale = event                        .rationale                        .as_deref()                        .unwrap_or("Auto-review did not include a rationale.");                    SelectionItem {                        name: summary.clone(),                        description: Some(rationale.to_string()),                        selected_description: Some(rationale.to_string()),                        search_value: Some(format!("{summary} {rationale}")),                        actions: vec![Box::new(move |tx| {                            tx.send(AppEvent::ApproveRecentAutoReviewDenial {                                thread_id,                                id: id.clone(),                            });                        })],                        dismiss_on_select: true,                        ..Default::default()                    }                }),        );        self.bottom_pane.show_selection_view(SelectionViewParams {            title: Some("Auto-review Denials".to_string()),            subtitle: Some("Select a denied action to approve.".to_string()),            footer_hint: Some(standard_popup_hint_line()),            items,            is_searchable: true,            col_width_mode: ColumnWidthMode::AutoAllRows,            ..Default::default()        });        self.request_redraw();    }    pub(crate) fn approve_recent_auto_review_denial(&mut self, thread_id: ThreadId, id: String) {        let Some(event) = self.review.recent_auto_review_denials.take(&id) else {            self.add_error_message("That auto-review denial is no longer available.".to_string());            return;        };        self.app_event_tx.send(AppEvent::SubmitThreadOp {            thread_id,            op: AppCommand::approve_guardian_denied_action(event),        });        self.add_info_message(            "Approval recorded for one retry of the selected auto-review denial.".to_string(),            Some(                "The model will see the approval context; the retry still goes through auto-review."                    .to_string(),            ),        );    }    pub(super) fn approval_preset_actions(        approval: AskForApproval,        permission_profile: PermissionProfile,        active_permission_profile: ActivePermissionProfile,        label: String,        approvals_reviewer: ApprovalsReviewer,    ) -> Vec<SelectionAction> {        vec![Box::new(move |tx| {            tx.send(AppEvent::CodexOp(AppCommand::override_turn_context(                /*cwd*/ None,                Some(approval),                Some(approvals_reviewer),                Some(permission_profile.clone()),                Some(active_permission_profile.clone()),                /*windows_sandbox_level*/ None,                /*model*/ None,                /*effort*/ None,                /*summary*/ None,                /*service_tier*/ None,                /*collaboration_mode*/ None,                /*personality*/ None,            )));            tx.send(AppEvent::UpdateAskForApprovalPolicy(approval));            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(                    format!("Permissions updated to {label}"),                    /*hint*/ None,                ),            )));        })]    }    pub(super) fn permission_profile_selection_actions(        selection: PermissionProfileSelection,    ) -> Vec<SelectionAction> {        vec![Box::new(move |tx| {            tx.send(AppEvent::SelectPermissionProfile(selection.clone()));        })]    }    pub(super) fn permission_mode_actions(        &self,        preset: &ApprovalPreset,        label: String,        approvals_reviewer: ApprovalsReviewer,        profile_selection: Option<PermissionProfileSelection>,        return_to_permissions: bool,    ) -> Vec<SelectionAction> {        let apply_actions = || {            profile_selection.clone().map_or_else(                || {                    Self::approval_preset_actions(                        AskForApproval::from(preset.approval),                        preset.permission_profile.clone(),                        preset.active_permission_profile.clone(),                        label.clone(),                        approvals_reviewer,                    )                },                Self::permission_profile_selection_actions,            )        };        let requires_confirmation = approvals_reviewer == ApprovalsReviewer::User            && preset.id == "full-access"            && !self                .config                .notices                .hide_full_access_warning                .unwrap_or(false);        if requires_confirmation {            let preset = preset.clone();            return vec![Box::new(move |tx| {                tx.send(AppEvent::OpenFullAccessConfirmation {                    preset: preset.clone(),                    return_to_permissions,                    profile_selection: profile_selection.clone(),                });            })];        }        if approvals_reviewer == ApprovalsReviewer::User && preset.id == "auto" {            #[cfg(target_os = "windows")]            {                if crate::windows_sandbox::level_from_config(&self.config)                    == WindowsSandboxLevel::Disabled                {                    let preset = preset.clone();                    if crate::windows_sandbox::sandbox_setup_is_complete(                        self.config.codex_home.as_path(),                    ) {                        return vec![Box::new(move |tx| {                            tx.send(AppEvent::EnableWindowsSandboxForAgentMode {                                preset: preset.clone(),                                mode: WindowsSandboxEnableMode::Elevated,                                profile_selection: profile_selection.clone(),                            });                        })];                    }                    return vec![Box::new(move |tx| {                        tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {                            preset: preset.clone(),                            profile_selection: profile_selection.clone(),                        });                    })];                }                if let Some((sample_paths, extra_count, failed_scan)) =                    self.world_writable_warning_details()                {                    let preset = preset.clone();                    return vec![Box::new(move |tx| {                        tx.send(AppEvent::OpenWorldWritableWarningConfirmation {                            preset: Some(preset.clone()),                            profile_selection: profile_selection.clone(),                            sample_paths: sample_paths.clone(),                            extra_count,                            failed_scan,                        });                    })];                }            }        }        apply_actions()    }    pub(super) fn preset_matches_current(        current_approval: AskForApproval,        current_permission_profile: &PermissionProfile,        cwd: &std::path::Path,        preset: &ApprovalPreset,    ) -> bool {        let preset_approval = AskForApproval::from(preset.approval);        if current_approval != preset_approval {            return false;        }        match preset.id {            "full-access" => matches!(current_permission_profile, PermissionProfile::Disabled),            "read-only" => {                let file_system_policy = current_permission_profile.file_system_sandbox_policy();                matches!(                    current_permission_profile,                    PermissionProfile::Managed { .. }                ) && !file_system_policy.has_full_disk_write_access()                    && file_system_policy                        .get_writable_roots_with_cwd(cwd)                        .is_empty()                    && current_permission_profile.network_sandbox_policy()                        == preset.permission_profile.network_sandbox_policy()            }            "auto" => {                let file_system_policy = current_permission_profile.file_system_sandbox_policy();                matches!(                    current_permission_profile,                    PermissionProfile::Managed { .. }                ) && file_system_policy.can_write_path_with_cwd(cwd, cwd)                    && !file_system_policy.has_full_disk_write_access()                    && current_permission_profile.network_sandbox_policy()                        == preset.permission_profile.network_sandbox_policy()            }            _ => current_permission_profile == &preset.permission_profile,        }    }    pub(crate) fn open_full_access_confirmation(        &mut self,        preset: ApprovalPreset,        return_to_permissions: bool,        profile_selection: Option<PermissionProfileSelection>,    ) {        let selected_name = preset.label.to_string();        let approval = AskForApproval::from(preset.approval);        let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();        let title_line = Line::from("Enable full access?").bold();        let info_line = Line::from(vec![            "When Codex runs with full access, it can edit any file on your computer and run commands with network, without your approval. "                .into(),            "Exercise caution when enabling full access. This significantly increases the risk of data loss, leaks, or unexpected behavior."                .fg(Color::Red),        ]);        header_children.push(Box::new(title_line));        header_children.push(Box::new(            Paragraph::new(vec![info_line]).wrap(Wrap { trim: false }),        ));        let header = ColumnRenderable::with(header_children);        let mut accept_actions = profile_selection.clone().map_or_else(            || {                Self::approval_preset_actions(                    approval,                    preset.permission_profile.clone(),                    preset.active_permission_profile.clone(),                    selected_name.clone(),                    ApprovalsReviewer::User,                )            },            Self::permission_profile_selection_actions,        );        accept_actions.push(Box::new(|tx| {            tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));        }));        let mut accept_and_remember_actions = profile_selection.map_or_else(            || {                Self::approval_preset_actions(                    approval,                    preset.permission_profile,                    preset.active_permission_profile,                    selected_name,                    ApprovalsReviewer::User,                )            },            Self::permission_profile_selection_actions,        );        accept_and_remember_actions.push(Box::new(|tx| {            tx.send(AppEvent::UpdateFullAccessWarningAcknowledged(true));            tx.send(AppEvent::PersistFullAccessWarningAcknowledged);        }));        let deny_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {            if return_to_permissions {                tx.send(AppEvent::OpenPermissionsPopup);            } else {                tx.send(AppEvent::OpenApprovalsPopup);            }        })];        let items = vec![            SelectionItem {                name: "Yes, continue anyway".to_string(),                description: Some("Apply full access for this session".to_string()),                actions: accept_actions,                dismiss_on_select: true,                ..Default::default()            },            SelectionItem {                name: "Yes, and don't ask again".to_string(),                description: Some("Enable full access and remember this choice".to_string()),                actions: accept_and_remember_actions,                dismiss_on_select: true,                ..Default::default()            },            SelectionItem {                name: "Cancel".to_string(),                description: Some("Go back without enabling full access".to_string()),                actions: deny_actions,                dismiss_on_select: true,                ..Default::default()            },        ];        self.bottom_pane.show_selection_view(SelectionViewParams {            footer_hint: Some(standard_popup_hint_line()),            items,            header: Box::new(header),            ..Default::default()        });    }}