mirror of
https://github.com/openai/codex.git
synced 2026-10-20 08:19:50 +00:00
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
This commit is contained in:
@@ -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<bool> {
|
||||
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(())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:"))
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+29
@@ -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
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user