Compare commits

...

4 Commits

Author SHA1 Message Date
David Wiesen
3e0931e079 fix(windows-sandbox): disable private desktop in headless sessions 2026-04-30 10:00:43 -07:00
David Wiesen
080a390eb1 fix(windows-sandbox): launch legacy sandbox via junction cwd 2026-04-30 10:00:21 -07:00
David Wiesen
c92ca4d0c7 fix(windows-sandbox): normalize mapped drive workspaces 2026-04-30 09:58:51 -07:00
David Wiesen
2ba1fedded fix(windows): normalize sandbox cwd for mapped drives 2026-04-30 09:57:40 -07:00
12 changed files with 447 additions and 24 deletions

View File

@@ -10,6 +10,7 @@ use codex_login::default_client::originator;
use codex_otel::sanitize_metric_tag_value;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_path::env::is_headless_environment;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
@@ -85,7 +86,21 @@ pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml, profile: &Confi
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
})
.unwrap_or(true)
.unwrap_or_else(default_windows_sandbox_private_desktop)
}
fn default_windows_sandbox_private_desktop() -> bool {
default_windows_sandbox_private_desktop_for_environment(
cfg!(target_os = "windows"),
is_headless_environment(),
)
}
fn default_windows_sandbox_private_desktop_for_environment(
is_windows: bool,
is_headless: bool,
) -> bool {
!is_windows || !is_headless
}
fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool {

View File

@@ -166,9 +166,9 @@ fn resolve_windows_sandbox_private_desktop_prefers_profile_windows() {
#[test]
fn resolve_windows_sandbox_private_desktop_defaults_to_true() {
assert!(resolve_windows_sandbox_private_desktop(
&ConfigToml::default(),
&ConfigProfile::default()
assert!(default_windows_sandbox_private_desktop_for_environment(
/*is_windows*/ true,
/*is_headless*/ false
));
}
@@ -187,3 +187,19 @@ fn resolve_windows_sandbox_private_desktop_respects_explicit_cfg_value() {
&ConfigProfile::default()
));
}
#[test]
fn resolve_windows_sandbox_private_desktop_defaults_to_false_for_headless_windows() {
assert!(!default_windows_sandbox_private_desktop_for_environment(
/*is_windows*/ true,
/*is_headless*/ true
));
}
#[test]
fn resolve_windows_sandbox_private_desktop_non_windows_default_stays_true() {
assert!(default_windows_sandbox_private_desktop_for_environment(
/*is_windows*/ false,
/*is_headless*/ true
));
}

View File

@@ -84,6 +84,7 @@ features = [
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",
"Win32_System_Registry",
"Win32_NetworkManagement_WNet",
]
version = "0.52"

View File

@@ -35,7 +35,9 @@ use codex_windows_sandbox::encode_bytes;
use codex_windows_sandbox::get_current_token_for_restriction;
use codex_windows_sandbox::hide_current_user_profile_dir;
use codex_windows_sandbox::log_note;
use codex_windows_sandbox::normalize_spawn_cwd;
use codex_windows_sandbox::parse_policy;
use codex_windows_sandbox::path_uses_unc_prefix;
use codex_windows_sandbox::read_frame;
use codex_windows_sandbox::read_handle_loop;
use codex_windows_sandbox::spawn_process_with_pipes;
@@ -200,6 +202,21 @@ fn read_spawn_request(reader: &mut File) -> Result<SpawnRequest> {
/// Pick an effective CWD, using a junction if the ACL helper is active.
fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
let normalized_cwd = normalize_spawn_cwd(req_cwd);
if path_uses_unc_prefix(&normalized_cwd) {
if normalized_cwd != req_cwd {
log_note(
&format!(
"junction: using UNC cwd {} resolved from {}",
normalized_cwd.display(),
req_cwd.display()
),
log_dir,
);
}
return normalized_cwd;
}
let use_junction = match read_acl_mutex::read_acl_mutex_exists() {
Ok(exists) => exists,
Err(err) => {
@@ -213,9 +230,10 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
}
};
if use_junction {
cwd_junction::create_cwd_junction(req_cwd, log_dir).unwrap_or_else(|| req_cwd.to_path_buf())
cwd_junction::create_cwd_junction(req_cwd, log_dir)
.unwrap_or_else(|| normalized_cwd.clone())
} else {
req_cwd.to_path_buf()
normalized_cwd
}
}

View File

@@ -4,6 +4,7 @@ use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::ipc_framed::read_frame;
use crate::ipc_framed::write_frame;
use crate::path_normalization::resolve_sandbox_path;
use crate::runner_pipe::PIPE_ACCESS_INBOUND;
use crate::runner_pipe::PIPE_ACCESS_OUTBOUND;
use crate::runner_pipe::connect_pipe;
@@ -241,7 +242,8 @@ pub(crate) fn spawn_runner_transport(
);
let mut cmdline_vec = to_wide(&runner_full_cmd);
let exe_w = to_wide(&runner_cmdline);
let cwd_w = to_wide(cwd);
let resolved_cwd = resolve_sandbox_path(cwd);
let cwd_w = to_wide(&resolved_cwd);
let user_w = to_wide(&sandbox_creds.username);
let domain_w = to_wide(".");
let password_w = to_wide(&sandbox_creds.password);

View File

@@ -34,6 +34,7 @@ mod windows_impl {
use crate::logging::log_failure;
use crate::logging::log_start;
use crate::logging::log_success;
use crate::path_normalization::resolve_sandbox_path;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::runner_client::spawn_runner_transport;
@@ -184,12 +185,14 @@ mod windows_impl {
}
(|| -> Result<CaptureResult> {
let resolved_cwd = resolve_sandbox_path(cwd);
let resolved_policy_cwd = resolve_sandbox_path(sandbox_policy_cwd);
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
cwd: resolved_cwd,
env: env_map.clone(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
sandbox_policy_cwd: resolved_policy_cwd,
codex_home: sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids,

View File

@@ -0,0 +1,209 @@
#![cfg(target_os = "windows")]
use crate::log_note;
use crate::path_normalization::normalize_spawn_cwd;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use std::os::windows::fs::MetadataExt as _;
use std::os::windows::process::CommandExt as _;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::path::Prefix;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
fn junction_name_for_path(path: &Path) -> String {
let mut hasher = DefaultHasher::new();
path.to_string_lossy().hash(&mut hasher);
format!("{:x}", hasher.finish())
}
fn junction_root_for_userprofile(userprofile: &str) -> PathBuf {
PathBuf::from(userprofile)
.join(".codex")
.join(".sandbox")
.join("cwd")
}
fn drive_letter(path: &Path) -> Option<char> {
match path.components().next()? {
Component::Prefix(prefix) => match prefix.kind() {
Prefix::Disk(drive) | Prefix::VerbatimDisk(drive) => {
Some((drive as char).to_ascii_uppercase())
}
_ => None,
},
_ => None,
}
}
fn system_drive_letter(system_drive: Option<&str>) -> Option<char> {
drive_letter(Path::new(system_drive?))
}
fn should_materialize_junction(requested_cwd: &Path, system_drive: Option<&str>) -> bool {
let Some(requested_drive) = drive_letter(requested_cwd) else {
return false;
};
let Some(system_drive) = system_drive_letter(system_drive) else {
return false;
};
requested_drive != system_drive
}
fn create_cwd_junction(requested_cwd: &Path, log_dir: Option<&Path>) -> Option<PathBuf> {
let userprofile = std::env::var("USERPROFILE").ok()?;
let junction_root = junction_root_for_userprofile(&userprofile);
if let Err(err) = std::fs::create_dir_all(&junction_root) {
log_note(
&format!(
"junction: failed to create {}: {err}",
junction_root.display()
),
log_dir,
);
return None;
}
let junction_path = junction_root.join(junction_name_for_path(requested_cwd));
if junction_path.exists() {
match std::fs::symlink_metadata(&junction_path) {
Ok(md) if (md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT) != 0 => {
log_note(
&format!("junction: reusing existing {}", junction_path.display()),
log_dir,
);
return Some(junction_path);
}
Ok(_) => {
log_note(
&format!(
"junction: existing path is not a reparse point, recreating {}",
junction_path.display()
),
log_dir,
);
}
Err(err) => {
log_note(
&format!(
"junction: failed to stat existing {}: {err}",
junction_path.display()
),
log_dir,
);
return None;
}
}
if let Err(err) = std::fs::remove_dir(&junction_path) {
log_note(
&format!(
"junction: failed to remove existing {}: {err}",
junction_path.display()
),
log_dir,
);
return None;
}
}
let link = junction_path.to_string_lossy().to_string();
let target = requested_cwd.to_string_lossy().to_string();
let link_quoted = format!("\"{link}\"");
let target_quoted = format!("\"{target}\"");
log_note(
&format!("junction: creating via cmd /c mklink /J {link_quoted} {target_quoted}"),
log_dir,
);
let output = match std::process::Command::new("cmd")
.raw_arg("/c")
.raw_arg("mklink")
.raw_arg("/J")
.raw_arg(&link_quoted)
.raw_arg(&target_quoted)
.output()
{
Ok(output) => output,
Err(err) => {
log_note(&format!("junction: mklink failed to run: {err}"), log_dir);
return None;
}
};
if output.status.success() && junction_path.exists() {
log_note(
&format!(
"junction: created {} -> {}",
junction_path.display(),
requested_cwd.display()
),
log_dir,
);
return Some(junction_path);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
log_note(
&format!(
"junction: mklink failed status={:?} stdout={} stderr={}",
output.status,
stdout.trim(),
stderr.trim()
),
log_dir,
);
None
}
pub(crate) fn effective_legacy_spawn_cwd(cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
let normalized_cwd = normalize_spawn_cwd(cwd);
if should_materialize_junction(
&normalized_cwd,
std::env::var("SystemDrive").ok().as_deref(),
) {
create_cwd_junction(&normalized_cwd, log_dir).unwrap_or_else(|| normalized_cwd.clone())
} else {
normalized_cwd
}
}
#[cfg(test)]
mod tests {
use super::effective_legacy_spawn_cwd;
use super::should_materialize_junction;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::path::PathBuf;
#[test]
fn skips_system_drive_workspaces() {
assert!(!should_materialize_junction(
Path::new(r"C:\repo"),
Some("C:"),
));
}
#[test]
fn uses_junction_for_non_system_drive_workspaces() {
assert!(should_materialize_junction(
Path::new(r"F:\repo"),
Some("C:"),
));
}
#[test]
fn skips_unc_paths() {
assert!(!should_materialize_junction(
Path::new(r"\\server\share\repo"),
Some("C:"),
));
}
#[test]
fn leaves_system_drive_paths_unchanged() {
let cwd = PathBuf::from(r"C:\repo");
assert_eq!(effective_legacy_spawn_cwd(&cwd, None), cwd);
}
}

View File

@@ -22,6 +22,7 @@ windows_modules!(
helper_materialization,
hide_users,
identity,
legacy_cwd,
logging,
path_normalization,
policy,
@@ -149,6 +150,10 @@ pub use logging::log_note;
#[cfg(target_os = "windows")]
pub use path_normalization::canonicalize_path;
#[cfg(target_os = "windows")]
pub use path_normalization::normalize_spawn_cwd;
#[cfg(target_os = "windows")]
pub use path_normalization::path_uses_unc_prefix;
#[cfg(target_os = "windows")]
pub use policy::SandboxPolicy;
#[cfg(target_os = "windows")]
pub use policy::parse_policy;
@@ -249,6 +254,7 @@ mod windows_impl {
use super::allow::compute_allow_paths;
use super::cap::load_or_create_cap_sids;
use super::cap::workspace_cap_sid_for_cwd;
use super::legacy_cwd::effective_legacy_spawn_cwd;
use super::logging::log_failure;
use super::logging::log_success;
use super::path_normalization::canonicalize_path;
@@ -457,11 +463,12 @@ mod windows_impl {
}
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair);
let effective_cwd = effective_legacy_spawn_cwd(cwd, logs_base_dir);
let spawn_res = unsafe {
create_process_as_user(
h_token,
&command,
cwd,
&effective_cwd,
&env_map,
logs_base_dir,
Some((in_r, out_w, err_w)),

View File

@@ -1,8 +1,35 @@
use crate::winutil::to_wide;
use std::path::Path;
use std::path::PathBuf;
#[cfg(target_os = "windows")]
use std::path::Prefix;
use windows_sys::Win32::Foundation::ERROR_MORE_DATA;
use windows_sys::Win32::Foundation::NO_ERROR;
use windows_sys::Win32::NetworkManagement::WNet::WNetGetConnectionW;
pub fn canonicalize_path(path: &Path) -> PathBuf {
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
let mapped_path = resolve_sandbox_path(path);
dunce::canonicalize(&mapped_path).unwrap_or(mapped_path)
}
pub fn normalize_spawn_cwd(path: &Path) -> PathBuf {
let simplified = dunce::simplified(path).to_path_buf();
if path_uses_unc_prefix(&simplified) {
return simplified;
}
let canonical = dunce::canonicalize(path).ok();
let canonical = canonical
.as_deref()
.map(dunce::simplified)
.map(Path::to_path_buf);
if let Some(canonical) = canonical
&& path_uses_unc_prefix(&canonical)
{
return canonical;
}
simplified
}
pub fn canonical_path_key(path: &Path) -> String {
@@ -12,17 +39,136 @@ pub fn canonical_path_key(path: &Path) -> String {
.to_ascii_lowercase()
}
pub fn path_uses_unc_prefix(path: &Path) -> bool {
#[cfg(target_os = "windows")]
{
matches!(
path.components().next(),
Some(std::path::Component::Prefix(prefix))
if matches!(prefix.kind(), Prefix::UNC(..) | Prefix::VerbatimUNC(..))
)
}
#[cfg(not(target_os = "windows"))]
{
let _ = path;
false
}
}
pub fn resolve_sandbox_path(path: &Path) -> PathBuf {
resolve_mapped_drive_path(path).unwrap_or_else(|| path.to_path_buf())
}
fn resolve_mapped_drive_path(path: &Path) -> Option<PathBuf> {
let (drive, suffix) = split_mapped_drive_path(path)?;
let drive_w = to_wide(drive);
let mut remote_len = 0u32;
let mut status =
unsafe { WNetGetConnectionW(drive_w.as_ptr(), std::ptr::null_mut(), &mut remote_len) };
if status != ERROR_MORE_DATA && status != NO_ERROR {
return None;
}
let mut remote_buf = vec![0u16; remote_len as usize + 1];
status =
unsafe { WNetGetConnectionW(drive_w.as_ptr(), remote_buf.as_mut_ptr(), &mut remote_len) };
if status != NO_ERROR {
return None;
}
let remote_end = remote_buf
.iter()
.position(|ch| *ch == 0)
.unwrap_or(remote_buf.len());
let remote = String::from_utf16_lossy(&remote_buf[..remote_end]);
if remote.is_empty() {
return None;
}
let mut resolved = PathBuf::from(remote);
if let Some(suffix) = suffix {
resolved.push(suffix);
}
Some(resolved)
}
fn split_mapped_drive_path(path: &Path) -> Option<(&str, Option<&str>)> {
let raw = path.to_str()?;
let bytes = raw.as_bytes();
if bytes.len() < 2 || !bytes[0].is_ascii_alphabetic() || bytes[1] != b':' {
return None;
}
if bytes.len() > 2 && bytes[2] != b'\\' && bytes[2] != b'/' {
return None;
}
let suffix = if bytes.len() > 3 {
Some(&raw[3..])
} else {
None
};
Some((&raw[..2], suffix))
}
#[cfg(test)]
mod tests {
use super::canonical_path_key;
use super::normalize_spawn_cwd;
use super::path_uses_unc_prefix;
use super::split_mapped_drive_path;
use pretty_assertions::assert_eq;
use std::path::Path;
use std::path::PathBuf;
#[test]
fn canonical_path_key_normalizes_case_and_separators() {
let windows_style = Path::new(r"C:\Users\Dev\Repo");
let slash_style = Path::new("c:/users/dev/repo");
assert_eq!(canonical_path_key(windows_style), canonical_path_key(slash_style));
assert_eq!(
canonical_path_key(windows_style),
canonical_path_key(slash_style)
);
}
#[cfg(target_os = "windows")]
#[test]
fn path_uses_unc_prefix_matches_standard_and_verbatim_unc_paths() {
assert!(path_uses_unc_prefix(Path::new(r"\\server\share\repo")));
assert!(path_uses_unc_prefix(Path::new(
r"\\?\UNC\server\share\repo"
)));
assert!(!path_uses_unc_prefix(Path::new(r"C:\repo")));
}
#[test]
fn normalize_spawn_cwd_preserves_regular_local_paths() {
let path = PathBuf::from(r"C:\repo");
assert_eq!(normalize_spawn_cwd(&path), path);
}
#[cfg(target_os = "windows")]
#[test]
fn normalize_spawn_cwd_simplifies_verbatim_unc_paths() {
let path = PathBuf::from(r"\\?\UNC\server\share\repo");
assert_eq!(
normalize_spawn_cwd(&path),
PathBuf::from(r"\\server\share\repo")
);
}
#[test]
fn split_mapped_drive_path_keeps_drive_relative_paths_unchanged() {
assert_eq!(split_mapped_drive_path(Path::new(r"L:repo")), None);
}
#[test]
fn split_mapped_drive_path_extracts_drive_root_suffix() {
assert_eq!(
split_mapped_drive_path(Path::new(r"L:\cs-web\context")),
Some(("L:", Some("cs-web\\context")))
);
}
}

View File

@@ -1,29 +1,30 @@
use crate::desktop::LaunchDesktop;
use crate::logging;
use crate::path_normalization::normalize_spawn_cwd;
use crate::proc_thread_attr::ProcThreadAttributeList;
use crate::winutil::argv_to_command_line;
use crate::winutil::format_last_error;
use crate::winutil::to_wide;
use anyhow::anyhow;
use anyhow::Result;
use anyhow::anyhow;
use std::collections::HashMap;
use std::ffi::c_void;
use std::path::Path;
use std::ptr;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::SetHandleInformation;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Foundation::SetHandleInformation;
use windows_sys::Win32::Storage::FileSystem::ReadFile;
use windows_sys::Win32::System::Console::GetStdHandle;
use windows_sys::Win32::System::Console::STD_ERROR_HANDLE;
use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
use windows_sys::Win32::System::Pipes::CreatePipe;
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT;
use windows_sys::Win32::System::Threading::CreateProcessAsUserW;
use windows_sys::Win32::System::Threading::EXTENDED_STARTUPINFO_PRESENT;
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES;
@@ -89,7 +90,8 @@ pub unsafe fn create_process_as_user(
let env_block = make_env_block(env_map);
let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
let mut pi: PROCESS_INFORMATION = std::mem::zeroed();
let cwd_wide = to_wide(cwd);
let spawn_cwd = normalize_spawn_cwd(cwd);
let cwd_wide = to_wide(&spawn_cwd);
let env_block_len = env_block.len();
match stdio {
Some((stdin_h, stdout_h, stderr_h)) => {
@@ -139,7 +141,7 @@ pub unsafe fn create_process_as_user(
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}",
err,
format_last_error(err),
cwd.display(),
spawn_cwd.display(),
cmdline_str,
env_block_len,
si.StartupInfo.dwFlags,
@@ -180,7 +182,7 @@ pub unsafe fn create_process_as_user(
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}",
err,
format_last_error(err),
cwd.display(),
spawn_cwd.display(),
cmdline_str,
env_block_len,
si.dwFlags,

View File

@@ -15,6 +15,8 @@ use crate::allow::compute_allow_paths;
use crate::helper_materialization::helper_bin_dir;
use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::path_normalization::canonicalize_path;
use crate::path_normalization::resolve_sandbox_path;
use crate::policy::SandboxPolicy;
use crate::setup_error::SetupErrorCode;
use crate::setup_error::SetupFailure;
@@ -177,7 +179,7 @@ fn run_setup_refresh_inner(
offline_username: OFFLINE_USERNAME.to_string(),
online_username: ONLINE_USERNAME.to_string(),
codex_home: request.codex_home.to_path_buf(),
command_cwd: request.command_cwd.to_path_buf(),
command_cwd: resolve_sandbox_path(request.command_cwd),
read_roots,
write_roots,
deny_write_paths,
@@ -322,7 +324,7 @@ fn canonical_existing(paths: &[PathBuf]) -> Vec<PathBuf> {
if !p.exists() {
return None;
}
Some(dunce::canonicalize(p).unwrap_or_else(|_| p.clone()))
Some(canonicalize_path(p))
})
.collect()
}
@@ -727,7 +729,7 @@ pub fn run_elevated_setup(
offline_username: OFFLINE_USERNAME.to_string(),
online_username: ONLINE_USERNAME.to_string(),
codex_home: request.codex_home.to_path_buf(),
command_cwd: request.command_cwd.to_path_buf(),
command_cwd: resolve_sandbox_path(request.command_cwd),
read_roots,
write_roots,
deny_write_paths,

View File

@@ -3,6 +3,7 @@ use super::windows_common::normalize_windows_tty_input;
use crate::acl::revoke_ace;
use crate::conpty::spawn_conpty_process_as_user;
use crate::desktop::LaunchDesktop;
use crate::legacy_cwd::effective_legacy_spawn_cwd;
use crate::logging::log_failure;
use crate::logging::log_success;
use crate::process::StderrMode;
@@ -66,11 +67,12 @@ fn spawn_legacy_process(
writer_rx: mpsc::Receiver<Vec<u8>>,
logs_base_dir: Option<&Path>,
) -> Result<LegacyProcessHandles> {
let effective_cwd = effective_legacy_spawn_cwd(cwd, logs_base_dir);
let (pi, output_join, writer_handle, hpc, desktop) = if tty {
let (pi, conpty) = spawn_conpty_process_as_user(
h_token,
command,
cwd,
&effective_cwd,
env_map,
use_private_desktop,
logs_base_dir,
@@ -87,7 +89,7 @@ fn spawn_legacy_process(
let pipe_handles = spawn_process_with_pipes(
h_token,
command,
cwd,
&effective_cwd,
env_map,
if stdin_open {
StdinMode::Open