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
This commit is contained in:
Eric Traut
2026-09-16 01:01:26 +00:00
committed by copyberry
parent f2b5b81f39
commit 73db60e71f
27 changed files with 476 additions and 341 deletions
-2
View File
@@ -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;
+10 -37
View File
@@ -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<FeaturesToml> {
serde_json::from_value(value.clone()).ok()
}
#[cfg(target_os = "windows")]
fn windows_sandbox_mode_from_effective_config(
effective_config: &ConfigReadResponse,
) -> Option<codex_config::types::WindowsSandboxModeToml> {
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<WindowsToml> {
serde_json::from_value(value.clone()).ok()
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
+16 -16
View File
@@ -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 {
+83 -28
View File
@@ -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<Instant>,
#[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()),
},
}),
)
+4 -1
View File
@@ -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 =
+2 -1
View File
@@ -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 =
+15 -34
View File
@@ -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(),
@@ -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()?;
@@ -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() {
+95 -56
View File
@@ -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::<Vec<_>>()
});
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::<Vec<_>>()
});
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(())
}
+6
View File
@@ -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 {
+8 -4
View File
@@ -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<image_submission::PendingImageSubmission>,
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(
+3 -8
View File
@@ -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
+14 -3
View File
@@ -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);
@@ -41,15 +41,20 @@ impl ChatWidget {
let presets: Vec<ApprovalPreset> = 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 {
@@ -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;
}
@@ -9,6 +9,12 @@ impl ChatWidget {
display: SessionConfiguredDisplay,
fork_parent_title: Option<String>,
) {
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();
+16 -18
View File
@@ -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<WindowsSandboxModeToml>) {
self.config.permissions.windows_sandbox_mode = mode;
pub(crate) fn set_windows_sandbox_mode(&mut self, mode: Option<WindowsSandboxSetupMode>) {
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);
+13 -7
View File
@@ -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;
@@ -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
<https://developers.openai.com/codex/windows>
› 1. Use non-admin sandbox (higher risk if prompt injected)
2. Quit
Press enter to confirm or esc to go back
+1 -1
View File
@@ -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;
@@ -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);
@@ -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"
));
}
}
@@ -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<WindowsSandboxModeToml>,
) -> 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 { .. })
));
}
@@ -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))]
+1 -16
View File
@@ -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;
+1 -2
View File
@@ -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<WindowsSandboxSetupMode>,
@@ -21,7 +20,7 @@ pub(crate) struct WindowsSandboxConfig {
pub(crate) requirements: Option<Option<Vec<WindowsSandboxSetupMode>>>,
}
#[allow(dead_code)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
impl WindowsSandboxConfig {
pub(crate) fn from_responses(
config: &ConfigReadResponse,