Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 85 additions & 21 deletions codex-rs/windows-sandbox-rs/src/elevated_impl.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
Expand All @@ -20,8 +21,26 @@ pub struct ElevatedSandboxCaptureRequest<'a> {
pub deny_write_paths_override: &'a [AbsolutePathBuf],
}

pub struct ElevatedSandboxProfileCaptureRequest<'a> {
pub permission_profile: &'a PermissionProfile,
pub permission_profile_cwd: &'a Path,
pub codex_home: &'a Path,
pub command: Vec<String>,
pub cwd: &'a Path,
pub env_map: HashMap<String, String>,
pub timeout_ms: Option<u64>,
pub use_private_desktop: bool,
pub proxy_enforced: bool,
pub read_roots_override: Option<&'a [PathBuf]>,
pub read_roots_include_platform_defaults: bool,
pub write_roots_override: Option<&'a [PathBuf]>,
pub deny_read_paths_override: &'a [AbsolutePathBuf],
pub deny_write_paths_override: &'a [AbsolutePathBuf],
}

mod windows_impl {
use super::ElevatedSandboxCaptureRequest;
use super::ElevatedSandboxProfileCaptureRequest;
use crate::acl::allow_null_device;
use crate::cap::load_or_create_cap_sids;
use crate::cap::workspace_write_cap_sid_for_root;
Expand All @@ -37,7 +56,6 @@ mod windows_impl {
use crate::logging::log_failure;
use crate::logging::log_start;
use crate::logging::log_success;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::runner_client::spawn_runner_transport;
Expand All @@ -54,12 +72,12 @@ mod windows_impl {

/// Launches the command runner under the sandbox user and captures its output.
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
request: ElevatedSandboxCaptureRequest<'_>,
pub fn run_windows_sandbox_capture_for_permission_profile(
request: ElevatedSandboxProfileCaptureRequest<'_>,
) -> Result<CaptureResult> {
let ElevatedSandboxCaptureRequest {
policy_json_or_preset,
sandbox_policy_cwd,
let ElevatedSandboxProfileCaptureRequest {
permission_profile,
permission_profile_cwd,
codex_home,
command,
cwd,
Expand All @@ -73,6 +91,10 @@ mod windows_impl {
deny_read_paths_override,
deny_write_paths_override,
} = request;
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
permission_profile,
permission_profile_cwd,
)?;
let deny_read_paths_override = deny_read_paths_override
.iter()
.map(AbsolutePathBuf::to_path_buf)
Expand All @@ -81,11 +103,6 @@ mod windows_impl {
.iter()
.map(AbsolutePathBuf::to_path_buf)
.collect::<Vec<_>>();
let policy = parse_policy(policy_json_or_preset)?;
let permissions = ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
&policy,
sandbox_policy_cwd,
);
normalize_null_device_env(&mut env_map);
ensure_non_interactive_pager(&mut env_map);
inherit_path_env(&mut env_map);
Expand All @@ -109,12 +126,6 @@ mod windows_impl {
proxy_enforced,
)?;
// Build capability SID for ACL grants.
if matches!(
&policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
}
let caps = load_or_create_cap_sids(codex_home)?;
let (sid_for_null, cap_sids) = if permissions.uses_write_capabilities_for_cwd(cwd, &env_map)
{
Expand Down Expand Up @@ -143,14 +154,12 @@ mod windows_impl {
}

(|| -> Result<CaptureResult> {
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd);
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
env: env_map.clone(),
permission_profile,
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
permission_profile: permission_profile.clone(),
permission_profile_cwd: permission_profile_cwd.to_path_buf(),
codex_home: sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids,
Expand Down Expand Up @@ -210,6 +219,48 @@ mod windows_impl {
})()
}

/// Legacy policy-string adapter for callers that have not moved to permission profiles yet.
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture(
request: ElevatedSandboxCaptureRequest<'_>,
) -> Result<CaptureResult> {
let ElevatedSandboxCaptureRequest {
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
use_private_desktop,
proxy_enforced,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
deny_read_paths_override,
deny_write_paths_override,
} = request;
let policy = parse_policy(policy_json_or_preset)?;
let permission_profile =
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd);
run_windows_sandbox_capture_for_permission_profile(ElevatedSandboxProfileCaptureRequest {
permission_profile: &permission_profile,
permission_profile_cwd: sandbox_policy_cwd,
codex_home,
command,
cwd,
env_map,
timeout_ms,
use_private_desktop,
proxy_enforced,
read_roots_override,
read_roots_include_platform_defaults,
write_roots_override,
deny_read_paths_override,
deny_write_paths_override,
})
}

#[cfg(test)]
mod tests {
use crate::policy::SandboxPolicy;
Expand Down Expand Up @@ -242,10 +293,13 @@ mod windows_impl {

#[cfg(target_os = "windows")]
pub use windows_impl::run_windows_sandbox_capture;
#[cfg(target_os = "windows")]
pub use windows_impl::run_windows_sandbox_capture_for_permission_profile;

#[cfg(not(target_os = "windows"))]
mod stub {
use super::ElevatedSandboxCaptureRequest;
use super::ElevatedSandboxProfileCaptureRequest;
use anyhow::Result;
use anyhow::bail;

Expand All @@ -264,7 +318,17 @@ mod stub {
) -> Result<CaptureResult> {
bail!("Windows sandbox is only available on Windows")
}

/// Stub implementation for non-Windows targets; sandboxing only works on Windows.
#[allow(clippy::too_many_arguments)]
pub fn run_windows_sandbox_capture_for_permission_profile(
_request: ElevatedSandboxProfileCaptureRequest<'_>,
) -> Result<CaptureResult> {
bail!("Windows sandbox is only available on Windows")
}
}

#[cfg(not(target_os = "windows"))]
pub use stub::run_windows_sandbox_capture;
#[cfg(not(target_os = "windows"))]
pub use stub::run_windows_sandbox_capture_for_permission_profile;
6 changes: 6 additions & 0 deletions codex-rs/windows-sandbox-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ pub use dpapi::unprotect as dpapi_unprotect;
#[cfg(target_os = "windows")]
pub use elevated_impl::ElevatedSandboxCaptureRequest;
#[cfg(target_os = "windows")]
pub use elevated_impl::ElevatedSandboxProfileCaptureRequest;
#[cfg(target_os = "windows")]
pub use elevated_impl::run_windows_sandbox_capture as run_windows_sandbox_capture_elevated;
#[cfg(target_os = "windows")]
pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run_windows_sandbox_capture_for_permission_profile_elevated;
#[cfg(target_os = "windows")]
pub use helper_materialization::resolve_current_exe_for_launch;
#[cfg(target_os = "windows")]
pub use hide_users::hide_current_user_profile_dir;
Expand Down Expand Up @@ -258,6 +262,8 @@ pub use token::get_current_token_for_restriction;
#[cfg(target_os = "windows")]
pub use unified_exec::spawn_windows_sandbox_session_elevated;
#[cfg(target_os = "windows")]
pub use unified_exec::spawn_windows_sandbox_session_elevated_for_permission_profile;
#[cfg(target_os = "windows")]
pub use unified_exec::spawn_windows_sandbox_session_legacy;
#[cfg(target_os = "windows")]
pub use wfp::install_wfp_filters_for_account;
Expand Down
56 changes: 54 additions & 2 deletions codex-rs/windows-sandbox-rs/src/resolved_permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ pub fn token_mode_for_permission_profile(
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Result<WindowsSandboxTokenMode> {
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile(permission_profile)?;
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
permission_profile,
cwd,
)?;
if permissions.file_system.has_full_disk_write_access() {
anyhow::bail!(
"permission profile requests full-disk filesystem writes, which cannot be enforced by the Windows sandbox"
Expand Down Expand Up @@ -82,6 +84,19 @@ impl ResolvedWindowsSandboxPermissions {
})
}

/// Resolves a managed permission profile and binds symbolic `:workspace_roots`
/// entries to the permission root supplied by the caller.
pub fn try_from_permission_profile_for_cwd(
permission_profile: &PermissionProfile,
cwd: &Path,
) -> Result<Self> {
let mut permissions = Self::try_from_permission_profile(permission_profile)?;
permissions.file_system = permissions
.file_system
.materialize_project_roots_with_cwd(cwd);
Ok(permissions)
}

pub(crate) fn should_apply_network_block(&self) -> bool {
!self.network.is_enabled()
}
Expand Down Expand Up @@ -259,6 +274,43 @@ mod tests {
);
}

#[test]
fn permission_profile_workspace_root_stays_bound_to_profile_cwd() {
let tmp = TempDir::new().expect("tempdir");
let profile_cwd = tmp.path().join("workspace");
let command_cwd = profile_cwd.join("subdir");
std::fs::create_dir_all(&command_cwd).expect("create command cwd");

let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd(
&permission_profile,
&profile_cwd,
)
.expect("managed permission profile");

let roots = permissions
.writable_roots_for_cwd(&command_cwd, &HashMap::new())
.into_iter()
.map(|root| root.root)
.collect::<Vec<_>>();

assert_eq!(
roots,
vec![dunce::canonicalize(&profile_cwd).expect("canonical profile cwd")]
);
}

#[test]
fn token_mode_for_profile_without_writable_roots_uses_readonly_capability() {
let tmp = TempDir::new().expect("tempdir");
Expand Down
Loading
Loading