unified-exec: preserve PathUri through exec-server (#28681)

## Why

It should be possible for app-server to handle "foreign" OS paths in
unified_exec working directories, allowing e.g. a Linux app-server to
run processes on e.g. a Windows exec-server.

## What

Convert the core unified_exec cwd values to use `PathUri`.

Adds fallible path conversion in several places to try to minimize the
scope of this change. The only time this change suppresses errors from
converting `PathUri` to an `AbsolutePathBuf` is when the turn is
configured with no sandboxing at all to allow us to make progress
testing without sandboxing.

Future changes to apply_patch and sandboxing will clean up these error
paths.

A tool's cwd is resolved from joining a model-provided workdir to the
environment's cwd. When using `AbsolutePathBuf::join()`, an
absolute-path workdir would overwrite the environment's cwd and we would
resolve permissions/sandboxing against the model-provided path. This
change extends `PathUri::join()` to also treat an absolute rhs as an
override of the base/lhs.

This also removes some coverage from the remove_env_windows tests until
a follow-up converts foreign paths in command exec events correctly.

## Breaking Changes

When using `AbsolutePathBuf::join()` for workdir resolution, we ended up
resolving tilde-prefixed paths against the app-server's `$HOME`, e.g.
`~/foo/bar` becomes `/home/anp/foo/bar`. It's difficult to do this with
`PathUri` joining, so after offline discussion this PR no longer
implements it.

A quick check of some power users' rollouts suggests that models don't
actually generate home-prefixed absolute working directories for their
spawns, so this shouldn't have any real blast radius.
This commit is contained in:
Adam Perry @ OpenAI authored and GitHub committed 2026-06-17 19:36:16 +00:00
1 parent cca39d51ba
commit 5867b529ae
33 files changed
+634 -206

No files matched your search

+124 -41
View File
@@ -109,8 +109,8 @@ pub struct SandboxCommand {
#[derive(Debug)]
pub struct SandboxExecRequest {
pub command: Vec<String>,
pub cwd: AbsolutePathBuf,
pub sandbox_policy_cwd: AbsolutePathBuf,
pub cwd: PathUri,
pub sandbox_policy_cwd: PathUri,
pub env: HashMap<String, String>,
pub network: Option<NetworkProxy>,
pub sandbox: SandboxType,
@@ -150,6 +150,52 @@ pub struct SandboxDirectSpawnTransformRequest<'a> {
pub workspace_roots: &'a [AbsolutePathBuf],
}
// TODO(anp): Revisit this preparation type once this module's PathUri migration is complete.
struct PendingSandboxedExecRequest {
native_command_cwd: AbsolutePathBuf,
native_sandbox_policy_cwd: AbsolutePathBuf,
effective_permission_profile: PermissionProfile,
effective_file_system_policy: FileSystemSandboxPolicy,
effective_network_policy: NetworkSandboxPolicy,
}
impl PendingSandboxedExecRequest {
fn new(
command_cwd: &PathUri,
sandbox_policy_cwd: &PathUri,
effective_permission_profile: PermissionProfile,
managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>,
) -> Result<Self, SandboxTransformError> {
// TODO(anp): Move PathUri conversion into the platform sandbox implementations.
let native_command_cwd = command_cwd.to_abs_path().map_err(|source| {
SandboxTransformError::InvalidCommandCwd {
cwd: command_cwd.clone(),
source,
}
})?;
let native_sandbox_policy_cwd = sandbox_policy_cwd.to_abs_path().map_err(|source| {
SandboxTransformError::InvalidSandboxPolicyCwd {
cwd: sandbox_policy_cwd.clone(),
source,
}
})?;
let effective_permission_profile = with_managed_mitm_ca_readable_root(
effective_permission_profile,
managed_mitm_ca_trust_bundle_path,
native_sandbox_policy_cwd.as_path(),
);
let (effective_file_system_policy, effective_network_policy) =
effective_permission_profile.to_runtime_permissions();
Ok(Self {
native_command_cwd,
native_sandbox_policy_cwd,
effective_permission_profile,
effective_file_system_policy,
effective_network_policy,
})
}
}
#[derive(Debug)]
pub enum SandboxTransformError {
InvalidCommandCwd {
@@ -266,47 +312,37 @@ impl SandboxManager {
windows_sandbox_level,
windows_sandbox_private_desktop,
} = request;
let native_command_cwd = command.cwd.to_abs_path().map_err(|source| {
SandboxTransformError::InvalidCommandCwd {
cwd: command.cwd.clone(),
source,
}
})?;
let native_sandbox_policy_cwd = sandbox_policy_cwd.to_abs_path().map_err(|source| {
SandboxTransformError::InvalidSandboxPolicyCwd {
cwd: sandbox_policy_cwd.clone(),
source,
}
})?;
let additional_permissions = command.additional_permissions.take();
let managed_mitm_ca_trust_bundle_path =
network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);
let effective_permission_profile =
let base_effective_permission_profile =
effective_permission_profile(permissions, additional_permissions.as_ref());
let effective_permission_profile = with_managed_mitm_ca_readable_root(
effective_permission_profile,
let pending_sandboxed_request = PendingSandboxedExecRequest::new(
&command.cwd,
sandbox_policy_cwd,
base_effective_permission_profile.clone(),
managed_mitm_ca_trust_bundle_path.as_ref(),
native_sandbox_policy_cwd.as_path(),
);
let (effective_file_system_policy, effective_network_policy) =
effective_permission_profile.to_runtime_permissions();
let (base_file_system_policy, base_network_policy) =
base_effective_permission_profile.to_runtime_permissions();
let mut argv = Vec::with_capacity(1 + command.args.len());
argv.push(command.program);
argv.extend(command.args.into_iter().map(OsString::from));
let (argv, arg0_override) = match sandbox {
SandboxType::None => (os_argv_to_strings(argv), None),
let (argv, arg0_override, pending_sandboxed_request) = match sandbox {
SandboxType::None => (os_argv_to_strings(argv), None, None),
#[cfg(target_os = "macos")]
SandboxType::MacosSeatbelt => {
use crate::seatbelt::CreateSeatbeltCommandArgsParams;
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
use crate::seatbelt::create_seatbelt_command_args;
let pending = pending_sandboxed_request?;
let mut args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: os_argv_to_strings(argv),
file_system_sandbox_policy: &effective_file_system_policy,
network_sandbox_policy: effective_network_policy,
sandbox_policy_cwd: native_sandbox_policy_cwd.as_path(),
file_system_sandbox_policy: &pending.effective_file_system_policy,
network_sandbox_policy: pending.effective_network_policy,
sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(),
enforce_managed_network,
network,
extra_allow_unix_sockets: &[],
@@ -314,52 +350,84 @@ impl SandboxManager {
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string());
full_command.append(&mut args);
(full_command, None)
(full_command, None, Some(pending))
}
#[cfg(not(target_os = "macos"))]
SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
SandboxType::LinuxSeccomp => {
let pending = pending_sandboxed_request?;
let exe = codex_linux_sandbox_exe
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
#[cfg(target_os = "linux")]
ensure_linux_bubblewrap_is_supported(
&effective_file_system_policy,
&pending.effective_file_system_policy,
use_legacy_landlock,
allow_proxy_network,
is_wsl1(),
)?;
let mut args = create_linux_sandbox_command_args_for_permission_profile(
os_argv_to_strings(argv),
native_command_cwd.as_path(),
&effective_permission_profile,
native_sandbox_policy_cwd.as_path(),
pending.native_command_cwd.as_path(),
&pending.effective_permission_profile,
pending.native_sandbox_policy_cwd.as_path(),
use_legacy_landlock,
allow_proxy_network,
);
let mut full_command = Vec::with_capacity(1 + args.len());
full_command.push(os_string_to_command_component(exe.as_os_str().to_owned()));
full_command.append(&mut args);
(full_command, Some(linux_sandbox_arg0_override(exe)))
(
full_command,
Some(linux_sandbox_arg0_override(exe)),
Some(pending),
)
}
#[cfg(target_os = "windows")]
SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
SandboxType::WindowsRestrictedToken => (
os_argv_to_strings(argv),
None,
Some(pending_sandboxed_request?),
),
#[cfg(not(target_os = "windows"))]
SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
SandboxType::WindowsRestrictedToken => (
os_argv_to_strings(argv),
None,
Some(pending_sandboxed_request?),
),
};
// Unsandboxed exec-server requests may have foreign cwd values that cannot be prepared
// locally, but their effective permissions must still be preserved. In that case, carry
// forward the base profile and its derived runtime policies.
let (permission_profile, file_system_sandbox_policy, network_sandbox_policy) =
pending_sandboxed_request.map_or(
(
base_effective_permission_profile,
base_file_system_policy,
base_network_policy,
),
|pending| {
(
pending.effective_permission_profile,
pending.effective_file_system_policy,
pending.effective_network_policy,
)
},
);
Ok(SandboxExecRequest {
command: argv,
cwd: native_command_cwd,
sandbox_policy_cwd: native_sandbox_policy_cwd,
cwd: command.cwd,
sandbox_policy_cwd: sandbox_policy_cwd.clone(),
env: command.env,
network: network.cloned(),
sandbox,
windows_sandbox_level,
windows_sandbox_private_desktop,
permission_profile: effective_permission_profile,
file_system_sandbox_policy: effective_file_system_policy,
network_sandbox_policy: effective_network_policy,
permission_profile,
file_system_sandbox_policy,
network_sandbox_policy,
arg0: arg0_override,
})
}
@@ -406,6 +474,21 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
) -> Result<(), SandboxTransformError> {
// TODO(anp): Keep PathUri through the Windows sandbox wrapper boundary.
let native_cwd =
request
.cwd
.to_abs_path()
.map_err(|source| SandboxTransformError::InvalidCommandCwd {
cwd: request.cwd.clone(),
source,
})?;
let native_sandbox_policy_cwd = request.sandbox_policy_cwd.to_abs_path().map_err(|source| {
SandboxTransformError::InvalidSandboxPolicyCwd {
cwd: request.sandbox_policy_cwd.clone(),
source,
}
})?;
let Some(program) = request.command.first_mut() else {
return Err(SandboxTransformError::WindowsSandboxPreparation(
"sandbox command was empty".to_string(),
@@ -423,14 +506,14 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
resolve_windows_elevated_filesystem_overrides(
request.sandbox,
&request.permission_profile,
&request.sandbox_policy_cwd,
&native_sandbox_policy_cwd,
use_elevated,
)
} else {
resolve_windows_restricted_token_filesystem_overrides(
request.sandbox,
&request.permission_profile,
&request.sandbox_policy_cwd,
&native_sandbox_policy_cwd,
request.windows_sandbox_level,
)
}
@@ -454,7 +537,7 @@ fn wrap_windows_sandbox_exec_request_for_direct_spawn(
let mut wrapper_args =
codex_windows_sandbox::create_windows_sandbox_command_args_for_permission_profile(
inner_command,
&request.cwd,
&native_cwd,
workspace_roots,
&request.env,
&request.permission_profile,