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:
Josh McKinney
2025-10-27 18:19:32 -07:00
committed by GitHub
parent d7b333be97
commit 66a4b89822
10 changed files with 336 additions and 74 deletions

View File

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