Codex Handbook
tui/src/chatwidget/windows_sandbox_prompts.rs 511 lines
//! Windows sandbox prompts and warning surfaces for `ChatWidget`.use super::*;impl ChatWidget {    #[cfg(any(target_os = "windows", test))]    pub(crate) fn windows_sandbox_mode_allowed(&self, mode: WindowsSandboxModeToml) -> bool {        self.config            .config_layer_stack            .requirements()            .windows_sandbox_mode            .can_set(&Some(mode))            .is_ok()    }    #[cfg(any(target_os = "windows", test))]    pub(super) fn elevated_windows_sandbox_setup_required(&self) -> bool {        crate::windows_sandbox::level_from_config(&self.config) == WindowsSandboxLevel::Elevated            && self                .config                .config_layer_stack                .requirements()                .windows_sandbox_mode                .source                .is_some()            && !crate::windows_sandbox::sandbox_setup_is_complete(self.config.codex_home.as_path())    }    #[cfg(target_os = "windows")]    pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)> {        if self            .config            .notices            .hide_world_writable_warning            .unwrap_or(false)        {            return None;        }        let cwd = self.config.cwd.clone();        let workspace_roots = self.config.effective_workspace_roots();        let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();        let permission_profile = self.config.permissions.effective_permission_profile();        let Ok(permissions) =            codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(                &permission_profile,                workspace_roots.as_slice(),            )        else {            return None;        };        match codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions(            self.config.codex_home.as_path(),            cwd.as_path(),            &env_map,            &permissions,            Some(self.config.codex_home.as_path()),        ) {            Ok(_) => None,            Err(_) => Some((Vec::new(), 0, true)),        }    }    #[cfg(not(target_os = "windows"))]    #[allow(dead_code)]    pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec<String>, usize, bool)> {        None    }    #[cfg(target_os = "windows")]    pub(crate) fn open_world_writable_warning_confirmation(        &mut self,        preset: Option<ApprovalPreset>,        profile_selection: Option<PermissionProfileSelection>,        sample_paths: Vec<String>,        extra_count: usize,        failed_scan: bool,    ) {        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),        };        let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();        let describe_profile = |profile: &PermissionProfile| {            if matches!(profile, PermissionProfile::Disabled) {                "Full Access mode"            } else if profile                .file_system_sandbox_policy()                .can_write_path_with_cwd(self.config.cwd.as_path(), self.config.cwd.as_path())            {                "Agent mode"            } else {                "Read-Only mode"            }        };        let mode_label = preset            .as_ref()            .map(|p| describe_profile(&p.permission_profile))            .unwrap_or_else(|| {                describe_profile(&self.config.permissions.effective_permission_profile())            });        let info_line = if failed_scan {            Line::from(vec![                "We couldn't complete the world-writable scan, so protections cannot be verified. "                    .into(),                format!("The Windows sandbox cannot guarantee protection in {mode_label}.")                    .fg(Color::Red),            ])        } else {            Line::from(vec![                "The Windows sandbox cannot protect writes to folders that are writable by Everyone.".into(),                " Consider removing write access for Everyone from the following folders:".into(),            ])        };        header_children.push(Box::new(            Paragraph::new(vec![info_line]).wrap(Wrap { trim: false }),        ));        if !sample_paths.is_empty() {            // Show up to three examples and optionally an "and X more" line.            let mut lines: Vec<Line> = Vec::new();            lines.push(Line::from(""));            for p in &sample_paths {                lines.push(Line::from(format!("  - {p}")));            }            if extra_count > 0 {                lines.push(Line::from(format!("and {extra_count} more")));            }            header_children.push(Box::new(Paragraph::new(lines).wrap(Wrap { trim: false })));        }        let header = ColumnRenderable::with(header_children);        // Build actions ensuring acknowledgement happens before applying the        // new permission profile, so downstream policy-change hooks don't        // re-trigger the warning.        let mut accept_actions: Vec<SelectionAction> = Vec::new();        // Suppress the immediate re-scan only when a preset will be applied via        // /permissions, to avoid duplicate warnings from the ensuing policy change.        if preset.is_some() {            accept_actions.push(Box::new(|tx| {                tx.send(AppEvent::SkipNextWorldWritableScan);            }));        }        if let Some(selection) = profile_selection.clone() {            accept_actions.extend(Self::permission_profile_selection_actions(selection));        } else 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,            ));        }        let mut accept_and_remember_actions: Vec<SelectionAction> = Vec::new();        accept_and_remember_actions.push(Box::new(|tx| {            tx.send(AppEvent::UpdateWorldWritableWarningAcknowledged(true));            tx.send(AppEvent::PersistWorldWritableWarningAcknowledged);        }));        if let Some(selection) = profile_selection {            accept_and_remember_actions                .extend(Self::permission_profile_selection_actions(selection));        } else 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,            ));        }        let items = vec![            SelectionItem {                name: "Continue".to_string(),                description: Some(format!("Apply {mode_label} for this session")),                actions: accept_actions,                dismiss_on_select: true,                ..Default::default()            },            SelectionItem {                name: "Continue and don't warn again".to_string(),                description: Some(format!("Enable {mode_label} and remember this choice")),                actions: accept_and_remember_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()        });    }    #[cfg(not(target_os = "windows"))]    pub(crate) fn open_world_writable_warning_confirmation(        &mut self,        _preset: Option<ApprovalPreset>,        _profile_selection: Option<PermissionProfileSelection>,        _sample_paths: Vec<String>,        _extra_count: usize,        _failed_scan: bool,    ) {    }    #[cfg(any(target_os = "windows", test))]    pub(crate) fn open_windows_sandbox_enable_prompt(        &mut self,        preset: ApprovalPreset,        profile_selection: Option<PermissionProfileSelection>,    ) {        use ratatui_macros::line;        self.session_telemetry.counter(            "codex.windows_sandbox.elevated_prompt_shown",            /*inc*/ 1,            &[],        );        let allow_unelevated =            self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated);        let setup_choice_is_required =            !allow_unelevated || self.elevated_windows_sandbox_setup_required();        let mut header = ColumnRenderable::new();        header.push(*Box::new(            Paragraph::new(if allow_unelevated {                vec![                    line!["Set up the Codex agent sandbox to protect your files and control network access. Learn more <https://developers.openai.com/codex/windows>"],                ]            } else {                vec![                    line!["Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control network access."],                    line!["Learn more <https://developers.openai.com/codex/windows>"],                ]            })            .wrap(Wrap { trim: false }),        ));        let accept_otel = self.session_telemetry.clone();        let legacy_otel = self.session_telemetry.clone();        let legacy_preset = preset.clone();        let legacy_profile_selection = profile_selection.clone();        let quit_otel = self.session_telemetry.clone();        let retry_preset = preset.clone();        let retry_profile_selection = profile_selection.clone();        let mut items = vec![SelectionItem {            name: "Set up default sandbox (requires Administrator permissions)".to_string(),            description: None,            actions: vec![Box::new(move |tx| {                accept_otel.counter(                    "codex.windows_sandbox.elevated_prompt_accept",                    /*inc*/ 1,                    &[],                );                tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {                    preset: preset.clone(),                    profile_selection: profile_selection.clone(),                });            })],            dismiss_on_select: true,            ..Default::default()        }];        if allow_unelevated {            items.push(SelectionItem {                name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(),                description: None,                actions: vec![Box::new(move |tx| {                    legacy_otel.counter(                        "codex.windows_sandbox.elevated_prompt_use_legacy",                        /*inc*/ 1,                        &[],                    );                    tx.send(AppEvent::BeginWindowsSandboxLegacySetup {                        preset: legacy_preset.clone(),                        profile_selection: legacy_profile_selection.clone(),                    });                })],                dismiss_on_select: true,                ..Default::default()            });        }        items.push(SelectionItem {            name: "Quit".to_string(),            description: None,            actions: vec![Box::new(move |tx| {                quit_otel.counter(                    "codex.windows_sandbox.elevated_prompt_quit",                    /*inc*/ 1,                    &[],                );                tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));            })],            dismiss_on_select: true,            ..Default::default()        });        self.bottom_pane.show_selection_view(SelectionViewParams {            title: None,            footer_hint: Some(standard_popup_hint_line()),            items,            header: Box::new(header),            on_cancel: setup_choice_is_required.then(|| {                Box::new(move |tx: &AppEventSender| {                    tx.send(AppEvent::OpenWindowsSandboxEnablePrompt {                        preset: retry_preset.clone(),                        profile_selection: retry_profile_selection.clone(),                    });                }) as _            }),            ..Default::default()        });    }    #[cfg(all(not(target_os = "windows"), not(test)))]    pub(crate) fn open_windows_sandbox_enable_prompt(        &mut self,        _preset: ApprovalPreset,        _profile_selection: Option<PermissionProfileSelection>,    ) {    }    #[cfg(any(target_os = "windows", test))]    pub(crate) fn open_windows_sandbox_fallback_prompt(        &mut self,        preset: ApprovalPreset,        profile_selection: Option<PermissionProfileSelection>,    ) {        use ratatui_macros::line;        let allow_unelevated =            self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated);        let setup_choice_is_required =            !allow_unelevated || self.elevated_windows_sandbox_setup_required();        let mut lines = Vec::new();        lines.push(line![            "Couldn't set up your sandbox with Administrator permissions".bold()        ]);        lines.push(line![""]);        if allow_unelevated {            lines.push(line![                "You can still use Codex in a non-admin sandbox. It carries greater risk if prompt injected."            ]);        } else {            lines.push(line![                "Your organization requires the default sandbox before Codex can continue."            ]);        }        lines.push(line![            "Learn more <https://developers.openai.com/codex/windows>"        ]);        let mut header = ColumnRenderable::new();        header.push(*Box::new(Paragraph::new(lines).wrap(Wrap { trim: false })));        let elevated_preset = preset.clone();        let legacy_preset = preset;        let retry_preset = elevated_preset.clone();        let retry_profile_selection = profile_selection.clone();        let elevated_profile_selection = profile_selection.clone();        let legacy_profile_selection = profile_selection;        let quit_otel = self.session_telemetry.clone();        let mut items = vec![SelectionItem {            name: "Try setting up admin sandbox again".to_string(),            description: None,            actions: vec![Box::new({                let otel = self.session_telemetry.clone();                let preset = elevated_preset;                move |tx| {                    otel.counter(                        "codex.windows_sandbox.fallback_retry_elevated",                        /*inc*/ 1,                        &[],                    );                    tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {                        preset: preset.clone(),                        profile_selection: elevated_profile_selection.clone(),                    });                }            })],            dismiss_on_select: true,            ..Default::default()        }];        if allow_unelevated {            items.push(SelectionItem {                name: "Use Codex with non-admin sandbox".to_string(),                description: None,                actions: vec![Box::new({                    let otel = self.session_telemetry.clone();                    let preset = legacy_preset;                    move |tx| {                        otel.counter(                            "codex.windows_sandbox.fallback_use_legacy",                            /*inc*/ 1,                            &[],                        );                        tx.send(AppEvent::BeginWindowsSandboxLegacySetup {                            preset: preset.clone(),                            profile_selection: legacy_profile_selection.clone(),                        });                    }                })],                dismiss_on_select: true,                ..Default::default()            });        }        items.push(SelectionItem {            name: "Quit".to_string(),            description: None,            actions: vec![Box::new(move |tx| {                quit_otel.counter(                    "codex.windows_sandbox.fallback_prompt_quit",                    /*inc*/ 1,                    &[],                );                tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));            })],            dismiss_on_select: true,            ..Default::default()        });        self.bottom_pane.show_selection_view(SelectionViewParams {            title: None,            footer_hint: Some(standard_popup_hint_line()),            items,            header: Box::new(header),            on_cancel: setup_choice_is_required.then(|| {                Box::new(move |tx: &AppEventSender| {                    tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt {                        preset: retry_preset.clone(),                        profile_selection: retry_profile_selection.clone(),                    });                }) as _            }),            ..Default::default()        });    }    #[cfg(all(not(target_os = "windows"), not(test)))]    pub(crate) fn open_windows_sandbox_fallback_prompt(        &mut self,        _preset: ApprovalPreset,        _profile_selection: Option<PermissionProfileSelection>,    ) {    }    #[cfg(target_os = "windows")]    pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) {        let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config);        let setup_is_required = windows_sandbox_level == WindowsSandboxLevel::Disabled            || self.elevated_windows_sandbox_setup_required();        if show_now            && setup_is_required            && let Some(preset) = builtin_approval_presets()                .into_iter()                .find(|preset| preset.id == "auto")        {            self.open_windows_sandbox_enable_prompt(preset, /*profile_selection*/ None);        }    }    #[cfg(not(target_os = "windows"))]    pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, _show_now: bool) {}    #[cfg(target_os = "windows")]    pub(crate) fn show_windows_sandbox_setup_status(&mut self) {        // While elevated sandbox setup runs, prevent typing so the user doesn't        // accidentally queue messages that will run under an unexpected mode.        self.bottom_pane.set_composer_input_enabled(            /*enabled*/ false,            Some("Input disabled until setup completes.".to_string()),        );        self.bottom_pane.ensure_status_indicator();        self.bottom_pane            .set_interrupt_hint_visible(/*visible*/ false);        self.set_status(            "Setting up sandbox...".to_string(),            Some("Hang tight, this may take a few minutes".to_string()),            StatusDetailsCapitalization::CapitalizeFirst,            STATUS_DETAILS_DEFAULT_MAX_LINES,        );        self.request_redraw();    }    #[cfg(not(target_os = "windows"))]    #[allow(dead_code)]    pub(crate) fn show_windows_sandbox_setup_status(&mut self) {}    #[cfg(target_os = "windows")]    pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {        self.bottom_pane            .set_composer_input_enabled(/*enabled*/ true, /*placeholder*/ None);        self.bottom_pane.hide_status_indicator();        self.request_redraw();    }    #[cfg(not(target_os = "windows"))]    pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {}}