From 73db60e71f7b29de2cdcbf66cb32dd5e83e155c2 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 16 Sep 2026 00:35:59 +0000 Subject: [PATCH] Use app-server state for TUI Windows sandbox decisions (#45821) ## Why Windows sandbox setup and permission choices need to reflect the connected app server's requirements and the active thread's executors. ## What changed - Refresh sandbox configuration and requirements for the current working directory when threads or directories change, before setup, and after setup completes. - Use the thread's observed environments to determine whether sandbox setup is local, remote, mixed, or unknown. - Defer initial prompt submission while requirements load and preserve the draft if loading fails or required setup is unavailable. - Hide sandbox setup choices and elevation commands that requirements or executor selection disallow. ## Testing Add coverage for observed thread hosts, app-server configuration reads, draft preservation and recovery after read failures, deferred initial prompts, and non-admin-only setup choices. GitOrigin-RevId: d85ac66d18951b752e71d1a6bf25404eeedcad02 --- codex-rs/tui/src/app.rs | 2 - codex-rs/tui/src/app/config_persistence.rs | 47 ++---- codex-rs/tui/src/app/event_dispatch.rs | 32 ++-- codex-rs/tui/src/app/platform_actions.rs | 111 +++++++++---- codex-rs/tui/src/app/reconnect.rs | 5 +- codex-rs/tui/src/app/session_lifecycle.rs | 3 +- codex-rs/tui/src/app/startup.rs | 49 ++---- .../src/app/tests/agents_navigation_tests.rs | 1 + .../tests/background_task_defaults_tests.rs | 3 +- codex-rs/tui/src/app/tests/startup.rs | 151 +++++++++++------- codex-rs/tui/src/app_event.rs | 6 + codex-rs/tui/src/chatwidget.rs | 12 +- codex-rs/tui/src/chatwidget/constructor.rs | 11 +- codex-rs/tui/src/chatwidget/input_restore.rs | 17 +- .../tui/src/chatwidget/permission_popups.rs | 23 ++- .../src/chatwidget/permission_shortcuts.rs | 13 +- codex-rs/tui/src/chatwidget/session_flow.rs | 16 ++ codex-rs/tui/src/chatwidget/settings.rs | 34 ++-- codex-rs/tui/src/chatwidget/slash_dispatch.rs | 20 ++- ...ts__windows_sandbox_unelevated_policy.snap | 11 ++ codex-rs/tui/src/chatwidget/tests.rs | 2 +- codex-rs/tui/src/chatwidget/tests/helpers.rs | 2 + .../tests/permission_shortcuts_tests.rs | 18 ++- .../tui/src/chatwidget/tests/permissions.rs | 144 ++++++++++------- .../src/chatwidget/windows_sandbox_prompts.rs | 64 ++++---- codex-rs/tui/src/lib.rs | 17 +- codex-rs/tui/src/windows_sandbox.rs | 3 +- 27 files changed, 476 insertions(+), 341 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_unelevated_policy.snap diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5abbb01a0a..dbbdebcd72 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -143,8 +143,6 @@ use codex_config::LoaderOverrides; use codex_config::types::ApprovalsReviewer; use codex_config::types::MemoriesToml; use codex_config::types::ModelAvailabilityNuxConfig; -#[cfg(target_os = "windows")] -use codex_config::types::WindowsToml; use codex_exec_server::EnvironmentManager; use codex_features::Feature; use codex_features::FeaturesToml; diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index b7344b7da6..8d9afdf888 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -1249,30 +1249,20 @@ impl App { pub(super) async fn verify_windows_sandbox_mode_after_setup( &mut self, app_server: &mut AppServerSession, - requested_mode: codex_config::types::WindowsSandboxModeToml, + requested_mode: codex_app_server_protocol::WindowsSandboxSetupMode, ) -> bool { - let cwd = self.chat_widget.config_ref().cwd.display().to_string(); - let mode = crate::config_update::read_effective_config(app_server.request_handle(), cwd) - .await - .ok() - .and_then(|config| windows_sandbox_mode_from_effective_config(&config)); - let Some(mode) = mode else { - self.chat_widget.add_error_message( - "Windows sandbox setup completed, but Codex could not verify the effective sandbox mode." - .to_string(), - ); + if !self.refresh_windows_sandbox_config(app_server).await { return false; - }; - self.config.permissions.windows_sandbox_mode = Some(mode); - if mode == requested_mode { + } + if self.chat_widget.windows_sandbox_config.mode == Some(requested_mode) + && self + .chat_widget + .windows_sandbox_config + .allows(requested_mode) + { return true; } - self.chat_widget.set_windows_sandbox_mode(Some(mode)); - self.propagate_windows_sandbox_turn_context(); - self.chat_widget.add_error_message( - "Windows sandbox setup completed, but its mode was overridden by the effective configuration." - .to_string(), - ); + self.chat_widget.add_error_message("Windows sandbox setup completed, but its mode was overridden by the effective configuration.".to_string()); false } @@ -1355,23 +1345,6 @@ fn features_toml_from_json(value: &serde_json::Value) -> Option { serde_json::from_value(value.clone()).ok() } -#[cfg(target_os = "windows")] -fn windows_sandbox_mode_from_effective_config( - effective_config: &ConfigReadResponse, -) -> Option { - let root_windows = effective_config - .config - .additional - .get("windows") - .and_then(windows_toml_from_json); - root_windows.and_then(|windows| windows.sandbox) -} - -#[cfg(target_os = "windows")] -fn windows_toml_from_json(value: &serde_json::Value) -> Option { - serde_json::from_value(value.clone()).ok() -} - #[cfg(test)] mod tests { use std::sync::Arc; diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index ee4041233f..8b6c43f5aa 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -19,7 +19,7 @@ use crate::pager_overlay::TranscriptHistoryState; use crate::session_resume::cwds_differ; use codex_app_server_protocol::ThreadGoalStatus; #[cfg(target_os = "windows")] -use codex_config::types::WindowsSandboxModeToml; +use codex_app_server_protocol::WindowsSandboxSetupMode as WindowsSandboxModeToml; pub(super) const SHUTDOWN_FIRST_EXIT_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 2); @@ -1962,6 +1962,12 @@ impl App { self.launch_external_editor(tui).await; } } + AppEvent::RefreshWindowsSandbox { thread_id } => { + #[cfg(any(target_os = "windows", test))] + self.refresh_windows_sandbox_for_thread(app_server, thread_id).await; + #[cfg(not(any(target_os = "windows", test)))] + let _ = thread_id; + } AppEvent::OpenWindowsSandboxEnablePrompt { preset, profile_selection, @@ -2041,18 +2047,6 @@ impl App { WindowsSandboxEnableMode::Legacy => WindowsSandboxModeToml::Unelevated, }; let elevated_enabled = selected_mode == WindowsSandboxModeToml::Elevated; - if !self.chat_widget.windows_sandbox_mode_allowed(selected_mode) { - tracing::warn!( - ?selected_mode, - "refusing to enable Windows sandbox mode disallowed by requirements" - ); - self.chat_widget.add_info_message( - "That Windows sandbox option is disallowed by requirements." - .to_string(), - /*hint*/ None, - ); - return Ok(AppRunControl::Continue); - } if self .verify_windows_sandbox_mode_after_setup(app_server, selected_mode) .await @@ -2068,9 +2062,7 @@ impl App { self.config .set_windows_elevated_sandbox_enabled(/*value*/ false); } - self.chat_widget.set_windows_sandbox_mode( - self.config.permissions.windows_sandbox_mode, - ); + self.chat_widget.set_windows_sandbox_mode(Some(selected_mode)); let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); if let Some(selection) = profile_selection { @@ -2706,6 +2698,14 @@ impl App { if self.reject_pending_permission_change() { return Ok(AppRunControl::Continue); } + #[cfg(any(target_os = "windows", test))] + if self.chat_widget.windows_sandbox_local_server + && self.windows_sandbox_host() != WindowsSandboxHost::Remote + && self.chat_widget.windows_sandbox_config.requirements.is_none() + && !self.refresh_windows_sandbox_config(app_server).await + { + return Ok(AppRunControl::Continue); + } if app_server.uses_remote_workspace() { self.chat_widget.request_permission_profiles(); } else { diff --git a/codex-rs/tui/src/app/platform_actions.rs b/codex-rs/tui/src/app/platform_actions.rs index 1f157ae291..2fc94fe22e 100644 --- a/codex-rs/tui/src/app/platform_actions.rs +++ b/codex-rs/tui/src/app/platform_actions.rs @@ -7,7 +7,7 @@ use super::*; #[cfg(all(test, not(target_os = "windows")))] use crate::app_event::WindowsSandboxEnableMode; #[cfg(target_os = "windows")] -use codex_config::types::WindowsSandboxModeToml; +use codex_app_server_protocol::WindowsSandboxSetupMode as WindowsSandboxModeToml; #[cfg(any(target_os = "windows", test))] use codex_utils_approval_presets::ApprovalPreset; @@ -22,6 +22,8 @@ pub(crate) enum WindowsSandboxHost { #[derive(Default)] pub(super) struct WindowsSandboxState { pub(super) setup_started_at: Option, + #[cfg_attr(not(any(target_os = "windows", test)), allow(dead_code))] + pub(super) prompt_after_trust: bool, #[cfg(any(target_os = "windows", test))] pub(super) pending_setup: Option<( WindowsSandboxEnableMode, @@ -30,28 +32,6 @@ pub(super) struct WindowsSandboxState { )>, } -pub(super) fn windows_sandbox_host( - target: &AppServerTarget, - environment_manager: &EnvironmentManager, -) -> WindowsSandboxHost { - if target.uses_remote_workspace() { - WindowsSandboxHost::Remote - } else if environment_manager - .default_environment_ids() - .into_iter() - .filter_map(|id| environment_manager.get_environment(&id)) - .any(|environment| environment.is_remote()) - { - if environment_manager.try_local_environment().is_some() { - WindowsSandboxHost::Mixed - } else { - WindowsSandboxHost::Remote - } - } else { - WindowsSandboxHost::Local - } -} - #[cfg(target_os = "windows")] pub(super) async fn windows_sandbox_ready(app_server: &mut AppServerSession) -> bool { let request_id = app_server.next_request_id(); @@ -76,12 +56,17 @@ pub(super) async fn windows_sandbox_ready(app_server: &mut AppServerSession) -> impl App { pub(super) fn windows_sandbox_host(&self) -> WindowsSandboxHost { - windows_sandbox_host(&self.app_server_target, self.environment_manager.as_ref()) + if self.app_server_target.uses_remote_workspace() { + WindowsSandboxHost::Remote + } else { + self.chat_widget.windows_sandbox_host + } } /// A local app server owns setup for both embedded and daemon connections. pub(super) fn windows_sandbox_setup_is_local(&self) -> bool { - self.windows_sandbox_host() == WindowsSandboxHost::Local + self.chat_widget.windows_sandbox_local_server + && self.windows_sandbox_host() == WindowsSandboxHost::Local } pub(super) fn windows_sandbox_blocks_thread_switch(&self) -> bool { @@ -91,7 +76,7 @@ impl App { && (self.windows_sandbox.pending_setup.is_some() || self.windows_sandbox.setup_started_at.is_some() || self.chat_widget.initial_user_message.is_some() - && self.chat_widget.required_elevated_windows_sandbox() + && self.chat_widget.windows_sandbox_config.requires_elevated() && !self.chat_widget.windows_sandbox_elevated_setup_complete) } #[cfg(not(any(target_os = "windows", test)))] @@ -100,6 +85,71 @@ impl App { } } + #[cfg(any(target_os = "windows", test))] + pub(super) async fn refresh_windows_sandbox_for_thread( + &mut self, + app_server: &mut AppServerSession, + thread_id: ThreadId, + ) { + if self.chat_widget.thread_id() == Some(thread_id) + && !self.app_server_target.uses_remote_workspace() + && self.refresh_windows_sandbox_config(app_server).await + { + #[cfg(target_os = "windows")] + if self.windows_sandbox_setup_is_local() + && self.chat_widget.windows_sandbox_config.requires_elevated() + && !self.chat_widget.windows_sandbox_elevated_setup_complete + { + self.chat_widget.windows_sandbox_elevated_setup_complete = + windows_sandbox_ready(app_server).await; + } + let show_nux = std::mem::take(&mut self.windows_sandbox.prompt_after_trust) + && self.chat_widget.windows_sandbox_config.mode.is_none() + || self.chat_widget.windows_sandbox_config.requires_elevated(); + if self.windows_sandbox_setup_is_local() { + self.chat_widget.maybe_prompt_windows_sandbox_enable( + show_nux && !self.chat_widget.windows_sandbox_elevated_setup_complete, + ); + } else if self.windows_sandbox_host() == WindowsSandboxHost::Mixed && show_nux { + self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::StartupWarningsCell::new(vec![ + "Windows sandbox setup is unavailable when local and remote executors are configured together." + .to_string(), + ]), + ))); + } + self.chat_widget.submit_initial_user_message_if_pending(); + } + } + + #[cfg(any(target_os = "windows", test))] + pub(super) async fn refresh_windows_sandbox_config( + &mut self, + app_server: &AppServerSession, + ) -> bool { + match crate::windows_sandbox::WindowsSandboxConfig::read( + app_server.request_handle(), + self.chat_widget.config_ref().cwd.display().to_string(), + ) + .await + { + Ok(state) => { + self.chat_widget.windows_sandbox_config = state; + self.chat_widget + .set_windows_sandbox_mode(self.chat_widget.windows_sandbox_config.mode); + true + } + Err(_) => { + self.chat_widget.windows_sandbox_config = Default::default(); + self.chat_widget + .retain_input_after_failed_permission_selection(); + self.chat_widget.set_windows_sandbox_mode(/*mode*/ None); + self.chat_widget.add_error_message("Could not read Windows sandbox configuration and requirements from the app server.".to_string()); + false + } + } + } + #[cfg(target_os = "windows")] pub(super) async fn begin_windows_sandbox_setup( &mut self, @@ -127,7 +177,12 @@ impl App { } return; } - if !self.chat_widget.windows_sandbox_mode_allowed(config_mode) { + if !self.refresh_windows_sandbox_config(app_server).await { + return; + } + if !self.chat_widget.windows_sandbox_config.allows(config_mode) { + self.chat_widget + .retain_input_after_failed_permission_selection(); self.chat_widget.add_info_message( "That Windows sandbox option is disallowed by requirements.".to_string(), /*hint*/ None, @@ -147,7 +202,7 @@ impl App { request_id, params: codex_app_server_protocol::WindowsSandboxSetupStartParams { mode: setup_mode, - cwd: Some(self.config.cwd.clone()), + cwd: Some(self.chat_widget.config_ref().cwd.clone()), }, }), ) diff --git a/codex-rs/tui/src/app/reconnect.rs b/codex-rs/tui/src/app/reconnect.rs index d8c248a568..aff846153e 100644 --- a/codex-rs/tui/src/app/reconnect.rs +++ b/codex-rs/tui/src/app/reconnect.rs @@ -292,7 +292,10 @@ impl App { self.environment_manager.as_ref(), ), ); - self.chat_widget.windows_sandbox_host = self.windows_sandbox_host(); + self.chat_widget.windows_sandbox_local_server = + !self.app_server_target.uses_remote_workspace() + && app_server.app_server_platform_os() == Some("windows"); + self.chat_widget.windows_sandbox_host = WindowsSandboxHost::Unknown; self.chat_widget.cyber_policy_notice = Default::default(); self.chat_widget.requires_openai_auth = bootstrap.requires_openai_auth; self.chat_widget.remote_connection = diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index f816316d33..a743b3b2bd 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -498,7 +498,8 @@ impl App { chat_widget.remote_connection = self.chat_widget.remote_connection.clone(); chat_widget.snapshot_local_images = self.app_server_target.uses_remote_workspace(); chat_widget.set_local_worktree_operations(self.chat_widget.local_worktree_operations); - chat_widget.windows_sandbox_host = self.chat_widget.windows_sandbox_host; + chat_widget.windows_sandbox_local_server = self.chat_widget.windows_sandbox_local_server; + chat_widget.windows_sandbox_host = WindowsSandboxHost::Unknown; #[cfg(any(target_os = "windows", test))] { chat_widget.windows_sandbox_elevated_setup_complete = diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index 6bfa19b3a1..7dc0d525d4 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -682,39 +682,17 @@ impl App { AppServerTarget::LocalDaemon { .. } )); let thread_and_widget_ms = thread_and_widget_started_at.elapsed().as_millis(); - let windows_sandbox_host = - windows_sandbox_host(&app_server_target, environment_manager.as_ref()); - chat_widget.windows_sandbox_host = windows_sandbox_host; - let windows_sandbox_host_is_local = windows_sandbox_host == WindowsSandboxHost::Local; - #[cfg(target_os = "windows")] - let sandbox_ready = - if windows_sandbox_host_is_local && chat_widget.required_elevated_windows_sandbox() { - windows_sandbox_ready(&mut app_server).await - } else { - false - }; - #[cfg(not(target_os = "windows"))] - let sandbox_ready = false; - #[cfg(target_os = "windows")] - if sandbox_ready { - chat_widget.windows_sandbox_elevated_setup_complete = true; - } - chat_widget.maybe_prompt_windows_sandbox_enable( - should_prompt_windows_sandbox_nux_at_startup - && windows_sandbox_host_is_local - && !sandbox_ready, - ); - #[cfg(target_os = "windows")] - if windows_sandbox_host == WindowsSandboxHost::Mixed - && should_prompt_windows_sandbox_nux_at_startup - { - app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( - history_cell::StartupWarningsCell::new(vec![ - "Windows sandbox setup is unavailable when local and remote executors are configured together." - .to_string(), - ]), - ))); - } + chat_widget.windows_sandbox_local_server = !app_server_target.uses_remote_workspace() + && app_server.app_server_platform_os() == Some("windows"); + chat_widget.windows_sandbox_host = if app_server_target.uses_remote_workspace() { + WindowsSandboxHost::Remote + } else { + initial_started_thread + .as_ref() + .map_or(WindowsSandboxHost::Unknown, |started| { + started.session.windows_sandbox_host + }) + }; let file_search = FileSearchManager::new(config.cwd.to_path_buf(), app_event_tx.clone()); let runtime_keymap = RuntimeKeymap::from_config(&local_settings.tui.keymap).map_err(|err| { @@ -780,7 +758,10 @@ See the Codex keymap documentation for supported actions and examples." }, pending_update_action: None, pending_shutdown_exit_thread_id: None, - windows_sandbox: WindowsSandboxState::default(), + windows_sandbox: WindowsSandboxState { + prompt_after_trust: should_prompt_windows_sandbox_nux_at_startup, + ..Default::default() + }, thread_event_channels: HashMap::new(), pending_realtime_speech_replay: HashMap::new(), pending_realtime_transcript_replay: HashMap::new(), diff --git a/codex-rs/tui/src/app/tests/agents_navigation_tests.rs b/codex-rs/tui/src/app/tests/agents_navigation_tests.rs index 4dcd62f07b..1daac6c13e 100644 --- a/codex-rs/tui/src/app/tests/agents_navigation_tests.rs +++ b/codex-rs/tui/src/app/tests/agents_navigation_tests.rs @@ -51,6 +51,7 @@ async fn pending_windows_sandbox_setup_blocks_thread_replacement() -> Result<()> use crate::app_event::WindowsSandboxEnableMode; let (mut app, mut events, _ops) = make_test_app_with_channels().await; + app.chat_widget.windows_sandbox_local_server = true; while events.try_recv().is_ok() {} let mut app_server = start_config_write_test_app_server(&app).await?; let mut tui = crate::tui::test_support::make_test_tui()?; diff --git a/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs b/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs index a996943e5d..0e1a673e33 100644 --- a/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs +++ b/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs @@ -819,6 +819,7 @@ async fn command_center_new_checkout_and_worktree_preserve_source_and_default_br let (mut app, mut events, _) = make_test_app_with_channels().await; app.config.codex_home = home.clone().abs(); app.config.cwd = selected.cwd.clone().abs(); + app.chat_widget.windows_sandbox_local_server = cfg!(target_os = "windows"); app.harness_overrides.cwd = Some(selected.cwd.clone()); app.cli_kv_overrides .push(("features.worktrees".into(), TomlValue::Boolean(true))); @@ -857,7 +858,7 @@ async fn command_center_new_checkout_and_worktree_preserve_source_and_default_br // Exercise the menu's ordered events, not the separate profile-selection API. app.chat_widget .set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true); - app.chat_widget.open_permissions_popup(); + Box::pin(app.handle_event(&mut tui, &mut server, AppEvent::OpenPermissionsPopup)).await?; app.chat_widget.handle_key_event(KeyCode::Up.into()); app.chat_widget.handle_key_event(KeyCode::Enter.into()); while let Ok(event) = events.try_recv() { diff --git a/codex-rs/tui/src/app/tests/startup.rs b/codex-rs/tui/src/app/tests/startup.rs index b178368a68..cd875484d7 100644 --- a/codex-rs/tui/src/app/tests/startup.rs +++ b/codex-rs/tui/src/app/tests/startup.rs @@ -1,3 +1,5 @@ +use super::session_lifecycle_requests::recorded_params; +use super::session_lifecycle_requests::start_recording_remote_app_server; use super::*; use crate::ThreadParamsMode; use crate::app_event_sender::AppEventSender; @@ -18,6 +20,7 @@ use tokio::sync::mpsc::unbounded_channel; #[tokio::test] async fn windows_sandbox_setup_uses_local_app_server_connection() { let mut app = make_test_app().await; + app.chat_widget.windows_sandbox_local_server = true; assert!(app.windows_sandbox_setup_is_local()); let endpoint = crate::RemoteAppServerEndpoint::WebSocket { @@ -34,33 +37,48 @@ async fn windows_sandbox_setup_uses_local_app_server_connection() { } #[tokio::test] -async fn windows_sandbox_setup_skips_remote_default_executor() -> Result<()> { +async fn windows_sandbox_setup_uses_observed_thread_host() { let mut app = make_test_app().await; - app.environment_manager = Arc::new( - EnvironmentManager::create_for_tests( - Some("ws://127.0.0.1:8765".to_string()), - Some(codex_exec_server::ExecServerRuntimePaths::new( - std::env::current_exe()?, - /*codex_linux_sandbox_exe*/ None, - )?), - ) - .await, - ); + app.chat_widget.windows_sandbox_local_server = true; + app.chat_widget.windows_sandbox_elevated_setup_complete = true; + for (ids, expected) in [ + (None, WindowsSandboxHost::Unknown), + (Some(vec![]), WindowsSandboxHost::Unknown), + ( + Some(vec![codex_exec_server::LOCAL_ENVIRONMENT_ID]), + WindowsSandboxHost::Local, + ), + (Some(vec!["remote"]), WindowsSandboxHost::Remote), + ( + Some(vec!["remote", codex_exec_server::LOCAL_ENVIRONMENT_ID]), + WindowsSandboxHost::Mixed, + ), + ] { + let environments = ids.map(|ids| { + ids.into_iter() + .map(|id| codex_app_server_protocol::ThreadEnvironment { + environment_id: id.to_string(), + cwd: codex_utils_absolute_path::AbsolutePathBuf::try_from(std::env::temp_dir()) + .unwrap() + .into(), + runtime_workspace_roots: Vec::new(), + }) + .collect::>() + }); + let mut session = test_thread_session(ThreadId::new(), app.config.cwd.to_path_buf()); + session.windows_sandbox_host = + crate::windows_sandbox::host_from_environments(environments.as_deref()); + app.chat_widget.handle_thread_session(session); + assert!(app.chat_widget.windows_sandbox_elevated_setup_complete); + assert_eq!(app.windows_sandbox_host(), expected); + assert_eq!( + app.windows_sandbox_setup_is_local(), + expected == WindowsSandboxHost::Local + ); + } + app.chat_widget.windows_sandbox_host = WindowsSandboxHost::Local; + app.chat_widget.windows_sandbox_local_server = false; assert!(!app.windows_sandbox_setup_is_local()); - - app.environment_manager = Arc::new( - EnvironmentManager::create_for_tests_with_local( - Some("ws://127.0.0.1:8765".to_string()), - codex_exec_server::ExecServerRuntimePaths::new( - std::env::current_exe()?, - /*codex_linux_sandbox_exe*/ None, - )?, - ) - .await, - ); - assert_eq!(app.windows_sandbox_host(), WindowsSandboxHost::Mixed); - assert!(!app.windows_sandbox_setup_is_local()); - Ok(()) } #[tokio::test] @@ -119,6 +137,7 @@ async fn windows_sandbox_setup_completion_requires_matching_pending_mode() -> Re use codex_app_server_protocol::WindowsSandboxSetupMode; let (mut app, mut events, _ops) = make_test_app_with_channels().await; + app.chat_widget.windows_sandbox_local_server = true; while events.try_recv().is_ok() {} let app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref()).await?; @@ -1634,35 +1653,55 @@ async fn external_writer_startup_keeps_initial_prompt_as_draft() -> Result<()> { Ok(()) } -#[test] -fn windows_sandbox_host_from_observed_environments() { - for (ids, expected) in [ - (None, WindowsSandboxHost::Unknown), - (Some(vec![]), WindowsSandboxHost::Unknown), - ( - Some(vec![codex_exec_server::LOCAL_ENVIRONMENT_ID]), - WindowsSandboxHost::Local, - ), - (Some(vec!["remote"]), WindowsSandboxHost::Remote), - ( - Some(vec!["remote", codex_exec_server::LOCAL_ENVIRONMENT_ID]), - WindowsSandboxHost::Mixed, - ), - ] { - let environments = ids.map(|ids| { - ids.into_iter() - .map(|id| codex_app_server_protocol::ThreadEnvironment { - environment_id: id.to_string(), - cwd: codex_utils_absolute_path::AbsolutePathBuf::try_from(std::env::temp_dir()) - .unwrap() - .into(), - runtime_workspace_roots: Vec::new(), - }) - .collect::>() - }); - assert_eq!( - crate::windows_sandbox::host_from_environments(environments.as_deref()), - expected - ); - } +#[tokio::test] +async fn windows_sandbox_config_refresh_uses_connected_server() -> Result<()> { + let (mut app, mut events, _ops) = make_test_app_with_channels().await; + let (server, requests, proxy) = start_recording_remote_app_server(&app.config).await?; + std::fs::write( + app.config.codex_home.join("config.toml"), + "[windows]\nsandbox = \"unelevated\"\n", + )?; + app.chat_widget.windows_sandbox_config.mode = + Some(codex_app_server_protocol::WindowsSandboxSetupMode::Elevated); + assert!(app.refresh_windows_sandbox_config(&server).await); + let loaded_config = crate::windows_sandbox::WindowsSandboxConfig { + mode: Some(codex_app_server_protocol::WindowsSandboxSetupMode::Unelevated), + requirements: Some(None), + }; + assert_eq!(app.chat_widget.windows_sandbox_config, loaded_config); + assert_eq!( + recorded_params(&requests, "config/read")[0]["cwd"], + app.config.cwd.display().to_string() + ); + assert_eq!( + recorded_params(&requests, "configRequirements/read").len(), + 1 + ); + proxy.abort(); + let _ = proxy.await; + set_test_initial_prompt(&mut app, "keep this draft".to_string()); + app.chat_widget.windows_sandbox_elevated_setup_complete = true; + while events.try_recv().is_ok() {} + assert!(!app.refresh_windows_sandbox_config(&server).await); + let cell = match events.try_recv()? { + AppEvent::InsertHistoryCell(cell) => cell, + other => panic!("expected configuration error message, got {other:?}"), + }; + insta::assert_snapshot!(lines_to_single_string(&cell.display_lines(/*width*/ 160)), @"■ Could not read Windows sandbox configuration and requirements from the app server."); + assert!(app.chat_widget.windows_sandbox_elevated_setup_complete); + assert_eq!( + app.chat_widget.composer_text_with_pending(), + "keep this draft" + ); + assert!(app.chat_widget.initial_user_message.is_none()); + let mut recovered = crate::start_embedded_app_server_for_picker(&app.config).await?; + let mut tui = crate::tui::test_support::make_test_tui()?; + app.chat_widget.windows_sandbox_local_server = true; + app.chat_widget.windows_sandbox_host = WindowsSandboxHost::Local; + Box::pin(app.handle_event(&mut tui, &mut recovered, AppEvent::OpenPermissionsPopup)).await?; + assert_eq!(app.chat_widget.windows_sandbox_config, loaded_config); + assert!(app.chat_widget.has_active_view()); + recovered.shutdown().await?; + server.shutdown().await?; + Ok(()) } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 1544b3e199..a53a4ddf4b 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -1213,6 +1213,12 @@ pub(crate) enum AppEvent { selection: PermissionProfileSelection, }, + /// Refresh server-owned Windows state after selecting a thread or project. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + RefreshWindowsSandbox { + thread_id: ThreadId, + }, + /// Prompt to enable the Windows sandbox feature before using Agent mode. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] OpenWindowsSandboxEnablePrompt { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0222a34771..e1123385b8 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -116,11 +116,11 @@ use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnPlanStepStatus; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; +use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::Constrained; use codex_config::ConstraintResult; use codex_config::types::ApprovalsReviewer; use codex_config::types::Notifications; -use codex_config::types::WindowsSandboxModeToml; use codex_connectors::AppInfo; use codex_features::Feature; #[cfg(test)] @@ -142,7 +142,7 @@ use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; -#[cfg(any(target_os = "windows", test))] +#[cfg(target_os = "windows")] use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; @@ -600,6 +600,8 @@ pub(crate) struct ChatWidget { pub(crate) snapshot_local_images: bool, pending_image_submission: Option, pub(crate) local_worktree_operations: bool, + pub(crate) windows_sandbox_local_server: bool, + pub(crate) windows_sandbox_config: crate::windows_sandbox::WindowsSandboxConfig, pub(crate) windows_sandbox_host: crate::app::WindowsSandboxHost, #[cfg(any(target_os = "windows", test))] pub(crate) windows_sandbox_elevated_setup_complete: bool, @@ -1063,8 +1065,10 @@ impl ChatWidget { let snapshot = self.feedback.snapshot(self.thread_id); #[cfg(target_os = "windows")] let include_windows_sandbox_log = - codex_windows_sandbox::current_log_file_path_for_codex_home(&self.config.codex_home) - .is_file(); + codex_windows_sandbox::current_log_file_path_for_codex_home( + &self.local_settings.codex_home, + ) + .is_file(); #[cfg(not(target_os = "windows"))] let include_windows_sandbox_log = false; let params = crate::bottom_pane::feedback_upload_consent_params( diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index 97ede3622f..580948259d 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -137,7 +137,9 @@ impl ChatWidget { snapshot_local_images: false, pending_image_submission: None, local_worktree_operations: true, - windows_sandbox_host: crate::app::WindowsSandboxHost::Local, + windows_sandbox_local_server: false, + windows_sandbox_config: Default::default(), + windows_sandbox_host: crate::app::WindowsSandboxHost::Unknown, #[cfg(any(target_os = "windows", test))] windows_sandbox_elevated_setup_complete: false, token_info: None, @@ -303,13 +305,6 @@ impl ChatWidget { widget .bottom_pane .set_queued_message_edit_binding(widget.queued_message_edit_hint_binding); - #[cfg(target_os = "windows")] - widget - .bottom_pane - .set_windows_degraded_sandbox_active(matches!( - crate::windows_sandbox::level_from_config(&widget.config), - WindowsSandboxLevel::RestrictedToken - )); widget.update_collaboration_mode_indicator(); widget diff --git a/codex-rs/tui/src/chatwidget/input_restore.rs b/codex-rs/tui/src/chatwidget/input_restore.rs index 75e94dea8d..1d11bf04d9 100644 --- a/codex-rs/tui/src/chatwidget/input_restore.rs +++ b/codex-rs/tui/src/chatwidget/input_restore.rs @@ -119,7 +119,9 @@ impl ChatWidget { } #[cfg(any(target_os = "windows", test))] if self.windows_sandbox_host == crate::app::WindowsSandboxHost::Local - && self.elevated_windows_sandbox_setup_required() + && (self.windows_sandbox_local_server + && self.windows_sandbox_config.requirements.is_none() + || self.elevated_windows_sandbox_setup_required()) { return; } @@ -138,14 +140,23 @@ impl ChatWidget { return; } #[cfg(any(target_os = "windows", test))] + if self.windows_sandbox_local_server + && self.windows_sandbox_host != crate::app::WindowsSandboxHost::Remote + && self.windows_sandbox_config.requirements.is_none() + { + return; + } + #[cfg(any(target_os = "windows", test))] if self.windows_sandbox_host == crate::app::WindowsSandboxHost::Local && self.elevated_windows_sandbox_setup_required() { return; } #[cfg(any(target_os = "windows", test))] - if self.windows_sandbox_host == crate::app::WindowsSandboxHost::Mixed - && self.elevated_windows_sandbox_setup_required() + if matches!( + self.windows_sandbox_host, + crate::app::WindowsSandboxHost::Mixed | crate::app::WindowsSandboxHost::Unknown + ) && self.elevated_windows_sandbox_setup_required() { if let Some(user_message) = self.initial_user_message.take() { self.restore_user_message_to_composer(user_message); diff --git a/codex-rs/tui/src/chatwidget/permission_popups.rs b/codex-rs/tui/src/chatwidget/permission_popups.rs index 9ccd41b427..67067ed50f 100644 --- a/codex-rs/tui/src/chatwidget/permission_popups.rs +++ b/codex-rs/tui/src/chatwidget/permission_popups.rs @@ -41,15 +41,20 @@ impl ChatWidget { let presets: Vec = builtin_approval_presets(); #[cfg(target_os = "windows")] - let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); + let windows_sandbox_level = self.windows_sandbox_config.level(); #[cfg(target_os = "windows")] let windows_degraded_sandbox_enabled = - matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); + matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken) + && self.windows_sandbox_local_server + && self.windows_sandbox_host == crate::app::WindowsSandboxHost::Local; #[cfg(not(target_os = "windows"))] let windows_degraded_sandbox_enabled = false; - let show_elevate_sandbox_hint = - windows_degraded_sandbox_enabled && presets.iter().any(|preset| preset.id == "auto"); + let show_elevate_sandbox_hint = windows_degraded_sandbox_enabled + && self + .windows_sandbox_config + .allows(WindowsSandboxSetupMode::Elevated) + && presets.iter().any(|preset| preset.id == "auto"); let guardian_disabled_reason = |enabled: bool| { let mut next_features = self.config.features.get().clone(); @@ -324,7 +329,11 @@ impl ChatWidget { let requires_confirmation = approvals_reviewer == ApprovalsReviewer::User && preset.id == "full-access"; #[cfg(target_os = "windows")] - if preset.id == "auto" && self.windows_sandbox_host == crate::app::WindowsSandboxHost::Mixed + if preset.id == "auto" + && matches!( + self.windows_sandbox_host, + crate::app::WindowsSandboxHost::Mixed | crate::app::WindowsSandboxHost::Unknown + ) { let preset = preset.clone(); return vec![Box::new(move |tx| { @@ -352,9 +361,7 @@ impl ChatWidget { // cannot be set up from this TUI's Windows account. return apply_actions(); } - if crate::windows_sandbox::level_from_config(&self.config) - == WindowsSandboxLevel::Disabled - { + if self.windows_sandbox_config.level() == WindowsSandboxLevel::Disabled { let preset = preset.clone(); return vec![Box::new(move |tx| { tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { diff --git a/codex-rs/tui/src/chatwidget/permission_shortcuts.rs b/codex-rs/tui/src/chatwidget/permission_shortcuts.rs index 8079f471c0..730b4e4925 100644 --- a/codex-rs/tui/src/chatwidget/permission_shortcuts.rs +++ b/codex-rs/tui/src/chatwidget/permission_shortcuts.rs @@ -55,13 +55,16 @@ impl ChatWidget { { continue; } - // These modes still need the explicit Windows setup/warning flow. + // These modes still need explicit Windows setup. #[cfg(target_os = "windows")] if preset.id == "auto" - && (self.windows_sandbox_host == crate::app::WindowsSandboxHost::Mixed - || (reviewer == ApprovalsReviewer::User - && crate::windows_sandbox::level_from_config(&self.config) - == WindowsSandboxLevel::Disabled)) + && (matches!( + self.windows_sandbox_host, + crate::app::WindowsSandboxHost::Mixed + | crate::app::WindowsSandboxHost::Unknown + ) || (self.windows_sandbox_host != crate::app::WindowsSandboxHost::Remote + && reviewer == ApprovalsReviewer::User + && self.windows_sandbox_config.level() == WindowsSandboxLevel::Disabled)) { continue; } diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs index 3f3f2aa615..1c4129420b 100644 --- a/codex-rs/tui/src/chatwidget/session_flow.rs +++ b/codex-rs/tui/src/chatwidget/session_flow.rs @@ -9,6 +9,12 @@ impl ChatWidget { display: SessionConfiguredDisplay, fork_parent_title: Option, ) { + self.windows_sandbox_host = + if !self.windows_sandbox_local_server && self.remote_connection.is_some() { + crate::app::WindowsSandboxHost::Remote + } else { + session.windows_sandbox_host + }; self.invalidate_permission_discovery(); self.permission_profiles_menu_opened = false; self.transcript.reset_copy_history(); @@ -24,6 +30,16 @@ impl ChatWidget { let connector_scope_changed = previous_thread_id != Some(session.thread_id) || self.config.cwd.as_path() != session.cwd.as_path(); self.thread_id = Some(session.thread_id); + #[cfg(target_os = "windows")] + if self.windows_sandbox_local_server + && matches!(self.codex_op_target, CodexOpTarget::AppEvent) + { + self.windows_sandbox_config = Default::default(); + self.set_windows_sandbox_mode(/*mode*/ None); + self.app_event_tx.send(AppEvent::RefreshWindowsSandbox { + thread_id: session.thread_id, + }); + } self.realtime_conversation_available_for_thread = self.config.features.enabled(Feature::RealtimeConversation) && codex_realtime_webrtc::RealtimeWebrtcSession::is_supported(); diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index 61817a46c9..169c0fd1f9 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -56,14 +56,14 @@ impl ChatWidget { } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] - pub(crate) fn set_windows_sandbox_mode(&mut self, mode: Option) { - self.config.permissions.windows_sandbox_mode = mode; + pub(crate) fn set_windows_sandbox_mode(&mut self, mode: Option) { + self.windows_sandbox_config.mode = mode; #[cfg(target_os = "windows")] - self.bottom_pane - .set_windows_degraded_sandbox_active(matches!( - crate::windows_sandbox::level_from_config(&self.config), - WindowsSandboxLevel::RestrictedToken - )); + { + let enabled = self.builtin_command_flags().allow_elevate_sandbox; + self.bottom_pane + .set_windows_degraded_sandbox_active(enabled); + } } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] @@ -118,17 +118,6 @@ impl ChatWidget { if feature == Feature::PreventIdleSleep { self.turn_lifecycle.set_prevent_idle_sleep(enabled); } - #[cfg(target_os = "windows")] - if matches!( - feature, - Feature::WindowsSandbox | Feature::WindowsSandboxElevated - ) { - self.bottom_pane - .set_windows_degraded_sandbox_active(matches!( - crate::windows_sandbox::level_from_config(&self.config), - WindowsSandboxLevel::RestrictedToken - )); - } enabled } @@ -494,6 +483,15 @@ impl ChatWidget { self.refresh_status_surfaces(); self.sync_service_tier_commands(); if cwd_changed { + #[cfg(target_os = "windows")] + if self.windows_sandbox_local_server + && let Some(thread_id) = self.thread_id + { + self.windows_sandbox_config = Default::default(); + self.set_windows_sandbox_mode(/*mode*/ None); + self.app_event_tx + .send(AppEvent::RefreshWindowsSandbox { thread_id }); + } self.invalidate_connector_scope(); self.refresh_skills_for_current_cwd(/*force_reload*/ true); self.refresh_connector_mentions(/*force_refresh*/ false); diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 156c671ffe..9b2b43a93e 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -6,6 +6,7 @@ //! slash-command recall follows the same submitted-input rule as ordinary text. use super::*; +use crate::app::WindowsSandboxHost; use crate::app_event::ManagedWorktreeMode; use crate::app_event::ThreadGoalSetMode; use crate::bottom_pane::prompt_args::parse_slash_name; @@ -346,7 +347,11 @@ impl ChatWidget { self.app_event_tx.send(AppEvent::OpenAgentPicker); } SlashCommand::Permissions => { - if self.remote_connection.is_some() { + if self.remote_connection.is_some() + || self.windows_sandbox_local_server + && self.windows_sandbox_host != WindowsSandboxHost::Remote + && self.windows_sandbox_config.requirements.is_none() + { self.app_event_tx.send(AppEvent::OpenPermissionsPopup); } else { self.open_permissions_popup(); @@ -362,11 +367,7 @@ impl ChatWidget { SlashCommand::ElevateSandbox => { #[cfg(target_os = "windows")] { - let windows_sandbox_level = - crate::windows_sandbox::level_from_config(&self.config); - let windows_degraded_sandbox_enabled = - matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); - if !windows_degraded_sandbox_enabled { + if !self.builtin_command_flags().allow_elevate_sandbox { // This command should not be visible/recognized outside degraded mode, // but guard anyway in case something dispatches it directly. return; @@ -1132,8 +1133,13 @@ impl ChatWidget { pub(super) fn builtin_command_flags(&self) -> BuiltinCommandFlags { #[cfg(target_os = "windows")] let allow_elevate_sandbox = { - let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); + let windows_sandbox_level = self.windows_sandbox_config.level(); matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken) + && self.windows_sandbox_local_server + && self.windows_sandbox_host == crate::app::WindowsSandboxHost::Local + && self + .windows_sandbox_config + .allows(WindowsSandboxSetupMode::Elevated) }; #[cfg(not(target_os = "windows"))] let allow_elevate_sandbox = false; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_unelevated_policy.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_unelevated_policy.snap new file mode 100644 index 0000000000..ccf3f16ac6 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_unelevated_policy.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/chatwidget/tests/permissions.rs +expression: "render_bottom_popup(&chat, 100)" +--- + Set up the Codex agent sandbox to protect your files and control network access. Learn more + + +› 1. Use non-admin sandbox (higher risk if prompt injected) + 2. Quit + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index c7aaa1a7a5..5f70dee7a7 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -114,13 +114,13 @@ pub(super) use codex_app_server_protocol::TurnStatus as AppServerTurnStatus; pub(super) use codex_app_server_protocol::UserInput; pub(super) use codex_app_server_protocol::UserInput as AppServerUserInput; pub(super) use codex_app_server_protocol::WarningNotification; +pub(super) use codex_app_server_protocol::WindowsSandboxSetupMode; pub(super) use codex_config::ConfigLayerStack; pub(super) use codex_config::Constrained; pub(super) use codex_config::ConstraintError; pub(super) use codex_config::RequirementSource; pub(super) use codex_config::types::ApprovalsReviewer; pub(super) use codex_config::types::Notifications; -pub(super) use codex_config::types::WindowsSandboxModeToml; pub(super) use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; pub(super) use codex_features::Feature; pub(super) use codex_git_utils::CommitLogEntry; diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index c750b86fd9..0f4d5a6aeb 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -212,6 +212,8 @@ pub(super) async fn make_chatwidget_manual_with_auth( session_telemetry, }; let mut widget = ChatWidget::new_with_op_target(common, super::CodexOpTarget::Direct(op_tx)); + widget.windows_sandbox_host = crate::app::WindowsSandboxHost::Local; + widget.windows_sandbox_config.requirements = Some(None); widget.transcript.active_cell = None; widget.transcript.active_cell_revision = 0; widget.set_model(&resolved_model); diff --git a/codex-rs/tui/src/chatwidget/tests/permission_shortcuts_tests.rs b/codex-rs/tui/src/chatwidget/tests/permission_shortcuts_tests.rs index f45405051d..1e718ba7ab 100644 --- a/codex-rs/tui/src/chatwidget/tests/permission_shortcuts_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/permission_shortcuts_tests.rs @@ -14,7 +14,7 @@ async fn permission_shortcuts_cycle_builtin_modes() { chat.chat_keymap.previous_permission_mode = vec![crate::key_hint::plain(KeyCode::F(7))]; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } for (current, reviewer, key, expected, next_reviewer) in [ (":workspace", User, KeyCode::F(8), ":workspace", AutoReview), @@ -67,8 +67,6 @@ async fn permission_shortcuts_cycle_builtin_modes() { #[cfg(target_os = "windows")] { chat.set_windows_sandbox_mode(/*mode*/ None); - chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false); - chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); chat.config .permissions .set_permission_profile(PermissionProfile::read_only()) @@ -86,6 +84,20 @@ async fn permission_shortcuts_cycle_builtin_modes() { }) )); assert!(rx.try_recv().is_err()); + chat.complete_permission_shortcut(thread_id); + chat.windows_sandbox_host = crate::app::WindowsSandboxHost::Remote; + chat.handle_key_event(KeyEvent::from(KeyCode::F(8))); + assert!(matches!( + rx.try_recv(), + Ok(AppEvent::ApplyPermissionShortcut { + selection: PermissionProfileSelection { + profile_id, + approvals_reviewer: Some(User), + .. + }, + .. + }) if profile_id == ":workspace" + )); } } diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 357b6fc832..b5bf1f7add 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -20,7 +20,7 @@ async fn remote_windows_agent_permission_uses_server_selection() { .expect("Agent preset"); let (mut chat, mut events, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.windows_sandbox_host = crate::app::WindowsSandboxHost::Remote; - chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated); + chat.windows_sandbox_config.mode = Some(WindowsSandboxSetupMode::Elevated); let actions = chat.permission_mode_actions( &preset, "Agent".to_string(), @@ -204,19 +204,6 @@ fn app_server_workspace_write_profile(extra_root: AbsolutePathBuf) -> Permission } } -fn windows_sandbox_requirements_stack( - allowed_sandbox_implementations: Vec, -) -> ConfigLayerStack { - let requirements_toml = codex_config::ConfigRequirementsToml { - windows: Some(codex_config::WindowsRequirementsToml { - allowed_sandbox_implementations: Some(allowed_sandbox_implementations), - sandbox_private_desktop: None, - }), - ..Default::default() - }; - requirements_stack(requirements_toml) -} - pub(super) fn requirements_stack( requirements_toml: codex_config::ConfigRequirementsToml, ) -> ConfigLayerStack { @@ -323,7 +310,7 @@ async fn profile_permissions_selection_emits_named_profile_event_only() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.config.explicit_permission_profile_mode = true; chat.config @@ -388,7 +375,7 @@ async fn profile_permissions_selection_emits_auto_review_mode_event() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.config.explicit_permission_profile_mode = true; chat.config @@ -451,9 +438,9 @@ async fn profile_permissions_full_access_always_opens_confirmation() { #[serial] async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.windows_sandbox_local_server = true; - chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ true); - chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); chat.open_approvals_popup(); @@ -470,6 +457,10 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { popup.contains("non-admin sandbox"), "expected degraded sandbox note in approvals popup: {popup}" ); + assert!(chat.builtin_command_flags().allow_elevate_sandbox); + chat.windows_sandbox_config.requirements = + Some(Some(vec![WindowsSandboxSetupMode::Unelevated])); + assert!(!chat.builtin_command_flags().allow_elevate_sandbox); } #[tokio::test] @@ -585,8 +576,7 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { async fn startup_prompts_for_windows_sandbox_when_agent_requested() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false); - chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); + chat.set_windows_sandbox_mode(/*mode*/ None); chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true); @@ -614,10 +604,8 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() { async fn startup_windows_sandbox_prompt_blocks_disallowed_unelevated_fallback() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false); - chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); - chat.config.config_layer_stack = - windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]); + chat.set_windows_sandbox_mode(/*mode*/ None); + chat.windows_sandbox_config.requirements = Some(Some(vec![WindowsSandboxSetupMode::Elevated])); chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true); @@ -636,8 +624,7 @@ async fn startup_windows_sandbox_prompt_blocks_disallowed_unelevated_fallback() async fn windows_sandbox_required_enable_prompt_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.config.config_layer_stack = - windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]); + chat.windows_sandbox_config.requirements = Some(Some(vec![WindowsSandboxSetupMode::Elevated])); let preset = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -655,11 +642,11 @@ async fn windows_sandbox_required_enable_prompt_snapshot() { async fn windows_sandbox_required_enable_prompt_reopens_on_cancel_when_unelevated_allowed() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated); - chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![ - WindowsSandboxModeToml::Elevated, - WindowsSandboxModeToml::Unelevated, - ]); + chat.windows_sandbox_config.mode = Some(WindowsSandboxSetupMode::Elevated); + chat.windows_sandbox_config.requirements = Some(Some(vec![ + WindowsSandboxSetupMode::Elevated, + WindowsSandboxSetupMode::Unelevated, + ])); let preset = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -736,11 +723,11 @@ async fn required_windows_sandbox_setup_defers_configured_initial_prompt() { let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; let initial_prompt = "fix required sandbox startup".to_string(); - chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated); - chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![ - WindowsSandboxModeToml::Elevated, - WindowsSandboxModeToml::Unelevated, - ]); + chat.windows_sandbox_config.mode = Some(WindowsSandboxSetupMode::Elevated); + chat.windows_sandbox_config.requirements = Some(Some(vec![ + WindowsSandboxSetupMode::Elevated, + WindowsSandboxSetupMode::Unelevated, + ])); chat.initial_user_message = create_initial_user_message(Some(initial_prompt.clone()), Vec::new(), Vec::new()); @@ -777,7 +764,7 @@ async fn required_windows_sandbox_setup_defers_configured_initial_prompt() { ); } - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); chat.submit_initial_user_message_if_pending(); let Op::UserTurn { items, .. } = next_submit_op(&mut op_rx) else { @@ -795,25 +782,45 @@ async fn required_windows_sandbox_setup_defers_configured_initial_prompt() { #[tokio::test] async fn mixed_executors_restore_required_sandbox_prompt_without_submitting() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.thread_id = Some(ThreadId::new()); + chat.windows_sandbox_local_server = true; chat.windows_sandbox_host = crate::app::WindowsSandboxHost::Mixed; - chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated); - chat.config.config_layer_stack = - windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]); + chat.windows_sandbox_elevated_setup_complete = true; + chat.windows_sandbox_config.mode = Some(WindowsSandboxSetupMode::Elevated); + chat.windows_sandbox_config.requirements = None; chat.initial_user_message = create_initial_user_message(Some("review this".to_string()), Vec::new(), Vec::new()); chat.submit_initial_user_message_if_pending(); - - assert_eq!(chat.composer_text_with_pending(), "review this"); + assert!(chat.initial_user_message.is_some()); + assert_eq!(chat.composer_text_with_pending(), ""); assert!(op_rx.try_recv().is_err()); + + chat.windows_sandbox_config.requirements = Some(None); + chat.submit_initial_user_message_if_pending(); + let Op::UserTurn { items, .. } = next_submit_op(&mut op_rx) else { + panic!("expected initial prompt submission after unrestricted policy loads"); + }; + assert_eq!( + items, + vec![UserInput::Text { + text: "review this".to_string(), + text_elements: Vec::new(), + }] + ); + + chat.windows_sandbox_config.requirements = Some(Some(vec![WindowsSandboxSetupMode::Elevated])); + chat.initial_user_message = Some("review this".into()); + chat.submit_initial_user_message_if_pending(); + assert_eq!(chat.composer_text_with_pending(), "review this"); + assert_no_submit_op(&mut op_rx); } #[tokio::test] async fn windows_sandbox_required_fallback_prompt_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.config.config_layer_stack = - windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]); + chat.windows_sandbox_config.requirements = Some(Some(vec![WindowsSandboxSetupMode::Elevated])); let preset = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -830,8 +837,7 @@ async fn windows_sandbox_required_fallback_prompt_snapshot() { async fn startup_does_not_prompt_for_windows_sandbox_when_not_requested() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false); - chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); + chat.set_windows_sandbox_mode(/*mode*/ None); chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ false); assert!( @@ -980,7 +986,7 @@ async fn permissions_selection_emits_history_cell_when_selection_changes() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true); chat.open_permissions_popup(); @@ -1005,7 +1011,7 @@ async fn permissions_selection_history_snapshot_after_mode_switch() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false); chat.open_permissions_popup(); @@ -1041,7 +1047,8 @@ async fn permissions_selection_history_snapshot_full_access_to_default() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.windows_sandbox_local_server = true; + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.config .permissions @@ -1082,7 +1089,7 @@ async fn permissions_selection_emits_history_cell_when_current_is_selected() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.config .permissions @@ -1115,7 +1122,7 @@ async fn permissions_selection_hides_auto_review_when_feature_disabled() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false); chat.open_permissions_popup(); @@ -1133,7 +1140,7 @@ async fn permissions_selection_hides_auto_review_when_feature_disabled_even_if_a let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false); chat.config.approvals_reviewer = ApprovalsReviewer::AutoReview; @@ -1161,7 +1168,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } let _ = chat .config @@ -1206,7 +1213,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } let _ = chat .config @@ -1255,7 +1262,7 @@ async fn permissions_selection_can_disable_auto_review() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true); chat.config @@ -1294,7 +1301,7 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ true); chat.config @@ -1374,7 +1381,7 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation() let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; #[cfg(target_os = "windows")] { - chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.set_windows_sandbox_mode(Some(WindowsSandboxSetupMode::Unelevated)); } chat.set_feature_enabled(Feature::GuardianApproval, /*enabled*/ false); chat.open_permissions_popup(); @@ -1433,3 +1440,26 @@ async fn permissions_full_access_history_cell_emitted_only_after_confirmation() "expected full access update history message, got: {rendered}" ); } + +#[tokio::test] +async fn windows_sandbox_unelevated_policy_hides_admin_setup() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.windows_sandbox_config.requirements = + Some(Some(vec![WindowsSandboxSetupMode::Unelevated])); + chat.open_windows_sandbox_enable_prompt( + builtin_approval_presets() + .into_iter() + .find(|preset| preset.id == "auto") + .expect("auto preset"), + /*profile_selection*/ None, + ); + assert_chatwidget_snapshot!( + "windows_sandbox_unelevated_policy", + render_bottom_popup(&chat, /*width*/ 100) + ); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + assert!(matches!( + rx.try_recv(), + Ok(AppEvent::BeginWindowsSandboxLegacySetup { .. }) + )); +} diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index fbe018fb48..fc83e525a8 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -3,31 +3,11 @@ use super::*; impl ChatWidget { - #[cfg(any(target_os = "windows", test))] - pub(crate) fn windows_sandbox_mode_allowed(&self, mode: WindowsSandboxModeToml) -> bool { - self.config - .config_layer_stack - .requirements() - .windows_sandbox_mode - .can_set(&Some(mode)) - .is_ok() - } - - #[cfg(any(target_os = "windows", test))] - pub(crate) fn required_elevated_windows_sandbox(&self) -> bool { - crate::windows_sandbox::level_from_config(&self.config) == WindowsSandboxLevel::Elevated - && self - .config - .config_layer_stack - .requirements() - .windows_sandbox_mode - .source - .is_some() - } - #[cfg(any(target_os = "windows", test))] pub(super) fn elevated_windows_sandbox_setup_required(&self) -> bool { - self.required_elevated_windows_sandbox() && !self.windows_sandbox_elevated_setup_complete + self.windows_sandbox_config.requires_elevated() + && (self.windows_sandbox_host != crate::app::WindowsSandboxHost::Local + || !self.windows_sandbox_elevated_setup_complete) } #[cfg(any(target_os = "windows", test))] @@ -44,8 +24,9 @@ impl ChatWidget { &[], ); - let allow_unelevated = - self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated); + let allow_unelevated = self + .windows_sandbox_config + .allows(WindowsSandboxSetupMode::Unelevated); let setup_choice_is_required = !allow_unelevated || self.elevated_windows_sandbox_setup_required(); let mut header = ColumnRenderable::new(); @@ -70,7 +51,7 @@ impl ChatWidget { let quit_otel = self.session_telemetry.clone(); let retry_preset = preset.clone(); let retry_profile_selection = profile_selection.clone(); - let mut items = vec![SelectionItem { + let elevated_item = SelectionItem { name: "Set up default sandbox (requires Administrator permissions)".to_string(), description: None, actions: vec![Box::new(move |tx| { @@ -86,7 +67,15 @@ impl ChatWidget { })], dismiss_on_select: true, ..Default::default() - }]; + }; + let mut items = if self + .windows_sandbox_config + .allows(WindowsSandboxSetupMode::Elevated) + { + vec![elevated_item] + } else { + Vec::new() + }; if allow_unelevated { items.push(SelectionItem { name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(), @@ -155,8 +144,9 @@ impl ChatWidget { ) { use ratatui_macros::line; - let allow_unelevated = - self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated); + let allow_unelevated = self + .windows_sandbox_config + .allows(WindowsSandboxSetupMode::Unelevated); let setup_choice_is_required = !allow_unelevated || self.elevated_windows_sandbox_setup_required(); let mut lines = Vec::new(); @@ -187,7 +177,7 @@ impl ChatWidget { let elevated_profile_selection = profile_selection.clone(); let legacy_profile_selection = profile_selection; let quit_otel = self.session_telemetry.clone(); - let mut items = vec![SelectionItem { + let elevated_item = SelectionItem { name: "Try setting up admin sandbox again".to_string(), description: None, actions: vec![Box::new({ @@ -207,7 +197,15 @@ impl ChatWidget { })], dismiss_on_select: true, ..Default::default() - }]; + }; + let mut items = if self + .windows_sandbox_config + .allows(WindowsSandboxSetupMode::Elevated) + { + vec![elevated_item] + } else { + Vec::new() + }; if allow_unelevated { items.push(SelectionItem { name: "Use Codex with non-admin sandbox".to_string(), @@ -274,7 +272,7 @@ impl ChatWidget { #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) { - let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config); + let windows_sandbox_level = self.windows_sandbox_config.level(); let setup_is_required = windows_sandbox_level == WindowsSandboxLevel::Disabled || self.elevated_windows_sandbox_setup_required(); if show_now @@ -287,7 +285,7 @@ impl ChatWidget { } } - #[cfg(not(target_os = "windows"))] + #[cfg(all(not(target_os = "windows"), test))] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, _show_now: bool) {} #[cfg(any(target_os = "windows", test))] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 121a8542b9..8b9c20eb79 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -64,8 +64,6 @@ use codex_protocol::auth::AuthMode; use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::SandboxMode; -#[cfg(target_os = "windows")] -use codex_protocol::config_types::WindowsSandboxLevel; use codex_rollout::StateDbHandle; use codex_rollout::state_db; use codex_state::log_db; @@ -1810,20 +1808,7 @@ async fn run_ratatui_app( set_default_client_residency_requirement(config.enforce_residency.value()); let should_show_trust_screen = should_show_trust_screen(&config); #[cfg(target_os = "windows")] - let windows_sandbox_level = crate::windows_sandbox::level_from_config(&config); - #[cfg(target_os = "windows")] - let required_elevated_sandbox_needs_setup = windows_sandbox_level - == WindowsSandboxLevel::Elevated - && config - .config_layer_stack - .requirements() - .windows_sandbox_mode - .source - .is_some(); - #[cfg(target_os = "windows")] - let should_prompt_windows_sandbox_nux_at_startup = (trust_decision_was_made - && windows_sandbox_level == WindowsSandboxLevel::Disabled) - || required_elevated_sandbox_needs_setup; + let should_prompt_windows_sandbox_nux_at_startup = trust_decision_was_made; #[cfg(not(target_os = "windows"))] let should_prompt_windows_sandbox_nux_at_startup = false; diff --git a/codex-rs/tui/src/windows_sandbox.rs b/codex-rs/tui/src/windows_sandbox.rs index 15264028ec..f235792e93 100644 --- a/codex-rs/tui/src/windows_sandbox.rs +++ b/codex-rs/tui/src/windows_sandbox.rs @@ -13,7 +13,6 @@ use codex_features::Feature; use codex_protocol::config_types::WindowsSandboxLevel; use uuid::Uuid; -#[cfg_attr(not(test), allow(dead_code))] #[derive(Clone, Debug, Default, PartialEq, Eq)] pub(crate) struct WindowsSandboxConfig { pub(crate) mode: Option, @@ -21,7 +20,7 @@ pub(crate) struct WindowsSandboxConfig { pub(crate) requirements: Option>>, } -#[allow(dead_code)] +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] impl WindowsSandboxConfig { pub(crate) fn from_responses( config: &ConfigReadResponse,