diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 547a05774..69fa5df33 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2183,6 +2183,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 55526b4d0..bb45db455 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 ba254d57a..2102eaf87 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; @@ -99,6 +101,10 @@ pub fn arg0_dispatch() -> Option { if argv1 == CODEX_FS_HELPER_ARG1 { codex_exec_server::run_fs_helper_main(); } + #[cfg(target_os = "windows")] + if argv1 == CODEX_WINDOWS_SANDBOX_ARG1 { + codex_windows_sandbox::run_windows_sandbox_wrapper_main(); + } if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 { let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned)); let exit_code = match patch_arg { diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 909e6e8a7..55a22d015 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -382,6 +382,7 @@ async fn run_command_under_windows_session( cwd: cwd.as_path(), env_map: env, windows_sandbox_level: WindowsSandboxLevel::from_config(config), + proxy_enforced: false, timeout_ms: None, read_roots_override: None, read_roots_include_platform_defaults: false, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index a44cc5253..755590c86 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -935,6 +935,7 @@ impl UnifiedExecProcessManager { request.command.clone(), request.cwd.as_path(), request.env.clone(), + request.network.is_some(), None, elevated_read_roots_override.as_deref(), elevated_read_roots_include_platform_defaults, diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 346f60488..4c1858430 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -109,6 +109,8 @@ mod stdio_bridge; #[cfg(target_os = "windows")] mod unified_exec; +#[cfg(target_os = "windows")] +mod wrapper; #[cfg(target_os = "windows")] pub(crate) use elevated::ipc_framed; @@ -318,6 +320,12 @@ 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::create_windows_sandbox_command_args_for_permission_profile; +#[cfg(target_os = "windows")] +pub use wrapper::run_windows_sandbox_wrapper_main; #[cfg(not(target_os = "windows"))] pub use stub::CaptureResult; diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs index 7bdd59bf5..9668edb1a 100644 --- a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -356,6 +356,7 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions( write_roots_override: Option<&[PathBuf]>, deny_read_paths_override: &[PathBuf], deny_write_paths_override: &[PathBuf], + proxy_enforced: bool, ) -> Result { normalize_null_device_env(env_map); ensure_non_interactive_pager(env_map); @@ -410,7 +411,7 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions( } else { deny_write_paths_override }, - /*proxy_enforced*/ false, + proxy_enforced, )?; let caps = load_or_create_cap_sids(codex_home)?; let (psid_to_use, cap_sids) = if uses_write_capabilities { diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs index e1a7a69fb..2e7437ad3 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs @@ -55,6 +55,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil command: Vec, cwd: &Path, mut env_map: HashMap, + proxy_enforced: bool, timeout_ms: Option, read_roots_override: Option<&[PathBuf]>, read_roots_include_platform_defaults: bool, @@ -89,6 +90,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil write_roots_override, &deny_read_paths_override, &deny_write_paths_override, + proxy_enforced, )?; let spawn_request = SpawnRequest { diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs index 80e5216b0..02792d6cb 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs @@ -30,6 +30,7 @@ pub struct WindowsSandboxSessionRequest<'a> { pub cwd: &'a Path, pub env_map: HashMap, pub windows_sandbox_level: WindowsSandboxLevel, + pub proxy_enforced: bool, pub timeout_ms: Option, pub read_roots_override: Option<&'a [PathBuf]>, pub read_roots_include_platform_defaults: bool, @@ -44,44 +45,44 @@ pub struct WindowsSandboxSessionRequest<'a> { pub async fn spawn_windows_sandbox_session_for_level( request: WindowsSandboxSessionRequest<'_>, ) -> Result { - match request.windows_sandbox_level { - WindowsSandboxLevel::Elevated => { - spawn_windows_sandbox_session_elevated_for_permission_profile( - request.permission_profile, - request.workspace_roots, - request.codex_home, - request.command, - request.cwd, - request.env_map, - request.timeout_ms, - request.read_roots_override, - request.read_roots_include_platform_defaults, - request.write_roots_override, - request.deny_read_paths_override, - request.deny_write_paths_override, - request.tty, - request.stdin_open, - request.use_private_desktop, - ) - .await - } - WindowsSandboxLevel::RestrictedToken | WindowsSandboxLevel::Disabled => { - spawn_windows_sandbox_session_legacy( - request.permission_profile, - request.workspace_roots, - request.codex_home, - request.command, - request.cwd, - request.env_map, - request.timeout_ms, - request.deny_read_paths_override, - request.deny_write_paths_override, - request.tty, - request.stdin_open, - request.use_private_desktop, - ) - .await - } + if request.proxy_enforced + || matches!(request.windows_sandbox_level, WindowsSandboxLevel::Elevated) + { + spawn_windows_sandbox_session_elevated_for_permission_profile( + request.permission_profile, + request.workspace_roots, + request.codex_home, + request.command, + request.cwd, + request.env_map, + request.proxy_enforced, + request.timeout_ms, + request.read_roots_override, + request.read_roots_include_platform_defaults, + request.write_roots_override, + request.deny_read_paths_override, + request.deny_write_paths_override, + request.tty, + request.stdin_open, + request.use_private_desktop, + ) + .await + } else { + spawn_windows_sandbox_session_legacy( + request.permission_profile, + request.workspace_roots, + request.codex_home, + request.command, + request.cwd, + request.env_map, + request.timeout_ms, + request.deny_read_paths_override, + request.deny_write_paths_override, + request.tty, + request.stdin_open, + request.use_private_desktop, + ) + .await } } @@ -125,6 +126,7 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile( command: Vec, cwd: &Path, env_map: HashMap, + proxy_enforced: bool, timeout_ms: Option, read_roots_override: Option<&[PathBuf]>, read_roots_include_platform_defaults: bool, @@ -142,6 +144,7 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile( command, cwd, env_map, + proxy_enforced, timeout_ms, read_roots_override, read_roots_include_platform_defaults, 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 000000000..375fb07cf --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/wrapper.rs @@ -0,0 +1,320 @@ +//! Internal `codex.exe --run-as-windows-sandbox` wrapper. +//! +//! This gives direct-spawn callers an argv-shaped Windows sandbox launcher, +//! analogous to the macOS seatbelt and Linux sandbox wrapper paths. The wrapper +//! parses sandbox metadata from argv, launches the requested inner command in a +//! Windows sandbox session, and forwards stdio to that inner command. + +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; + +pub const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox"; + +const COMMAND_CWD_FLAG: &str = "--command-cwd"; +const CODEX_HOME_FLAG: &str = "--codex-home"; +const DENY_READ_PATHS_JSON_FLAG: &str = "--deny-read-paths-json"; +const DENY_WRITE_PATHS_JSON_FLAG: &str = "--deny-write-paths-json"; +const ENV_JSON_FLAG: &str = "--env-json"; +const PERMISSION_PROFILE_FLAG: &str = "--permission-profile"; +const PRIVATE_DESKTOP_FLAG: &str = "--windows-sandbox-private-desktop"; +const PROXY_ENFORCED_FLAG: &str = "--proxy-enforced"; +const READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG: &str = "--read-roots-include-platform-defaults"; +const READ_ROOTS_JSON_FLAG: &str = "--read-roots-json"; +const SANDBOX_LEVEL_FLAG: &str = "--windows-sandbox-level"; +const WRITE_ROOTS_JSON_FLAG: &str = "--write-roots-json"; +const WORKSPACE_ROOT_FLAG: &str = "--workspace-root"; + +#[allow(clippy::too_many_arguments)] +pub fn create_windows_sandbox_command_args_for_permission_profile( + command: Vec, + command_cwd: &AbsolutePathBuf, + workspace_roots: &[AbsolutePathBuf], + env_map: &HashMap, + permission_profile: &PermissionProfile, + windows_sandbox_level: WindowsSandboxLevel, + windows_sandbox_private_desktop: bool, + proxy_enforced: bool, + read_roots_override: Option<&[PathBuf]>, + read_roots_include_platform_defaults: bool, + write_roots_override: Option<&[PathBuf]>, + deny_read_paths_override: &[AbsolutePathBuf], + deny_write_paths_override: &[AbsolutePathBuf], + codex_home: &Path, +) -> Vec { + let permission_profile_json = serde_json::to_string(permission_profile) + .unwrap_or_else(|err| panic!("failed to serialize permission profile: {err}")); + let env_json = serde_json::to_string(env_map) + .unwrap_or_else(|err| panic!("failed to serialize env: {err}")); + let mut args = vec![ + CODEX_WINDOWS_SANDBOX_ARG1.to_string(), + CODEX_HOME_FLAG.to_string(), + codex_home.to_string_lossy().into_owned(), + COMMAND_CWD_FLAG.to_string(), + command_cwd.as_path().to_string_lossy().into_owned(), + PERMISSION_PROFILE_FLAG.to_string(), + permission_profile_json, + ENV_JSON_FLAG.to_string(), + env_json, + SANDBOX_LEVEL_FLAG.to_string(), + windows_sandbox_level.to_string(), + ]; + let workspace_roots = if workspace_roots.is_empty() { + std::slice::from_ref(command_cwd) + } else { + workspace_roots + }; + for root in workspace_roots { + args.push(WORKSPACE_ROOT_FLAG.to_string()); + args.push(root.as_path().to_string_lossy().into_owned()); + } + if windows_sandbox_private_desktop { + args.push(PRIVATE_DESKTOP_FLAG.to_string()); + } + if proxy_enforced { + args.push(PROXY_ENFORCED_FLAG.to_string()); + } + if let Some(read_roots_override) = read_roots_override { + push_json_arg(&mut args, READ_ROOTS_JSON_FLAG, &read_roots_override); + } + if read_roots_include_platform_defaults { + args.push(READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG.to_string()); + } + if let Some(write_roots_override) = write_roots_override { + push_json_arg(&mut args, WRITE_ROOTS_JSON_FLAG, &write_roots_override); + } + if !deny_read_paths_override.is_empty() { + push_json_arg( + &mut args, + DENY_READ_PATHS_JSON_FLAG, + &deny_read_paths_override, + ); + } + if !deny_write_paths_override.is_empty() { + push_json_arg( + &mut args, + DENY_WRITE_PATHS_JSON_FLAG, + &deny_write_paths_override, + ); + } + args.push("--".to_string()); + args.extend(command); + args +} + +fn push_json_arg(args: &mut Vec, flag: &str, value: &T) { + args.push(flag.to_string()); + args.push( + serde_json::to_string(value) + .unwrap_or_else(|err| panic!("failed to serialize {flag}: {err}")), + ); +} + +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 = parse_windows_sandbox_wrapper_args(args)?; + run_windows_sandbox_wrapper_request(request).await +} + +struct WindowsSandboxWrapperRequest { + codex_home: PathBuf, + command_cwd: AbsolutePathBuf, + workspace_roots: Vec, + env_map: HashMap, + permission_profile: PermissionProfile, + windows_sandbox_level: WindowsSandboxLevel, + windows_sandbox_private_desktop: bool, + proxy_enforced: bool, + read_roots_override: Option>, + read_roots_include_platform_defaults: bool, + write_roots_override: Option>, + deny_read_paths_override: Vec, + deny_write_paths_override: Vec, + command: Vec, +} + +async fn run_windows_sandbox_wrapper_request(request: WindowsSandboxWrapperRequest) -> Result { + if request.command.is_empty() { + bail!("missing sandboxed command in windows sandbox wrapper request"); + } + let spawned = + crate::spawn_windows_sandbox_session_for_level(crate::WindowsSandboxSessionRequest { + permission_profile: &request.permission_profile, + workspace_roots: request.workspace_roots.as_slice(), + codex_home: request.codex_home.as_path(), + command: request.command, + cwd: request.command_cwd.as_path(), + env_map: request.env_map, + windows_sandbox_level: request.windows_sandbox_level, + proxy_enforced: request.proxy_enforced, + timeout_ms: None, + read_roots_override: request.read_roots_override.as_deref(), + read_roots_include_platform_defaults: request.read_roots_include_platform_defaults, + write_roots_override: request.write_roots_override.as_deref(), + deny_read_paths_override: request.deny_read_paths_override.as_slice(), + deny_write_paths_override: request.deny_write_paths_override.as_slice(), + tty: false, + stdin_open: true, + use_private_desktop: request.windows_sandbox_private_desktop, + }) + .await?; + + Ok(crate::forward_sandbox_session_stdio(spawned).await) +} + +fn parse_windows_sandbox_wrapper_args(args: Vec) -> Result { + let mut args = args.into_iter(); + let mut codex_home = None; + let mut command_cwd = None; + let mut workspace_roots = Vec::new(); + let mut env_map = None; + let mut permission_profile = None; + let mut windows_sandbox_level = None; + let mut windows_sandbox_private_desktop = false; + let mut proxy_enforced = false; + let mut read_roots_override = None; + let mut read_roots_include_platform_defaults = false; + let mut write_roots_override = None; + let mut deny_read_paths_override = Vec::new(); + let mut deny_write_paths_override = Vec::new(); + let mut command = None; + + while let Some(arg) = args.next() { + match arg.as_str() { + CODEX_HOME_FLAG => codex_home = Some(PathBuf::from(next_flag_value(&mut args, &arg)?)), + COMMAND_CWD_FLAG => { + command_cwd = Some(absolute_path_arg(next_flag_value(&mut args, &arg)?, &arg)?); + } + WORKSPACE_ROOT_FLAG => { + workspace_roots.push(absolute_path_arg(next_flag_value(&mut args, &arg)?, &arg)?); + } + ENV_JSON_FLAG => { + let value = next_flag_value(&mut args, &arg)?; + env_map = Some(serde_json::from_str(&value).context("failed to parse env json")?); + } + DENY_READ_PATHS_JSON_FLAG => { + deny_read_paths_override = + json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?; + } + DENY_WRITE_PATHS_JSON_FLAG => { + deny_write_paths_override = + json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?; + } + PERMISSION_PROFILE_FLAG => { + let value = next_flag_value(&mut args, &arg)?; + permission_profile = Some( + serde_json::from_str(&value).context("failed to parse permission profile")?, + ); + } + SANDBOX_LEVEL_FLAG => { + let value = next_flag_value(&mut args, &arg)?; + windows_sandbox_level = Some(parse_windows_sandbox_level(&value)?); + } + PRIVATE_DESKTOP_FLAG => windows_sandbox_private_desktop = true, + PROXY_ENFORCED_FLAG => proxy_enforced = true, + READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG => { + read_roots_include_platform_defaults = true; + } + READ_ROOTS_JSON_FLAG => { + read_roots_override = + Some(json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?); + } + WRITE_ROOTS_JSON_FLAG => { + write_roots_override = + Some(json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?); + } + "--" => { + command = Some(args.collect::>()); + break; + } + _ => bail!("unexpected windows sandbox wrapper argument: {arg}"), + } + } + + let codex_home = codex_home.ok_or_else(|| anyhow!("missing required {CODEX_HOME_FLAG}"))?; + if !codex_home.is_absolute() { + bail!( + "{CODEX_HOME_FLAG} must be absolute: {}", + codex_home.display() + ); + } + let command_cwd = command_cwd.ok_or_else(|| anyhow!("missing required {COMMAND_CWD_FLAG}"))?; + if workspace_roots.is_empty() { + workspace_roots.push(command_cwd.clone()); + } + Ok(WindowsSandboxWrapperRequest { + codex_home, + command_cwd, + workspace_roots, + env_map: env_map.ok_or_else(|| anyhow!("missing required {ENV_JSON_FLAG}"))?, + permission_profile: permission_profile + .ok_or_else(|| anyhow!("missing required {PERMISSION_PROFILE_FLAG}"))?, + windows_sandbox_level: windows_sandbox_level + .ok_or_else(|| anyhow!("missing required {SANDBOX_LEVEL_FLAG}"))?, + windows_sandbox_private_desktop, + proxy_enforced, + read_roots_override, + read_roots_include_platform_defaults, + write_roots_override, + deny_read_paths_override, + deny_write_paths_override, + command: command.ok_or_else(|| anyhow!("missing sandboxed command separator --"))?, + }) +} + +fn next_flag_value(args: &mut impl Iterator, flag: &str) -> Result { + args.next() + .ok_or_else(|| anyhow!("missing value for {flag}")) +} + +fn absolute_path_arg(value: String, flag: &str) -> Result { + let path = PathBuf::from(value); + AbsolutePathBuf::from_absolute_path(path.as_path()) + .with_context(|| format!("{flag} must be absolute: {}", path.display())) +} + +fn json_flag_value(value: String, flag: &str) -> Result { + serde_json::from_str(&value).with_context(|| format!("failed to parse {flag}")) +} + +fn parse_windows_sandbox_level(value: &str) -> Result { + match value { + "disabled" => Ok(WindowsSandboxLevel::Disabled), + "restricted-token" => Ok(WindowsSandboxLevel::RestrictedToken), + "elevated" => Ok(WindowsSandboxLevel::Elevated), + _ => bail!("invalid windows sandbox level: {value}"), + } +} + +#[cfg(test)] +#[path = "wrapper_tests.rs"] +mod tests; diff --git a/codex-rs/windows-sandbox-rs/src/wrapper_tests.rs b/codex-rs/windows-sandbox-rs/src/wrapper_tests.rs new file mode 100644 index 000000000..bf90da4ef --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/wrapper_tests.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; +use std::path::Path; +use std::path::PathBuf; + +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; +use pretty_assertions::assert_eq; + +use super::CODEX_HOME_FLAG; +use super::CODEX_WINDOWS_SANDBOX_ARG1; +use super::COMMAND_CWD_FLAG; +use super::DENY_READ_PATHS_JSON_FLAG; +use super::DENY_WRITE_PATHS_JSON_FLAG; +use super::ENV_JSON_FLAG; +use super::PERMISSION_PROFILE_FLAG; +use super::PRIVATE_DESKTOP_FLAG; +use super::PROXY_ENFORCED_FLAG; +use super::READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG; +use super::READ_ROOTS_JSON_FLAG; +use super::SANDBOX_LEVEL_FLAG; +use super::WORKSPACE_ROOT_FLAG; +use super::WRITE_ROOTS_JSON_FLAG; +use super::create_windows_sandbox_command_args_for_permission_profile; +use super::parse_windows_sandbox_wrapper_args; + +#[test] +fn windows_wrapper_args_round_trip() { + let command_cwd = AbsolutePathBuf::from_absolute_path(Path::new(r"C:\workspace")) + .expect("absolute command cwd"); + let workspace_roots = vec![ + command_cwd.clone(), + AbsolutePathBuf::from_absolute_path(Path::new(r"D:\other-workspace")) + .expect("absolute workspace root"), + ]; + let env = HashMap::from([("Path".to_string(), r"C:\Windows\System32".to_string())]); + let permission_profile = PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }; + let read_roots_override = vec![PathBuf::from(r"C:\read")]; + let write_roots_override = vec![PathBuf::from(r"C:\write")]; + let deny_read_paths_override = vec![ + AbsolutePathBuf::from_absolute_path(Path::new(r"C:\blocked-read")) + .expect("absolute deny-read"), + ]; + let deny_write_paths_override = vec![ + AbsolutePathBuf::from_absolute_path(Path::new(r"C:\blocked-write")) + .expect("absolute deny-write"), + ]; + + let args = create_windows_sandbox_command_args_for_permission_profile( + vec![ + "codex.exe".to_string(), + "--codex-run-as-fs-helper".to_string(), + ], + &command_cwd, + workspace_roots.as_slice(), + &env, + &permission_profile, + WindowsSandboxLevel::Elevated, + /*windows_sandbox_private_desktop*/ true, + /*proxy_enforced*/ true, + Some(read_roots_override.as_slice()), + /*read_roots_include_platform_defaults*/ true, + Some(write_roots_override.as_slice()), + deny_read_paths_override.as_slice(), + deny_write_paths_override.as_slice(), + Path::new(r"C:\Users\me\.codex"), + ); + + assert_eq!(args[0], CODEX_WINDOWS_SANDBOX_ARG1); + assert!(args.contains(&CODEX_HOME_FLAG.to_string())); + assert!(args.contains(&COMMAND_CWD_FLAG.to_string())); + assert!(args.contains(&WORKSPACE_ROOT_FLAG.to_string())); + assert!(args.contains(&PERMISSION_PROFILE_FLAG.to_string())); + assert!(args.contains(&ENV_JSON_FLAG.to_string())); + assert!(args.contains(&SANDBOX_LEVEL_FLAG.to_string())); + assert!(args.contains(&PRIVATE_DESKTOP_FLAG.to_string())); + assert!(args.contains(&PROXY_ENFORCED_FLAG.to_string())); + assert!(args.contains(&READ_ROOTS_JSON_FLAG.to_string())); + assert!(args.contains(&READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG.to_string())); + assert!(args.contains(&WRITE_ROOTS_JSON_FLAG.to_string())); + assert!(args.contains(&DENY_READ_PATHS_JSON_FLAG.to_string())); + assert!(args.contains(&DENY_WRITE_PATHS_JSON_FLAG.to_string())); + + let parsed = + parse_windows_sandbox_wrapper_args(args[1..].to_vec()).expect("parse wrapper args"); + + assert_eq!( + parsed.command, + vec!["codex.exe", "--codex-run-as-fs-helper"] + ); + assert_eq!(parsed.command_cwd, command_cwd); + assert_eq!(parsed.workspace_roots, workspace_roots); + assert_eq!(parsed.env_map, env); + assert_eq!(parsed.permission_profile, permission_profile); + assert_eq!(parsed.windows_sandbox_level, WindowsSandboxLevel::Elevated); + assert_eq!(parsed.windows_sandbox_private_desktop, true); + assert_eq!(parsed.proxy_enforced, true); + assert_eq!(parsed.read_roots_override, Some(read_roots_override)); + assert_eq!(parsed.read_roots_include_platform_defaults, true); + assert_eq!(parsed.write_roots_override, Some(write_roots_override)); + assert_eq!(parsed.deny_read_paths_override, deny_read_paths_override); + assert_eq!(parsed.deny_write_paths_override, deny_write_paths_override); +}