mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Run fs helper through Windows sandbox wrapper (#28359)
## Why This is the final PR in the Windows fs-helper sandbox stack and contains the actual bug fix. The exec-server filesystem helper is a direct-spawn path: it asks `SandboxManager` for a `SandboxExecRequest`, then launches the returned argv itself. That works on macOS and Linux because the transformed argv is already a self-contained sandbox wrapper. On Windows, the transformed request carried `WindowsRestrictedToken` metadata, but the direct-spawn fs-helper runner still launched the helper argv directly. That means Windows filesystem built-ins backed by the fs-helper could run with the parent Codex process permissions instead of the configured Windows sandbox. This PR makes the direct-spawn transform produce a self-contained Windows wrapper argv before fs-helper launches it. ## What Changed - Added `SandboxManager::transform_for_direct_spawn()` for callers that launch the returned argv themselves. - Wrapped Windows restricted-token direct-spawn requests with `codex.exe --run-as-windows-sandbox` and then marked the outer request as unsandboxed, matching the macOS/Linux wrapper argv shape. - Updated `exec-server/src/fs_sandbox.rs` to use the direct-spawn transform for fs-helper launches. - Materialized the inner `codex.exe --codex-run-as-fs-helper` executable into `.sandbox-bin` so the sandboxed user can run it. - Carried runtime workspace roots through `FileSystemSandboxContext` as `PathUri` values so `:workspace_roots` policies resolve correctly without sending native client paths over exec-server JSON. - Preserved wrapper setup identity environment needed by Windows sandbox setup without changing the serialized inner helper environment. ## Verification - `just bazel-lock-update` - `just bazel-lock-check` - `just test -p codex-sandboxing transform_for_direct_spawn_windows` - `just test -p codex-exec-server fs_sandbox::tests` - `just fix -p codex-windows-sandbox -p codex-sandboxing -p codex-exec-server -p codex-core -p codex-file-system` Local note: `just fmt` completed Rust formatting, but this workstation still fails the non-Rust formatter phases because uv cannot open its cache and the local buildifier/dotslash path is missing.
This commit is contained in:
1 parent
c78911e37f
commit
ef75171f18
16 files changed
+853
-391
No files matched your search
Generated
+2
@@ -3792,7 +3792,9 @@ dependencies = [
|
|||||||
"codex-network-proxy",
|
"codex-network-proxy",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
"codex-utils-absolute-path",
|
"codex-utils-absolute-path",
|
||||||
|
"codex-utils-home-dir",
|
||||||
"codex-utils-path-uri",
|
"codex-utils-path-uri",
|
||||||
|
"codex-windows-sandbox",
|
||||||
"dunce",
|
"dunce",
|
||||||
"libc",
|
"libc",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
|||||||
+9
-377
@@ -1,9 +1,9 @@
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitStatus;
|
use std::process::ExitStatus;
|
||||||
@@ -24,7 +24,6 @@ use crate::spawn::SpawnChildRequest;
|
|||||||
use crate::spawn::StdioPolicy;
|
use crate::spawn::StdioPolicy;
|
||||||
use crate::spawn::spawn_child_async;
|
use crate::spawn::spawn_child_async;
|
||||||
use codex_network_proxy::NetworkProxy;
|
use codex_network_proxy::NetworkProxy;
|
||||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
use codex_protocol::error::Result;
|
use codex_protocol::error::Result;
|
||||||
use codex_protocol::error::SandboxErr;
|
use codex_protocol::error::SandboxErr;
|
||||||
@@ -42,7 +41,14 @@ use codex_sandboxing::SandboxManager;
|
|||||||
use codex_sandboxing::SandboxTransformRequest;
|
use codex_sandboxing::SandboxTransformRequest;
|
||||||
use codex_sandboxing::SandboxType;
|
use codex_sandboxing::SandboxType;
|
||||||
use codex_sandboxing::SandboxablePreference;
|
use codex_sandboxing::SandboxablePreference;
|
||||||
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
|
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
|
||||||
|
#[cfg(test)]
|
||||||
|
use codex_sandboxing::permission_profile_supports_windows_restricted_token_sandbox;
|
||||||
|
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
|
||||||
|
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
|
||||||
|
#[cfg(test)]
|
||||||
|
use codex_sandboxing::unsupported_windows_restricted_token_sandbox_reason;
|
||||||
|
use codex_sandboxing::windows_sandbox_uses_elevated_backend;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
|
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
|
||||||
@@ -96,34 +102,6 @@ pub struct ExecParams {
|
|||||||
pub arg0: Option<String>,
|
pub arg0: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolved filesystem overrides for the Windows sandbox backends.
|
|
||||||
///
|
|
||||||
/// The elevated Windows backend consumes extra deny-read paths plus explicit
|
|
||||||
/// read and write roots during setup/refresh. The unelevated restricted-token
|
|
||||||
/// backend only consumes extra deny-write carveouts on top of the legacy
|
|
||||||
/// `WorkspaceWrite` allow set. Read-root overrides are layered on top of the
|
|
||||||
/// baseline helper roots that the elevated setup path needs to launch the
|
|
||||||
/// sandboxed command; split policies that opt into platform defaults carry
|
|
||||||
/// that explicitly with the override.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub(crate) struct WindowsSandboxFilesystemOverrides {
|
|
||||||
pub(crate) read_roots_override: Option<Vec<PathBuf>>,
|
|
||||||
pub(crate) read_roots_include_platform_defaults: bool,
|
|
||||||
pub(crate) write_roots_override: Option<Vec<PathBuf>>,
|
|
||||||
pub(crate) additional_deny_read_paths: Vec<AbsolutePathBuf>,
|
|
||||||
pub(crate) additional_deny_write_paths: Vec<AbsolutePathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn windows_sandbox_uses_elevated_backend(
|
|
||||||
sandbox_level: WindowsSandboxLevel,
|
|
||||||
proxy_enforced: bool,
|
|
||||||
) -> bool {
|
|
||||||
// Windows firewall enforcement is tied to the logon-user sandbox identities, so
|
|
||||||
// proxy-enforced sessions must use that backend even when the configured mode is
|
|
||||||
// the default restricted-token sandbox.
|
|
||||||
proxy_enforced || matches!(sandbox_level, WindowsSandboxLevel::Elevated)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
pub enum ExecCapturePolicy {
|
pub enum ExecCapturePolicy {
|
||||||
/// Shell-like execs keep the historical output cap and timeout behavior.
|
/// Shell-like execs keep the historical output cap and timeout behavior.
|
||||||
@@ -997,352 +975,6 @@ async fn exec(
|
|||||||
consume_output(child, expiration, capture_policy, stdout_stream).await
|
consume_output(child, expiration, capture_policy, stdout_stream).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
|
||||||
fn permission_profile_supports_windows_restricted_token_sandbox(
|
|
||||||
permission_profile: &PermissionProfile,
|
|
||||||
) -> bool {
|
|
||||||
match permission_profile {
|
|
||||||
PermissionProfile::Managed { file_system, .. } => {
|
|
||||||
!file_system.to_sandbox_policy().has_full_disk_write_access()
|
|
||||||
}
|
|
||||||
PermissionProfile::Disabled | PermissionProfile::External { .. } => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(not(test), allow(dead_code))]
|
|
||||||
pub(crate) fn unsupported_windows_restricted_token_sandbox_reason(
|
|
||||||
sandbox: SandboxType,
|
|
||||||
permission_profile: &PermissionProfile,
|
|
||||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
|
||||||
windows_sandbox_level: WindowsSandboxLevel,
|
|
||||||
) -> Option<String> {
|
|
||||||
if windows_sandbox_level == WindowsSandboxLevel::Elevated {
|
|
||||||
resolve_windows_elevated_filesystem_overrides(
|
|
||||||
sandbox,
|
|
||||||
permission_profile,
|
|
||||||
sandbox_policy_cwd,
|
|
||||||
windows_sandbox_level == WindowsSandboxLevel::Elevated,
|
|
||||||
)
|
|
||||||
.err()
|
|
||||||
} else {
|
|
||||||
resolve_windows_restricted_token_filesystem_overrides(
|
|
||||||
sandbox,
|
|
||||||
permission_profile,
|
|
||||||
sandbox_policy_cwd,
|
|
||||||
windows_sandbox_level,
|
|
||||||
)
|
|
||||||
.err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
|
|
||||||
sandbox: SandboxType,
|
|
||||||
permission_profile: &PermissionProfile,
|
|
||||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
|
||||||
windows_sandbox_level: WindowsSandboxLevel,
|
|
||||||
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
|
|
||||||
if sandbox != SandboxType::WindowsRestrictedToken
|
|
||||||
|| windows_sandbox_level == WindowsSandboxLevel::Elevated
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
|
||||||
permission_profile.to_runtime_permissions();
|
|
||||||
|
|
||||||
let needs_direct_runtime_enforcement = file_system_sandbox_policy
|
|
||||||
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
|
|
||||||
|
|
||||||
if permission_profile_supports_windows_restricted_token_sandbox(permission_profile)
|
|
||||||
&& !needs_direct_runtime_enforcement
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) {
|
|
||||||
let permission_profile_name = permission_profile_display_name(permission_profile);
|
|
||||||
return Err(format!(
|
|
||||||
"windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, permission_profile={permission_profile_name}; refusing to run unsandboxed",
|
|
||||||
file_system_sandbox_policy.kind,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The restricted-token backend can still enforce split write restrictions,
|
|
||||||
// but its WRITE_RESTRICTED token does not make capability SID deny-read ACEs
|
|
||||||
// participate in read access checks. Read restrictions therefore require the
|
|
||||||
// elevated backend, even when the filesystem root remains readable.
|
|
||||||
if !windows_policy_has_root_read_access(&file_system_sandbox_policy, sandbox_policy_cwd) {
|
|
||||||
return Err(
|
|
||||||
"windows unelevated restricted-token sandbox cannot enforce split filesystem read restrictions directly; refusing to run unsandboxed"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths(
|
|
||||||
&file_system_sandbox_policy,
|
|
||||||
sandbox_policy_cwd,
|
|
||||||
)?;
|
|
||||||
if !additional_deny_read_paths.is_empty() {
|
|
||||||
return Err(
|
|
||||||
"windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let legacy_projection = compatibility_sandbox_policy_for_permission_profile(
|
|
||||||
permission_profile,
|
|
||||||
sandbox_policy_cwd.as_path(),
|
|
||||||
);
|
|
||||||
let legacy_writable_roots = legacy_projection.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
|
||||||
let split_writable_roots =
|
|
||||||
file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
|
||||||
let legacy_root_paths: BTreeSet<PathBuf> = legacy_writable_roots
|
|
||||||
.iter()
|
|
||||||
.map(|root| normalize_windows_override_path(root.root.as_path()))
|
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
|
||||||
let split_root_paths: BTreeSet<PathBuf> = split_writable_roots
|
|
||||||
.iter()
|
|
||||||
.map(|root| normalize_windows_override_path(root.root.as_path()))
|
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
|
||||||
|
|
||||||
if legacy_root_paths != split_root_paths {
|
|
||||||
return Err(
|
|
||||||
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for writable_root in &split_writable_roots {
|
|
||||||
for read_only_subpath in &writable_root.read_only_subpaths {
|
|
||||||
if split_writable_roots.iter().any(|candidate| {
|
|
||||||
candidate.root.as_path() != writable_root.root.as_path()
|
|
||||||
&& candidate
|
|
||||||
.root
|
|
||||||
.as_path()
|
|
||||||
.starts_with(read_only_subpath.as_path())
|
|
||||||
}) {
|
|
||||||
return Err(
|
|
||||||
"windows unelevated restricted-token sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut additional_deny_write_paths = BTreeSet::new();
|
|
||||||
for split_root in &split_writable_roots {
|
|
||||||
let split_root_path = normalize_windows_override_path(split_root.root.as_path())?;
|
|
||||||
let Some(legacy_root) = legacy_writable_roots.iter().find(|candidate| {
|
|
||||||
normalize_windows_override_path(candidate.root.as_path())
|
|
||||||
.is_ok_and(|candidate_path| candidate_path == split_root_path)
|
|
||||||
}) else {
|
|
||||||
return Err(
|
|
||||||
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
for read_only_subpath in &split_root.read_only_subpaths {
|
|
||||||
if !legacy_root
|
|
||||||
.read_only_subpaths
|
|
||||||
.iter()
|
|
||||||
.any(|candidate| candidate == read_only_subpath)
|
|
||||||
{
|
|
||||||
additional_deny_write_paths.insert(normalize_windows_override_path(
|
|
||||||
read_only_subpath.as_path(),
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if additional_deny_read_paths.is_empty() && additional_deny_write_paths.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(WindowsSandboxFilesystemOverrides {
|
|
||||||
read_roots_override: None,
|
|
||||||
read_roots_include_platform_defaults: false,
|
|
||||||
write_roots_override: None,
|
|
||||||
additional_deny_read_paths,
|
|
||||||
additional_deny_write_paths: additional_deny_write_paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
|
|
||||||
.collect::<std::result::Result<_, _>>()?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_windows_override_path(path: &Path) -> std::result::Result<PathBuf, String> {
|
|
||||||
AbsolutePathBuf::from_absolute_path(dunce::simplified(path))
|
|
||||||
.map(AbsolutePathBuf::into_path_buf)
|
|
||||||
.map_err(|err| err.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn windows_policy_has_root_read_access(
|
|
||||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
|
||||||
cwd: &AbsolutePathBuf,
|
|
||||||
) -> bool {
|
|
||||||
let Some(root) = cwd.as_path().ancestors().last() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
file_system_sandbox_policy.can_read_path_with_cwd(root, cwd.as_path())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn resolve_windows_elevated_filesystem_overrides(
|
|
||||||
sandbox: SandboxType,
|
|
||||||
permission_profile: &PermissionProfile,
|
|
||||||
sandbox_policy_cwd: &AbsolutePathBuf,
|
|
||||||
use_windows_elevated_backend: bool,
|
|
||||||
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
|
|
||||||
if sandbox != SandboxType::WindowsRestrictedToken || !use_windows_elevated_backend {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (file_system_sandbox_policy, network_sandbox_policy) =
|
|
||||||
permission_profile.to_runtime_permissions();
|
|
||||||
|
|
||||||
if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) {
|
|
||||||
let permission_profile_name = permission_profile_display_name(permission_profile);
|
|
||||||
return Err(format!(
|
|
||||||
"windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, permission_profile={permission_profile_name}; refusing to run unsandboxed",
|
|
||||||
file_system_sandbox_policy.kind,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths(
|
|
||||||
&file_system_sandbox_policy,
|
|
||||||
sandbox_policy_cwd,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let split_writable_roots =
|
|
||||||
file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
|
||||||
if has_reopened_writable_descendant(&split_writable_roots) {
|
|
||||||
return Err(
|
|
||||||
"windows elevated sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let needs_direct_runtime_enforcement = file_system_sandbox_policy
|
|
||||||
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
|
|
||||||
let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path);
|
|
||||||
let legacy_projection = compatibility_sandbox_policy_for_permission_profile(
|
|
||||||
permission_profile,
|
|
||||||
sandbox_policy_cwd.as_path(),
|
|
||||||
);
|
|
||||||
let legacy_writable_roots = legacy_projection.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
|
||||||
let legacy_root_paths: BTreeSet<PathBuf> = legacy_writable_roots
|
|
||||||
.iter()
|
|
||||||
.map(|root| normalize_path(root.root.to_path_buf()))
|
|
||||||
.collect();
|
|
||||||
let split_readable_roots: Vec<PathBuf> = file_system_sandbox_policy
|
|
||||||
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
|
||||||
.into_iter()
|
|
||||||
.map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf)
|
|
||||||
.map(&normalize_path)
|
|
||||||
.collect();
|
|
||||||
let split_root_paths: Vec<PathBuf> = split_writable_roots
|
|
||||||
.iter()
|
|
||||||
.map(|root| normalize_path(root.root.to_path_buf()))
|
|
||||||
.collect();
|
|
||||||
let split_root_path_set: BTreeSet<PathBuf> = split_root_paths.iter().cloned().collect();
|
|
||||||
|
|
||||||
// `has_full_disk_read_access()` is intentionally false when deny-read
|
|
||||||
// entries exist. For Windows setup overrides, the important question is
|
|
||||||
// whether the baseline still reads from the filesystem root and only needs
|
|
||||||
// additional deny ACLs layered on top.
|
|
||||||
let split_has_root_read_access =
|
|
||||||
windows_policy_has_root_read_access(&file_system_sandbox_policy, sandbox_policy_cwd);
|
|
||||||
let read_roots_override = if split_has_root_read_access {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(split_readable_roots)
|
|
||||||
};
|
|
||||||
|
|
||||||
let write_roots_override = if split_root_path_set == legacy_root_paths {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(split_root_paths)
|
|
||||||
};
|
|
||||||
|
|
||||||
let additional_deny_write_paths = if needs_direct_runtime_enforcement {
|
|
||||||
let mut deny_paths = BTreeSet::new();
|
|
||||||
for writable_root in &split_writable_roots {
|
|
||||||
let writable_root_path = normalize_path(writable_root.root.to_path_buf());
|
|
||||||
let legacy_root = legacy_writable_roots.iter().find(|candidate| {
|
|
||||||
normalize_path(candidate.root.to_path_buf()) == writable_root_path
|
|
||||||
});
|
|
||||||
for read_only_subpath in &writable_root.read_only_subpaths {
|
|
||||||
let read_only_subpath_suffix = read_only_subpath
|
|
||||||
.as_path()
|
|
||||||
.strip_prefix(writable_root.root.as_path())
|
|
||||||
.ok();
|
|
||||||
let already_denied_by_legacy = legacy_root.is_some_and(|legacy_root| {
|
|
||||||
legacy_root.read_only_subpaths.iter().any(|candidate| {
|
|
||||||
candidate
|
|
||||||
.as_path()
|
|
||||||
.strip_prefix(legacy_root.root.as_path())
|
|
||||||
.ok()
|
|
||||||
== read_only_subpath_suffix
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if !already_denied_by_legacy {
|
|
||||||
deny_paths.insert(normalize_path(read_only_subpath.to_path_buf()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deny_paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
|
|
||||||
.collect::<std::result::Result<_, _>>()?
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
if read_roots_override.is_none()
|
|
||||||
&& write_roots_override.is_none()
|
|
||||||
&& additional_deny_read_paths.is_empty()
|
|
||||||
&& additional_deny_write_paths.is_empty()
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(WindowsSandboxFilesystemOverrides {
|
|
||||||
read_roots_include_platform_defaults: read_roots_override.is_some()
|
|
||||||
&& file_system_sandbox_policy.include_platform_defaults(),
|
|
||||||
read_roots_override,
|
|
||||||
write_roots_override,
|
|
||||||
additional_deny_read_paths,
|
|
||||||
additional_deny_write_paths,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn permission_profile_display_name(permission_profile: &PermissionProfile) -> &'static str {
|
|
||||||
match permission_profile {
|
|
||||||
PermissionProfile::Managed { .. } => "Managed",
|
|
||||||
PermissionProfile::Disabled => "Disabled",
|
|
||||||
PermissionProfile::External { .. } => "External",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_reopened_writable_descendant(
|
|
||||||
writable_roots: &[codex_protocol::protocol::WritableRoot],
|
|
||||||
) -> bool {
|
|
||||||
writable_roots.iter().any(|writable_root| {
|
|
||||||
writable_root
|
|
||||||
.read_only_subpaths
|
|
||||||
.iter()
|
|
||||||
.any(|read_only_subpath| {
|
|
||||||
writable_roots.iter().any(|candidate| {
|
|
||||||
candidate.root.as_path() != writable_root.root.as_path()
|
|
||||||
&& candidate
|
|
||||||
.root
|
|
||||||
.as_path()
|
|
||||||
.starts_with(read_only_subpath.as_path())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consumes the output of a child process according to the configured capture
|
/// Consumes the output of a child process according to the configured capture
|
||||||
/// policy.
|
/// policy.
|
||||||
async fn consume_output(
|
async fn consume_output(
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ ExecRequest for execution.
|
|||||||
use crate::exec::ExecCapturePolicy;
|
use crate::exec::ExecCapturePolicy;
|
||||||
use crate::exec::ExecExpiration;
|
use crate::exec::ExecExpiration;
|
||||||
use crate::exec::StdoutStream;
|
use crate::exec::StdoutStream;
|
||||||
use crate::exec::WindowsSandboxFilesystemOverrides;
|
|
||||||
use crate::exec::execute_exec_request;
|
use crate::exec::execute_exec_request;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||||
@@ -24,6 +23,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
use codex_sandboxing::SandboxExecRequest;
|
use codex_sandboxing::SandboxExecRequest;
|
||||||
use codex_sandboxing::SandboxType;
|
use codex_sandboxing::SandboxType;
|
||||||
|
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,12 @@ impl TurnContext {
|
|||||||
FileSystemSandboxContext {
|
FileSystemSandboxContext {
|
||||||
permissions: permissions.into(),
|
permissions: permissions.into(),
|
||||||
cwd: Some(cwd.clone()),
|
cwd: Some(cwd.clone()),
|
||||||
|
workspace_roots: self
|
||||||
|
.config
|
||||||
|
.effective_workspace_roots()
|
||||||
|
.iter()
|
||||||
|
.map(PathUri::from_abs_path)
|
||||||
|
.collect(),
|
||||||
windows_sandbox_level: self.windows_sandbox_level,
|
windows_sandbox_level: self.windows_sandbox_level,
|
||||||
windows_sandbox_private_desktop: self
|
windows_sandbox_private_desktop: self
|
||||||
.config
|
.config
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use codex_sandboxing::SandboxType;
|
|||||||
use codex_sandboxing::SandboxablePreference;
|
use codex_sandboxing::SandboxablePreference;
|
||||||
use codex_sandboxing::policy_transforms::effective_permission_profile;
|
use codex_sandboxing::policy_transforms::effective_permission_profile;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use codex_utils_path_uri::PathUri;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -99,6 +100,11 @@ impl ApplyPatchRuntime {
|
|||||||
Some(FileSystemSandboxContext {
|
Some(FileSystemSandboxContext {
|
||||||
permissions: permissions.into(),
|
permissions: permissions.into(),
|
||||||
cwd: Some(attempt.sandbox_cwd.clone()),
|
cwd: Some(attempt.sandbox_cwd.clone()),
|
||||||
|
workspace_roots: attempt
|
||||||
|
.workspace_roots
|
||||||
|
.iter()
|
||||||
|
.map(PathUri::from_abs_path)
|
||||||
|
.collect(),
|
||||||
windows_sandbox_level: attempt.windows_sandbox_level,
|
windows_sandbox_level: attempt.windows_sandbox_level,
|
||||||
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
|
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
|
||||||
use_legacy_landlock: attempt.use_legacy_landlock,
|
use_legacy_landlock: attempt.use_legacy_landlock,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
|||||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||||
use codex_sandboxing::SandboxCommand;
|
use codex_sandboxing::SandboxCommand;
|
||||||
|
use codex_sandboxing::SandboxDirectSpawnTransformRequest;
|
||||||
use codex_sandboxing::SandboxExecRequest;
|
use codex_sandboxing::SandboxExecRequest;
|
||||||
use codex_sandboxing::SandboxManager;
|
use codex_sandboxing::SandboxManager;
|
||||||
use codex_sandboxing::SandboxTransformRequest;
|
use codex_sandboxing::SandboxTransformRequest;
|
||||||
@@ -88,7 +89,7 @@ impl FileSystemSandboxRunner {
|
|||||||
&file_system_policy,
|
&file_system_policy,
|
||||||
network_policy,
|
network_policy,
|
||||||
);
|
);
|
||||||
let command = self.sandbox_exec_request(&permission_profile, &cwd.uri, sandbox)?;
|
let command = self.sandbox_exec_request(&permission_profile, &cwd, sandbox)?;
|
||||||
let request_json = serde_json::to_vec(&request).map_err(json_error)?;
|
let request_json = serde_json::to_vec(&request).map_err(json_error)?;
|
||||||
run_command(command, request_json).await
|
run_command(command, request_json).await
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ impl FileSystemSandboxRunner {
|
|||||||
fn sandbox_exec_request(
|
fn sandbox_exec_request(
|
||||||
&self,
|
&self,
|
||||||
permission_profile: &PermissionProfile,
|
permission_profile: &PermissionProfile,
|
||||||
cwd: &PathUri,
|
cwd: &SandboxCwd,
|
||||||
sandbox_context: &FileSystemSandboxContext,
|
sandbox_context: &FileSystemSandboxContext,
|
||||||
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
|
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
|
||||||
let helper = &self.runtime_paths.codex_self_exe;
|
let helper = &self.runtime_paths.codex_self_exe;
|
||||||
@@ -112,22 +113,36 @@ impl FileSystemSandboxRunner {
|
|||||||
let command = SandboxCommand {
|
let command = SandboxCommand {
|
||||||
program: helper.as_path().as_os_str().to_owned(),
|
program: helper.as_path().as_os_str().to_owned(),
|
||||||
args: vec![CODEX_FS_HELPER_ARG1.to_string()],
|
args: vec![CODEX_FS_HELPER_ARG1.to_string()],
|
||||||
cwd: cwd.clone(),
|
cwd: cwd.uri.clone(),
|
||||||
env: self.helper_env.clone(),
|
env: self.helper_env.clone(),
|
||||||
additional_permissions: None,
|
additional_permissions: None,
|
||||||
};
|
};
|
||||||
|
let native_workspace_roots = sandbox_context
|
||||||
|
.workspace_roots
|
||||||
|
.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()
|
||||||
|
};
|
||||||
sandbox_manager
|
sandbox_manager
|
||||||
.transform(SandboxTransformRequest {
|
.transform_for_direct_spawn(SandboxDirectSpawnTransformRequest {
|
||||||
|
workspace_roots,
|
||||||
|
transform: SandboxTransformRequest {
|
||||||
command,
|
command,
|
||||||
permissions: permission_profile,
|
permissions: permission_profile,
|
||||||
sandbox,
|
sandbox,
|
||||||
enforce_managed_network: false,
|
enforce_managed_network: false,
|
||||||
network: None,
|
network: None,
|
||||||
sandbox_policy_cwd: cwd,
|
sandbox_policy_cwd: &cwd.uri,
|
||||||
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||||
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
use_legacy_landlock: sandbox_context.use_legacy_landlock,
|
||||||
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
windows_sandbox_level: sandbox_context.windows_sandbox_level,
|
||||||
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
|
windows_sandbox_private_desktop: sandbox_context
|
||||||
|
.windows_sandbox_private_desktop,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))
|
.map_err(|err| invalid_request(format!("failed to prepare fs sandbox: {err}")))
|
||||||
}
|
}
|
||||||
@@ -158,6 +173,14 @@ fn native_sandbox_cwd(cwd: &PathUri) -> Result<AbsolutePathBuf, JSONRPCErrorErro
|
|||||||
.map_err(|err| invalid_request(err.to_string()))
|
.map_err(|err| invalid_request(err.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn native_workspace_root(root: &PathUri) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
|
||||||
|
root.to_abs_path().map_err(|err| {
|
||||||
|
invalid_request(format!(
|
||||||
|
"file system sandbox workspace root is not native to this exec-server host: {err}"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn helper_read_roots(runtime_paths: &ExecServerRuntimePaths) -> Vec<AbsolutePathBuf> {
|
fn helper_read_roots(runtime_paths: &ExecServerRuntimePaths) -> Vec<AbsolutePathBuf> {
|
||||||
let mut roots = Vec::new();
|
let mut roots = Vec::new();
|
||||||
for path in std::iter::once(runtime_paths.codex_self_exe.as_path())
|
for path in std::iter::once(runtime_paths.codex_self_exe.as_path())
|
||||||
@@ -517,15 +540,21 @@ mod tests {
|
|||||||
let runner = FileSystemSandboxRunner::new(runtime_paths);
|
let runner = FileSystemSandboxRunner::new(runtime_paths);
|
||||||
let native_cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
let native_cwd = AbsolutePathBuf::current_dir().expect("cwd");
|
||||||
let cwd = PathUri::from_abs_path(&native_cwd);
|
let cwd = PathUri::from_abs_path(&native_cwd);
|
||||||
let file_system_policy =
|
let file_system_policy = restricted_policy(vec![path_entry(
|
||||||
restricted_policy(vec![path_entry(native_cwd, FileSystemAccessMode::Write)]);
|
native_cwd.clone(),
|
||||||
|
FileSystemAccessMode::Write,
|
||||||
|
)]);
|
||||||
let network_policy = NetworkSandboxPolicy::Restricted;
|
let network_policy = NetworkSandboxPolicy::Restricted;
|
||||||
let permission_profile =
|
let permission_profile =
|
||||||
PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy);
|
PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy);
|
||||||
let sandbox_context = sandbox_context_with_cwd(&file_system_policy, cwd.clone());
|
let sandbox_context = sandbox_context_with_cwd(&file_system_policy, cwd.clone());
|
||||||
|
let sandbox_cwd = SandboxCwd {
|
||||||
|
uri: cwd,
|
||||||
|
native: native_cwd,
|
||||||
|
};
|
||||||
|
|
||||||
let request = runner
|
let request = runner
|
||||||
.sandbox_exec_request(&permission_profile, &cwd, &sandbox_context)
|
.sandbox_exec_request(&permission_profile, &sandbox_cwd, &sandbox_context)
|
||||||
.expect("sandbox exec request");
|
.expect("sandbox exec request");
|
||||||
|
|
||||||
assert_eq!(request.env.get(&path_key), Some(&path));
|
assert_eq!(request.env.get(&path_key), Some(&path));
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub(crate) mod exec_server;
|
|||||||
pub(crate) const DELAYED_OUTPUT_AFTER_EXIT_PARENT_ARG: &str =
|
pub(crate) const DELAYED_OUTPUT_AFTER_EXIT_PARENT_ARG: &str =
|
||||||
"--codex-test-delayed-output-after-exit-parent";
|
"--codex-test-delayed-output-after-exit-parent";
|
||||||
|
|
||||||
|
const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox";
|
||||||
const DELAYED_OUTPUT_AFTER_EXIT_CHILD_ARG: &str = "--codex-test-delayed-output-after-exit-child";
|
const DELAYED_OUTPUT_AFTER_EXIT_CHILD_ARG: &str = "--codex-test-delayed-output-after-exit-child";
|
||||||
|
|
||||||
#[ctor]
|
#[ctor]
|
||||||
@@ -27,6 +28,9 @@ pub static TEST_BINARY_DISPATCH_GUARD: Option<TestBinaryDispatchGuard> = {
|
|||||||
if argv1 == Some(CODEX_FS_HELPER_ARG1) {
|
if argv1 == Some(CODEX_FS_HELPER_ARG1) {
|
||||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||||
}
|
}
|
||||||
|
if argv1 == Some(CODEX_WINDOWS_SANDBOX_ARG1) {
|
||||||
|
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||||
|
}
|
||||||
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
|
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
|
||||||
return TestBinaryDispatchMode::DispatchArg0Only;
|
return TestBinaryDispatchMode::DispatchArg0Only;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ use std::path::Path;
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use codex_exec_server::FileSystemSandboxContext;
|
||||||
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||||
|
use codex_protocol::protocol::SandboxPolicy;
|
||||||
|
use codex_utils_path_uri::PathUri;
|
||||||
use test_case::test_case;
|
use test_case::test_case;
|
||||||
|
|
||||||
use crate::support::FileSystemImplementation;
|
use crate::support::FileSystemImplementation;
|
||||||
|
use crate::support::create_file_system_context;
|
||||||
|
|
||||||
fn create_directory_junction(target: &Path, alias: &Path) -> Result<()> {
|
fn create_directory_junction(target: &Path, alias: &Path) -> Result<()> {
|
||||||
let output = Command::new("cmd")
|
let output = Command::new("cmd")
|
||||||
@@ -54,3 +59,58 @@ async fn file_system_sandboxed_canonicalize_resolves_directory_junction(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy() -> Result<()> {
|
||||||
|
let context = create_file_system_context(FileSystemImplementation::Remote).await?;
|
||||||
|
let file_system = context.file_system;
|
||||||
|
let tmp = tempfile::TempDir::new()?;
|
||||||
|
let readonly_dir = tmp.path().join("readonly");
|
||||||
|
std::fs::create_dir_all(&readonly_dir)?;
|
||||||
|
|
||||||
|
let mut sandbox = read_only_sandbox_for_cwd(readonly_dir.clone())?;
|
||||||
|
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
|
||||||
|
|
||||||
|
let readable_file = readonly_dir.join("readable.txt");
|
||||||
|
std::fs::write(&readable_file, b"readable")?;
|
||||||
|
let read_result = file_system
|
||||||
|
.read_file(&PathUri::from_path(&readable_file)?, Some(&sandbox))
|
||||||
|
.await;
|
||||||
|
// Some local Windows hosts cannot create restricted tokens. Reaching that
|
||||||
|
// error still proves the remote fs helper went through the Windows sandbox
|
||||||
|
// launcher; before the wrapper fix this read would have run unsandboxed.
|
||||||
|
if is_unsupported_restricted_token_host(&read_result) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
assert_eq!(read_result?, b"readable");
|
||||||
|
|
||||||
|
let blocked_file = readonly_dir.join("blocked.txt");
|
||||||
|
let error = file_system
|
||||||
|
.write_file(
|
||||||
|
&PathUri::from_path(&blocked_file)?,
|
||||||
|
b"blocked".to_vec(),
|
||||||
|
Some(&sandbox),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect_err("write outside the sandbox should fail");
|
||||||
|
assert!(
|
||||||
|
!blocked_file.exists(),
|
||||||
|
"sandboxed fs helper must not create blocked file after error: {error}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_only_sandbox_for_cwd(cwd: std::path::PathBuf) -> Result<FileSystemSandboxContext> {
|
||||||
|
Ok(FileSystemSandboxContext::from_legacy_sandbox_policy(
|
||||||
|
SandboxPolicy::new_read_only_policy(),
|
||||||
|
PathUri::from_path(cwd)?,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_unsupported_restricted_token_host<T>(result: &std::io::Result<T>) -> bool {
|
||||||
|
result.as_ref().err().is_some_and(|err| {
|
||||||
|
err.to_string()
|
||||||
|
.contains("windows sandbox failed: CreateRestrictedToken failed: 87")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -62,6 +62,8 @@ pub struct FileSystemSandboxContext {
|
|||||||
pub permissions: PermissionProfile<PathUri>,
|
pub permissions: PermissionProfile<PathUri>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub cwd: Option<PathUri>,
|
pub cwd: Option<PathUri>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub workspace_roots: Vec<PathUri>,
|
||||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub windows_sandbox_private_desktop: bool,
|
pub windows_sandbox_private_desktop: bool,
|
||||||
@@ -109,6 +111,7 @@ impl FileSystemSandboxContext {
|
|||||||
Self {
|
Self {
|
||||||
permissions: permissions.into(),
|
permissions: permissions.into(),
|
||||||
cwd,
|
cwd,
|
||||||
|
workspace_roots: Vec::new(),
|
||||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||||
windows_sandbox_private_desktop: false,
|
windows_sandbox_private_desktop: false,
|
||||||
use_legacy_landlock: false,
|
use_legacy_landlock: false,
|
||||||
@@ -151,6 +154,7 @@ impl FileSystemSandboxContext {
|
|||||||
pub fn drop_cwd_if_unused(mut self) -> Self {
|
pub fn drop_cwd_if_unused(mut self) -> Self {
|
||||||
if !self.has_cwd_dependent_permissions() {
|
if !self.has_cwd_dependent_permissions() {
|
||||||
self.cwd = None;
|
self.cwd = None;
|
||||||
|
self.workspace_roots.clear();
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ codex-network-proxy = { workspace = true }
|
|||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
codex-utils-absolute-path = { workspace = true }
|
codex-utils-absolute-path = { workspace = true }
|
||||||
codex-utils-path-uri = { workspace = true }
|
codex-utils-path-uri = { workspace = true }
|
||||||
|
codex-windows-sandbox = { workspace = true }
|
||||||
dunce = { workspace = true }
|
dunce = { workspace = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
@@ -25,6 +26,9 @@ tracing = { workspace = true, features = ["log"] }
|
|||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
which = { workspace = true }
|
which = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
codex-utils-home-dir = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ mod manager;
|
|||||||
pub mod policy_transforms;
|
pub mod policy_transforms;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub mod seatbelt;
|
pub mod seatbelt;
|
||||||
|
mod windows;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub use bwrap::find_system_bwrap_in_path;
|
pub use bwrap::find_system_bwrap_in_path;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub use bwrap::system_bwrap_warning;
|
pub use bwrap::system_bwrap_warning;
|
||||||
pub use manager::SandboxCommand;
|
pub use manager::SandboxCommand;
|
||||||
|
pub use manager::SandboxDirectSpawnTransformRequest;
|
||||||
pub use manager::SandboxExecRequest;
|
pub use manager::SandboxExecRequest;
|
||||||
pub use manager::SandboxManager;
|
pub use manager::SandboxManager;
|
||||||
pub use manager::SandboxTransformError;
|
pub use manager::SandboxTransformError;
|
||||||
@@ -20,6 +22,12 @@ pub use manager::SandboxablePreference;
|
|||||||
pub use manager::compatibility_sandbox_policy_for_permission_profile;
|
pub use manager::compatibility_sandbox_policy_for_permission_profile;
|
||||||
pub use manager::get_platform_sandbox;
|
pub use manager::get_platform_sandbox;
|
||||||
pub use manager::with_managed_mitm_ca_readable_root;
|
pub use manager::with_managed_mitm_ca_readable_root;
|
||||||
|
pub use windows::WindowsSandboxFilesystemOverrides;
|
||||||
|
pub use windows::permission_profile_supports_windows_restricted_token_sandbox;
|
||||||
|
pub use windows::resolve_windows_elevated_filesystem_overrides;
|
||||||
|
pub use windows::resolve_windows_restricted_token_filesystem_overrides;
|
||||||
|
pub use windows::unsupported_windows_restricted_token_sandbox_reason;
|
||||||
|
pub use windows::windows_sandbox_uses_elevated_backend;
|
||||||
|
|
||||||
use codex_protocol::error::CodexErr;
|
use codex_protocol::error::CodexErr;
|
||||||
|
|
||||||
@@ -48,6 +56,10 @@ impl From<SandboxTransformError> for CodexErr {
|
|||||||
SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation(
|
SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation(
|
||||||
"seatbelt sandbox is only available on macOS".to_string(),
|
"seatbelt sandbox is only available on macOS".to_string(),
|
||||||
),
|
),
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
SandboxTransformError::WindowsSandboxPreparation(message) => {
|
||||||
|
CodexErr::UnsupportedOperation(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,12 @@ use crate::landlock::allow_network_for_proxy;
|
|||||||
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
use crate::landlock::create_linux_sandbox_command_args_for_permission_profile;
|
||||||
use crate::policy_transforms::effective_permission_profile;
|
use crate::policy_transforms::effective_permission_profile;
|
||||||
use crate::policy_transforms::should_require_platform_sandbox;
|
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_network_proxy::NetworkProxy;
|
||||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||||
use codex_protocol::models::AdditionalPermissionProfile;
|
use codex_protocol::models::AdditionalPermissionProfile;
|
||||||
@@ -21,6 +27,9 @@ use std::ffi::OsString;
|
|||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const WINDOWS_SANDBOX_WRAPPER_SETUP_ENV_ALLOWLIST: &[&str] = &["USERNAME", "USERPROFILE"];
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum SandboxType {
|
pub enum SandboxType {
|
||||||
None,
|
None,
|
||||||
@@ -131,6 +140,16 @@ pub struct SandboxTransformRequest<'a> {
|
|||||||
pub windows_sandbox_private_desktop: bool,
|
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],
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SandboxTransformError {
|
pub enum SandboxTransformError {
|
||||||
InvalidCommandCwd {
|
InvalidCommandCwd {
|
||||||
@@ -146,6 +165,8 @@ pub enum SandboxTransformError {
|
|||||||
Wsl1UnsupportedForBubblewrap,
|
Wsl1UnsupportedForBubblewrap,
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
SeatbeltUnavailable,
|
SeatbeltUnavailable,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
WindowsSandboxPreparation(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for SandboxTransformError {
|
impl std::fmt::Display for SandboxTransformError {
|
||||||
@@ -168,6 +189,10 @@ impl std::fmt::Display for SandboxTransformError {
|
|||||||
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
|
Self::Wsl1UnsupportedForBubblewrap => write!(f, "{WSL1_BWRAP_WARNING}"),
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on 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}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +207,8 @@ impl std::error::Error for SandboxTransformError {
|
|||||||
Self::Wsl1UnsupportedForBubblewrap => None,
|
Self::Wsl1UnsupportedForBubblewrap => None,
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
Self::SeatbeltUnavailable => None,
|
Self::SeatbeltUnavailable => None,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
Self::WindowsSandboxPreparation(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,6 +363,142 @@ impl SandboxManager {
|
|||||||
arg0: arg0_override,
|
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> {
|
||||||
|
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,
|
||||||
|
&request.sandbox_policy_cwd,
|
||||||
|
use_elevated,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
resolve_windows_restricted_token_filesystem_overrides(
|
||||||
|
request.sandbox,
|
||||||
|
&request.permission_profile,
|
||||||
|
&request.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,
|
||||||
|
&request.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(
|
pub fn compatibility_sandbox_policy_for_permission_profile(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use super::SandboxCommand;
|
use super::SandboxCommand;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
use super::SandboxDirectSpawnTransformRequest;
|
||||||
use super::SandboxManager;
|
use super::SandboxManager;
|
||||||
use super::SandboxTransformRequest;
|
use super::SandboxTransformRequest;
|
||||||
use super::SandboxType;
|
use super::SandboxType;
|
||||||
@@ -410,3 +412,153 @@ fn transform_linux_seccomp_uses_helper_alias_when_launcher_is_not_helper_path()
|
|||||||
|
|
||||||
assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string()));
|
assert_eq!(exec_request.arg0, Some("codex-linux-sandbox".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn transform_for_direct_spawn_windows_preserves_only_wrapper_setup_identity() {
|
||||||
|
let mut env = HashMap::from([
|
||||||
|
("Path".to_string(), r"C:\Windows\System32".to_string()),
|
||||||
|
("username".to_string(), "wrong-user".to_string()),
|
||||||
|
("UserProfile".to_string(), r"C:\wrong".to_string()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
super::add_windows_sandbox_wrapper_setup_env_from_vars(
|
||||||
|
&mut env,
|
||||||
|
[
|
||||||
|
("USERNAME", "alice"),
|
||||||
|
("USERPROFILE", r"C:\Users\alice"),
|
||||||
|
("OPENAI_API_KEY", "secret"),
|
||||||
|
]
|
||||||
|
.map(|(key, value)| {
|
||||||
|
(
|
||||||
|
std::ffi::OsString::from(key),
|
||||||
|
std::ffi::OsString::from(value),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
env,
|
||||||
|
HashMap::from([
|
||||||
|
("Path".to_string(), r"C:\Windows\System32".to_string()),
|
||||||
|
("USERNAME".to_string(), "alice".to_string()),
|
||||||
|
("USERPROFILE".to_string(), r"C:\Users\alice".to_string()),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn transform_for_direct_spawn_windows_materializes_inner_helper() {
|
||||||
|
let codex_home = tempfile::TempDir::new().expect("codex home");
|
||||||
|
let helper_dir = tempfile::TempDir::new().expect("helper dir");
|
||||||
|
let configured_helper = helper_dir.path().join("configured-codex-helper.exe");
|
||||||
|
std::fs::write(&configured_helper, b"helper").expect("write configured helper");
|
||||||
|
let cwd = AbsolutePathBuf::from_absolute_path(helper_dir.path()).expect("absolute cwd");
|
||||||
|
let cwd_uri = PathUri::from_abs_path(&cwd);
|
||||||
|
let blocked = cwd.join("blocked");
|
||||||
|
std::fs::create_dir_all(blocked.as_path()).expect("create blocked path");
|
||||||
|
let permissions = PermissionProfile::from_runtime_permissions(
|
||||||
|
&FileSystemSandboxPolicy::restricted(vec![
|
||||||
|
FileSystemSandboxEntry {
|
||||||
|
path: FileSystemPath::Special {
|
||||||
|
value: FileSystemSpecialPath::Root,
|
||||||
|
},
|
||||||
|
access: FileSystemAccessMode::Read,
|
||||||
|
},
|
||||||
|
FileSystemSandboxEntry {
|
||||||
|
path: FileSystemPath::Special {
|
||||||
|
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
|
||||||
|
},
|
||||||
|
access: FileSystemAccessMode::Write,
|
||||||
|
},
|
||||||
|
FileSystemSandboxEntry {
|
||||||
|
path: FileSystemPath::Path { path: blocked },
|
||||||
|
access: FileSystemAccessMode::Deny,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
NetworkSandboxPolicy::Restricted,
|
||||||
|
);
|
||||||
|
let other_workspace = tempfile::TempDir::new().expect("other workspace");
|
||||||
|
let other_workspace_root = AbsolutePathBuf::from_absolute_path(other_workspace.path())
|
||||||
|
.expect("absolute other workspace");
|
||||||
|
let workspace_roots = vec![cwd, other_workspace_root];
|
||||||
|
let manager = SandboxManager::new();
|
||||||
|
let exec_request = manager
|
||||||
|
.transform_for_direct_spawn_with_codex_home(
|
||||||
|
SandboxDirectSpawnTransformRequest {
|
||||||
|
workspace_roots: workspace_roots.as_slice(),
|
||||||
|
transform: SandboxTransformRequest {
|
||||||
|
command: SandboxCommand {
|
||||||
|
program: configured_helper.as_os_str().to_owned(),
|
||||||
|
args: vec!["--codex-run-as-fs-helper".to_string()],
|
||||||
|
cwd: cwd_uri.clone(),
|
||||||
|
env: HashMap::from([(
|
||||||
|
"Path".to_string(),
|
||||||
|
r"C:\Windows\System32".to_string(),
|
||||||
|
)]),
|
||||||
|
additional_permissions: None,
|
||||||
|
},
|
||||||
|
permissions: &permissions,
|
||||||
|
sandbox: SandboxType::WindowsRestrictedToken,
|
||||||
|
enforce_managed_network: false,
|
||||||
|
network: None,
|
||||||
|
sandbox_policy_cwd: &cwd_uri,
|
||||||
|
codex_linux_sandbox_exe: None,
|
||||||
|
use_legacy_landlock: false,
|
||||||
|
windows_sandbox_level: WindowsSandboxLevel::Elevated,
|
||||||
|
windows_sandbox_private_desktop: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
codex_home.path(),
|
||||||
|
)
|
||||||
|
.expect("transform for direct spawn");
|
||||||
|
|
||||||
|
let separator_index = exec_request
|
||||||
|
.command
|
||||||
|
.iter()
|
||||||
|
.position(|arg| arg == "--")
|
||||||
|
.expect("wrapper argv separator");
|
||||||
|
let materialized_helper = std::path::PathBuf::from(&exec_request.command[separator_index + 1]);
|
||||||
|
assert_eq!(exec_request.sandbox, SandboxType::None);
|
||||||
|
assert_eq!(
|
||||||
|
exec_request.command.first(),
|
||||||
|
Some(&configured_helper.display().to_string())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
exec_request
|
||||||
|
.command
|
||||||
|
.iter()
|
||||||
|
.any(|arg| arg == "--run-as-windows-sandbox")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
exec_request
|
||||||
|
.command
|
||||||
|
.iter()
|
||||||
|
.any(|arg| arg == "--deny-read-paths-json")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
exec_request.command[separator_index + 2],
|
||||||
|
"--codex-run-as-fs-helper"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
exec_request
|
||||||
|
.command
|
||||||
|
.windows(2)
|
||||||
|
.filter_map(|args| {
|
||||||
|
(args[0] == "--workspace-root").then_some(std::path::PathBuf::from(&args[1]))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
workspace_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| root.as_path().to_path_buf())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
materialized_helper
|
||||||
|
.parent()
|
||||||
|
.and_then(std::path::Path::file_name),
|
||||||
|
Some(std::ffi::OsStr::new(".sandbox-bin"))
|
||||||
|
);
|
||||||
|
assert!(materialized_helper.exists());
|
||||||
|
}
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||||
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||||
|
use codex_protocol::protocol::WritableRoot;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
|
||||||
|
use crate::SandboxType;
|
||||||
|
use crate::compatibility_sandbox_policy_for_permission_profile;
|
||||||
|
|
||||||
|
/// Resolved filesystem overrides for the Windows sandbox backends.
|
||||||
|
///
|
||||||
|
/// The elevated Windows backend consumes extra deny-read paths plus explicit
|
||||||
|
/// read and write roots during setup/refresh. The unelevated restricted-token
|
||||||
|
/// backend only consumes extra deny-write carveouts on top of the legacy
|
||||||
|
/// `WorkspaceWrite` allow set. Read-root overrides are layered on top of the
|
||||||
|
/// baseline helper roots that the elevated setup path needs to launch the
|
||||||
|
/// sandboxed command; split policies that opt into platform defaults carry
|
||||||
|
/// that explicitly with the override.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct WindowsSandboxFilesystemOverrides {
|
||||||
|
pub read_roots_override: Option<Vec<PathBuf>>,
|
||||||
|
pub read_roots_include_platform_defaults: bool,
|
||||||
|
pub write_roots_override: Option<Vec<PathBuf>>,
|
||||||
|
pub additional_deny_read_paths: Vec<AbsolutePathBuf>,
|
||||||
|
pub additional_deny_write_paths: Vec<AbsolutePathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn windows_sandbox_uses_elevated_backend(
|
||||||
|
sandbox_level: WindowsSandboxLevel,
|
||||||
|
proxy_enforced: bool,
|
||||||
|
) -> bool {
|
||||||
|
// Windows firewall enforcement is tied to the logon-user sandbox identities, so
|
||||||
|
// proxy-enforced sessions must use that backend even when the configured mode is
|
||||||
|
// the default restricted-token sandbox.
|
||||||
|
proxy_enforced || matches!(sandbox_level, WindowsSandboxLevel::Elevated)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn permission_profile_supports_windows_restricted_token_sandbox(
|
||||||
|
permission_profile: &PermissionProfile,
|
||||||
|
) -> bool {
|
||||||
|
match permission_profile {
|
||||||
|
PermissionProfile::Managed { file_system, .. } => {
|
||||||
|
!file_system.to_sandbox_policy().has_full_disk_write_access()
|
||||||
|
}
|
||||||
|
PermissionProfile::Disabled | PermissionProfile::External { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsupported_windows_restricted_token_sandbox_reason(
|
||||||
|
sandbox: SandboxType,
|
||||||
|
permission_profile: &PermissionProfile,
|
||||||
|
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||||
|
windows_sandbox_level: WindowsSandboxLevel,
|
||||||
|
) -> Option<String> {
|
||||||
|
if windows_sandbox_level == WindowsSandboxLevel::Elevated {
|
||||||
|
resolve_windows_elevated_filesystem_overrides(
|
||||||
|
sandbox,
|
||||||
|
permission_profile,
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
windows_sandbox_level == WindowsSandboxLevel::Elevated,
|
||||||
|
)
|
||||||
|
.err()
|
||||||
|
} else {
|
||||||
|
resolve_windows_restricted_token_filesystem_overrides(
|
||||||
|
sandbox,
|
||||||
|
permission_profile,
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
windows_sandbox_level,
|
||||||
|
)
|
||||||
|
.err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_windows_restricted_token_filesystem_overrides(
|
||||||
|
sandbox: SandboxType,
|
||||||
|
permission_profile: &PermissionProfile,
|
||||||
|
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||||
|
windows_sandbox_level: WindowsSandboxLevel,
|
||||||
|
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
|
||||||
|
if sandbox != SandboxType::WindowsRestrictedToken
|
||||||
|
|| windows_sandbox_level == WindowsSandboxLevel::Elevated
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||||
|
permission_profile.to_runtime_permissions();
|
||||||
|
|
||||||
|
let needs_direct_runtime_enforcement = file_system_sandbox_policy
|
||||||
|
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
|
||||||
|
|
||||||
|
if permission_profile_supports_windows_restricted_token_sandbox(permission_profile)
|
||||||
|
&& !needs_direct_runtime_enforcement
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) {
|
||||||
|
let permission_profile_name = permission_profile_display_name(permission_profile);
|
||||||
|
return Err(format!(
|
||||||
|
"windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, permission_profile={permission_profile_name}; refusing to run unsandboxed",
|
||||||
|
file_system_sandbox_policy.kind,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The restricted-token backend can still enforce split write restrictions,
|
||||||
|
// but its WRITE_RESTRICTED token does not make capability SID deny-read ACEs
|
||||||
|
// participate in read access checks. Read restrictions therefore require the
|
||||||
|
// elevated backend, even when the filesystem root remains readable.
|
||||||
|
if !windows_policy_has_root_read_access(&file_system_sandbox_policy, sandbox_policy_cwd) {
|
||||||
|
return Err(
|
||||||
|
"windows unelevated restricted-token sandbox cannot enforce split filesystem read restrictions directly; refusing to run unsandboxed"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths(
|
||||||
|
&file_system_sandbox_policy,
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
)?;
|
||||||
|
if !additional_deny_read_paths.is_empty() {
|
||||||
|
return Err(
|
||||||
|
"windows unelevated restricted-token sandbox cannot enforce deny-read restrictions directly; refusing to run unsandboxed"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let legacy_projection = compatibility_sandbox_policy_for_permission_profile(
|
||||||
|
permission_profile,
|
||||||
|
sandbox_policy_cwd.as_path(),
|
||||||
|
);
|
||||||
|
let legacy_writable_roots = legacy_projection.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
||||||
|
let split_writable_roots =
|
||||||
|
file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
||||||
|
let legacy_root_paths: BTreeSet<PathBuf> = legacy_writable_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| normalize_windows_override_path(root.root.as_path()))
|
||||||
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
let split_root_paths: BTreeSet<PathBuf> = split_writable_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| normalize_windows_override_path(root.root.as_path()))
|
||||||
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
|
||||||
|
if legacy_root_paths != split_root_paths {
|
||||||
|
return Err(
|
||||||
|
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for writable_root in &split_writable_roots {
|
||||||
|
for read_only_subpath in &writable_root.read_only_subpaths {
|
||||||
|
if split_writable_roots.iter().any(|candidate| {
|
||||||
|
candidate.root.as_path() != writable_root.root.as_path()
|
||||||
|
&& candidate
|
||||||
|
.root
|
||||||
|
.as_path()
|
||||||
|
.starts_with(read_only_subpath.as_path())
|
||||||
|
}) {
|
||||||
|
return Err(
|
||||||
|
"windows unelevated restricted-token sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut additional_deny_write_paths = BTreeSet::new();
|
||||||
|
for split_root in &split_writable_roots {
|
||||||
|
let split_root_path = normalize_windows_override_path(split_root.root.as_path())?;
|
||||||
|
let Some(legacy_root) = legacy_writable_roots.iter().find(|candidate| {
|
||||||
|
normalize_windows_override_path(candidate.root.as_path())
|
||||||
|
.is_ok_and(|candidate_path| candidate_path == split_root_path)
|
||||||
|
}) else {
|
||||||
|
return Err(
|
||||||
|
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
for read_only_subpath in &split_root.read_only_subpaths {
|
||||||
|
if !legacy_root
|
||||||
|
.read_only_subpaths
|
||||||
|
.iter()
|
||||||
|
.any(|candidate| candidate == read_only_subpath)
|
||||||
|
{
|
||||||
|
additional_deny_write_paths.insert(normalize_windows_override_path(
|
||||||
|
read_only_subpath.as_path(),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if additional_deny_read_paths.is_empty() && additional_deny_write_paths.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(WindowsSandboxFilesystemOverrides {
|
||||||
|
read_roots_override: None,
|
||||||
|
read_roots_include_platform_defaults: false,
|
||||||
|
write_roots_override: None,
|
||||||
|
additional_deny_read_paths,
|
||||||
|
additional_deny_write_paths: additional_deny_write_paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
|
||||||
|
.collect::<std::result::Result<_, _>>()?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_windows_elevated_filesystem_overrides(
|
||||||
|
sandbox: SandboxType,
|
||||||
|
permission_profile: &PermissionProfile,
|
||||||
|
sandbox_policy_cwd: &AbsolutePathBuf,
|
||||||
|
use_windows_elevated_backend: bool,
|
||||||
|
) -> std::result::Result<Option<WindowsSandboxFilesystemOverrides>, String> {
|
||||||
|
if sandbox != SandboxType::WindowsRestrictedToken || !use_windows_elevated_backend {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (file_system_sandbox_policy, network_sandbox_policy) =
|
||||||
|
permission_profile.to_runtime_permissions();
|
||||||
|
|
||||||
|
if !permission_profile_supports_windows_restricted_token_sandbox(permission_profile) {
|
||||||
|
let permission_profile_name = permission_profile_display_name(permission_profile);
|
||||||
|
return Err(format!(
|
||||||
|
"windows sandbox backend cannot enforce file_system={:?}, network={network_sandbox_policy:?}, permission_profile={permission_profile_name}; refusing to run unsandboxed",
|
||||||
|
file_system_sandbox_policy.kind,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let additional_deny_read_paths = codex_windows_sandbox::resolve_windows_deny_read_paths(
|
||||||
|
&file_system_sandbox_policy,
|
||||||
|
sandbox_policy_cwd,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let split_writable_roots =
|
||||||
|
file_system_sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
||||||
|
if has_reopened_writable_descendant(&split_writable_roots) {
|
||||||
|
return Err(
|
||||||
|
"windows elevated sandbox cannot reopen writable descendants under read-only carveouts directly; refusing to run unsandboxed"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let needs_direct_runtime_enforcement = file_system_sandbox_policy
|
||||||
|
.needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd);
|
||||||
|
let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path);
|
||||||
|
let legacy_projection = compatibility_sandbox_policy_for_permission_profile(
|
||||||
|
permission_profile,
|
||||||
|
sandbox_policy_cwd.as_path(),
|
||||||
|
);
|
||||||
|
let legacy_writable_roots = legacy_projection.get_writable_roots_with_cwd(sandbox_policy_cwd);
|
||||||
|
let legacy_root_paths: BTreeSet<PathBuf> = legacy_writable_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| normalize_path(root.root.to_path_buf()))
|
||||||
|
.collect();
|
||||||
|
let split_readable_roots: Vec<PathBuf> = file_system_sandbox_policy
|
||||||
|
.get_readable_roots_with_cwd(sandbox_policy_cwd)
|
||||||
|
.into_iter()
|
||||||
|
.map(AbsolutePathBuf::into_path_buf)
|
||||||
|
.map(&normalize_path)
|
||||||
|
.collect();
|
||||||
|
let split_root_paths: Vec<PathBuf> = split_writable_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| normalize_path(root.root.to_path_buf()))
|
||||||
|
.collect();
|
||||||
|
let split_root_path_set: BTreeSet<PathBuf> = split_root_paths.iter().cloned().collect();
|
||||||
|
|
||||||
|
// `has_full_disk_read_access()` is intentionally false when deny-read
|
||||||
|
// entries exist. For Windows setup overrides, the important question is
|
||||||
|
// whether the baseline still reads from the filesystem root and only needs
|
||||||
|
// additional deny ACLs layered on top.
|
||||||
|
let split_has_root_read_access =
|
||||||
|
windows_policy_has_root_read_access(&file_system_sandbox_policy, sandbox_policy_cwd);
|
||||||
|
let read_roots_override = if split_has_root_read_access {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(split_readable_roots)
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_roots_override = if split_root_path_set == legacy_root_paths {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(split_root_paths)
|
||||||
|
};
|
||||||
|
|
||||||
|
let additional_deny_write_paths = if needs_direct_runtime_enforcement {
|
||||||
|
let mut deny_paths = BTreeSet::new();
|
||||||
|
for writable_root in &split_writable_roots {
|
||||||
|
let writable_root_path = normalize_path(writable_root.root.to_path_buf());
|
||||||
|
let legacy_root = legacy_writable_roots.iter().find(|candidate| {
|
||||||
|
normalize_path(candidate.root.to_path_buf()) == writable_root_path
|
||||||
|
});
|
||||||
|
for read_only_subpath in &writable_root.read_only_subpaths {
|
||||||
|
let read_only_subpath_suffix = read_only_subpath
|
||||||
|
.as_path()
|
||||||
|
.strip_prefix(writable_root.root.as_path())
|
||||||
|
.ok();
|
||||||
|
let already_denied_by_legacy = legacy_root.is_some_and(|legacy_root| {
|
||||||
|
legacy_root.read_only_subpaths.iter().any(|candidate| {
|
||||||
|
candidate
|
||||||
|
.as_path()
|
||||||
|
.strip_prefix(legacy_root.root.as_path())
|
||||||
|
.ok()
|
||||||
|
== read_only_subpath_suffix
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !already_denied_by_legacy {
|
||||||
|
deny_paths.insert(normalize_path(read_only_subpath.to_path_buf()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deny_paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
|
||||||
|
.collect::<std::result::Result<_, _>>()?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
if read_roots_override.is_none()
|
||||||
|
&& write_roots_override.is_none()
|
||||||
|
&& additional_deny_read_paths.is_empty()
|
||||||
|
&& additional_deny_write_paths.is_empty()
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(WindowsSandboxFilesystemOverrides {
|
||||||
|
read_roots_include_platform_defaults: read_roots_override.is_some()
|
||||||
|
&& file_system_sandbox_policy.include_platform_defaults(),
|
||||||
|
read_roots_override,
|
||||||
|
write_roots_override,
|
||||||
|
additional_deny_read_paths,
|
||||||
|
additional_deny_write_paths,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_windows_override_path(path: &Path) -> std::result::Result<PathBuf, String> {
|
||||||
|
AbsolutePathBuf::from_absolute_path(dunce::simplified(path))
|
||||||
|
.map(AbsolutePathBuf::into_path_buf)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn windows_policy_has_root_read_access(
|
||||||
|
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||||
|
cwd: &AbsolutePathBuf,
|
||||||
|
) -> bool {
|
||||||
|
let Some(root) = cwd.as_path().ancestors().last() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
file_system_sandbox_policy.can_read_path_with_cwd(root, cwd.as_path())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn permission_profile_display_name(permission_profile: &PermissionProfile) -> &'static str {
|
||||||
|
match permission_profile {
|
||||||
|
PermissionProfile::Managed { .. } => "Managed",
|
||||||
|
PermissionProfile::Disabled => "Disabled",
|
||||||
|
PermissionProfile::External { .. } => "External",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_reopened_writable_descendant(writable_roots: &[WritableRoot]) -> bool {
|
||||||
|
writable_roots.iter().any(|writable_root| {
|
||||||
|
writable_root
|
||||||
|
.read_only_subpaths
|
||||||
|
.iter()
|
||||||
|
.any(|read_only_subpath| {
|
||||||
|
writable_roots.iter().any(|candidate| {
|
||||||
|
candidate.root.as_path() != writable_root.root.as_path()
|
||||||
|
&& candidate
|
||||||
|
.root
|
||||||
|
.as_path()
|
||||||
|
.starts_with(read_only_subpath.as_path())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -96,22 +96,26 @@ pub fn resolve_current_exe_for_launch(codex_home: &Path, fallback_executable: &s
|
|||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(_) => return PathBuf::from(fallback_executable),
|
Err(_) => return PathBuf::from(fallback_executable),
|
||||||
};
|
};
|
||||||
|
resolve_exe_for_launch(&source, codex_home)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_exe_for_launch(source: &Path, codex_home: &Path) -> PathBuf {
|
||||||
let Some(file_name) = source.file_name() else {
|
let Some(file_name) = source.file_name() else {
|
||||||
return source;
|
return source.to_path_buf();
|
||||||
};
|
};
|
||||||
let destination = helper_bin_dir(codex_home).join(file_name);
|
let destination = helper_bin_dir(codex_home).join(file_name);
|
||||||
match copy_from_source_if_needed(&source, &destination) {
|
match copy_from_source_if_needed(source, &destination) {
|
||||||
Ok(_) => destination,
|
Ok(_) => destination,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let sandbox_log_dir = crate::sandbox_dir(codex_home);
|
let sandbox_log_dir = crate::sandbox_dir(codex_home);
|
||||||
log_note(
|
log_note(
|
||||||
&format!(
|
&format!(
|
||||||
"helper copy failed for current executable: {err:#}; falling back to legacy path {}",
|
"helper copy failed for executable: {err:#}; falling back to legacy path {}",
|
||||||
source.display()
|
source.display()
|
||||||
),
|
),
|
||||||
Some(&sandbox_log_dir),
|
Some(&sandbox_log_dir),
|
||||||
);
|
);
|
||||||
source
|
source.to_path_buf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use helper_materialization::resolve_current_exe_for_launch;
|
pub use helper_materialization::resolve_current_exe_for_launch;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use helper_materialization::resolve_exe_for_launch;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub use hide_users::hide_current_user_profile_dir;
|
pub use hide_users::hide_current_user_profile_dir;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use hide_users::hide_newly_created_users;
|
pub use hide_users::hide_newly_created_users;
|
||||||
|
|||||||
Reference in new issue
Block a user