diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index 0b639b7d5c..aa82ea9513 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -76,10 +76,8 @@ impl WindowsSandboxRequestProcessor { Ok(config) => { let setup_request = WindowsSandboxSetupRequest { mode, - policy: config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), - policy_cwd: config.cwd.to_path_buf(), + permission_profile: config.permissions.effective_permission_profile(), + workspace_roots: config.effective_workspace_roots(), command_cwd, env_map: std::env::vars().collect(), codex_home: config.codex_home.to_path_buf(), diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 4833ff2c6d..ebbc633582 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -197,10 +197,11 @@ async fn run_command_under_sandbox( // In practice, this should be `std::env::current_dir()` because this CLI // does not support `--cwd`, but let's use the config value for consistency. let cwd = config.cwd.clone(); - // For now, we always use the same cwd for both the command and the - // sandbox policy. In the future, we could add a CLI option to set them - // separately. + // Non-Windows sandbox launchers still materialize profile-relative entries + // against the command cwd. let sandbox_policy_cwd = cwd.clone(); + #[cfg(target_os = "windows")] + let workspace_roots = config.effective_workspace_roots(); let env = create_env( &config.permissions.shell_environment_policy, @@ -211,7 +212,7 @@ async fn run_command_under_sandbox( if let SandboxType::Windows = sandbox_type { #[cfg(target_os = "windows")] { - run_command_under_windows_session(&config, command, cwd, sandbox_policy_cwd, env).await; + run_command_under_windows_session(&config, command, cwd, workspace_roots, env).await; } #[cfg(not(target_os = "windows"))] { @@ -338,24 +339,15 @@ async fn run_command_under_windows_session( config: &Config, command: Vec, cwd: AbsolutePathBuf, - sandbox_policy_cwd: AbsolutePathBuf, + workspace_roots: Vec, env: std::collections::HashMap, ) -> ! { use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_protocol::config_types::WindowsSandboxLevel; - use codex_windows_sandbox::spawn_windows_sandbox_session_elevated; + use codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile; use codex_windows_sandbox::spawn_windows_sandbox_session_legacy; - let sandbox_policy = config - .permissions - .legacy_sandbox_policy(sandbox_policy_cwd.as_path()); - let policy_str = match serde_json::to_string(&sandbox_policy) { - Ok(policy_str) => policy_str, - Err(err) => { - eprintln!("windows sandbox failed to serialize policy: {err}"); - std::process::exit(1); - } - }; + let permission_profile = config.permissions.effective_permission_profile(); let use_elevated = matches!( WindowsSandboxLevel::from_config(config), @@ -363,9 +355,9 @@ async fn run_command_under_windows_session( ); let spawned = if use_elevated { - spawn_windows_sandbox_session_elevated( - policy_str.as_str(), - sandbox_policy_cwd.as_path(), + spawn_windows_sandbox_session_elevated_for_permission_profile( + &permission_profile, + workspace_roots.as_slice(), config.codex_home.as_path(), command, cwd.as_path(), @@ -383,8 +375,8 @@ async fn run_command_under_windows_session( .await } else { spawn_windows_sandbox_session_legacy( - policy_str.as_str(), - sandbox_policy_cwd.as_path(), + &permission_profile, + workspace_roots.as_slice(), config.codex_home.as_path(), command, cwd.as_path(), @@ -1020,7 +1012,7 @@ mod tests { } #[tokio::test] - async fn debug_sandbox_uses_explicit_profile_cwd() -> anyhow::Result<()> { + async fn debug_sandbox_uses_explicit_cwd() -> anyhow::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 84ebe337c6..5a1046ee35 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -387,7 +387,13 @@ pub fn build_exec_request( .map(|request| { let windows_sandbox_policy_cwd = AbsolutePathBuf::try_from(sandbox_cwd.to_path_buf()) .unwrap_or_else(|_| request.cwd.clone()); - ExecRequest::from_sandbox_exec_request(request, options, windows_sandbox_policy_cwd) + let windows_sandbox_workspace_roots = vec![windows_sandbox_policy_cwd.clone()]; + ExecRequest::from_sandbox_exec_request( + request, + options, + windows_sandbox_policy_cwd, + windows_sandbox_workspace_roots, + ) }) .map_err(CodexErr::from)?; let use_windows_elevated_backend = windows_sandbox_uses_elevated_backend( @@ -423,7 +429,6 @@ pub(crate) async fn execute_exec_request( stdout_stream: Option, after_spawn: Option>, ) -> Result { - let sandbox_policy = exec_request.compatibility_sandbox_policy(); let ExecRequest { command, cwd, @@ -434,6 +439,7 @@ pub(crate) async fn execute_exec_request( capture_policy, sandbox, windows_sandbox_policy_cwd, + windows_sandbox_workspace_roots, windows_sandbox_level, windows_sandbox_private_desktop, permission_profile, @@ -464,9 +470,9 @@ pub(crate) async fn execute_exec_request( stdout_stream, after_spawn, sandbox, - &sandbox_policy, &permission_profile, &windows_sandbox_policy_cwd, + &windows_sandbox_workspace_roots, windows_sandbox_filesystem_overrides.as_ref(), ) .await; @@ -481,9 +487,10 @@ async fn get_raw_output_result( stdout_stream: Option, after_spawn: Option>, #[cfg_attr(not(windows), allow(unused_variables))] sandbox: SandboxType, - #[cfg_attr(not(windows), allow(unused_variables))] sandbox_policy: &SandboxPolicy, #[cfg_attr(not(windows), allow(unused_variables))] permission_profile: &PermissionProfile, #[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_policy_cwd: &AbsolutePathBuf, + #[cfg_attr(not(windows), allow(unused_variables))] + windows_sandbox_workspace_roots: &[AbsolutePathBuf], #[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_filesystem_overrides: Option< &WindowsSandboxFilesystemOverrides, >, @@ -492,9 +499,9 @@ async fn get_raw_output_result( if sandbox == SandboxType::WindowsRestrictedToken { return exec_windows_sandbox( params, - sandbox_policy, permission_profile, windows_sandbox_policy_cwd, + windows_sandbox_workspace_roots, windows_sandbox_filesystem_overrides, ) .await; @@ -572,9 +579,9 @@ fn record_windows_sandbox_spawn_failure( #[cfg(target_os = "windows")] async fn exec_windows_sandbox( params: ExecParams, - sandbox_policy: &SandboxPolicy, permission_profile: &PermissionProfile, windows_sandbox_policy_cwd: &AbsolutePathBuf, + windows_sandbox_workspace_roots: &[AbsolutePathBuf], windows_sandbox_filesystem_overrides: Option<&WindowsSandboxFilesystemOverrides>, ) -> Result { use crate::config::find_codex_home; @@ -604,12 +611,11 @@ async fn exec_windows_sandbox( None }; - let policy_str = serde_json::to_string(sandbox_policy).map_err(|err| { - CodexErr::Io(io::Error::other(format!( - "failed to serialize Windows sandbox policy: {err}" - ))) - })?; - let sandbox_cwd = windows_sandbox_policy_cwd.clone(); + let workspace_roots = if windows_sandbox_workspace_roots.is_empty() { + vec![windows_sandbox_policy_cwd.clone()] + } else { + windows_sandbox_workspace_roots.to_vec() + }; let permission_profile = permission_profile.clone(); let codex_home = find_codex_home().map_err(|err| { CodexErr::Io(io::Error::other(format!( @@ -637,7 +643,7 @@ async fn exec_windows_sandbox( run_windows_sandbox_capture_for_permission_profile_elevated( codex_windows_sandbox::ElevatedSandboxProfileCaptureRequest { permission_profile: &permission_profile, - permission_profile_cwd: &sandbox_cwd, + workspace_roots: workspace_roots.as_slice(), codex_home: codex_home.as_ref(), command, cwd: &cwd, @@ -655,8 +661,8 @@ async fn exec_windows_sandbox( ) } else { run_windows_sandbox_capture_with_filesystem_overrides( - policy_str.as_str(), - &sandbox_cwd, + &permission_profile, + workspace_roots.as_slice(), codex_home.as_ref(), command, &cwd, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 5070d8da3a..45f885c897 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -52,6 +52,7 @@ pub struct ExecRequest { pub capture_policy: ExecCapturePolicy, pub sandbox: SandboxType, pub windows_sandbox_policy_cwd: AbsolutePathBuf, + pub windows_sandbox_workspace_roots: Vec, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, pub permission_profile: PermissionProfile, @@ -77,6 +78,7 @@ impl ExecRequest { arg0: Option, ) -> Self { let windows_sandbox_policy_cwd = cwd.clone(); + let windows_sandbox_workspace_roots = vec![cwd.clone()]; let (file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); Self { @@ -89,6 +91,7 @@ impl ExecRequest { capture_policy, sandbox, windows_sandbox_policy_cwd, + windows_sandbox_workspace_roots, windows_sandbox_level, windows_sandbox_private_desktop, permission_profile, @@ -112,6 +115,7 @@ impl ExecRequest { request: SandboxExecRequest, options: ExecOptions, windows_sandbox_policy_cwd: AbsolutePathBuf, + windows_sandbox_workspace_roots: Vec, ) -> Self { let SandboxExecRequest { command, @@ -150,6 +154,7 @@ impl ExecRequest { capture_policy, sandbox, windows_sandbox_policy_cwd, + windows_sandbox_workspace_roots, windows_sandbox_level, windows_sandbox_private_desktop, permission_profile, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 23f3882eda..edc08044b4 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -192,6 +192,7 @@ pub(crate) async fn execute_user_shell_command( capture_policy: ExecCapturePolicy::ShellTool, sandbox: SandboxType::None, windows_sandbox_policy_cwd: cwd.clone(), + windows_sandbox_workspace_roots: turn_context.config.effective_workspace_roots(), windows_sandbox_level: turn_context.windows_sandbox_level, windows_sandbox_private_desktop: turn_context .config diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index deb9ae596e..dc9f3f33b5 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -85,6 +85,7 @@ impl ToolOrchestrator { enforce_managed_network: attempt.enforce_managed_network, manager: attempt.manager, sandbox_cwd: attempt.sandbox_cwd, + workspace_roots: attempt.workspace_roots, codex_linux_sandbox_exe: attempt.codex_linux_sandbox_exe, use_legacy_landlock: attempt.use_legacy_landlock, windows_sandbox_level: attempt.windows_sandbox_level, @@ -236,12 +237,14 @@ impl ToolOrchestrator { let use_legacy_landlock = turn_ctx.features.use_legacy_landlock(); #[allow(deprecated)] let sandbox_cwd = tool.sandbox_cwd(req).unwrap_or(&turn_ctx.cwd); + let workspace_roots = turn_ctx.config.effective_workspace_roots(); let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, permissions: &turn_ctx.permission_profile, enforce_managed_network: managed_network_active, manager: &self.sandbox, sandbox_cwd, + workspace_roots: workspace_roots.as_slice(), codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(), use_legacy_landlock, windows_sandbox_level: turn_ctx.windows_sandbox_level, @@ -360,6 +363,7 @@ impl ToolOrchestrator { enforce_managed_network: managed_network_active, manager: &self.sandbox, sandbox_cwd, + workspace_roots: workspace_roots.as_slice(), codex_linux_sandbox_exe: None, use_legacy_landlock, windows_sandbox_level: turn_ctx.windows_sandbox_level, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index f4d8d461b2..0a741861fc 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -212,6 +212,7 @@ async fn file_system_sandbox_context_uses_active_attempt() { enforce_managed_network: false, manager: &manager, sandbox_cwd: &path, + workspace_roots: std::slice::from_ref(&path), codex_linux_sandbox_exe: None, use_legacy_landlock: true, windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, @@ -265,6 +266,7 @@ async fn no_sandbox_attempt_has_no_file_system_context() { enforce_managed_network: false, manager: &manager, sandbox_cwd: &path, + workspace_roots: std::slice::from_ref(&path), codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 539bbb90de..9a473cd25f 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -111,6 +111,7 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow:: enforce_managed_network: false, manager: &manager, sandbox_cwd: &cwd, + workspace_roots: std::slice::from_ref(&cwd), codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 597ab78dbe..554cb0e43c 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -139,6 +139,7 @@ pub(super) async fn try_run_zsh_fork( capture_policy: _capture_policy, sandbox, windows_sandbox_policy_cwd: sandbox_policy_cwd, + windows_sandbox_workspace_roots: _windows_sandbox_workspace_roots, windows_sandbox_level, windows_sandbox_private_desktop: _windows_sandbox_private_desktop, permission_profile, @@ -794,6 +795,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { capture_policy: ExecCapturePolicy::ShellTool, sandbox: self.sandbox, windows_sandbox_policy_cwd: self.sandbox_policy_cwd.clone(), + windows_sandbox_workspace_roots: vec![self.sandbox_policy_cwd.clone()], windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, permission_profile: self.permission_profile.clone(), @@ -931,6 +933,7 @@ impl CoreShellCommandExecutor { exec_request, options, self.sandbox_policy_cwd.clone(), + vec![self.sandbox_policy_cwd.clone()], ); if let Some(network) = exec_request.network.as_ref() { network.apply_to_env(&mut exec_request.env); diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 1ca589a5f9..90060fed50 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -383,6 +383,7 @@ pub(crate) struct SandboxAttempt<'a> { pub enforce_managed_network: bool, pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a AbsolutePathBuf, + pub(crate) workspace_roots: &'a [AbsolutePathBuf], pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, pub use_legacy_landlock: bool, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, @@ -422,6 +423,7 @@ impl<'a> SandboxAttempt<'a> { request, options, windows_sandbox_policy_cwd, + self.workspace_roots.to_vec(), ) }) } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index fb99500755..83c750ebc4 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -866,12 +866,6 @@ impl UnifiedExecProcessManager { #[cfg(target_os = "windows")] if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken { - let sandbox_policy = request.compatibility_sandbox_policy(); - let policy_json = serde_json::to_string(&sandbox_policy).map_err(|err| { - UnifiedExecError::create_process(format!( - "failed to serialize Windows sandbox policy: {err}" - )) - })?; let codex_home = crate::config::find_codex_home().map_err(|err| { UnifiedExecError::create_process(format!( "windows sandbox: failed to resolve codex_home: {err}" @@ -903,7 +897,7 @@ impl UnifiedExecProcessManager { codex_protocol::config_types::WindowsSandboxLevel::Elevated => { codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile( &request.permission_profile, - request.windows_sandbox_policy_cwd.as_path(), + request.windows_sandbox_workspace_roots.as_slice(), codex_home.as_ref(), request.command.clone(), request.cwd.as_path(), @@ -923,8 +917,8 @@ impl UnifiedExecProcessManager { codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken | codex_protocol::config_types::WindowsSandboxLevel::Disabled => { codex_windows_sandbox::spawn_windows_sandbox_session_legacy( - policy_json.as_str(), - request.windows_sandbox_policy_cwd.as_path(), + &request.permission_profile, + request.windows_sandbox_workspace_roots.as_slice(), codex_home.as_ref(), request.command.clone(), request.cwd.as_path(), diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index cd83ce7a9c..4d0597a844 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -100,7 +100,8 @@ fn exec_server_params_use_env_policy_overlay_contract() { expiration: crate::exec::ExecExpiration::DefaultTimeout, capture_policy: crate::exec::ExecCapturePolicy::ShellTool, sandbox: codex_sandboxing::SandboxType::None, - windows_sandbox_policy_cwd: cwd, + windows_sandbox_policy_cwd: cwd.clone(), + windows_sandbox_workspace_roots: vec![cwd], windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, permission_profile, diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 1494ce262e..19b404fb71 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -8,7 +8,8 @@ use codex_features::FeaturesToml; use codex_login::default_client::originator; use codex_otel::sanitize_metric_tag_value; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; @@ -145,16 +146,17 @@ pub fn elevated_setup_failure_metric_name(_err: &anyhow::Error) -> &'static str #[cfg(target_os = "windows")] pub fn run_elevated_setup( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], command_cwd: &Path, env_map: &HashMap, codex_home: &Path, ) -> anyhow::Result<()> { let permissions = - codex_windows_sandbox::ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd( - policy, policy_cwd, - ); + codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + )?; codex_windows_sandbox::run_elevated_setup( codex_windows_sandbox::SandboxSetupRequest { permissions: &permissions, @@ -169,8 +171,8 @@ pub fn run_elevated_setup( #[cfg(not(target_os = "windows"))] pub fn run_elevated_setup( - _policy: &SandboxPolicy, - _policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _workspace_roots: &[AbsolutePathBuf], _command_cwd: &Path, _env_map: &HashMap, _codex_home: &Path, @@ -180,15 +182,15 @@ pub fn run_elevated_setup( #[cfg(target_os = "windows")] pub fn run_legacy_setup_preflight( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], command_cwd: &Path, env_map: &HashMap, codex_home: &Path, ) -> anyhow::Result<()> { codex_windows_sandbox::run_windows_sandbox_legacy_preflight( - policy, - policy_cwd, + permission_profile, + workspace_roots, codex_home, command_cwd, env_map, @@ -197,16 +199,16 @@ pub fn run_legacy_setup_preflight( #[cfg(target_os = "windows")] pub fn run_setup_refresh_with_extra_read_roots( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], command_cwd: &Path, env_map: &HashMap, codex_home: &Path, extra_read_roots: Vec, ) -> anyhow::Result<()> { codex_windows_sandbox::run_setup_refresh_with_extra_read_roots( - policy, - policy_cwd, + permission_profile, + workspace_roots, command_cwd, env_map, codex_home, @@ -217,8 +219,8 @@ pub fn run_setup_refresh_with_extra_read_roots( #[cfg(not(target_os = "windows"))] pub fn run_legacy_setup_preflight( - _policy: &SandboxPolicy, - _policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _workspace_roots: &[AbsolutePathBuf], _command_cwd: &Path, _env_map: &HashMap, _codex_home: &Path, @@ -228,8 +230,8 @@ pub fn run_legacy_setup_preflight( #[cfg(not(target_os = "windows"))] pub fn run_setup_refresh_with_extra_read_roots( - _policy: &SandboxPolicy, - _policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _workspace_roots: &[AbsolutePathBuf], _command_cwd: &Path, _env_map: &HashMap, _codex_home: &Path, @@ -247,8 +249,8 @@ pub enum WindowsSandboxSetupMode { #[derive(Debug, Clone)] pub struct WindowsSandboxSetupRequest { pub mode: WindowsSandboxSetupMode, - pub policy: SandboxPolicy, - pub policy_cwd: PathBuf, + pub permission_profile: PermissionProfile, + pub workspace_roots: Vec, pub command_cwd: PathBuf, pub env_map: HashMap, pub codex_home: PathBuf, @@ -285,8 +287,8 @@ async fn run_windows_sandbox_setup_and_persist( request: WindowsSandboxSetupRequest, ) -> anyhow::Result<()> { let mode = request.mode; - let policy = request.policy; - let policy_cwd = request.policy_cwd; + let permission_profile = request.permission_profile; + let workspace_roots = request.workspace_roots; let command_cwd = request.command_cwd; let env_map = request.env_map; let codex_home = request.codex_home; @@ -297,8 +299,8 @@ async fn run_windows_sandbox_setup_and_persist( WindowsSandboxSetupMode::Elevated => { if !sandbox_setup_is_complete(setup_codex_home.as_path()) { run_elevated_setup( - &policy, - policy_cwd.as_path(), + &permission_profile, + workspace_roots.as_slice(), command_cwd.as_path(), &env_map, setup_codex_home.as_path(), @@ -307,8 +309,8 @@ async fn run_windows_sandbox_setup_and_persist( } WindowsSandboxSetupMode::Unelevated => { run_legacy_setup_preflight( - &policy, - policy_cwd.as_path(), + &permission_profile, + workspace_roots.as_slice(), command_cwd.as_path(), &env_map, setup_codex_home.as_path(), diff --git a/codex-rs/core/src/windows_sandbox_read_grants.rs b/codex-rs/core/src/windows_sandbox_read_grants.rs index b0891ec6a6..af87b6e6bc 100644 --- a/codex-rs/core/src/windows_sandbox_read_grants.rs +++ b/codex-rs/core/src/windows_sandbox_read_grants.rs @@ -1,13 +1,14 @@ use crate::windows_sandbox::run_setup_refresh_with_extra_read_roots; use anyhow::Result; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; pub fn grant_read_root_non_elevated( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], command_cwd: &Path, env_map: &HashMap, codex_home: &Path, @@ -25,8 +26,8 @@ pub fn grant_read_root_non_elevated( let canonical_root = dunce::canonicalize(read_root)?; run_setup_refresh_with_extra_read_roots( - policy, - policy_cwd, + permission_profile, + workspace_roots, command_cwd, env_map, codex_home, diff --git a/codex-rs/core/src/windows_sandbox_read_grants_tests.rs b/codex-rs/core/src/windows_sandbox_read_grants_tests.rs index bffa840ede..edef9d4a19 100644 --- a/codex-rs/core/src/windows_sandbox_read_grants_tests.rs +++ b/codex-rs/core/src/windows_sandbox_read_grants_tests.rs @@ -1,19 +1,25 @@ use super::grant_read_root_non_elevated; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; -fn policy() -> SandboxPolicy { - SandboxPolicy::new_workspace_write_policy() +fn permission_profile() -> PermissionProfile { + PermissionProfile::workspace_write() +} + +fn workspace_roots_for(root: &Path) -> Vec { + vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")] } #[test] fn rejects_relative_path() { let tmp = TempDir::new().expect("tempdir"); + let workspace_roots = workspace_roots_for(tmp.path()); let err = grant_read_root_non_elevated( - &policy(), - tmp.path(), + &permission_profile(), + workspace_roots.as_slice(), tmp.path(), &HashMap::new(), tmp.path(), @@ -27,9 +33,10 @@ fn rejects_relative_path() { fn rejects_missing_path() { let tmp = TempDir::new().expect("tempdir"); let missing = tmp.path().join("does-not-exist"); + let workspace_roots = workspace_roots_for(tmp.path()); let err = grant_read_root_non_elevated( - &policy(), - tmp.path(), + &permission_profile(), + workspace_roots.as_slice(), tmp.path(), &HashMap::new(), tmp.path(), @@ -44,9 +51,10 @@ fn rejects_file_path() { let tmp = TempDir::new().expect("tempdir"); let file_path = tmp.path().join("file.txt"); std::fs::write(&file_path, "hello").expect("write file"); + let workspace_roots = workspace_roots_for(tmp.path()); let err = grant_read_root_non_elevated( - &policy(), - tmp.path(), + &permission_profile(), + workspace_roots.as_slice(), tmp.path(), &HashMap::new(), tmp.path(), diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 3c3d15086f..d37cb8819c 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1040,11 +1040,13 @@ See the Codex keymap documentation for supported actions and examples." .unwrap_or(false); if should_check { let cwd = app.config.cwd.clone(); + let workspace_roots = app.config.effective_workspace_roots(); let env_map: std::collections::HashMap = std::env::vars().collect(); let tx = app.app_event_tx.clone(); let logs_base_dir = app.config.codex_home.clone(); Self::spawn_world_writable_scan( cwd, + workspace_roots, env_map, logs_base_dir, startup_permission_profile, diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index e83662c2a2..8c80230cdf 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -8,6 +8,12 @@ use super::*; #[cfg(target_os = "windows")] use codex_utils_approval_presets::ApprovalPreset; +#[cfg(target_os = "windows")] +pub(super) struct WindowsSetupPermissions { + pub(super) permission_profile: PermissionProfile, + pub(super) workspace_roots: Vec, +} + impl App { pub(super) async fn rebuild_config_for_cwd(&self, cwd: PathBuf) -> Result { let mut overrides = self.harness_overrides.clone(); @@ -45,19 +51,25 @@ impl App { } #[cfg(target_os = "windows")] - pub(super) async fn permission_profile_for_windows_setup( + pub(super) async fn windows_setup_permissions( &self, preset: &ApprovalPreset, profile_selection: Option<&PermissionProfileSelection>, - ) -> Result { + ) -> Result { match profile_selection { - Some(selection) => Ok(self - .rebuild_config_for_permission_profile(selection.profile_id.as_str()) - .await? - .permissions - .permission_profile() - .clone()), - None => Ok(preset.permission_profile.clone()), + Some(selection) => { + let selected_config = self + .rebuild_config_for_permission_profile(selection.profile_id.as_str()) + .await?; + Ok(WindowsSetupPermissions { + permission_profile: selected_config.permissions.permission_profile().clone(), + workspace_roots: selected_config.effective_workspace_roots(), + }) + } + None => Ok(WindowsSetupPermissions { + permission_profile: preset.permission_profile.clone(), + workspace_roots: self.config.effective_workspace_roots(), + }), } } diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index e150dbccd2..2752953b61 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -887,11 +887,11 @@ impl App { } => { #[cfg(target_os = "windows")] { - let permission_profile = match self - .permission_profile_for_windows_setup(&preset, profile_selection.as_ref()) + let setup_permissions = match self + .windows_setup_permissions(&preset, profile_selection.as_ref()) .await { - Ok(permission_profile) => permission_profile, + Ok(setup_permissions) => setup_permissions, Err(err) => { tracing::warn!( error = %err, @@ -903,8 +903,9 @@ impl App { return Ok(AppRunControl::Continue); } }; - let policy_cwd = self.config.cwd.clone(); - let command_cwd = policy_cwd.clone(); + let permission_profile = setup_permissions.permission_profile; + let workspace_roots = setup_permissions.workspace_roots; + let command_cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); let codex_home = self.config.codex_home.clone(); @@ -926,25 +927,10 @@ impl App { self.chat_widget.show_windows_sandbox_setup_status(); self.windows_sandbox.setup_started_at = Some(Instant::now()); let session_telemetry = self.session_telemetry.clone(); - let Ok(policy) = permission_profile - .to_legacy_sandbox_policy(policy_cwd.as_path()) - .inspect_err(|err| { - tracing::error!( - %err, - "approval preset permissions cannot be projected for elevated Windows sandbox setup" - ); - }) - else { - tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { - preset, - profile_selection, - }); - return Ok(AppRunControl::Continue); - }; tokio::task::spawn_blocking(move || { let result = crate::legacy_core::windows_sandbox::run_elevated_setup( - &policy, - policy_cwd.as_path(), + &permission_profile, + workspace_roots.as_slice(), command_cwd.as_path(), &env_map, codex_home.as_path(), @@ -1011,11 +997,11 @@ impl App { } => { #[cfg(target_os = "windows")] { - let permission_profile = match self - .permission_profile_for_windows_setup(&preset, profile_selection.as_ref()) + let setup_permissions = match self + .windows_setup_permissions(&preset, profile_selection.as_ref()) .await { - Ok(permission_profile) => permission_profile, + Ok(setup_permissions) => setup_permissions, Err(err) => { tracing::warn!( error = %err, @@ -1027,8 +1013,9 @@ impl App { return Ok(AppRunControl::Continue); } }; - let policy_cwd = self.config.cwd.clone(); - let command_cwd = policy_cwd.clone(); + let permission_profile = setup_permissions.permission_profile; + let workspace_roots = setup_permissions.workspace_roots; + let command_cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); let codex_home = self.config.codex_home.clone(); @@ -1036,26 +1023,11 @@ impl App { let session_telemetry = self.session_telemetry.clone(); self.chat_widget.show_windows_sandbox_setup_status(); - let Ok(policy) = permission_profile - .to_legacy_sandbox_policy(policy_cwd.as_path()) - .inspect_err(|err| { - tracing::error!( - %err, - "approval preset permissions cannot be projected for legacy Windows sandbox setup" - ); - }) - else { - tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { - preset, - profile_selection, - }); - return Ok(AppRunControl::Continue); - }; tokio::task::spawn_blocking(move || { if let Err(err) = crate::legacy_core::windows_sandbox::run_legacy_setup_preflight( - &policy, - policy_cwd.as_path(), + &permission_profile, + workspace_roots.as_slice(), command_cwd.as_path(), &env_map, codex_home.as_path(), @@ -1092,11 +1064,8 @@ impl App { /*hint*/ None, )); - let policy = self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()); - let policy_cwd = self.config.cwd.clone(); + let permission_profile = self.config.permissions.effective_permission_profile(); + let workspace_roots = self.config.effective_workspace_roots(); let command_cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); @@ -1106,8 +1075,8 @@ impl App { tokio::task::spawn_blocking(move || { let requested_path = PathBuf::from(path); let event = match crate::legacy_core::grant_read_root_non_elevated( - &policy, - policy_cwd.as_path(), + &permission_profile, + workspace_roots.as_slice(), command_cwd.as_path(), &env_map, codex_home.as_path(), @@ -1535,6 +1504,7 @@ impl App { && !self.chat_widget.world_writable_warning_hidden(); if should_check { let cwd = self.config.cwd.clone(); + let workspace_roots = self.config.effective_workspace_roots(); let env_map: std::collections::HashMap = std::env::vars().collect(); let tx = self.app_event_tx.clone(); @@ -1543,6 +1513,7 @@ impl App { self.config.permissions.effective_permission_profile(); Self::spawn_world_writable_scan( cwd, + workspace_roots, env_map, logs_base_dir, permission_profile, diff --git a/codex-rs/tui/src/app/platform_actions.rs b/codex-rs/tui/src/app/platform_actions.rs index c87776f48b..f4a708a026 100644 --- a/codex-rs/tui/src/app/platform_actions.rs +++ b/codex-rs/tui/src/app/platform_actions.rs @@ -16,25 +16,31 @@ impl App { #[cfg(target_os = "windows")] pub(super) fn spawn_world_writable_scan( cwd: AbsolutePathBuf, + workspace_roots: Vec, env_map: std::collections::HashMap, logs_base_dir: AbsolutePathBuf, permission_profile: PermissionProfile, tx: AppEventSender, ) { - let Ok(sandbox_policy) = permission_profile.to_legacy_sandbox_policy(cwd.as_path()) else { - send_world_writable_scan_failed(&tx); + let Ok(permissions) = + codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + &permission_profile, + workspace_roots.as_slice(), + ) + else { return; }; tokio::task::spawn_blocking(move || { let logs_base_dir_path = logs_base_dir.as_path(); - let result = codex_windows_sandbox::apply_world_writable_scan_and_denies( - logs_base_dir_path, - cwd.as_path(), - &env_map, - &sandbox_policy, - Some(logs_base_dir_path), - ); + let result = + codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions( + logs_base_dir_path, + cwd.as_path(), + &env_map, + &permissions, + Some(logs_base_dir_path), + ); if result.is_err() { // Scan failed: warn without examples. send_world_writable_scan_failed(&tx); diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index dd90af0557..ebeb23429c 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -14,13 +14,22 @@ impl ChatWidget { return None; } let cwd = self.config.cwd.clone(); + let workspace_roots = self.config.effective_workspace_roots(); let env_map: std::collections::HashMap = std::env::vars().collect(); - let policy = self.config.legacy_sandbox_policy(); - match codex_windows_sandbox::apply_world_writable_scan_and_denies( + let permission_profile = self.config.permissions.effective_permission_profile(); + let Ok(permissions) = + codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + &permission_profile, + workspace_roots.as_slice(), + ) + else { + return None; + }; + match codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions( self.config.codex_home.as_path(), cwd.as_path(), &env_map, - &policy, + &permissions, Some(self.config.codex_home.as_path()), ) { Ok(_) => None, diff --git a/codex-rs/windows-sandbox-rs/src/allow.rs b/codex-rs/windows-sandbox-rs/src/allow.rs index 541a86fc1c..5a10261d0c 100644 --- a/codex-rs/windows-sandbox-rs/src/allow.rs +++ b/codex-rs/windows-sandbox-rs/src/allow.rs @@ -44,19 +44,41 @@ pub(crate) fn compute_allow_paths_for_permissions( #[cfg(test)] mod tests { use super::*; - use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::fs; use tempfile::TempDir; + fn workspace_write_profile( + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + ) -> PermissionProfile { + PermissionProfile::workspace_write_with( + writable_roots, + NetworkSandboxPolicy::Restricted, + exclude_tmpdir_env_var, + exclude_slash_tmp, + ) + } + + fn workspace_roots_for(root: &Path) -> Vec { + vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")] + } + fn compute_allow_paths( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], command_cwd: &Path, env_map: &HashMap, ) -> AllowDenyPaths { let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + ) + .expect("managed permission profile"); compute_allow_paths_for_permissions(&permissions, command_cwd, env_map) } @@ -68,14 +90,20 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::create_dir_all(&extra_root); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; + let writable_roots = vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()]; + let permission_profile = workspace_write_profile( + &writable_roots, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &HashMap::new(), + ); assert!( paths @@ -91,25 +119,30 @@ mod tests { } #[test] - fn uses_policy_cwd_for_legacy_workspace_root() { + fn uses_runtime_workspace_roots_for_workspace_root() { let tmp = TempDir::new().expect("tempdir"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); + let workspace_root = tmp.path().join("workspace"); + let command_cwd = workspace_root.join("subdir"); fs::create_dir_all(&command_cwd).expect("create command cwd"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let workspace_roots = workspace_roots_for(workspace_root.as_path()); - let paths = compute_allow_paths(&policy, &policy_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &HashMap::new(), + ); assert!( paths .allow - .contains(&dunce::canonicalize(&policy_cwd).unwrap()) + .contains(&dunce::canonicalize(&workspace_root).unwrap()) ); assert!( !paths @@ -127,17 +160,22 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::create_dir_all(&temp_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); let mut env_map = HashMap::new(); env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string()); env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string()); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &env_map, + ); assert!( paths @@ -160,17 +198,22 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::create_dir_all(&temp_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); let mut env_map = HashMap::new(); env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string()); env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string()); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &env_map, + ); let expected_allow: HashSet = [ dunce::canonicalize(&command_cwd).unwrap(), @@ -189,14 +232,19 @@ mod tests { let command_cwd = tmp.path().join("workspace"); let _ = fs::create_dir_all(&command_cwd); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -212,14 +260,19 @@ mod tests { let git_dir = command_cwd.join(".git"); let _ = fs::create_dir_all(&git_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -239,14 +292,19 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::write(&git_file, "gitdir: .git/worktrees/example"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -267,14 +325,19 @@ mod tests { let _ = fs::create_dir_all(&codex_dir); let _ = fs::create_dir_all(&agents_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -295,14 +358,19 @@ mod tests { let command_cwd = tmp.path().join("workspace"); let _ = fs::create_dir_all(&command_cwd); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + workspace_roots.as_slice(), + &command_cwd, + &HashMap::new(), + ); assert_eq!(paths.allow.len(), 1); assert!( paths.deny.is_empty(), diff --git a/codex-rs/windows-sandbox-rs/src/audit.rs b/codex-rs/windows-sandbox-rs/src/audit.rs index 0d6f4b5744..58dd15c2c8 100644 --- a/codex-rs/windows-sandbox-rs/src/audit.rs +++ b/codex-rs/windows-sandbox-rs/src/audit.rs @@ -7,7 +7,6 @@ use crate::cap::workspace_write_root_contains_path; use crate::logging::debug_log; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; -use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::setup::effective_write_roots_for_permissions; use crate::token::LocalSid; @@ -218,24 +217,6 @@ pub fn audit_everyone_writable( Ok(Vec::new()) } -pub fn apply_world_writable_scan_and_denies( - codex_home: &Path, - cwd: &Path, - env_map: &std::collections::HashMap, - sandbox_policy: &SandboxPolicy, - logs_base_dir: Option<&Path>, -) -> Result<()> { - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(sandbox_policy, cwd); - apply_world_writable_scan_and_denies_for_permissions( - codex_home, - cwd, - env_map, - &permissions, - logs_base_dir, - ) -} - pub fn apply_world_writable_scan_and_denies_for_permissions( codex_home: &Path, cwd: &Path, diff --git a/codex-rs/windows-sandbox-rs/src/bin/command_runner/win.rs b/codex-rs/windows-sandbox-rs/src/bin/command_runner/win.rs index afdbdd5813..682dc06191 100644 --- a/codex-rs/windows-sandbox-rs/src/bin/command_runner/win.rs +++ b/codex-rs/windows-sandbox-rs/src/bin/command_runner/win.rs @@ -238,7 +238,8 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result { hide_current_user_profile_dir(req.codex_home.as_path()); let token_mode = token_mode_for_permission_profile( &req.permission_profile, - &req.permission_profile_cwd, + &req.workspace_roots, + &req.cwd, &req.env, ) .context("resolve permission profile token mode")?; diff --git a/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs b/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs index 5512ebefb4..efd3299a0f 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated/ipc_framed.rs @@ -11,6 +11,7 @@ use anyhow::Result; use base64::Engine as _; use base64::engine::general_purpose::STANDARD; use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; @@ -25,7 +26,7 @@ use std::path::PathBuf; const MAX_FRAME_LEN: usize = 8 * 1024 * 1024; /// Protocol version shared by the parent process and elevated command runner. -pub const IPC_PROTOCOL_VERSION: u8 = 2; +pub const IPC_PROTOCOL_VERSION: u8 = 3; /// Length-prefixed, JSON-encoded frame. #[derive(Debug, Serialize, Deserialize, Clone)] @@ -60,7 +61,7 @@ pub struct SpawnRequest { pub cwd: PathBuf, pub env: HashMap, pub permission_profile: PermissionProfile, - pub permission_profile_cwd: PathBuf, + pub workspace_roots: Vec, pub codex_home: PathBuf, pub real_codex_home: PathBuf, pub cap_sids: Vec, @@ -197,6 +198,9 @@ mod tests { #[test] fn spawn_request_serializes_permission_profile() { + let workspace_roots = vec![ + AbsolutePathBuf::from_absolute_path("workspace").expect("absolute workspace root"), + ]; let msg = FramedMessage { version: IPC_PROTOCOL_VERSION, message: Message::SpawnRequest { @@ -205,7 +209,7 @@ mod tests { cwd: PathBuf::from(r"C:\workspace"), env: HashMap::new(), permission_profile: PermissionProfile::read_only(), - permission_profile_cwd: PathBuf::from(r"C:\workspace"), + workspace_roots: workspace_roots.clone(), codex_home: PathBuf::from(r"C:\codex"), real_codex_home: PathBuf::from(r"C:\Users\codex"), cap_sids: vec!["S-1-15-3-1024-1".to_string()], @@ -222,15 +226,13 @@ mod tests { assert_eq!("managed", encoded["payload"]["permission_profile"]["type"]); assert_eq!(None, encoded["payload"].get("policy_json_or_preset")); assert_eq!(None, encoded["payload"].get("sandbox_policy_cwd")); + assert_eq!(None, encoded["payload"].get("permission_profile_cwd")); let decoded: FramedMessage = serde_json::from_value(encoded).expect("deserialize"); let Message::SpawnRequest { payload } = decoded.message else { panic!("unexpected message"); }; assert_eq!(PermissionProfile::read_only(), payload.permission_profile); - assert_eq!( - PathBuf::from(r"C:\workspace"), - payload.permission_profile_cwd - ); + assert_eq!(workspace_roots, payload.workspace_roots); } } diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index f8a059918a..3c598a3a90 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -4,26 +4,9 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -pub struct ElevatedSandboxCaptureRequest<'a> { - pub policy_json_or_preset: &'a str, - pub sandbox_policy_cwd: &'a Path, - pub codex_home: &'a Path, - pub command: Vec, - pub cwd: &'a Path, - pub env_map: HashMap, - pub timeout_ms: Option, - pub use_private_desktop: bool, - pub proxy_enforced: bool, - pub read_roots_override: Option<&'a [PathBuf]>, - pub read_roots_include_platform_defaults: bool, - pub write_roots_override: Option<&'a [PathBuf]>, - pub deny_read_paths_override: &'a [AbsolutePathBuf], - pub deny_write_paths_override: &'a [AbsolutePathBuf], -} - pub struct ElevatedSandboxProfileCaptureRequest<'a> { pub permission_profile: &'a PermissionProfile, - pub permission_profile_cwd: &'a Path, + pub workspace_roots: &'a [AbsolutePathBuf], pub codex_home: &'a Path, pub command: Vec, pub cwd: &'a Path, @@ -39,7 +22,6 @@ pub struct ElevatedSandboxProfileCaptureRequest<'a> { } mod windows_impl { - use super::ElevatedSandboxCaptureRequest; use super::ElevatedSandboxProfileCaptureRequest; use crate::acl::allow_null_device; use crate::cap::load_or_create_cap_sids; @@ -56,7 +38,6 @@ mod windows_impl { use crate::logging::log_failure; use crate::logging::log_start; use crate::logging::log_success; - use crate::policy::parse_policy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::runner_client::spawn_runner_transport; use crate::sandbox_utils::ensure_codex_home_exists; @@ -64,7 +45,6 @@ mod windows_impl { use crate::setup::effective_write_roots_for_permissions; use crate::token::LocalSid; use anyhow::Result; - use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; @@ -77,7 +57,7 @@ mod windows_impl { ) -> Result { let ElevatedSandboxProfileCaptureRequest { permission_profile, - permission_profile_cwd, + workspace_roots, codex_home, command, cwd, @@ -91,10 +71,11 @@ mod windows_impl { deny_read_paths_override, deny_write_paths_override, } = request; - let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( - permission_profile, - permission_profile_cwd, - )?; + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + )?; let deny_read_paths_override = deny_read_paths_override .iter() .map(AbsolutePathBuf::to_path_buf) @@ -159,7 +140,7 @@ mod windows_impl { cwd: cwd.to_path_buf(), env: env_map.clone(), permission_profile: permission_profile.clone(), - permission_profile_cwd: permission_profile_cwd.to_path_buf(), + workspace_roots: workspace_roots.to_vec(), codex_home: sandbox_base.clone(), real_codex_home: codex_home.to_path_buf(), cap_sids, @@ -218,87 +199,13 @@ mod windows_impl { }) })() } - - /// Legacy policy-string adapter for callers that have not moved to permission profiles yet. - #[allow(clippy::too_many_arguments)] - pub fn run_windows_sandbox_capture( - request: ElevatedSandboxCaptureRequest<'_>, - ) -> Result { - let ElevatedSandboxCaptureRequest { - policy_json_or_preset, - sandbox_policy_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - use_private_desktop, - proxy_enforced, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - } = request; - let policy = parse_policy(policy_json_or_preset)?; - let permission_profile = - PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd); - run_windows_sandbox_capture_for_permission_profile(ElevatedSandboxProfileCaptureRequest { - permission_profile: &permission_profile, - permission_profile_cwd: sandbox_policy_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - use_private_desktop, - proxy_enforced, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - }) - } - - #[cfg(test)] - mod tests { - use crate::policy::SandboxPolicy; - - fn workspace_policy(network_access: bool) -> SandboxPolicy { - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - } - } - - #[test] - fn applies_network_block_when_access_is_disabled() { - assert!(!workspace_policy(/*network_access*/ false).has_full_network_access()); - } - - #[test] - fn skips_network_block_when_access_is_allowed() { - assert!(workspace_policy(/*network_access*/ true).has_full_network_access()); - } - - #[test] - fn applies_network_block_for_read_only() { - assert!(!SandboxPolicy::new_read_only_policy().has_full_network_access()); - } - } } -#[cfg(target_os = "windows")] -pub use windows_impl::run_windows_sandbox_capture; #[cfg(target_os = "windows")] pub use windows_impl::run_windows_sandbox_capture_for_permission_profile; #[cfg(not(target_os = "windows"))] mod stub { - use super::ElevatedSandboxCaptureRequest; use super::ElevatedSandboxProfileCaptureRequest; use anyhow::Result; use anyhow::bail; @@ -311,14 +218,6 @@ mod stub { pub timed_out: bool, } - /// Stub implementation for non-Windows targets; sandboxing only works on Windows. - #[allow(clippy::too_many_arguments)] - pub fn run_windows_sandbox_capture( - _request: ElevatedSandboxCaptureRequest<'_>, - ) -> Result { - bail!("Windows sandbox is only available on Windows") - } - /// Stub implementation for non-Windows targets; sandboxing only works on Windows. #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture_for_permission_profile( @@ -328,7 +227,5 @@ mod stub { } } -#[cfg(not(target_os = "windows"))] -pub use stub::run_windows_sandbox_capture; #[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_capture_for_permission_profile; diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 43b3f71955..841a63cedf 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -34,8 +34,6 @@ mod logging; #[cfg(target_os = "windows")] mod path_normalization; #[cfg(target_os = "windows")] -mod policy; -#[cfg(target_os = "windows")] mod process; #[cfg(target_os = "windows")] mod resolved_permissions; @@ -106,8 +104,6 @@ pub use acl::fetch_dacl_handle; #[cfg(target_os = "windows")] pub use acl::path_mask_allows; #[cfg(target_os = "windows")] -pub use audit::apply_world_writable_scan_and_denies; -#[cfg(target_os = "windows")] pub use audit::apply_world_writable_scan_and_denies_for_permissions; #[cfg(target_os = "windows")] pub use cap::load_or_create_cap_sids; @@ -137,12 +133,8 @@ pub use dpapi::protect as dpapi_protect; #[cfg(target_os = "windows")] pub use dpapi::unprotect as dpapi_unprotect; #[cfg(target_os = "windows")] -pub use elevated_impl::ElevatedSandboxCaptureRequest; -#[cfg(target_os = "windows")] pub use elevated_impl::ElevatedSandboxProfileCaptureRequest; #[cfg(target_os = "windows")] -pub use elevated_impl::run_windows_sandbox_capture as run_windows_sandbox_capture_elevated; -#[cfg(target_os = "windows")] pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run_windows_sandbox_capture_for_permission_profile_elevated; #[cfg(target_os = "windows")] pub use helper_materialization::resolve_current_exe_for_launch; @@ -189,10 +181,6 @@ pub use logging::log_note; #[cfg(target_os = "windows")] pub use path_normalization::canonicalize_path; #[cfg(target_os = "windows")] -pub use policy::SandboxPolicy; -#[cfg(target_os = "windows")] -pub use policy::parse_policy; -#[cfg(target_os = "windows")] pub use process::PipeSpawnHandles; #[cfg(target_os = "windows")] pub use process::StderrMode; @@ -260,8 +248,6 @@ pub use token::create_workspace_write_token_with_caps_from; #[cfg(target_os = "windows")] pub use token::get_current_token_for_restriction; #[cfg(target_os = "windows")] -pub use unified_exec::spawn_windows_sandbox_session_elevated; -#[cfg(target_os = "windows")] pub use unified_exec::spawn_windows_sandbox_session_elevated_for_permission_profile; #[cfg(target_os = "windows")] pub use unified_exec::spawn_windows_sandbox_session_legacy; @@ -289,8 +275,6 @@ pub use workspace_acl::is_command_cwd_root; #[cfg(not(target_os = "windows"))] pub use stub::CaptureResult; #[cfg(not(target_os = "windows"))] -pub use stub::apply_world_writable_scan_and_denies; -#[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_capture; #[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_legacy_preflight; @@ -299,9 +283,7 @@ pub use stub::run_windows_sandbox_legacy_preflight; mod windows_impl { use super::logging::log_failure; use super::logging::log_success; - use super::policy::SandboxPolicy; use super::process::create_process_as_user; - use super::resolved_permissions::ResolvedWindowsSandboxPermissions; use super::sandbox_utils::ensure_codex_home_exists; use super::spawn_prep::LegacyAclSids; use super::spawn_prep::SpawnPrepOptions; @@ -312,6 +294,7 @@ mod windows_impl { use super::spawn_prep::prepare_legacy_spawn_context; use super::spawn_prep::root_capability_sids; use anyhow::Result; + use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::io; @@ -366,8 +349,8 @@ mod windows_impl { #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, command: Vec, cwd: &Path, @@ -376,8 +359,8 @@ mod windows_impl { use_private_desktop: bool, ) -> Result { run_windows_sandbox_capture_with_filesystem_overrides( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + workspace_roots, codex_home, command, cwd, @@ -391,8 +374,8 @@ mod windows_impl { #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture_with_filesystem_overrides( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, command: Vec, cwd: &Path, @@ -411,8 +394,8 @@ mod windows_impl { .map(AbsolutePathBuf::to_path_buf) .collect::>(); let common = prepare_legacy_spawn_context( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + workspace_roots, codex_home, cwd, &mut env_map, @@ -422,12 +405,11 @@ mod windows_impl { add_git_safe_directory: false, }, )?; - let policy = common.policy; let permissions = common.permissions; let current_dir = common.current_dir; let logs_base_dir = common.logs_base_dir.as_deref(); let uses_write_capabilities = common.uses_write_capabilities; - if !policy.has_full_disk_read_access() { + if !permissions.has_full_disk_read_access() { anyhow::bail!( "Restricted read-only access requires the elevated Windows sandbox backend" ); @@ -437,14 +419,14 @@ mod windows_impl { if !additional_deny_read_paths.is_empty() { anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend"); } - let capability_roots = legacy_session_capability_roots( - &policy, - sandbox_policy_cwd, - ¤t_dir, - &env_map, + let capability_roots = + legacy_session_capability_roots(&permissions, ¤t_dir, &env_map, codex_home); + let security = prepare_legacy_session_security( + uses_write_capabilities, codex_home, - ); - let security = prepare_legacy_session_security(&policy, codex_home, cwd, capability_roots)?; + cwd, + capability_roots, + )?; allow_null_device_for_workspace_write(uses_write_capabilities); apply_legacy_session_acl_rules( &permissions, @@ -591,32 +573,29 @@ mod windows_impl { } pub fn run_windows_sandbox_legacy_preflight( - sandbox_policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, cwd: &Path, env_map: &HashMap, ) -> Result<()> { - let is_workspace_write = matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }); - if !is_workspace_write { + let Ok(permissions) = super::resolved_permissions::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + ) else { + return Ok(()); + }; + if !permissions.uses_write_capabilities_for_cwd(cwd, env_map) { return Ok(()); } ensure_codex_home_exists(codex_home)?; let current_dir = cwd.to_path_buf(); - let capability_roots = legacy_session_capability_roots( - sandbox_policy, - sandbox_policy_cwd, - ¤t_dir, - env_map, - codex_home, - ); + let capability_roots = + legacy_session_capability_roots(&permissions, ¤t_dir, env_map, codex_home); let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?; apply_legacy_session_acl_rules( - &ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd( - sandbox_policy, - sandbox_policy_cwd, - ), + &permissions, codex_home, ¤t_dir, env_map, @@ -634,43 +613,66 @@ mod windows_impl { #[cfg(test)] mod tests { - use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; + use std::collections::HashMap; use std::path::Path; - fn workspace_policy(network_access: bool) -> SandboxPolicy { - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - } + fn workspace_profile(network_policy: NetworkSandboxPolicy) -> PermissionProfile { + PermissionProfile::workspace_write_with( + &[], + network_policy, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ) } - fn should_apply_network_block(policy: &SandboxPolicy) -> bool { - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new(".")) - .should_apply_network_block() + fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool { + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + &[], + ) + .expect("managed permissions") + .should_apply_network_block() } #[test] fn applies_network_block_when_access_is_disabled() { - assert!(should_apply_network_block(&workspace_policy( - /*network_access*/ false + assert!(should_apply_network_block(&workspace_profile( + NetworkSandboxPolicy::Restricted ))); } #[test] fn skips_network_block_when_access_is_allowed() { - assert!(!should_apply_network_block(&workspace_policy( - /*network_access*/ true + assert!(!should_apply_network_block(&workspace_profile( + NetworkSandboxPolicy::Enabled ))); } #[test] fn applies_network_block_for_read_only() { - assert!(should_apply_network_block( - &SandboxPolicy::new_read_only_policy() - )); + assert!(should_apply_network_block(&PermissionProfile::read_only())); + } + + #[test] + fn legacy_preflight_skips_profiles_without_managed_filesystem_permissions() { + for permission_profile in [ + PermissionProfile::Disabled, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }, + ] { + super::run_windows_sandbox_legacy_preflight( + &permission_profile, + &[], + Path::new("."), + Path::new("."), + &HashMap::new(), + ) + .expect("unsupported profiles do not need ACL preflight"); + } } } } @@ -679,7 +681,8 @@ mod windows_impl { mod stub { use anyhow::Result; use anyhow::bail; - use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::models::PermissionProfile; + use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; @@ -693,8 +696,8 @@ mod stub { #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( - _policy_json_or_preset: &str, - _sandbox_policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _workspace_roots: &[AbsolutePathBuf], _codex_home: &Path, _command: Vec, _cwd: &Path, @@ -705,19 +708,9 @@ mod stub { bail!("Windows sandbox is only available on Windows") } - pub fn apply_world_writable_scan_and_denies( - _codex_home: &Path, - _cwd: &Path, - _env_map: &HashMap, - _sandbox_policy: &SandboxPolicy, - _logs_base_dir: Option<&Path>, - ) -> Result<()> { - bail!("Windows sandbox is only available on Windows") - } - pub fn run_windows_sandbox_legacy_preflight( - _sandbox_policy: &SandboxPolicy, - _sandbox_policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _workspace_roots: &[AbsolutePathBuf], _codex_home: &Path, _cwd: &Path, _env_map: &HashMap, diff --git a/codex-rs/windows-sandbox-rs/src/policy.rs b/codex-rs/windows-sandbox-rs/src/policy.rs deleted file mode 100644 index cfaa9689ee..0000000000 --- a/codex-rs/windows-sandbox-rs/src/policy.rs +++ /dev/null @@ -1,61 +0,0 @@ -use anyhow::Result; -pub use codex_protocol::protocol::SandboxPolicy; - -pub fn parse_policy(value: &str) -> Result { - match value { - "read-only" => Ok(SandboxPolicy::new_read_only_policy()), - "workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()), - "danger-full-access" | "external-sandbox" => { - anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") - } - other => { - let parsed: SandboxPolicy = serde_json::from_str(other)?; - if matches!( - parsed, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { - anyhow::bail!( - "DangerFullAccess and ExternalSandbox are not supported for sandboxing" - ); - } - Ok(parsed) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn rejects_external_sandbox_preset() { - let err = parse_policy("external-sandbox").unwrap_err(); - assert!( - err.to_string() - .contains("DangerFullAccess and ExternalSandbox are not supported") - ); - } - - #[test] - fn rejects_external_sandbox_json() { - let payload = - serde_json::to_string(&codex_protocol::protocol::SandboxPolicy::ExternalSandbox { - network_access: codex_protocol::protocol::NetworkAccess::Enabled, - }) - .unwrap(); - let err = parse_policy(&payload).unwrap_err(); - assert!( - err.to_string() - .contains("DangerFullAccess and ExternalSandbox are not supported") - ); - } - - #[test] - fn parses_read_only_policy() { - assert_eq!( - parse_policy("read-only").unwrap(), - SandboxPolicy::new_read_only_policy() - ); - } -} diff --git a/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs b/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs index dbb08b2115..712a80aade 100644 --- a/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs +++ b/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs @@ -5,7 +5,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; @@ -38,13 +37,15 @@ pub enum WindowsSandboxTokenMode { /// Chooses the restricted-token family needed for a managed permission profile. pub fn token_mode_for_permission_profile( permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], cwd: &Path, env_map: &HashMap, ) -> Result { - let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( - permission_profile, - cwd, - )?; + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + )?; if permissions.file_system.has_full_disk_write_access() { anyhow::bail!( "permission profile requests full-disk filesystem writes, which cannot be enforced by the Windows sandbox" @@ -58,14 +59,6 @@ pub fn token_mode_for_permission_profile( } impl ResolvedWindowsSandboxPermissions { - pub fn from_legacy_policy_for_cwd(policy: &SandboxPolicy, cwd: &Path) -> Self { - Self { - file_system: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(policy, cwd) - .materialize_project_roots_with_cwd(cwd), - network: NetworkSandboxPolicy::from(policy), - } - } - pub fn try_from_permission_profile(permission_profile: &PermissionProfile) -> Result { if !matches!(permission_profile, PermissionProfile::Managed { .. }) { anyhow::bail!( @@ -85,15 +78,15 @@ impl ResolvedWindowsSandboxPermissions { } /// Resolves a managed permission profile and binds symbolic `:workspace_roots` - /// entries to the permission root supplied by the caller. - pub fn try_from_permission_profile_for_cwd( + /// entries to the workspace roots supplied by the caller. + pub fn try_from_permission_profile_for_workspace_roots( permission_profile: &PermissionProfile, - cwd: &Path, + workspace_roots: &[AbsolutePathBuf], ) -> Result { let mut permissions = Self::try_from_permission_profile(permission_profile)?; permissions.file_system = permissions .file_system - .materialize_project_roots_with_cwd(cwd); + .materialize_project_roots_with_workspace_roots(workspace_roots); Ok(permissions) } @@ -211,9 +204,14 @@ mod tests { use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSpecialPath; + use codex_protocol::permissions::project_roots_glob_pattern; use pretty_assertions::assert_eq; use tempfile::TempDir; + fn workspace_roots_for(root: &Path) -> Vec { + vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")] + } + #[test] fn permission_profile_workspace_write_uses_windows_temp_env_vars() { let tmp = TempDir::new().expect("tempdir"); @@ -247,38 +245,10 @@ mod tests { } #[test] - fn legacy_workspace_root_stays_bound_to_policy_cwd() { + fn permission_profile_workspace_root_uses_runtime_workspace_roots() { let tmp = TempDir::new().expect("tempdir"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); - std::fs::create_dir_all(&command_cwd).expect("create command cwd"); - - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, &policy_cwd); - - let roots = permissions - .writable_roots_for_cwd(&command_cwd, &HashMap::new()) - .into_iter() - .map(|root| root.root) - .collect::>(); - - assert_eq!( - roots, - vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")] - ); - } - - #[test] - fn permission_profile_workspace_root_stays_bound_to_profile_cwd() { - let tmp = TempDir::new().expect("tempdir"); - let profile_cwd = tmp.path().join("workspace"); - let command_cwd = profile_cwd.join("subdir"); + let workspace_root = tmp.path().join("workspace"); + let command_cwd = workspace_root.join("subdir"); std::fs::create_dir_all(&command_cwd).expect("create command cwd"); let permission_profile = PermissionProfile::Managed { @@ -293,11 +263,13 @@ mod tests { }, network: NetworkSandboxPolicy::Restricted, }; - let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( - &permission_profile, - &profile_cwd, - ) - .expect("managed permission profile"); + let workspace_roots = workspace_roots_for(workspace_root.as_path()); + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + &permission_profile, + workspace_roots.as_slice(), + ) + .expect("managed permission profile"); let roots = permissions .writable_roots_for_cwd(&command_cwd, &HashMap::new()) @@ -307,7 +279,101 @@ mod tests { assert_eq!( roots, - vec![dunce::canonicalize(&profile_cwd).expect("canonical profile cwd")] + vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")] + ); + } + + #[test] + fn permission_profile_workspace_roots_expand_all_runtime_workspace_roots() { + let tmp = TempDir::new().expect("tempdir"); + let first = AbsolutePathBuf::from_absolute_path(tmp.path().join("first")) + .expect("absolute first root"); + let second = AbsolutePathBuf::from_absolute_path(tmp.path().join("second")) + .expect("absolute second root"); + let permission_profile = PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(/*subpath*/ None), + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some(".git".into())), + }, + access: FileSystemAccessMode::Deny, + }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: project_roots_glob_pattern(Path::new("**/*.env")), + }, + access: FileSystemAccessMode::Deny, + }, + ], + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + }; + + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + &permission_profile, + &[first.clone(), second.clone()], + ) + .expect("managed permission profile"); + + assert_eq!( + permissions.file_system, + FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: first.clone(), + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: second.clone(), + }, + access: FileSystemAccessMode::Write, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: first.join(".git"), + }, + access: FileSystemAccessMode::Deny, + }, + FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: second.join(".git"), + }, + access: FileSystemAccessMode::Deny, + }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: AbsolutePathBuf::resolve_path_against_base( + "**/*.env", + first.as_path(), + ) + .to_string_lossy() + .into_owned(), + }, + access: FileSystemAccessMode::Deny, + }, + FileSystemSandboxEntry { + path: FileSystemPath::GlobPattern { + pattern: AbsolutePathBuf::resolve_path_against_base( + "**/*.env", + second.as_path(), + ) + .to_string_lossy() + .into_owned(), + }, + access: FileSystemAccessMode::Deny, + }, + ]) ); } @@ -316,9 +382,11 @@ mod tests { let tmp = TempDir::new().expect("tempdir"); let cwd = tmp.path().join("workspace"); std::fs::create_dir_all(&cwd).expect("create cwd"); + let workspace_roots = workspace_roots_for(cwd.as_path()); let token_mode = token_mode_for_permission_profile( &PermissionProfile::read_only(), + workspace_roots.as_slice(), &cwd, &HashMap::new(), ) @@ -332,9 +400,11 @@ mod tests { let tmp = TempDir::new().expect("tempdir"); let cwd = tmp.path().join("workspace"); std::fs::create_dir_all(&cwd).expect("create cwd"); + let workspace_roots = workspace_roots_for(cwd.as_path()); let token_mode = token_mode_for_permission_profile( &PermissionProfile::workspace_write(), + workspace_roots.as_slice(), &cwd, &HashMap::new(), ) @@ -390,9 +460,15 @@ mod tests { }, network: NetworkSandboxPolicy::Restricted, }; + let workspace_roots = workspace_roots_for(cwd.as_path()); - let err = token_mode_for_permission_profile(&permission_profile, &cwd, &HashMap::new()) - .expect_err("full disk writes should not resolve to a token mode"); + let err = token_mode_for_permission_profile( + &permission_profile, + workspace_roots.as_slice(), + &cwd, + &HashMap::new(), + ) + .expect_err("full disk writes should not resolve to a token mode"); assert!( err.to_string() diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs index b77a874160..a5de246f64 100644 --- a/codex-rs/windows-sandbox-rs/src/setup.rs +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -17,7 +17,6 @@ use crate::helper_materialization::helper_bin_dir; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; use crate::path_normalization::canonicalize_path; -use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupFailure; @@ -30,6 +29,8 @@ use anyhow::Result; use anyhow::anyhow; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::GetLastError; @@ -104,21 +105,21 @@ pub struct SetupRootOverrides { } pub fn run_setup_refresh( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], command_cwd: &Path, env_map: &HashMap, codex_home: &Path, proxy_enforced: bool, ) -> Result<()> { - if matches!( - policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { + let Ok(permissions) = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + ) + else { return Ok(()); - } - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); + }; run_setup_refresh_inner( SandboxSetupRequest { permissions: &permissions, @@ -139,22 +140,22 @@ pub fn run_setup_refresh_with_overrides( } pub fn run_setup_refresh_with_extra_read_roots( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], command_cwd: &Path, env_map: &HashMap, codex_home: &Path, extra_read_roots: Vec, proxy_enforced: bool, ) -> Result<()> { - if matches!( - policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { + let Ok(permissions) = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + ) + else { return Ok(()); - } - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); + }; let mut read_roots = gather_read_roots(command_cwd, &permissions, env_map, codex_home); read_roots.extend(extra_read_roots); run_setup_refresh_inner( @@ -1020,13 +1021,15 @@ mod tests { use crate::helper_materialization::BIN_DIRNAME; use crate::helper_materialization::RESOURCES_DIRNAME; use crate::helper_materialization::helper_bin_dir; - use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::collections::HashSet; use std::fs; + use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; @@ -1038,10 +1041,67 @@ mod tests { } fn permissions_for( - policy: &SandboxPolicy, - policy_cwd: &std::path::Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], ) -> ResolvedWindowsSandboxPermissions { - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd) + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + ) + .expect("managed permission profile") + } + + fn workspace_roots_for(root: &Path) -> Vec { + vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")] + } + + fn workspace_write_profile( + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + ) -> PermissionProfile { + PermissionProfile::workspace_write_with( + writable_roots, + NetworkSandboxPolicy::Restricted, + exclude_tmpdir_env_var, + exclude_slash_tmp, + ) + } + + #[test] + fn setup_refresh_skips_profiles_without_managed_filesystem_permissions() { + let tmp = TempDir::new().expect("tempdir"); + let command_cwd = tmp.path().join("workspace"); + let codex_home = tmp.path().join("codex-home"); + fs::create_dir_all(&command_cwd).expect("create workspace"); + + for permission_profile in [ + PermissionProfile::Disabled, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }, + ] { + super::run_setup_refresh( + &permission_profile, + command_cwd.as_path(), + command_cwd.as_path(), + &HashMap::new(), + codex_home.as_path(), + /*proxy_enforced*/ false, + ) + .expect("unsupported profiles do not need setup refresh"); + + super::run_setup_refresh_with_extra_read_roots( + &permission_profile, + command_cwd.as_path(), + command_cwd.as_path(), + &HashMap::new(), + codex_home.as_path(), + vec![command_cwd.clone()], + /*proxy_enforced*/ false, + ) + .expect("unsupported profiles do not need setup refresh"); + } } #[test] @@ -1361,8 +1421,9 @@ mod tests { let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); fs::create_dir_all(&command_cwd).expect("create workspace"); - let policy = SandboxPolicy::new_read_only_policy(); - let permissions = permissions_for(&policy, &command_cwd); + let permission_profile = PermissionProfile::read_only(); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home); let expected = @@ -1379,16 +1440,16 @@ mod tests { let writable_root = tmp.path().join("extra-write-root"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&writable_root).expect("create writable root"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![ - AbsolutePathBuf::from_absolute_path(&writable_root) - .expect("absolute writable root"), - ], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &command_cwd); + let writable_roots = vec![ + AbsolutePathBuf::from_absolute_path(&writable_root).expect("absolute writable root"), + ]; + let permission_profile = workspace_write_profile( + &writable_roots, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home); let expected_writable = @@ -1401,16 +1462,15 @@ mod tests { fn build_payload_roots_preserves_helper_roots_when_read_override_is_provided() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("policy-cwd"); + let workspace_root = tmp.path().join("workspace-root"); let command_cwd = tmp.path().join("workspace"); let readable_root = tmp.path().join("docs"); - fs::create_dir_all(&policy_cwd).expect("create policy cwd"); + fs::create_dir_all(&workspace_root).expect("create workspace root"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&readable_root).expect("create readable root"); - let policy = SandboxPolicy::ReadOnly { - network_access: false, - }; - let permissions = permissions_for(&policy, &policy_cwd); + let permission_profile = PermissionProfile::read_only(); + let workspace_roots = workspace_roots_for(workspace_root.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let (read_roots, write_roots) = build_payload_roots( &super::SandboxSetupRequest { @@ -1449,16 +1509,15 @@ mod tests { fn build_payload_roots_replaces_full_read_policy_when_read_override_is_provided() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("policy-cwd"); + let workspace_root = tmp.path().join("workspace-root"); let command_cwd = tmp.path().join("workspace"); let readable_root = tmp.path().join("docs"); - fs::create_dir_all(&policy_cwd).expect("create policy cwd"); + fs::create_dir_all(&workspace_root).expect("create workspace root"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&readable_root).expect("create readable root"); - let policy = SandboxPolicy::ReadOnly { - network_access: false, - }; - let permissions = permissions_for(&policy, &policy_cwd); + let permission_profile = PermissionProfile::read_only(); + let workspace_roots = workspace_roots_for(workspace_root.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let (read_roots, write_roots) = build_payload_roots( &super::SandboxSetupRequest { @@ -1504,13 +1563,13 @@ mod tests { fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&extra_root).expect("create extra root"); fs::create_dir_all(&sandbox_root).expect("create sandbox root"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &command_cwd); + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let override_roots = vec![ command_cwd.clone(), extra_root.clone(), @@ -1553,21 +1612,21 @@ mod tests { } #[test] - fn effective_write_roots_use_policy_cwd_for_legacy_workspace_root() { + fn effective_write_roots_use_runtime_workspace_roots_for_workspace_root() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); + let workspace_root = tmp.path().join("workspace"); + let command_cwd = workspace_root.join("subdir"); fs::create_dir_all(&codex_home).expect("create codex home"); fs::create_dir_all(&command_cwd).expect("create command cwd"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &policy_cwd); + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let workspace_roots = workspace_roots_for(workspace_root.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let effective_write_roots = super::effective_write_roots_for_setup( &permissions, @@ -1579,7 +1638,7 @@ mod tests { assert_eq!( effective_write_roots, - vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")] + vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")] ); } @@ -1594,16 +1653,16 @@ mod tests { let explicit_deny = tmp.path().join("explicit-deny"); fs::create_dir_all(&command_git).expect("create command .git"); fs::create_dir_all(&extra_codex).expect("create extra .codex"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![ - AbsolutePathBuf::from_absolute_path(&extra_write_root) - .expect("absolute writable root"), - ], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &command_cwd); + let writable_roots = vec![ + AbsolutePathBuf::from_absolute_path(&extra_write_root).expect("absolute writable root"), + ]; + let permission_profile = workspace_write_profile( + &writable_roots, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let request = super::SandboxSetupRequest { permissions: &permissions, command_cwd: &command_cwd, @@ -1633,8 +1692,9 @@ mod tests { let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); fs::create_dir_all(&command_cwd).expect("create workspace"); - let policy = SandboxPolicy::new_read_only_policy(); - let permissions = permissions_for(&policy, &command_cwd); + let permission_profile = PermissionProfile::read_only(); + let workspace_roots = workspace_roots_for(command_cwd.as_path()); + let permissions = permissions_for(&permission_profile, workspace_roots.as_slice()); let roots = gather_full_read_roots_for_permissions( &command_cwd, diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs index f16e964308..7bdd59bf53 100644 --- a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -17,8 +17,6 @@ use crate::identity::SandboxCreds; use crate::identity::require_logon_sandbox_creds; use crate::logging::log_start; use crate::path_normalization::canonicalize_path; -use crate::policy::SandboxPolicy; -use crate::policy::parse_policy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::sandbox_utils::ensure_codex_home_exists; use crate::sandbox_utils::inject_git_safe_directory; @@ -33,6 +31,8 @@ use crate::workspace_acl::protect_workspace_agents_dir; use crate::workspace_acl::protect_workspace_codex_dir; use anyhow::Context; use anyhow::Result; +use codex_protocol::models::PermissionProfile; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::ffi::c_void; use std::path::Path; @@ -41,7 +41,6 @@ use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::HANDLE; pub(crate) struct SpawnContext { - pub(crate) policy: SandboxPolicy, pub(crate) permissions: ResolvedWindowsSandboxPermissions, pub(crate) current_dir: PathBuf, pub(crate) logs_base_dir: Option, @@ -81,21 +80,19 @@ pub(crate) struct LegacyAclSids<'a> { } fn prepare_spawn_context_common( - policy_json_or_preset: &str, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, cwd: &Path, env_map: &mut HashMap, command: &[String], options: SpawnPrepOptions, ) -> Result { - let policy = parse_policy(policy_json_or_preset)?; - if matches!( - &policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { - anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") - } + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + )?; normalize_null_device_env(env_map); ensure_non_interactive_pager(env_map); @@ -112,12 +109,9 @@ fn prepare_spawn_context_common( let logs_base_dir = Some(sandbox_base); log_start(command, logs_base_dir.as_deref()); - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, policy_cwd); let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map); Ok(SpawnContext { - policy, permissions, current_dir: cwd.to_path_buf(), logs_base_dir, @@ -126,8 +120,8 @@ fn prepare_spawn_context_common( } pub(crate) fn prepare_legacy_spawn_context( - policy_json_or_preset: &str, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, cwd: &Path, env_map: &mut HashMap, @@ -135,8 +129,8 @@ pub(crate) fn prepare_legacy_spawn_context( options: SpawnPrepOptions, ) -> Result { let common = prepare_spawn_context_common( - policy_json_or_preset, - policy_cwd, + permission_profile, + workspace_roots, codex_home, cwd, env_map, @@ -150,38 +144,31 @@ pub(crate) fn prepare_legacy_spawn_context( } pub(crate) fn prepare_legacy_session_security( - policy: &SandboxPolicy, + uses_write_capabilities: bool, codex_home: &Path, cwd: &Path, capability_roots: impl IntoIterator, ) -> Result { let caps = load_or_create_cap_sids(codex_home)?; let (h_token, readonly_sid, readonly_sid_str, write_root_sids) = unsafe { - match policy { - SandboxPolicy::ReadOnly { .. } => { - let psid = LocalSid::from_string(&caps.readonly)?; - let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?; - (h_token, Some(psid), Some(caps.readonly), Vec::new()) - } - SandboxPolicy::WorkspaceWrite { .. } => { - let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?; - if write_root_sids.is_empty() { - anyhow::bail!("workspace-write sandbox has no writable root capability SIDs"); - } - let base = get_current_token_for_restriction()?; - let cap_ptrs: Vec<*mut c_void> = write_root_sids - .iter() - .map(|root| root.sid.as_ptr()) - .collect(); - let h_token = - create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice()); - CloseHandle(base); - let h_token = h_token?; - (h_token, None, None, write_root_sids) - } - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - unreachable!("dangerous policies rejected before legacy session prep") + if uses_write_capabilities { + let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?; + if write_root_sids.is_empty() { + anyhow::bail!("workspace-write sandbox has no writable root capability SIDs"); } + let base = get_current_token_for_restriction()?; + let cap_ptrs: Vec<*mut c_void> = write_root_sids + .iter() + .map(|root| root.sid.as_ptr()) + .collect(); + let h_token = create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice()); + CloseHandle(base); + let h_token = h_token?; + (h_token, None, None, write_root_sids) + } else { + let psid = LocalSid::from_string(&caps.readonly)?; + let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?; + (h_token, Some(psid), Some(caps.readonly), Vec::new()) } }; @@ -194,21 +181,18 @@ pub(crate) fn prepare_legacy_session_security( } pub(crate) fn legacy_session_capability_roots( - policy: &SandboxPolicy, - policy_cwd: &Path, + permissions: &ResolvedWindowsSandboxPermissions, current_dir: &Path, env_map: &HashMap, codex_home: &Path, ) -> Vec { - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); - let allow_paths = compute_allow_paths_for_permissions(&permissions, current_dir, env_map) + let allow_paths = compute_allow_paths_for_permissions(permissions, current_dir, env_map) .allow .into_iter() .collect::>(); if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) { effective_write_roots_for_permissions( - &permissions, + permissions, current_dir, env_map, codex_home, @@ -459,7 +443,6 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions( #[cfg(test)] mod tests { - use super::SandboxPolicy; use super::SpawnPrepOptions; use super::deny_root_capabilities_for_path; use super::legacy_session_capability_roots; @@ -469,34 +452,56 @@ mod tests { use crate::cap::load_or_create_cap_sids; use crate::cap::workspace_write_cap_sid_for_root; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; - fn should_apply_network_block(policy: &SandboxPolicy) -> bool { - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new(".")) - .should_apply_network_block() + fn workspace_profile( + network_policy: NetworkSandboxPolicy, + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + ) -> PermissionProfile { + PermissionProfile::workspace_write_with( + writable_roots, + network_policy, + exclude_tmpdir_env_var, + exclude_slash_tmp, + ) + } + + fn workspace_roots_for(root: &Path) -> Vec { + vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")] + } + + fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool { + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + &[], + ) + .expect("managed permission profile") + .should_apply_network_block() } #[test] fn no_network_env_rewrite_applies_for_workspace_write() { assert!(should_apply_network_block( - &SandboxPolicy::new_workspace_write_policy(), + &PermissionProfile::workspace_write() )); } #[test] fn no_network_env_rewrite_skips_when_network_access_is_allowed() { - assert!(!should_apply_network_block( - &SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }, - )); + assert!(!should_apply_network_block(&workspace_profile( + NetworkSandboxPolicy::Enabled, + &[], + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ))); } #[test] @@ -504,10 +509,11 @@ mod tests { let codex_home = TempDir::new().expect("tempdir"); let cwd = TempDir::new().expect("tempdir"); let mut env_map = HashMap::new(); + let workspace_roots = workspace_roots_for(cwd.path()); let _context = prepare_legacy_spawn_context( - "workspace-write", - cwd.path(), + &PermissionProfile::workspace_write(), + workspace_roots.as_slice(), codex_home.path(), cwd.path(), &mut env_map, @@ -534,10 +540,11 @@ mod tests { "HTTP_PROXY".to_string(), "http://user.proxy:8080".to_string(), )]); + let workspace_roots = workspace_roots_for(cwd.path()); let context = prepare_spawn_context_common( - "workspace-write", - cwd.path(), + &PermissionProfile::workspace_write(), + workspace_roots.as_slice(), codex_home.path(), cwd.path(), &mut env_map, @@ -548,7 +555,7 @@ mod tests { }, ) .expect("preserve existing env prep"); - assert_eq!(context.policy, SandboxPolicy::new_workspace_write_policy()); + assert!(context.uses_write_capabilities); assert_eq!(env_map.get("SBX_NONET_ACTIVE"), None); assert_eq!( @@ -558,24 +565,30 @@ mod tests { } #[test] - fn legacy_session_capability_roots_use_policy_cwd_for_workspace_root() { + fn legacy_session_capability_roots_use_runtime_workspace_roots_for_workspace_root() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); + let workspace_root = tmp.path().join("workspace"); + let command_cwd = workspace_root.join("subdir"); std::fs::create_dir_all(&codex_home).expect("create codex home"); std::fs::create_dir_all(&command_cwd).expect("create command cwd"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; + let permission_profile = workspace_profile( + NetworkSandboxPolicy::Restricted, + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let workspace_roots = workspace_roots_for(workspace_root.as_path()); + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + &permission_profile, + workspace_roots.as_slice(), + ) + .expect("managed permission profile"); let roots = legacy_session_capability_roots( - &policy, - &policy_cwd, + &permissions, &command_cwd, &HashMap::new(), &codex_home, @@ -583,7 +596,7 @@ mod tests { assert_eq!( roots, - vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")] + vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")] ); } @@ -672,24 +685,27 @@ mod tests { std::fs::create_dir_all(&active_root).expect("create active root"); std::fs::create_dir_all(&sandbox_root).expect("create sandbox root"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![ - AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"), - AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"), - AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"), - ], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - - let roots = legacy_session_capability_roots( - &policy, - &workspace, - &workspace, - &HashMap::new(), - &codex_home, + let writable_roots = vec![ + AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"), + AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"), + AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"), + ]; + let permission_profile = workspace_profile( + NetworkSandboxPolicy::Restricted, + &writable_roots, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, ); + let workspace_roots = workspace_roots_for(workspace.as_path()); + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + &permission_profile, + workspace_roots.as_slice(), + ) + .expect("managed permission profile"); + + let roots = + legacy_session_capability_roots(&permissions, &workspace, &HashMap::new(), &codex_home); assert!(roots.contains(&dunce::canonicalize(&workspace).expect("workspace"))); assert!(roots.contains(&dunce::canonicalize(&active_root).expect("active root"))); 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 1e13e45cc0..0a2af6d7ad 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 @@ -8,7 +8,6 @@ use crate::ipc_framed::FramedMessage; use crate::ipc_framed::IPC_PROTOCOL_VERSION; use crate::ipc_framed::Message; use crate::ipc_framed::SpawnRequest; -use crate::policy::parse_policy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::runner_client::spawn_runner_transport; use crate::spawn_prep::prepare_elevated_spawn_context_for_permissions; @@ -27,7 +26,7 @@ use tokio::sync::oneshot; #[allow(clippy::too_many_arguments)] pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profile( permission_profile: &PermissionProfile, - permission_profile_cwd: &Path, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, command: Vec, cwd: &Path, @@ -50,10 +49,11 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil .iter() .map(AbsolutePathBuf::to_path_buf) .collect::>(); - let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( - permission_profile, - permission_profile_cwd, - )?; + let permissions = + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + permission_profile, + workspace_roots, + )?; let elevated = prepare_elevated_spawn_context_for_permissions( permissions, codex_home, @@ -72,7 +72,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil cwd: cwd.to_path_buf(), env: env_map.clone(), permission_profile: permission_profile.clone(), - permission_profile_cwd: permission_profile_cwd.to_path_buf(), + workspace_roots: workspace_roots.to_vec(), codex_home: elevated.sandbox_base.clone(), real_codex_home: codex_home.to_path_buf(), cap_sids: elevated.cap_sids.clone(), @@ -145,44 +145,3 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil stdin_open, )) } - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn spawn_windows_sandbox_session_elevated( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, - codex_home: &Path, - command: Vec, - cwd: &Path, - env_map: HashMap, - timeout_ms: Option, - 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], - tty: bool, - stdin_open: bool, - use_private_desktop: bool, -) -> Result { - let policy = parse_policy(policy_json_or_preset)?; - let permission_profile = - PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd); - spawn_windows_sandbox_session_elevated_for_permission_profile( - &permission_profile, - sandbox_policy_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - tty, - stdin_open, - use_private_desktop, - ) - .await -} diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs index 4db782a601..abd0c020d3 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs @@ -17,6 +17,7 @@ use crate::spawn_prep::legacy_session_capability_roots; use crate::spawn_prep::prepare_legacy_session_security; use crate::spawn_prep::prepare_legacy_spawn_context; use anyhow::Result; +use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_pty::ProcessDriver; use codex_utils_pty::SpawnedProcess; @@ -269,8 +270,8 @@ fn resize_conpty_handle(hpc: &Arc>>, size: TerminalSize) #[allow(clippy::too_many_arguments)] pub(crate) async fn spawn_windows_sandbox_session_legacy( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, command: Vec, cwd: &Path, @@ -283,8 +284,8 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( use_private_desktop: bool, ) -> Result { let common = prepare_legacy_spawn_context( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + workspace_roots, codex_home, cwd, &mut env_map, @@ -294,7 +295,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( add_git_safe_directory: false, }, )?; - if !common.policy.has_full_disk_read_access() { + if !common.permissions.has_full_disk_read_access() { anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend"); } // WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this @@ -307,14 +308,17 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( .map(AbsolutePathBuf::to_path_buf) .collect::>(); let capability_roots = legacy_session_capability_roots( - &common.policy, - sandbox_policy_cwd, + &common.permissions, &common.current_dir, &env_map, codex_home, ); - let security = - prepare_legacy_session_security(&common.policy, codex_home, cwd, capability_roots)?; + let security = prepare_legacy_session_security( + common.uses_write_capabilities, + codex_home, + cwd, + capability_roots, + )?; allow_null_device_for_workspace_write(common.uses_write_capabilities); apply_legacy_session_acl_rules( 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 ceb4bd03b6..2fe8b80ee1 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs @@ -19,8 +19,8 @@ use std::path::PathBuf; #[allow(clippy::too_many_arguments)] pub async fn spawn_windows_sandbox_session_legacy( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, command: Vec, cwd: &Path, @@ -33,8 +33,8 @@ pub async fn spawn_windows_sandbox_session_legacy( use_private_desktop: bool, ) -> Result { backends::legacy::spawn_windows_sandbox_session_legacy( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + workspace_roots, codex_home, command, cwd, @@ -52,7 +52,7 @@ pub async fn spawn_windows_sandbox_session_legacy( #[allow(clippy::too_many_arguments)] pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile( permission_profile: &PermissionProfile, - permission_profile_cwd: &Path, + workspace_roots: &[AbsolutePathBuf], codex_home: &Path, command: Vec, cwd: &Path, @@ -69,45 +69,7 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile( ) -> Result { backends::elevated::spawn_windows_sandbox_session_elevated_for_permission_profile( permission_profile, - permission_profile_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - tty, - stdin_open, - use_private_desktop, - ) - .await -} - -#[allow(clippy::too_many_arguments)] -pub async fn spawn_windows_sandbox_session_elevated( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, - codex_home: &Path, - command: Vec, - cwd: &Path, - env_map: HashMap, - timeout_ms: Option, - 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], - tty: bool, - stdin_open: bool, - use_private_desktop: bool, -) -> Result { - backends::elevated::spawn_windows_sandbox_session_elevated( - policy_json_or_preset, - sandbox_policy_cwd, + workspace_roots, codex_home, command, cwd, diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs index 9cd3d6d96d..aa26e69292 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs @@ -5,6 +5,7 @@ use crate::ipc_framed::Message; use crate::ipc_framed::decode_bytes; use crate::ipc_framed::read_frame; use crate::run_windows_sandbox_capture; +use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_pty::ProcessDriver; use pretty_assertions::assert_eq; @@ -75,6 +76,14 @@ fn sandbox_log(codex_home: &Path) -> String { .unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display())) } +fn workspace_write_profile() -> PermissionProfile { + PermissionProfile::workspace_write() +} + +fn workspace_roots_for(root: &Path) -> Vec { + vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")] +} + fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec { let deadline = Instant::now() + Duration::from_secs(2); loop { @@ -149,9 +158,10 @@ fn legacy_non_tty_cmd_emits_output() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-non-tty-cmd"); println!("cmd codex_home={}", codex_home.path().display()); + let permission_profile = workspace_write_profile(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", - cwd.as_path(), + &permission_profile, + workspace_roots_for(cwd.as_path()).as_slice(), codex_home.path(), vec![ "C:\\Windows\\System32\\cmd.exe".to_string(), @@ -189,9 +199,10 @@ fn legacy_non_tty_cmd_rejects_deny_read_overrides() { let secret_path = AbsolutePathBuf::from_absolute_path(cwd.join("legacy-non-tty-deny-read-secret.env")) .expect("absolute deny-read fixture path"); + let permission_profile = workspace_write_profile(); let err = spawn_windows_sandbox_session_legacy( - "workspace-write", - cwd.as_path(), + &permission_profile, + workspace_roots_for(cwd.as_path()).as_slice(), codex_home.path(), vec![ "C:\\Windows\\System32\\cmd.exe".to_string(), @@ -228,9 +239,10 @@ fn legacy_non_tty_powershell_emits_output() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-non-tty-pwsh"); println!("pwsh codex_home={}", codex_home.path().display()); + let permission_profile = workspace_write_profile(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", - cwd.as_path(), + &permission_profile, + workspace_roots_for(cwd.as_path()).as_slice(), codex_home.path(), vec![ pwsh.display().to_string(), @@ -413,9 +425,10 @@ fn legacy_capture_powershell_emits_output() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-capture-pwsh"); println!("capture pwsh codex_home={}", codex_home.path().display()); + let permission_profile = workspace_write_profile(); let result = run_windows_sandbox_capture( - "workspace-write", - cwd.as_path(), + &permission_profile, + workspace_roots_for(cwd.as_path()).as_slice(), codex_home.path(), vec![ pwsh.display().to_string(), @@ -452,9 +465,10 @@ fn legacy_tty_powershell_emits_output_and_accepts_input() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-tty-pwsh"); println!("tty pwsh codex_home={}", codex_home.path().display()); + let permission_profile = workspace_write_profile(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", - cwd.as_path(), + &permission_profile, + workspace_roots_for(cwd.as_path()).as_slice(), codex_home.path(), vec![ pwsh.display().to_string(), @@ -505,9 +519,10 @@ fn legacy_tty_cmd_emits_output_and_accepts_input() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-tty-cmd"); println!("tty cmd codex_home={}", codex_home.path().display()); + let permission_profile = workspace_write_profile(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", - cwd.as_path(), + &permission_profile, + workspace_roots_for(cwd.as_path()).as_slice(), codex_home.path(), vec![ "C:\\Windows\\System32\\cmd.exe".to_string(), @@ -558,9 +573,10 @@ fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() { "tty cmd default desktop codex_home={}", codex_home.path().display() ); + let permission_profile = workspace_write_profile(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", - cwd.as_path(), + &permission_profile, + workspace_roots_for(cwd.as_path()).as_slice(), codex_home.path(), vec![ "C:\\Windows\\System32\\cmd.exe".to_string(),