Compare commits

...

4 Commits

Author SHA1 Message Date
Michael Bolin
ec92703f8a windows-sandbox: feed setup from resolved permissions 2026-05-17 10:33:07 -07:00
Michael Bolin
49a9062253 windows-sandbox: drive write roots from resolved permissions 2026-05-15 17:38:07 -07:00
Michael Bolin
56238d2fa2 windows-sandbox: send permission profiles to elevated runner 2026-05-15 15:50:23 -07:00
Michael Bolin
86e11d7697 windows-sandbox: add resolved permissions helper 2026-05-15 15:50:23 -07:00
14 changed files with 914 additions and 275 deletions

View File

@@ -179,10 +179,13 @@ pub fn run_elevated_setup(
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> anyhow::Result<()> {
let permissions =
codex_windows_sandbox::ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
policy, policy_cwd,
);
codex_windows_sandbox::run_elevated_setup(
codex_windows_sandbox::SandboxSetupRequest {
policy,
policy_cwd,
permissions: &permissions,
command_cwd,
env_map,
codex_home,

View File

@@ -56,7 +56,22 @@ fn stage_windows_sandbox_helpers() -> anyhow::Result<()> {
for helper_name in ["codex-windows-sandbox-setup", "codex-command-runner"] {
let helper = codex_utils_cargo_bin::cargo_bin(helper_name)?;
let file_name = Path::new(helper_name).with_extension("exe");
std::fs::copy(helper, resources_dir.join(file_name))?;
let destination = resources_dir.join(file_name);
if let Err(err) = std::fs::copy(&helper, &destination) {
// A sandbox helper can briefly remain alive after the sandboxed
// command exits. Bazel may retry the test while that process still
// has the staged executable open, so keep the already-staged copy.
if err.kind() == std::io::ErrorKind::PermissionDenied && destination.exists() {
continue;
}
return Err(err).with_context(|| {
format!(
"stage Windows sandbox helper {} at {}",
helper.display(),
destination.display()
)
});
}
}
Ok(())
}

View File

@@ -1,4 +1,4 @@
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use dunce::canonicalize;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -11,9 +11,8 @@ pub struct AllowDenyPaths {
pub deny: HashSet<PathBuf>,
}
pub fn compute_allow_paths(
policy: &SandboxPolicy,
policy_cwd: &Path,
pub(crate) fn compute_allow_paths_for_permissions(
permissions: &ResolvedWindowsSandboxPermissions,
command_cwd: &Path,
env_map: &HashMap<String, String>,
) -> AllowDenyPaths {
@@ -30,65 +29,15 @@ pub fn compute_allow_paths(
deny.insert(p);
}
};
let include_tmp_env_vars = matches!(
policy,
SandboxPolicy::WorkspaceWrite {
exclude_tmpdir_env_var: false,
..
}
);
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
let add_writable_root =
|root: PathBuf,
policy_cwd: &Path,
add_allow: &mut dyn FnMut(PathBuf),
add_deny: &mut dyn FnMut(PathBuf)| {
let candidate = if root.is_absolute() {
root
} else {
policy_cwd.join(root)
};
let canonical = canonicalize(&candidate).unwrap_or(candidate);
add_allow(canonical.clone());
for protected_subdir in [".git", ".codex", ".agents"] {
let protected_entry = canonical.join(protected_subdir);
if protected_entry.exists() {
add_deny(protected_entry);
}
}
};
add_writable_root(
command_cwd.to_path_buf(),
policy_cwd,
&mut add_allow_path,
&mut add_deny_path,
);
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
for root in writable_roots {
add_writable_root(
root.clone().into(),
policy_cwd,
&mut add_allow_path,
&mut add_deny_path,
);
}
}
}
if include_tmp_env_vars {
for key in ["TEMP", "TMP"] {
if let Some(v) = env_map.get(key) {
let abs = PathBuf::from(v);
add_allow_path(abs);
} else if let Ok(v) = std::env::var(key) {
let abs = PathBuf::from(v);
add_allow_path(abs);
}
for writable_root in permissions.writable_roots_for_cwd(command_cwd, env_map) {
let canonical = canonicalize(&writable_root.root).unwrap_or(writable_root.root);
add_allow_path(canonical);
for read_only_subpath in writable_root.read_only_subpaths {
add_deny_path(read_only_subpath);
}
}
AllowDenyPaths { allow, deny }
}
@@ -100,6 +49,17 @@ mod tests {
use std::fs;
use tempfile::TempDir;
fn compute_allow_paths(
policy: &SandboxPolicy,
policy_cwd: &Path,
command_cwd: &Path,
env_map: &HashMap<String, String>,
) -> AllowDenyPaths {
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
compute_allow_paths_for_permissions(&permissions, command_cwd, env_map)
}
#[test]
fn includes_additional_writable_roots() {
let tmp = TempDir::new().expect("tempdir");
@@ -130,6 +90,35 @@ mod tests {
assert!(paths.deny.is_empty(), "no deny paths expected");
}
#[test]
fn uses_policy_cwd_for_legacy_workspace_root() {
let tmp = TempDir::new().expect("tempdir");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let paths = compute_allow_paths(&policy, &policy_cwd, &command_cwd, &HashMap::new());
assert!(
paths
.allow
.contains(&dunce::canonicalize(&policy_cwd).unwrap())
);
assert!(
!paths
.allow
.contains(&dunce::canonicalize(&command_cwd).unwrap())
);
assert!(paths.deny.is_empty(), "no deny paths expected");
}
#[test]
fn excludes_tmp_env_vars_when_requested() {
let tmp = TempDir::new().expect("tempdir");
@@ -146,6 +135,7 @@ mod tests {
};
let mut env_map = HashMap::new();
env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map);
@@ -162,6 +152,59 @@ mod tests {
assert!(paths.deny.is_empty(), "no deny paths expected");
}
#[test]
fn includes_tmp_env_vars_when_requested() {
let tmp = TempDir::new().expect("tempdir");
let command_cwd = tmp.path().join("workspace");
let temp_dir = tmp.path().join("temp");
let _ = fs::create_dir_all(&command_cwd);
let _ = fs::create_dir_all(&temp_dir);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
};
let mut env_map = HashMap::new();
env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string());
env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string());
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map);
let expected_allow: HashSet<PathBuf> = [
dunce::canonicalize(&command_cwd).unwrap(),
dunce::canonicalize(&temp_dir).unwrap(),
]
.into_iter()
.collect();
assert_eq!(expected_allow, paths.allow);
assert!(paths.deny.is_empty(), "no deny paths expected");
}
#[test]
fn ignores_unix_slash_tmp_for_windows_allow_roots() {
let tmp = TempDir::new().expect("tempdir");
let command_cwd = tmp.path().join("workspace");
let _ = fs::create_dir_all(&command_cwd);
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
};
let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new());
let expected_allow: HashSet<PathBuf> = [dunce::canonicalize(&command_cwd).unwrap()]
.into_iter()
.collect();
assert_eq!(expected_allow, paths.allow);
assert!(paths.deny.is_empty(), "no deny paths expected");
}
#[test]
fn denies_git_dir_inside_writable_root() {
let tmp = TempDir::new().expect("tempdir");

View File

@@ -8,7 +8,8 @@ use crate::logging::debug_log;
use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::policy::SandboxPolicy;
use crate::setup::effective_write_roots_for_setup;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
use crate::token::world_sid;
use anyhow::Result;
@@ -223,15 +224,33 @@ pub fn apply_world_writable_scan_and_denies(
env_map: &std::collections::HashMap<String, String>,
sandbox_policy: &SandboxPolicy,
logs_base_dir: Option<&Path>,
) -> Result<()> {
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(sandbox_policy, cwd);
apply_world_writable_scan_and_denies_for_permissions(
codex_home,
cwd,
env_map,
&permissions,
logs_base_dir,
)
}
pub fn apply_world_writable_scan_and_denies_for_permissions(
codex_home: &Path,
cwd: &Path,
env_map: &std::collections::HashMap<String, String>,
permissions: &ResolvedWindowsSandboxPermissions,
logs_base_dir: Option<&Path>,
) -> Result<()> {
let flagged = audit_everyone_writable(cwd, env_map, logs_base_dir)?;
if flagged.is_empty() {
return Ok(());
}
if let Err(err) = apply_capability_denies_for_world_writable(
if let Err(err) = apply_capability_denies_for_world_writable_for_permissions(
codex_home,
&flagged,
sandbox_policy,
permissions,
cwd,
env_map,
logs_base_dir,
@@ -244,10 +263,10 @@ pub fn apply_world_writable_scan_and_denies(
Ok(())
}
pub fn apply_capability_denies_for_world_writable(
fn apply_capability_denies_for_world_writable_for_permissions(
codex_home: &Path,
flagged: &[PathBuf],
sandbox_policy: &SandboxPolicy,
permissions: &ResolvedWindowsSandboxPermissions,
cwd: &Path,
env_map: &std::collections::HashMap<String, String>,
logs_base_dir: Option<&Path>,
@@ -259,11 +278,13 @@ pub fn apply_capability_denies_for_world_writable(
let cap_path = cap_sid_file(codex_home);
let caps = load_or_create_cap_sids(codex_home)?;
std::fs::write(&cap_path, serde_json::to_string(&caps)?)?;
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) = match sandbox_policy {
SandboxPolicy::WorkspaceWrite { .. } => {
let roots = effective_write_roots_for_setup(
sandbox_policy,
cwd,
if !permissions.is_enforceable_by_windows_sandbox() {
return Ok(());
}
let (active_sids, workspace_roots): (Vec<LocalSid>, Vec<PathBuf>) =
if permissions.uses_write_capabilities_for_cwd(cwd, env_map) {
let roots = effective_write_roots_for_permissions(
permissions,
cwd,
env_map,
codex_home,
@@ -277,14 +298,9 @@ pub fn apply_capability_denies_for_world_writable(
})
.collect::<Result<Vec<_>>>()?;
(active_sids, roots)
}
SandboxPolicy::ReadOnly { .. } => {
} else {
(vec![LocalSid::from_string(&caps.readonly)?], Vec::new())
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
return Ok(());
}
};
};
for path in flagged {
if workspace_roots
.iter()

View File

@@ -22,11 +22,11 @@ use codex_windows_sandbox::OutputPayload;
use codex_windows_sandbox::OutputStream;
use codex_windows_sandbox::PipeSpawnHandles;
use codex_windows_sandbox::ResizePayload;
use codex_windows_sandbox::SandboxPolicy;
use codex_windows_sandbox::SpawnReady;
use codex_windows_sandbox::SpawnRequest;
use codex_windows_sandbox::StderrMode;
use codex_windows_sandbox::StdinMode;
use codex_windows_sandbox::WindowsSandboxTokenMode;
use codex_windows_sandbox::allow_null_device;
use codex_windows_sandbox::create_readonly_token_with_caps_and_user_from;
use codex_windows_sandbox::create_workspace_write_token_with_caps_and_user_from;
@@ -35,11 +35,11 @@ use codex_windows_sandbox::encode_bytes;
use codex_windows_sandbox::get_current_token_for_restriction;
use codex_windows_sandbox::hide_current_user_profile_dir;
use codex_windows_sandbox::log_note;
use codex_windows_sandbox::parse_policy;
use codex_windows_sandbox::read_frame;
use codex_windows_sandbox::read_handle_loop;
use codex_windows_sandbox::spawn_process_with_pipes;
use codex_windows_sandbox::to_wide;
use codex_windows_sandbox::token_mode_for_permission_profile;
use codex_windows_sandbox::write_frame;
use std::ffi::OsStr;
use std::fs::File;
@@ -235,7 +235,12 @@ fn effective_cwd(req_cwd: &Path, log_dir: Option<&Path>) -> PathBuf {
fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
let log_dir = req.codex_home.clone();
hide_current_user_profile_dir(req.codex_home.as_path());
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
let token_mode = token_mode_for_permission_profile(
&req.permission_profile,
&req.permission_profile_cwd,
&req.env,
)
.context("resolve permission profile token mode")?;
let mut cap_psids: Vec<LocalSid> = Vec::new();
for sid in &req.cap_sids {
cap_psids.push(
@@ -253,16 +258,13 @@ fn spawn_ipc_process(req: &SpawnRequest) -> Result<IpcSpawnedProcess> {
let cap_psid_ptrs: Vec<*mut _> = cap_psids.iter().map(LocalSid::as_ptr).collect();
let base = OwnedWinHandle::new(unsafe { get_current_token_for_restriction()? });
let h_token = OwnedWinHandle::new(unsafe {
match &policy {
SandboxPolicy::ReadOnly { .. } => {
match token_mode {
WindowsSandboxTokenMode::ReadOnlyCapability => {
create_readonly_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
}
SandboxPolicy::WorkspaceWrite { .. } => {
WindowsSandboxTokenMode::WriteCapabilityRoots => {
create_workspace_write_token_with_caps_and_user_from(base.raw(), &cap_psid_ptrs)
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
unreachable!()
}
}
}?);
unsafe {

View File

@@ -10,6 +10,7 @@
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use codex_protocol::models::PermissionProfile;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@@ -55,8 +56,8 @@ pub struct SpawnRequest {
pub command: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub policy_json_or_preset: String,
pub sandbox_policy_cwd: PathBuf,
pub permission_profile: PermissionProfile,
pub permission_profile_cwd: PathBuf,
pub codex_home: PathBuf,
pub real_codex_home: PathBuf,
pub cap_sids: Vec<String>,
@@ -164,6 +165,7 @@ pub fn read_frame<R: Read>(mut reader: R) -> Result<Option<FramedMessage>> {
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn framed_round_trip() {
@@ -189,4 +191,43 @@ mod tests {
other => panic!("unexpected message: {other:?}"),
}
}
#[test]
fn spawn_request_serializes_permission_profile() {
let msg = FramedMessage {
version: 1,
message: Message::SpawnRequest {
payload: Box::new(SpawnRequest {
command: vec!["cmd.exe".to_string(), "/c".to_string(), "ver".to_string()],
cwd: PathBuf::from(r"C:\workspace"),
env: HashMap::new(),
permission_profile: PermissionProfile::read_only(),
permission_profile_cwd: PathBuf::from(r"C:\workspace"),
codex_home: PathBuf::from(r"C:\codex"),
real_codex_home: PathBuf::from(r"C:\Users\codex"),
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
timeout_ms: Some(1000),
tty: false,
stdin_open: false,
use_private_desktop: false,
}),
},
};
let encoded = serde_json::to_value(&msg).expect("serialize");
assert_eq!("spawn_request", encoded["type"]);
assert_eq!("managed", encoded["payload"]["permission_profile"]["type"]);
assert_eq!(None, encoded["payload"].get("policy_json_or_preset"));
assert_eq!(None, encoded["payload"].get("sandbox_policy_cwd"));
let decoded: FramedMessage = serde_json::from_value(encoded).expect("deserialize");
let Message::SpawnRequest { payload } = decoded.message else {
panic!("unexpected message");
};
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
assert_eq!(
PathBuf::from(r"C:\workspace"),
payload.permission_profile_cwd
);
}
}

View File

@@ -39,12 +39,14 @@ mod windows_impl {
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;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
use crate::setup::effective_write_roots_for_setup;
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Path;
@@ -80,6 +82,10 @@ mod windows_impl {
.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);
@@ -91,8 +97,7 @@ mod windows_impl {
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
log_start(&command, logs_base_dir);
let sandbox_creds = require_logon_sandbox_creds(
&policy,
sandbox_policy_cwd,
&permissions,
cwd,
&env_map,
codex_home,
@@ -111,32 +116,26 @@ mod windows_impl {
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) = match &policy {
SandboxPolicy::ReadOnly { .. } => {
let sid = LocalSid::from_string(&caps.readonly)?;
(sid, vec![caps.readonly])
}
SandboxPolicy::WorkspaceWrite { .. } => {
let write_roots = effective_write_roots_for_setup(
&policy,
sandbox_policy_cwd,
cwd,
&env_map,
codex_home,
write_roots_override,
);
let cap_sids = write_roots
.iter()
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
.collect::<Result<Vec<_>>>()?;
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
unreachable!("DangerFullAccess handled above")
let (sid_for_null, cap_sids) = if permissions.uses_write_capabilities_for_cwd(cwd, &env_map)
{
let write_roots = effective_write_roots_for_permissions(
&permissions,
cwd,
&env_map,
codex_home,
write_roots_override,
);
let cap_sids = write_roots
.iter()
.map(|root| workspace_write_cap_sid_for_root(codex_home, cwd, root))
.collect::<Result<Vec<_>>>()?;
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
} else {
let sid = LocalSid::from_string(&caps.readonly)?;
(sid, vec![caps.readonly])
};
unsafe {
@@ -144,12 +143,14 @@ 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(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
permission_profile,
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
codex_home: sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids,

View File

@@ -1,12 +1,12 @@
use crate::dpapi;
use crate::logging::debug_log;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup::SandboxNetworkIdentity;
use crate::setup::SandboxUserRecord;
use crate::setup::SandboxUsersFile;
use crate::setup::SetupMarker;
use crate::setup::gather_read_roots;
use crate::setup::gather_write_roots;
use crate::setup::gather_write_roots_for_permissions;
use crate::setup::offline_proxy_settings_from_env;
use crate::setup::run_elevated_setup;
use crate::setup::run_setup_refresh_with_overrides;
@@ -131,8 +131,7 @@ fn select_identity(
#[allow(clippy::too_many_arguments)]
pub fn require_logon_sandbox_creds(
policy: &SandboxPolicy,
policy_cwd: &Path,
permissions: &ResolvedWindowsSandboxPermissions,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
@@ -146,11 +145,11 @@ pub fn require_logon_sandbox_creds(
let sandbox_dir = crate::setup::sandbox_dir(codex_home);
let needed_read = read_roots_override
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_read_roots(command_cwd, policy, codex_home));
.unwrap_or_else(|| gather_read_roots(command_cwd, permissions, env_map, codex_home));
let needed_write = write_roots_override
.map(<[PathBuf]>::to_vec)
.unwrap_or_else(|| gather_write_roots(policy, policy_cwd, command_cwd, env_map));
let network_identity = SandboxNetworkIdentity::from_policy(policy, proxy_enforced);
.unwrap_or_else(|| gather_write_roots_for_permissions(permissions, command_cwd, env_map));
let network_identity = SandboxNetworkIdentity::from_permissions(permissions, proxy_enforced);
let desired_offline_proxy_settings = offline_proxy_settings_from_env(env_map, network_identity);
// NOTE: Do not add CODEX_HOME/.sandbox to `needed_write`; it must remain non-writable by the
// restricted capability token. The setup helper's `lock_sandbox_dir` is responsible for
@@ -191,8 +190,7 @@ pub fn require_logon_sandbox_creds(
}
run_elevated_setup(
crate::setup::SandboxSetupRequest {
policy,
policy_cwd,
permissions,
command_cwd,
env_map,
codex_home,
@@ -211,8 +209,7 @@ pub fn require_logon_sandbox_creds(
// Always refresh ACLs (non-elevated) for current roots via the setup binary.
run_setup_refresh_with_overrides(
crate::setup::SandboxSetupRequest {
policy,
policy_cwd,
permissions,
command_cwd,
env_map,
codex_home,

View File

@@ -38,6 +38,8 @@ mod policy;
#[cfg(target_os = "windows")]
mod process;
#[cfg(target_os = "windows")]
mod resolved_permissions;
#[cfg(target_os = "windows")]
mod token;
#[cfg(target_os = "windows")]
mod wfp;
@@ -106,6 +108,8 @@ pub use acl::path_mask_allows;
#[cfg(target_os = "windows")]
pub use audit::apply_world_writable_scan_and_denies;
#[cfg(target_os = "windows")]
pub use audit::apply_world_writable_scan_and_denies_for_permissions;
#[cfg(target_os = "windows")]
pub use cap::load_or_create_cap_sids;
#[cfg(target_os = "windows")]
pub use cap::workspace_cap_sid_for_cwd;
@@ -195,6 +199,12 @@ pub use process::read_handle_loop;
#[cfg(target_os = "windows")]
pub use process::spawn_process_with_pipes;
#[cfg(target_os = "windows")]
pub use resolved_permissions::ResolvedWindowsSandboxPermissions;
#[cfg(target_os = "windows")]
pub use resolved_permissions::WindowsSandboxTokenMode;
#[cfg(target_os = "windows")]
pub use resolved_permissions::token_mode_for_permission_profile;
#[cfg(target_os = "windows")]
pub use setup::SETUP_VERSION;
#[cfg(target_os = "windows")]
pub use setup::SandboxSetupRequest;
@@ -284,8 +294,10 @@ mod windows_impl {
use super::logging::log_success;
use super::policy::SandboxPolicy;
use super::process::create_process_as_user;
use super::resolved_permissions::ResolvedWindowsSandboxPermissions;
use super::sandbox_utils::ensure_codex_home_exists;
use super::spawn_prep::LegacyAclSids;
use super::spawn_prep::SpawnPrepOptions;
use super::spawn_prep::allow_null_device_for_workspace_write;
use super::spawn_prep::apply_legacy_session_acl_rules;
use super::spawn_prep::legacy_session_capability_roots;
@@ -394,17 +406,21 @@ mod windows_impl {
.collect::<Vec<_>>();
let common = prepare_legacy_spawn_context(
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
cwd,
&mut env_map,
&command,
/*inherit_path*/ false,
/*add_git_safe_directory*/ false,
SpawnPrepOptions {
inherit_path: false,
add_git_safe_directory: false,
},
)?;
let policy = common.policy;
let permissions = common.permissions;
let current_dir = common.current_dir;
let logs_base_dir = common.logs_base_dir.as_deref();
let is_workspace_write = common.is_workspace_write;
let uses_write_capabilities = common.uses_write_capabilities;
if !policy.has_full_disk_read_access() {
anyhow::bail!(
"Restricted read-only access requires the elevated Windows sandbox backend"
@@ -423,11 +439,10 @@ mod windows_impl {
codex_home,
);
let security = prepare_legacy_session_security(&policy, codex_home, cwd, capability_roots)?;
allow_null_device_for_workspace_write(is_workspace_write);
let persist_aces = is_workspace_write;
allow_null_device_for_workspace_write(uses_write_capabilities);
let persist_aces = uses_write_capabilities;
let guards = apply_legacy_session_acl_rules(
&policy,
sandbox_policy_cwd,
&permissions,
codex_home,
&current_dir,
&env_map,
@@ -610,8 +625,10 @@ mod windows_impl {
);
let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?;
let _guards = apply_legacy_session_acl_rules(
sandbox_policy,
sandbox_policy_cwd,
&ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(
sandbox_policy,
sandbox_policy_cwd,
),
codex_home,
&current_dir,
env_map,
@@ -631,7 +648,8 @@ mod windows_impl {
#[cfg(test)]
mod tests {
use crate::policy::SandboxPolicy;
use crate::spawn_prep::should_apply_network_block;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use std::path::Path;
fn workspace_policy(network_access: bool) -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
@@ -642,6 +660,11 @@ mod windows_impl {
}
}
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new("."))
.should_apply_network_block()
}
#[test]
fn applies_network_block_when_access_is_disabled() {
assert!(should_apply_network_block(&workspace_policy(

View File

@@ -0,0 +1,350 @@
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSandboxKind;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
/// Windows-local view of the runtime permission profile.
///
/// Most Windows sandbox code needs resolved runtime permissions plus a few
/// Windows-specific path conventions, not the user/config-facing
/// `PermissionProfile` enum itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedWindowsSandboxPermissions {
file_system: FileSystemSandboxPolicy,
network: NetworkSandboxPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WindowsWritableRoot {
pub(crate) root: PathBuf,
pub(crate) read_only_subpaths: Vec<PathBuf>,
}
/// Restricted-token family needed to enforce a Windows permission profile.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowsSandboxTokenMode {
ReadOnlyCapability,
WriteCapabilityRoots,
}
/// Chooses the restricted-token family needed for a managed permission profile.
pub fn token_mode_for_permission_profile(
permission_profile: &PermissionProfile,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Result<WindowsSandboxTokenMode> {
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile(permission_profile)?;
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"
);
}
if permissions.writable_roots_for_cwd(cwd, env_map).is_empty() {
Ok(WindowsSandboxTokenMode::ReadOnlyCapability)
} else {
Ok(WindowsSandboxTokenMode::WriteCapabilityRoots)
}
}
impl ResolvedWindowsSandboxPermissions {
pub fn from_legacy_policy_for_cwd(policy: &SandboxPolicy, cwd: &Path) -> Self {
Self {
file_system: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(policy, cwd)
.materialize_project_roots_with_cwd(cwd),
network: NetworkSandboxPolicy::from(policy),
}
}
pub fn try_from_permission_profile(permission_profile: &PermissionProfile) -> Result<Self> {
if !matches!(permission_profile, PermissionProfile::Managed { .. }) {
anyhow::bail!(
"only managed permission profiles can be enforced by the Windows sandbox"
);
}
let (file_system, network) = permission_profile.to_runtime_permissions();
if !matches!(file_system.kind, FileSystemSandboxKind::Restricted) {
anyhow::bail!(
"only restricted managed filesystem permissions can be enforced by the Windows sandbox"
);
}
Ok(Self {
file_system,
network,
})
}
pub(crate) fn should_apply_network_block(&self) -> bool {
!self.network.is_enabled()
}
pub(crate) fn network_policy(&self) -> NetworkSandboxPolicy {
self.network
}
pub(crate) fn is_enforceable_by_windows_sandbox(&self) -> bool {
matches!(self.file_system.kind, FileSystemSandboxKind::Restricted)
}
pub(crate) fn has_full_disk_read_access(&self) -> bool {
self.file_system.has_full_disk_read_access()
}
pub(crate) fn include_platform_defaults(&self) -> bool {
self.file_system.include_platform_defaults()
}
pub(crate) fn readable_roots_for_cwd(&self, cwd: &Path) -> Vec<PathBuf> {
self.file_system
.get_readable_roots_with_cwd(cwd)
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.collect()
}
pub(crate) fn uses_write_capabilities_for_cwd(
&self,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> bool {
!self.writable_roots_for_cwd(cwd, env_map).is_empty()
}
pub(crate) fn writable_roots_for_cwd(
&self,
cwd: &Path,
env_map: &HashMap<String, String>,
) -> Vec<WindowsWritableRoot> {
let mut file_system = self.file_system.clone();
file_system
.entries
.retain(|FileSystemSandboxEntry { path, .. }| {
!matches!(
path,
FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Tmpdir
| codex_protocol::permissions::FileSystemSpecialPath::SlashTmp,
}
)
});
let mut roots = file_system
.get_writable_roots_with_cwd(cwd)
.into_iter()
.map(|root| WindowsWritableRoot {
root: root.root.into_path_buf(),
read_only_subpaths: root
.read_only_subpaths
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.collect(),
})
.collect::<Vec<_>>();
if self.has_writable_tmpdir_entry() {
roots.extend(windows_temp_env_roots(env_map).into_iter().map(|root| {
WindowsWritableRoot {
root,
read_only_subpaths: Vec::new(),
}
}));
}
roots
}
fn has_writable_tmpdir_entry(&self) -> bool {
self.file_system
.entries
.iter()
.any(|FileSystemSandboxEntry { path, access }| {
matches!(
path,
FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Tmpdir,
}
) && access.can_write()
})
}
}
fn windows_temp_env_roots(env_map: &HashMap<String, String>) -> Vec<PathBuf> {
["TEMP", "TMP"]
.into_iter()
.filter_map(|key| {
env_map
.get(key)
.map(|value| PathBuf::from(value.as_str()))
.or_else(|| std::env::var_os(key).map(PathBuf::from))
})
.filter(|path| path.is_absolute())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
#[test]
fn permission_profile_workspace_write_uses_windows_temp_env_vars() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
let temp_dir = tmp.path().join("temp");
std::fs::create_dir_all(&cwd).expect("create cwd");
std::fs::create_dir_all(&temp_dir).expect("create temp dir");
let mut env_map = HashMap::new();
env_map.insert("TEMP".to_string(), temp_dir.to_string_lossy().to_string());
env_map.insert("TMP".to_string(), temp_dir.to_string_lossy().to_string());
let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
&PermissionProfile::workspace_write(),
)
.expect("managed permission profile");
let roots = permissions
.writable_roots_for_cwd(&cwd, &env_map)
.into_iter()
.map(|root| root.root)
.collect::<std::collections::HashSet<_>>();
let expected_roots = [
temp_dir,
dunce::canonicalize(&cwd).expect("canonicalize cwd"),
]
.into_iter()
.collect::<std::collections::HashSet<_>>();
assert_eq!(expected_roots, roots);
}
#[test]
fn legacy_workspace_root_stays_bound_to_policy_cwd() {
let tmp = TempDir::new().expect("tempdir");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
std::fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, &policy_cwd);
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(&policy_cwd).expect("canonical policy cwd")]
);
}
#[test]
fn token_mode_for_profile_without_writable_roots_uses_readonly_capability() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::read_only(),
&cwd,
&HashMap::new(),
)
.expect("token mode");
assert_eq!(WindowsSandboxTokenMode::ReadOnlyCapability, token_mode);
}
#[test]
fn token_mode_for_profile_with_writable_roots_uses_write_capabilities() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let token_mode = token_mode_for_permission_profile(
&PermissionProfile::workspace_write(),
&cwd,
&HashMap::new(),
)
.expect("token mode");
assert_eq!(WindowsSandboxTokenMode::WriteCapabilityRoots, token_mode);
}
#[test]
fn permission_profile_rejects_disabled_profiles() {
let err = ResolvedWindowsSandboxPermissions::try_from_permission_profile(
&PermissionProfile::Disabled,
)
.expect_err("disabled profile should not resolve for sandbox enforcement");
assert!(
err.to_string()
.contains("only managed permission profiles can be enforced")
);
}
#[test]
fn permission_profile_rejects_unrestricted_managed_filesystem() {
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: NetworkSandboxPolicy::Restricted,
};
let err =
ResolvedWindowsSandboxPermissions::try_from_permission_profile(&permission_profile)
.expect_err("unrestricted profile should not resolve for sandbox enforcement");
assert!(
err.to_string()
.contains("only restricted managed filesystem permissions can be enforced")
);
}
#[test]
fn token_mode_rejects_full_disk_write_entries() {
let tmp = TempDir::new().expect("tempdir");
let cwd = tmp.path().join("workspace");
std::fs::create_dir_all(&cwd).expect("create cwd");
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let err = token_mode_for_permission_profile(&permission_profile, &cwd, &HashMap::new())
.expect_err("full disk writes should not resolve to a token mode");
assert!(
err.to_string()
.contains("full-disk filesystem writes, which cannot be enforced")
);
}
}

View File

@@ -11,12 +11,13 @@ use std::process::Command;
use std::process::Stdio;
use crate::allow::AllowDenyPaths;
use crate::allow::compute_allow_paths;
use crate::allow::compute_allow_paths_for_permissions;
use crate::helper_materialization::helper_bin_dir;
use crate::logging::log_note;
use crate::path_normalization::canonical_path_key;
use crate::path_normalization::canonicalize_path;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup_error::SetupErrorCode;
use crate::setup_error::SetupFailure;
use crate::setup_error::clear_setup_error_report;
@@ -84,8 +85,7 @@ pub fn sandbox_users_path(codex_home: &Path) -> PathBuf {
}
pub struct SandboxSetupRequest<'a> {
pub policy: &'a SandboxPolicy,
pub policy_cwd: &'a Path,
pub permissions: &'a ResolvedWindowsSandboxPermissions,
pub command_cwd: &'a Path,
pub env_map: &'a HashMap<String, String>,
pub codex_home: &'a Path,
@@ -109,10 +109,17 @@ pub fn run_setup_refresh(
codex_home: &Path,
proxy_enforced: bool,
) -> Result<()> {
if matches!(
policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
return Ok(());
}
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
run_setup_refresh_inner(
SandboxSetupRequest {
policy,
policy_cwd,
permissions: &permissions,
command_cwd,
env_map,
codex_home,
@@ -138,12 +145,19 @@ pub fn run_setup_refresh_with_extra_read_roots(
extra_read_roots: Vec<PathBuf>,
proxy_enforced: bool,
) -> Result<()> {
let mut read_roots = gather_read_roots(command_cwd, policy, codex_home);
if matches!(
policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
return Ok(());
}
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
let mut read_roots = gather_read_roots(command_cwd, &permissions, env_map, codex_home);
read_roots.extend(extra_read_roots);
run_setup_refresh_inner(
SandboxSetupRequest {
policy,
policy_cwd,
permissions: &permissions,
command_cwd,
env_map,
codex_home,
@@ -163,18 +177,14 @@ fn run_setup_refresh_inner(
request: SandboxSetupRequest<'_>,
overrides: SetupRootOverrides,
) -> Result<()> {
// Skip in danger-full-access.
if matches!(
request.policy,
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. }
) {
return Ok(());
if !request.permissions.is_enforceable_by_windows_sandbox() {
anyhow::bail!("unsupported filesystem permissions for Windows sandbox setup");
}
let (read_roots, write_roots) = build_payload_roots(&request, &overrides);
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
let network_identity =
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
SandboxNetworkIdentity::from_permissions(request.permissions, request.proxy_enforced);
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
let payload = ElevationPayload {
version: SETUP_VERSION,
@@ -358,9 +368,10 @@ fn gather_helper_read_roots(codex_home: &Path) -> Vec<PathBuf> {
vec![helper_dir]
}
fn gather_legacy_full_read_roots(
fn gather_full_read_roots_for_permissions(
command_cwd: &Path,
policy: &SandboxPolicy,
permissions: &ResolvedWindowsSandboxPermissions,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> Vec<PathBuf> {
let mut roots = gather_helper_read_roots(codex_home);
@@ -373,36 +384,52 @@ fn gather_legacy_full_read_roots(
roots.extend(profile_read_roots(Path::new(&up)));
}
roots.push(command_cwd.to_path_buf());
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = policy {
for root in writable_roots {
roots.push(root.to_path_buf());
}
}
roots.extend(
permissions
.writable_roots_for_cwd(command_cwd, env_map)
.into_iter()
.map(|root| root.root),
);
canonical_existing(&roots)
}
pub(crate) fn gather_read_roots(
command_cwd: &Path,
policy: &SandboxPolicy,
permissions: &ResolvedWindowsSandboxPermissions,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> Vec<PathBuf> {
gather_legacy_full_read_roots(command_cwd, policy, codex_home)
if permissions.has_full_disk_read_access() {
return gather_full_read_roots_for_permissions(
command_cwd,
permissions,
env_map,
codex_home,
);
}
let mut roots = gather_helper_read_roots(codex_home);
if permissions.include_platform_defaults() {
roots.extend(
WINDOWS_PLATFORM_DEFAULT_READ_ROOTS
.iter()
.map(PathBuf::from),
);
}
roots.extend(permissions.readable_roots_for_cwd(command_cwd));
canonical_existing(&roots)
}
pub(crate) fn gather_write_roots(
policy: &SandboxPolicy,
policy_cwd: &Path,
pub(crate) fn gather_write_roots_for_permissions(
permissions: &ResolvedWindowsSandboxPermissions,
command_cwd: &Path,
env_map: &HashMap<String, String>,
) -> Vec<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
// Always include the command CWD for workspace-write.
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
roots.push(command_cwd.to_path_buf());
}
let AllowDenyPaths { allow, .. } =
compute_allow_paths(policy, policy_cwd, command_cwd, env_map);
roots.extend(allow);
let roots = permissions
.writable_roots_for_cwd(command_cwd, env_map)
.into_iter()
.map(|root| root.root)
.collect::<Vec<_>>();
let mut dedup: HashSet<PathBuf> = HashSet::new();
let mut out: Vec<PathBuf> = Vec::new();
for r in canonical_existing(&roots) {
@@ -414,8 +441,23 @@ pub(crate) fn gather_write_roots(
}
pub(crate) fn effective_write_roots_for_setup(
policy: &SandboxPolicy,
policy_cwd: &Path,
permissions: &ResolvedWindowsSandboxPermissions,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
write_roots_override: Option<&[PathBuf]>,
) -> Vec<PathBuf> {
effective_write_roots_for_permissions(
permissions,
command_cwd,
env_map,
codex_home,
write_roots_override,
)
}
pub(crate) fn effective_write_roots_for_permissions(
permissions: &ResolvedWindowsSandboxPermissions,
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
@@ -424,7 +466,7 @@ pub(crate) fn effective_write_roots_for_setup(
let write_roots = if let Some(roots) = write_roots_override {
canonical_existing(roots)
} else {
gather_write_roots(policy, policy_cwd, command_cwd, env_map)
gather_write_roots_for_permissions(permissions, command_cwd, env_map)
};
let write_roots = expand_user_profile_root(write_roots);
let write_roots = filter_user_profile_root(write_roots);
@@ -468,8 +510,11 @@ pub(crate) enum SandboxNetworkIdentity {
}
impl SandboxNetworkIdentity {
pub(crate) fn from_policy(policy: &SandboxPolicy, proxy_enforced: bool) -> Self {
if proxy_enforced || !policy.has_full_network_access() {
pub(crate) fn from_permissions(
permissions: &ResolvedWindowsSandboxPermissions,
proxy_enforced: bool,
) -> Self {
if proxy_enforced || !permissions.network_policy().is_enabled() {
Self::Offline
} else {
Self::Online
@@ -738,6 +783,9 @@ pub fn run_elevated_setup(
request: SandboxSetupRequest<'_>,
overrides: SetupRootOverrides,
) -> Result<()> {
if !request.permissions.is_enforceable_by_windows_sandbox() {
anyhow::bail!("unsupported filesystem permissions for Windows sandbox setup");
}
// Ensure the shared sandbox directory exists before we send it to the elevated helper.
let sbx_dir = sandbox_dir(request.codex_home);
std::fs::create_dir_all(&sbx_dir).map_err(|err| {
@@ -750,7 +798,7 @@ pub fn run_elevated_setup(
let deny_read_paths = build_payload_deny_read_paths(overrides.deny_read_paths);
let deny_write_paths = build_payload_deny_write_paths(&request, overrides.deny_write_paths);
let network_identity =
SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced);
SandboxNetworkIdentity::from_permissions(request.permissions, request.proxy_enforced);
let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity);
let payload = ElevationPayload {
version: SETUP_VERSION,
@@ -782,8 +830,7 @@ fn build_payload_roots(
overrides: &SetupRootOverrides,
) -> (Vec<PathBuf>, Vec<PathBuf>) {
let write_roots = effective_write_roots_for_setup(
request.policy,
request.policy_cwd,
request.permissions,
request.command_cwd,
request.env_map,
request.codex_home,
@@ -803,7 +850,12 @@ fn build_payload_roots(
read_roots.extend(roots.iter().cloned());
canonical_existing(&read_roots)
} else {
gather_read_roots(request.command_cwd, request.policy, request.codex_home)
gather_read_roots(
request.command_cwd,
request.permissions,
request.env_map,
request.codex_home,
)
};
read_roots = expand_user_profile_root(read_roots);
read_roots = filter_user_profile_root(read_roots);
@@ -818,9 +870,8 @@ fn build_payload_deny_write_paths(
request: &SandboxSetupRequest<'_>,
explicit_deny_write_paths: Option<Vec<PathBuf>>,
) -> Vec<PathBuf> {
let allow_deny_paths: AllowDenyPaths = compute_allow_paths(
request.policy,
request.policy_cwd,
let allow_deny_paths: AllowDenyPaths = compute_allow_paths_for_permissions(
request.permissions,
request.command_cwd,
request.env_map,
);
@@ -966,7 +1017,7 @@ fn filter_sensitive_write_roots(mut roots: Vec<PathBuf>, codex_home: &Path) -> V
mod tests {
use super::WINDOWS_PLATFORM_DEFAULT_READ_ROOTS;
use super::build_payload_roots;
use super::gather_legacy_full_read_roots;
use super::gather_full_read_roots_for_permissions;
use super::gather_read_roots;
use super::loopback_proxy_port_from_url;
use super::offline_proxy_settings_from_env;
@@ -974,6 +1025,7 @@ mod tests {
use super::proxy_ports_from_env;
use crate::helper_materialization::helper_bin_dir;
use crate::policy::SandboxPolicy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
@@ -989,6 +1041,13 @@ mod tests {
.collect()
}
fn permissions_for(
policy: &SandboxPolicy,
policy_cwd: &std::path::Path,
) -> ResolvedWindowsSandboxPermissions {
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd)
}
#[test]
fn loopback_proxy_url_parsing_supports_common_forms() {
assert_eq!(
@@ -1289,8 +1348,9 @@ mod tests {
let command_cwd = tmp.path().join("workspace");
fs::create_dir_all(&command_cwd).expect("create workspace");
let policy = SandboxPolicy::new_read_only_policy();
let permissions = permissions_for(&policy, &command_cwd);
let roots = gather_read_roots(&command_cwd, &policy, &codex_home);
let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home);
let expected =
dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir");
@@ -1314,8 +1374,9 @@ mod tests {
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &command_cwd);
let roots = gather_read_roots(&command_cwd, &policy, &codex_home);
let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home);
let expected_writable =
dunce::canonicalize(&writable_root).expect("canonical writable root");
@@ -1335,11 +1396,11 @@ mod tests {
let policy = SandboxPolicy::ReadOnly {
network_access: false,
};
let permissions = permissions_for(&policy, &policy_cwd);
let (read_roots, write_roots) = build_payload_roots(
&super::SandboxSetupRequest {
policy: &policy,
policy_cwd: &policy_cwd,
permissions: &permissions,
command_cwd: &command_cwd,
env_map: &HashMap::new(),
codex_home: &codex_home,
@@ -1383,11 +1444,11 @@ mod tests {
let policy = SandboxPolicy::ReadOnly {
network_access: false,
};
let permissions = permissions_for(&policy, &policy_cwd);
let (read_roots, write_roots) = build_payload_roots(
&super::SandboxSetupRequest {
policy: &policy,
policy_cwd: &policy_cwd,
permissions: &permissions,
command_cwd: &command_cwd,
env_map: &HashMap::new(),
codex_home: &codex_home,
@@ -1435,6 +1496,7 @@ mod tests {
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &command_cwd);
let override_roots = vec![
command_cwd.clone(),
extra_root.clone(),
@@ -1442,8 +1504,7 @@ mod tests {
sandbox_root.clone(),
];
let request = super::SandboxSetupRequest {
policy: &policy,
policy_cwd: &command_cwd,
permissions: &permissions,
command_cwd: &command_cwd,
env_map: &HashMap::new(),
codex_home: &codex_home,
@@ -1458,8 +1519,7 @@ mod tests {
};
let effective_write_roots = super::effective_write_roots_for_setup(
&policy,
&command_cwd,
&permissions,
&command_cwd,
&HashMap::new(),
&codex_home,
@@ -1478,6 +1538,37 @@ mod tests {
assert!(!effective_write_roots.contains(&forbidden_sandbox));
}
#[test]
fn effective_write_roots_use_policy_cwd_for_legacy_workspace_root() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
fs::create_dir_all(&codex_home).expect("create codex home");
fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &policy_cwd);
let effective_write_roots = super::effective_write_roots_for_setup(
&permissions,
&command_cwd,
&HashMap::new(),
&codex_home,
/*write_roots_override*/ None,
);
assert_eq!(
effective_write_roots,
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
);
}
#[test]
fn payload_deny_write_paths_merge_explicit_and_protected_children() {
let tmp = TempDir::new().expect("tempdir");
@@ -1498,9 +1589,9 @@ mod tests {
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let permissions = permissions_for(&policy, &command_cwd);
let request = super::SandboxSetupRequest {
policy: &policy,
policy_cwd: &command_cwd,
permissions: &permissions,
command_cwd: &command_cwd,
env_map: &HashMap::new(),
codex_home: &codex_home,
@@ -1529,8 +1620,14 @@ mod tests {
let command_cwd = tmp.path().join("workspace");
fs::create_dir_all(&command_cwd).expect("create workspace");
let policy = SandboxPolicy::new_read_only_policy();
let permissions = permissions_for(&policy, &command_cwd);
let roots = gather_legacy_full_read_roots(&command_cwd, &policy, &codex_home);
let roots = gather_full_read_roots_for_permissions(
&command_cwd,
&permissions,
&HashMap::new(),
&codex_home,
);
assert!(
canonical_windows_platform_default_roots()

View File

@@ -2,7 +2,7 @@ use crate::acl::add_allow_ace;
use crate::acl::add_deny_write_ace;
use crate::acl::allow_null_device;
use crate::allow::AllowDenyPaths;
use crate::allow::compute_allow_paths;
use crate::allow::compute_allow_paths_for_permissions;
use crate::cap::load_or_create_cap_sids;
use crate::cap::workspace_write_cap_sid_for_root;
use crate::cap::workspace_write_root_contains_path;
@@ -20,9 +20,10 @@ use crate::logging::log_start;
use crate::path_normalization::canonicalize_path;
use crate::policy::SandboxPolicy;
use crate::policy::parse_policy;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::sandbox_utils::ensure_codex_home_exists;
use crate::sandbox_utils::inject_git_safe_directory;
use crate::setup::effective_write_roots_for_setup;
use crate::setup::effective_write_roots_for_permissions;
use crate::token::LocalSid;
use crate::token::create_readonly_token_with_cap;
use crate::token::create_workspace_write_token_with_caps_from;
@@ -42,10 +43,11 @@ use windows_sys::Win32::Foundation::HANDLE;
pub(crate) struct SpawnContext {
pub(crate) policy: SandboxPolicy,
pub(crate) permissions: ResolvedWindowsSandboxPermissions,
pub(crate) current_dir: PathBuf,
pub(crate) sandbox_base: PathBuf,
pub(crate) logs_base_dir: Option<PathBuf>,
pub(crate) is_workspace_write: bool,
pub(crate) uses_write_capabilities: bool,
}
pub(crate) struct ElevatedSpawnContext {
@@ -54,6 +56,12 @@ pub(crate) struct ElevatedSpawnContext {
pub(crate) cap_sids: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SpawnPrepOptions {
pub(crate) inherit_path: bool,
pub(crate) add_git_safe_directory: bool,
}
pub(crate) struct LegacySessionSecurity {
pub(crate) h_token: HANDLE,
pub(crate) readonly_sid: Option<LocalSid>,
@@ -73,18 +81,14 @@ pub(crate) struct LegacyAclSids<'a> {
pub(crate) write_root_sids: &'a [RootCapabilitySid],
}
pub(crate) fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
!policy.has_full_network_access()
}
fn prepare_spawn_context_common(
policy_json_or_preset: &str,
policy_cwd: &Path,
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
command: &[String],
inherit_path: bool,
add_git_safe_directory: bool,
options: SpawnPrepOptions,
) -> Result<SpawnContext> {
let policy = parse_policy(policy_json_or_preset)?;
if matches!(
@@ -96,10 +100,10 @@ fn prepare_spawn_context_common(
normalize_null_device_env(env_map);
ensure_non_interactive_pager(env_map);
if inherit_path {
if options.inherit_path {
inherit_path_env(env_map);
}
if add_git_safe_directory {
if options.add_git_safe_directory {
inject_git_safe_directory(env_map, cwd);
}
@@ -109,36 +113,39 @@ fn prepare_spawn_context_common(
let logs_base_dir = Some(sandbox_base.clone());
log_start(command, logs_base_dir.as_deref());
let is_workspace_write = matches!(&policy, SandboxPolicy::WorkspaceWrite { .. });
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, policy_cwd);
let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);
Ok(SpawnContext {
policy,
permissions,
current_dir: cwd.to_path_buf(),
sandbox_base,
logs_base_dir,
is_workspace_write,
uses_write_capabilities,
})
}
pub(crate) fn prepare_legacy_spawn_context(
policy_json_or_preset: &str,
policy_cwd: &Path,
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
command: &[String],
inherit_path: bool,
add_git_safe_directory: bool,
options: SpawnPrepOptions,
) -> Result<SpawnContext> {
let common = prepare_spawn_context_common(
policy_json_or_preset,
policy_cwd,
codex_home,
cwd,
env_map,
command,
inherit_path,
add_git_safe_directory,
options,
)?;
if should_apply_network_block(&common.policy) {
if common.permissions.should_apply_network_block() {
apply_no_network_to_env(env_map)?;
}
Ok(common)
@@ -195,14 +202,15 @@ pub(crate) fn legacy_session_capability_roots(
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> Vec<PathBuf> {
let allow_paths = compute_allow_paths(policy, policy_cwd, current_dir, env_map)
let permissions =
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd);
let allow_paths = compute_allow_paths_for_permissions(&permissions, current_dir, env_map)
.allow
.into_iter()
.collect::<Vec<_>>();
if matches!(policy, SandboxPolicy::WorkspaceWrite { .. }) {
effective_write_roots_for_setup(
policy,
policy_cwd,
if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) {
effective_write_roots_for_permissions(
&permissions,
current_dir,
env_map,
codex_home,
@@ -275,8 +283,7 @@ pub(crate) fn allow_null_device_for_workspace_write(is_workspace_write: bool) {
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_legacy_session_acl_rules(
policy: &SandboxPolicy,
sandbox_policy_cwd: &Path,
permissions: &ResolvedWindowsSandboxPermissions,
codex_home: &Path,
current_dir: &Path,
env_map: &HashMap<String, String>,
@@ -286,7 +293,7 @@ pub(crate) fn apply_legacy_session_acl_rules(
persist_aces: bool,
) -> Result<Vec<(PathBuf, String)>> {
let AllowDenyPaths { allow, mut deny } =
compute_allow_paths(policy, sandbox_policy_cwd, current_dir, env_map);
compute_allow_paths_for_permissions(permissions, current_dir, env_map);
let mut guards: Vec<(PathBuf, String)> = Vec::new();
unsafe {
for path in additional_deny_write_paths {
@@ -378,7 +385,7 @@ pub(crate) fn apply_legacy_session_acl_rules(
allow_null_device(readonly_sid.as_ptr());
}
if persist_aces
&& matches!(policy, SandboxPolicy::WorkspaceWrite { .. })
&& !acl_sids.write_root_sids.is_empty()
&& let Some(workspace_sid) =
matching_root_capability(current_dir, acl_sids.write_root_sids)
{
@@ -408,32 +415,30 @@ pub(crate) fn prepare_elevated_spawn_context(
) -> Result<ElevatedSpawnContext> {
let common = prepare_spawn_context_common(
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
cwd,
env_map,
command,
/*inherit_path*/ true,
/*add_git_safe_directory*/ true,
SpawnPrepOptions {
inherit_path: true,
add_git_safe_directory: true,
},
)?;
let AllowDenyPaths { allow, deny } = compute_allow_paths(
&common.policy,
sandbox_policy_cwd,
&common.current_dir,
env_map,
);
let AllowDenyPaths { allow, deny } =
compute_allow_paths_for_permissions(&common.permissions, &common.current_dir, env_map);
let write_roots: Vec<PathBuf> = allow.into_iter().collect();
let deny_write_paths: Vec<PathBuf> = deny.into_iter().collect();
let computed_write_roots_override = if common.is_workspace_write {
let computed_write_roots_override = if common.uses_write_capabilities {
Some(write_roots.as_slice())
} else {
None
};
let write_roots_for_setup = write_roots_override.or(computed_write_roots_override);
let effective_write_roots = if common.is_workspace_write {
effective_write_roots_for_setup(
&common.policy,
sandbox_policy_cwd,
let effective_write_roots = if common.uses_write_capabilities {
effective_write_roots_for_permissions(
&common.permissions,
&common.current_dir,
env_map,
codex_home,
@@ -442,14 +447,13 @@ pub(crate) fn prepare_elevated_spawn_context(
} else {
Vec::new()
};
let setup_write_roots_override = if common.is_workspace_write {
let setup_write_roots_override = if common.uses_write_capabilities {
Some(effective_write_roots.as_slice())
} else {
write_roots_override
};
let sandbox_creds = require_logon_sandbox_creds(
&common.policy,
sandbox_policy_cwd,
&common.permissions,
cwd,
env_map,
codex_home,
@@ -465,24 +469,20 @@ pub(crate) fn prepare_elevated_spawn_context(
/*proxy_enforced*/ false,
)?;
let caps = load_or_create_cap_sids(codex_home)?;
let (psid_to_use, cap_sids) = match &common.policy {
SandboxPolicy::ReadOnly { .. } => (
let (psid_to_use, cap_sids) = if common.uses_write_capabilities {
let cap_sids = root_capability_sids(codex_home, cwd, effective_write_roots)?
.into_iter()
.map(|root_sid| root_sid.sid_str)
.collect::<Vec<_>>();
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
} else {
(
LocalSid::from_string(&caps.readonly)?,
vec![caps.readonly.clone()],
),
SandboxPolicy::WorkspaceWrite { .. } => {
let cap_sids = root_capability_sids(codex_home, cwd, effective_write_roots)?
.into_iter()
.map(|root_sid| root_sid.sid_str)
.collect::<Vec<_>>();
if cap_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
(LocalSid::from_string(&cap_sids[0])?, cap_sids)
}
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
unreachable!("dangerous policies rejected before elevated session prep")
}
)
};
unsafe {
@@ -499,19 +499,26 @@ pub(crate) fn prepare_elevated_spawn_context(
#[cfg(test)]
mod tests {
use super::SandboxPolicy;
use super::SpawnPrepOptions;
use super::deny_root_capabilities_for_path;
use super::legacy_session_capability_roots;
use super::prepare_legacy_spawn_context;
use super::prepare_spawn_context_common;
use super::root_capability_sids;
use super::should_apply_network_block;
use crate::cap::load_or_create_cap_sids;
use crate::cap::workspace_write_cap_sid_for_root;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
fn should_apply_network_block(policy: &SandboxPolicy) -> bool {
ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new("."))
.should_apply_network_block()
}
#[test]
fn no_network_env_rewrite_applies_for_workspace_write() {
assert!(should_apply_network_block(
@@ -539,12 +546,15 @@ mod tests {
let _context = prepare_legacy_spawn_context(
"workspace-write",
cwd.path(),
codex_home.path(),
cwd.path(),
&mut env_map,
&["cmd.exe".to_string()],
/*inherit_path*/ true,
/*add_git_safe_directory*/ false,
SpawnPrepOptions {
inherit_path: true,
add_git_safe_directory: false,
},
)
.expect("legacy env prep");
@@ -566,12 +576,15 @@ mod tests {
let context = prepare_spawn_context_common(
"workspace-write",
cwd.path(),
codex_home.path(),
cwd.path(),
&mut env_map,
&["cmd.exe".to_string()],
/*inherit_path*/ true,
/*add_git_safe_directory*/ true,
SpawnPrepOptions {
inherit_path: true,
add_git_safe_directory: true,
},
)
.expect("preserve existing env prep");
assert_eq!(context.policy, SandboxPolicy::new_workspace_write_policy());
@@ -583,6 +596,36 @@ mod tests {
);
}
#[test]
fn legacy_session_capability_roots_use_policy_cwd_for_workspace_root() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let policy_cwd = tmp.path().join("workspace");
let command_cwd = policy_cwd.join("subdir");
std::fs::create_dir_all(&codex_home).expect("create codex home");
std::fs::create_dir_all(&command_cwd).expect("create command cwd");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let roots = legacy_session_capability_roots(
&policy,
&policy_cwd,
&command_cwd,
&HashMap::new(),
&codex_home,
);
assert_eq!(
roots,
vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")]
);
}
#[test]
fn root_capability_sids_only_include_active_roots() {
let temp = TempDir::new().expect("tempdir");

View File

@@ -10,6 +10,7 @@ use crate::ipc_framed::SpawnRequest;
use crate::runner_client::spawn_runner_transport;
use crate::spawn_prep::prepare_elevated_spawn_context;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
@@ -60,12 +61,16 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated(
&deny_write_paths_override,
)?;
let permission_profile = PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&elevated.common.policy,
sandbox_policy_cwd,
);
let spawn_request = SpawnRequest {
command: command.clone(),
cwd: cwd.to_path_buf(),
env: env_map.clone(),
policy_json_or_preset: policy_json_or_preset.to_string(),
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
permission_profile,
permission_profile_cwd: sandbox_policy_cwd.to_path_buf(),
codex_home: elevated.common.sandbox_base.clone(),
real_codex_home: codex_home.to_path_buf(),
cap_sids: elevated.cap_sids.clone(),

View File

@@ -11,6 +11,7 @@ use crate::process::StdinMode;
use crate::process::read_handle_loop;
use crate::process::spawn_process_with_pipes;
use crate::spawn_prep::LegacyAclSids;
use crate::spawn_prep::SpawnPrepOptions;
use crate::spawn_prep::allow_null_device_for_workspace_write;
use crate::spawn_prep::apply_legacy_session_acl_rules;
use crate::spawn_prep::legacy_session_capability_roots;
@@ -295,12 +296,15 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
) -> Result<SpawnedProcess> {
let common = prepare_legacy_spawn_context(
policy_json_or_preset,
sandbox_policy_cwd,
codex_home,
cwd,
&mut env_map,
&command,
/*inherit_path*/ false,
/*add_git_safe_directory*/ false,
SpawnPrepOptions {
inherit_path: false,
add_git_safe_directory: false,
},
)?;
if !common.policy.has_full_disk_read_access() {
anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend");
@@ -323,12 +327,11 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy(
);
let security =
prepare_legacy_session_security(&common.policy, codex_home, cwd, capability_roots)?;
allow_null_device_for_workspace_write(common.is_workspace_write);
allow_null_device_for_workspace_write(common.uses_write_capabilities);
let persist_aces = common.is_workspace_write;
let persist_aces = common.uses_write_capabilities;
let guards = apply_legacy_session_acl_rules(
&common.policy,
sandbox_policy_cwd,
&common.permissions,
codex_home,
&common.current_dir,
&env_map,