mirror of
https://github.com/openai/codex.git
synced 2026-10-21 12:10:06 +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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))]
|
||||
|
||||
+1
-16
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user