windows-sandbox: pass workspace roots to runner (#24108)

## Why

#23813 switches the Windows sandbox runner path to `PermissionProfile`,
but it still left one runtime anchor for resolving symbolic
`:workspace_roots` entries. That is not enough once a turn has multiple
effective workspace roots: exact entries and deny globs under
`:workspace_roots` need to be materialized for every runtime root before
the command runner chooses token mode or builds ACL plans.

## What Changed

- Replaces the Windows runner/setup `permission_profile_cwd` plumbing
with `workspace_roots: Vec<AbsolutePathBuf>`.
- Resolves Windows-local `PermissionProfile` data with
`materialize_project_roots_with_workspace_roots(...)` instead of the
single-cwd helper.
- Threads `Config::effective_workspace_roots()` through core execution,
unified exec, TUI setup/read-grant flows, app-server setup, app-server
`command/exec`, and `debug sandbox` on Windows.
- Preserves those workspace roots through the zsh-fork escalation
executor instead of rebuilding them from `sandbox_policy_cwd`.
- Makes `ExecRequest::new(...)` and the remaining
`build_exec_request(...)` helper path take
`windows_sandbox_workspace_roots` explicitly so new call sites cannot
silently fall back to `vec![cwd]`.
- Clarifies the `debug sandbox` non-Windows comment: remaining
cwd-dependent resolution still uses `sandbox_policy_cwd`, while
`:workspace_roots` entries are already materialized from config roots.
- Updates elevated runner IPC `SpawnRequest` to send `workspace_roots`
and bumps the framed IPC protocol version to `3` for the payload shape
change.
- Adds Windows-local resolver coverage for expanding exact and glob
`:workspace_roots` entries across multiple roots, plus core helper
coverage proving explicit roots are preserved.

## Verification

- `cargo check -p codex-windows-sandbox -p codex-core -p codex-tui -p
codex-cli -p codex-app-server`
- `cargo test -p codex-windows-sandbox`
- `cargo test -p codex-core windows_sandbox`
- `cargo test -p codex-core unix_escalation`
- `cargo test -p codex-app-server windows_sandbox`
- `cargo test -p codex-tui windows_sandbox`
- `cargo test -p codex-cli debug_sandbox`
- `just test -p codex-core unified_exec`
- `just test -p codex-core
build_exec_request_preserves_windows_workspace_roots`
- `env -u CODEX_NETWORK_PROXY_ACTIVE -u
CODEX_NETWORK_ALLOW_LOCAL_BINDING just test -p codex-app-server --lib
command_exec`
- `just test -p codex-windows-sandbox`
- `just test -p codex-exec sandbox`
- `just fix -p codex-core -p codex-app-server -p codex-windows-sandbox`

A local macOS cross-check with `cargo check --target
x86_64-pc-windows-msvc ...` did not reach crate Rust code because native
dependencies require Windows SDK headers (`windows.h` / `assert.h`) in
this environment; Windows CI remains the real target validation.

Two local targeted filters compile but do not run assertions on macOS:
`env -u CODEX_NETWORK_PROXY_ACTIVE -u CODEX_NETWORK_ALLOW_LOCAL_BINDING
just test -p codex-app-server --lib command_exec_processor` matched zero
tests, and `just test -p codex-linux-sandbox landlock` matched zero
tests because the landlock suite is Linux-only.
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-05-28 15:26:55 -07:00
1 parent e7dda8070e
commit 986c60467b
40 files changed
+517 -213

No files matched your search

@@ -26,7 +26,7 @@ use tokio::sync::oneshot;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
permission_profile: &PermissionProfile,
permission_profile_cwd: &Path,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -49,10 +49,11 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>();
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
permission_profile,
permission_profile_cwd,
)?;
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
let elevated = prepare_elevated_spawn_context_for_permissions(
permissions,
codex_home,
@@ -71,7 +72,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
cwd: cwd.to_path_buf(),
env: env_map.clone(),
permission_profile: permission_profile.clone(),
permission_profile_cwd: permission_profile_cwd.to_path_buf(),
workspace_roots: workspace_roots.to_vec(),
codex_home: elevated.sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids: elevated.cap_sids.clone(),
@@ -271,7 +271,7 @@ fn resize_conpty_handle(hpc: &Arc<StdMutex<Option<HANDLE>>>, size: TerminalSize)
#[allow(clippy::too_many_arguments)]
pub(crate) async fn spawn_windows_sandbox_session_legacy(
permission_profile: &PermissionProfile,
permission_profile_cwd: &Path,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -285,7 +285,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
) -> Result<SpawnedProcess> {
let common = prepare_legacy_spawn_context(
permission_profile,
permission_profile_cwd,
workspace_roots,
codex_home,
cwd,
&mut env_map,
@@ -20,7 +20,7 @@ use std::path::PathBuf;
#[allow(clippy::too_many_arguments)]
pub async fn spawn_windows_sandbox_session_legacy(
permission_profile: &PermissionProfile,
permission_profile_cwd: &Path,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -34,7 +34,7 @@ pub async fn spawn_windows_sandbox_session_legacy(
) -> Result<SpawnedProcess> {
backends::legacy::spawn_windows_sandbox_session_legacy(
permission_profile,
permission_profile_cwd,
workspace_roots,
codex_home,
command,
cwd,
@@ -52,7 +52,7 @@ pub async fn spawn_windows_sandbox_session_legacy(
#[allow(clippy::too_many_arguments)]
pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
permission_profile: &PermissionProfile,
permission_profile_cwd: &Path,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
@@ -69,7 +69,7 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile(
) -> Result<SpawnedProcess> {
backends::elevated::spawn_windows_sandbox_session_elevated_for_permission_profile(
permission_profile,
permission_profile_cwd,
workspace_roots,
codex_home,
command,
cwd,
@@ -79,6 +79,10 @@ fn sandbox_log(codex_home: &Path) -> String {
.unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display()))
}
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec<Message> {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
@@ -156,7 +160,7 @@ fn legacy_non_tty_cmd_emits_output() {
let permission_profile = PermissionProfile::workspace_write();
let spawned = spawn_windows_sandbox_session_legacy(
&permission_profile,
cwd.as_path(),
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
@@ -197,7 +201,7 @@ fn legacy_non_tty_cmd_rejects_deny_read_overrides() {
let permission_profile = PermissionProfile::workspace_write();
let err = spawn_windows_sandbox_session_legacy(
&permission_profile,
cwd.as_path(),
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
@@ -237,7 +241,7 @@ fn legacy_non_tty_powershell_emits_output() {
let permission_profile = PermissionProfile::workspace_write();
let spawned = spawn_windows_sandbox_session_legacy(
&permission_profile,
cwd.as_path(),
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
pwsh.display().to_string(),
@@ -423,7 +427,7 @@ fn legacy_capture_powershell_emits_output() {
let permission_profile = PermissionProfile::workspace_write();
let result = run_windows_sandbox_capture(
&permission_profile,
cwd.as_path(),
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
pwsh.display().to_string(),
@@ -515,7 +519,7 @@ fn legacy_tty_powershell_emits_output_and_accepts_input() {
let permission_profile = PermissionProfile::workspace_write();
let spawned = spawn_windows_sandbox_session_legacy(
&permission_profile,
cwd.as_path(),
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
pwsh.display().to_string(),
@@ -569,7 +573,7 @@ fn legacy_tty_cmd_emits_output_and_accepts_input() {
let permission_profile = PermissionProfile::workspace_write();
let spawned = spawn_windows_sandbox_session_legacy(
&permission_profile,
cwd.as_path(),
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),
@@ -623,7 +627,7 @@ fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {
let permission_profile = PermissionProfile::workspace_write();
let spawned = spawn_windows_sandbox_session_legacy(
&permission_profile,
cwd.as_path(),
workspace_roots_for(cwd.as_path()).as_slice(),
codex_home.path(),
vec![
"C:\\Windows\\System32\\cmd.exe".to_string(),