mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## 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.
669 lines
25 KiB
Rust
669 lines
25 KiB
Rust
#[cfg(target_os = "linux")]
|
|
use crate::bwrap::WSL1_BWRAP_WARNING;
|
|
#[cfg(target_os = "linux")]
|
|
use crate::bwrap::is_wsl1;
|
|
use crate::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
|
use crate::landlock::allow_network_for_proxy;
|
|
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
|
use crate::policy_transforms::effective_permission_profile;
|
|
use crate::policy_transforms::should_require_platform_sandbox;
|
|
#[cfg(target_os = "windows")]
|
|
use crate::resolve_windows_elevated_filesystem_overrides;
|
|
#[cfg(target_os = "windows")]
|
|
use crate::resolve_windows_restricted_token_filesystem_overrides;
|
|
#[cfg(target_os = "windows")]
|
|
use crate::windows_sandbox_uses_elevated_backend;
|
|
use codex_network_proxy::NetworkProxy;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::models::AdditionalPermissionProfile;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
|
use codex_protocol::protocol::SandboxPolicy;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_path_uri::PathUri;
|
|
use std::collections::HashMap;
|
|
use std::ffi::OsString;
|
|
use std::io;
|
|
use std::path::Path;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
const WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST: &[&str] = &["USERNAME", "USERPROFILE"];
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum SandboxType {
|
|
None,
|
|
MacosSeatbelt,
|
|
LinuxSeccomp,
|
|
WindowsRestrictedToken,
|
|
}
|
|
|
|
impl SandboxType {
|
|
pub fn as_metric_tag(self) -> &'static str {
|
|
match self {
|
|
SandboxType::None => "none",
|
|
SandboxType::MacosSeatbelt => "seatbelt",
|
|
SandboxType::LinuxSeccomp => "seccomp",
|
|
SandboxType::WindowsRestrictedToken => "windows_sandbox",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum SandboxablePreference {
|
|
Auto,
|
|
Require,
|
|
Forbid,
|
|
}
|
|
|
|
pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
|
|
if cfg!(target_os = "macos") {
|
|
Some(SandboxType::MacosSeatbelt)
|
|
} else if cfg!(target_os = "linux") {
|
|
Some(SandboxType::LinuxSeccomp)
|
|
} else if cfg!(target_os = "windows") {
|
|
if windows_sandbox_enabled {
|
|
Some(SandboxType::WindowsRestrictedToken)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn with_managed_mitm_ca_readable_root(
|
|
permission_profile: PermissionProfile,
|
|
managed_mitm_ca_trust_bundle_path: Option<&AbsolutePathBuf>,
|
|
sandbox_policy_cwd: &Path,
|
|
) -> PermissionProfile {
|
|
let Some(managed_mitm_ca_trust_bundle_path) = managed_mitm_ca_trust_bundle_path else {
|
|
return permission_profile;
|
|
};
|
|
let (file_system_sandbox_policy, network_sandbox_policy) =
|
|
permission_profile.to_runtime_permissions();
|
|
let file_system_sandbox_policy = file_system_sandbox_policy.with_additional_readable_roots(
|
|
sandbox_policy_cwd,
|
|
std::slice::from_ref(managed_mitm_ca_trust_bundle_path),
|
|
);
|
|
PermissionProfile::from_runtime_permissions_with_enforcement(
|
|
permission_profile.enforcement(),
|
|
&file_system_sandbox_policy,
|
|
network_sandbox_policy,
|
|
)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct SandboxCommand {
|
|
pub program: OsString,
|
|
pub args: Vec<String>,
|
|
pub cwd: PathUri,
|
|
pub env: HashMap<String, String>,
|
|
pub additional_permissions: Option<AdditionalPermissionProfile>,
|
|
}
|
|
|
|
/// A host-native launch request produced after [`SandboxManager::transform`] validates URI inputs.
|
|
/// Build this only at the execution boundary: in exec-server, or in its logical equivalent within
|
|
/// app-server. Orchestration and transport code should retain [`PathUri`] values and defer
|
|
/// conversion to native paths until this request is created.
|
|
#[derive(Debug)]
|
|
pub struct SandboxExecRequest {
|
|
pub command: Vec<String>,
|
|
pub cwd: PathUri,
|
|
pub sandbox_policy_cwd: PathUri,
|
|
pub env: HashMap<String, String>,
|
|
pub network: Option<NetworkProxy>,
|
|
pub sandbox: SandboxType,
|
|
pub windows_sandbox_level: WindowsSandboxLevel,
|
|
pub windows_sandbox_private_desktop: bool,
|
|
pub permission_profile: PermissionProfile,
|
|
pub file_system_sandbox_policy: FileSystemSandboxPolicy,
|
|
pub network_sandbox_policy: NetworkSandboxPolicy,
|
|
pub arg0: Option<String>,
|
|
}
|
|
|
|
/// Bundled arguments for sandbox transformation.
|
|
///
|
|
/// This keeps call sites self-documenting when several fields are optional.
|
|
pub struct SandboxTransformRequest<'a> {
|
|
pub command: SandboxCommand,
|
|
pub permissions: &'a PermissionProfile,
|
|
pub sandbox: SandboxType,
|
|
pub enforce_managed_network: bool,
|
|
// TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>
|
|
// to make shared ownership explicit across runtime/sandbox plumbing.
|
|
pub network: Option<&'a NetworkProxy>,
|
|
pub sandbox_policy_cwd: &'a PathUri,
|
|
pub codex_linux_sandbox_exe: Option<&'a Path>,
|
|
pub use_legacy_landlock: bool,
|
|
pub windows_sandbox_level: WindowsSandboxLevel,
|
|
pub windows_sandbox_private_desktop: bool,
|
|
}
|
|
|
|
/// Bundled arguments for a sandbox transformation whose result will be spawned
|
|
/// directly from argv.
|
|
///
|
|
/// Direct-spawn callers will not run a later platform-specific launcher, so the
|
|
/// returned command must encode any sandbox wrapper it needs.
|
|
pub struct SandboxDirectSpawnTransformRequest<'a> {
|
|
pub transform: SandboxTransformRequest<'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 {
|
|
cwd: PathUri,
|
|
source: io::Error,
|
|
},
|
|
InvalidSandboxPolicyCwd {
|
|
cwd: PathUri,
|
|
source: io::Error,
|
|
},
|
|
MissingLinuxSandboxExecutable,
|
|
#[cfg(target_os = "linux")]
|
|
Wsl1UnsupportedForBubblewrap,
|
|
#[cfg(not(target_os = "macos"))]
|
|
SeatbeltUnavailable,
|
|
#[cfg(target_os = "windows")]
|
|
WindowsSandboxPreparation(String),
|
|
}
|
|
|
|
impl std::fmt::Display for SandboxTransformError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::InvalidCommandCwd { cwd, source } => {
|
|
write!(
|
|
f,
|
|
"command cwd URI `{cwd}` is not valid on this host: {source}"
|
|
)
|
|
}
|
|
Self::InvalidSandboxPolicyCwd { cwd, source } => write!(
|
|
f,
|
|
"sandbox policy cwd URI `{cwd}` is not valid on this host: {source}"
|
|
),
|
|
Self::MissingLinuxSandboxExecutable => {
|
|
write!(f, "missing codex-linux-sandbox executable path")
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
|
|
#[cfg(not(target_os = "macos"))]
|
|
Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"),
|
|
#[cfg(target_os = "windows")]
|
|
Self::WindowsSandboxPreparation(err) => {
|
|
write!(f, "failed to prepare windows sandbox wrapper: {err}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for SandboxTransformError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
match self {
|
|
Self::InvalidCommandCwd { source, .. }
|
|
| Self::InvalidSandboxPolicyCwd { source, .. } => Some(source),
|
|
Self::MissingLinuxSandboxExecutable => None,
|
|
#[cfg(target_os = "linux")]
|
|
Self::Wsl1UnsupportedForBubblewrap => None,
|
|
#[cfg(not(target_os = "macos"))]
|
|
Self::SeatbeltUnavailable => None,
|
|
#[cfg(target_os = "windows")]
|
|
Self::WindowsSandboxPreparation(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct SandboxManager;
|
|
|
|
impl SandboxManager {
|
|
pub fn new() -> Self {
|
|
Self
|
|
}
|
|
|
|
pub fn select_initial(
|
|
&self,
|
|
file_system_policy: &FileSystemSandboxPolicy,
|
|
network_policy: NetworkSandboxPolicy,
|
|
pref: SandboxablePreference,
|
|
windows_sandbox_level: WindowsSandboxLevel,
|
|
has_managed_network_requirements: bool,
|
|
) -> SandboxType {
|
|
match pref {
|
|
SandboxablePreference::Forbid => SandboxType::None,
|
|
SandboxablePreference::Require => {
|
|
get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
|
|
.unwrap_or(SandboxType::None)
|
|
}
|
|
SandboxablePreference::Auto => {
|
|
if should_require_platform_sandbox(
|
|
file_system_policy,
|
|
network_policy,
|
|
has_managed_network_requirements,
|
|
) {
|
|
get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled)
|
|
.unwrap_or(SandboxType::None)
|
|
} else {
|
|
SandboxType::None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn transform(
|
|
&self,
|
|
request: SandboxTransformRequest<'_>,
|
|
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
|
let SandboxTransformRequest {
|
|
mut command,
|
|
permissions,
|
|
sandbox,
|
|
enforce_managed_network,
|
|
network,
|
|
sandbox_policy_cwd,
|
|
codex_linux_sandbox_exe,
|
|
use_legacy_landlock,
|
|
windows_sandbox_level,
|
|
windows_sandbox_private_desktop,
|
|
} = request;
|
|
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 base_effective_permission_profile =
|
|
effective_permission_profile(permissions, additional_permissions.as_ref());
|
|
let pending_sandboxed_request = PendingSandboxedExecRequest::new(
|
|
&command.cwd,
|
|
sandbox_policy_cwd,
|
|
base_effective_permission_profile.clone(),
|
|
managed_mitm_ca_trust_bundle_path.as_ref(),
|
|
);
|
|
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, 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: &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: &[],
|
|
});
|
|
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, 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(
|
|
&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),
|
|
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)),
|
|
Some(pending),
|
|
)
|
|
}
|
|
#[cfg(target_os = "windows")]
|
|
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,
|
|
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: 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,
|
|
file_system_sandbox_policy,
|
|
network_sandbox_policy,
|
|
arg0: arg0_override,
|
|
})
|
|
}
|
|
|
|
pub fn transform_for_direct_spawn(
|
|
&self,
|
|
request: SandboxDirectSpawnTransformRequest<'_>,
|
|
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
let codex_home = codex_utils_home_dir::find_codex_home()
|
|
.map_err(|err| SandboxTransformError::WindowsSandboxPreparation(err.to_string()))?;
|
|
self.transform_for_direct_spawn_with_codex_home(request, codex_home.as_path())
|
|
}
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
self.transform(request.transform)
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn transform_for_direct_spawn_with_codex_home(
|
|
&self,
|
|
request: SandboxDirectSpawnTransformRequest<'_>,
|
|
codex_home: &Path,
|
|
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
|
let workspace_roots = request.workspace_roots;
|
|
let mut request = self.transform(request.transform)?;
|
|
if request.sandbox == SandboxType::WindowsRestrictedToken {
|
|
wrap_windows_sandbox_exec_request_for_direct_spawn(
|
|
&mut request,
|
|
workspace_roots,
|
|
codex_home,
|
|
)?;
|
|
}
|
|
Ok(request)
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn wrap_windows_sandbox_exec_request_for_direct_spawn(
|
|
request: &mut SandboxExecRequest,
|
|
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(),
|
|
));
|
|
};
|
|
let source = std::path::PathBuf::from(&program);
|
|
let helper = codex_windows_sandbox::resolve_exe_for_launch(source.as_path(), codex_home);
|
|
*program = helper.to_string_lossy().into_owned();
|
|
|
|
let inner_command = std::mem::take(&mut request.command);
|
|
let proxy_enforced = request.network.is_some();
|
|
let use_elevated =
|
|
windows_sandbox_uses_elevated_backend(request.windows_sandbox_level, proxy_enforced);
|
|
let overrides = if use_elevated {
|
|
resolve_windows_elevated_filesystem_overrides(
|
|
request.sandbox,
|
|
&request.permission_profile,
|
|
&native_sandbox_policy_cwd,
|
|
use_elevated,
|
|
)
|
|
} else {
|
|
resolve_windows_restricted_token_filesystem_overrides(
|
|
request.sandbox,
|
|
&request.permission_profile,
|
|
&native_sandbox_policy_cwd,
|
|
request.windows_sandbox_level,
|
|
)
|
|
}
|
|
.map_err(SandboxTransformError::WindowsSandboxPreparation)?;
|
|
let empty_paths: &[AbsolutePathBuf] = &[];
|
|
let read_roots_override = overrides
|
|
.as_ref()
|
|
.and_then(|overrides| overrides.read_roots_override.as_deref());
|
|
let read_roots_include_platform_defaults = overrides
|
|
.as_ref()
|
|
.is_some_and(|overrides| overrides.read_roots_include_platform_defaults);
|
|
let write_roots_override = overrides
|
|
.as_ref()
|
|
.and_then(|overrides| overrides.write_roots_override.as_deref());
|
|
let deny_read_paths_override = overrides.as_ref().map_or(empty_paths, |overrides| {
|
|
overrides.additional_deny_read_paths.as_slice()
|
|
});
|
|
let deny_write_paths_override = overrides.as_ref().map_or(empty_paths, |overrides| {
|
|
overrides.additional_deny_write_paths.as_slice()
|
|
});
|
|
let mut wrapper_args =
|
|
codex_windows_sandbox::create_windows_sandbox_command_args_for_permission_profile(
|
|
inner_command,
|
|
&native_cwd,
|
|
workspace_roots,
|
|
&request.env,
|
|
&request.permission_profile,
|
|
request.windows_sandbox_level,
|
|
request.windows_sandbox_private_desktop,
|
|
proxy_enforced,
|
|
read_roots_override,
|
|
read_roots_include_platform_defaults,
|
|
write_roots_override,
|
|
deny_read_paths_override,
|
|
deny_write_paths_override,
|
|
codex_home,
|
|
);
|
|
|
|
request.command = Vec::with_capacity(1 + wrapper_args.len());
|
|
request.command.push(source.to_string_lossy().into_owned());
|
|
request.command.append(&mut wrapper_args);
|
|
request.sandbox = SandboxType::None;
|
|
request.arg0 = None;
|
|
add_windows_sandbox_wrapper_setup_env(&mut request.env);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn add_windows_sandbox_wrapper_setup_env(env: &mut HashMap<String, String>) {
|
|
add_windows_sandbox_wrapper_setup_env_from_vars(env, std::env::vars_os());
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
fn add_windows_sandbox_wrapper_setup_env_from_vars(
|
|
env: &mut HashMap<String, String>,
|
|
vars: impl IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
|
|
) {
|
|
for (key, value) in vars {
|
|
let key = key.to_string_lossy().into_owned();
|
|
if !WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST
|
|
.iter()
|
|
.any(|allowed| key.eq_ignore_ascii_case(allowed))
|
|
{
|
|
continue;
|
|
}
|
|
env.retain(|existing, _| !existing.eq_ignore_ascii_case(&key));
|
|
env.insert(key, value.to_string_lossy().into_owned());
|
|
}
|
|
}
|
|
|
|
pub fn compatibility_sandbox_policy_for_permission_profile(
|
|
permissions: &PermissionProfile,
|
|
cwd: &Path,
|
|
) -> SandboxPolicy {
|
|
permissions
|
|
.to_legacy_sandbox_policy(cwd)
|
|
.unwrap_or_else(|_| {
|
|
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
|
|
compatibility_workspace_write_policy(file_system_policy, network_policy, cwd)
|
|
})
|
|
}
|
|
|
|
fn compatibility_workspace_write_policy(
|
|
file_system_policy: FileSystemSandboxPolicy,
|
|
network_policy: NetworkSandboxPolicy,
|
|
cwd: &Path,
|
|
) -> SandboxPolicy {
|
|
let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
|
let writable_roots = file_system_policy
|
|
.get_writable_roots_with_cwd(cwd)
|
|
.into_iter()
|
|
.map(|root| root.root)
|
|
.filter(|root| cwd_abs.as_ref() != Some(root))
|
|
.collect();
|
|
let tmpdir_writable = std::env::var_os("TMPDIR")
|
|
.filter(|tmpdir| !tmpdir.is_empty())
|
|
.and_then(|tmpdir| {
|
|
AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()
|
|
})
|
|
.is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd));
|
|
let slash_tmp = Path::new("/tmp");
|
|
let slash_tmp_writable = slash_tmp.is_absolute()
|
|
&& slash_tmp.is_dir()
|
|
&& file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);
|
|
|
|
SandboxPolicy::WorkspaceWrite {
|
|
writable_roots,
|
|
network_access: network_policy.is_enabled(),
|
|
exclude_tmpdir_env_var: !tmpdir_writable,
|
|
exclude_slash_tmp: !slash_tmp_writable,
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn ensure_linux_bubblewrap_is_supported(
|
|
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
|
use_legacy_landlock: bool,
|
|
allow_network_for_proxy: bool,
|
|
is_wsl1: bool,
|
|
) -> Result<(), SandboxTransformError> {
|
|
let requires_bubblewrap = allow_network_for_proxy
|
|
|| (!use_legacy_landlock && !file_system_sandbox_policy.has_full_disk_write_access());
|
|
if is_wsl1 && requires_bubblewrap {
|
|
return Err(SandboxTransformError::Wsl1UnsupportedForBubblewrap);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn os_argv_to_strings(argv: Vec<OsString>) -> Vec<String> {
|
|
argv.into_iter()
|
|
.map(os_string_to_command_component)
|
|
.collect()
|
|
}
|
|
|
|
fn os_string_to_command_component(value: OsString) -> String {
|
|
value
|
|
.into_string()
|
|
.unwrap_or_else(|value| value.to_string_lossy().into_owned())
|
|
}
|
|
|
|
fn linux_sandbox_arg0_override(exe: &Path) -> String {
|
|
if exe.file_name().and_then(|name| name.to_str()) == Some(CODEX_LINUX_SANDBOX_ARG0) {
|
|
os_string_to_command_component(exe.as_os_str().to_owned())
|
|
} else {
|
|
CODEX_LINUX_SANDBOX_ARG0.to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "manager_tests.rs"]
|
|
mod tests;
|