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 {
|
ApprovalPreset {
|
||||||
id: "read-only",
|
id: "read-only",
|
||||||
label: "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,
|
approval: AskForApproval::OnRequest,
|
||||||
sandbox: SandboxPolicy::ReadOnly,
|
sandbox: SandboxPolicy::ReadOnly,
|
||||||
},
|
},
|
||||||
ApprovalPreset {
|
ApprovalPreset {
|
||||||
id: "auto",
|
id: "auto",
|
||||||
label: "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,
|
approval: AskForApproval::OnRequest,
|
||||||
sandbox: SandboxPolicy::new_workspace_write_policy(),
|
sandbox: SandboxPolicy::new_workspace_write_policy(),
|
||||||
},
|
},
|
||||||
ApprovalPreset {
|
ApprovalPreset {
|
||||||
id: "full-access",
|
id: "full-access",
|
||||||
label: "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,
|
approval: AskForApproval::Never,
|
||||||
sandbox: SandboxPolicy::DangerFullAccess,
|
sandbox: SandboxPolicy::DangerFullAccess,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ pub struct Config {
|
|||||||
/// for either of approval_policy or sandbox_mode.
|
/// for either of approval_policy or sandbox_mode.
|
||||||
pub did_user_set_custom_approval_policy_or_sandbox_mode: bool,
|
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,
|
pub shell_environment_policy: ShellEnvironmentPolicy,
|
||||||
|
|
||||||
/// When `true`, `AgentReasoning` events emitted by the backend will be
|
/// 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 {
|
impl ConfigToml {
|
||||||
/// Derive the effective sandbox policy from the configuration.
|
/// Derive the effective sandbox policy from the configuration.
|
||||||
fn derive_sandbox_policy(
|
fn derive_sandbox_policy(
|
||||||
@@ -1029,7 +1039,7 @@ impl ConfigToml {
|
|||||||
sandbox_mode_override: Option<SandboxMode>,
|
sandbox_mode_override: Option<SandboxMode>,
|
||||||
profile_sandbox_mode: Option<SandboxMode>,
|
profile_sandbox_mode: Option<SandboxMode>,
|
||||||
resolved_cwd: &Path,
|
resolved_cwd: &Path,
|
||||||
) -> SandboxPolicy {
|
) -> SandboxPolicyResolution {
|
||||||
let resolved_sandbox_mode = sandbox_mode_override
|
let resolved_sandbox_mode = sandbox_mode_override
|
||||||
.or(profile_sandbox_mode)
|
.or(profile_sandbox_mode)
|
||||||
.or(self.sandbox_mode)
|
.or(self.sandbox_mode)
|
||||||
@@ -1044,7 +1054,7 @@ impl ConfigToml {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
match resolved_sandbox_mode {
|
let mut sandbox_policy = match resolved_sandbox_mode {
|
||||||
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
|
SandboxMode::ReadOnly => SandboxPolicy::new_read_only_policy(),
|
||||||
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
|
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
|
||||||
Some(SandboxWorkspaceWrite {
|
Some(SandboxWorkspaceWrite {
|
||||||
@@ -1061,6 +1071,17 @@ impl ConfigToml {
|
|||||||
None => SandboxPolicy::new_workspace_write_policy(),
|
None => SandboxPolicy::new_workspace_write_policy(),
|
||||||
},
|
},
|
||||||
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
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)
|
.get_active_project(&resolved_cwd)
|
||||||
.unwrap_or(ProjectConfig { trust_level: None });
|
.unwrap_or(ProjectConfig { trust_level: None });
|
||||||
|
|
||||||
let mut sandbox_policy =
|
let SandboxPolicyResolution {
|
||||||
cfg.derive_sandbox_policy(sandbox_mode, config_profile.sandbox_mode, &resolved_cwd);
|
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 {
|
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||||
for path in additional_writable_roots {
|
for path in additional_writable_roots {
|
||||||
if !writable_roots.iter().any(|existing| existing == &path) {
|
if !writable_roots.iter().any(|existing| existing == &path) {
|
||||||
@@ -1353,6 +1376,7 @@ impl Config {
|
|||||||
approval_policy,
|
approval_policy,
|
||||||
sandbox_policy,
|
sandbox_policy,
|
||||||
did_user_set_custom_approval_policy_or_sandbox_mode,
|
did_user_set_custom_approval_policy_or_sandbox_mode,
|
||||||
|
forced_auto_mode_downgraded_on_windows,
|
||||||
shell_environment_policy,
|
shell_environment_policy,
|
||||||
notify: cfg.notify,
|
notify: cfg.notify,
|
||||||
user_instructions,
|
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)
|
let sandbox_full_access_cfg = toml::from_str::<ConfigToml>(sandbox_full_access)
|
||||||
.expect("TOML deserialization should succeed");
|
.expect("TOML deserialization should succeed");
|
||||||
let sandbox_mode_override = None;
|
let sandbox_mode_override = None;
|
||||||
|
let resolution = sandbox_full_access_cfg.derive_sandbox_policy(
|
||||||
|
sandbox_mode_override,
|
||||||
|
None,
|
||||||
|
&PathBuf::from("/tmp/test"),
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SandboxPolicy::DangerFullAccess,
|
resolution,
|
||||||
sandbox_full_access_cfg.derive_sandbox_policy(
|
SandboxPolicyResolution {
|
||||||
sandbox_mode_override,
|
policy: SandboxPolicy::DangerFullAccess,
|
||||||
None,
|
forced_auto_mode_downgraded_on_windows: false,
|
||||||
&PathBuf::from("/tmp/test")
|
}
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let sandbox_read_only = r#"
|
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)
|
let sandbox_read_only_cfg = toml::from_str::<ConfigToml>(sandbox_read_only)
|
||||||
.expect("TOML deserialization should succeed");
|
.expect("TOML deserialization should succeed");
|
||||||
let sandbox_mode_override = None;
|
let sandbox_mode_override = None;
|
||||||
|
let resolution = sandbox_read_only_cfg.derive_sandbox_policy(
|
||||||
|
sandbox_mode_override,
|
||||||
|
None,
|
||||||
|
&PathBuf::from("/tmp/test"),
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SandboxPolicy::ReadOnly,
|
resolution,
|
||||||
sandbox_read_only_cfg.derive_sandbox_policy(
|
SandboxPolicyResolution {
|
||||||
sandbox_mode_override,
|
policy: SandboxPolicy::ReadOnly,
|
||||||
None,
|
forced_auto_mode_downgraded_on_windows: false,
|
||||||
&PathBuf::from("/tmp/test")
|
}
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let sandbox_workspace_write = r#"
|
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)
|
let sandbox_workspace_write_cfg = toml::from_str::<ConfigToml>(sandbox_workspace_write)
|
||||||
.expect("TOML deserialization should succeed");
|
.expect("TOML deserialization should succeed");
|
||||||
let sandbox_mode_override = None;
|
let sandbox_mode_override = None;
|
||||||
assert_eq!(
|
let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy(
|
||||||
SandboxPolicy::WorkspaceWrite {
|
sandbox_mode_override,
|
||||||
writable_roots: vec![PathBuf::from("/my/workspace")],
|
None,
|
||||||
network_access: false,
|
&PathBuf::from("/tmp/test"),
|
||||||
exclude_tmpdir_env_var: true,
|
|
||||||
exclude_slash_tmp: true,
|
|
||||||
},
|
|
||||||
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#"
|
let sandbox_workspace_write = r#"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
@@ -1677,19 +1723,33 @@ trust_level = "trusted"
|
|||||||
let sandbox_workspace_write_cfg = toml::from_str::<ConfigToml>(sandbox_workspace_write)
|
let sandbox_workspace_write_cfg = toml::from_str::<ConfigToml>(sandbox_workspace_write)
|
||||||
.expect("TOML deserialization should succeed");
|
.expect("TOML deserialization should succeed");
|
||||||
let sandbox_mode_override = None;
|
let sandbox_mode_override = None;
|
||||||
assert_eq!(
|
let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy(
|
||||||
SandboxPolicy::WorkspaceWrite {
|
sandbox_mode_override,
|
||||||
writable_roots: vec![PathBuf::from("/my/workspace")],
|
None,
|
||||||
network_access: false,
|
&PathBuf::from("/tmp/test"),
|
||||||
exclude_tmpdir_env_var: true,
|
|
||||||
exclude_slash_tmp: true,
|
|
||||||
},
|
|
||||||
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]
|
#[test]
|
||||||
@@ -1714,19 +1774,30 @@ trust_level = "trusted"
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let expected_backend = canonicalize(&backend).expect("canonicalize backend directory");
|
let expected_backend = canonicalize(&backend).expect("canonicalize backend directory");
|
||||||
match config.sandbox_policy {
|
if cfg!(target_os = "windows") {
|
||||||
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
|
assert!(
|
||||||
assert_eq!(
|
config.forced_auto_mode_downgraded_on_windows,
|
||||||
writable_roots
|
"expected workspace-write request to be downgraded on Windows"
|
||||||
.iter()
|
);
|
||||||
.filter(|root| **root == expected_backend)
|
match config.sandbox_policy {
|
||||||
.count(),
|
SandboxPolicy::ReadOnly => {}
|
||||||
1,
|
other => panic!("expected read-only policy on Windows, got {other:?}"),
|
||||||
"expected single writable root entry for {}",
|
}
|
||||||
expected_backend.display()
|
} 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(())
|
Ok(())
|
||||||
@@ -1841,10 +1912,16 @@ trust_level = "trusted"
|
|||||||
codex_home.path().to_path_buf(),
|
codex_home.path().to_path_buf(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
assert!(matches!(
|
if cfg!(target_os = "windows") {
|
||||||
config.sandbox_policy,
|
assert!(matches!(config.sandbox_policy, SandboxPolicy::ReadOnly));
|
||||||
SandboxPolicy::WorkspaceWrite { .. }
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2943,6 +3020,7 @@ model_verbosity = "high"
|
|||||||
approval_policy: AskForApproval::Never,
|
approval_policy: AskForApproval::Never,
|
||||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||||
|
forced_auto_mode_downgraded_on_windows: false,
|
||||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||||
user_instructions: None,
|
user_instructions: None,
|
||||||
notify: None,
|
notify: None,
|
||||||
@@ -3012,6 +3090,7 @@ model_verbosity = "high"
|
|||||||
approval_policy: AskForApproval::UnlessTrusted,
|
approval_policy: AskForApproval::UnlessTrusted,
|
||||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||||
|
forced_auto_mode_downgraded_on_windows: false,
|
||||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||||
user_instructions: None,
|
user_instructions: None,
|
||||||
notify: None,
|
notify: None,
|
||||||
@@ -3096,6 +3175,7 @@ model_verbosity = "high"
|
|||||||
approval_policy: AskForApproval::OnFailure,
|
approval_policy: AskForApproval::OnFailure,
|
||||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||||
|
forced_auto_mode_downgraded_on_windows: false,
|
||||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||||
user_instructions: None,
|
user_instructions: None,
|
||||||
notify: None,
|
notify: None,
|
||||||
@@ -3166,6 +3246,7 @@ model_verbosity = "high"
|
|||||||
approval_policy: AskForApproval::OnFailure,
|
approval_policy: AskForApproval::OnFailure,
|
||||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||||
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
did_user_set_custom_approval_policy_or_sandbox_mode: true,
|
||||||
|
forced_auto_mode_downgraded_on_windows: false,
|
||||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||||
user_instructions: None,
|
user_instructions: None,
|
||||||
notify: None,
|
notify: None,
|
||||||
|
|||||||
@@ -234,10 +234,17 @@ fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
|
|||||||
stderr.contains("model: gpt-5-high"),
|
stderr.contains("model: gpt-5-high"),
|
||||||
"stderr missing model override: {stderr}"
|
"stderr missing model override: {stderr}"
|
||||||
);
|
);
|
||||||
assert!(
|
if cfg!(target_os = "windows") {
|
||||||
stderr.contains("sandbox: workspace-write"),
|
assert!(
|
||||||
"stderr missing sandbox override: {stderr}"
|
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)
|
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
|
||||||
.expect("no resumed session file containing marker2");
|
.expect("no resumed session file containing marker2");
|
||||||
|
|||||||
@@ -369,6 +369,9 @@ impl App {
|
|||||||
AppEvent::OpenFeedbackConsent { category } => {
|
AppEvent::OpenFeedbackConsent { category } => {
|
||||||
self.chat_widget.open_feedback_consent(category);
|
self.chat_widget.open_feedback_consent(category);
|
||||||
}
|
}
|
||||||
|
AppEvent::ShowWindowsAutoModeInstructions => {
|
||||||
|
self.chat_widget.open_windows_auto_mode_instructions();
|
||||||
|
}
|
||||||
AppEvent::PersistModelSelection { model, effort } => {
|
AppEvent::PersistModelSelection { model, effort } => {
|
||||||
let profile = self.active_profile.as_deref();
|
let profile = self.active_profile.as_deref();
|
||||||
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
|
match persist_model_selection(&self.config.codex_home, profile, &model, effort)
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ pub(crate) enum AppEvent {
|
|||||||
preset: ApprovalPreset,
|
preset: ApprovalPreset,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Show Windows Subsystem for Linux setup instructions for auto mode.
|
||||||
|
ShowWindowsAutoModeInstructions,
|
||||||
|
|
||||||
/// Update the current approval policy in the running app and widget.
|
/// Update the current approval policy in the running app and widget.
|
||||||
UpdateAskForApprovalPolicy(AskForApproval),
|
UpdateAskForApprovalPolicy(AskForApproval),
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ use crate::history_cell::AgentMessageCell;
|
|||||||
use crate::history_cell::HistoryCell;
|
use crate::history_cell::HistoryCell;
|
||||||
use crate::history_cell::McpToolCallCell;
|
use crate::history_cell::McpToolCallCell;
|
||||||
use crate::markdown::append_markdown;
|
use crate::markdown::append_markdown;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use crate::onboarding::WSL_INSTRUCTIONS;
|
||||||
use crate::render::renderable::ColumnRenderable;
|
use crate::render::renderable::ColumnRenderable;
|
||||||
use crate::render::renderable::Renderable;
|
use crate::render::renderable::Renderable;
|
||||||
use crate::slash_command::SlashCommand;
|
use crate::slash_command::SlashCommand;
|
||||||
@@ -1787,11 +1789,38 @@ impl ChatWidget {
|
|||||||
let current_sandbox = self.config.sandbox_policy.clone();
|
let current_sandbox = self.config.sandbox_policy.clone();
|
||||||
let mut items: Vec<SelectionItem> = Vec::new();
|
let mut items: Vec<SelectionItem> = Vec::new();
|
||||||
let presets: Vec<ApprovalPreset> = builtin_approval_presets();
|
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() {
|
for preset in presets.into_iter() {
|
||||||
let is_current =
|
let is_current =
|
||||||
current_approval == preset.approval && current_sandbox == preset.sandbox;
|
current_approval == preset.approval && current_sandbox == preset.sandbox;
|
||||||
let name = preset.label.to_string();
|
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"
|
let requires_confirmation = preset.id == "full-access"
|
||||||
&& !self
|
&& !self
|
||||||
.config
|
.config
|
||||||
@@ -1805,6 +1834,10 @@ impl ChatWidget {
|
|||||||
preset: preset_clone.clone(),
|
preset: preset_clone.clone(),
|
||||||
});
|
});
|
||||||
})]
|
})]
|
||||||
|
} else if cfg!(target_os = "windows") && preset.id == "auto" {
|
||||||
|
vec![Box::new(|tx| {
|
||||||
|
tx.send(AppEvent::ShowWindowsAutoModeInstructions);
|
||||||
|
})]
|
||||||
} else {
|
} else {
|
||||||
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
|
Self::approval_preset_actions(preset.approval, preset.sandbox.clone())
|
||||||
};
|
};
|
||||||
@@ -1822,6 +1855,7 @@ impl ChatWidget {
|
|||||||
title: Some("Select Approval Mode".to_string()),
|
title: Some("Select Approval Mode".to_string()),
|
||||||
footer_hint: Some(standard_popup_hint_line()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items,
|
items,
|
||||||
|
header: header_renderable,
|
||||||
..Default::default()
|
..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.
|
/// Set the approval policy in the widget's config copy.
|
||||||
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) {
|
||||||
self.config.approval_policy = policy;
|
self.config.approval_policy = policy;
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ expression: popup
|
|||||||
|
|
||||||
› 1. Read Only (current) Codex can read files and answer questions. Codex
|
› 1. Read Only (current) Codex can read files and answer questions. Codex
|
||||||
requires approval to make edits, run commands, or
|
requires approval to make edits, run commands, or
|
||||||
access network
|
access network.
|
||||||
2. Auto Codex can read files, make edits, and run commands
|
2. Auto Codex can read files, make edits, and run commands
|
||||||
in the workspace. Codex requires approval to work
|
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
|
3. Full Access Codex can read files, make edits, and run commands
|
||||||
with network access, without approval. Exercise
|
with network access, without approval. Exercise
|
||||||
caution
|
caution.
|
||||||
|
|
||||||
Press enter to confirm or esc to go back
|
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();
|
chat.open_approvals_popup();
|
||||||
|
|
||||||
let popup = render_bottom_popup(&chat, 80);
|
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);
|
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]
|
#[test]
|
||||||
fn full_access_confirmation_popup_snapshot() {
|
fn full_access_confirmation_popup_snapshot() {
|
||||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
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);
|
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]
|
#[test]
|
||||||
fn model_reasoning_selection_popup_snapshot() {
|
fn model_reasoning_selection_popup_snapshot() {
|
||||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
|
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
|
/// or if the current cwd project is already trusted. If not, we need to
|
||||||
/// show the trust screen.
|
/// show the trust screen.
|
||||||
fn should_show_trust_screen(config: &Config) -> bool {
|
fn should_show_trust_screen(config: &Config) -> bool {
|
||||||
if config.did_user_set_custom_approval_policy_or_sandbox_mode {
|
if cfg!(target_os = "windows") {
|
||||||
// if the user has overridden either approval policy or sandbox mode,
|
// Native Windows cannot enforce sandboxed write access without WSL; skip the trust prompt entirely.
|
||||||
// skip the trust flow
|
return false;
|
||||||
false
|
|
||||||
} else {
|
|
||||||
// otherwise, skip iff the active project is trusted
|
|
||||||
!config.active_project.is_trusted()
|
|
||||||
}
|
}
|
||||||
|
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(
|
fn should_show_onboarding(
|
||||||
@@ -543,3 +545,38 @@ fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool
|
|||||||
|
|
||||||
login_status == LoginStatus::NotAuthenticated
|
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