Use app-server configuration for Windows sandbox state in the TUI (#45830)

## Why

The TUI's Windows sandbox turn-context override was ignored by `thread/settings/update`, and onboarding derived sandbox state from local configuration instead of the app server's effective configuration.

## What changed

- Read Windows sandbox configuration from the app server before showing the onboarding sandbox creation hint. Show the hint only when the read succeeds and the sandbox is disabled.
- Remove `windows_sandbox_level` from `AppCommand::OverrideTurnContext` and stop sending sandbox-only overrides after setup or feature changes.
- Rely on the effective configuration refresh after setup instead of updating local sandbox flags manually.

## Testing

Update the trust-directory rendering test and snapshot to cover the sandbox creation hint.

GitOrigin-RevId: 2ee6b0634812e7b39fd73dab3ae344729910ba70
This commit is contained in:
Eric Traut
2026-09-16 01:50:27 +00:00
committed by copyberry
parent 5bf132cd52
commit ca99b271d4
15 changed files with 29 additions and 130 deletions
@@ -228,7 +228,6 @@ impl App {
approvals_reviewer,
Some(permission_profile.clone()),
active_permission_profile,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
@@ -588,12 +587,6 @@ impl App {
let auto_review_preset = auto_review_mode();
let mut next_config = self.config.clone();
let windows_sandbox_changed = updates.iter().any(|(feature, _)| {
matches!(
feature,
Feature::WindowsSandbox | Feature::WindowsSandboxElevated
)
});
let mut approval_policy_override = None;
let mut approvals_reviewer_override = None;
let mut permission_profile_override = None;
@@ -733,9 +726,6 @@ impl App {
&feature_updates_to_apply,
)
.await;
if windows_sandbox_changed {
self.propagate_windows_sandbox_turn_context();
}
}
return;
}
@@ -803,7 +793,6 @@ impl App {
approvals_reviewer_override,
permission_profile_override,
active_permission_profile_override,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
@@ -820,10 +809,6 @@ impl App {
}
}
if windows_sandbox_changed {
self.propagate_windows_sandbox_turn_context();
}
if let Some(label) = permissions_history_label {
self.chat_widget.add_info_message(
format!("Permissions updated to {label}"),
@@ -1205,7 +1190,6 @@ impl App {
Some(self.config.approvals_reviewer),
/*permission_profile*/ None,
Some(auto_review_preset.active_permission_profile),
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
@@ -1265,28 +1249,6 @@ impl App {
self.chat_widget.add_error_message("Windows sandbox setup completed, but its mode was overridden by the effective configuration.".to_string());
false
}
fn propagate_windows_sandbox_turn_context(&self) {
#[cfg(target_os = "windows")]
{
let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config);
self.app_event_tx
.send(AppEvent::CodexOp(AppCommand::override_turn_context(
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
Some(windows_sandbox_level),
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
/*service_tier*/ None,
/*collaboration_mode*/ None,
/*personality*/ None,
)));
}
}
}
pub(super) fn overridden_write_message(write_response: &ConfigWriteResponse) -> &str {
+5 -38
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_app_server_protocol::WindowsSandboxSetupMode as WindowsSandboxModeToml;
use codex_app_server_protocol::WindowsSandboxSetupMode;
pub(super) const SHUTDOWN_FIRST_EXIT_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 2);
@@ -838,15 +838,13 @@ impl App {
approvals_reviewer,
permission_profile,
active_permission_profile,
windows_sandbox_level,
..
} = &op
&& (cwd.is_some()
|| approval_policy.is_some()
|| approvals_reviewer.is_some()
|| permission_profile.is_some()
|| active_permission_profile.is_some()
|| windows_sandbox_level.is_some())
|| active_permission_profile.is_some())
&& self.reject_pending_permission_change()
{
return Ok(AppRunControl::Continue);
@@ -2043,46 +2041,17 @@ impl App {
);
}
let selected_mode = match mode {
WindowsSandboxEnableMode::Elevated => WindowsSandboxModeToml::Elevated,
WindowsSandboxEnableMode::Legacy => WindowsSandboxModeToml::Unelevated,
WindowsSandboxEnableMode::Elevated => WindowsSandboxSetupMode::Elevated,
WindowsSandboxEnableMode::Legacy => WindowsSandboxSetupMode::Unelevated,
};
let elevated_enabled = selected_mode == WindowsSandboxModeToml::Elevated;
let elevated_enabled = selected_mode == WindowsSandboxSetupMode::Elevated;
if self
.verify_windows_sandbox_mode_after_setup(app_server, selected_mode)
.await
{
self.chat_widget.windows_sandbox_elevated_setup_complete =
elevated_enabled;
if elevated_enabled {
self.config.set_windows_sandbox_enabled(/*value*/ false);
self.config
.set_windows_elevated_sandbox_enabled(/*value*/ true);
} else {
self.config.set_windows_sandbox_enabled(/*value*/ true);
self.config
.set_windows_elevated_sandbox_enabled(/*value*/ false);
}
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 {
self.app_event_tx.send(AppEvent::CodexOp(
AppCommand::override_turn_context(
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
/*service_tier*/ None,
/*collaboration_mode*/ None,
/*personality*/ None,
),
));
if self.apply_permission_profile_selection(selection).await {
self.chat_widget.submit_initial_user_message_if_pending();
}
@@ -2094,8 +2063,6 @@ impl App {
Some(self.config.approvals_reviewer),
Some(preset.permission_profile.clone()),
Some(preset.active_permission_profile.clone()),
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
+5 -11
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_app_server_protocol::WindowsSandboxSetupMode as WindowsSandboxModeToml;
use codex_app_server_protocol::WindowsSandboxSetupMode;
#[cfg(any(target_os = "windows", test))]
use codex_utils_approval_presets::ApprovalPreset;
@@ -158,15 +158,9 @@ impl App {
profile_selection: Option<PermissionProfileSelection>,
mode: WindowsSandboxEnableMode,
) {
let (setup_mode, config_mode) = match mode {
WindowsSandboxEnableMode::Elevated => (
codex_app_server_protocol::WindowsSandboxSetupMode::Elevated,
WindowsSandboxModeToml::Elevated,
),
WindowsSandboxEnableMode::Legacy => (
codex_app_server_protocol::WindowsSandboxSetupMode::Unelevated,
WindowsSandboxModeToml::Unelevated,
),
let setup_mode = match mode {
WindowsSandboxEnableMode::Elevated => WindowsSandboxSetupMode::Elevated,
WindowsSandboxEnableMode::Legacy => WindowsSandboxSetupMode::Unelevated,
};
if self.windows_sandbox.pending_setup.is_some() {
if self.windows_sandbox.setup_started_at.is_none() {
@@ -180,7 +174,7 @@ impl App {
if !self.refresh_windows_sandbox_config(app_server).await {
return;
}
if !self.chat_widget.windows_sandbox_config.allows(config_mode) {
if !self.chat_widget.windows_sandbox_config.allows(setup_mode) {
self.chat_widget
.retain_input_after_failed_permission_selection();
self.chat_widget.add_info_message(
-6
View File
@@ -3239,7 +3239,6 @@ default_permissions = "locked-down"
approvals_reviewer: None,
permission_profile: Some(app.config.permissions.permission_profile().clone()),
active_permission_profile: app.config.permissions.active_permission_profile(),
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@@ -3333,7 +3332,6 @@ async fn update_feature_flags_enabling_guardian_selects_auto_review() -> Result<
approvals_reviewer: Some(auto_review.approvals_reviewer),
permission_profile: Some(auto_review.permission_profile()),
active_permission_profile: Some(auto_review.active_permission_profile.clone()),
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@@ -3428,7 +3426,6 @@ async fn update_feature_flags_disabling_guardian_clears_review_policy_and_restor
approvals_reviewer: Some(ApprovalsReviewer::User),
permission_profile: None,
active_permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@@ -3509,7 +3506,6 @@ async fn update_feature_flags_enabling_guardian_overrides_explicit_manual_review
approvals_reviewer: Some(auto_review.approvals_reviewer),
permission_profile: Some(auto_review.permission_profile()),
active_permission_profile: Some(auto_review.active_permission_profile.clone()),
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@@ -3569,7 +3565,6 @@ async fn update_feature_flags_disabling_guardian_clears_manual_review_policy_wit
approvals_reviewer: Some(ApprovalsReviewer::User),
permission_profile: None,
active_permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@@ -8713,7 +8708,6 @@ async fn override_turn_context_sends_thread_settings_update() {
Some(ActivePermissionProfile::new(
codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
)),
/*windows_sandbox_level*/ None,
Some("gpt-5.4".to_string()),
Some(Some(ReasoningEffortConfig::High)),
/*summary*/ None,
-3
View File
@@ -157,9 +157,6 @@ impl App {
approvals_reviewer,
permission_profile: _,
active_permission_profile,
// TODO(anp): Support Windows sandbox updates through environment configuration;
// thread/settings/update cannot currently represent this override.
windows_sandbox_level: _,
model,
effort,
summary,
-4
View File
@@ -16,7 +16,6 @@ use codex_protocol::approvals::GuardianAssessmentEvent;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -138,7 +137,6 @@ pub(crate) enum AppCommand {
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: Option<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffortConfig>>,
summary: Option<ReasoningSummaryConfig>,
@@ -255,7 +253,6 @@ impl AppCommand {
approvals_reviewer: Option<ApprovalsReviewer>,
permission_profile: Option<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
windows_sandbox_level: Option<WindowsSandboxLevel>,
model: Option<String>,
effort: Option<Option<ReasoningEffortConfig>>,
summary: Option<ReasoningSummaryConfig>,
@@ -269,7 +266,6 @@ impl AppCommand {
approvals_reviewer,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
summary,
@@ -274,7 +274,6 @@ impl ChatWidget {
Some(approvals_reviewer),
Some(permission_profile.clone()),
Some(active_permission_profile.clone()),
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
@@ -450,7 +450,6 @@ impl ChatWidget {
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
/*windows_sandbox_level*/ None,
Some(switch_model_for_events.clone()),
Some(Some(default_effort.clone())),
/*summary*/ None,
@@ -113,7 +113,6 @@ impl ChatWidget {
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
-1
View File
@@ -697,7 +697,6 @@ impl ChatWidget {
/*approvals_reviewer*/ None,
/*permission_profile*/ None,
/*active_permission_profile*/ None,
/*windows_sandbox_level*/ None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
@@ -1351,7 +1351,6 @@ async fn permissions_selection_sends_approvals_reviewer_in_override_turn_context
active_permission_profile: Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE,
)),
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
@@ -139,6 +139,22 @@ impl OnboardingScreen {
tui.frame_requester(),
local_settings.tui.animations,
)));
#[cfg(target_os = "windows")]
let show_windows_create_sandbox_hint = if show_trust_screen
&& remote_project_trust.is_none()
&& let Some(handle) = &app_server_request_handle
{
crate::windows_sandbox::WindowsSandboxConfig::read(
handle.clone(),
config.cwd.display().to_string(),
)
.await
.is_ok_and(|state| state.level() == WindowsSandboxLevel::Disabled)
} else {
false
};
#[cfg(not(target_os = "windows"))]
let show_windows_create_sandbox_hint = false;
if show_login_screen {
let highlighted_mode =
if auth_config.is_login_method_allowed(ForcedLoginMethod::Chatgpt) {
@@ -163,11 +179,6 @@ impl OnboardingScreen {
tracing::warn!("skipping onboarding login step without app-server request handle");
}
}
#[cfg(target_os = "windows")]
let show_windows_create_sandbox_hint = remote_project_trust.is_none()
&& crate::windows_sandbox::level_from_config(&config) == WindowsSandboxLevel::Disabled;
#[cfg(not(target_os = "windows"))]
let show_windows_create_sandbox_hint = false;
let highlighted = TrustDirectorySelection::Trust;
if show_trust_screen {
let (cwd, trust_target) = match remote_project_trust {
@@ -13,4 +13,4 @@ expression: terminal.backend()
› 1. Trust and continue
2. Quit
Press enter to continue; esc to quit
Press enter to continue and create a sandbox...
@@ -298,7 +298,8 @@ mod tests {
#[test]
fn renders_snapshot_for_git_repo() {
let widget = widget(/*error*/ None);
let mut widget = widget(/*error*/ None);
widget.show_windows_create_sandbox_hint = true;
let mut terminal =
Terminal::new(VT100Backend::new(/*width*/ 70, /*height*/ 14)).expect("terminal");
-18
View File
@@ -1,6 +1,5 @@
//! Windows sandbox configuration, managed requirements, and executor selection for the TUI.
use crate::legacy_core::config::Config;
use codex_app_server_client::AppServerRequestHandle;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ConfigReadResponse;
@@ -8,8 +7,6 @@ use codex_app_server_protocol::ConfigRequirementsReadResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::WindowsSandboxImplementation;
use codex_app_server_protocol::WindowsSandboxSetupMode;
use codex_config::types::WindowsSandboxModeToml;
use codex_features::Feature;
use codex_protocol::config_types::WindowsSandboxLevel;
use uuid::Uuid;
@@ -131,21 +128,6 @@ impl WindowsSandboxConfig {
}
}
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
pub(crate) fn level_from_config(config: &Config) -> WindowsSandboxLevel {
match config.permissions.windows_sandbox_mode {
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
None if config.features.enabled(Feature::WindowsSandboxElevated) => {
WindowsSandboxLevel::Elevated
}
None if config.features.enabled(Feature::WindowsSandbox) => {
WindowsSandboxLevel::RestrictedToken
}
None => WindowsSandboxLevel::Disabled,
}
}
/// A server-local connection can still select remote executors. Missing selections are unknown.
pub(crate) fn host_from_environments(
environments: Option<&[codex_app_server_protocol::ThreadEnvironment]>,