From 7bd45bab3de1d512bd728068786e4b0e63e27e88 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Thu, 9 Jul 2026 15:00:10 -0700 Subject: [PATCH] exec-server: preserve empty workspace roots --- codex-rs/exec-server/src/fs_sandbox.rs | 6 +- codex-rs/exec-server/src/process_sandbox.rs | 6 +- codex-rs/exec-server/tests/exec_process.rs | 61 ++++++++++++++++--- .../exec-server/tests/file_system_unix.rs | 29 +++++++++ codex-rs/file-system/src/lib.rs | 3 +- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 9dfbeba976..ab122f415d 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -72,11 +72,7 @@ impl FileSystemSandboxRunner { .iter() .map(native_workspace_root) .collect::, _>>()?; - let workspace_roots = if native_workspace_roots.is_empty() { - std::slice::from_ref(&cwd.native) - } else { - native_workspace_roots.as_slice() - }; + let workspace_roots = native_workspace_roots.as_slice(); let native_permissions: PermissionProfile = sandbox.permissions.clone().try_into().map_err(|err| { invalid_request(format!("invalid sandbox permission path URI: {err}")) diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index 050e151e6e..8354196d06 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -56,11 +56,7 @@ pub(crate) fn prepare_exec_request( .iter() .map(|root| native_path(root, "sandbox workspace root")) .collect::, _>>()?; - let workspace_roots = if native_workspace_roots.is_empty() { - std::slice::from_ref(&native_sandbox_policy_cwd) - } else { - native_workspace_roots.as_slice() - }; + let workspace_roots = native_workspace_roots.as_slice(); let permissions = permissions.materialize_project_roots_with_workspace_roots(workspace_roots); let managed_mitm_ca_trust_bundle_path = params.managed_network.as_ref().and_then(|_| { CUSTOM_CA_ENV_KEYS.iter().find_map(|key| { diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index 5e51882772..4a283ad382 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -13,26 +13,26 @@ use codex_exec_server::ExecOutputStream; use codex_exec_server::ExecParams; use codex_exec_server::ExecProcess; use codex_exec_server::ExecProcessEvent; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::ProcessId; use codex_exec_server::ProcessSignal; use codex_exec_server::ReadResponse; use codex_exec_server::StartedExecProcess; use codex_exec_server::WriteStatus; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_protocol::models::PermissionProfile; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_protocol::permissions::FileSystemAccessMode; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_protocol::permissions::FileSystemPath; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_protocol::permissions::FileSystemSandboxEntry; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_protocol::permissions::FileSystemSandboxPolicy; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_protocol::permissions::FileSystemSpecialPath; -#[cfg(target_os = "linux")] +#[cfg(unix)] use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; @@ -240,6 +240,51 @@ async fn remote_tty_process_uses_configured_sandbox_helper_with_hostile_path() - Ok(()) } +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_process_preserves_empty_workspace_roots() -> Result<()> { + let context = create_process_context(/*use_remote*/ true).await?; + let tmp = TempDir::new()?; + let file = tmp.path().join("excluded.txt"); + std::fs::write(&file, b"excluded")?; + let cwd = PathUri::from_host_native_path(tmp.path())?; + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(/*subpath*/ None), + }, + access: FileSystemAccessMode::Read, + }]); + let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( + PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), + cwd.clone(), + ); + sandbox.workspace_roots.clear(); + + let session = context + .backend + .start(ExecParams { + process_id: ProcessId::from("proc-empty-workspace-roots"), + argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()], + cwd, + env_policy: None, + env: HashMap::new(), + tty: false, + pipe_stdin: false, + arg0: None, + sandbox: Some(sandbox), + enforce_managed_network: false, + managed_network: None, + }) + .await?; + let (stdout, _stderr, exit_code, closed) = + collect_process_output_from_events(session.process).await?; + + assert!(!stdout.contains("excluded"), "unexpected stdout: {stdout}"); + assert_ne!(exit_code, Some(0)); + assert!(closed); + Ok(()) +} + async fn read_process_until_change( session: Arc, wake_rx: &mut watch::Receiver, diff --git a/codex-rs/exec-server/tests/file_system_unix.rs b/codex-rs/exec-server/tests/file_system_unix.rs index bfcf9df601..878749acfa 100644 --- a/codex-rs/exec-server/tests/file_system_unix.rs +++ b/codex-rs/exec-server/tests/file_system_unix.rs @@ -263,6 +263,35 @@ async fn remote_read_file_materializes_environment_workspace_roots() -> Result<( Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_read_file_preserves_empty_workspace_roots() -> Result<()> { + let context = create_file_system_context(FileSystemImplementation::Remote).await?; + let file_system = context.file_system; + let tmp = TempDir::new()?; + let file = tmp.path().join("excluded.txt"); + std::fs::write(&file, b"excluded")?; + + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(/*subpath*/ None), + }, + access: FileSystemAccessMode::Read, + }]); + let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( + PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), + PathUri::from_host_native_path(tmp.path())?, + ); + sandbox.workspace_roots.clear(); + + let error = file_system + .read_file(&PathUri::from_host_native_path(&file)?, Some(&sandbox)) + .await + .expect_err("empty workspace roots should not grant cwd access"); + assert_sandbox_denied(&error); + + Ok(()) +} + #[test_case(FileSystemImplementation::Local ; "local")] #[test_case(FileSystemImplementation::Remote ; "remote")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/codex-rs/file-system/src/lib.rs b/codex-rs/file-system/src/lib.rs index 2ef3bcf710..e402ef3382 100644 --- a/codex-rs/file-system/src/lib.rs +++ b/codex-rs/file-system/src/lib.rs @@ -171,10 +171,11 @@ impl FileSystemSandboxContext { permissions: PermissionProfile, cwd: Option, ) -> Self { + let workspace_roots = cwd.iter().cloned().collect(); Self { permissions: permissions.into(), cwd, - workspace_roots: Vec::new(), + workspace_roots, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, use_legacy_landlock: false,