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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user