From bc197b77bc57ff659c5d5e9aa708e1e49ed61757 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 1 Oct 2026 03:57:11 +0000 Subject: [PATCH] Use embedded mode for elevated Windows TUI sessions (#49855) ## Why The Windows shared daemon rejects elevated launches so shared clients cannot inherit administrator privileges. Local TUI sessions launched as administrator should still be able to start. ## What changed Select embedded mode before daemon discovery or startup for elevated local Windows sessions. Show a startup warning explaining that the shared background server is disabled and that restarting Codex in a non-elevated terminal enables it. Record `elevated_windows` as the server selection reason. ## Testing Add Windows integration coverage for elevated normal, `resume`, and `fork` launches without creating daemon directories, and verify that explicit daemon `start` and `restart` still reject elevation. Add a snapshot test for the startup warning in the warning center. GitOrigin-RevId: 89594e86d6ee19b080dfb11553cee8676395f188 --- .../app-server-daemon/src/backend/windows.rs | 10 ++- codex-rs/app-server-daemon/src/lib.rs | 2 + codex-rs/cli/tests/daemon_startup.rs | 66 +++++++++++++++---- codex-rs/cli/tests/worktree.rs | 4 ++ ...tests__elevated_launch_warning_center.snap | 29 ++++++++ codex-rs/tui/src/app/warning_notice_tests.rs | 19 ++++++ codex-rs/tui/src/daemon_startup.rs | 6 +- codex-rs/tui/src/startup_orchestration.rs | 23 ++++++- 8 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__warning_notice_tests__elevated_launch_warning_center.snap diff --git a/codex-rs/app-server-daemon/src/backend/windows.rs b/codex-rs/app-server-daemon/src/backend/windows.rs index 53603afe2b..af72ebef29 100644 --- a/codex-rs/app-server-daemon/src/backend/windows.rs +++ b/codex-rs/app-server-daemon/src/backend/windows.rs @@ -88,7 +88,9 @@ pub(super) fn spawn_without_inheriting_stdio( Ok(command.spawn()?) } -pub(crate) fn ensure_not_elevated() -> Result<()> { +/// Reports whether this process has administrator privileges, rather than +/// merely belonging to an administrator account. +pub fn is_elevated() -> Result { let mut token = 0; if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) } == 0 { return Err(io::Error::last_os_error()).context("failed to query daemon launcher token"); @@ -109,8 +111,12 @@ pub(crate) fn ensure_not_elevated() -> Result<()> { return Err(io::Error::last_os_error()) .context("failed to query daemon launcher elevation"); } + Ok(elevation.TokenIsElevated != 0) +} + +pub(crate) fn ensure_not_elevated() -> Result<()> { anyhow::ensure!( - elevation.TokenIsElevated == 0, + !is_elevated()?, "start the Windows daemon from a non-elevated terminal; shared clients must not inherit administrator privileges" ); Ok(()) diff --git a/codex-rs/app-server-daemon/src/lib.rs b/codex-rs/app-server-daemon/src/lib.rs index 24df96a586..cadc695b23 100644 --- a/codex-rs/app-server-daemon/src/lib.rs +++ b/codex-rs/app-server-daemon/src/lib.rs @@ -5,6 +5,8 @@ mod background_command; #[cfg(windows)] pub use backend::windows::DetachedLaunchRestricted; #[cfg(windows)] +pub use backend::windows::is_elevated; +#[cfg(windows)] use backend::windows::try_lock_file; mod client; mod diagnostics; diff --git a/codex-rs/cli/tests/daemon_startup.rs b/codex-rs/cli/tests/daemon_startup.rs index 3f6a22e7d8..8f6cce3d55 100644 --- a/codex-rs/cli/tests/daemon_startup.rs +++ b/codex-rs/cli/tests/daemon_startup.rs @@ -59,6 +59,37 @@ async fn daemon_auto_start_preserves_bedrock_onboarding() -> Result<()> { daemon_startup("bedrock").await } +#[tokio::test] +#[cfg(windows)] +async fn elevated_local_tui_uses_embedded_without_starting_daemon() -> Result<()> { + if !codex_app_server_daemon::is_elevated()? { + eprintln!("requires an elevated Windows runner"); + return Ok(()); + } + for command in ["elevated", "elevated-resume", "elevated-fork"] { + daemon_startup(command).await?; + } + let codex = codex_utils_cargo_bin::cargo_bin("codex")?; + let home = tempfile::tempdir()?; + for operation in ["start", "restart"] { + let output = Command::new(&codex) + .env("CODEX_HOME", home.path()) + .args(["app-server", "daemon", operation]) + .output()?; + ensure!( + !output.status.success(), + "{operation} must reject elevation" + ); + ensure!( + String::from_utf8_lossy(&output.stderr).contains("non-elevated terminal"), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + } + ensure!(!home.path().join("app-server-daemon").exists()); + Ok(()) +} + #[tokio::test] #[cfg(windows)] async fn restrictive_launcher_uses_embedded_if_daemon_cannot_start() -> Result<()> { @@ -97,6 +128,11 @@ async fn bedrock_onboarding_leaves_a_running_daemon_untouched() -> Result<()> { } async fn daemon_startup(command: &str) -> Result<()> { + #[cfg(windows)] + let elevated = codex_app_server_daemon::is_elevated()?; + #[cfg(not(windows))] + let elevated = false; + let elevated_launch = command.starts_with("elevated"); let codex = codex_utils_cargo_bin::cargo_bin("codex")?.canonicalize()?; let workspace = tempfile::tempdir()?; let workspace_path = workspace.path().canonicalize()?; @@ -206,6 +242,13 @@ async fn daemon_startup(command: &str) -> Result<()> { // The draft header is visible before the session's command composer is ready. steps.push_back(("GPT-5.6-Terra", b"/status\r")); "Server:Localbackgroundserver" + } else if elevated_launch || command == "restrictive-job" && elevated { + if let Some(action) = command.strip_prefix("elevated-") { + args.push(action.into()); + steps.push_back(("Nosessionsyet", b"\x1b")); + } + steps.push_back(("GPT-5.6-Terra", b"\x14")); + "Runningasadministrator:sharedbackgroundserverdisabled." } else if command == "restrictive-job" { steps.push_back(("GPT-5.6-Terra", b"\x14")); "Runningwithoutthesharedbackgroundserver:thisWindowslauncher" @@ -234,7 +277,11 @@ async fn daemon_startup(command: &str) -> Result<()> { args.extend([command.into(), "--strict-config".into()]); steps.push_back(("Nosessionsyet", b"\x1b")); steps.push_back(("GPT-5.6-Terra", b"\x14")); - "Runningwithoutthesharedbackgroundserver:--strict-config" + if elevated { + "Runningasadministrator:sharedbackgroundserverdisabled." + } else { + "Runningwithoutthesharedbackgroundserver:--strict-config" + } }; let program = if cfg!(windows) && command == "restrictive-job" { env.insert("CODEX_TEST_RESTRICTIVE_DAEMON_START".into(), "1".into()); @@ -260,7 +307,7 @@ async fn daemon_startup(command: &str) -> Result<()> { codex_utils_pty::ChildFds::Inherited(&[]), ) .await?; - let mut exit = spawned.exit_rx; + let exit = spawned.exit_rx; let session = spawned.session; let writer = session.writer_sender(); let mut stdout = spawned.stdout_rx; @@ -293,16 +340,6 @@ async fn daemon_startup(command: &str) -> Result<()> { .chars() .filter(|c| !c.is_whitespace()) .collect(); - if command == "restrictive-job" - && text.contains("starttheWindowsdaemonfromanon-elevatedterminal") - { - // Elevated runners must still reject startup before the job probe. - ensure!(!pid_file.exists()); - let status = tokio::time::timeout(Duration::from_secs(/*secs*/ 5), &mut exit) - .await??; - ensure!(status != 0, "elevated daemon launch must fail"); - return Ok::<_, anyhow::Error>(()); - } if let Some((ready, input)) = steps.front() && text.contains(ready) { @@ -332,9 +369,12 @@ async fn daemon_startup(command: &str) -> Result<()> { ensure!(home.path().join("app-server-daemon/daemon.pid").exists()); } else if let Some(existing_daemon) = &existing_daemon { ensure!(fs::read(&pid_file)? == *existing_daemon); + } else if elevated_launch { + ensure!(!home.path().join("app-server-daemon").exists()); + ensure!(!home.path().join("packages/app-server-daemon").exists()); } else if bedrock_onboarding || command == "restrictive-job" { ensure!(!pid_file.exists()); - if command == "restrictive-job" { + if command == "restrictive-job" && !elevated { let contents = screen.screen().contents(); let warning = contents.lines() .find(|line| line.contains("Running without the shared background server:")) diff --git a/codex-rs/cli/tests/worktree.rs b/codex-rs/cli/tests/worktree.rs index 1ec06aa306..32a65e0562 100644 --- a/codex-rs/cli/tests/worktree.rs +++ b/codex-rs/cli/tests/worktree.rs @@ -660,6 +660,10 @@ trust_level = "trusted" ("update_interval_setting", "default"), ("shutdown_grace_setting", "default"), ]); + #[cfg(windows)] + if codex_app_server_daemon::is_elevated()? { + expected_tags.insert("daemon_selection_reason", "elevated_windows"); + } if backend == "daemon" { expected_tags.extend([ ("auto_update", "disabled"), diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__warning_notice_tests__elevated_launch_warning_center.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__warning_notice_tests__elevated_launch_warning_center.snap new file mode 100644 index 0000000000..a03de6a489 --- /dev/null +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__warning_notice_tests__elevated_launch_warning_center.snap @@ -0,0 +1,29 @@ +--- +source: tui/src/app/warning_notice_tests.rs +assertion_line: 116 +expression: screen(&tui) +--- + + + + + + + + + + + + + Warnings · 1 of 1 · Startup + + Running as administrator: shared background server disabled. To enable it, + restart Codex in a terminal without administrator permissions. + + + + + + + + k keep & next · esc dismiss & close · ctrl+o copy · ←/→ warning · ↓ scroll diff --git a/codex-rs/tui/src/app/warning_notice_tests.rs b/codex-rs/tui/src/app/warning_notice_tests.rs index 649160e1e6..e8f36e183f 100644 --- a/codex-rs/tui/src/app/warning_notice_tests.rs +++ b/codex-rs/tui/src/app/warning_notice_tests.rs @@ -96,3 +96,22 @@ async fn warning_notice_keeps_details_in_transcript_and_preserves_draft() -> Res tui.set_owned_screen(/*owned*/ false)?; Ok(()) } + +#[tokio::test] +async fn elevated_launch_warning_appears_in_warning_center() -> Result<()> { + let mut app = crate::app::test_support::make_test_app().await; + let mut tui = crate::tui::test_support::make_test_tui()?; + tui.set_owned_screen(/*owned*/ true)?; + let size = Size::new(/*width*/ 80, /*height*/ 24); + app.insert_history_cell( + &mut tui, + Box::new(history_cell::StartupWarningsCell::new(vec![ + crate::daemon_startup::ELEVATED_LAUNCH_WARNING.into(), + ])), + ); + app.chat_widget.open_warnings(&app.transcript_cells); + app.render_owned_transcript(&mut tui, size)?; + insta::assert_snapshot!("elevated_launch_warning_center", screen(&tui)); + tui.set_owned_screen(/*owned*/ false)?; + Ok(()) +} diff --git a/codex-rs/tui/src/daemon_startup.rs b/codex-rs/tui/src/daemon_startup.rs index ba6de532fb..0831d92e3c 100644 --- a/codex-rs/tui/src/daemon_startup.rs +++ b/codex-rs/tui/src/daemon_startup.rs @@ -1,7 +1,8 @@ //! Local daemon launch policy. Explicit embedded launches never discover or start a daemon; //! optional attachment may fall back to embedded mode, while automatic launches //! require a compatible shared server and a successful connection, except when -//! the Windows launcher forbids detaching a missing server. +//! the Windows launcher forbids detaching a missing server. Elevated local +//! Windows sessions use explicit embedded behavior before discovery or startup. use super::*; use std::collections::BTreeMap; @@ -15,6 +16,9 @@ const SERVER_FEATURES: [Feature; 4] = [ pub(super) const FAILURE_HINT: &str = "To work without the background server, rerun the same command with --no-daemon (including resume or fork and its arguments)."; +#[cfg(any(windows, test))] +pub(super) const ELEVATED_LAUNCH_WARNING: &str = "Running as administrator: shared background server disabled. To enable it, restart Codex in a terminal without administrator permissions."; + #[derive(Debug, thiserror::Error)] #[error("Cannot use the shared background server: {reason}.\n{FAILURE_HINT}")] pub(super) struct CompatibilityError { diff --git a/codex-rs/tui/src/startup_orchestration.rs b/codex-rs/tui/src/startup_orchestration.rs index a0ff9cb31f..ba442b70e0 100644 --- a/codex-rs/tui/src/startup_orchestration.rs +++ b/codex-rs/tui/src/startup_orchestration.rs @@ -23,6 +23,19 @@ pub(super) async fn run_main_inner( "--add-dir is not supported with --remote. Configure additional workspace roots on the server.", )); } + #[cfg(windows)] + let elevated_warning = if explicit_remote_endpoint.is_none() + && !cli.no_daemon + && !cli.agents_overview + && codex_app_server_daemon::is_elevated().map_err(std::io::Error::other)? + { + cli.no_daemon = true; + Some(daemon_startup::ELEVATED_LAUNCH_WARNING) + } else { + None + }; + #[cfg(not(windows))] + let elevated_warning: Option<&str> = None; let strict_config = cli.strict_config; if cli.shared.worktree { if explicit_remote_endpoint.is_some() { @@ -574,15 +587,18 @@ pub(super) async fn run_main_inner( if app_server_target.uses_embedded_network_policy() { embedded_network_policy.activate(&mut config); } - let daemon_startup_warning = compatibility_warning.or_else(|| { - daemon_exclusion + let daemon_startup_warning = elevated_warning + .map(str::to_string) + .or(compatibility_warning) + .or_else(|| { + daemon_exclusion .filter(|_| auto_start_daemon) .map(|reason| { format!( "Running without the shared background server: {reason} requires embedded mode." ) }) - }); + }); #[cfg(target_os = "macos")] let local_runtime_paths = local_runtime_paths.with_allowed_symlinked_codex_home( codex_config::allowed_symlinked_codex_home(&config.config_layer_stack, &config.codex_home), @@ -644,6 +660,7 @@ pub(super) async fn run_main_inner( let selection_reason = match (&app_server_target, daemon_exclusion) { (AppServerTarget::Remote { .. }, _) => "explicit_remote", _ if cli.agents_overview => "agents", + _ if elevated_warning.is_some() => "elevated_windows", (_, Some("--no-daemon")) => "explicit_no_daemon", (_, Some(_)) => "incompatible_option", _ if auto_start_daemon => "auto_start",