Add Windows sandbox unified exec runtime support (#15578)

## Summary

This is the runtime/foundation half of the Windows sandbox unified-exec
work.

- add Windows sandbox `unified_exec` session support in
`windows-sandbox-rs` for both:
  - the legacy restricted-token backend
  - the elevated runner backend
- extend the PTY/process runtime so driver-backed sessions can support:
  - stdin streaming
  - stdout/stderr separation
  - exit propagation
  - PTY resize hooks
- add Windows sandbox runtime coverage in `codex-windows-sandbox` /
`codex-utils-pty`

This PR does **not** enable Windows sandbox `UnifiedExec` for product
callers yet because hooking this up to app-server comes in the next PR.

Windows sandbox advertising is intentionally kept aligned with `main`,
so sandboxed Windows callers still fall back to `ShellCommand`.

This PR isolates the runtime/session layer so it can be reviewed
independently from product-surface enablement.

---------

Co-authored-by: jif-oai <jif@openai.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
iceweasel-oai
2026-04-21 10:44:49 -07:00
committed by GitHub
co-authored by jif-oai Codex
parent 38ba876ea9
commit 8612714aa6
32 changed files with 2620 additions and 210 deletions
@@ -0,0 +1,118 @@
use super::windows_common::finish_driver_spawn;
use super::windows_common::make_runner_resizer;
use super::windows_common::start_runner_pipe_writer;
use super::windows_common::start_runner_stdin_writer;
use super::windows_common::start_runner_stdout_reader;
use crate::ipc_framed::EmptyPayload;
use crate::ipc_framed::FramedMessage;
use crate::ipc_framed::Message;
use crate::ipc_framed::SpawnRequest;
use crate::runner_client::spawn_runner_transport;
use crate::spawn_prep::prepare_elevated_spawn_context;
use anyhow::Result;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
use std::collections::HashMap;
use std::path::Path;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_elevated(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
mut env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
let elevated = prepare_elevated_spawn_context(
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
cwd,
&mut env_map,
&command,
)?;
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
env: env_map.clone(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
codex_home: elevated.common.sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids: elevated.cap_sids.clone(),
timeout_ms,
tty,
stdin_open,
use_private_desktop,
};
let codex_home = codex_home.to_path_buf();
let cwd = cwd.to_path_buf();
let sandbox_creds = elevated.sandbox_creds.clone();
let logs_base_dir = elevated.common.logs_base_dir.clone();
let transport = tokio::task::spawn_blocking(move || -> Result<_> {
let mut transport =
spawn_runner_transport(&codex_home, &cwd, &sandbox_creds, logs_base_dir.as_deref())?;
transport.send_spawn_request(spawn_request)?;
transport.read_spawn_ready()?;
Ok(transport)
})
.await
.map_err(|err| anyhow::anyhow!("runner handshake task failed: {err}"))??;
let (pipe_write, pipe_read) = transport.into_files();
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
let stderr_rx = if tty {
None
} else {
Some(broadcast::channel::<Vec<u8>>(256))
};
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
let outbound_tx = start_runner_pipe_writer(pipe_write);
let writer_handle = start_runner_stdin_writer(writer_rx, outbound_tx.clone(), tty, stdin_open);
let terminator = {
let outbound_tx = outbound_tx.clone();
Some(Box::new(move || {
let _ = outbound_tx.send(FramedMessage {
version: 1,
message: Message::Terminate {
payload: EmptyPayload::default(),
},
});
}) as Box<dyn FnMut() + Send + Sync>)
};
start_runner_stdout_reader(
pipe_read,
stdout_tx,
stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),
exit_tx,
);
Ok(finish_driver_spawn(
ProcessDriver {
writer_tx,
stdout_rx,
stderr_rx: stderr_rx.map(|(_tx, rx)| rx),
exit_rx,
terminator,
writer_handle: Some(writer_handle),
resizer: if tty {
Some(make_runner_resizer(outbound_tx))
} else {
None
},
},
stdin_open,
))
}
@@ -0,0 +1,439 @@
use super::windows_common::finish_driver_spawn;
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::logging::log_failure;
use crate::logging::log_success;
use crate::process::StderrMode;
use crate::process::StdinMode;
use crate::process::read_handle_loop;
use crate::process::spawn_process_with_pipes;
use crate::spawn_prep::LocalSid;
use crate::spawn_prep::allow_null_device_for_workspace_write;
use crate::spawn_prep::apply_legacy_session_acl_rules;
use crate::spawn_prep::prepare_legacy_session_security;
use crate::spawn_prep::prepare_legacy_spawn_context;
use anyhow::Result;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
use codex_utils_pty::TerminalSize;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::ptr;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Storage::FileSystem::WriteFile;
use windows_sys::Win32::System::Console::COORD;
use windows_sys::Win32::System::Console::ClosePseudoConsole;
use windows_sys::Win32::System::Console::ResizePseudoConsole;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
use windows_sys::Win32::System::Threading::INFINITE;
use windows_sys::Win32::System::Threading::PROCESS_INFORMATION;
use windows_sys::Win32::System::Threading::TerminateProcess;
use windows_sys::Win32::System::Threading::WaitForSingleObject;
const WAIT_TIMEOUT: u32 = 0x0000_0102;
struct LegacyProcessHandles {
process: PROCESS_INFORMATION,
output_join: std::thread::JoinHandle<()>,
writer_handle: tokio::task::JoinHandle<()>,
hpc: Option<HANDLE>,
token_handle: HANDLE,
desktop: Option<LaunchDesktop>,
}
#[allow(clippy::too_many_arguments)]
fn spawn_legacy_process(
h_token: HANDLE,
command: &[String],
cwd: &Path,
env_map: &HashMap<String, String>,
use_private_desktop: bool,
tty: bool,
stdin_open: bool,
stdout_tx: broadcast::Sender<Vec<u8>>,
stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
writer_rx: mpsc::Receiver<Vec<u8>>,
logs_base_dir: Option<&Path>,
) -> Result<LegacyProcessHandles> {
let (pi, output_join, writer_handle, hpc, desktop) = if tty {
let (pi, conpty) = spawn_conpty_process_as_user(
h_token,
command,
cwd,
env_map,
use_private_desktop,
logs_base_dir,
)?;
let (hpc, input_write, output_read, desktop) = conpty.into_raw();
let output_join = spawn_output_reader(output_read, stdout_tx);
let writer_handle = spawn_input_writer(
Some(input_write),
writer_rx,
/*normalize_newlines*/ true,
);
(pi, output_join, writer_handle, Some(hpc), desktop)
} else {
let pipe_handles = spawn_process_with_pipes(
h_token,
command,
cwd,
env_map,
if stdin_open {
StdinMode::Open
} else {
StdinMode::Closed
},
StderrMode::Separate,
use_private_desktop,
logs_base_dir,
)?;
let stdout_join = spawn_output_reader(pipe_handles.stdout_read, stdout_tx);
let Some(stderr_read) = pipe_handles.stderr_read else {
anyhow::bail!("separate stderr handle should be present");
};
let Some(stderr_tx) = stderr_tx else {
anyhow::bail!("separate stderr channel should be present");
};
let stderr_join = spawn_output_reader(stderr_read, stderr_tx);
let output_join = std::thread::spawn(move || {
let _ = stdout_join.join();
let _ = stderr_join.join();
});
let writer_handle = spawn_input_writer(
pipe_handles.stdin_write,
writer_rx,
/*normalize_newlines*/ false,
);
(
pipe_handles.process,
output_join,
writer_handle,
None,
Some(pipe_handles.desktop),
)
};
Ok(LegacyProcessHandles {
process: pi,
output_join,
writer_handle,
hpc,
token_handle: h_token,
desktop,
})
}
fn spawn_output_reader(
output_read: HANDLE,
output_tx: broadcast::Sender<Vec<u8>>,
) -> std::thread::JoinHandle<()> {
read_handle_loop(output_read, move |chunk| {
let _ = output_tx.send(chunk.to_vec());
})
}
fn spawn_input_writer(
input_write: Option<HANDLE>,
mut writer_rx: mpsc::Receiver<Vec<u8>>,
normalize_newlines: bool,
) -> tokio::task::JoinHandle<()> {
tokio::task::spawn_blocking(move || {
let mut previous_was_cr = false;
while let Some(bytes) = writer_rx.blocking_recv() {
let Some(handle) = input_write else {
continue;
};
let bytes = if normalize_newlines {
normalize_windows_tty_input(&bytes, &mut previous_was_cr)
} else {
bytes
};
if write_all_handle(handle, &bytes).is_err() {
break;
}
}
if let Some(handle) = input_write {
unsafe {
CloseHandle(handle);
}
}
})
}
fn write_all_handle(handle: HANDLE, mut bytes: &[u8]) -> Result<()> {
while !bytes.is_empty() {
let mut written = 0u32;
let ok = unsafe {
WriteFile(
handle,
bytes.as_ptr() as *const _,
bytes.len() as u32,
&mut written,
ptr::null_mut(),
)
};
if ok == 0 {
let err = unsafe { GetLastError() } as i32;
return Err(anyhow::anyhow!("WriteFile failed: {err}"));
}
if written == 0 {
anyhow::bail!("WriteFile returned success but wrote 0 bytes");
}
bytes = &bytes[written as usize..];
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn finalize_exit(
exit_tx: oneshot::Sender<i32>,
process_handle: Arc<StdMutex<Option<HANDLE>>>,
thread_handle: HANDLE,
output_join: std::thread::JoinHandle<()>,
guards: Vec<PathBuf>,
cap_sid: Option<String>,
logs_base_dir: Option<&Path>,
command: Vec<String>,
) {
let exit_code = {
let mut raw_exit = 1u32;
if let Ok(guard) = process_handle.lock()
&& let Some(handle) = guard.as_ref()
{
unsafe {
WaitForSingleObject(*handle, INFINITE);
GetExitCodeProcess(*handle, &mut raw_exit);
}
}
raw_exit as i32
};
let _ = output_join.join();
let _ = exit_tx.send(exit_code);
unsafe {
if thread_handle != 0 && thread_handle != INVALID_HANDLE_VALUE {
CloseHandle(thread_handle);
}
if let Ok(mut guard) = process_handle.lock()
&& let Some(handle) = guard.take()
{
CloseHandle(handle);
}
}
if exit_code == 0 {
log_success(&command, logs_base_dir);
} else {
log_failure(&command, &format!("exit code {exit_code}"), logs_base_dir);
}
if let Some(cap_sid) = cap_sid
&& let Ok(sid) = LocalSid::from_string(&cap_sid)
{
unsafe {
for path in guards {
revoke_ace(&path, sid.as_ptr());
}
}
}
}
fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize) -> Result<()> {
let guard = hpc
.lock()
.map_err(|_| anyhow::anyhow!("failed to lock ConPTY handle"))?;
let hpc = guard
.as_ref()
.copied()
.ok_or_else(|| anyhow::anyhow!("process is not attached to a PTY"))?;
let result = unsafe {
ResizePseudoConsole(
hpc,
COORD {
X: size.cols as i16,
Y: size.rows as i16,
},
)
};
if result == 0 {
Ok(())
} else {
Err(anyhow::anyhow!(
"failed to resize console: HRESULT {result}"
))
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_legacy(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
mut env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
let common = prepare_legacy_spawn_context(
policy_json_or_preset,
codex_home,
cwd,
&mut env_map,
&command,
/*inherit_path*/ false,
/*add_git_safe_directory*/ false,
)?;
if !common.policy.has_full_disk_read_access() {
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
}
let security = prepare_legacy_session_security(&common.policy, codex_home, cwd)?;
allow_null_device_for_workspace_write(common.is_workspace_write);
let persist_aces = common.is_workspace_write;
let guards = apply_legacy_session_acl_rules(
&common.policy,
sandbox_policy_cwd,
&common.current_dir,
&env_map,
&security.psid_generic,
security.psid_workspace.as_ref(),
persist_aces,
);
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
let (stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(256);
let stderr_rx = if tty {
None
} else {
Some(broadcast::channel::<Vec<u8>>(256))
};
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
let LegacyProcessHandles {
process: pi,
output_join,
writer_handle,
hpc,
token_handle,
desktop,
} = match spawn_legacy_process(
security.h_token,
&command,
cwd,
&env_map,
use_private_desktop,
tty,
stdin_open,
stdout_tx,
stderr_rx.as_ref().map(|(tx, _rx)| tx.clone()),
writer_rx,
common.logs_base_dir.as_deref(),
) {
Ok(handles) => handles,
Err(err) => {
unsafe {
if !persist_aces
&& !guards.is_empty()
&& let Ok(sid) = LocalSid::from_string(&security.cap_sid_str)
{
for path in &guards {
revoke_ace(path, sid.as_ptr());
}
}
CloseHandle(security.h_token);
}
return Err(err);
}
};
let hpc_handle = hpc.map(|hpc| Arc::new(StdMutex::new(Some(hpc))));
let process_handle = Arc::new(StdMutex::new(Some(pi.hProcess)));
let wait_handle = Arc::clone(&process_handle);
let command_for_wait = command.clone();
let guards_for_wait = if persist_aces { Vec::new() } else { guards };
let cap_sid_for_wait = if guards_for_wait.is_empty() {
None
} else {
Some(security.cap_sid_str)
};
let hpc_for_wait = hpc_handle.clone();
std::thread::spawn(move || {
let _desktop = desktop;
let timeout = timeout_ms.map(|ms| ms as u32).unwrap_or(INFINITE);
let wait_res = unsafe { WaitForSingleObject(pi.hProcess, timeout) };
if wait_res == WAIT_TIMEOUT {
unsafe {
if let Ok(guard) = wait_handle.lock()
&& let Some(handle) = guard.as_ref()
{
let _ = TerminateProcess(*handle, 1);
}
}
}
if let Some(hpc) = hpc_for_wait
&& let Ok(mut guard) = hpc.lock()
&& let Some(hpc) = guard.take()
{
unsafe {
ClosePseudoConsole(hpc);
}
}
unsafe {
if token_handle != 0 && token_handle != INVALID_HANDLE_VALUE {
CloseHandle(token_handle);
}
}
finalize_exit(
exit_tx,
wait_handle,
pi.hThread,
output_join,
guards_for_wait,
cap_sid_for_wait,
common.logs_base_dir.as_deref(),
command_for_wait,
);
});
let terminator = {
let process_handle = Arc::clone(&process_handle);
Some(Box::new(move || {
if let Ok(guard) = process_handle.lock()
&& let Some(handle) = guard.as_ref()
{
unsafe {
let _ = TerminateProcess(*handle, 1);
}
}
}) as Box<dyn FnMut() + Send + Sync>)
};
let driver = ProcessDriver {
writer_tx,
stdout_rx,
stderr_rx: stderr_rx.map(|(_tx, rx)| rx),
exit_rx,
terminator,
writer_handle: Some(writer_handle),
resizer: hpc_handle.map(|hpc| {
Box::new(move |size| resize_conpty_handle(&hpc, size))
as Box<dyn FnMut(TerminalSize) -> Result<()> + Send>
}),
};
Ok(finish_driver_spawn(driver, stdin_open))
}
@@ -0,0 +1,3 @@
pub(crate) mod elevated;
pub(crate) mod legacy;
pub(crate) mod windows_common;
@@ -0,0 +1,191 @@
use crate::ipc_framed::EmptyPayload;
use crate::ipc_framed::FramedMessage;
use crate::ipc_framed::Message;
use crate::ipc_framed::OutputStream;
use crate::ipc_framed::ResizePayload;
use crate::ipc_framed::StdinPayload;
use crate::ipc_framed::decode_bytes;
use crate::ipc_framed::encode_bytes;
use anyhow::Result;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
use codex_utils_pty::TerminalSize;
use codex_utils_pty::spawn_from_driver;
use std::fs::File;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
pub(crate) fn finish_driver_spawn(driver: ProcessDriver, stdin_open: bool) -> SpawnedProcess {
let spawned = spawn_from_driver(driver);
if !stdin_open {
spawned.session.close_stdin();
}
spawned
}
pub(crate) fn normalize_windows_tty_input(bytes: &[u8], previous_was_cr: &mut bool) -> Vec<u8> {
let mut normalized = Vec::with_capacity(bytes.len());
for &byte in bytes {
if byte == b'\n' {
if !*previous_was_cr {
normalized.push(b'\r');
}
normalized.push(b'\n');
*previous_was_cr = false;
} else {
normalized.push(byte);
*previous_was_cr = byte == b'\r';
}
}
normalized
}
pub(crate) fn start_runner_pipe_writer(
mut pipe_write: File,
) -> std::sync::mpsc::Sender<FramedMessage> {
let (outbound_tx, outbound_rx) = std::sync::mpsc::channel::<FramedMessage>();
tokio::task::spawn_blocking(move || {
while let Ok(msg) = outbound_rx.recv() {
if crate::ipc_framed::write_frame(&mut pipe_write, &msg).is_err() {
break;
}
}
});
outbound_tx
}
pub(crate) fn start_runner_stdin_writer(
mut writer_rx: mpsc::Receiver<Vec<u8>>,
outbound_tx: std::sync::mpsc::Sender<FramedMessage>,
normalize_newlines: bool,
stdin_open: bool,
) -> tokio::task::JoinHandle<()> {
tokio::task::spawn_blocking(move || {
let mut previous_was_cr = false;
while let Some(bytes) = writer_rx.blocking_recv() {
let bytes = if normalize_newlines {
normalize_windows_tty_input(&bytes, &mut previous_was_cr)
} else {
bytes
};
let msg = FramedMessage {
version: 1,
message: Message::Stdin {
payload: StdinPayload {
data_b64: encode_bytes(&bytes),
},
},
};
if outbound_tx.send(msg).is_err() {
break;
}
}
if stdin_open {
let _ = outbound_tx.send(FramedMessage {
version: 1,
message: Message::CloseStdin {
payload: EmptyPayload::default(),
},
});
}
})
}
pub(crate) fn start_runner_stdout_reader(
mut pipe_read: File,
stdout_tx: broadcast::Sender<Vec<u8>>,
stderr_tx: Option<broadcast::Sender<Vec<u8>>>,
exit_tx: oneshot::Sender<i32>,
) {
std::thread::spawn(move || {
loop {
let msg = match crate::ipc_framed::read_frame(&mut pipe_read) {
Ok(Some(v)) => v,
Ok(None) => {
send_runner_error(
"runner pipe closed before exit",
&stdout_tx,
stderr_tx.as_ref(),
);
let _ = exit_tx.send(-1);
break;
}
Err(err) => {
send_runner_error(
&format!("runner read failed: {err}"),
&stdout_tx,
stderr_tx.as_ref(),
);
let _ = exit_tx.send(-1);
break;
}
};
match msg.message {
Message::Output { payload } => {
if let Ok(data) = decode_bytes(&payload.data_b64) {
match payload.stream {
OutputStream::Stdout => {
let _ = stdout_tx.send(data);
}
OutputStream::Stderr => {
if let Some(stderr_tx) = stderr_tx.as_ref() {
let _ = stderr_tx.send(data);
} else {
let _ = stdout_tx.send(data);
}
}
}
}
}
Message::Exit { payload } => {
let _ = exit_tx.send(payload.exit_code);
break;
}
Message::Error { payload } => {
send_runner_error(&payload.message, &stdout_tx, stderr_tx.as_ref());
let _ = exit_tx.send(-1);
break;
}
Message::SpawnReady { .. }
| Message::Stdin { .. }
| Message::CloseStdin { .. }
| Message::Resize { .. }
| Message::SpawnRequest { .. }
| Message::Terminate { .. } => {}
}
}
});
}
pub(crate) fn make_runner_resizer(
outbound_tx: std::sync::mpsc::Sender<FramedMessage>,
) -> Box<dyn FnMut(TerminalSize) -> Result<()> + Send> {
Box::new(move |size: TerminalSize| {
outbound_tx
.send(FramedMessage {
version: 1,
message: Message::Resize {
payload: ResizePayload {
rows: size.rows,
cols: size.cols,
},
},
})
.map_err(|_| anyhow::anyhow!("runner resize pipe closed"))
})
}
fn send_runner_error(
message: &str,
stdout_tx: &broadcast::Sender<Vec<u8>>,
stderr_tx: Option<&broadcast::Sender<Vec<u8>>>,
) {
let formatted = format!("runner error: {message}\n").into_bytes();
if let Some(stderr_tx) = stderr_tx {
let _ = stderr_tx.send(formatted);
} else {
let _ = stdout_tx.send(formatted);
}
}
@@ -0,0 +1,83 @@
//! Unified exec session spawner for Windows sandboxing.
//!
//! This module is the thin orchestration layer for Windows unified-exec sessions.
//! Backend-specific mechanics live in sibling modules:
//! - `backends::legacy` adapts the direct restricted-token spawn path into a live session.
//! - `backends::elevated` adapts the elevated command-runner IPC path into the same session API.
//! - `backends::windows_common` holds the small shared Windows backend helpers
//! used by both.
mod backends;
use anyhow::Result;
use codex_utils_pty::SpawnedProcess;
use std::collections::HashMap;
use std::path::Path;
#[allow(clippy::too_many_arguments)]
pub async fn spawn_windows_sandbox_session_legacy(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
backends::legacy::spawn_windows_sandbox_session_legacy(
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
tty,
stdin_open,
use_private_desktop,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn spawn_windows_sandbox_session_elevated(
policy_json_or_preset: &str,
sandbox_policy_cwd: &Path,
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
tty: bool,
stdin_open: bool,
use_private_desktop: bool,
) -> Result<SpawnedProcess> {
backends::elevated::spawn_windows_sandbox_session_elevated(
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
tty,
stdin_open,
use_private_desktop,
)
.await
}
#[cfg(test)]
pub(crate) use backends::windows_common::finish_driver_spawn;
#[cfg(test)]
pub(crate) use backends::windows_common::make_runner_resizer;
#[cfg(test)]
pub(crate) use backends::windows_common::start_runner_pipe_writer;
#[cfg(test)]
pub(crate) use backends::windows_common::start_runner_stdin_writer;
#[cfg(test)]
mod tests;
@@ -0,0 +1,533 @@
#![cfg(target_os = "windows")]
use super::spawn_windows_sandbox_session_legacy;
use crate::ipc_framed::Message;
use crate::ipc_framed::decode_bytes;
use crate::ipc_framed::read_frame;
use crate::run_windows_sandbox_capture;
use codex_utils_pty::ProcessDriver;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::fs;
use std::fs::OpenOptions;
use std::io::Seek;
use std::io::SeekFrom;
use std::path::Path;
use std::path::PathBuf;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use tempfile::TempDir;
use tokio::runtime::Builder;
use tokio::sync::broadcast;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::time::timeout;
static TEST_HOME_COUNTER: AtomicU64 = AtomicU64::new(0);
fn current_thread_runtime() -> tokio::runtime::Runtime {
Builder::new_current_thread()
.enable_all()
.build()
.expect("build tokio runtime")
}
fn pwsh_path() -> Option<PathBuf> {
let program_files = std::env::var_os("ProgramFiles")?;
let path = PathBuf::from(program_files).join("PowerShell\\7\\pwsh.exe");
path.is_file().then_some(path)
}
fn sandbox_cwd() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("repo root")
.to_path_buf()
}
fn sandbox_home(name: &str) -> TempDir {
let id = TEST_HOME_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!("codex-windows-sandbox-{name}-{id}"));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(&path).expect("create sandbox home");
tempfile::TempDir::new_in(&path).expect("create sandbox home tempdir")
}
fn sandbox_log(codex_home: &Path) -> String {
let log_path = codex_home.join(".sandbox").join("sandbox.log");
fs::read_to_string(&log_path)
.unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display()))
}
fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec<Message> {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
let mut reader = OpenOptions::new()
.read(true)
.open(frames_path)
.expect("open frame file for read");
reader
.seek(SeekFrom::Start(0))
.expect("seek to start of frame file");
let mut frames = Vec::new();
loop {
match read_frame(&mut reader) {
Ok(Some(frame)) => frames.push(frame.message),
Ok(None) => break,
Err(_) => break,
}
}
if frames.len() >= expected_frames {
return frames;
}
assert!(
Instant::now() < deadline,
"timed out waiting for {expected_frames} frames, saw {}",
frames.len()
);
std::thread::sleep(Duration::from_millis(10));
}
}
async fn collect_stdout_and_exit(
spawned: codex_utils_pty::SpawnedProcess,
codex_home: &Path,
timeout_duration: Duration,
) -> (Vec<u8>, i32) {
let codex_utils_pty::SpawnedProcess {
session: _session,
mut stdout_rx,
stderr_rx: _stderr_rx,
exit_rx,
} = spawned;
let stdout_task = tokio::spawn(async move {
let mut stdout = Vec::new();
while let Some(chunk) = stdout_rx.recv().await {
stdout.extend(chunk);
}
stdout
});
let exit_code = timeout(timeout_duration, exit_rx)
.await
.unwrap_or_else(|_| panic!("timed out waiting for exit\n{}", sandbox_log(codex_home)))
.unwrap_or(-1);
let stdout = timeout(timeout_duration, stdout_task)
.await
.unwrap_or_else(|_| {
panic!(
"timed out waiting for stdout task\n{}",
sandbox_log(codex_home)
)
})
.expect("stdout task join");
(stdout, exit_code)
}
#[test]
fn legacy_non_tty_cmd_emits_output() {
let runtime = current_thread_runtime();
runtime.block_on(async move {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-non-tty-cmd");
println!("cmd codex_home={}", codex_home.path().display());
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
"/c".to_string(),
"echo LEGACY-NONTTY-CMD".to_string(),
],
cwd.as_path(),
HashMap::new(),
Some(5_000),
/*tty*/ false,
/*stdin_open*/ false,
/*use_private_desktop*/ true,
)
.await
.expect("spawn legacy non-tty cmd session");
println!("cmd spawn returned");
let (stdout, exit_code) =
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await;
println!("cmd collect returned exit_code={exit_code}");
let stdout = String::from_utf8_lossy(&stdout);
assert_eq!(exit_code, 0, "stdout={stdout:?}");
assert!(stdout.contains("LEGACY-NONTTY-CMD"), "stdout={stdout:?}");
});
}
#[test]
fn legacy_non_tty_powershell_emits_output() {
let Some(pwsh) = pwsh_path() else {
return;
};
let runtime = current_thread_runtime();
runtime.block_on(async move {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-non-tty-pwsh");
println!("pwsh codex_home={}", codex_home.path().display());
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
codex_home.path(),
vec![
pwsh.display().to_string(),
"-NoProfile".to_string(),
"-Command".to_string(),
"Write-Output LEGACY-NONTTY-DIRECT".to_string(),
],
cwd.as_path(),
HashMap::new(),
Some(5_000),
/*tty*/ false,
/*stdin_open*/ false,
/*use_private_desktop*/ true,
)
.await
.expect("spawn legacy non-tty powershell session");
println!("pwsh spawn returned");
let (stdout, exit_code) =
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await;
println!("pwsh collect returned exit_code={exit_code}");
let stdout = String::from_utf8_lossy(&stdout);
assert_eq!(exit_code, 0, "stdout={stdout:?}");
assert!(stdout.contains("LEGACY-NONTTY-DIRECT"), "stdout={stdout:?}");
});
}
#[test]
fn finish_driver_spawn_keeps_stdin_open_when_requested() {
let runtime = current_thread_runtime();
runtime.block_on(async move {
let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(1);
let (_stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(1);
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
drop(exit_tx);
let spawned = super::finish_driver_spawn(
ProcessDriver {
writer_tx,
stdout_rx,
stderr_rx: None,
exit_rx,
terminator: None,
writer_handle: None,
resizer: None,
},
/*stdin_open*/ true,
);
spawned
.session
.writer_sender()
.send(b"open".to_vec())
.await
.expect("stdin should stay open");
assert_eq!(writer_rx.recv().await, Some(b"open".to_vec()));
});
}
#[test]
fn finish_driver_spawn_closes_stdin_when_not_requested() {
let runtime = current_thread_runtime();
runtime.block_on(async move {
let (writer_tx, _writer_rx) = mpsc::channel::<Vec<u8>>(1);
let (_stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(1);
let (exit_tx, exit_rx) = oneshot::channel::<i32>();
drop(exit_tx);
let spawned = super::finish_driver_spawn(
ProcessDriver {
writer_tx,
stdout_rx,
stderr_rx: None,
exit_rx,
terminator: None,
writer_handle: None,
resizer: None,
},
/*stdin_open*/ false,
);
assert!(
spawned
.session
.writer_sender()
.send(b"closed".to_vec())
.await
.is_err(),
"stdin should be closed when streaming input is disabled"
);
});
}
#[test]
fn runner_stdin_writer_sends_close_stdin_after_input_eof() {
let runtime = current_thread_runtime();
runtime.block_on(async move {
let tempdir = TempDir::new().expect("create tempdir");
let frames_path = tempdir.path().join("runner-stdin-frames.bin");
let file = OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&frames_path)
.expect("create frame file");
let outbound_tx = super::start_runner_pipe_writer(file);
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(1);
let writer_handle = super::start_runner_stdin_writer(
writer_rx,
outbound_tx,
/*normalize_newlines*/ false,
/*stdin_open*/ true,
);
writer_tx
.send(b"hello".to_vec())
.await
.expect("send stdin bytes");
drop(writer_tx);
writer_handle.await.expect("join stdin writer");
let frames = wait_for_frame_count(&frames_path, 2);
match &frames[0] {
Message::Stdin { payload } => {
let bytes = decode_bytes(&payload.data_b64).expect("decode stdin payload");
assert_eq!(bytes, b"hello".to_vec());
}
other => panic!("expected stdin frame, got {other:?}"),
}
match &frames[1] {
Message::CloseStdin { .. } => {}
other => panic!("expected close-stdin frame, got {other:?}"),
}
});
}
#[test]
fn runner_resizer_sends_resize_frame() {
let runtime = current_thread_runtime();
runtime.block_on(async move {
let tempdir = TempDir::new().expect("create tempdir");
let frames_path = tempdir.path().join("runner-resize-frames.bin");
let file = OpenOptions::new()
.create(true)
.truncate(true)
.read(true)
.write(true)
.open(&frames_path)
.expect("create frame file");
let outbound_tx = super::start_runner_pipe_writer(file);
let mut resizer = super::make_runner_resizer(outbound_tx);
resizer(codex_utils_pty::TerminalSize {
rows: 45,
cols: 132,
})
.expect("send resize frame");
let frames = wait_for_frame_count(&frames_path, 1);
match &frames[0] {
Message::Resize { payload } => {
assert_eq!(payload.rows, 45);
assert_eq!(payload.cols, 132);
}
other => panic!("expected resize frame, got {other:?}"),
}
});
}
#[test]
fn legacy_capture_powershell_emits_output() {
let Some(pwsh) = pwsh_path() else {
return;
};
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-capture-pwsh");
println!("capture pwsh codex_home={}", codex_home.path().display());
let result = run_windows_sandbox_capture(
"workspace-write",
cwd.as_path(),
codex_home.path(),
vec![
pwsh.display().to_string(),
"-NoProfile".to_string(),
"-Command".to_string(),
"Write-Output LEGACY-CAPTURE-DIRECT".to_string(),
],
cwd.as_path(),
HashMap::new(),
Some(10_000),
/*use_private_desktop*/ true,
)
.expect("run legacy capture powershell");
println!("capture pwsh exit_code={}", result.exit_code);
println!("capture pwsh timed_out={}", result.timed_out);
let stdout = String::from_utf8_lossy(&result.stdout);
let stderr = String::from_utf8_lossy(&result.stderr);
println!("capture pwsh stderr={stderr:?}");
assert_eq!(result.exit_code, 0, "stdout={stdout:?} stderr={stderr:?}");
assert!(
stdout.contains("LEGACY-CAPTURE-DIRECT"),
"stdout={stdout:?}"
);
}
#[test]
fn legacy_tty_powershell_emits_output_and_accepts_input() {
let Some(pwsh) = pwsh_path() else {
return;
};
let runtime = current_thread_runtime();
runtime.block_on(async move {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-tty-pwsh");
println!("tty pwsh codex_home={}", codex_home.path().display());
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
codex_home.path(),
vec![
pwsh.display().to_string(),
"-NoLogo".to_string(),
"-NoProfile".to_string(),
"-NoExit".to_string(),
"-Command".to_string(),
"$PID; Write-Output ready".to_string(),
],
cwd.as_path(),
HashMap::new(),
Some(10_000),
/*tty*/ true,
/*stdin_open*/ true,
/*use_private_desktop*/ true,
)
.await
.expect("spawn legacy tty powershell session");
println!("tty pwsh spawn returned");
let writer = spawned.session.writer_sender();
writer
.send(b"Write-Output second\n".to_vec())
.await
.expect("send second command");
writer
.send(b"exit\n".to_vec())
.await
.expect("send exit command");
spawned.session.close_stdin();
let (stdout, exit_code) =
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;
let stdout = String::from_utf8_lossy(&stdout);
assert_eq!(exit_code, 0, "stdout={stdout:?}");
assert!(stdout.contains("ready"), "stdout={stdout:?}");
assert!(stdout.contains("second"), "stdout={stdout:?}");
});
}
#[test]
fn legacy_tty_cmd_emits_output_and_accepts_input() {
let runtime = current_thread_runtime();
runtime.block_on(async move {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-tty-cmd");
println!("tty cmd codex_home={}", codex_home.path().display());
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
"/K".to_string(),
"echo ready".to_string(),
],
cwd.as_path(),
HashMap::new(),
Some(10_000),
/*tty*/ true,
/*stdin_open*/ true,
/*use_private_desktop*/ true,
)
.await
.expect("spawn legacy tty cmd session");
println!("tty cmd spawn returned");
let writer = spawned.session.writer_sender();
writer
.send(b"echo second\n".to_vec())
.await
.expect("send second command");
writer
.send(b"exit\n".to_vec())
.await
.expect("send exit command");
spawned.session.close_stdin();
let (stdout, exit_code) =
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;
let stdout = String::from_utf8_lossy(&stdout);
assert_eq!(exit_code, 0, "stdout={stdout:?}");
assert!(stdout.contains("ready"), "stdout={stdout:?}");
assert!(stdout.contains("second"), "stdout={stdout:?}");
});
}
#[test]
fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {
let runtime = current_thread_runtime();
runtime.block_on(async move {
let cwd = sandbox_cwd();
let codex_home = sandbox_home("legacy-tty-cmd-default-desktop");
println!(
"tty cmd default desktop codex_home={}",
codex_home.path().display()
);
let spawned = spawn_windows_sandbox_session_legacy(
"workspace-write",
cwd.as_path(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
"/K".to_string(),
"echo ready".to_string(),
],
cwd.as_path(),
HashMap::new(),
Some(10_000),
/*tty*/ true,
/*stdin_open*/ true,
/*use_private_desktop*/ false,
)
.await
.expect("spawn legacy tty cmd session");
println!("tty cmd default desktop spawn returned");
let writer = spawned.session.writer_sender();
writer
.send(b"echo second\n".to_vec())
.await
.expect("send second command");
writer
.send(b"exit\n".to_vec())
.await
.expect("send exit command");
spawned.session.close_stdin();
let (stdout, exit_code) =
collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;
let stdout = String::from_utf8_lossy(&stdout);
assert_eq!(exit_code, 0, "stdout={stdout:?}");
assert!(stdout.contains("ready"), "stdout={stdout:?}");
assert!(stdout.contains("second"), "stdout={stdout:?}");
});
}