From b295c376ddd243ff5fcd7580edb61fcae27094b6 Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Thu, 11 Jun 2026 12:55:24 -0700 Subject: [PATCH] Add Windows sandbox wrapper entrypoint --- codex-rs/Cargo.lock | 1 + codex-rs/arg0/Cargo.toml | 3 + codex-rs/arg0/src/lib.rs | 6 + .../src/helper_materialization.rs | 19 +- codex-rs/windows-sandbox-rs/src/lib.rs | 17 +- codex-rs/windows-sandbox-rs/src/wrapper.rs | 251 ++++++++++++++++++ 6 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 codex-rs/windows-sandbox-rs/src/wrapper.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b6cb9de0f2..69318a7ce8 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2202,6 +2202,7 @@ dependencies = [ "codex-shell-escalation", "codex-utils-absolute-path", "codex-utils-home-dir", + "codex-windows-sandbox", "dotenvy", "pretty_assertions", "tempfile", diff --git a/codex-rs/arg0/Cargo.toml b/codex-rs/arg0/Cargo.toml index 55526b4d06..bb45db4552 100644 --- a/codex-rs/arg0/Cargo.toml +++ b/codex-rs/arg0/Cargo.toml @@ -26,5 +26,8 @@ dotenvy = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } +[target.'cfg(windows)'.dependencies] +codex-windows-sandbox = { workspace = true } + [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index ba254d57ab..e561bd762e 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -9,6 +9,8 @@ use codex_exec_server::CODEX_FS_HELPER_ARG1; use codex_install_context::InstallContext; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_home_dir::find_codex_home; +#[cfg(target_os = "windows")] +use codex_windows_sandbox::CODEX_WINDOWS_SANDBOX_ARG1; #[cfg(unix)] use std::os::unix::fs::symlink; use tempfile::TempDir; @@ -96,6 +98,10 @@ pub fn arg0_dispatch() -> Option { } let argv1 = args.next().unwrap_or_default(); + #[cfg(target_os = "windows")] + if argv1 == CODEX_WINDOWS_SANDBOX_ARG1 { + codex_windows_sandbox::run_windows_sandbox_wrapper_main(); + } if argv1 == CODEX_FS_HELPER_ARG1 { codex_exec_server::run_fs_helper_main(); } diff --git a/codex-rs/windows-sandbox-rs/src/helper_materialization.rs b/codex-rs/windows-sandbox-rs/src/helper_materialization.rs index e5b202dede..ffc300e7ca 100644 --- a/codex-rs/windows-sandbox-rs/src/helper_materialization.rs +++ b/codex-rs/windows-sandbox-rs/src/helper_materialization.rs @@ -96,22 +96,33 @@ pub fn resolve_current_exe_for_launch(codex_home: &Path, fallback_executable: &s Ok(path) => path, Err(_) => return PathBuf::from(fallback_executable), }; + resolve_exe_for_launch(&source, codex_home) +} + +/// Returns the executable path that should be launched from a Windows sandbox. +/// +/// Windows sandbox launch setup may grant access to helper binaries under +/// CODEX_HOME/.sandbox-bin. Callers that already know the intended helper +/// binary should pass it here instead of relying on `current_exe()`, which can +/// name a host process rather than the Codex helper in embedded exec-server +/// scenarios. +pub fn resolve_exe_for_launch(source: &Path, codex_home: &Path) -> PathBuf { let Some(file_name) = source.file_name() else { - return source; + return source.to_path_buf(); }; let destination = helper_bin_dir(codex_home).join(file_name); - match copy_from_source_if_needed(&source, &destination) { + match copy_from_source_if_needed(source, &destination) { Ok(_) => destination, Err(err) => { let sandbox_log_dir = crate::sandbox_dir(codex_home); log_note( &format!( - "helper copy failed for current executable: {err:#}; falling back to legacy path {}", + "helper copy failed for executable: {err:#}; falling back to legacy path {}", source.display() ), Some(&sandbox_log_dir), ); - source + source.to_path_buf() } } } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 91d85e3fbc..0786f27786 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -78,6 +78,8 @@ mod wfp_setup; mod winutil; #[cfg(target_os = "windows")] mod workspace_acl; +#[cfg(target_os = "windows")] +mod wrapper; mod deny_read_resolver; @@ -178,6 +180,8 @@ pub use filesystem_overrides::unsupported_windows_restricted_token_sandbox_reaso #[cfg(target_os = "windows")] pub use helper_materialization::resolve_current_exe_for_launch; #[cfg(target_os = "windows")] +pub use helper_materialization::resolve_exe_for_launch; +#[cfg(target_os = "windows")] pub use hide_users::hide_current_user_profile_dir; #[cfg(target_os = "windows")] pub use hide_users::hide_newly_created_users; @@ -320,6 +324,16 @@ pub use winutil::string_from_sid_bytes; pub use winutil::to_wide; #[cfg(target_os = "windows")] pub use workspace_acl::is_command_cwd_root; +#[cfg(target_os = "windows")] +pub use wrapper::CODEX_WINDOWS_SANDBOX_ARG1; +#[cfg(target_os = "windows")] +pub use wrapper::WindowsSandboxWrapperRequest; +#[cfg(target_os = "windows")] +pub use wrapper::create_windows_sandbox_command_args_for_request_file; +#[cfg(target_os = "windows")] +pub use wrapper::create_windows_sandbox_wrapper_request_for_permission_profile; +#[cfg(target_os = "windows")] +pub use wrapper::run_windows_sandbox_wrapper_main; #[cfg(not(target_os = "windows"))] pub use stub::CaptureResult; @@ -477,13 +491,14 @@ mod windows_impl { codex_home: &Path, command: Vec, cwd: &Path, - mut env_map: HashMap, + env_map: HashMap, timeout_ms: Option, cancellation: Option, additional_deny_read_paths: &[AbsolutePathBuf], additional_deny_write_paths: &[AbsolutePathBuf], use_private_desktop: bool, ) -> Result { + let mut env_map = env_map; let additional_deny_read_paths = additional_deny_read_paths .iter() .map(AbsolutePathBuf::to_path_buf) diff --git a/codex-rs/windows-sandbox-rs/src/wrapper.rs b/codex-rs/windows-sandbox-rs/src/wrapper.rs new file mode 100644 index 0000000000..df71f81022 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/wrapper.rs @@ -0,0 +1,251 @@ +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; +use anyhow::bail; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; +use serde::Deserialize; +use serde::Serialize; + +pub const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox"; + +const REQUEST_FILE_FLAG: &str = "--request-file"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WindowsSandboxWrapperRequest { + pub codex_home: PathBuf, + pub command_cwd: AbsolutePathBuf, + pub permission_profile: PermissionProfile, + pub windows_sandbox_level: WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, + pub command: Vec, +} + +pub fn create_windows_sandbox_wrapper_request_for_permission_profile( + command: Vec, + command_cwd: AbsolutePathBuf, + permission_profile: PermissionProfile, + windows_sandbox_level: WindowsSandboxLevel, + windows_sandbox_private_desktop: bool, + codex_home: PathBuf, +) -> WindowsSandboxWrapperRequest { + WindowsSandboxWrapperRequest { + codex_home, + command_cwd, + permission_profile, + windows_sandbox_level, + windows_sandbox_private_desktop, + command, + } +} + +pub fn create_windows_sandbox_command_args_for_request_file(request_file: &Path) -> Vec { + vec![ + CODEX_WINDOWS_SANDBOX_ARG1.to_string(), + REQUEST_FILE_FLAG.to_string(), + request_file.to_string_lossy().into_owned(), + ] +} + +pub fn run_windows_sandbox_wrapper_main() -> ! { + let args = std::env::args().skip(2).collect::>(); + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => runtime, + Err(err) => { + eprintln!("windows sandbox failed to build runtime: {err}"); + std::process::exit(1); + } + }; + let exit_code = match runtime.block_on(run_windows_sandbox_wrapper_args(args)) { + Ok(exit_code) => exit_code, + Err(err) => { + eprintln!("windows sandbox failed: {err:#}"); + 1 + } + }; + std::process::exit(exit_code); +} + +async fn run_windows_sandbox_wrapper_args(args: Vec) -> Result { + let request_file = parse_windows_sandbox_wrapper_args(args)?; + let request_json = std::fs::read(&request_file).with_context(|| { + format!( + "failed to read windows sandbox wrapper request {}", + request_file.display() + ) + })?; + let _ = std::fs::remove_file(&request_file); + let request: WindowsSandboxWrapperRequest = serde_json::from_slice(&request_json) + .context("failed to parse windows sandbox wrapper request")?; + run_windows_sandbox_wrapper_request(request).await +} + +async fn run_windows_sandbox_wrapper_request(request: WindowsSandboxWrapperRequest) -> Result { + if !request.codex_home.is_absolute() { + bail!( + "windows sandbox wrapper codex_home must be absolute: {}", + request.codex_home.display() + ); + } + if request.command.is_empty() { + bail!("missing sandboxed command in windows sandbox wrapper request"); + } + + let env = std::env::vars().collect::>(); + let workspace_roots = vec![request.command_cwd.clone()]; + let spawned = match request.windows_sandbox_level { + WindowsSandboxLevel::Elevated => { + let overrides = crate::resolve_windows_elevated_filesystem_overrides( + /*windows_sandbox_active*/ true, + &request.permission_profile, + &request.command_cwd, + /*use_windows_elevated_backend*/ true, + ) + .map_err(anyhow::Error::msg)? + .unwrap_or_default(); + crate::unified_exec::spawn_windows_sandbox_session_elevated_for_permission_profile( + &request.permission_profile, + workspace_roots.as_slice(), + request.codex_home.as_path(), + request.command, + request.command_cwd.as_path(), + env, + /*timeout_ms*/ None, + overrides.read_roots_override.as_deref(), + overrides.read_roots_include_platform_defaults, + overrides.write_roots_override.as_deref(), + &overrides.additional_deny_read_paths, + &overrides.additional_deny_write_paths, + /*tty*/ false, + /*stdin_open*/ true, + request.windows_sandbox_private_desktop, + ) + .await + } + WindowsSandboxLevel::RestrictedToken | WindowsSandboxLevel::Disabled => { + let overrides = crate::resolve_windows_restricted_token_filesystem_overrides( + /*windows_sandbox_active*/ true, + &request.permission_profile, + &request.command_cwd, + request.windows_sandbox_level, + ) + .map_err(anyhow::Error::msg)? + .unwrap_or_default(); + crate::unified_exec::spawn_windows_sandbox_session_legacy( + &request.permission_profile, + workspace_roots.as_slice(), + request.codex_home.as_path(), + request.command, + request.command_cwd.as_path(), + env, + /*timeout_ms*/ None, + &overrides.additional_deny_read_paths, + &overrides.additional_deny_write_paths, + /*tty*/ false, + /*stdin_open*/ true, + request.windows_sandbox_private_desktop, + ) + .await + } + }?; + + Ok(crate::stdio_bridge::forward_sandbox_session_stdio(spawned).await) +} + +fn parse_windows_sandbox_wrapper_args(args: Vec) -> Result { + let mut args = args.into_iter(); + let Some(flag) = args.next() else { + bail!("missing required argument {REQUEST_FILE_FLAG}"); + }; + if flag != REQUEST_FILE_FLAG { + bail!("expected {REQUEST_FILE_FLAG}, got {flag}"); + } + let request_file = PathBuf::from(next_flag_value(&mut args, REQUEST_FILE_FLAG)?); + if !request_file.is_absolute() { + bail!( + "{REQUEST_FILE_FLAG} must be absolute: {}", + request_file.display() + ); + } + if let Some(arg) = args.next() { + bail!("unexpected windows sandbox wrapper argument: {arg}"); + } + Ok(request_file) +} + +fn next_flag_value(args: &mut impl Iterator, flag: &str) -> Result { + args.next() + .ok_or_else(|| anyhow!("missing value for {flag}")) +} + +#[cfg(test)] +mod tests { + use codex_protocol::permissions::NetworkSandboxPolicy; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn windows_wrapper_args_reference_request_file_only() -> Result<()> { + let args = create_windows_sandbox_command_args_for_request_file(Path::new( + r"C:\codex-home\.sandbox\request.json", + )); + + assert_eq!( + args, + vec![ + CODEX_WINDOWS_SANDBOX_ARG1.to_string(), + REQUEST_FILE_FLAG.to_string(), + r"C:\codex-home\.sandbox\request.json".to_string(), + ] + ); + assert_eq!( + parse_windows_sandbox_wrapper_args(args.into_iter().skip(1).collect())?, + PathBuf::from(r"C:\codex-home\.sandbox\request.json") + ); + Ok(()) + } + + #[test] + fn windows_wrapper_request_round_trips() -> Result<()> { + let permission_profile = PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }; + let request = create_windows_sandbox_wrapper_request_for_permission_profile( + vec![ + "helper.exe".to_string(), + "--codex-run-as-fs-helper".to_string(), + ], + AbsolutePathBuf::from_absolute_path(Path::new(r"C:\work"))?, + permission_profile.clone(), + WindowsSandboxLevel::RestrictedToken, + /*windows_sandbox_private_desktop*/ true, + PathBuf::from(r"C:\codex-home"), + ); + let request: WindowsSandboxWrapperRequest = + serde_json::from_slice(&serde_json::to_vec(&request)?)?; + + assert_eq!( + request.command, + vec![ + "helper.exe".to_string(), + "--codex-run-as-fs-helper".to_string() + ] + ); + assert_eq!(request.permission_profile, permission_profile); + assert_eq!( + request.windows_sandbox_level, + WindowsSandboxLevel::RestrictedToken + ); + assert_eq!(request.windows_sandbox_private_desktop, true); + Ok(()) + } +}