mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+11
@@ -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
|
||||
@@ -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))]
|
||||
|
||||
Reference in New Issue
Block a user