From cb508310460046a651888796b41f6055c12e3398 Mon Sep 17 00:00:00 2001 From: David Wiesen Date: Thu, 11 Jun 2026 15:30:34 -0700 Subject: [PATCH] Run fs helper through Windows sandbox wrapper --- codex-rs/Cargo.lock | 3 + codex-rs/exec-server/Cargo.toml | 5 + codex-rs/exec-server/src/fs_sandbox.rs | 245 ++++++++++++-- codex-rs/exec-server/tests/common/mod.rs | 6 + .../tests/windows_file_system_sandbox.rs | 300 ++++++++++++++++++ 5 files changed, 532 insertions(+), 27 deletions(-) create mode 100644 codex-rs/exec-server/tests/windows_file_system_sandbox.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 69318a7ce8..237eaf5f16 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2846,8 +2846,11 @@ dependencies = [ "codex-shell-command", "codex-test-binary-support", "codex-utils-absolute-path", + "codex-utils-cargo-bin", + "codex-utils-home-dir", "codex-utils-pty", "codex-utils-rustls-provider", + "codex-windows-sandbox", "ctor 0.6.3", "futures", "http 1.4.0", diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index eadab83fe7..38bfe315bb 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -49,9 +49,14 @@ tokio-tungstenite = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["v4"] } +[target.'cfg(windows)'.dependencies] +codex-utils-home-dir = { workspace = true } +codex-windows-sandbox = { workspace = true } + [dev-dependencies] anyhow = { workspace = true } codex-test-binary-support = { workspace = true } +codex-utils-cargo-bin = { workspace = true } ctor = { workspace = true } http = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 8ef20b2d1d..3b28d11fb8 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -59,11 +59,15 @@ impl FileSystemSandboxRunner { request: FsHelperRequest, ) -> Result { let cwd = sandbox_cwd(sandbox)?; + let helper = self.helper_exe_for_launch()?; let mut file_system_policy = sandbox.permissions.file_system_sandbox_policy(); + // The sandboxed child re-enters Codex as the filesystem helper. On Windows + // that executable can be a materialized copy under CODEX_HOME/.sandbox-bin, + // so derive helper read roots from the exact helper path we will launch. let helper_read_roots = if sandbox.use_legacy_landlock { Vec::new() } else { - helper_read_roots(&self.runtime_paths) + helper_read_roots(&helper, self.runtime_paths.codex_linux_sandbox_exe.as_ref()) }; add_helper_runtime_permissions(&mut file_system_policy, &helper_read_roots, cwd.as_path()); normalize_file_system_policy_root_aliases(&mut file_system_policy); @@ -73,18 +77,48 @@ impl FileSystemSandboxRunner { &file_system_policy, network_policy, ); - let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox)?; + let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox, &helper)?; let request_json = serde_json::to_vec(&request).map_err(json_error)?; run_command(command, request_json).await } + fn helper_exe_for_launch(&self) -> Result { + #[cfg(target_os = "windows")] + { + // Windows sandbox launch grants are prepared around the executable + // path that the sandbox will spawn. When exec-server is embedded or + // hosted, `current_exe()` can point at the host process instead of + // the configured Codex binary, so materialize the runtime-provided + // helper and use that exact path for both launch and read roots. + let codex_home = codex_utils_home_dir::find_codex_home().map_err(|err| { + internal_error(format!( + "windows fs sandbox helper failed to resolve CODEX_HOME: {err}" + )) + })?; + let helper = codex_windows_sandbox::resolve_exe_for_launch( + self.runtime_paths.codex_self_exe.as_path(), + codex_home.as_path(), + ); + AbsolutePathBuf::from_absolute_path(helper.as_path()).map_err(|err| { + internal_error(format!( + "windows fs sandbox helper path is not absolute: {err}" + )) + }) + } + + #[cfg(not(target_os = "windows"))] + { + Ok(self.runtime_paths.codex_self_exe.clone()) + } + } + fn sandbox_exec_request( &self, permission_profile: &PermissionProfile, cwd: &AbsolutePathBuf, sandbox_context: &FileSystemSandboxContext, - ) -> Result { - let helper = &self.runtime_paths.codex_self_exe; + helper: &AbsolutePathBuf, + ) -> Result { let sandbox_manager = SandboxManager::new(); let (file_system_policy, network_policy) = permission_profile.to_runtime_permissions(); let sandbox = sandbox_manager.select_initial( @@ -101,7 +135,7 @@ impl FileSystemSandboxRunner { env: self.helper_env.clone(), additional_permissions: None, }; - sandbox_manager + let mut command = sandbox_manager .transform(SandboxTransformRequest { command, permissions: permission_profile, @@ -114,7 +148,43 @@ impl FileSystemSandboxRunner { windows_sandbox_level: sandbox_context.windows_sandbox_level, windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop, }) - .map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}"))) + .map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))?; + #[cfg(target_os = "windows")] + if command.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken { + let request_file = wrap_windows_sandbox_command(&mut command, helper)?; + return Ok(PreparedFsHelperCommand::with_windows_wrapper_request_file( + command, + request_file, + )); + } + Ok(PreparedFsHelperCommand::new(command)) + } +} + +struct PreparedFsHelperCommand { + request: SandboxExecRequest, + #[cfg(target_os = "windows")] + _windows_wrapper_request_file: Option, +} + +impl PreparedFsHelperCommand { + fn new(request: SandboxExecRequest) -> Self { + Self { + request, + #[cfg(target_os = "windows")] + _windows_wrapper_request_file: None, + } + } + + #[cfg(target_os = "windows")] + fn with_windows_wrapper_request_file( + request: SandboxExecRequest, + request_file: WindowsSandboxWrapperRequestFile, + ) -> Self { + Self { + request, + _windows_wrapper_request_file: Some(request_file), + } } } @@ -134,10 +204,13 @@ fn sandbox_cwd(sandbox: &FileSystemSandboxContext) -> Result Vec { +fn helper_read_roots( + codex_self_exe: &AbsolutePathBuf, + codex_linux_sandbox_exe: Option<&AbsolutePathBuf>, +) -> Vec { let mut roots = Vec::new(); - for path in std::iter::once(runtime_paths.codex_self_exe.as_path()) - .chain(runtime_paths.codex_linux_sandbox_exe.as_deref()) + for path in std::iter::once(codex_self_exe.as_path()) + .chain(codex_linux_sandbox_exe.map(AbsolutePathBuf::as_path)) { if let Some(parent) = path.parent() && let Ok(root) = AbsolutePathBuf::from_absolute_path(parent) @@ -247,20 +320,11 @@ fn bazel_bwrap_env_key_is_allowed(_key: &str) -> bool { } async fn run_command( - command: SandboxExecRequest, + command: PreparedFsHelperCommand, request_json: Vec, ) -> Result { - let mut child = spawn_command(command)?; - let mut stdin = child - .stdin - .take() - .ok_or_else(|| internal_error("failed to open fs sandbox helper stdin".to_string()))?; - stdin.write_all(&request_json).await.map_err(io_error)?; - stdin.shutdown().await.map_err(io_error)?; - drop(stdin); - - let output = child.wait_with_output().await.map_err(io_error)?; - if !output.status.success() { + let output = run_child_command(command, request_json).await?; + if !output.success { return Err(internal_error(format!( "fs sandbox helper failed with status {status}: {stderr}", status = output.status, @@ -274,7 +338,36 @@ async fn run_command( } } -fn spawn_command( +struct FsHelperCommandOutput { + success: bool, + status: String, + stdout: Vec, + stderr: Vec, +} + +async fn run_child_command( + command: PreparedFsHelperCommand, + request_json: Vec, +) -> Result { + let mut child = spawn_child_command(command.request)?; + let mut stdin = child + .stdin + .take() + .ok_or_else(|| internal_error("failed to open fs sandbox helper stdin".to_string()))?; + stdin.write_all(&request_json).await.map_err(io_error)?; + stdin.shutdown().await.map_err(io_error)?; + drop(stdin); + + let output = child.wait_with_output().await.map_err(io_error)?; + Ok(FsHelperCommandOutput { + success: output.status.success(), + status: output.status.to_string(), + stdout: output.stdout, + stderr: output.stderr, + }) +} + +fn spawn_child_command( SandboxExecRequest { command: argv, cwd, @@ -304,6 +397,90 @@ fn spawn_command( command.spawn().map_err(io_error) } +#[cfg(target_os = "windows")] +fn wrap_windows_sandbox_command( + request: &mut SandboxExecRequest, + helper: &AbsolutePathBuf, +) -> Result { + let codex_home = codex_utils_home_dir::find_codex_home().map_err(|err| { + internal_error(format!( + "windows fs sandbox helper failed to resolve CODEX_HOME: {err}" + )) + })?; + let wrapper_request = + codex_windows_sandbox::create_windows_sandbox_wrapper_request_for_permission_profile( + std::mem::take(&mut request.command), + request.cwd.clone(), + request.permission_profile.clone(), + request.windows_sandbox_level, + request.windows_sandbox_private_desktop, + codex_home.to_path_buf(), + ); + let request_file = + WindowsSandboxWrapperRequestFile::create(codex_home.as_path(), &wrapper_request)?; + let mut args = codex_windows_sandbox::create_windows_sandbox_command_args_for_request_file( + request_file.path.as_path(), + ); + request.command = Vec::with_capacity(1 + args.len()); + request + .command + .push(helper.as_path().to_string_lossy().into_owned()); + request.command.append(&mut args); + request.sandbox = codex_sandboxing::SandboxType::None; + request.arg0 = None; + Ok(request_file) +} + +#[cfg(target_os = "windows")] +struct WindowsSandboxWrapperRequestFile { + path: std::path::PathBuf, +} + +#[cfg(target_os = "windows")] +impl WindowsSandboxWrapperRequestFile { + fn create( + codex_home: &std::path::Path, + request: &codex_windows_sandbox::WindowsSandboxWrapperRequest, + ) -> Result { + let request_dir = codex_windows_sandbox::sandbox_dir(codex_home); + std::fs::create_dir_all(&request_dir).map_err(|err| { + internal_error(format!( + "failed to create windows fs sandbox wrapper request dir {}: {err}", + request_dir.display() + )) + })?; + let path = request_dir.join(format!( + "fs-helper-wrapper-request-{}.json", + uuid::Uuid::new_v4() + )); + let request_json = serde_json::to_vec(request).map_err(json_error)?; + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .map_err(|err| { + internal_error(format!( + "failed to create windows fs sandbox wrapper request file {}: {err}", + path.display() + )) + })?; + std::io::Write::write_all(&mut file, &request_json).map_err(|err| { + internal_error(format!( + "failed to write windows fs sandbox wrapper request file {}: {err}", + path.display() + )) + })?; + Ok(Self { path }) + } +} + +#[cfg(target_os = "windows")] +impl Drop for WindowsSandboxWrapperRequestFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.path); + } +} + fn io_error(err: std::io::Error) -> JSONRPCErrorError { internal_error(err.to_string()) } @@ -387,7 +564,10 @@ mod tests { add_helper_runtime_permissions( &mut policy, - &helper_read_roots(&runtime_paths), + &helper_read_roots( + &runtime_paths.codex_self_exe, + /*codex_linux_sandbox_exe*/ None, + ), cwd.as_path(), ); @@ -498,10 +678,15 @@ mod tests { let sandbox_context = sandbox_context_with_cwd(&file_system_policy, cwd.clone()); let request = runner - .sandbox_exec_request(&permission_profile, &cwd, &sandbox_context) + .sandbox_exec_request( + &permission_profile, + &cwd, + &sandbox_context, + &runner.runtime_paths.codex_self_exe, + ) .expect("sandbox exec request"); - assert_eq!(request.env.get(&path_key), Some(&path)); + assert_eq!(request.request.env.get(&path_key), Some(&path)); } #[test] @@ -556,7 +741,10 @@ mod tests { add_helper_runtime_permissions( &mut policy, - &helper_read_roots(&runtime_paths), + &helper_read_roots( + &runtime_paths.codex_self_exe, + /*codex_linux_sandbox_exe*/ None, + ), cwd.as_path(), ); @@ -581,7 +769,10 @@ mod tests { add_helper_runtime_permissions( &mut policy, - &helper_read_roots(&runtime_paths), + &helper_read_roots( + &runtime_paths.codex_self_exe, + runtime_paths.codex_linux_sandbox_exe.as_ref(), + ), cwd.as_path(), ); diff --git a/codex-rs/exec-server/tests/common/mod.rs b/codex-rs/exec-server/tests/common/mod.rs index 387edf36db..532af5c3db 100644 --- a/codex-rs/exec-server/tests/common/mod.rs +++ b/codex-rs/exec-server/tests/common/mod.rs @@ -12,6 +12,8 @@ use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_test_binary_support::TestBinaryDispatchGuard; use codex_test_binary_support::TestBinaryDispatchMode; use codex_test_binary_support::configure_test_binary_dispatch; +#[cfg(target_os = "windows")] +use codex_windows_sandbox::CODEX_WINDOWS_SANDBOX_ARG1; use ctor::ctor; pub(crate) mod exec_server; @@ -27,6 +29,10 @@ pub static TEST_BINARY_DISPATCH_GUARD: Option = { if argv1 == Some(CODEX_FS_HELPER_ARG1) { return TestBinaryDispatchMode::DispatchArg0Only; } + #[cfg(target_os = "windows")] + if argv1 == Some(CODEX_WINDOWS_SANDBOX_ARG1) { + return TestBinaryDispatchMode::DispatchArg0Only; + } if exe_name == CODEX_LINUX_SANDBOX_ARG0 { return TestBinaryDispatchMode::DispatchArg0Only; } diff --git a/codex-rs/exec-server/tests/windows_file_system_sandbox.rs b/codex-rs/exec-server/tests/windows_file_system_sandbox.rs new file mode 100644 index 0000000000..568448a7ef --- /dev/null +++ b/codex-rs/exec-server/tests/windows_file_system_sandbox.rs @@ -0,0 +1,300 @@ +#![cfg(target_os = "windows")] + +mod common; + +use anyhow::Context; +use anyhow::Result; +use codex_exec_server::ExecServerRuntimePaths; +use codex_exec_server::ExecutorFileSystem; +use codex_exec_server::FileSystemSandboxContext; +use codex_exec_server::LocalFileSystem; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_utils_absolute_path::AbsolutePathBuf; +use pretty_assertions::assert_eq; +use serial_test::serial; +use std::path::Path; +use tempfile::TempDir; + +struct EnvVarGuard { + key: &'static str, + original: Option, +} + +impl EnvVarGuard { + fn set(key: &'static str, value: &std::ffi::OsStr) -> Self { + let original = std::env::var_os(key); + unsafe { + std::env::set_var(key, value); + } + Self { key, original } + } +} + +impl Drop for EnvVarGuard { + fn drop(&mut self) { + unsafe { + match &self.original { + Some(value) => std::env::set_var(self.key, value), + None => std::env::remove_var(self.key), + } + } + } +} + +struct SandboxedFileSystem { + _codex_home: TempDir, + _guard: EnvVarGuard, + file_system: LocalFileSystem, +} + +fn sandboxed_file_system() -> Result { + let (codex_exe, _) = common::current_test_binary_helper_paths()?; + sandboxed_file_system_with_codex_exe(codex_exe) +} + +fn sandboxed_file_system_with_codex_exe( + codex_exe: std::path::PathBuf, +) -> Result { + let codex_home = TempDir::new()?; + let guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); + let file_system = LocalFileSystem::with_runtime_paths(ExecServerRuntimePaths::new( + codex_exe, /*codex_linux_sandbox_exe*/ None, + )?); + + Ok(SandboxedFileSystem { + _codex_home: codex_home, + _guard: guard, + file_system, + }) +} + +fn process_is_elevated() -> bool { + let status = std::process::Command::new("powershell.exe") + .args([ + "-NoProfile", + "-NonInteractive", + "-Command", + "$principal = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent(); if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 } else { exit 1 }", + ]) + .status(); + status.is_ok_and(|status| status.success()) +} + +fn stage_windows_sandbox_helpers(codex_home: &Path) -> Result { + let helper_dir = codex_home.join(".sandbox-bin"); + std::fs::create_dir_all(&helper_dir) + .with_context(|| format!("create helper dir {}", helper_dir.display()))?; + for helper_name in ["codex-windows-sandbox-setup", "codex-command-runner"] { + let helper = match codex_utils_cargo_bin::cargo_bin(helper_name) { + Ok(helper) => helper, + Err(err) => { + eprintln!("skipping elevated fs-helper test: {helper_name} unavailable: {err}"); + return Ok(false); + } + }; + std::fs::copy( + &helper, + helper_dir.join(Path::new(helper_name).with_extension("exe")), + ) + .with_context(|| format!("stage Windows sandbox helper {}", helper.display()))?; + } + Ok(true) +} + +fn temp_workspace() -> Result<(TempDir, AbsolutePathBuf)> { + let dir = TempDir::new()?; + let path = AbsolutePathBuf::try_from(std::fs::canonicalize(dir.path())?)?; + Ok((dir, path)) +} + +fn root_read_entry() -> FileSystemSandboxEntry { + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + } +} + +fn path_entry(path: AbsolutePathBuf, access: FileSystemAccessMode) -> FileSystemSandboxEntry { + FileSystemSandboxEntry { + path: FileSystemPath::Path { path }, + access, + } +} + +fn sandbox_from_entries( + workspace: AbsolutePathBuf, + windows_sandbox_level: WindowsSandboxLevel, + entries: Vec, +) -> FileSystemSandboxContext { + let file_system_policy = FileSystemSandboxPolicy::restricted(entries); + let permissions = PermissionProfile::from_runtime_permissions( + &file_system_policy, + NetworkSandboxPolicy::Restricted, + ); + let mut sandbox = + FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, workspace); + sandbox.windows_sandbox_level = windows_sandbox_level; + sandbox +} + +fn workspace_write_sandbox( + workspace: AbsolutePathBuf, + windows_sandbox_level: WindowsSandboxLevel, +) -> FileSystemSandboxContext { + sandbox_from_entries( + workspace.clone(), + windows_sandbox_level, + vec![ + root_read_entry(), + path_entry(workspace, FileSystemAccessMode::Write), + ], + ) +} + +fn workspace_write_sandbox_with_read_only_path( + workspace: AbsolutePathBuf, + read_only_path: AbsolutePathBuf, + windows_sandbox_level: WindowsSandboxLevel, +) -> FileSystemSandboxContext { + sandbox_from_entries( + workspace.clone(), + windows_sandbox_level, + vec![ + root_read_entry(), + path_entry(workspace, FileSystemAccessMode::Write), + path_entry(read_only_path, FileSystemAccessMode::Read), + ], + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial(codex_home)] +async fn restricted_token_fs_helper_uses_configured_helper_path() -> Result<()> { + let helper_dir = TempDir::new()?; + let current_exe = std::env::current_exe()?; + let configured_helper = helper_dir.path().join("configured-codex-helper.exe"); + std::fs::copy(¤t_exe, &configured_helper)?; + let sandboxed = sandboxed_file_system_with_codex_exe(configured_helper.clone())?; + let (_workspace_dir, workspace) = temp_workspace()?; + let file_path = workspace.join("allowed.txt"); + let sandbox = workspace_write_sandbox(workspace, WindowsSandboxLevel::RestrictedToken); + + sandboxed + .file_system + .write_file(&file_path, b"allowed".to_vec(), Some(&sandbox)) + .await?; + + let materialized_helper = sandboxed._codex_home.path().join(".sandbox-bin").join( + configured_helper + .file_name() + .expect("configured helper name"), + ); + assert_eq!(std::fs::read(&file_path)?, b"allowed"); + assert_eq!(materialized_helper.exists(), true); + assert_eq!( + materialized_helper.file_name(), + configured_helper.file_name() + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial(codex_home)] +async fn restricted_token_fs_helper_rejects_missing_read_only_carveout() -> Result<()> { + let sandboxed = sandboxed_file_system()?; + let (_workspace_dir, workspace) = temp_workspace()?; + let protected_dir = workspace.join("protected"); + let blocked_path = protected_dir.join("blocked.txt"); + let sandbox = workspace_write_sandbox_with_read_only_path( + workspace, + protected_dir, + WindowsSandboxLevel::RestrictedToken, + ); + + let error = sandboxed + .file_system + .write_file(&blocked_path, b"blocked".to_vec(), Some(&sandbox)) + .await + .expect_err("read-only carveout should reject writes even before it exists"); + assert!( + error.to_string().contains("Access is denied") + || error.to_string().contains("Permission denied") + || error.to_string().contains("Operation not permitted") + || error.to_string().contains("is not permitted"), + "unexpected sandbox denial message: {error}", + ); + assert_eq!(blocked_path.exists(), false); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial(codex_home)] +async fn elevated_fs_helper_rejects_unenforceable_reopened_writable_descendant() -> Result<()> { + let sandboxed = sandboxed_file_system()?; + let (_workspace_dir, workspace) = temp_workspace()?; + let read_only_dir = workspace.join("read-only"); + let reopened_dir = read_only_dir.join("writable"); + let target_path = reopened_dir.join("blocked.txt"); + let sandbox = sandbox_from_entries( + workspace.clone(), + WindowsSandboxLevel::Elevated, + vec![ + root_read_entry(), + path_entry(workspace, FileSystemAccessMode::Write), + path_entry(read_only_dir, FileSystemAccessMode::Read), + path_entry(reopened_dir, FileSystemAccessMode::Write), + ], + ); + + let error = sandboxed + .file_system + .write_file(&target_path, b"blocked".to_vec(), Some(&sandbox)) + .await + .expect_err("elevated fs helper should reject unenforceable reopened descendants"); + + assert!( + error.to_string().contains( + "windows elevated sandbox cannot reopen writable descendants under read-only carveouts" + ), + "unexpected elevated sandbox error: {error}", + ); + assert_eq!(target_path.exists(), false); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial(codex_home)] +async fn elevated_fs_helper_allows_writable_path_when_setup_is_available() -> Result<()> { + if !process_is_elevated() { + eprintln!("skipping elevated fs-helper test: test process is not elevated"); + return Ok(()); + } + + let sandboxed = sandboxed_file_system()?; + if !stage_windows_sandbox_helpers(sandboxed._codex_home.path())? { + return Ok(()); + } + + let (_workspace_dir, workspace) = temp_workspace()?; + let file_path = workspace.join("allowed-elevated.txt"); + let sandbox = workspace_write_sandbox(workspace, WindowsSandboxLevel::Elevated); + + sandboxed + .file_system + .write_file(&file_path, b"allowed elevated".to_vec(), Some(&sandbox)) + .await?; + + assert_eq!(std::fs::read(&file_path)?, b"allowed elevated"); + Ok(()) +}