Prompt to turn on windows sandbox when auto mode selected. (#6618)

- stop prompting users to install WSL 
- prompt users to turn on Windows sandbox when auto mode requested.

<img width="1660" height="195" alt="Screenshot 2025-11-17 110612"
src="https://github.com/user-attachments/assets/c67fc239-a227-417e-94bb-599a8ed8f11e"
/>
<img width="1684" height="168" alt="Screenshot 2025-11-17 110637"
src="https://github.com/user-attachments/assets/d18c3370-830d-4971-8746-04757ae2f709"
/>
<img width="1655" height="293" alt="Screenshot 2025-11-17 110719"
src="https://github.com/user-attachments/assets/d21f6ce9-c23e-4842-baf6-8938b77c16db"
/>
This commit is contained in:
iceweasel-oai
2025-11-18 11:38:18 -08:00
committed by GitHub
parent 3de8790714
commit 4bada5a84d
16 changed files with 298 additions and 428 deletions

View File

@@ -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()));

View File

@@ -24,21 +24,21 @@ pub fn builtin_approval_presets() -> Vec<ApprovalPreset> {
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,
},

View File

@@ -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)

View File

@@ -1320,6 +1320,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 {

View File

@@ -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);
@@ -537,8 +543,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();
@@ -593,6 +662,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.
@@ -868,7 +944,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",
)));

View File

@@ -91,9 +91,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),

View File

@@ -95,8 +95,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;
@@ -2172,40 +2170,16 @@ impl ChatWidget {
let mut items: Vec<SelectionItem> = Vec::new();
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
#[cfg(target_os = "windows")]
let header_renderable: Box<dyn Renderable> = 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<dyn Renderable> = 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
@@ -2223,53 +2197,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<String, String> =
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<String> = paths
.iter()
.map(|p| normalize_windows_path_for_display(p))
.collect();
let samples: Vec<String> =
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()),
@@ -2300,10 +2237,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()
});
}
@@ -2328,20 +2272,22 @@ impl ChatWidget {
}
#[cfg(target_os = "windows")]
fn windows_world_writable_flagged(&self) -> bool {
use std::collections::HashMap;
let mut env_map: HashMap<String, String> = 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<String>, 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<String>, usize, bool)> {
None
}
pub(crate) fn open_full_access_confirmation(&mut self, preset: ApprovalPreset) {
@@ -2426,7 +2372,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. "
@@ -2443,7 +2388,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 }),
));
@@ -2452,8 +2396,9 @@ impl ChatWidget {
// Show up to three examples and optionally an "and X more" line.
let mut lines: Vec<Line> = 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")));
@@ -2521,21 +2466,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| {
@@ -2555,7 +2512,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) {
@@ -2564,7 +2545,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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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(
@@ -1456,28 +1461,6 @@ fn approvals_selection_popup_snapshot() {
assert_snapshot!("approvals_selection_popup", popup);
}
#[test]
fn approvals_popup_includes_wsl_note_for_auto_mode() {
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 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}"
);
}
#[test]
fn full_access_confirmation_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
@@ -1494,18 +1477,41 @@ fn full_access_confirmation_popup_snapshot() {
#[cfg(target_os = "windows")]
#[test]
fn windows_auto_mode_instructions_popup_lists_install_steps() {
fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
chat.open_windows_auto_mode_instructions();
let preset = builtin_approval_presets()
.into_iter()
.find(|preset| preset.id == "auto")
.expect("auto preset");
chat.open_windows_sandbox_enable_prompt(preset);
let popup = render_bottom_popup(&chat, 120);
assert!(
popup.contains("wsl --install"),
"expected WSL instructions popup to include install command, popup: {popup}"
popup.contains("experimental Windows sandbox"),
"expected auto mode prompt to mention enabling the sandbox feature, popup: {popup}"
);
}
#[cfg(target_os = "windows")]
#[test]
fn startup_prompts_for_windows_sandbox_when_auto_requested() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
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("Turn on Windows sandbox and use Auto mode"),
"expected startup prompt to offer enabling the sandbox: {popup}"
);
set_windows_sandbox_enabled(true);
}
#[test]
fn model_reasoning_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();

View File

@@ -86,7 +86,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;
@@ -389,20 +389,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(),
@@ -421,21 +414,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
@@ -584,7 +568,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 {
@@ -599,12 +583,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;
}
@@ -628,7 +607,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;
@@ -643,7 +621,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") {
@@ -670,7 +648,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") {

View File

@@ -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;

View File

@@ -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<Step>,
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<TrustDirectorySelection>,
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<Step> = 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(),
})
}

View File

@@ -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<WindowsSetupSelection>,
pub highlighted: WindowsSetupSelection,
pub error: Option<String>,
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<Line> = 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());
}
}

View File

@@ -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<Path>) -> 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<Path>,
) -> Option<(Vec<String>, usize, bool)> {
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(_) => return Some((Vec::new(), 0, true)),
};
let env_map: HashMap<String, String> = 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<String> = paths
.iter()
.map(normalize_windows_path_for_display)
.collect();
let sample_paths: Vec<String> = 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).

View File

@@ -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")]
@@ -455,4 +459,10 @@ mod stub {
) -> Result<CaptureResult> {
bail!("Windows sandbox is only available on Windows")
}
pub fn world_writable_warning_details(
_codex_home: impl AsRef<Path>,
) -> Option<(Vec<String>, usize, bool)> {
None
}
}