feat(tui): clarify Windows auto mode requirements (#5568)
## Summary - Coerce Windows `workspace-write` configs back to read-only, surface the forced downgrade in the approvals popup, and funnel users toward WSL or Full Access. - Add WSL installation instructions to the Auto preset on Windows while keeping the preset available for other platforms. - Skip the trust-on-first-run prompt on native Windows so new folders remain read-only without additional confirmation. - Expose a structured sandbox policy resolution from config to flag Windows downgrades and adjust tests (core, exec, TUI) to reflect the new behavior; provide a Windows-only approvals snapshot. ## Testing - cargo fmt - cargo test -p codex-core config::tests::add_dir_override_extends_workspace_writable_roots - cargo test -p codex-exec suite::resume::exec_resume_preserves_cli_configuration_overrides - cargo test -p codex-tui chatwidget::tests::approvals_selection_popup_snapshot - cargo test -p codex-tui approvals_popup_includes_wsl_note_for_auto_mode - cargo test -p codex-tui windows_skips_trust_prompt - just fix -p codex-core - just fix -p codex-tui
This commit is contained in:
@@ -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: "Codex can read files and answer questions. Codex requires approval to make edits, run commands, or access network.",
|
||||
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",
|
||||
description: "Codex can read files, make edits, and run commands in the workspace. Codex requires approval to work outside the workspace or access network.",
|
||||
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",
|
||||
description: "Codex can read files, make edits, and run commands with network access, without approval. Exercise caution.",
|
||||
approval: AskForApproval::Never,
|
||||
sandbox: SandboxPolicy::DangerFullAccess,
|
||||
},
|
||||
|
||||
@@ -108,6 +108,10 @@ pub struct Config {
|
||||
/// for either of approval_policy or sandbox_mode.
|
||||
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
|
||||
|
||||
/// On Windows, indicates that a previously configured workspace-write sandbox
|
||||
/// was coerced to read-only because native auto mode is unsupported.
|
||||
pub forced_auto_mode_downgraded_on_windows: bool,
|
||||
|
||||
pub shell_environment_policy: ShellEnvironmentPolicy,
|
||||
|
||||
/// When `true`, `AgentReasoning` events emitted by the backend will be
|
||||
@@ -1022,6 +1026,12 @@ impl From<ToolsToml> for Tools {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SandboxPolicyResolution {
|
||||
pub policy: SandboxPolicy,
|
||||
pub forced_auto_mode_downgraded_on_windows: bool,
|
||||
}
|
||||
|
||||
impl ConfigToml {
|
||||
/// Derive the effective sandbox policy from the configuration.
|
||||
fn derive_sandbox_policy(
|
||||
@@ -1029,7 +1039,7 @@ impl ConfigToml {
|
||||
sandbox_mode_override: Option<SandboxMode>,
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
resolved_cwd: &Path,
|
||||
) -> SandboxPolicy {
|
||||
) -> SandboxPolicyResolution {
|
||||
let resolved_sandbox_mode = sandbox_mode_override
|
||||
.or(profile_sandbox_mode)
|
||||
.or(self.sandbox_mode)
|
||||
@@ -1044,7 +1054,7 @@ impl ConfigToml {
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
match resolved_sandbox_mode {
|
||||
let mut sandbox_policy = match resolved_sandbox_mode {
|
||||
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
|
||||
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
|
||||
Some(SandboxWorkspaceWrite {
|
||||
@@ -1061,6 +1071,17 @@ impl ConfigToml {
|
||||
None => SandboxPolicy::new_workspace_write_policy(),
|
||||
},
|
||||
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||||
};
|
||||
let mut forced_auto_mode_downgraded_on_windows = false;
|
||||
if cfg!(target_os = "windows")
|
||||
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
|
||||
{
|
||||
sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
forced_auto_mode_downgraded_on_windows = true;
|
||||
}
|
||||
SandboxPolicyResolution {
|
||||
policy: sandbox_policy,
|
||||
forced_auto_mode_downgraded_on_windows,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1221,8 +1242,10 @@ impl Config {
|
||||
.get_active_project(&resolved_cwd)
|
||||
.unwrap_or(ProjectConfig { trust_level: None });
|
||||
|
||||
let mut sandbox_policy =
|
||||
cfg.derive_sandbox_policy(sandbox_mode, config_profile.sandbox_mode, &resolved_cwd);
|
||||
let SandboxPolicyResolution {
|
||||
policy: mut sandbox_policy,
|
||||
forced_auto_mode_downgraded_on_windows,
|
||||
} = cfg.derive_sandbox_policy(sandbox_mode, config_profile.sandbox_mode, &resolved_cwd);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
for path in additional_writable_roots {
|
||||
if !writable_roots.iter().any(|existing| existing == &path) {
|
||||
@@ -1353,6 +1376,7 @@ impl Config {
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode,
|
||||
forced_auto_mode_downgraded_on_windows,
|
||||
shell_environment_policy,
|
||||
notify: cfg.notify,
|
||||
user_instructions,
|
||||
@@ -1604,13 +1628,17 @@ network_access = false # This should be ignored.
|
||||
let sandbox_full_access_cfg = toml::from_str::<ConfigToml>(sandbox_full_access)
|
||||
.expect("TOML deserialization should succeed");
|
||||
let sandbox_mode_override = None;
|
||||
let resolution = sandbox_full_access_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
);
|
||||
assert_eq!(
|
||||
SandboxPolicy::DangerFullAccess,
|
||||
sandbox_full_access_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test")
|
||||
)
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::DangerFullAccess,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
|
||||
let sandbox_read_only = r#"
|
||||
@@ -1623,13 +1651,17 @@ network_access = true # This should be ignored.
|
||||
let sandbox_read_only_cfg = toml::from_str::<ConfigToml>(sandbox_read_only)
|
||||
.expect("TOML deserialization should succeed");
|
||||
let sandbox_mode_override = None;
|
||||
let resolution = sandbox_read_only_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
);
|
||||
assert_eq!(
|
||||
SandboxPolicy::ReadOnly,
|
||||
sandbox_read_only_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test")
|
||||
)
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
|
||||
let sandbox_workspace_write = r#"
|
||||
@@ -1646,19 +1678,33 @@ exclude_slash_tmp = true
|
||||
let sandbox_workspace_write_cfg = toml::from_str::<ConfigToml>(sandbox_workspace_write)
|
||||
.expect("TOML deserialization should succeed");
|
||||
let sandbox_mode_override = None;
|
||||
assert_eq!(
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![PathBuf::from("/my/workspace")],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
sandbox_workspace_write_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test")
|
||||
)
|
||||
let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: true,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![PathBuf::from("/my/workspace")],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let sandbox_workspace_write = r#"
|
||||
sandbox_mode = "workspace-write"
|
||||
@@ -1677,19 +1723,33 @@ trust_level = "trusted"
|
||||
let sandbox_workspace_write_cfg = toml::from_str::<ConfigToml>(sandbox_workspace_write)
|
||||
.expect("TOML deserialization should succeed");
|
||||
let sandbox_mode_override = None;
|
||||
assert_eq!(
|
||||
SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![PathBuf::from("/my/workspace")],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
sandbox_workspace_write_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test")
|
||||
)
|
||||
let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy(
|
||||
sandbox_mode_override,
|
||||
None,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: true,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::WorkspaceWrite {
|
||||
writable_roots: vec![PathBuf::from("/my/workspace")],
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: true,
|
||||
exclude_slash_tmp: true,
|
||||
},
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1714,19 +1774,30 @@ trust_level = "trusted"
|
||||
)?;
|
||||
|
||||
let expected_backend = canonicalize(&backend).expect("canonicalize backend directory");
|
||||
match config.sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
assert_eq!(
|
||||
writable_roots
|
||||
.iter()
|
||||
.filter(|root| **root == expected_backend)
|
||||
.count(),
|
||||
1,
|
||||
"expected single writable root entry for {}",
|
||||
expected_backend.display()
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
config.forced_auto_mode_downgraded_on_windows,
|
||||
"expected workspace-write request to be downgraded on Windows"
|
||||
);
|
||||
match config.sandbox_policy {
|
||||
SandboxPolicy::ReadOnly => {}
|
||||
other => panic!("expected read-only policy on Windows, got {other:?}"),
|
||||
}
|
||||
} else {
|
||||
match config.sandbox_policy {
|
||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
||||
assert_eq!(
|
||||
writable_roots
|
||||
.iter()
|
||||
.filter(|root| **root == expected_backend)
|
||||
.count(),
|
||||
1,
|
||||
"expected single writable root entry for {}",
|
||||
expected_backend.display()
|
||||
);
|
||||
}
|
||||
other => panic!("expected workspace-write policy, got {other:?}"),
|
||||
}
|
||||
other => panic!("expected workspace-write policy, got {other:?}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1841,10 +1912,16 @@ trust_level = "trusted"
|
||||
codex_home.path().to_path_buf(),
|
||||
)?;
|
||||
|
||||
assert!(matches!(
|
||||
config.sandbox_policy,
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
));
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(matches!(config.sandbox_policy, SandboxPolicy::ReadOnly));
|
||||
assert!(config.forced_auto_mode_downgraded_on_windows);
|
||||
} else {
|
||||
assert!(matches!(
|
||||
config.sandbox_policy,
|
||||
SandboxPolicy::WorkspaceWrite { .. }
|
||||
));
|
||||
assert!(!config.forced_auto_mode_downgraded_on_windows);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2943,6 +3020,7 @@ model_verbosity = "high"
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -3012,6 +3090,7 @@ model_verbosity = "high"
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -3096,6 +3175,7 @@ model_verbosity = "high"
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
@@ -3166,6 +3246,7 @@ model_verbosity = "high"
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
user_instructions: None,
|
||||
notify: None,
|
||||
|
||||
@@ -234,10 +234,17 @@ fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
|
||||
stderr.contains("model: gpt-5-high"),
|
||||
"stderr missing model override: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("sandbox: workspace-write"),
|
||||
"stderr missing sandbox override: {stderr}"
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
stderr.contains("sandbox: read-only"),
|
||||
"stderr missing downgraded sandbox note: {stderr}"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
stderr.contains("sandbox: workspace-write"),
|
||||
"stderr missing sandbox override: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
||||
.expect("no resumed session file containing marker2");
|
||||
|
||||
@@ -369,6 +369,9 @@ impl App {
|
||||
AppEvent::OpenFeedbackConsent { category } => {
|
||||
self.chat_widget.open_feedback_consent(category);
|
||||
}
|
||||
AppEvent::ShowWindowsAutoModeInstructions => {
|
||||
self.chat_widget.open_windows_auto_mode_instructions();
|
||||
}
|
||||
AppEvent::PersistModelSelection { model, effort } => {
|
||||
let profile = self.active_profile.as_deref();
|
||||
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
|
||||
|
||||
@@ -72,6 +72,9 @@ pub(crate) enum AppEvent {
|
||||
preset: ApprovalPreset,
|
||||
},
|
||||
|
||||
/// Show Windows Subsystem for Linux setup instructions for auto mode.
|
||||
ShowWindowsAutoModeInstructions,
|
||||
|
||||
/// Update the current approval policy in the running app and widget.
|
||||
UpdateAskForApprovalPolicy(AskForApproval),
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ use crate::history_cell::AgentMessageCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::McpToolCallCell;
|
||||
use crate::markdown::append_markdown;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::onboarding::WSL_INSTRUCTIONS;
|
||||
use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::slash_command::SlashCommand;
|
||||
@@ -1787,11 +1789,38 @@ impl ChatWidget {
|
||||
let current_sandbox = self.config.sandbox_policy.clone();
|
||||
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(())
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let header_renderable: Box<dyn Renderable> = Box::new(());
|
||||
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 = Some(preset.description.to_string());
|
||||
let description_text = preset.description;
|
||||
let description = if cfg!(target_os = "windows") && preset.id == "auto" {
|
||||
Some(format!(
|
||||
"{description_text}\nRequires Windows Subsystem for Linux (WSL). Show installation instructions..."
|
||||
))
|
||||
} else {
|
||||
Some(description_text.to_string())
|
||||
};
|
||||
let requires_confirmation = preset.id == "full-access"
|
||||
&& !self
|
||||
.config
|
||||
@@ -1805,6 +1834,10 @@ impl ChatWidget {
|
||||
preset: preset_clone.clone(),
|
||||
});
|
||||
})]
|
||||
} else if cfg!(target_os = "windows") && preset.id == "auto" {
|
||||
vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::ShowWindowsAutoModeInstructions);
|
||||
})]
|
||||
} else {
|
||||
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
|
||||
};
|
||||
@@ -1822,6 +1855,7 @@ impl ChatWidget {
|
||||
title: Some("Select Approval Mode".to_string()),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
header: header_renderable,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -1909,6 +1943,43 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn open_windows_auto_mode_instructions(&mut self) {
|
||||
use ratatui_macros::line;
|
||||
|
||||
let mut header = ColumnRenderable::new();
|
||||
header.push(line![
|
||||
"Auto mode requires Windows Subsystem for Linux (WSL2).".bold()
|
||||
]);
|
||||
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 items = vec![SelectionItem {
|
||||
name: "Back".to_string(),
|
||||
description: Some(
|
||||
"Return to the approval mode list. Auto mode stays disabled outside WSL."
|
||||
.to_string(),
|
||||
),
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::OpenApprovalsPopup);
|
||||
})],
|
||||
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),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn open_windows_auto_mode_instructions(&mut self) {}
|
||||
|
||||
/// Set the approval policy in the widget's config copy.
|
||||
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
||||
self.config.approval_policy = policy;
|
||||
|
||||
@@ -6,12 +6,12 @@ expression: popup
|
||||
|
||||
› 1. Read Only (current) Codex can read files and answer questions. Codex
|
||||
requires approval to make edits, run commands, or
|
||||
access network
|
||||
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
|
||||
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
|
||||
caution.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
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.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -1276,9 +1276,36 @@ fn approvals_selection_popup_snapshot() {
|
||||
chat.open_approvals_popup();
|
||||
|
||||
let popup = render_bottom_popup(&chat, 80);
|
||||
#[cfg(target_os = "windows")]
|
||||
insta::with_settings!({ snapshot_suffix => "windows" }, {
|
||||
assert_snapshot!("approvals_selection_popup", popup);
|
||||
});
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
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();
|
||||
@@ -1293,6 +1320,20 @@ fn full_access_confirmation_popup_snapshot() {
|
||||
assert_snapshot!("full_access_confirmation_popup", popup);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_auto_mode_instructions_popup_lists_install_steps() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
chat.open_windows_auto_mode_instructions();
|
||||
|
||||
let popup = render_bottom_popup(&chat, 120);
|
||||
assert!(
|
||||
popup.contains("wsl --install"),
|
||||
"expected WSL instructions popup to include install command, popup: {popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_reasoning_selection_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
||||
|
||||
@@ -507,14 +507,16 @@ async fn load_config_or_exit(
|
||||
/// or if the current cwd project is already trusted. If not, we need to
|
||||
/// show the trust screen.
|
||||
fn should_show_trust_screen(config: &Config) -> bool {
|
||||
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
|
||||
// if the user has overridden either approval policy or sandbox mode,
|
||||
// skip the trust flow
|
||||
false
|
||||
} else {
|
||||
// otherwise, skip iff the active project is trusted
|
||||
!config.active_project.is_trusted()
|
||||
if cfg!(target_os = "windows") {
|
||||
// Native Windows cannot enforce sandboxed write access without WSL; skip the trust prompt entirely.
|
||||
return false;
|
||||
}
|
||||
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
|
||||
// Respect explicit approval/sandbox overrides made by the user.
|
||||
return false;
|
||||
}
|
||||
// otherwise, skip iff the active project is trusted
|
||||
!config.active_project.is_trusted()
|
||||
}
|
||||
|
||||
fn should_show_onboarding(
|
||||
@@ -543,3 +545,38 @@ fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool
|
||||
|
||||
login_status == LoginStatus::NotAuthenticated
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigToml;
|
||||
use codex_core::config::ProjectConfig;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn windows_skips_trust_prompt() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
temp_dir.path().to_path_buf(),
|
||||
)?;
|
||||
config.did_user_set_custom_approval_policy_or_sandbox_mode = false;
|
||||
config.active_project = ProjectConfig { trust_level: None };
|
||||
|
||||
let should_show = should_show_trust_screen(&config);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert!(
|
||||
!should_show,
|
||||
"Windows trust prompt should always be skipped on native Windows"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
should_show,
|
||||
"Non-Windows should still show trust prompt when project is untrusted"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user