use serde::Deserialize; use serde::Serialize; use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; use std::ffi::c_void; use std::os::windows::process::CommandExt; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::process::Stdio; use crate::allow::AllowDenyPaths; use crate::allow::compute_allow_paths; use crate::helper_materialization::helper_bin_dir; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; use crate::policy::SandboxPolicy; use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupFailure; use crate::setup_error::clear_setup_error_report; use crate::setup_error::failure; use crate::setup_error::read_setup_error_report; use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Security::AllocateAndInitializeSid; use windows_sys::Win32::Security::CheckTokenMembership; use windows_sys::Win32::Security::FreeSid; use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY; pub const SETUP_VERSION: u32 = 5; pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline"; pub const ONLINE_USERNAME: &str = "CodexSandboxOnline"; const ERROR_CANCELLED: u32 = 1223; const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020; const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220; const USERPROFILE_READ_ROOT_EXCLUSIONS: &[&str] = &[ ".ssh", ".gnupg", ".aws", ".azure", ".kube", ".docker", ".config", ".npm", ".pki", ".terraform.d", ]; const WINDOWS_PLATFORM_DEFAULT_READ_ROOTS: &[&str] = &[ r"C:\Windows", r"C:\Program Files", r"C:\Program Files (x86)", r"C:\ProgramData", ]; pub fn sandbox_dir(codex_home: &Path) -> PathBuf { codex_home.join(".sandbox") } pub fn sandbox_bin_dir(codex_home: &Path) -> PathBuf { codex_home.join(".sandbox-bin") } pub fn sandbox_secrets_dir(codex_home: &Path) -> PathBuf { codex_home.join(".sandbox-secrets") } pub fn setup_marker_path(codex_home: &Path) -> PathBuf { sandbox_dir(codex_home).join("setup_marker.json") } pub fn sandbox_users_path(codex_home: &Path) -> PathBuf { sandbox_secrets_dir(codex_home).join("sandbox_users.json") } pub struct SandboxSetupRequest<'a> { pub policy: &'a SandboxPolicy, pub policy_cwd: &'a Path, pub command_cwd: &'a Path, pub env_map: &'a HashMap, pub codex_home: &'a Path, pub proxy_enforced: bool, } #[derive(Default)] pub struct SetupRootOverrides { pub read_roots: Option>, pub write_roots: Option>, } pub fn run_setup_refresh( policy: &SandboxPolicy, policy_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, proxy_enforced: bool, ) -> Result<()> { run_setup_refresh_inner( SandboxSetupRequest { policy, policy_cwd, command_cwd, env_map, codex_home, proxy_enforced, }, SetupRootOverrides::default(), ) } pub fn run_setup_refresh_with_extra_read_roots( policy: &SandboxPolicy, policy_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, extra_read_roots: Vec, proxy_enforced: bool, ) -> Result<()> { let mut read_roots = gather_read_roots(command_cwd, policy, codex_home); read_roots.extend(extra_read_roots); run_setup_refresh_inner( SandboxSetupRequest { policy, policy_cwd, command_cwd, env_map, codex_home, proxy_enforced, }, SetupRootOverrides { read_roots: Some(read_roots), write_roots: Some(Vec::new()), }, ) } 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(()); } let (read_roots, write_roots) = build_payload_roots(&request, overrides); let network_identity = SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced); let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity); let payload = ElevationPayload { version: SETUP_VERSION, offline_username: OFFLINE_USERNAME.to_string(), online_username: ONLINE_USERNAME.to_string(), codex_home: request.codex_home.to_path_buf(), command_cwd: request.command_cwd.to_path_buf(), read_roots, write_roots, proxy_ports: offline_proxy_settings.proxy_ports, allow_local_binding: offline_proxy_settings.allow_local_binding, real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), refresh_only: true, }; let json = serde_json::to_vec(&payload)?; let b64 = BASE64_STANDARD.encode(json); let exe = find_setup_exe(); // Refresh should never request elevation; ensure verb isn't set and we don't trigger UAC. let mut cmd = Command::new(&exe); cmd.arg(&b64).stdout(Stdio::null()).stderr(Stdio::null()); let cwd = std::env::current_dir().unwrap_or_else(|_| request.codex_home.to_path_buf()); log_note( &format!( "setup refresh: spawning {} (cwd={}, payload_len={})", exe.display(), cwd.display(), b64.len() ), Some(&sandbox_dir(request.codex_home)), ); let status = cmd .status() .map_err(|e| { log_note( &format!("setup refresh: failed to spawn {}: {e}", exe.display()), Some(&sandbox_dir(request.codex_home)), ); e }) .context("spawn setup refresh")?; if !status.success() { log_note( &format!("setup refresh: exited with status {status:?}"), Some(&sandbox_dir(request.codex_home)), ); return Err(anyhow!("setup refresh failed with status {}", status)); } Ok(()) } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SetupMarker { pub version: u32, pub offline_username: String, pub online_username: String, #[serde(default)] pub created_at: Option, #[serde(default)] pub proxy_ports: Vec, #[serde(default)] pub allow_local_binding: bool, } impl SetupMarker { pub fn version_matches(&self) -> bool { self.version == SETUP_VERSION } pub(crate) fn request_mismatch_reason( &self, network_identity: SandboxNetworkIdentity, offline_proxy_settings: &OfflineProxySettings, ) -> Option { if !network_identity.uses_offline_identity() { return None; } if self.proxy_ports == offline_proxy_settings.proxy_ports && self.allow_local_binding == offline_proxy_settings.allow_local_binding { return None; } Some(format!( "offline firewall settings changed (stored_ports={:?}, desired_ports={:?}, stored_allow_local_binding={}, desired_allow_local_binding={})", self.proxy_ports, offline_proxy_settings.proxy_ports, self.allow_local_binding, offline_proxy_settings.allow_local_binding )) } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SandboxUserRecord { pub username: String, /// DPAPI-encrypted password blob, base64 encoded. pub password: String, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SandboxUsersFile { pub version: u32, pub offline: SandboxUserRecord, pub online: SandboxUserRecord, } impl SandboxUsersFile { pub fn version_matches(&self) -> bool { self.version == SETUP_VERSION } } fn is_elevated() -> Result { unsafe { let mut administrators_group: *mut c_void = std::ptr::null_mut(); let ok = AllocateAndInitializeSid( &SECURITY_NT_AUTHORITY, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &mut administrators_group, ); if ok == 0 { return Err(anyhow!( "AllocateAndInitializeSid failed: {}", GetLastError() )); } let mut is_member = 0i32; let check = CheckTokenMembership(0, administrators_group, &mut is_member as *mut _); FreeSid(administrators_group as *mut _); if check == 0 { return Err(anyhow!("CheckTokenMembership failed: {}", GetLastError())); } Ok(is_member != 0) } } fn canonical_existing(paths: &[PathBuf]) -> Vec { paths .iter() .filter_map(|p| { if !p.exists() { return None; } Some(dunce::canonicalize(p).unwrap_or_else(|_| p.clone())) }) .collect() } fn profile_read_roots(user_profile: &Path) -> Vec { let entries = match std::fs::read_dir(user_profile) { Ok(entries) => entries, Err(_) => return vec![user_profile.to_path_buf()], }; entries .filter_map(Result::ok) .map(|entry| (entry.file_name(), entry.path())) .filter(|(name, _)| { let name = name.to_string_lossy(); !USERPROFILE_READ_ROOT_EXCLUSIONS .iter() .any(|excluded| name.eq_ignore_ascii_case(excluded)) }) .map(|(_, path)| path) .collect() } fn gather_helper_read_roots(codex_home: &Path) -> Vec { let mut roots = Vec::new(); if let Ok(exe) = std::env::current_exe() && let Some(dir) = exe.parent() { roots.push(dir.to_path_buf()); } let helper_dir = helper_bin_dir(codex_home); let _ = std::fs::create_dir_all(&helper_dir); roots.push(helper_dir); roots } fn gather_legacy_full_read_roots( command_cwd: &Path, policy: &SandboxPolicy, codex_home: &Path, ) -> Vec { let mut roots = gather_helper_read_roots(codex_home); roots.extend( WINDOWS_PLATFORM_DEFAULT_READ_ROOTS .iter() .map(PathBuf::from), ); if let Ok(up) = std::env::var("USERPROFILE") { 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()); } } canonical_existing(&roots) } fn gather_restricted_read_roots( command_cwd: &Path, policy: &SandboxPolicy, codex_home: &Path, ) -> Vec { let mut roots = gather_helper_read_roots(codex_home); if policy.include_platform_defaults() { roots.extend( WINDOWS_PLATFORM_DEFAULT_READ_ROOTS .iter() .map(PathBuf::from), ); } roots.extend( policy .get_readable_roots_with_cwd(command_cwd) .into_iter() .map(|path| path.to_path_buf()), ); canonical_existing(&roots) } pub(crate) fn gather_read_roots( command_cwd: &Path, policy: &SandboxPolicy, codex_home: &Path, ) -> Vec { if policy.has_full_disk_read_access() { gather_legacy_full_read_roots(command_cwd, policy, codex_home) } else { gather_restricted_read_roots(command_cwd, policy, codex_home) } } pub(crate) fn gather_write_roots( policy: &SandboxPolicy, policy_cwd: &Path, command_cwd: &Path, env_map: &HashMap, ) -> Vec { let mut roots: Vec = 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 mut dedup: HashSet = HashSet::new(); let mut out: Vec = Vec::new(); for r in canonical_existing(&roots) { if dedup.insert(r.clone()) { out.push(r); } } out } #[derive(Serialize)] struct ElevationPayload { version: u32, offline_username: String, online_username: String, codex_home: PathBuf, command_cwd: PathBuf, read_roots: Vec, write_roots: Vec, #[serde(default)] proxy_ports: Vec, #[serde(default)] allow_local_binding: bool, real_user: String, #[serde(default)] refresh_only: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct OfflineProxySettings { pub proxy_ports: Vec, pub allow_local_binding: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SandboxNetworkIdentity { Offline, Online, } impl SandboxNetworkIdentity { pub(crate) fn from_policy(policy: &SandboxPolicy, proxy_enforced: bool) -> Self { if proxy_enforced || !policy.has_full_network_access() { Self::Offline } else { Self::Online } } pub(crate) fn uses_offline_identity(self) -> bool { matches!(self, Self::Offline) } } const PROXY_ENV_KEYS: &[&str] = &[ "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "WS_PROXY", "WSS_PROXY", "http_proxy", "https_proxy", "all_proxy", "ws_proxy", "wss_proxy", ]; const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING"; pub(crate) fn offline_proxy_settings_from_env( env_map: &HashMap, network_identity: SandboxNetworkIdentity, ) -> OfflineProxySettings { if !network_identity.uses_offline_identity() { return OfflineProxySettings { proxy_ports: vec![], allow_local_binding: false, }; } OfflineProxySettings { proxy_ports: proxy_ports_from_env(env_map), allow_local_binding: env_map .get(ALLOW_LOCAL_BINDING_ENV_KEY) .is_some_and(|value| value == "1"), } } pub(crate) fn proxy_ports_from_env(env_map: &HashMap) -> Vec { let mut ports = BTreeSet::new(); for key in PROXY_ENV_KEYS { if let Some(value) = env_map.get(*key) && let Some(port) = loopback_proxy_port_from_url(value) { ports.insert(port); } } ports.into_iter().collect() } fn loopback_proxy_port_from_url(url: &str) -> Option { let authority = url.trim().split_once("://")?.1.split('/').next()?; let host_port = authority.rsplit_once('@').map_or(authority, |(_, hp)| hp); if let Some(host) = host_port.strip_prefix('[') { let (host, rest) = host.split_once(']')?; if host != "::1" { return None; } let port = rest.strip_prefix(':')?.parse::().ok()?; return (port != 0).then_some(port); } let (host, port) = host_port.rsplit_once(':')?; if !(host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1") { return None; } let port = port.parse::().ok()?; (port != 0).then_some(port) } fn quote_arg(arg: &str) -> String { let needs = arg.is_empty() || arg .chars() .any(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '"')); if !needs { return arg.to_string(); } let mut out = String::from("\""); let mut bs = 0; for ch in arg.chars() { match ch { '\\' => { bs += 1; } '"' => { out.push_str(&"\\".repeat(bs * 2 + 1)); out.push('"'); bs = 0; } _ => { if bs > 0 { out.push_str(&"\\".repeat(bs)); bs = 0; } out.push(ch); } } } if bs > 0 { out.push_str(&"\\".repeat(bs * 2)); } out.push('"'); out } fn find_setup_exe() -> PathBuf { if let Ok(exe) = std::env::current_exe() && let Some(dir) = exe.parent() { let candidate = dir.join("codex-windows-sandbox-setup.exe"); if candidate.exists() { return candidate; } } PathBuf::from("codex-windows-sandbox-setup.exe") } fn report_helper_failure( codex_home: &Path, cleared_report: bool, exit_code: Option, ) -> anyhow::Error { let exit_detail = format!("setup helper exited with status {exit_code:?}"); if !cleared_report { return failure(SetupErrorCode::OrchestratorHelperExitNonzero, exit_detail); } match read_setup_error_report(codex_home) { Ok(Some(report)) => anyhow::Error::new(SetupFailure::from_report(report)), Ok(None) => failure(SetupErrorCode::OrchestratorHelperExitNonzero, exit_detail), Err(err) => failure( SetupErrorCode::OrchestratorHelperReportReadFailed, format!("{exit_detail}; failed to read setup_error.json: {err}"), ), } } fn run_setup_exe( payload: &ElevationPayload, needs_elevation: bool, codex_home: &Path, ) -> Result<()> { use windows_sys::Win32::System::Threading::GetExitCodeProcess; use windows_sys::Win32::System::Threading::INFINITE; use windows_sys::Win32::System::Threading::WaitForSingleObject; use windows_sys::Win32::UI::Shell::SEE_MASK_NOCLOSEPROCESS; use windows_sys::Win32::UI::Shell::SHELLEXECUTEINFOW; use windows_sys::Win32::UI::Shell::ShellExecuteExW; let exe = find_setup_exe(); let payload_json = serde_json::to_string(payload).map_err(|err| { failure( SetupErrorCode::OrchestratorPayloadSerializeFailed, format!("failed to serialize elevation payload: {err}"), ) })?; let payload_b64 = BASE64_STANDARD.encode(payload_json.as_bytes()); let cleared_report = match clear_setup_error_report(codex_home) { Ok(()) => true, Err(err) => { log_note( &format!( "setup orchestrator: failed to clear setup_error.json before launch: {err}" ), Some(&sandbox_dir(codex_home)), ); false } }; if !needs_elevation { let status = Command::new(&exe) .arg(&payload_b64) .creation_flags(0x08000000) // CREATE_NO_WINDOW .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .map_err(|err| { failure( SetupErrorCode::OrchestratorHelperLaunchFailed, format!("failed to launch setup helper (non-elevated): {err}"), ) })?; if !status.success() { return Err(report_helper_failure( codex_home, cleared_report, status.code(), )); } if let Err(err) = clear_setup_error_report(codex_home) { log_note( &format!( "setup orchestrator: failed to clear setup_error.json after success: {err}" ), Some(&sandbox_dir(codex_home)), ); } return Ok(()); } let exe_w = crate::winutil::to_wide(&exe); let params = quote_arg(&payload_b64); let params_w = crate::winutil::to_wide(params); let verb_w = crate::winutil::to_wide("runas"); let mut sei: SHELLEXECUTEINFOW = unsafe { std::mem::zeroed() }; sei.cbSize = std::mem::size_of::() as u32; sei.fMask = SEE_MASK_NOCLOSEPROCESS; sei.lpVerb = verb_w.as_ptr(); sei.lpFile = exe_w.as_ptr(); sei.lpParameters = params_w.as_ptr(); // Hide the window for the elevated helper. sei.nShow = 0; // SW_HIDE let ok = unsafe { ShellExecuteExW(&mut sei) }; if ok == 0 || sei.hProcess == 0 { let last_error = unsafe { GetLastError() }; let code = if last_error == ERROR_CANCELLED { SetupErrorCode::OrchestratorHelperLaunchCanceled } else { SetupErrorCode::OrchestratorHelperLaunchFailed }; return Err(failure( code, format!("ShellExecuteExW failed to launch setup helper: {last_error}"), )); } unsafe { WaitForSingleObject(sei.hProcess, INFINITE); let mut code: u32 = 1; GetExitCodeProcess(sei.hProcess, &mut code); CloseHandle(sei.hProcess); if code != 0 { return Err(report_helper_failure( codex_home, cleared_report, Some(code as i32), )); } } if let Err(err) = clear_setup_error_report(codex_home) { log_note( &format!("setup orchestrator: failed to clear setup_error.json after success: {err}"), Some(&sandbox_dir(codex_home)), ); } Ok(()) } pub fn run_elevated_setup( request: SandboxSetupRequest<'_>, overrides: SetupRootOverrides, ) -> Result<()> { // 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| { failure( SetupErrorCode::OrchestratorSandboxDirCreateFailed, format!("failed to create sandbox dir {}: {err}", sbx_dir.display()), ) })?; let (read_roots, write_roots) = build_payload_roots(&request, overrides); let network_identity = SandboxNetworkIdentity::from_policy(request.policy, request.proxy_enforced); let offline_proxy_settings = offline_proxy_settings_from_env(request.env_map, network_identity); let payload = ElevationPayload { version: SETUP_VERSION, offline_username: OFFLINE_USERNAME.to_string(), online_username: ONLINE_USERNAME.to_string(), codex_home: request.codex_home.to_path_buf(), command_cwd: request.command_cwd.to_path_buf(), read_roots, write_roots, proxy_ports: offline_proxy_settings.proxy_ports, allow_local_binding: offline_proxy_settings.allow_local_binding, real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), refresh_only: false, }; let needs_elevation = !is_elevated().map_err(|err| { failure( SetupErrorCode::OrchestratorElevationCheckFailed, format!("failed to determine elevation state: {err}"), ) })?; run_setup_exe(&payload, needs_elevation, request.codex_home) } fn build_payload_roots( request: &SandboxSetupRequest<'_>, overrides: SetupRootOverrides, ) -> (Vec, Vec) { let write_roots = if let Some(roots) = overrides.write_roots { canonical_existing(&roots) } else { gather_write_roots( request.policy, request.policy_cwd, request.command_cwd, request.env_map, ) }; let write_roots = filter_sensitive_write_roots(write_roots, request.codex_home); let mut read_roots = if let Some(roots) = overrides.read_roots { canonical_existing(&roots) } else { gather_read_roots(request.command_cwd, request.policy, request.codex_home) }; let write_root_set: HashSet = write_roots.iter().cloned().collect(); read_roots.retain(|root| !write_root_set.contains(root)); (read_roots, write_roots) } fn filter_sensitive_write_roots(mut roots: Vec, codex_home: &Path) -> Vec { // Never grant capability write access to CODEX_HOME or anything under CODEX_HOME/.sandbox, // CODEX_HOME/.sandbox-bin, or CODEX_HOME/.sandbox-secrets. These locations contain sandbox // control/state and helper binaries and must remain tamper-resistant. let codex_home_key = canonical_path_key(codex_home); let sbx_dir_key = canonical_path_key(&sandbox_dir(codex_home)); let sbx_dir_prefix = format!("{}/", sbx_dir_key.trim_end_matches('/')); let sbx_bin_dir_key = canonical_path_key(&sandbox_bin_dir(codex_home)); let sbx_bin_dir_prefix = format!("{}/", sbx_bin_dir_key.trim_end_matches('/')); let secrets_dir_key = canonical_path_key(&sandbox_secrets_dir(codex_home)); let secrets_dir_prefix = format!("{}/", secrets_dir_key.trim_end_matches('/')); roots.retain(|root| { let key = canonical_path_key(root); key != codex_home_key && key != sbx_dir_key && !key.starts_with(&sbx_dir_prefix) && key != sbx_bin_dir_key && !key.starts_with(&sbx_bin_dir_prefix) && key != secrets_dir_key && !key.starts_with(&secrets_dir_prefix) }); roots } #[cfg(test)] mod tests { use super::WINDOWS_PLATFORM_DEFAULT_READ_ROOTS; use super::gather_legacy_full_read_roots; use super::gather_read_roots; use super::loopback_proxy_port_from_url; use super::offline_proxy_settings_from_env; use super::profile_read_roots; use super::proxy_ports_from_env; use crate::helper_materialization::helper_bin_dir; use crate::policy::SandboxPolicy; use codex_protocol::protocol::ReadOnlyAccess; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::collections::HashSet; use std::fs; use std::path::PathBuf; use tempfile::TempDir; fn canonical_windows_platform_default_roots() -> Vec { WINDOWS_PLATFORM_DEFAULT_READ_ROOTS .iter() .map(|path| dunce::canonicalize(path).unwrap_or_else(|_| PathBuf::from(path))) .collect() } #[test] fn loopback_proxy_url_parsing_supports_common_forms() { assert_eq!( loopback_proxy_port_from_url("http://localhost:3128"), Some(3128) ); assert_eq!( loopback_proxy_port_from_url("https://127.0.0.1:8080"), Some(8080) ); assert_eq!( loopback_proxy_port_from_url("socks5h://user:pass@[::1]:1080"), Some(1080) ); } #[test] fn loopback_proxy_url_parsing_rejects_non_loopback_and_zero_port() { assert_eq!( loopback_proxy_port_from_url("http://example.com:3128"), None ); assert_eq!(loopback_proxy_port_from_url("http://127.0.0.1:0"), None); assert_eq!(loopback_proxy_port_from_url("localhost:8080"), None); } #[test] fn proxy_ports_from_env_dedupes_and_sorts() { let mut env = HashMap::new(); env.insert( "HTTP_PROXY".to_string(), "http://127.0.0.1:8080".to_string(), ); env.insert( "http_proxy".to_string(), "http://localhost:8080".to_string(), ); env.insert("ALL_PROXY".to_string(), "socks5h://[::1]:1081".to_string()); env.insert( "HTTPS_PROXY".to_string(), "https://example.com:9999".to_string(), ); assert_eq!(proxy_ports_from_env(&env), vec![1081, 8080]); } #[test] fn offline_proxy_settings_ignore_proxy_env_when_online_identity_selected() { let mut env = HashMap::new(); env.insert( "HTTP_PROXY".to_string(), "http://127.0.0.1:8080".to_string(), ); env.insert( "CODEX_NETWORK_ALLOW_LOCAL_BINDING".to_string(), "1".to_string(), ); assert_eq!( offline_proxy_settings_from_env(&env, super::SandboxNetworkIdentity::Online), super::OfflineProxySettings { proxy_ports: vec![], allow_local_binding: false, } ); } #[test] fn offline_proxy_settings_capture_proxy_ports_and_local_binding_for_offline_identity() { let mut env = HashMap::new(); env.insert( "HTTP_PROXY".to_string(), "http://127.0.0.1:8080".to_string(), ); env.insert( "ALL_PROXY".to_string(), "socks5h://127.0.0.1:1081".to_string(), ); env.insert( "CODEX_NETWORK_ALLOW_LOCAL_BINDING".to_string(), "1".to_string(), ); assert_eq!( offline_proxy_settings_from_env(&env, super::SandboxNetworkIdentity::Offline), super::OfflineProxySettings { proxy_ports: vec![1081, 8080], allow_local_binding: true, } ); } #[test] fn setup_marker_request_mismatch_reason_ignores_proxy_drift_for_online_identity() { let marker = super::SetupMarker { version: super::SETUP_VERSION, offline_username: "offline".to_string(), online_username: "online".to_string(), created_at: None, proxy_ports: vec![3128], allow_local_binding: false, }; let desired = super::OfflineProxySettings { proxy_ports: vec![1081, 8080], allow_local_binding: true, }; assert_eq!( marker.request_mismatch_reason(super::SandboxNetworkIdentity::Online, &desired), None ); } #[test] fn setup_marker_request_mismatch_reason_reports_offline_firewall_drift() { let marker = super::SetupMarker { version: super::SETUP_VERSION, offline_username: "offline".to_string(), online_username: "online".to_string(), created_at: None, proxy_ports: vec![3128], allow_local_binding: false, }; let desired = super::OfflineProxySettings { proxy_ports: vec![1081, 8080], allow_local_binding: true, }; assert_eq!( marker.request_mismatch_reason(super::SandboxNetworkIdentity::Offline, &desired), Some( "offline firewall settings changed (stored_ports=[3128], desired_ports=[1081, 8080], stored_allow_local_binding=false, desired_allow_local_binding=true)" .to_string() ) ); } #[test] fn profile_read_roots_excludes_configured_top_level_entries() { let tmp = TempDir::new().expect("tempdir"); let user_profile = tmp.path(); let allowed_dir = user_profile.join("Documents"); let allowed_file = user_profile.join(".gitconfig"); let excluded_dir = user_profile.join(".ssh"); let excluded_case_variant = user_profile.join(".AWS"); fs::create_dir_all(&allowed_dir).expect("create allowed dir"); fs::write(&allowed_file, "safe").expect("create allowed file"); fs::create_dir_all(&excluded_dir).expect("create excluded dir"); fs::create_dir_all(&excluded_case_variant).expect("create excluded case variant"); let roots = profile_read_roots(user_profile); let actual: HashSet = roots.into_iter().collect(); let expected: HashSet = [allowed_dir, allowed_file].into_iter().collect(); assert_eq!(expected, actual); } #[test] fn profile_read_roots_falls_back_to_profile_root_when_enumeration_fails() { let tmp = TempDir::new().expect("tempdir"); let missing_profile = tmp.path().join("missing-user-profile"); let roots = profile_read_roots(&missing_profile); assert_eq!(vec![missing_profile], roots); } #[test] fn gather_read_roots_includes_helper_bin_dir() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); fs::create_dir_all(&command_cwd).expect("create workspace"); let policy = SandboxPolicy::new_read_only_policy(); let roots = gather_read_roots(&command_cwd, &policy, &codex_home); let expected = dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir"); assert!(roots.contains(&expected)); } #[test] fn restricted_read_roots_skip_platform_defaults_when_disabled() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); let readable_root = tmp.path().join("docs"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&readable_root).expect("create readable root"); let policy = SandboxPolicy::ReadOnly { access: ReadOnlyAccess::Restricted { include_platform_defaults: false, readable_roots: vec![ AbsolutePathBuf::from_absolute_path(&readable_root) .expect("absolute readable root"), ], }, network_access: false, }; let roots = gather_read_roots(&command_cwd, &policy, &codex_home); let expected_helper = dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir"); let expected_cwd = dunce::canonicalize(&command_cwd).expect("canonical workspace"); let expected_readable = dunce::canonicalize(&readable_root).expect("canonical readable root"); assert!(roots.contains(&expected_helper)); assert!(roots.contains(&expected_cwd)); assert!(roots.contains(&expected_readable)); assert!( canonical_windows_platform_default_roots() .into_iter() .all(|path| !roots.contains(&path)) ); } #[test] fn restricted_read_roots_include_platform_defaults_when_enabled() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); fs::create_dir_all(&command_cwd).expect("create workspace"); let policy = SandboxPolicy::ReadOnly { access: ReadOnlyAccess::Restricted { include_platform_defaults: true, readable_roots: Vec::new(), }, network_access: false, }; let roots = gather_read_roots(&command_cwd, &policy, &codex_home); assert!( canonical_windows_platform_default_roots() .into_iter() .all(|path| roots.contains(&path)) ); } #[test] fn restricted_workspace_write_roots_remain_readable() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); let writable_root = tmp.path().join("extra-write-root"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&writable_root).expect("create writable root"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![ AbsolutePathBuf::from_absolute_path(&writable_root) .expect("absolute writable root"), ], read_only_access: ReadOnlyAccess::Restricted { include_platform_defaults: false, readable_roots: Vec::new(), }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; let roots = gather_read_roots(&command_cwd, &policy, &codex_home); let expected_writable = dunce::canonicalize(&writable_root).expect("canonical writable root"); assert!(roots.contains(&expected_writable)); } #[test] fn full_read_roots_preserve_legacy_platform_defaults() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); fs::create_dir_all(&command_cwd).expect("create workspace"); let policy = SandboxPolicy::new_read_only_policy(); let roots = gather_legacy_full_read_roots(&command_cwd, &policy, &codex_home); assert!( canonical_windows_platform_default_roots() .into_iter() .all(|path| roots.contains(&path)) ); } }