exec-server: preserve empty workspace roots

This commit is contained in:
pakrym-oai
2026-07-09 15:00:10 -07:00
parent 1f0566d3f5
commit 7bd45bab3d
5 changed files with 86 additions and 19 deletions

View File

@@ -72,11 +72,7 @@ impl FileSystemSandboxRunner {
.iter()
.map(native_workspace_root)
.collect::<Result<Vec<_>, _>>()?;
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}"))

View File

@@ -56,11 +56,7 @@ pub(crate) fn prepare_exec_request(
.iter()
.map(|root| native_path(root, "sandbox workspace root"))
.collect::<Result<Vec<_>, _>>()?;
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| {

View File

@@ -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<dyn ExecProcess>,
wake_rx: &mut watch::Receiver<u64>,

View File

@@ -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)]

View File

@@ -171,10 +171,11 @@ impl FileSystemSandboxContext {
permissions: PermissionProfile<AbsolutePathBuf>,
cwd: Option<PathUri>,
) -> 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,