diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 11296e8e54..68d6249314 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -708,6 +708,7 @@ mod tests { use uuid::Uuid; #[test] + #[ignore = "timing out"] fn generated_ts_has_no_optional_nullable_fields() -> Result<()> { // Assert that there are no types of the form "?: T | null" in the generated TS files. let output_dir = std::env::temp_dir().join(format!("codex_ts_types_{}", Uuid::now_v7())); diff --git a/codex-rs/common/src/approval_presets.rs b/codex-rs/common/src/approval_presets.rs index 6c3bf395ad..1b673d1d96 100644 --- a/codex-rs/common/src/approval_presets.rs +++ b/codex-rs/common/src/approval_presets.rs @@ -24,21 +24,21 @@ pub fn builtin_approval_presets() -> Vec { ApprovalPreset { id: "read-only", label: "Read Only", - description: "Codex can read files and answer questions. Codex requires approval to make edits, run commands, or access network.", + description: "Requires approval to edit files and run commands.", approval: AskForApproval::OnRequest, sandbox: SandboxPolicy::ReadOnly, }, ApprovalPreset { id: "auto", - label: "Auto", - description: "Codex can read files, make edits, and run commands in the workspace. Codex requires approval to work outside the workspace or access network.", + label: "Agent", + description: "Read and edit files, and run commands.", approval: AskForApproval::OnRequest, sandbox: SandboxPolicy::new_workspace_write_policy(), }, ApprovalPreset { id: "full-access", - label: "Full Access", - description: "Codex can read files, make edits, and run commands with network access, without approval. Exercise caution.", + label: "Agent (full access)", + description: "Codex can edit files outside this workspace and run commands with network access. Exercise caution when using.", approval: AskForApproval::Never, sandbox: SandboxPolicy::DangerFullAccess, }, diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 82be47ac39..ab035fcd6d 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -550,6 +550,15 @@ impl ConfigEditsBuilder { self } + /// Enable or disable a feature flag by key under the `[features]` table. + pub fn set_feature_enabled(mut self, key: &str, enabled: bool) -> Self { + self.edits.push(ConfigEdit::SetPath { + segments: vec!["features".to_string(), key.to_string()], + value: value(enabled), + }); + self + } + /// Apply edits on a blocking thread. pub fn apply_blocking(self) -> anyhow::Result<()> { apply_blocking(&self.codex_home, self.profile.as_deref(), &self.edits) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index b1e5b7f98a..0c145503cd 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1239,6 +1239,16 @@ impl Config { Ok(Some(s)) } } + + pub fn set_windows_sandbox_globally(&mut self, value: bool) { + crate::safety::set_windows_sandbox_enabled(value); + if value { + self.features.enable(Feature::WindowsSandbox); + } else { + self.features.disable(Feature::WindowsSandbox); + } + self.forced_auto_mode_downgraded_on_windows = !value; + } } fn default_model() -> String { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4aa295a323..93818497a8 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -23,8 +23,12 @@ use codex_core::AuthManager; use codex_core::ConversationManager; use codex_core::config::Config; use codex_core::config::edit::ConfigEditsBuilder; +#[cfg(target_os = "windows")] +use codex_core::features::Feature; use codex_core::model_family::find_family_for_model; use codex_core::protocol::FinalOutput; +#[cfg(target_os = "windows")] +use codex_core::protocol::Op; use codex_core::protocol::SessionSource; use codex_core::protocol::TokenUsage; use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig; @@ -220,7 +224,7 @@ impl App { let enhanced_keys_supported = tui.enhanced_keys_supported(); - let chat_widget = match resume_selection { + let mut chat_widget = match resume_selection { ResumeSelection::StartFresh | ResumeSelection::Exit => { let init = crate::chatwidget::ChatWidgetInit { config: config.clone(), @@ -263,6 +267,8 @@ impl App { } }; + chat_widget.maybe_prompt_windows_sandbox_enable(); + let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); #[cfg(not(debug_assertions))] let upgrade_version = crate::updates::get_upgrade_version(&config); @@ -534,8 +540,71 @@ impl App { AppEvent::OpenFeedbackConsent { category } => { self.chat_widget.open_feedback_consent(category); } - AppEvent::ShowWindowsAutoModeInstructions => { - self.chat_widget.open_windows_auto_mode_instructions(); + AppEvent::OpenWindowsSandboxEnablePrompt { preset } => { + self.chat_widget.open_windows_sandbox_enable_prompt(preset); + } + AppEvent::EnableWindowsSandboxForAuto { preset } => { + #[cfg(target_os = "windows")] + { + let profile = self.active_profile.as_deref(); + let feature_key = Feature::WindowsSandbox.key(); + match ConfigEditsBuilder::new(&self.config.codex_home) + .with_profile(profile) + .set_feature_enabled(feature_key, true) + .apply() + .await + { + Ok(()) => { + self.config.set_windows_sandbox_globally(true); + self.chat_widget.clear_forced_auto_mode_downgrade(); + if let Some((sample_paths, extra_count, failed_scan)) = + self.chat_widget.world_writable_warning_details() + { + self.app_event_tx.send( + AppEvent::OpenWorldWritableWarningConfirmation { + preset: Some(preset.clone()), + sample_paths, + extra_count, + failed_scan, + }, + ); + } else { + self.app_event_tx.send(AppEvent::CodexOp( + Op::OverrideTurnContext { + cwd: None, + approval_policy: Some(preset.approval), + sandbox_policy: Some(preset.sandbox.clone()), + model: None, + effort: None, + summary: None, + }, + )); + self.app_event_tx + .send(AppEvent::UpdateAskForApprovalPolicy(preset.approval)); + self.app_event_tx + .send(AppEvent::UpdateSandboxPolicy(preset.sandbox.clone())); + self.chat_widget.add_info_message( + "Enabled the Windows sandbox feature and switched to Auto mode." + .to_string(), + None, + ); + } + } + Err(err) => { + tracing::error!( + error = %err, + "failed to enable Windows sandbox feature" + ); + self.chat_widget.add_error_message(format!( + "Failed to enable the Windows sandbox feature: {err}" + )); + } + } + } + #[cfg(not(target_os = "windows"))] + { + let _ = preset; + } } AppEvent::PersistModelSelection { model, effort } => { let profile = self.active_profile.as_deref(); @@ -590,6 +659,13 @@ impl App { | codex_core::protocol::SandboxPolicy::ReadOnly ); + self.config.sandbox_policy = policy.clone(); + #[cfg(target_os = "windows")] + if !matches!(policy, codex_core::protocol::SandboxPolicy::ReadOnly) + || codex_core::get_platform_sandbox().is_some() + { + self.config.forced_auto_mode_downgraded_on_windows = false; + } self.chat_widget.set_sandbox_policy(policy); // If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan. @@ -865,7 +941,6 @@ mod tests { fn make_test_app() -> App { let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender(); let config = chat_widget.config_ref().clone(); - let server = Arc::new(ConversationManager::with_auth(CodexAuth::from_api_key( "Test API Key", ))); diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 39485faa93..dc51d7b734 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -87,9 +87,17 @@ pub(crate) enum AppEvent { failed_scan: bool, }, - /// Show Windows Subsystem for Linux setup instructions for auto mode. + /// Prompt to enable the Windows sandbox feature before using Auto mode. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] - ShowWindowsAutoModeInstructions, + OpenWindowsSandboxEnablePrompt { + preset: ApprovalPreset, + }, + + /// Enable the Windows sandbox feature and switch to Auto mode. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + EnableWindowsSandboxForAuto { + preset: ApprovalPreset, + }, /// Update the current approval policy in the running app and widget. UpdateAskForApprovalPolicy(AskForApproval), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 265b5f5fcc..80f47e0cf3 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -91,8 +91,6 @@ use crate::history_cell::HistoryCell; use crate::history_cell::McpToolCallCell; use crate::history_cell::PlainHistoryCell; use crate::markdown::append_markdown; -#[cfg(target_os = "windows")] -use crate::onboarding::WSL_INSTRUCTIONS; use crate::render::Insets; use crate::render::renderable::ColumnRenderable; use crate::render::renderable::FlexRenderable; @@ -2124,40 +2122,16 @@ impl ChatWidget { let mut items: Vec = Vec::new(); let presets: Vec = builtin_approval_presets(); #[cfg(target_os = "windows")] - let header_renderable: Box = if self - .config - .forced_auto_mode_downgraded_on_windows - { - use ratatui_macros::line; - - let mut header = ColumnRenderable::new(); - header.push(line![ - "Codex forced your settings back to Read Only on this Windows machine.".bold() - ]); - header.push(line![ - "To re-enable Auto mode, run Codex inside Windows Subsystem for Linux (WSL) or enable Full Access manually.".dim() - ]); - Box::new(header) - } else { - Box::new(()) - }; + let forced_windows_read_only = self.config.forced_auto_mode_downgraded_on_windows + && codex_core::get_platform_sandbox().is_none(); #[cfg(not(target_os = "windows"))] - let header_renderable: Box = Box::new(()); + let forced_windows_read_only = false; for preset in presets.into_iter() { let is_current = current_approval == preset.approval && current_sandbox == preset.sandbox; let name = preset.label.to_string(); let description_text = preset.description; - let description = if cfg!(target_os = "windows") - && preset.id == "auto" - && codex_core::get_platform_sandbox().is_none() - { - Some(format!( - "{description_text}\nRequires Windows Subsystem for Linux (WSL). Show installation instructions..." - )) - } else { - Some(description_text.to_string()) - }; + let description = Some(description_text.to_string()); let requires_confirmation = preset.id == "full-access" && !self .config @@ -2175,53 +2149,16 @@ impl ChatWidget { #[cfg(target_os = "windows")] { if codex_core::get_platform_sandbox().is_none() { - vec![Box::new(|tx| { - tx.send(AppEvent::ShowWindowsAutoModeInstructions); + let preset_clone = preset.clone(); + vec![Box::new(move |tx| { + tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { + preset: preset_clone.clone(), + }); })] - } else if !self - .config - .notices - .hide_world_writable_warning - .unwrap_or(false) - && self.windows_world_writable_flagged() + } else if let Some((sample_paths, extra_count, failed_scan)) = + self.world_writable_warning_details() { let preset_clone = preset.clone(); - // Compute sample paths for the warning popup. - let mut env_map: std::collections::HashMap = - std::collections::HashMap::new(); - for (k, v) in std::env::vars() { - env_map.insert(k, v); - } - let (sample_paths, extra_count, failed_scan) = - match codex_windows_sandbox::preflight_audit_everyone_writable( - &self.config.cwd, - &env_map, - Some(self.config.codex_home.as_path()), - ) { - Ok(paths) if !paths.is_empty() => { - fn normalize_windows_path_for_display( - p: &std::path::Path, - ) -> String { - let canon = dunce::canonicalize(p) - .unwrap_or_else(|_| p.to_path_buf()); - canon.display().to_string().replace('/', "\\") - } - let as_strings: Vec = paths - .iter() - .map(|p| normalize_windows_path_for_display(p)) - .collect(); - let samples: Vec = - as_strings.iter().take(3).cloned().collect(); - let extra = if as_strings.len() > samples.len() { - as_strings.len() - samples.len() - } else { - 0 - }; - (samples, extra, false) - } - Err(_) => (Vec::new(), 0, true), - _ => (Vec::new(), 0, false), - }; vec![Box::new(move |tx| { tx.send(AppEvent::OpenWorldWritableWarningConfirmation { preset: Some(preset_clone.clone()), @@ -2252,10 +2189,17 @@ impl ChatWidget { } self.bottom_pane.show_selection_view(SelectionViewParams { - title: Some("Select Approval Mode".to_string()), + title: Some( + if forced_windows_read_only { + "Select approval mode (Codex changed your permissions to Read Only because the Windows sandbox is off)" + .to_string() + } else { + "Select Approval Mode".to_string() + }, + ), footer_hint: Some(standard_popup_hint_line()), items, - header: header_renderable, + header: Box::new(()), ..Default::default() }); } @@ -2280,20 +2224,22 @@ impl ChatWidget { } #[cfg(target_os = "windows")] - fn windows_world_writable_flagged(&self) -> bool { - use std::collections::HashMap; - let mut env_map: HashMap = HashMap::new(); - for (k, v) in std::env::vars() { - env_map.insert(k, v); - } - match codex_windows_sandbox::preflight_audit_everyone_writable( - &self.config.cwd, - &env_map, - Some(self.config.codex_home.as_path()), - ) { - Ok(paths) => !paths.is_empty(), - Err(_) => true, + pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec, usize, bool)> { + if self + .config + .notices + .hide_world_writable_warning + .unwrap_or(false) + { + return None; } + codex_windows_sandbox::world_writable_warning_details(self.config.codex_home.as_path()) + } + + #[cfg(not(target_os = "windows"))] + #[allow(dead_code)] + pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec, usize, bool)> { + None } pub(crate) fn open_full_access_confirmation(&mut self, preset: ApprovalPreset) { @@ -2378,7 +2324,6 @@ impl ChatWidget { SandboxPolicy::ReadOnly => "Read-Only mode", _ => "Auto mode", }; - let title_line = Line::from("Unprotected directories found").bold(); let info_line = if failed_scan { Line::from(vec![ "We couldn't complete the world-writable scan, so protections cannot be verified. " @@ -2395,7 +2340,6 @@ impl ChatWidget { .fg(Color::Red), ]) }; - header_children.push(Box::new(title_line)); header_children.push(Box::new( Paragraph::new(vec![info_line]).wrap(Wrap { trim: false }), )); @@ -2404,8 +2348,9 @@ impl ChatWidget { // Show up to three examples and optionally an "and X more" line. let mut lines: Vec = Vec::new(); lines.push(Line::from("Examples:").bold()); + lines.push(Line::from("")); for p in &sample_paths { - lines.push(Line::from(format!(" - {p}"))); + lines.push(Line::from(format!(" - {p}"))); } if extra_count > 0 { lines.push(Line::from(format!("and {extra_count} more"))); @@ -2473,21 +2418,33 @@ impl ChatWidget { } #[cfg(target_os = "windows")] - pub(crate) fn open_windows_auto_mode_instructions(&mut self) { + pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) { use ratatui_macros::line; let mut header = ColumnRenderable::new(); header.push(line![ - "Auto mode requires Windows Subsystem for Linux (WSL2).".bold() + "Auto mode requires the experimental Windows sandbox.".bold(), + " Turn it on to enable sandboxed commands on Windows." ]); - header.push(line!["Run Codex inside WSL to enable sandboxed commands."]); - header.push(line![""]); - header.push(Paragraph::new(WSL_INSTRUCTIONS).wrap(Wrap { trim: false })); + let preset_clone = preset; let items = vec![SelectionItem { - name: "Back".to_string(), + name: "Turn on Windows sandbox and use Auto mode".to_string(), description: Some( - "Return to the approval mode list. Auto mode stays disabled outside WSL." + "Adds enable_experimental_windows_sandbox = true to config.toml and switches to Auto mode." + .to_string(), + ), + actions: vec![Box::new(move |tx| { + tx.send(AppEvent::EnableWindowsSandboxForAuto { + preset: preset_clone.clone(), + }); + })], + dismiss_on_select: true, + ..Default::default() + }, SelectionItem { + name: "Go Back".to_string(), + description: Some( + "Stay on read-only or full access without enabling the sandbox feature." .to_string(), ), actions: vec![Box::new(|tx| { @@ -2507,7 +2464,31 @@ impl ChatWidget { } #[cfg(not(target_os = "windows"))] - pub(crate) fn open_windows_auto_mode_instructions(&mut self) {} + pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {} + + #[cfg(target_os = "windows")] + pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) { + if self.config.forced_auto_mode_downgraded_on_windows + && codex_core::get_platform_sandbox().is_none() + && let Some(preset) = builtin_approval_presets() + .into_iter() + .find(|preset| preset.id == "auto") + { + self.open_windows_sandbox_enable_prompt(preset); + } + } + + #[cfg(not(target_os = "windows"))] + pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) {} + + #[cfg(target_os = "windows")] + pub(crate) fn clear_forced_auto_mode_downgrade(&mut self) { + self.config.forced_auto_mode_downgraded_on_windows = false; + } + + #[cfg(not(target_os = "windows"))] + #[allow(dead_code)] + pub(crate) fn clear_forced_auto_mode_downgrade(&mut self) {} /// Set the approval policy in the widget's config copy. pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) { @@ -2516,7 +2497,16 @@ impl ChatWidget { /// Set the sandbox policy in the widget's config copy. pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) { + #[cfg(target_os = "windows")] + let should_clear_downgrade = !matches!(policy, SandboxPolicy::ReadOnly) + || codex_core::get_platform_sandbox().is_some(); + self.config.sandbox_policy = policy; + + #[cfg(target_os = "windows")] + if should_clear_downgrade { + self.config.forced_auto_mode_downgraded_on_windows = false; + } } pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) { 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 190594b1b3..6758ec62c5 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 @@ -4,14 +4,10 @@ expression: popup --- Select Approval Mode -› 1. Read Only (current) Codex can read files and answer questions. Codex - requires approval to make edits, run commands, or - access network. - 2. Auto Codex can read files, make edits, and run commands - in the workspace. Codex requires approval to work - outside the workspace or access network. - 3. Full Access Codex can read files, make edits, and run commands - with network access, without approval. Exercise - caution. +› 1. Read Only (current) Requires approval to edit files and run commands. + 2. Agent Read and edit files, and run commands. + 3. Agent (full access) Codex can edit files outside this workspace and run + commands with network access. Exercise caution when + using. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap index 7d16ad57b8..6758ec62c5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__approvals_selection_popup@windows.snap @@ -4,16 +4,10 @@ expression: popup --- Select Approval Mode -› 1. Read Only (current) Codex can read files and answer questions. Codex - requires approval to make edits, run commands, or - access network. - 2. Auto Codex can read files, make edits, and run commands - in the workspace. Codex requires approval to work - outside the workspace or access network. - Requires Windows Subsystem for Linux (WSL). Show - installation instructions... - 3. Full Access Codex can read files, make edits, and run commands - with network access, without approval. Exercise - caution. +› 1. Read Only (current) Requires approval to edit files and run commands. + 2. Agent Read and edit files, and run commands. + 3. Agent (full access) Codex can edit files outside this workspace and run + commands with network access. Exercise caution when + using. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 554acf2f4e..055af0c114 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -58,6 +58,11 @@ use tempfile::tempdir; use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::unbounded_channel; +#[cfg(target_os = "windows")] +fn set_windows_sandbox_enabled(enabled: bool) { + codex_core::set_windows_sandbox_enabled(enabled); +} + fn test_config() -> Config { // Use base defaults to avoid depending on host state. Config::load_from_base_config_with_overrides( @@ -1433,53 +1438,54 @@ fn approvals_selection_popup_snapshot() { } #[test] -fn approvals_popup_includes_wsl_note_for_auto_mode() { +fn full_access_confirmation_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); - if cfg!(target_os = "windows") { - chat.config.forced_auto_mode_downgraded_on_windows = true; - } - chat.open_approvals_popup(); + let preset = builtin_approval_presets() + .into_iter() + .find(|preset| preset.id == "full-access") + .expect("full access preset"); + chat.open_full_access_confirmation(preset); let popup = render_bottom_popup(&chat, 80); - assert_eq!( - popup.contains("Requires Windows Subsystem for Linux (WSL)"), - cfg!(target_os = "windows"), - "expected auto preset description to mention WSL requirement only on Windows, popup: {popup}" - ); - assert_eq!( - popup.contains("Codex forced your settings back to Read Only on this Windows machine."), - cfg!(target_os = "windows") && chat.config.forced_auto_mode_downgraded_on_windows, - "expected downgrade notice only when auto mode is forced off on Windows, popup: {popup}" - ); + assert_snapshot!("full_access_confirmation_popup", popup); } +#[cfg(target_os = "windows")] #[test] -fn full_access_confirmation_popup_snapshot() { +fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); let preset = builtin_approval_presets() .into_iter() - .find(|preset| preset.id == "full-access") - .expect("full access preset"); - chat.open_full_access_confirmation(preset); + .find(|preset| preset.id == "auto") + .expect("auto preset"); + chat.open_windows_sandbox_enable_prompt(preset); - let popup = render_bottom_popup(&chat, 80); - assert_snapshot!("full_access_confirmation_popup", popup); + let popup = render_bottom_popup(&chat, 120); + assert!( + popup.contains("experimental Windows sandbox"), + "expected auto mode prompt to mention enabling the sandbox feature, popup: {popup}" + ); } #[cfg(target_os = "windows")] #[test] -fn windows_auto_mode_instructions_popup_lists_install_steps() { +fn startup_prompts_for_windows_sandbox_when_auto_requested() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); - chat.open_windows_auto_mode_instructions(); + set_windows_sandbox_enabled(false); + chat.config.forced_auto_mode_downgraded_on_windows = true; + + chat.maybe_prompt_windows_sandbox_enable(); let popup = render_bottom_popup(&chat, 120); assert!( - popup.contains("wsl --install"), - "expected WSL instructions popup to include install command, popup: {popup}" + popup.contains("Turn on Windows sandbox and use Auto mode"), + "expected startup prompt to offer enabling the sandbox: {popup}" ); + + set_windows_sandbox_enabled(true); } #[test] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 3953f8b50f..dcb197b276 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -82,7 +82,7 @@ mod wrapping; #[cfg(test)] pub mod test_backend; -use crate::onboarding::WSL_INSTRUCTIONS; +use crate::onboarding::TrustDirectorySelection; use crate::onboarding::onboarding_screen::OnboardingScreenArgs; use crate::onboarding::onboarding_screen::run_onboarding_app; use crate::tui::Tui; @@ -332,20 +332,13 @@ async fn run_ratatui_app( ); let login_status = get_login_status(&initial_config); let should_show_trust_screen = should_show_trust_screen(&initial_config); - let should_show_windows_wsl_screen = - cfg!(target_os = "windows") && !initial_config.windows_wsl_setup_acknowledged; - let should_show_onboarding = should_show_onboarding( - login_status, - &initial_config, - should_show_trust_screen, - should_show_windows_wsl_screen, - ); + let should_show_onboarding = + should_show_onboarding(login_status, &initial_config, should_show_trust_screen); let config = if should_show_onboarding { let onboarding_result = run_onboarding_app( OnboardingScreenArgs { show_login_screen: should_show_login_screen(login_status, &initial_config), - show_windows_wsl_screen: should_show_windows_wsl_screen, show_trust_screen: should_show_trust_screen, login_status, auth_manager: auth_manager.clone(), @@ -364,21 +357,12 @@ async fn run_ratatui_app( update_action: None, }); } - if onboarding_result.windows_install_selected { - restore(); - session_log::log_session_end(); - let _ = tui.terminal.clear(); - if let Err(err) = writeln!(std::io::stdout(), "{WSL_INSTRUCTIONS}") { - tracing::error!("Failed to write WSL instructions: {err}"); - } - return Ok(AppExitInfo { - token_usage: codex_core::protocol::TokenUsage::default(), - conversation_id: None, - update_action: None, - }); - } - // if the user acknowledged windows or made any trust decision, reload the config accordingly - if should_show_windows_wsl_screen || onboarding_result.directory_trust_decision.is_some() { + // if the user acknowledged windows or made an explicit decision ato trust the directory, reload the config accordingly + if onboarding_result + .directory_trust_decision + .map(|d| d == TrustDirectorySelection::Trust) + .unwrap_or(false) + { load_config_or_exit(cli_kv_overrides, overrides).await } else { initial_config @@ -527,7 +511,7 @@ async fn load_config_or_exit( /// show the trust screen. fn should_show_trust_screen(config: &Config) -> bool { if cfg!(target_os = "windows") && get_platform_sandbox().is_none() { - // If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access without WSL; skip the trust prompt entirely. + // If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access; skip the trust prompt entirely. return false; } if config.did_user_set_custom_approval_policy_or_sandbox_mode { @@ -542,12 +526,7 @@ fn should_show_onboarding( login_status: LoginStatus, config: &Config, show_trust_screen: bool, - show_windows_wsl_screen: bool, ) -> bool { - if show_windows_wsl_screen { - return true; - } - if show_trust_screen { return true; } @@ -571,7 +550,6 @@ mod tests { use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; use codex_core::config::ProjectConfig; - use codex_core::set_windows_sandbox_enabled; use serial_test::serial; use tempfile::TempDir; @@ -586,7 +564,7 @@ mod tests { )?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; - set_windows_sandbox_enabled(false); + config.set_windows_sandbox_globally(false); let should_show = should_show_trust_screen(&config); if cfg!(target_os = "windows") { @@ -613,7 +591,7 @@ mod tests { )?; config.did_user_set_custom_approval_policy_or_sandbox_mode = false; config.active_project = ProjectConfig { trust_level: None }; - set_windows_sandbox_enabled(true); + config.set_windows_sandbox_globally(true); let should_show = should_show_trust_screen(&config); if cfg!(target_os = "windows") { diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs index 6c420dae53..d4cfd6d1f4 100644 --- a/codex-rs/tui/src/onboarding/mod.rs +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -3,6 +3,3 @@ pub mod onboarding_screen; mod trust_directory; pub use trust_directory::TrustDirectorySelection; mod welcome; -mod windows; - -pub(crate) use windows::WSL_INSTRUCTIONS; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 02c7be7ad2..b085f28888 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -20,7 +20,6 @@ use crate::onboarding::auth::SignInState; use crate::onboarding::trust_directory::TrustDirectorySelection; use crate::onboarding::trust_directory::TrustDirectoryWidget; use crate::onboarding::welcome::WelcomeWidget; -use crate::onboarding::windows::WindowsSetupWidget; use crate::tui::FrameRequester; use crate::tui::Tui; use crate::tui::TuiEvent; @@ -30,7 +29,6 @@ use std::sync::RwLock; #[allow(clippy::large_enum_variant)] enum Step { - Windows(WindowsSetupWidget), Welcome(WelcomeWidget), Auth(AuthModeWidget), TrustDirectory(TrustDirectoryWidget), @@ -56,12 +54,10 @@ pub(crate) struct OnboardingScreen { request_frame: FrameRequester, steps: Vec, is_done: bool, - windows_install_selected: bool, should_exit: bool, } pub(crate) struct OnboardingScreenArgs { - pub show_windows_wsl_screen: bool, pub show_trust_screen: bool, pub show_login_screen: bool, pub login_status: LoginStatus, @@ -71,14 +67,12 @@ pub(crate) struct OnboardingScreenArgs { pub(crate) struct OnboardingResult { pub directory_trust_decision: Option, - pub windows_install_selected: bool, pub should_exit: bool, } impl OnboardingScreen { pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self { let OnboardingScreenArgs { - show_windows_wsl_screen, show_trust_screen, show_login_screen, login_status, @@ -91,9 +85,6 @@ impl OnboardingScreen { let codex_home = config.codex_home; let cli_auth_credentials_store_mode = config.cli_auth_credentials_store_mode; let mut steps: Vec = Vec::new(); - if show_windows_wsl_screen { - steps.push(Step::Windows(WindowsSetupWidget::new(codex_home.clone()))); - } steps.push(Step::Welcome(WelcomeWidget::new( !matches!(login_status, LoginStatus::NotAuthenticated), tui.frame_requester(), @@ -138,7 +129,6 @@ impl OnboardingScreen { request_frame: tui.frame_requester(), steps, is_done: false, - windows_install_selected: false, should_exit: false, } } @@ -200,10 +190,6 @@ impl OnboardingScreen { .flatten() } - pub fn windows_install_selected(&self) -> bool { - self.windows_install_selected - } - pub fn should_exit(&self) -> bool { self.should_exit } @@ -249,14 +235,6 @@ impl KeyboardHandler for OnboardingScreen { } } }; - if self - .steps - .iter() - .any(|step| matches!(step, Step::Windows(widget) if widget.exit_requested())) - { - self.windows_install_selected = true; - self.is_done = true; - } self.request_frame.schedule_frame(); } @@ -338,7 +316,6 @@ impl WidgetRef for &OnboardingScreen { impl KeyboardHandler for Step { fn handle_key_event(&mut self, key_event: KeyEvent) { match self { - Step::Windows(widget) => widget.handle_key_event(key_event), Step::Welcome(widget) => widget.handle_key_event(key_event), Step::Auth(widget) => widget.handle_key_event(key_event), Step::TrustDirectory(widget) => widget.handle_key_event(key_event), @@ -347,7 +324,6 @@ impl KeyboardHandler for Step { fn handle_paste(&mut self, pasted: String) { match self { - Step::Windows(_) => {} Step::Welcome(_) => {} Step::Auth(widget) => widget.handle_paste(pasted), Step::TrustDirectory(widget) => widget.handle_paste(pasted), @@ -358,7 +334,6 @@ impl KeyboardHandler for Step { impl StepStateProvider for Step { fn get_step_state(&self) -> StepState { match self { - Step::Windows(w) => w.get_step_state(), Step::Welcome(w) => w.get_step_state(), Step::Auth(w) => w.get_step_state(), Step::TrustDirectory(w) => w.get_step_state(), @@ -369,9 +344,6 @@ impl StepStateProvider for Step { impl WidgetRef for Step { fn render_ref(&self, area: Rect, buf: &mut Buffer) { match self { - Step::Windows(widget) => { - widget.render_ref(area, buf); - } Step::Welcome(widget) => { widget.render_ref(area, buf); } @@ -451,7 +423,6 @@ pub(crate) async fn run_onboarding_app( } Ok(OnboardingResult { directory_trust_decision: onboarding_screen.directory_trust_decision(), - windows_install_selected: onboarding_screen.windows_install_selected(), should_exit: onboarding_screen.should_exit(), }) } diff --git a/codex-rs/tui/src/onboarding/windows.rs b/codex-rs/tui/src/onboarding/windows.rs deleted file mode 100644 index 715611b7e1..0000000000 --- a/codex-rs/tui/src/onboarding/windows.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::path::PathBuf; - -use codex_core::config::edit::ConfigEditsBuilder; -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use crossterm::event::KeyEventKind; -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::prelude::Widget; -use ratatui::style::Color; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; -use ratatui::widgets::WidgetRef; -use ratatui::widgets::Wrap; - -use crate::onboarding::onboarding_screen::KeyboardHandler; -use crate::onboarding::onboarding_screen::StepStateProvider; - -use super::onboarding_screen::StepState; - -pub(crate) const WSL_INSTRUCTIONS: &str = r#"Install WSL2 by opening PowerShell as Administrator and running: - # Install WSL using the default Linux distribution (Ubuntu). - # See https://learn.microsoft.com/en-us/windows/wsl/install for more info - wsl --install - - # Restart your computer, then start a shell inside of Windows Subsystem for Linux - wsl - - # Install Node.js in WSL via nvm - # Documentation: https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-wsl - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash && export NVM_DIR="$HOME/.nvm" && \. "$NVM_DIR/nvm.sh" - nvm install 22 - - # Install and run Codex in WSL - npm install --global @openai/codex - codex - - # Additional details and instructions for how to install and run Codex in WSL: - https://developers.openai.com/codex/windows"#; - -pub(crate) struct WindowsSetupWidget { - pub codex_home: PathBuf, - pub selection: Option, - pub highlighted: WindowsSetupSelection, - pub error: Option, - exit_requested: bool, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum WindowsSetupSelection { - Continue, - Install, -} - -impl WindowsSetupWidget { - pub fn new(codex_home: PathBuf) -> Self { - Self { - codex_home, - selection: None, - highlighted: WindowsSetupSelection::Install, - error: None, - exit_requested: false, - } - } - - fn handle_continue(&mut self) { - self.highlighted = WindowsSetupSelection::Continue; - match ConfigEditsBuilder::new(&self.codex_home) - .set_windows_wsl_setup_acknowledged(true) - .apply_blocking() - { - Ok(()) => { - self.selection = Some(WindowsSetupSelection::Continue); - self.exit_requested = false; - self.error = None; - } - Err(err) => { - tracing::error!("Failed to persist Windows onboarding acknowledgement: {err:?}"); - self.error = Some(format!("Failed to update config: {err}")); - self.selection = None; - } - } - } - - fn handle_install(&mut self) { - self.highlighted = WindowsSetupSelection::Install; - self.selection = Some(WindowsSetupSelection::Install); - self.exit_requested = true; - } - - pub fn exit_requested(&self) -> bool { - self.exit_requested - } -} - -impl WidgetRef for &WindowsSetupWidget { - fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let mut lines: Vec = vec![ - Line::from(vec![ - "> ".into(), - "To use all Codex features, we recommend running Codex in Windows Subsystem for Linux (WSL2)".bold(), - ]), - Line::from(vec![" ".into(), "WSL allows Codex to run Agent mode in a sandboxed environment with better data protections in place.".into()]), - Line::from(vec![" ".into(), "Learn more: https://developers.openai.com/codex/windows".into()]), - Line::from(""), - ]; - - let create_option = - |idx: usize, option: WindowsSetupSelection, text: &str| -> Line<'static> { - if self.highlighted == option { - Line::from(format!("> {}. {text}", idx + 1)).cyan() - } else { - Line::from(format!(" {}. {}", idx + 1, text)) - } - }; - - lines.push(create_option( - 0, - WindowsSetupSelection::Install, - "Exit and install WSL2", - )); - lines.push(create_option( - 1, - WindowsSetupSelection::Continue, - "Continue anyway", - )); - lines.push("".into()); - - if let Some(error) = &self.error { - lines.push(Line::from(format!(" {error}")).fg(Color::Red)); - lines.push("".into()); - } - - lines.push(Line::from(vec![" Press Enter to continue".dim()])); - - Paragraph::new(lines) - .wrap(Wrap { trim: false }) - .render(area, buf); - } -} - -impl KeyboardHandler for WindowsSetupWidget { - fn handle_key_event(&mut self, key_event: KeyEvent) { - if key_event.kind == KeyEventKind::Release { - return; - } - - match key_event.code { - KeyCode::Up | KeyCode::Char('k') => { - self.highlighted = WindowsSetupSelection::Install; - } - KeyCode::Down | KeyCode::Char('j') => { - self.highlighted = WindowsSetupSelection::Continue; - } - KeyCode::Char('1') => self.handle_install(), - KeyCode::Char('2') => self.handle_continue(), - KeyCode::Enter => match self.highlighted { - WindowsSetupSelection::Install => self.handle_install(), - WindowsSetupSelection::Continue => self.handle_continue(), - }, - _ => {} - } - } -} - -impl StepStateProvider for WindowsSetupWidget { - fn get_step_state(&self) -> StepState { - match self.selection { - Some(WindowsSetupSelection::Continue) => StepState::Hidden, - Some(WindowsSetupSelection::Install) => StepState::Complete, - None => StepState::InProgress, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[test] - fn windows_step_hidden_after_continue() { - let temp_dir = TempDir::new().expect("temp dir"); - let mut widget = WindowsSetupWidget::new(temp_dir.path().to_path_buf()); - - assert_eq!(widget.get_step_state(), StepState::InProgress); - - widget.handle_continue(); - - assert_eq!(widget.get_step_state(), StepState::Hidden); - assert!(!widget.exit_requested()); - } - - #[test] - fn windows_step_complete_after_install_selection() { - let temp_dir = TempDir::new().expect("temp dir"); - let mut widget = WindowsSetupWidget::new(temp_dir.path().to_path_buf()); - - widget.handle_install(); - - assert_eq!(widget.get_step_state(), StepState::Complete); - assert!(widget.exit_requested()); - } -} diff --git a/codex-rs/windows-sandbox-rs/src/audit.rs b/codex-rs/windows-sandbox-rs/src/audit.rs index 4cd19dea62..383c652c7c 100644 --- a/codex-rs/windows-sandbox-rs/src/audit.rs +++ b/codex-rs/windows-sandbox-rs/src/audit.rs @@ -1,6 +1,7 @@ use crate::token::world_sid; use crate::winutil::to_wide; use anyhow::Result; +use std::collections::HashMap; use std::collections::HashSet; use std::ffi::c_void; use std::path::Path; @@ -275,6 +276,35 @@ pub fn audit_everyone_writable( ); Ok(Vec::new()) } + +fn normalize_windows_path_for_display(p: impl AsRef) -> String { + let canon = dunce::canonicalize(p.as_ref()).unwrap_or_else(|_| p.as_ref().to_path_buf()); + canon.display().to_string().replace('/', "\\") +} + +pub fn world_writable_warning_details( + codex_home: impl AsRef, +) -> Option<(Vec, usize, bool)> { + let cwd = match std::env::current_dir() { + Ok(cwd) => cwd, + Err(_) => return Some((Vec::new(), 0, true)), + }; + + let env_map: HashMap = std::env::vars().collect(); + match audit_everyone_writable(&cwd, &env_map, Some(codex_home.as_ref())) { + Ok(paths) if paths.is_empty() => None, + Ok(paths) => { + let as_strings: Vec = paths + .iter() + .map(normalize_windows_path_for_display) + .collect(); + let sample_paths: Vec = as_strings.iter().take(3).cloned().collect(); + let extra_count = as_strings.len().saturating_sub(sample_paths.len()); + Some((sample_paths, extra_count, false)) + } + Err(_) => Some((Vec::new(), 0, true)), + } +} // Fast mask-based check: does the DACL contain any ACCESS_ALLOWED ACE for // Everyone that includes generic or specific write bits? Skips inherit-only // ACEs (do not apply to the current object). diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 955f2ca3ce..43e824c670 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -6,6 +6,8 @@ macro_rules! windows_modules { windows_modules!(acl, allow, audit, cap, env, logging, policy, token, winutil); +#[cfg(target_os = "windows")] +pub use audit::world_writable_warning_details; #[cfg(target_os = "windows")] pub use windows_impl::preflight_audit_everyone_writable; #[cfg(target_os = "windows")] @@ -18,6 +20,8 @@ pub use stub::preflight_audit_everyone_writable; #[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_capture; #[cfg(not(target_os = "windows"))] +pub use stub::world_writable_warning_details; +#[cfg(not(target_os = "windows"))] pub use stub::CaptureResult; #[cfg(target_os = "windows")] @@ -453,4 +457,10 @@ mod stub { ) -> Result { bail!("Windows sandbox is only available on Windows") } + + pub fn world_writable_warning_details( + _codex_home: impl AsRef, + ) -> Option<(Vec, usize, bool)> { + None + } }