diff --git a/codex-rs/core/tests/windows_sandbox.rs b/codex-rs/core/tests/windows_sandbox.rs index 8957895686..54d9ecf309 100644 --- a/codex-rs/core/tests/windows_sandbox.rs +++ b/codex-rs/core/tests/windows_sandbox.rs @@ -38,12 +38,10 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::user_input::UserInput; use codex_sandboxing::SandboxCommand; -use codex_sandboxing::SandboxDirectSpawnTransformRequest; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxSelection; use codex_sandboxing::SandboxTransformRequest; use codex_sandboxing::SandboxType; -use codex_sandboxing::WindowsSandboxProxySettingsMode; use codex_utils_path_uri::PathUri; use codex_windows_sandbox_test_support::WindowsSandboxAccountTestGuard; use core_test_support::PathExt; @@ -504,33 +502,31 @@ async fn windows_elevated_temp_only_core_and_direct_spawn_enforce_carveouts() -> let command = format!("echo CHILD-TEMP:{child_var} & {command}"); let (exit_code, stdout, stderr) = if direct_spawn { let cwd_uri = PathUri::from_abs_path(&cwd); - let request = SandboxManager::new().transform_for_direct_spawn( - SandboxDirectSpawnTransformRequest { - workspace_roots: std::slice::from_ref(&cwd), - windows_sandbox_proxy_settings_mode: WindowsSandboxProxySettingsMode::Preserve, - transform: SandboxTransformRequest { - command: SandboxCommand { - program: "cmd.exe".into(), - args: vec!["/D".into(), "/C".into(), command], - cwd: cwd_uri.clone(), - env, - managed_network: None, - additional_permissions: None, - }, - permissions: &permission_profile, - workspace_roots: None, - runtime_readable_paths: &[], - selection: SandboxSelection::Explicit(SandboxType::WindowsRestrictedToken), - enforce_managed_network: false, - environment_id: None, - network: None, - sandbox_policy_cwd: &cwd_uri, - sandbox_exe: Some(codex.as_path()), - use_legacy_landlock: false, - windows_sandbox_level: WindowsSandboxLevel::Elevated, + // Exec-server spawns the Windows sandbox wrapper returned by transform for filesystem + // helpers. Use cmd.exe as the inner command to probe file access through that wrapper. + let request = SandboxManager::for_file_system_helpers() + .transform(SandboxTransformRequest { + command: SandboxCommand { + program: "cmd.exe".into(), + args: vec!["/D".into(), "/C".into(), command], + cwd: cwd_uri.clone(), + env, + managed_network: None, + additional_permissions: None, }, - }, - )?; + permissions: &permission_profile, + workspace_roots: Some(std::slice::from_ref(&cwd_uri)), + runtime_readable_paths: &[], + selection: SandboxSelection::Explicit(SandboxType::WindowsRestrictedToken), + enforce_managed_network: false, + environment_id: None, + network: None, + sandbox_policy_cwd: &cwd_uri, + sandbox_exe: Some(codex.as_path()), + use_legacy_landlock: false, + windows_sandbox_level: WindowsSandboxLevel::Elevated, + })? + .into_command_request(); let output = Command::new(&request.command[0]) .args(&request.command[1..]) .current_dir(request.cwd.to_abs_path()?) diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 622c123c97..1035220e64 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -153,8 +153,6 @@ impl FileSystemSandboxRunner { })? .into_command_request(); request.sandbox_override = sandbox.sandbox_override; - let policy = request.permission_profile.file_system_sandbox_policy(); - tracing::Span::current().record("permission_entries", policy.entries.len()); Ok(request) } } @@ -535,6 +533,32 @@ mod tests { assert_eq!(request.env.get(&path_key), Some(&path)); } + #[test] + fn sandbox_command_rejects_non_native_policy_cwd() { + #[cfg(unix)] + let foreign_uri = "file://server/share/checkout"; + #[cfg(windows)] + let foreign_uri = "file:///usr/local/checkout"; + let cwd = PathUri::parse(foreign_uri).expect("valid foreign URI"); + let sandbox = sandbox_context_with_cwd(&FileSystemSandboxPolicy::read_only(), cwd.clone()); + let paths = ExecServerRuntimeOptions::new( + std::env::current_exe().expect("current executable"), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths"); + + let error = FileSystemSandboxRunner::new(paths) + .sandbox_command(&sandbox) + .expect_err("non-native policy cwd should be rejected"); + assert_eq!( + error, + crate::rpc::invalid_request(format!( + "'{cwd}' is invalid on '{}'", + std::env::consts::OS + )) + ); + } + /// Removing the selected directory must not change permission anchoring or prevent launch. #[test] fn sandbox_exec_request_uses_filesystem_root_and_preserves_policy_cwd() { @@ -655,45 +679,113 @@ mod tests { #[cfg(windows)] #[test] - fn helper_permissions_include_only_the_helper_executable() { - let codex_self_exe = std::env::current_exe().expect("current exe"); - let codex_self_exe = codex_self_exe.canonicalize().expect("canonical helper"); - let runtime_paths = - ExecServerRuntimeOptions::new(codex_self_exe, /*codex_linux_sandbox_exe*/ None) - .expect("runtime paths"); - // :root covers the cwd's drive. The helper needs an exact read on its own drive. - let drive = if runtime_paths.codex_self_exe.as_path().starts_with(r"C:\") { - r"D:\" - } else { - r"C:\" - }; - let cwd = AbsolutePathBuf::from_absolute_path(drive).expect("absolute cwd"); - let parent = runtime_paths - .codex_self_exe - .parent() - .expect("current exe parent"); - let sibling = parent.join("credentials.json"); + fn windows_helper_binds_executable_and_workspace_grants() { + const CHILD: &str = "CODEX_TEST_WINDOWS_HELPER_GRANTS_CHILD"; + if std::env::var_os(CHILD).is_none() { + // Preparing the token wrapper persists capability SIDs under CODEX_HOME. + let home = tempfile::tempdir().expect("isolated Codex home"); + let output = std::process::Command::new(std::env::current_exe().expect("test binary")) + .args([ + "--exact", + "fs_sandbox::tests::windows_helper_binds_executable_and_workspace_grants", + ]) + .env(CHILD, "1") + .env("CODEX_HOME", home.path()) + .output() + .expect("run isolated Windows helper test"); + assert!( + output.status.success() + && String::from_utf8_lossy(&output.stdout).contains("1 passed"), + "isolated helper test: {output:?}" + ); + return; + } - let context = crate::FileSystemSandboxContext { - windows_sandbox_selection: codex_file_system::WindowsSandboxSelection::RestrictedToken, - ..sandbox_context_with_cwd( - &FileSystemSandboxPolicy::read_only(), - PathUri::from_abs_path(&cwd), - ) + let workspace = tempfile::tempdir().expect("workspace"); + let other = tempfile::tempdir().expect("other workspace"); + let roots = [workspace.path(), other.path()].map(|path| { + AbsolutePathBuf::from_absolute_path(path.canonicalize().expect("canonical workspace")) + .expect("absolute workspace") + }); + let drive = roots[0].ancestors().last().expect("workspace drive"); + // An unused drive also avoids resolving a mapped drive back into the workspace. + let helper = ('D'..='Z') + .rev() + .map(|letter| std::path::PathBuf::from(format!(r"{letter}:\"))) + .find(|root| !root.exists()) + .expect("unused drive for the helper policy") + .join(r"codex-test-runtime\helper.exe"); + let helper = AbsolutePathBuf::from_absolute_path(helper).expect("helper"); + let helper_dir = helper.parent().expect("helper directory"); + let runtime_paths = ExecServerRuntimeOptions::new( + helper.to_path_buf(), + /*codex_linux_sandbox_exe*/ None, + ) + .expect("runtime paths"); + let runner = FileSystemSandboxRunner { + runtime_paths, + helper_env: HashMap::new(), }; - let request = FileSystemSandboxRunner::new(runtime_paths.clone()) - .sandbox_command(&context) - .expect("sandbox command"); - let policy = request.permission_profile.file_system_sandbox_policy(); - - assert!( - policy.can_read_local_path_with_cwd( - runtime_paths.codex_self_exe.as_path(), - cwd.as_path(), - ) - ); - assert!(!policy.can_read_local_path_with_cwd(parent.as_path(), cwd.as_path())); - assert!(!policy.can_read_local_path_with_cwd(sibling.as_path(), cwd.as_path())); + let policy = restricted_policy(vec![ + special_entry(FileSystemSpecialPath::Root, FileSystemAccessMode::Read), + special_entry( + FileSystemSpecialPath::project_roots(/*subpath*/ None), + FileSystemAccessMode::Write, + ), + ]); + for (grants, policy_cwd, wrapper_roots, writable) in [ + (&roots[..], &roots[0], &roots[..], [true, true]), + // The wrapper encodes the command's drive root when no write roots were granted. + ( + &roots[..0], + &roots[0], + std::slice::from_ref(&drive), + [false, false], + ), + (&roots[1..], &roots[1], &roots[1..], [false, true]), + ] { + let sandbox = crate::FileSystemSandboxContext { + workspace_roots: grants.iter().map(PathUri::from_abs_path).collect(), + windows_sandbox_selection: + codex_file_system::WindowsSandboxSelection::RestrictedToken, + ..sandbox_context_with_cwd(&policy, PathUri::from_abs_path(policy_cwd)) + }; + let request = runner + .sandbox_command(&sandbox) + .expect("prepare Windows helper"); + let args = &request.command; + assert_eq!( + args.windows(2) + .filter(|pair| pair[0] == "--workspace-root") + .map(|pair| std::path::Path::new(&pair[1])) + .collect::>(), + wrapper_roots + .iter() + .map(AbsolutePathBuf::as_path) + .collect::>() + ); + let profile = args + .windows(2) + .find(|pair| pair[0] == "--permission-profile") + .expect("serialized helper policy"); + let profile: PermissionProfile = + serde_json::from_str(&profile[1]).expect("helper policy"); + let fs = profile.file_system_sandbox_policy(); + assert_eq!( + roots + .each_ref() + .map(|root| fs.can_write_local_path_with_cwd(root, policy_cwd)), + writable + ); + assert_eq!( + fs.resolve_access_for_local_path_with_cwd(&helper, policy_cwd), + FileSystemAccessMode::Read + ); + assert!(!fs.can_read_local_path_with_cwd(&helper_dir, policy_cwd)); + assert!( + !fs.can_read_local_path_with_cwd(&helper_dir.join("credentials.json"), policy_cwd) + ); + } } #[cfg(unix)] diff --git a/codex-rs/exec-server/tests/file_system/shared.rs b/codex-rs/exec-server/tests/file_system/shared.rs index 13b0b11431..1ecefb5e64 100644 --- a/codex-rs/exec-server/tests/file_system/shared.rs +++ b/codex-rs/exec-server/tests/file_system/shared.rs @@ -1225,14 +1225,19 @@ async fn sandboxed_file_operations_cannot_read_or_write_helper_siblings() -> Res std::os::unix::fs::symlink(&sibling, &escaping_link)?; let sandbox = workspace_write_sandbox(workspace.clone()); let allowed_file = workspace.join("allowed.txt"); - std::fs::write(&allowed_file, b"allowed")?; - let allowed_contents = file_system - .read_file( - &PathUri::from_host_native_path(&allowed_file)?, + let allowed_uri = PathUri::from_host_native_path(&allowed_file)?; + file_system + .write_file( + &allowed_uri, + b"allowed".to_vec(), Default::default(), Some(&sandbox), ) .await?; + assert_eq!(std::fs::read(&allowed_file)?, b"allowed"); + let allowed_contents = file_system + .read_file(&allowed_uri, Default::default(), Some(&sandbox)) + .await?; assert_eq!(allowed_contents, b"allowed"); #[cfg(target_os = "macos")] diff --git a/codex-rs/sandboxing/src/launch.rs b/codex-rs/sandboxing/src/launch.rs index 1945b0d515..1725a9ec6f 100644 --- a/codex-rs/sandboxing/src/launch.rs +++ b/codex-rs/sandboxing/src/launch.rs @@ -113,7 +113,10 @@ impl PreparedLaunchPolicy { }); }; let cwd = native_path(policy_cwd, "sandbox policy cwd")?; - let workspace_roots = native_workspace_roots(roots)?; + let workspace_roots = roots + .iter() + .map(|root| native_path(root, "sandbox workspace root")) + .collect::>>()?; let (file_system, network) = permissions.to_runtime_permissions(); for entry in &file_system.entries { if let FileSystemPath::Path { path } = &entry.path { @@ -123,6 +126,7 @@ impl PreparedLaunchPolicy { let mut file_system = file_system.materialize_project_roots_with_workspace_roots(&workspace_roots); let permissions = if is_file_system_helper { + tracing::Span::current().record("permission_entries", file_system.entries.len()); if !file_system.has_full_disk_read_access() { let minimal = FileSystemSandboxEntry::new( FileSystemPath::Special { @@ -170,13 +174,6 @@ impl PreparedLaunchPolicy { } } -pub(super) fn native_workspace_roots(roots: &[PathUri]) -> io::Result> { - roots - .iter() - .map(|root| native_path(root, "sandbox workspace root")) - .collect::>>() -} - fn native_path(path: &PathUri, field: &str) -> io::Result { path.to_abs_path() .map_err(|err| io::Error::new(err.kind(), format!("invalid {field}: {err}"))) diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index 6e3345afc2..4b8583ff53 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -31,7 +31,6 @@ pub use denial::is_likely_executor_managed_sandbox_denied; pub use denial::is_likely_sandbox_denied; pub use linux_pid_namespace::LinuxSandboxPidNamespace; pub use manager::SandboxCommand; -pub use manager::SandboxDirectSpawnTransformRequest; pub use manager::SandboxExecRequest; pub use manager::SandboxManager; pub use manager::SandboxSelection; diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index f80f2f06aa..eb2454f4ff 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -149,17 +149,6 @@ pub struct SandboxTransformRequest<'a> { pub windows_sandbox_level: WindowsSandboxLevel, } -/// Bundled arguments for a sandbox transformation whose result will be spawned -/// directly from argv. -/// -/// Direct-spawn callers will not run a later platform-specific launcher, so the -/// returned command must encode any sandbox wrapper it needs. -pub struct SandboxDirectSpawnTransformRequest<'a> { - pub transform: SandboxTransformRequest<'a>, - pub workspace_roots: &'a [AbsolutePathBuf], - pub windows_sandbox_proxy_settings_mode: codex_windows_sandbox::WindowsSandboxProxySettingsMode, -} - // TODO(anp): Revisit this preparation type once this module's PathUri migration is complete. struct PendingSandboxedExecRequest { native_command_cwd: AbsolutePathBuf, @@ -624,15 +613,22 @@ impl SandboxManager { } SandboxType::WindowsRestrictedToken => { exec.permission_profile = pending?.effective_permission_profile; - #[cfg(windows)] if is_file_system_helper { - wrap_windows_sandbox_exec_request_for_direct_spawn( - &mut exec, - &policy.workspace_roots, - /*codex_home*/ None, - request.sandbox_exe, - codex_windows_sandbox::WindowsSandboxProxySettingsMode::Preserve, - )?; + #[cfg(not(windows))] + return Err(SandboxTransformError::SandboxUnavailable); + #[cfg(windows)] + { + let codex_home = + codex_utils_home_dir::find_codex_home().map_err(|err| { + SandboxTransformError::WindowsSandboxPreparation(err.to_string()) + })?; + wrap_windows_file_system_helper( + &mut exec, + &policy.workspace_roots, + &codex_home, + request.sandbox_exe, + )?; + } } } } @@ -679,67 +675,15 @@ impl SandboxManager { exec.arg0 = Some(arg0.to_owned()); Ok(()) } - - pub fn transform_for_direct_spawn( - &self, - request: SandboxDirectSpawnTransformRequest<'_>, - ) -> Result { - #[cfg(windows)] - { - self.transform_for_direct_spawn_with_codex_home(request, /*codex_home*/ None) - } - #[cfg(not(windows))] - { - self.transform(request.transform) - .map(WrappedSandboxCommand::into_command_request) - } - } - - #[cfg(target_os = "windows")] - fn transform_for_direct_spawn_with_codex_home( - &self, - request: SandboxDirectSpawnTransformRequest<'_>, - codex_home: Option<&Path>, - ) -> Result { - let roots = request.transform.workspace_roots; - let fallback_roots = request.workspace_roots; - let proxy_settings_mode = request.windows_sandbox_proxy_settings_mode; - let sandbox_exe = request.transform.sandbox_exe; - let mut request = self.transform(request.transform)?.into_command_request(); - if request.sandbox != SandboxType::WindowsRestrictedToken { - return Ok(request); - } - let workspace_roots = roots - .map(launch::native_workspace_roots) - .transpose() - .map_err(SandboxTransformError::InvalidPolicy)?; - wrap_windows_sandbox_exec_request_for_direct_spawn( - &mut request, - workspace_roots.as_deref().unwrap_or(fallback_roots), - codex_home, - sandbox_exe, - proxy_settings_mode, - )?; - Ok(request) - } } #[cfg(target_os = "windows")] -fn wrap_windows_sandbox_exec_request_for_direct_spawn( +fn wrap_windows_file_system_helper( request: &mut SandboxExecRequest, workspace_roots: &[AbsolutePathBuf], - codex_home: Option<&Path>, + codex_home: &Path, sandbox_exe: Option<&Path>, - proxy_settings_mode: codex_windows_sandbox::WindowsSandboxProxySettingsMode, ) -> Result<(), SandboxTransformError> { - let owned_codex_home; - let codex_home = if let Some(codex_home) = codex_home { - codex_home - } else { - owned_codex_home = codex_utils_home_dir::find_codex_home() - .map_err(|err| SandboxTransformError::WindowsSandboxPreparation(err.to_string()))?; - owned_codex_home.as_path() - }; let sandbox_exe = sandbox_exe.ok_or_else(|| { SandboxTransformError::WindowsSandboxPreparation( "missing Codex executable path".to_string(), @@ -821,7 +765,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn( request.windows_sandbox_level, proxy_enforced, network_proxy_restricting_sid.as_deref(), - proxy_settings_mode, + codex_windows_sandbox::WindowsSandboxProxySettingsMode::Preserve, read_roots_override, read_roots_include_platform_defaults, write_roots_override, diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 11162adeaf..b838f5c2ec 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -1,6 +1,4 @@ use super::SandboxCommand; -#[cfg(windows)] -use super::SandboxDirectSpawnTransformRequest; use super::SandboxManager; use super::SandboxSelection; use super::SandboxTransformRequest; @@ -394,35 +392,58 @@ fn managed_mitm_ca_bundle_becomes_readable_for_restricted_sandbox() { ); } +#[cfg(unix)] +fn sandbox_transform_request(cwd: &PathUri, sandbox: SandboxType) -> SandboxTransformRequest<'_> { + SandboxTransformRequest { + command: SandboxCommand { + program: "true".into(), + args: Vec::new(), + cwd: cwd.clone(), + env: HashMap::new(), + managed_network: None, + additional_permissions: None, + }, + permissions: &PermissionProfile::Disabled, + workspace_roots: None, + runtime_readable_paths: &[], + selection: SandboxSelection::Explicit(sandbox), + enforce_managed_network: false, + environment_id: None, + network: None, + sandbox_policy_cwd: cwd, + sandbox_exe: None, + use_legacy_landlock: false, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + } +} + +#[cfg(unix)] +#[test] +fn windows_token_filesystem_helper_is_unavailable_on_unix() { + let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("current dir")); + let request = SandboxTransformRequest { + permissions: &PermissionProfile::read_only(), + workspace_roots: Some(std::slice::from_ref(&cwd)), + windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, + ..sandbox_transform_request(&cwd, SandboxType::WindowsRestrictedToken) + }; + + assert!(matches!( + SandboxManager::for_file_system_helpers().transform(request), + Err(super::SandboxTransformError::SandboxUnavailable) + )); +} + #[cfg(unix)] fn transform_linux_seccomp_request( codex_linux_sandbox_exe: &std::path::Path, manager: SandboxManager, ) -> super::WrappedSandboxCommand { - let cwd = AbsolutePathBuf::current_dir().expect("current dir"); - let cwd_uri = PathUri::from_abs_path(&cwd); - let permissions = PermissionProfile::Disabled; + let cwd = PathUri::from_abs_path(&AbsolutePathBuf::current_dir().expect("current dir")); manager .transform(SandboxTransformRequest { - command: SandboxCommand { - program: "true".into(), - args: Vec::new(), - cwd: cwd_uri.clone(), - env: HashMap::new(), - managed_network: None, - additional_permissions: None, - }, - permissions: &permissions, - workspace_roots: None, - runtime_readable_paths: &[], - selection: SandboxSelection::Explicit(SandboxType::LinuxSeccomp), - enforce_managed_network: false, - environment_id: None, - network: None, - sandbox_policy_cwd: &cwd_uri, sandbox_exe: Some(codex_linux_sandbox_exe), - use_legacy_landlock: false, - windows_sandbox_level: WindowsSandboxLevel::Disabled, + ..sandbox_transform_request(&cwd, SandboxType::LinuxSeccomp) }) .expect("transform") } @@ -700,7 +721,7 @@ async fn linux_unix_socket_grant_uses_effective_managed_policy() -> anyhow::Resu #[cfg(target_os = "windows")] #[test] -fn transform_for_direct_spawn_windows_preserves_only_wrapper_setup_environment() { +fn windows_wrapper_preserves_only_setup_environment() { let mut env = HashMap::from([ ("Path".to_string(), r"C:\Windows\System32".to_string()), ("username".to_string(), "wrong-user".to_string()), @@ -760,7 +781,7 @@ fn wrapper_runtime_selection_uses_the_parent_not_environment_overrides() { #[cfg(target_os = "windows")] #[test] -fn transform_for_direct_spawn_windows_materializes_inner_helper() { +fn windows_file_system_wrapper_materializes_inner_helper() { let codex_home = tempfile::TempDir::new().expect("codex home"); let helper_dir = tempfile::TempDir::new().expect("helper dir"); let configured_helper = helper_dir.path().join("configured-codex-helper.exe"); @@ -796,50 +817,37 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { ]), NetworkSandboxPolicy::Restricted, ); - let other_workspace = tempfile::TempDir::new().expect("other workspace"); - let other_workspace_root = AbsolutePathBuf::from_absolute_path( - canonicalize(other_workspace.path()).expect("canonical other workspace"), + let policy = super::PreparedLaunchPolicy::resolve( + permissions, + &cwd_uri, + Some(std::slice::from_ref(&cwd_uri)), + &[], + &super::SandboxOperation::FileSystemHelper, ) - .expect("absolute other workspace"); - let workspace_roots = [cwd, other_workspace_root]; - let manager = SandboxManager::new(); - let prepare = |roots, policy_cwd| { - manager - .transform_for_direct_spawn_with_codex_home( - SandboxDirectSpawnTransformRequest { - workspace_roots: workspace_roots.as_slice(), - windows_sandbox_proxy_settings_mode: - codex_windows_sandbox::WindowsSandboxProxySettingsMode::Preserve, - transform: SandboxTransformRequest { - command: SandboxCommand { - program: configured_helper.as_os_str().to_owned(), - args: vec!["--codex-run-as-fs-helper".to_string()], - cwd: cwd_uri.clone(), - env: HashMap::from([( - "Path".to_string(), - r"C:\Windows\System32".to_string(), - )]), - managed_network: None, - additional_permissions: None, - }, - permissions: &permissions, - workspace_roots: roots, - runtime_readable_paths: &[], - selection: SandboxSelection::Explicit(SandboxType::WindowsRestrictedToken), - enforce_managed_network: false, - environment_id: None, - network: None, - sandbox_policy_cwd: policy_cwd, - sandbox_exe: Some(configured_helper.as_path()), - use_legacy_landlock: false, - windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, - }, - }, - Some(codex_home.path()), - ) - .expect("transform for direct spawn") + .expect("resolve helper policy"); + let mut exec_request = super::SandboxExecRequest { + sandbox_override: super::SandboxOverride::NoOverride, + command: vec![ + configured_helper.display().to_string(), + "--codex-run-as-fs-helper".to_string(), + ], + cwd: cwd_uri.clone(), + sandbox_policy_cwd: cwd_uri, + env: HashMap::from([("Path".to_string(), r"C:\Windows\System32".to_string())]), + network: None, + network_environment_id: None, + sandbox: SandboxType::WindowsRestrictedToken, + windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, + permission_profile: policy.permissions, + arg0: None, }; - let exec_request = prepare(/*roots*/ None, &cwd_uri); + super::wrap_windows_file_system_helper( + &mut exec_request, + &policy.workspace_roots, + codex_home.path(), + Some(&configured_helper), + ) + .expect("wrap filesystem helper"); let inner_env_json = exec_request .command @@ -885,54 +893,6 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { exec_request.command[separator_index + 2], "--codex-run-as-fs-helper" ); - let explicit_root = PathUri::from_abs_path(&workspace_roots[1]); - // The wrapper encodes the cwd for an empty root list. Its serialized policy must - // already have removed the workspace write grant. - for (request, expected_roots, expected_writes) in [ - (&exec_request, &workspace_roots[..], None), - ( - &prepare(Some(&[]), &cwd_uri), - &workspace_roots[..1], - Some([false, false]), - ), - ( - // The legacy workspace-write projection always includes its policy cwd. - // Anchor it at the grant and check the command cwd independently. - &prepare(Some(std::slice::from_ref(&explicit_root)), &explicit_root), - &workspace_roots[1..], - Some([false, true]), - ), - ] { - assert_eq!( - request - .command - .windows(2) - .filter_map(|args| { - (args[0] == "--workspace-root").then_some(std::path::PathBuf::from(&args[1])) - }) - .collect::>(), - expected_roots - .iter() - .map(|root| root.as_path().to_path_buf()) - .collect::>() - ); - if let Some(expected_writes) = expected_writes { - let profile_json = request - .command - .windows(2) - .find(|args| args[0] == "--permission-profile") - .expect("serialized wrapper policy"); - let policy = serde_json::from_str::(&profile_json[1]) - .expect("decode wrapper policy") - .file_system_sandbox_policy(); - assert_eq!( - workspace_roots - .each_ref() - .map(|root| policy.can_write_local_path_with_cwd(root, &workspace_roots[0])), - expected_writes - ); - } - } assert_eq!( materialized_helper .parent()