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
+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))]