diff --git a/codex-rs/windows-sandbox-rs/src/path_normalization.rs b/codex-rs/windows-sandbox-rs/src/path_normalization.rs index fe6a932306..0d3fc9ddca 100644 --- a/codex-rs/windows-sandbox-rs/src/path_normalization.rs +++ b/codex-rs/windows-sandbox-rs/src/path_normalization.rs @@ -1,5 +1,10 @@ +use anyhow::Result; use std::path::Path; use std::path::PathBuf; +use windows_sys::Win32::Storage::FileSystem::DRIVE_REMOTE; +use windows_sys::Win32::Storage::FileSystem::GetDriveTypeW; + +use crate::winutil::to_wide; pub fn canonicalize_path(path: &Path) -> PathBuf { dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) @@ -12,9 +17,41 @@ pub fn canonical_path_key(path: &Path) -> String { .to_ascii_lowercase() } +pub fn ensure_windows_sandbox_local_path(path: &Path, context: &str) -> Result<()> { + let raw = path.to_string_lossy(); + let normalized = normalize_windows_device_path(&raw).unwrap_or_else(|| raw.into_owned()); + if normalized.starts_with(r"\\") { + anyhow::bail!( + "windows sandbox does not support {context} on UNC or mapped network paths: {}. Use a local drive workspace or run without the Windows sandbox for this session.", + path.display() + ); + } + + if let Some(root) = windows_drive_root(&normalized) { + let drive_type = unsafe { GetDriveTypeW(to_wide(&root).as_ptr()) }; + if drive_type == DRIVE_REMOTE { + anyhow::bail!( + "windows sandbox does not support {context} on mapped network drives: {}. Use the underlying local drive path or run without the Windows sandbox for this session.", + path.display() + ); + } + } + + Ok(()) +} + +fn windows_drive_root(path: &str) -> Option { + let bytes = path.as_bytes(); + if bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { + return Some(format!("{}:\\", path[..1].to_ascii_uppercase())); + } + None +} + #[cfg(test)] mod tests { use super::canonical_path_key; + use super::windows_drive_root; use pretty_assertions::assert_eq; use std::path::Path; @@ -23,6 +60,16 @@ mod tests { 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) + ); + } + + #[test] + fn windows_drive_root_extracts_drive_prefix() { + assert_eq!(windows_drive_root(r"l:\cs-web"), Some(r"L:\".to_string())); + assert_eq!(windows_drive_root(r"C:/repo"), Some(r"C:\".to_string())); + assert_eq!(windows_drive_root(r"\\video1\node\cs-web"), None); } } diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 8c334000ec..b608c5155b 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -15,6 +15,7 @@ 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::ensure_windows_sandbox_local_path; use crate::policy::SandboxPolicy; use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupFailure; @@ -167,6 +168,13 @@ fn run_setup_refresh_inner( } let (read_roots, write_roots) = build_payload_roots(&request, &overrides); let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths); + ensure_windows_sandbox_local_path(request.command_cwd, "sandbox working directory")?; + for root in &write_roots { + ensure_windows_sandbox_local_path(root, "sandbox writable root")?; + } + for path in &deny_write_paths { + ensure_windows_sandbox_local_path(path, "sandbox protected path")?; + } let network_identity = SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced); let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity); diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs index aab2b54462..a106f4b0e8 100644 --- a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -13,6 +13,7 @@ use crate::identity::SandboxCreds; use crate::identity::require_logon_sandbox_creds; use crate::logging::log_start; use crate::path_normalization::canonicalize_path; +use crate::path_normalization::ensure_windows_sandbox_local_path; use crate::policy::SandboxPolicy; use crate::policy::parse_policy; use crate::sandbox_utils::ensure_codex_home_exists; @@ -103,6 +104,8 @@ fn prepare_spawn_context_common( anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") } + ensure_windows_sandbox_local_path(cwd, "sandbox working directory")?; + normalize_null_device_env(env_map); ensure_non_interactive_pager(env_map); if inherit_path { @@ -282,6 +285,12 @@ pub(crate) fn prepare_elevated_spawn_context( ); let write_roots: Vec = allow.into_iter().collect(); let deny_write_paths: Vec = deny.into_iter().collect(); + for root in &write_roots { + ensure_windows_sandbox_local_path(root, "sandbox writable root")?; + } + for path in &deny_write_paths { + ensure_windows_sandbox_local_path(path, "sandbox protected path")?; + } let write_roots_override = if common.is_workspace_write { Some(write_roots.as_slice()) } else {