diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 8dfe6dc33f4..e5660301b5b 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -50,6 +50,8 @@ mod marketplace_cmd; mod mcp_cmd; mod plugin_cmd; mod remote_control_cmd; +#[cfg(target_os = "windows")] +mod sandbox_setup; mod state_db_recovery; #[cfg(not(windows))] mod wsl_paths; @@ -1250,6 +1252,16 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { .await?; } Some(Subcommand::Sandbox(mut sandbox_cli)) => { + #[cfg(target_os = "windows")] + if let Some(setup_cli) = sandbox_setup::parse_setup_command(&sandbox_cli.command)? { + reject_remote_mode_for_subcommand( + root_remote.as_deref(), + root_remote_auth_token_env.as_deref(), + "sandbox setup", + )?; + sandbox_setup::run(setup_cli).await?; + return Ok(()); + } reject_remote_mode_for_subcommand( root_remote.as_deref(), root_remote_auth_token_env.as_deref(), diff --git a/codex-rs/cli/src/sandbox_setup.rs b/codex-rs/cli/src/sandbox_setup.rs new file mode 100644 index 00000000000..958411c4285 --- /dev/null +++ b/codex-rs/cli/src/sandbox_setup.rs @@ -0,0 +1,206 @@ +use std::path::PathBuf; + +use clap::ArgAction; +use clap::ArgGroup; +use clap::Parser; +use codex_core::config::edit::ConfigEditsBuilder; +use codex_core::config::find_codex_home; + +#[derive(Debug, Parser)] +#[command(group( + ArgGroup::new("sandbox_user") + .required(true) + .args(["user", "current_user"]) +))] +pub(crate) struct SandboxSetupCommand { + /// Set up the elevated Windows sandbox. + #[arg(long = "elevated", action = ArgAction::SetTrue)] + elevated_sandbox_level: bool, + + /// Windows user that will run Codex after managed deployment. + #[arg( + long = "user", + value_name = "USER", + conflicts_with = "current_user", + requires = "codex_home" + )] + user: Option, + + /// Use the current Windows user as the Codex user. + #[arg( + long = "current-user", + default_value_t = false, + conflicts_with = "user" + )] + current_user: bool, + + /// CODEX_HOME for the Codex user. Required with --user. + #[arg(long = "codex-home", value_name = "DIR")] + codex_home: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SandboxSetupLevel { + Elevated, +} + +impl SandboxSetupCommand { + fn setup_level(&self) -> anyhow::Result { + if self.elevated_sandbox_level { + Ok(SandboxSetupLevel::Elevated) + } else { + anyhow::bail!("`codex sandbox setup` currently requires --elevated"); + } + } +} + +pub(crate) async fn run(cmd: SandboxSetupCommand) -> anyhow::Result<()> { + match cmd.setup_level()? { + SandboxSetupLevel::Elevated => run_elevated(cmd).await, + } +} + +pub(crate) fn parse_setup_command( + sandbox_command: &[String], +) -> anyhow::Result> { + if sandbox_command + .first() + .is_none_or(|command| command != "setup") + { + return Ok(None); + } + + SandboxSetupCommand::try_parse_from(sandbox_command.iter().map(String::as_str)) + .map(Some) + .map_err(anyhow::Error::from) +} + +async fn run_elevated(cmd: SandboxSetupCommand) -> anyhow::Result<()> { + let identity = resolve_sandbox_setup_identity(&cmd)?; + + codex_core::windows_sandbox::run_elevated_provisioning_setup( + identity.codex_home.as_path(), + identity.real_user.as_str(), + )?; + ConfigEditsBuilder::new(identity.codex_home.as_path()) + .set_windows_sandbox_mode("elevated") + .apply() + .await + .map_err(|err| { + anyhow::anyhow!( + "sandbox provisioning succeeded, but failed to persist elevated sandbox config: {err}" + ) + })?; + + println!( + "Windows elevated sandbox setup completed for {} at {}.", + identity.real_user, + identity.codex_home.display() + ); + Ok(()) +} + +struct SandboxSetupIdentity { + real_user: String, + codex_home: PathBuf, +} + +fn resolve_sandbox_setup_identity( + cmd: &SandboxSetupCommand, +) -> anyhow::Result { + if cmd.current_user { + let real_user = std::env::var("USERNAME") + .or_else(|_| std::env::var("USER")) + .map_err(|err| { + anyhow::anyhow!("failed to determine current user from environment: {err}") + })?; + let codex_home = match cmd.codex_home.clone() { + Some(codex_home) => codex_home, + None => find_codex_home()?.to_path_buf(), + }; + return Ok(SandboxSetupIdentity { + real_user, + codex_home, + }); + } + + let real_user = cmd + .user + .clone() + .ok_or_else(|| anyhow::anyhow!("--user or --current-user is required"))?; + let codex_home = cmd + .codex_home + .clone() + .ok_or_else(|| anyhow::anyhow!("--codex-home is required with --user"))?; + Ok(SandboxSetupIdentity { + real_user, + codex_home, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_managed_user_identity() { + let command = SandboxSetupCommand::try_parse_from([ + "setup", + "--elevated", + "--user", + "DOMAIN\\alice", + "--codex-home", + r"C:\Users\alice\.codex", + ]) + .expect("parse"); + + assert!(command.elevated_sandbox_level); + assert_eq!(command.user.as_deref(), Some(r"DOMAIN\alice")); + assert!(!command.current_user); + assert_eq!( + command.codex_home.as_deref(), + Some(std::path::Path::new(r"C:\Users\alice\.codex")) + ); + } + + #[test] + fn requires_explicit_user_identity() { + let err = SandboxSetupCommand::try_parse_from(["setup", "--elevated"]) + .expect_err("parse should fail"); + + assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument); + } + + #[test] + fn requires_codex_home_for_managed_user() { + let err = + SandboxSetupCommand::try_parse_from(["setup", "--elevated", "--user", "DOMAIN\\alice"]) + .expect_err("parse should fail"); + + assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument); + } + + #[test] + fn parses_setup_from_sandbox_command_args() { + let command = parse_setup_command(&[ + "setup".to_string(), + "--elevated".to_string(), + "--user".to_string(), + r"DOMAIN\alice".to_string(), + "--codex-home".to_string(), + r"C:\Users\alice\.codex".to_string(), + ]) + .expect("parse") + .expect("setup command"); + + assert_eq!(command.user.as_deref(), Some(r"DOMAIN\alice")); + } + + #[test] + fn ignores_non_setup_sandbox_command_args() { + let command = + parse_setup_command(&["echo".to_string(), "hello".to_string()]).expect("parse"); + + assert!(command.is_none()); + } +} diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index b2ac8bf1f02..3f21ba6864e 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -168,6 +168,11 @@ pub fn run_elevated_setup( ) } +#[cfg(target_os = "windows")] +pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> anyhow::Result<()> { + codex_windows_sandbox::run_elevated_provisioning_setup(codex_home, real_user) +} + #[cfg(not(target_os = "windows"))] pub fn run_elevated_setup( _permission_profile: &PermissionProfile, @@ -179,6 +184,11 @@ pub fn run_elevated_setup( anyhow::bail!("elevated Windows sandbox setup is only supported on Windows") } +#[cfg(not(target_os = "windows"))] +pub fn run_elevated_provisioning_setup(_codex_home: &Path, _real_user: &str) -> anyhow::Result<()> { + anyhow::bail!("elevated Windows sandbox setup is only supported on Windows") +} + #[cfg(target_os = "windows")] pub fn run_legacy_setup_preflight( permission_profile: &PermissionProfile, diff --git a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs index 9b178fdca9d..41fa2f720eb 100644 --- a/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs +++ b/codex-rs/windows-sandbox-rs/src/bin/setup_main/win.rs @@ -105,6 +105,7 @@ struct Payload { enum SetupMode { #[default] Full, + ProvisionOnly, ReadAclsOnly, } @@ -476,6 +477,7 @@ fn real_main() -> Result<()> { fn run_setup(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> { match payload.mode { SetupMode::ReadAclsOnly => run_read_acl_only(payload, log), + SetupMode::ProvisionOnly => run_provision_only(payload, log, sbx_dir), SetupMode::Full => run_setup_full(payload, log, sbx_dir), } } @@ -543,31 +545,182 @@ fn run_read_acl_only(payload: &Payload, log: &mut dyn Write) -> Result<()> { Ok(()) } +fn provision_and_hide_sandbox_users( + payload: &Payload, + log: &mut dyn Write, + sbx_dir: &Path, +) -> Result<()> { + let provision_result = provision_sandbox_users( + &payload.codex_home, + &payload.offline_username, + &payload.online_username, + &payload.proxy_ports, + payload.allow_local_binding, + log, + ); + if let Err(err) = provision_result { + if extract_setup_failure(&err).is_some() { + return Err(err); + } + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperUserProvisionFailed, + format!("provision sandbox users failed: {err}"), + ))); + } + let users = vec![ + payload.offline_username.clone(), + payload.online_username.clone(), + ]; + hide_newly_created_users(&users, sbx_dir); + Ok(()) +} + +fn configure_offline_sandbox_network( + payload: &Payload, + offline_sid_str: &str, + log: &mut dyn Write, +) -> Result<()> { + let proxy_allowlist_result = firewall::ensure_offline_proxy_allowlist( + offline_sid_str, + &payload.proxy_ports, + payload.allow_local_binding, + log, + ); + if let Err(err) = proxy_allowlist_result { + if extract_setup_failure(&err).is_some() { + return Err(err); + } + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("ensure offline proxy allowlist failed: {err}"), + ))); + } + let firewall_result = firewall::ensure_offline_outbound_block(offline_sid_str, log); + if let Err(err) = firewall_result { + if extract_setup_failure(&err).is_some() { + return Err(err); + } + return Err(anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, + format!("ensure offline outbound block failed: {err}"), + ))); + } + install_wfp_filters( + &payload.codex_home, + &payload.offline_username, + payload.otel.as_ref(), + |message| { + let _ = log_line(log, message); + }, + ); + Ok(()) +} + +fn lock_persistent_sandbox_dirs( + payload: &Payload, + sandbox_group_sid: &[u8], + log: &mut dyn Write, +) -> Result<()> { + lock_sandbox_dir( + &sandbox_dir(&payload.codex_home), + &payload.real_user, + sandbox_group_sid, + GRANT_ACCESS, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE, + log, + ) + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSandboxLockFailed, + format!( + "lock sandbox dir {} failed: {err}", + sandbox_dir(&payload.codex_home).display() + ), + )) + })?; + lock_sandbox_dir( + &sandbox_secrets_dir(&payload.codex_home), + &payload.real_user, + sandbox_group_sid, + DENY_ACCESS, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE, + log, + ) + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSandboxLockFailed, + format!( + "lock sandbox secrets dir {} failed: {err}", + sandbox_secrets_dir(&payload.codex_home).display() + ), + )) + })?; + let legacy_users = sandbox_dir(&payload.codex_home).join("sandbox_users.json"); + if legacy_users.exists() { + let _ = std::fs::remove_file(&legacy_users); + } + Ok(()) +} + +fn lock_sandbox_bin_dir( + payload: &Payload, + sandbox_group_sid: &[u8], + log: &mut dyn Write, +) -> Result<()> { + lock_sandbox_dir( + &sandbox_bin_dir(&payload.codex_home), + &payload.real_user, + sandbox_group_sid, + GRANT_ACCESS, + FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, + log, + ) + .map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSandboxLockFailed, + format!( + "lock sandbox bin dir {} failed: {err}", + sandbox_bin_dir(&payload.codex_home).display() + ), + )) + }) +} + +fn run_provision_only(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> { + provision_and_hide_sandbox_users(payload, log, sbx_dir)?; + let offline_sid = resolve_sid(&payload.offline_username).map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSidResolveFailed, + format!( + "resolve SID for offline user {} failed: {err}", + payload.offline_username + ), + )) + })?; + let offline_sid_str = string_from_sid_bytes(&offline_sid).map_err(anyhow::Error::msg)?; + + let sandbox_group_sid = resolve_sandbox_users_group_sid().map_err(|err| { + anyhow::Error::new(SetupFailure::new( + SetupErrorCode::HelperSidResolveFailed, + format!("resolve sandbox users group SID failed: {err}"), + )) + })?; + + configure_offline_sandbox_network(payload, &offline_sid_str, log)?; + + lock_sandbox_bin_dir(payload, &sandbox_group_sid, log)?; + lock_persistent_sandbox_dirs(payload, &sandbox_group_sid, log)?; + log_note("setup provisioning binary completed", Some(sbx_dir)); + Ok(()) +} + fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Result<()> { let refresh_only = payload.refresh_only; if !refresh_only { - let provision_result = provision_sandbox_users( - &payload.codex_home, - &payload.offline_username, - &payload.online_username, - &payload.proxy_ports, - payload.allow_local_binding, - log, - ); - if let Err(err) = provision_result { - if extract_setup_failure(&err).is_some() { - return Err(err); - } - return Err(anyhow::Error::new(SetupFailure::new( - SetupErrorCode::HelperUserProvisionFailed, - format!("provision sandbox users failed: {err}"), - ))); - } - let users = vec![ - payload.offline_username.clone(), - payload.online_username.clone(), - ]; - hide_newly_created_users(&users, sbx_dir); + provision_and_hide_sandbox_users(payload, log, sbx_dir)?; } let offline_sid = resolve_sid(&payload.offline_username).map_err(|err| { anyhow::Error::new(SetupFailure::new( @@ -597,39 +750,7 @@ fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Res let mut refresh_errors: Vec = Vec::new(); if !refresh_only { - let proxy_allowlist_result = firewall::ensure_offline_proxy_allowlist( - &offline_sid_str, - &payload.proxy_ports, - payload.allow_local_binding, - log, - ); - if let Err(err) = proxy_allowlist_result { - if extract_setup_failure(&err).is_some() { - return Err(err); - } - return Err(anyhow::Error::new(SetupFailure::new( - SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, - format!("ensure offline proxy allowlist failed: {err}"), - ))); - } - let firewall_result = firewall::ensure_offline_outbound_block(&offline_sid_str, log); - if let Err(err) = firewall_result { - if extract_setup_failure(&err).is_some() { - return Err(err); - } - return Err(anyhow::Error::new(SetupFailure::new( - SetupErrorCode::HelperFirewallRuleCreateOrAddFailed, - format!("ensure offline outbound block failed: {err}"), - ))); - } - install_wfp_filters( - &payload.codex_home, - &payload.offline_username, - payload.otel.as_ref(), - |message| { - let _ = log_line(log, message); - }, - ); + configure_offline_sandbox_network(payload, &offline_sid_str, log)?; } // Deny-read ACEs must be present before the sandboxed command starts. Apply @@ -865,24 +986,7 @@ fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Res } } - lock_sandbox_dir( - &sandbox_bin_dir(&payload.codex_home), - &payload.real_user, - &sandbox_group_sid, - GRANT_ACCESS, - FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, - FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, - log, - ) - .map_err(|err| { - anyhow::Error::new(SetupFailure::new( - SetupErrorCode::HelperSandboxLockFailed, - format!( - "lock sandbox bin dir {} failed: {err}", - sandbox_bin_dir(&payload.codex_home).display() - ), - )) - })?; + lock_sandbox_bin_dir(payload, &sandbox_group_sid, log)?; if refresh_only { log_line( @@ -895,46 +999,7 @@ fn run_setup_full(payload: &Payload, log: &mut dyn Write, sbx_dir: &Path) -> Res )?; } if !refresh_only { - lock_sandbox_dir( - &sandbox_dir(&payload.codex_home), - &payload.real_user, - &sandbox_group_sid, - GRANT_ACCESS, - FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, - FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE, - log, - ) - .map_err(|err| { - anyhow::Error::new(SetupFailure::new( - SetupErrorCode::HelperSandboxLockFailed, - format!( - "lock sandbox dir {} failed: {err}", - sandbox_dir(&payload.codex_home).display() - ), - )) - })?; - lock_sandbox_dir( - &sandbox_secrets_dir(&payload.codex_home), - &payload.real_user, - &sandbox_group_sid, - DENY_ACCESS, - FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE, - FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE, - log, - ) - .map_err(|err| { - anyhow::Error::new(SetupFailure::new( - SetupErrorCode::HelperSandboxLockFailed, - format!( - "lock sandbox secrets dir {} failed: {err}", - sandbox_secrets_dir(&payload.codex_home).display() - ), - )) - })?; - let legacy_users = sandbox_dir(&payload.codex_home).join("sandbox_users.json"); - if legacy_users.exists() { - let _ = std::fs::remove_file(&legacy_users); - } + lock_persistent_sandbox_dirs(payload, &sandbox_group_sid, log)?; } unsafe { @@ -986,6 +1051,15 @@ mod tests { assert_eq!(payload.otel, None); } + #[test] + fn payload_accepts_provision_only_mode() { + let mut payload = payload_json(); + payload["mode"] = json!("provision-only"); + let payload: Payload = serde_json::from_value(payload).expect("payload"); + + assert_eq!(payload.mode, super::SetupMode::ProvisionOnly); + } + #[test] fn payload_accepts_otel_settings() { let mut payload = payload_json(); diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 26e22dde070..136cd396074 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -211,6 +211,8 @@ pub use setup::SandboxSetupRequest; #[cfg(target_os = "windows")] pub use setup::SetupRootOverrides; #[cfg(target_os = "windows")] +pub use setup::run_elevated_provisioning_setup; +#[cfg(target_os = "windows")] pub use setup::run_elevated_setup; #[cfg(target_os = "windows")] pub use setup::run_setup_refresh; diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs index cc600b85ac4..cbcec94a4f4 100644 --- a/codex-rs/windows-sandbox-rs/src/setup.rs +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -198,6 +198,7 @@ fn run_setup_refresh_inner( allow_local_binding: offline_proxy_settings.allow_local_binding, otel: None, real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), + mode: SetupMode::Full, refresh_only: true, }; let json = serde_json::to_vec(&payload)?; @@ -491,10 +492,18 @@ struct ElevationPayload { allow_local_binding: bool, otel: Option, real_user: String, + mode: SetupMode, #[serde(default)] refresh_only: bool, } +#[derive(Clone, Copy, Serialize)] +#[serde(rename_all = "kebab-case")] +enum SetupMode { + Full, + ProvisionOnly, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct OfflineProxySettings { pub proxy_ports: Vec, @@ -803,6 +812,7 @@ pub fn run_elevated_setup( allow_local_binding: offline_proxy_settings.allow_local_binding, real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()), otel: codex_otel::global_statsig_metrics_settings(), + mode: SetupMode::Full, refresh_only: false, }; let needs_elevation = !is_elevated().map_err(|err| { @@ -814,6 +824,45 @@ pub fn run_elevated_setup( run_setup_exe(&payload, needs_elevation, request.codex_home) } +pub fn run_elevated_provisioning_setup(codex_home: &Path, real_user: &str) -> Result<()> { + let sbx_dir = sandbox_dir(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()), + ) + })?; + if !is_elevated().map_err(|err| { + failure( + SetupErrorCode::OrchestratorElevationCheckFailed, + format!("failed to determine elevation state: {err}"), + ) + })? { + return Err(failure( + SetupErrorCode::OrchestratorElevationRequired, + "sandbox provisioning setup must be run from an elevated process", + )); + } + let payload = ElevationPayload { + version: SETUP_VERSION, + offline_username: OFFLINE_USERNAME.to_string(), + online_username: ONLINE_USERNAME.to_string(), + codex_home: codex_home.to_path_buf(), + command_cwd: codex_home.to_path_buf(), + read_roots: Vec::new(), + write_roots: Vec::new(), + deny_read_paths: Vec::new(), + deny_write_paths: Vec::new(), + proxy_ports: Vec::new(), + allow_local_binding: false, + otel: codex_otel::global_statsig_metrics_settings(), + real_user: real_user.to_string(), + mode: SetupMode::ProvisionOnly, + refresh_only: false, + }; + run_setup_exe(&payload, /*needs_elevation*/ false, codex_home) +} + fn build_payload_roots( request: &SandboxSetupRequest<'_>, overrides: &SetupRootOverrides, diff --git a/codex-rs/windows-sandbox-rs/src/setup_error.rs b/codex-rs/windows-sandbox-rs/src/setup_error.rs index efaaa531e24..4d2a9d57dfd 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_error.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_error.rs @@ -19,6 +19,8 @@ pub enum SetupErrorCode { OrchestratorSandboxDirCreateFailed, /// Failed to determine whether the current process is elevated. OrchestratorElevationCheckFailed, + /// The setup command requires an already elevated process. + OrchestratorElevationRequired, /// Failed to serialize the elevation payload before launching the helper. OrchestratorPayloadSerializeFailed, /// Failed to launch the setup helper process (spawn or ShellExecuteExW). @@ -75,6 +77,7 @@ impl SetupErrorCode { match self { Self::OrchestratorSandboxDirCreateFailed => "orchestrator_sandbox_dir_create_failed", Self::OrchestratorElevationCheckFailed => "orchestrator_elevation_check_failed", + Self::OrchestratorElevationRequired => "orchestrator_elevation_required", Self::OrchestratorPayloadSerializeFailed => "orchestrator_payload_serialize_failed", Self::OrchestratorHelperLaunchFailed => "orchestrator_helper_launch_failed", Self::OrchestratorHelperLaunchCanceled => "orchestrator_helper_launch_canceled",