Files
codex/codex-rs/windows-sandbox-rs/src/spawn_prep.rs
iceweasel-oai 18fe1d9fe3 [codex] Preserve proxy state for filesystem sandbox helpers (#29671)
## Why

Filesystem helpers intentionally run with a minimal environment that
excludes proxy variables. After filesystem operations started using the
Windows sandbox wrapper, the wrapper derived an empty proxy
configuration from that helper environment and compared it with the
persistent sandbox setup marker. When the marker contained proxy ports,
every filesystem operation appeared to require a firewall update, which
could launch elevated setup, show a UAC or loader dialog, and fail
operations such as `apply_patch` with error 1223.

Filesystem helpers do not use network access, so they should preserve
the proxy/firewall state established by normal sandboxed process
launches.

## What changed

- Add an explicit Windows sandbox proxy-settings mode for reconciling or
preserving persistent proxy state.
- Use preserve mode for filesystem helpers while normal process launches
continue to reconcile proxy settings from their environment.
- Carry the selected proxy state consistently through setup validation,
elevated setup, and non-elevated ACL refreshes.
- Cover wrapper argument propagation and marker-derived proxy
preservation.

## Validation

- `cargo build -p codex-cli --bin codex`
- `just test -p codex-windows-sandbox
preserving_proxy_settings_uses_the_existing_marker`
- `just test -p codex-windows-sandbox windows_wrapper_args_round_trip`
- `just test -p codex-windows-sandbox
setup_request_prefers_explicit_proxy_settings`
- `just test -p codex-sandboxing transform_for_direct_spawn_windows`
- `just test -p codex-exec-server fs_sandbox::tests`
- Ran the same sandboxed `fs/writeFile` reproduction against published
`0.142.0-alpha.6` and the new CLI. The published CLI launched elevated
setup and failed with `ShellExecuteExW ... 1223`; the new CLI completed
without elevation.

Related to #28359.
2026-06-23 12:29:46 -07:00

719 lines
26 KiB
Rust

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_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;
use crate::cap::workspace_write_root_overlaps_path;
use crate::cap::workspace_write_root_specificity;
use crate::deny_read_state::sync_persistent_deny_read_acls;
use crate::env::apply_no_network_to_env;
use crate::env::ensure_non_interactive_pager;
use crate::env::inherit_path_env;
use crate::env::normalize_null_device_env;
use crate::identity::SandboxCreds;
use crate::identity::require_logon_sandbox_creds;
use crate::logging::log_start;
use crate::path_normalization::canonicalize_path;
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_permissions;
use crate::token::LocalSid;
use crate::token::create_readonly_token_with_cap;
use crate::token::create_workspace_write_token_with_caps_from;
use crate::token::get_current_token_for_restriction;
use crate::token::get_logon_sid_bytes;
use crate::workspace_acl::is_command_cwd_root;
use crate::workspace_acl::protect_workspace_agents_dir;
use crate::workspace_acl::protect_workspace_codex_dir;
use anyhow::Context;
use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::ffi::c_void;
use std::path::Path;
use std::path::PathBuf;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::Foundation::HANDLE;
pub(crate) struct SpawnContext {
pub(crate) permissions: ResolvedWindowsSandboxPermissions,
pub(crate) current_dir: PathBuf,
pub(crate) logs_base_dir: Option<PathBuf>,
pub(crate) uses_write_capabilities: bool,
}
pub(crate) struct ElevatedSpawnContext {
pub(crate) sandbox_base: PathBuf,
pub(crate) logs_base_dir: Option<PathBuf>,
pub(crate) sandbox_creds: SandboxCreds,
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>,
pub(crate) readonly_sid_str: Option<String>,
pub(crate) write_root_sids: Vec<RootCapabilitySid>,
}
pub(crate) struct RootCapabilitySid {
pub(crate) root: PathBuf,
pub(crate) sid: LocalSid,
pub(crate) sid_str: String,
}
pub(crate) struct LegacyAclSids<'a> {
pub(crate) readonly_sid: Option<&'a LocalSid>,
pub(crate) readonly_sid_str: Option<&'a str>,
pub(crate) write_root_sids: &'a [RootCapabilitySid],
}
fn prepare_spawn_context_common(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
command: &[String],
options: SpawnPrepOptions,
) -> Result<SpawnContext> {
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
normalize_null_device_env(env_map);
ensure_non_interactive_pager(env_map);
if options.inherit_path {
inherit_path_env(env_map);
}
if options.add_git_safe_directory {
inject_git_safe_directory(env_map, cwd);
}
ensure_codex_home_exists(codex_home)?;
let sandbox_base = codex_home.join(".sandbox");
std::fs::create_dir_all(&sandbox_base)?;
let logs_base_dir = Some(sandbox_base);
log_start(command, logs_base_dir.as_deref());
let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);
Ok(SpawnContext {
permissions,
current_dir: cwd.to_path_buf(),
logs_base_dir,
uses_write_capabilities,
})
}
pub(crate) fn prepare_legacy_spawn_context(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
command: &[String],
options: SpawnPrepOptions,
) -> Result<SpawnContext> {
let common = prepare_spawn_context_common(
permission_profile,
workspace_roots,
codex_home,
cwd,
env_map,
command,
options,
)?;
if common.permissions.should_apply_network_block() {
apply_no_network_to_env(env_map)?;
}
Ok(common)
}
pub(crate) fn prepare_legacy_session_security(
uses_write_capabilities: bool,
codex_home: &Path,
cwd: &Path,
capability_roots: impl IntoIterator<Item = PathBuf>,
) -> Result<LegacySessionSecurity> {
let caps = load_or_create_cap_sids(codex_home)?;
let (h_token, readonly_sid, readonly_sid_str, write_root_sids) = unsafe {
if uses_write_capabilities {
let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?;
if write_root_sids.is_empty() {
anyhow::bail!("workspace-write sandbox has no writable root capability SIDs");
}
let base = get_current_token_for_restriction()?;
let cap_ptrs: Vec<*mut c_void> = write_root_sids
.iter()
.map(|root| root.sid.as_ptr())
.collect();
let h_token = create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice());
CloseHandle(base);
let h_token = h_token?;
(h_token, None, None, write_root_sids)
} else {
let psid = LocalSid::from_string(&caps.readonly)?;
let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?;
(h_token, Some(psid), Some(caps.readonly), Vec::new())
}
};
Ok(LegacySessionSecurity {
h_token,
readonly_sid,
readonly_sid_str,
write_root_sids,
})
}
pub(crate) fn legacy_session_capability_roots(
permissions: &ResolvedWindowsSandboxPermissions,
current_dir: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> Vec<PathBuf> {
let allow_paths = compute_allow_paths_for_permissions(permissions, current_dir, env_map)
.allow
.into_iter()
.collect::<Vec<_>>();
if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) {
effective_write_roots_for_permissions(
permissions,
current_dir,
env_map,
codex_home,
Some(allow_paths.as_slice()),
)
} else {
allow_paths
}
}
pub(crate) fn root_capability_sids(
codex_home: &Path,
cwd: &Path,
allow_paths: impl IntoIterator<Item = PathBuf>,
) -> Result<Vec<RootCapabilitySid>> {
let mut roots: Vec<PathBuf> = allow_paths.into_iter().collect();
roots.sort_by_key(|root| canonicalize_path(root.as_path()));
roots.dedup_by(|a, b| canonicalize_path(a.as_path()) == canonicalize_path(b.as_path()));
let mut out = Vec::with_capacity(roots.len());
for root in roots {
let sid_str = workspace_write_cap_sid_for_root(codex_home, cwd, &root)?;
let sid = LocalSid::from_string(&sid_str)?;
out.push(RootCapabilitySid { root, sid, sid_str });
}
Ok(out)
}
fn matching_root_capability<'a>(
path: &Path,
root_sids: &'a [RootCapabilitySid],
) -> Option<&'a RootCapabilitySid> {
root_sids
.iter()
.filter(|root_sid| workspace_write_root_contains_path(&root_sid.root, path))
.max_by_key(|root_sid| workspace_write_root_specificity(&root_sid.root))
}
fn deny_root_capabilities_for_path<'a>(
path: &Path,
root_sids: &'a [RootCapabilitySid],
) -> Vec<&'a RootCapabilitySid> {
let matching_root_sids = root_sids
.iter()
.filter(|root_sid| workspace_write_root_overlaps_path(&root_sid.root, path))
.collect::<Vec<_>>();
if matching_root_sids.is_empty() {
root_sids.iter().collect()
} else {
matching_root_sids
}
}
pub(crate) fn allow_null_device_for_workspace_write(is_workspace_write: bool) {
if !is_workspace_write {
return;
}
unsafe {
if let Ok(base) = get_current_token_for_restriction() {
if let Ok(bytes) = get_logon_sid_bytes(base) {
let mut tmp = bytes;
let psid = tmp.as_mut_ptr() as *mut c_void;
allow_null_device(psid);
}
CloseHandle(base);
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_legacy_session_acl_rules(
permissions: &ResolvedWindowsSandboxPermissions,
codex_home: &Path,
current_dir: &Path,
env_map: &HashMap<String, String>,
additional_deny_read_paths: &[PathBuf],
additional_deny_write_paths: &[PathBuf],
acl_sids: LegacyAclSids<'_>,
) -> Result<()> {
let AllowDenyPaths { allow, mut deny } =
compute_allow_paths_for_permissions(permissions, current_dir, env_map);
unsafe {
for path in additional_deny_write_paths {
// Explicit carveouts must exist before the command starts so the
// sandbox cannot create them under a writable parent first.
if !path.exists() {
std::fs::create_dir_all(path)
.with_context(|| format!("create deny-write path {}", path.display()))?;
}
deny.insert(path.clone());
}
if let Some(readonly_sid) = acl_sids.readonly_sid {
for p in &allow {
let _ = add_allow_ace(p, readonly_sid.as_ptr());
}
} else {
for p in &allow {
let Some(root_sid) = matching_root_capability(p, acl_sids.write_root_sids) else {
continue;
};
let _ = add_allow_ace(p, root_sid.sid.as_ptr());
}
}
for p in &deny {
for root_sid in deny_root_capabilities_for_path(p, acl_sids.write_root_sids) {
let _ = add_deny_write_ace(p, root_sid.sid.as_ptr());
}
}
if !additional_deny_read_paths.is_empty() {
if let Some(readonly_sid) = acl_sids.readonly_sid {
let Some(readonly_sid_str) = acl_sids.readonly_sid_str else {
anyhow::bail!("readonly capability SID string missing");
};
sync_persistent_deny_read_acls(
codex_home,
readonly_sid_str,
additional_deny_read_paths,
readonly_sid.as_ptr(),
)?;
} else {
for root_sid in acl_sids.write_root_sids {
sync_persistent_deny_read_acls(
codex_home,
&root_sid.sid_str,
additional_deny_read_paths,
root_sid.sid.as_ptr(),
)?;
}
}
}
for root_sid in acl_sids.write_root_sids {
allow_null_device(root_sid.sid.as_ptr());
}
if let Some(readonly_sid) = acl_sids.readonly_sid {
allow_null_device(readonly_sid.as_ptr());
}
if !acl_sids.write_root_sids.is_empty()
&& let Some(workspace_sid) =
matching_root_capability(current_dir, acl_sids.write_root_sids)
{
let canonical_cwd = canonicalize_path(current_dir);
if is_command_cwd_root(&workspace_sid.root, &canonical_cwd) {
let _ = protect_workspace_codex_dir(current_dir, workspace_sid.sid.as_ptr());
let _ = protect_workspace_agents_dir(current_dir, workspace_sid.sid.as_ptr());
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn prepare_elevated_spawn_context_for_permissions(
permissions: ResolvedWindowsSandboxPermissions,
codex_home: &Path,
cwd: &Path,
env_map: &mut HashMap<String, String>,
command: &[String],
read_roots_override: Option<&[PathBuf]>,
read_roots_include_platform_defaults: bool,
write_roots_override: Option<&[PathBuf]>,
deny_read_paths_override: &[PathBuf],
deny_write_paths_override: &[PathBuf],
proxy_enforced: bool,
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
) -> Result<ElevatedSpawnContext> {
normalize_null_device_env(env_map);
ensure_non_interactive_pager(env_map);
inherit_path_env(env_map);
inject_git_safe_directory(env_map, cwd);
// Use a temp-based log dir that the sandbox user can write.
let sandbox_base = codex_home.join(".sandbox");
ensure_codex_home_exists(&sandbox_base)?;
let logs_base_dir = Some(sandbox_base.clone());
log_start(command, logs_base_dir.as_deref());
let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map);
let AllowDenyPaths { allow, deny } =
compute_allow_paths_for_permissions(&permissions, cwd, 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 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 uses_write_capabilities {
effective_write_roots_for_permissions(
&permissions,
cwd,
env_map,
codex_home,
write_roots_for_setup,
)
} else {
Vec::new()
};
let setup_write_roots_override = if uses_write_capabilities {
Some(effective_write_roots.as_slice())
} else {
write_roots_override
};
let sandbox_creds = require_logon_sandbox_creds(
&permissions,
cwd,
env_map,
codex_home,
read_roots_override,
read_roots_include_platform_defaults,
setup_write_roots_override,
deny_read_paths_override,
if deny_write_paths_override.is_empty() {
&deny_write_paths
} else {
deny_write_paths_override
},
proxy_enforced,
proxy_settings_mode,
)?;
let caps = load_or_create_cap_sids(codex_home)?;
let (psid_to_use, cap_sids) = if 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()],
)
};
unsafe {
allow_null_device(psid_to_use.as_ptr());
}
Ok(ElevatedSpawnContext {
sandbox_base,
logs_base_dir,
sandbox_creds,
cap_sids,
})
}
#[cfg(test)]
mod tests {
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 crate::cap::load_or_create_cap_sids;
use crate::cap::workspace_write_cap_sid_for_root;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
fn workspace_profile(
network_policy: NetworkSandboxPolicy,
writable_roots: &[AbsolutePathBuf],
exclude_tmpdir_env_var: bool,
exclude_slash_tmp: bool,
) -> PermissionProfile {
PermissionProfile::workspace_write_with(
writable_roots,
network_policy,
exclude_tmpdir_env_var,
exclude_slash_tmp,
)
}
fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {
vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]
}
fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool {
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
&[],
)
.expect("managed permission profile")
.should_apply_network_block()
}
#[test]
fn no_network_env_rewrite_applies_for_workspace_write() {
assert!(should_apply_network_block(
&PermissionProfile::workspace_write()
));
}
#[test]
fn no_network_env_rewrite_skips_when_network_access_is_allowed() {
assert!(!should_apply_network_block(&workspace_profile(
NetworkSandboxPolicy::Enabled,
&[],
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
)));
}
#[test]
fn legacy_spawn_env_applies_offline_network_rewrite() {
let codex_home = TempDir::new().expect("tempdir");
let cwd = TempDir::new().expect("tempdir");
let mut env_map = HashMap::new();
let workspace_roots = workspace_roots_for(cwd.path());
let _context = prepare_legacy_spawn_context(
&PermissionProfile::workspace_write(),
workspace_roots.as_slice(),
codex_home.path(),
cwd.path(),
&mut env_map,
&["cmd.exe".to_string()],
SpawnPrepOptions {
inherit_path: true,
add_git_safe_directory: false,
},
)
.expect("legacy env prep");
assert_eq!(env_map.get("SBX_NONET_ACTIVE"), Some(&"1".to_string()));
assert_eq!(
env_map.get("HTTP_PROXY"),
Some(&"http://127.0.0.1:9".to_string())
);
}
#[test]
fn common_spawn_env_keeps_network_env_unchanged() {
let codex_home = TempDir::new().expect("tempdir");
let cwd = TempDir::new().expect("tempdir");
let mut env_map = HashMap::from([(
"HTTP_PROXY".to_string(),
"http://user.proxy:8080".to_string(),
)]);
let workspace_roots = workspace_roots_for(cwd.path());
let context = prepare_spawn_context_common(
&PermissionProfile::workspace_write(),
workspace_roots.as_slice(),
codex_home.path(),
cwd.path(),
&mut env_map,
&["cmd.exe".to_string()],
SpawnPrepOptions {
inherit_path: true,
add_git_safe_directory: true,
},
)
.expect("preserve existing env prep");
assert!(context.uses_write_capabilities);
assert_eq!(env_map.get("SBX_NONET_ACTIVE"), None);
assert_eq!(
env_map.get("HTTP_PROXY"),
Some(&"http://user.proxy:8080".to_string())
);
}
#[test]
fn legacy_session_capability_roots_use_runtime_workspace_roots_for_workspace_root() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
let workspace_root = tmp.path().join("workspace");
let command_cwd = workspace_root.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 permission_profile = workspace_profile(
NetworkSandboxPolicy::Restricted,
&[],
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(workspace_root.as_path());
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
.expect("managed permission profile");
let roots = legacy_session_capability_roots(
&permissions,
&command_cwd,
&HashMap::new(),
&codex_home,
);
assert_eq!(
roots,
vec![dunce::canonicalize(&workspace_root).expect("canonical workspace root")]
);
}
#[test]
fn root_capability_sids_only_include_active_roots() {
let temp = TempDir::new().expect("tempdir");
let codex_home = temp.path().join("codex-home");
let workspace = temp.path().join("workspace");
let active_root = temp.path().join("active-root");
let stale_root = temp.path().join("stale-root");
std::fs::create_dir_all(&codex_home).expect("create codex home");
std::fs::create_dir_all(&workspace).expect("create workspace");
std::fs::create_dir_all(&active_root).expect("create active root");
std::fs::create_dir_all(&stale_root).expect("create stale root");
let stale_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &stale_root)
.expect("stale sid");
let active_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &active_root)
.expect("active sid");
let workspace_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &workspace)
.expect("workspace sid");
let caps = load_or_create_cap_sids(&codex_home).expect("load caps");
let sid_strs = root_capability_sids(
&codex_home,
&workspace,
vec![workspace.clone(), active_root],
)
.expect("root capabilities")
.into_iter()
.map(|root_sid| root_sid.sid_str)
.collect::<Vec<_>>();
assert_eq!(sid_strs.len(), 2);
assert!(sid_strs.contains(&workspace_sid));
assert!(sid_strs.contains(&active_sid));
assert!(!sid_strs.contains(&stale_sid));
assert!(!sid_strs.contains(&caps.workspace));
}
#[test]
fn legacy_deny_path_includes_nested_active_root_sid() {
let temp = TempDir::new().expect("tempdir");
let codex_home = temp.path().join("codex-home");
let workspace = temp.path().join("workspace");
let protected_dir = workspace.join(".codex");
let nested_root = protected_dir.join("nested-root");
let unrelated_root = temp.path().join("unrelated-root");
std::fs::create_dir_all(&codex_home).expect("create codex home");
std::fs::create_dir_all(&workspace).expect("create workspace");
std::fs::create_dir_all(&nested_root).expect("create nested root");
std::fs::create_dir_all(&unrelated_root).expect("create unrelated root");
let workspace_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &workspace)
.expect("workspace sid");
let nested_sid = workspace_write_cap_sid_for_root(&codex_home, &workspace, &nested_root)
.expect("nested sid");
let unrelated_sid =
workspace_write_cap_sid_for_root(&codex_home, &workspace, &unrelated_root)
.expect("unrelated sid");
let root_sids = root_capability_sids(
&codex_home,
&workspace,
vec![workspace.clone(), nested_root, unrelated_root],
)
.expect("root capabilities");
let deny_sid_strs = deny_root_capabilities_for_path(&protected_dir, &root_sids)
.into_iter()
.map(|root_sid| root_sid.sid_str.clone())
.collect::<Vec<_>>();
assert_eq!(deny_sid_strs, vec![workspace_sid, nested_sid]);
assert!(!deny_sid_strs.contains(&unrelated_sid));
}
#[test]
fn legacy_capability_roots_use_effective_write_roots() {
let temp = TempDir::new().expect("tempdir");
let codex_home = temp.path().join("codex-home");
let workspace = temp.path().join("workspace");
let active_root = temp.path().join("active-root");
let sandbox_root = codex_home.join(".sandbox");
std::fs::create_dir_all(&codex_home).expect("create codex home");
std::fs::create_dir_all(&workspace).expect("create workspace");
std::fs::create_dir_all(&active_root).expect("create active root");
std::fs::create_dir_all(&sandbox_root).expect("create sandbox root");
let writable_roots = vec![
AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"),
AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"),
AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"),
];
let permission_profile = workspace_profile(
NetworkSandboxPolicy::Restricted,
&writable_roots,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let workspace_roots = workspace_roots_for(workspace.as_path());
let permissions =
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
&permission_profile,
workspace_roots.as_slice(),
)
.expect("managed permission profile");
let roots =
legacy_session_capability_roots(&permissions, &workspace, &HashMap::new(), &codex_home);
assert!(roots.contains(&dunce::canonicalize(&workspace).expect("workspace")));
assert!(roots.contains(&dunce::canonicalize(&active_root).expect("active root")));
assert!(!roots.contains(&dunce::canonicalize(&codex_home).expect("codex home")));
assert!(!roots.contains(&dunce::canonicalize(&sandbox_root).expect("sandbox root")));
}
}