use crate::dpapi;use crate::logging::debug_log;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_for_permissions;use crate::setup::offline_proxy_settings_from_env;use crate::setup::run_elevated_setup;use crate::setup::run_setup_refresh_with_overrides;use crate::setup::sandbox_users_path;use crate::setup::setup_marker_path;use anyhow::Context;use anyhow::Result;use anyhow::anyhow;use base64::Engine;use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;use std::collections::HashMap;use std::fs;use std::path::Path;use std::path::PathBuf;#[derive(Debug, Clone)]struct SandboxIdentity { username: String, password: String,}#[derive(Debug, Clone)]pub struct SandboxCreds { pub username: String, pub password: String,}/// Returns true when the on-disk setup artifacts exist and match the current/// setup version.////// This is a coarse readiness check; `require_logon_sandbox_creds` performs the/// additional runtime validation for offline firewall settings.pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool { let marker_ok = matches!(load_marker(codex_home), Ok(Some(marker)) if marker.version_matches()); if !marker_ok { return false; } matches!(load_users(codex_home), Ok(Some(users)) if users.version_matches())}fn load_marker(codex_home: &Path) -> Result<Option<SetupMarker>> { let path = setup_marker_path(codex_home); let marker = match fs::read_to_string(&path) { Ok(contents) => match serde_json::from_str::<SetupMarker>(&contents) { Ok(m) => Some(m), Err(err) => { debug_log( &format!("sandbox setup marker parse failed: {err}"), Some(codex_home), ); None } }, Err(err) if err.kind() == std::io::ErrorKind::NotFound => None, Err(err) => { debug_log( &format!("sandbox setup marker read failed: {err}"), Some(codex_home), ); None } }; Ok(marker)}fn load_users(codex_home: &Path) -> Result<Option<SandboxUsersFile>> { let path = sandbox_users_path(codex_home); let file = match fs::read_to_string(&path) { Ok(contents) => contents, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(err) => { debug_log( &format!("sandbox users read failed: {err}"), Some(codex_home), ); return Ok(None); } }; match serde_json::from_str::<SandboxUsersFile>(&file) { Ok(users) => Ok(Some(users)), Err(err) => { debug_log( &format!("sandbox users parse failed: {err}"), Some(codex_home), ); Ok(None) } }}fn remove_sandbox_users_file(codex_home: &Path, reason: &str) -> Result<()> { let path = sandbox_users_path(codex_home); debug_log( &format!("{reason}; deleting {}", path.display()), Some(codex_home), ); match fs::remove_file(&path) { Ok(()) => Ok(()), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err).with_context(|| format!("delete {}", path.display())), }}fn decode_password(record: &SandboxUserRecord) -> Result<String> { let blob = BASE64_STANDARD .decode(record.password.as_bytes()) .context("base64 decode password")?; let decrypted = dpapi::unprotect(&blob)?; let pwd = String::from_utf8(decrypted).context("sandbox password not utf-8")?; Ok(pwd)}fn select_identity( network_identity: SandboxNetworkIdentity, codex_home: &Path,) -> Result<Option<SandboxIdentity>> { let _marker = match load_marker(codex_home)? { Some(m) if m.version_matches() => m, _ => return Ok(None), }; let users = match load_users(codex_home)? { Some(u) if u.version_matches() => u, _ => return Ok(None), }; let chosen = match network_identity { SandboxNetworkIdentity::Offline => users.offline, SandboxNetworkIdentity::Online => users.online, }; let password = decode_password(&chosen)?; Ok(Some(SandboxIdentity { username: chosen.username, password, }))}#[allow(clippy::too_many_arguments)]pub fn require_logon_sandbox_creds( permissions: &ResolvedWindowsSandboxPermissions, command_cwd: &Path, env_map: &HashMap<String, String>, codex_home: &Path, 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,) -> Result<SandboxCreds> { 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, permissions, env_map, codex_home)); let needed_write = write_roots_override .map(<[PathBuf]>::to_vec) .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 // granting the sandbox group access to this directory without granting the capability SID. let mut setup_reason: Option<String> = None; let mut identity = match load_marker(codex_home)? { Some(marker) if marker.version_matches() => { if let Some(reason) = marker.request_mismatch_reason(network_identity, &desired_offline_proxy_settings) { setup_reason = Some(reason); None } else { let selected = select_identity(network_identity, codex_home)?; if selected.is_none() { setup_reason = Some( "sandbox users missing or incompatible with marker version".to_string(), ); } selected } } _ => { setup_reason = Some("sandbox setup marker missing or incompatible".to_string()); None } }; if identity.is_none() { if let Some(reason) = &setup_reason { crate::logging::log_note( &format!("sandbox setup required: {reason}"), Some(&sandbox_dir), ); } else { crate::logging::log_note("sandbox setup required", Some(&sandbox_dir)); } run_elevated_setup( crate::setup::SandboxSetupRequest { permissions, command_cwd, env_map, codex_home, proxy_enforced, }, crate::setup::SetupRootOverrides { read_roots: Some(needed_read.clone()), read_roots_include_platform_defaults, write_roots: Some(needed_write.clone()), deny_read_paths: Some(deny_read_paths_override.to_vec()), deny_write_paths: Some(deny_write_paths_override.to_vec()), }, )?; identity = select_identity(network_identity, codex_home)?; } // Always refresh ACLs (non-elevated) for current roots via the setup binary. run_setup_refresh_with_overrides( crate::setup::SandboxSetupRequest { permissions, command_cwd, env_map, codex_home, proxy_enforced, }, crate::setup::SetupRootOverrides { read_roots: Some(needed_read), read_roots_include_platform_defaults, write_roots: Some(needed_write), deny_read_paths: Some(deny_read_paths_override.to_vec()), deny_write_paths: Some(deny_write_paths_override.to_vec()), }, )?; let identity = identity.ok_or_else(|| { anyhow!( "Windows sandbox setup is missing or out of date; rerun the sandbox setup with elevation" ) })?; Ok(SandboxCreds { username: identity.username, password: identity.password, })}#[allow(clippy::too_many_arguments)]pub(crate) fn refresh_logon_sandbox_creds( permissions: &ResolvedWindowsSandboxPermissions, command_cwd: &Path, env_map: &HashMap<String, String>, codex_home: &Path, 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,) -> Result<SandboxCreds> { remove_sandbox_users_file(codex_home, "sandbox user login failed")?; require_logon_sandbox_creds( permissions, command_cwd, env_map, codex_home, read_roots_override, read_roots_include_platform_defaults, write_roots_override, deny_read_paths_override, deny_write_paths_override, proxy_enforced, )}#[cfg(test)]mod tests { use super::remove_sandbox_users_file; use crate::setup::sandbox_users_path; use std::fs; use tempfile::TempDir; #[test] fn remove_sandbox_users_file_deletes_existing_file() { let codex_home = TempDir::new().expect("tempdir"); let users_path = sandbox_users_path(codex_home.path()); fs::create_dir_all(users_path.parent().expect("sandbox secrets dir")) .expect("create sandbox secrets dir"); fs::write(&users_path, "users").expect("write users"); remove_sandbox_users_file(codex_home.path(), "stale creds").expect("remove users"); assert!(!users_path.exists()); } #[test] fn remove_sandbox_users_file_ignores_missing_file() { let codex_home = TempDir::new().expect("tempdir"); let users_path = sandbox_users_path(codex_home.path()); remove_sandbox_users_file(codex_home.path(), "stale creds").expect("remove users"); assert!(!users_path.exists()); }}