diff --git a/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs b/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs index bbcaec330f..c886ff3a56 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs @@ -4,6 +4,7 @@ use crate::ipc_framed::Message; use crate::ipc_framed::SpawnRequest; use crate::ipc_framed::read_frame; use crate::ipc_framed::write_frame; +use crate::path_normalization::ensure_sandbox_local_path; use crate::runner_pipe::PIPE_ACCESS_INBOUND; use crate::runner_pipe::PIPE_ACCESS_OUTBOUND; use crate::runner_pipe::connect_pipe; @@ -75,6 +76,7 @@ pub(crate) fn spawn_runner_transport( sandbox_creds: &SandboxCreds, log_dir: Option<&Path>, ) -> Result { + ensure_sandbox_local_path(cwd, "sandbox workspace")?; let (pipe_in_name, pipe_out_name) = pipe_pair(); let h_pipe_in = create_named_pipe(&pipe_in_name, PIPE_ACCESS_OUTBOUND, &sandbox_creds.username)?; diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index 327425bd07..6309df8774 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -38,6 +38,7 @@ mod windows_impl { use crate::logging::log_note; use crate::logging::log_start; use crate::logging::log_success; + use crate::path_normalization::ensure_sandbox_local_path; use crate::policy::SandboxPolicy; use crate::policy::parse_policy; use crate::token::convert_string_sid_to_sid; @@ -241,6 +242,7 @@ mod windows_impl { write_roots_override, deny_write_paths_override, } = request; + ensure_sandbox_local_path(cwd, "sandbox workspace")?; let policy = parse_policy(policy_json_or_preset)?; normalize_null_device_env(&mut env_map); ensure_non_interactive_pager(&mut env_map); diff --git a/codex-rs/windows-sandbox-rs/src/path_normalization.rs b/codex-rs/windows-sandbox-rs/src/path_normalization.rs index fe6a932306..634bde76b2 100644 --- a/codex-rs/windows-sandbox-rs/src/path_normalization.rs +++ b/codex-rs/windows-sandbox-rs/src/path_normalization.rs @@ -1,5 +1,13 @@ +use anyhow::Result; use std::path::Path; use std::path::PathBuf; +#[cfg(target_os = "windows")] +use windows_sys::Win32::Storage::FileSystem::DRIVE_REMOTE; +#[cfg(target_os = "windows")] +use windows_sys::Win32::Storage::FileSystem::GetDriveTypeW; + +#[cfg(target_os = "windows")] +use crate::winutil::to_wide; pub fn canonicalize_path(path: &Path) -> PathBuf { dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) @@ -12,9 +20,64 @@ pub fn canonical_path_key(path: &Path) -> String { .to_ascii_lowercase() } +pub fn ensure_sandbox_local_path(path: &Path, context: &str) -> Result<()> { + if let Some(kind) = describe_remote_windows_path(path) { + anyhow::bail!( + "Windows sandbox does not support {kind} for {context}: {}. Use a local checkout or disable sandbox/full-access for this workspace.", + path.display() + ); + } + Ok(()) +} + +pub fn ensure_sandbox_local_paths<'a>( + paths: impl IntoIterator, + context: &str, +) -> Result<()> { + for path in paths { + ensure_sandbox_local_path(path, context)?; + } + Ok(()) +} + +fn describe_remote_windows_path(path: &Path) -> Option<&'static str> { + let path_str = path.as_os_str().to_string_lossy(); + if path_str.starts_with(r"\\") || path_str.starts_with("//") { + return Some("UNC network-share paths"); + } + + #[cfg(target_os = "windows")] + { + if let Some(root) = drive_root(path) { + let root_wide = to_wide(root); + if unsafe { GetDriveTypeW(root_wide.as_ptr()) } == DRIVE_REMOTE { + return Some("mapped network-drive paths"); + } + } + } + + None +} + +#[cfg(target_os = "windows")] +fn drive_root(path: &Path) -> Option { + use std::path::Component; + use std::path::Prefix; + + match path.components().next()? { + Component::Prefix(prefix) + if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_)) => + { + Some(format!(r"{}\", prefix.as_os_str().to_string_lossy())) + } + _ => None, + } +} + #[cfg(test)] mod tests { use super::canonical_path_key; + use super::ensure_sandbox_local_path; use pretty_assertions::assert_eq; use std::path::Path; @@ -23,6 +86,27 @@ 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 unc_paths_are_rejected_for_sandbox_use() { + let err = + ensure_sandbox_local_path(Path::new(r"\\server\share\workspace"), "sandbox workspace") + .expect_err("UNC path should be rejected"); + + assert!( + err.to_string() + .contains("Windows sandbox does not support UNC network-share paths") + ); + } + + #[test] + fn local_paths_are_allowed_for_sandbox_use() { + ensure_sandbox_local_path(Path::new(r"C:\Users\Dev\Repo"), "sandbox workspace") + .expect("local drive path should be allowed"); } } diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 8c334000ec..2d4d84f885 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -15,6 +15,8 @@ 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_sandbox_local_path; +use crate::path_normalization::ensure_sandbox_local_paths; use crate::policy::SandboxPolicy; use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupFailure; @@ -165,7 +167,7 @@ fn run_setup_refresh_inner( ) { return Ok(()); } - let (read_roots, write_roots) = build_payload_roots(&request, &overrides); + let (read_roots, write_roots) = build_payload_roots(&request, &overrides)?; let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths); let network_identity = SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced); @@ -741,7 +743,7 @@ pub fn run_elevated_setup( format!("failed to create sandbox dir {}: {err}", sbx_dir.display()), ) })?; - let (read_roots, write_roots) = build_payload_roots(&request, &overrides); + let (read_roots, write_roots) = build_payload_roots(&request, &overrides)?; let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths); let network_identity = SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced); @@ -772,7 +774,8 @@ pub fn run_elevated_setup( fn build_payload_roots( request: &SandboxSetupRequest<'_>, overrides: &SetupRootOverrides, -) -> (Vec, Vec) { +) -> Result<(Vec, Vec)> { + ensure_sandbox_local_path(request.command_cwd, "sandbox workspace")?; let write_roots = if let Some(roots) = overrides.write_roots.as_deref() { canonical_existing(roots) } else { @@ -810,7 +813,9 @@ fn build_payload_roots( read_roots = filter_ssh_config_dependency_roots(read_roots); let write_root_set: HashSet = write_roots.iter().cloned().collect(); read_roots.retain(|root| !write_root_set.contains(root)); - (read_roots, write_roots) + ensure_sandbox_local_paths(read_roots.iter(), "sandbox readable roots")?; + ensure_sandbox_local_paths(write_roots.iter(), "sandbox writable roots")?; + Ok((read_roots, write_roots)) } fn build_payload_deny_write_paths( @@ -1416,7 +1421,8 @@ mod tests { proxy_enforced: false, }, &super::SetupRootOverrides::default(), - ); + ) + .expect("build payload roots"); let expected_helper = dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir"); let expected_cwd = dunce::canonicalize(&command_cwd).expect("canonical workspace"); @@ -1466,7 +1472,8 @@ mod tests { write_roots: None, deny_write_paths: None, }, - ); + ) + .expect("build payload roots"); let expected_helper = dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir"); let expected_cwd = dunce::canonicalize(&command_cwd).expect("canonical workspace"); @@ -1513,7 +1520,8 @@ mod tests { write_roots: None, deny_write_paths: None, }, - ); + ) + .expect("build payload roots"); let expected_helper = dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir"); let expected_cwd = dunce::canonicalize(&command_cwd).expect("canonical workspace");