Files
codex/codex-rs/windows-sandbox-rs/src/setup_error.rs
T
iceweasel-oaiandGitHub cb9178e8b3 Add Windows sandbox provisioning setup command (#24831)
## Why

Some Windows users do not have local admin access, so they cannot
complete the elevated portion of the Windows sandbox setup when Codex
first needs it. This adds an alpha provisioning path that an admin or IT
deployment script can run ahead of time for the Codex user.

The intended managed-deployment shape is:

```powershell
codex sandbox setup --elevated --user "$env:COMPUTERNAME\Alice" --codex-home "C:\Users\Alice\.codex"
```

`--elevated` is treated as the requested sandbox setup level, not as
proof that the process is elevated. The Windows sandbox setup
orchestration still checks that the caller is actually elevated before
launching the helper without a UAC prompt.

## What changed

- Added `codex sandbox setup --elevated` with explicit user selection
via either `--current-user` or `--user ... --codex-home ...`.
- Moved the CLI implementation into `cli/src/sandbox_setup.rs` instead
of growing `cli/src/main.rs`.
- Added a Windows sandbox `ProvisionOnly` helper mode that runs the
elevation-required provisioning work without requiring a workspace cwd
or runtime sandbox policy.
- Reused the existing elevated helper path for creating/updating sandbox
users, configuring firewall/WFP rules, and applying sandbox directory
ACLs.
- Persisted `windows.sandbox = "elevated"` into the target `CODEX_HOME`
so the desktop app does not show the initial sandbox setup banner after
pre-provisioning succeeds.

## Validation

- `cargo fmt -p codex-windows-sandbox -p codex-core -p codex-cli`
- `cargo test -p codex-cli sandbox_setup --target-dir
target\sandbox-setup-check`
- `cargo test -p codex-windows-sandbox
payload_accepts_provision_only_mode --target-dir
target\sandbox-setup-check`
- `git diff --check`
- Manual Windows alpha flow with a standard local user (`Mandi Lavida`):
ran the new setup command from an admin shell, verified the target
`.codex` contents, sandbox marker/secrets, ACLs, firewall rules, and
desktop startup without the sandbox setup banner once experimental
network proxy requirements were disabled.

## Notes

This intentionally does not solve later elevated update coordination for
IT-managed deployments. The setup command can still apply provisioning
updates when run again, but a broader coordination/process story is out
of scope for this alpha.
2026-05-29 11:01:44 -07:00

287 lines
11 KiB
Rust

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,
/// 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).
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 detected that local firewall policy changes will not fully take effect.
HelperFirewallPolicyIneffective,
/// 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::OrchestratorElevationRequired => "orchestrator_elevation_required",
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::HelperFirewallPolicyIneffective => "helper_firewall_policy_ineffective",
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<String>) -> 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<String>) -> anyhow::Error {
anyhow::Error::new(SetupFailure::new(code, message))
}
pub fn extract_failure(err: &anyhow::Error) -> Option<&SetupFailure> {
err.downcast_ref::<SetupFailure>()
}
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<Option<SetupErrorReport>> {
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::<SetupErrorReport>(&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<String> = 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<String> = Vec::new();
let mut separators: Vec<char> = 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 = "<user>".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\\<user>\\file.txt; fallback D:\\Profiles\\<user>\\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\\<user>\\a and C:\\Users\\<user>\\b");
}
}