use anyhow::Context; use anyhow::Result; use codex_utils_string::sanitize_metric_tag_value; use serde::Deserialize; use serde::Serialize; use std::fs; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; /// These represent the most common failures for the elevated sandbox setup. /// /// Codes are used as metric tags. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SetupErrorCode { // Orchestrator (run in CLI) failures. /// Failed to create `codex_home/.sandbox` in the orchestrator. OrchestratorSandboxDirCreateFailed, /// Failed to determine whether the current process is elevated. OrchestratorElevationCheckFailed, /// Failed to serialize the elevation payload before launching the helper. OrchestratorPayloadSerializeFailed, /// Failed to launch the setup helper process (spawn or ShellExecuteExW). OrchestratorHelperLaunchFailed, /// User canceled the UAC prompt while launching the helper. OrchestratorHelperLaunchCanceled, /// Helper exited non-zero and no structured report was available. OrchestratorHelperExitNonzero, /// Helper exited non-zero and reading `setup_error.json` failed. OrchestratorHelperReportReadFailed, // Helper (elevated process) failures. /// Helper failed while validating or decoding the request payload. HelperRequestArgsFailed, /// Helper failed to create `codex_home/.sandbox`. HelperSandboxDirCreateFailed, /// Helper failed to open or write the setup log. HelperLogFailed, /// Helper failed in the provisioning phase (fallback bucket). HelperUserProvisionFailed, /// Helper failed to create the sandbox users local group. HelperUsersGroupCreateFailed, /// Helper failed to create or update a sandbox user account. HelperUserCreateOrUpdateFailed, /// Helper failed to protect user passwords with DPAPI. HelperDpapiProtectFailed, /// Helper failed to write the sandbox users secrets file. HelperUsersFileWriteFailed, /// Helper failed to write the setup marker file. HelperSetupMarkerWriteFailed, /// Helper failed to resolve a SID or convert it to a PSID. HelperSidResolveFailed, /// Helper failed to load or convert capability SIDs. HelperCapabilitySidFailed, /// Helper failed to initialize COM for firewall configuration. HelperFirewallComInitFailed, /// Helper failed to access firewall policy or rule collections. HelperFirewallPolicyAccessFailed, /// Helper failed to create, update, or add the firewall rule. HelperFirewallRuleCreateOrAddFailed, /// Helper failed to verify the configured firewall rule scope. HelperFirewallRuleVerifyFailed, /// Helper failed to spawn the read-ACL helper process. HelperReadAclHelperSpawnFailed, /// Helper failed to lock down sandbox directories via ACLs. HelperSandboxLockFailed, /// Helper failed for an unmapped or unexpected reason. HelperUnknownError, } impl SetupErrorCode { pub fn as_str(self) -> &'static str { match self { Self::OrchestratorSandboxDirCreateFailed => "orchestrator_sandbox_dir_create_failed", Self::OrchestratorElevationCheckFailed => "orchestrator_elevation_check_failed", Self::OrchestratorPayloadSerializeFailed => "orchestrator_payload_serialize_failed", Self::OrchestratorHelperLaunchFailed => "orchestrator_helper_launch_failed", Self::OrchestratorHelperLaunchCanceled => "orchestrator_helper_launch_canceled", Self::OrchestratorHelperExitNonzero => "orchestrator_helper_exit_nonzero", Self::OrchestratorHelperReportReadFailed => "orchestrator_helper_report_read_failed", Self::HelperRequestArgsFailed => "helper_request_args_failed", Self::HelperSandboxDirCreateFailed => "helper_sandbox_dir_create_failed", Self::HelperLogFailed => "helper_log_failed", Self::HelperUserProvisionFailed => "helper_user_provision_failed", Self::HelperUsersGroupCreateFailed => "helper_users_group_create_failed", Self::HelperUserCreateOrUpdateFailed => "helper_user_create_or_update_failed", Self::HelperDpapiProtectFailed => "helper_dpapi_protect_failed", Self::HelperUsersFileWriteFailed => "helper_users_file_write_failed", Self::HelperSetupMarkerWriteFailed => "helper_setup_marker_write_failed", Self::HelperSidResolveFailed => "helper_sid_resolve_failed", Self::HelperCapabilitySidFailed => "helper_capability_sid_failed", Self::HelperFirewallComInitFailed => "helper_firewall_com_init_failed", Self::HelperFirewallPolicyAccessFailed => "helper_firewall_policy_access_failed", Self::HelperFirewallRuleCreateOrAddFailed => { "helper_firewall_rule_create_or_add_failed" } Self::HelperFirewallRuleVerifyFailed => "helper_firewall_rule_verify_failed", Self::HelperReadAclHelperSpawnFailed => "helper_read_acl_helper_spawn_failed", Self::HelperSandboxLockFailed => "helper_sandbox_lock_failed", Self::HelperUnknownError => "helper_unknown_error", } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SetupErrorReport { pub code: SetupErrorCode, pub message: String, } #[derive(Debug)] pub struct SetupFailure { pub code: SetupErrorCode, pub message: String, } impl SetupFailure { pub fn new(code: SetupErrorCode, message: impl Into) -> Self { Self { code, message: message.into(), } } pub fn from_report(report: SetupErrorReport) -> Self { Self::new(report.code, report.message) } pub fn metric_message(&self) -> String { sanitize_setup_metric_tag_value(&self.message) } } impl std::fmt::Display for SetupFailure { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}: {}", self.code.as_str(), self.message) } } impl std::error::Error for SetupFailure {} pub fn failure(code: SetupErrorCode, message: impl Into) -> anyhow::Error { anyhow::Error::new(SetupFailure::new(code, message)) } pub fn extract_failure(err: &anyhow::Error) -> Option<&SetupFailure> { err.downcast_ref::() } pub fn setup_error_path(codex_home: &Path) -> PathBuf { codex_home.join(".sandbox").join("setup_error.json") } pub fn clear_setup_error_report(codex_home: &Path) -> Result<()> { let path = setup_error_path(codex_home); match fs::remove_file(&path) { Ok(()) => Ok(()), Err(err) if err.kind() == ErrorKind::NotFound => Ok(()), Err(err) => Err(err).with_context(|| format!("remove {}", path.display())), } } pub fn write_setup_error_report(codex_home: &Path, report: &SetupErrorReport) -> Result<()> { let sandbox_dir = codex_home.join(".sandbox"); fs::create_dir_all(&sandbox_dir) .with_context(|| format!("create sandbox dir {}", sandbox_dir.display()))?; let path = setup_error_path(codex_home); let json = serde_json::to_vec_pretty(report)?; fs::write(&path, json).with_context(|| format!("write {}", path.display()))?; Ok(()) } pub fn read_setup_error_report(codex_home: &Path) -> Result> { let path = setup_error_path(codex_home); let bytes = match fs::read(&path) { Ok(bytes) => bytes, Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), Err(err) => return Err(err).with_context(|| format!("read {}", path.display())), }; let report = serde_json::from_slice::(&bytes) .with_context(|| format!("parse {}", path.display()))?; Ok(Some(report)) } /// Sanitize a setup error message for use as a metric tag. pub fn sanitize_setup_metric_tag_value(value: &str) -> String { sanitize_metric_tag_value(redact_home_paths(value).as_str()) } fn redact_home_paths(value: &str) -> String { let mut usernames: Vec = Vec::new(); if let Ok(username) = std::env::var("USERNAME") && !username.trim().is_empty() { usernames.push(username); } if let Ok(user) = std::env::var("USER") && !user.trim().is_empty() && !usernames.iter().any(|v| v.eq_ignore_ascii_case(&user)) { usernames.push(user); } redact_username_segments(value, &usernames) } fn redact_username_segments(value: &str, usernames: &[String]) -> String { if usernames.is_empty() { return value.to_string(); } let mut segments: Vec = Vec::new(); let mut separators: Vec = Vec::new(); let mut current = String::new(); for ch in value.chars() { if ch == '\\' || ch == '/' { segments.push(std::mem::take(&mut current)); separators.push(ch); } else { current.push(ch); } } segments.push(current); for segment in &mut segments { let matches = if cfg!(windows) { usernames .iter() .any(|name| segment.eq_ignore_ascii_case(name)) } else { usernames.iter().any(|name| segment == name) }; if matches { *segment = "".to_string(); } } let mut out = String::new(); for (idx, segment) in segments.iter().enumerate() { out.push_str(segment); if let Some(sep) = separators.get(idx) { out.push(*sep); } } out } #[cfg(test)] mod tests { use super::redact_username_segments; use pretty_assertions::assert_eq; #[test] fn sanitize_tag_value_redacts_username_segments() { let usernames = vec!["Alice".to_string(), "Bob".to_string()]; let msg = "failed to write C:\\Users\\Alice\\file.txt; fallback D:\\Profiles\\Bob\\x"; let redacted = redact_username_segments(msg, &usernames); assert_eq!( redacted, "failed to write C:\\Users\\\\file.txt; fallback D:\\Profiles\\\\x" ); } #[test] fn sanitize_tag_value_leaves_unknown_segments() { let usernames = vec!["Alice".to_string()]; let msg = "failed to write E:\\data\\file.txt"; let redacted = redact_username_segments(msg, &usernames); assert_eq!(redacted, msg); } #[test] fn sanitize_tag_value_redacts_multiple_occurrences() { let usernames = vec!["Alice".to_string()]; let msg = "C:\\Users\\Alice\\a and C:\\Users\\Alice\\b"; let redacted = redact_username_segments(msg, &usernames); assert_eq!(redacted, "C:\\Users\\\\a and C:\\Users\\\\b"); } }