Files
codex/codex-rs/core/src/windows_sandbox.rs
T
iceweasel-oai 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

436 lines
14 KiB
Rust

use crate::config::Config;
use crate::config::edit::ConfigEditsBuilder;
use codex_config::config_toml::ConfigToml;
use codex_config::types::WindowsSandboxModeToml;
use codex_features::Feature;
use codex_features::Features;
use codex_features::FeaturesToml;
use codex_login::default_client::originator;
use codex_otel::sanitize_metric_tag_value;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::Instant;
/// Kill switch for the elevated sandbox NUX on Windows.
///
/// When false, revert to the previous sandbox NUX, which only
/// prompts users to enable the legacy sandbox feature.
pub const ELEVATED_SANDBOX_NUX_ENABLED: bool = true;
pub trait WindowsSandboxLevelExt {
fn from_config(config: &Config) -> WindowsSandboxLevel;
fn from_features(features: &Features) -> WindowsSandboxLevel;
}
impl WindowsSandboxLevelExt for WindowsSandboxLevel {
fn from_config(config: &Config) -> WindowsSandboxLevel {
match config.permissions.windows_sandbox_mode {
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
None => Self::from_features(&config.features),
}
}
fn from_features(features: &Features) -> WindowsSandboxLevel {
if features.enabled(Feature::WindowsSandboxElevated) {
return WindowsSandboxLevel::Elevated;
}
if features.enabled(Feature::WindowsSandbox) {
WindowsSandboxLevel::RestrictedToken
} else {
WindowsSandboxLevel::Disabled
}
}
}
pub fn windows_sandbox_level_from_config(config: &Config) -> WindowsSandboxLevel {
WindowsSandboxLevel::from_config(config)
}
pub fn windows_sandbox_level_from_features(features: &Features) -> WindowsSandboxLevel {
WindowsSandboxLevel::from_features(features)
}
pub fn resolve_windows_sandbox_mode(cfg: &ConfigToml) -> Option<WindowsSandboxModeToml> {
cfg.windows
.as_ref()
.and_then(|windows| windows.sandbox)
.or_else(|| legacy_windows_sandbox_mode(cfg.features.as_ref()))
}
pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml) -> bool {
cfg.windows
.as_ref()
.and_then(|windows| windows.sandbox_private_desktop)
.unwrap_or(true)
}
pub fn legacy_windows_sandbox_mode(
features: Option<&FeaturesToml>,
) -> Option<WindowsSandboxModeToml> {
let entries = features.map(FeaturesToml::entries)?;
legacy_windows_sandbox_mode_from_entries(&entries)
}
pub fn legacy_windows_sandbox_mode_from_entries(
entries: &BTreeMap<String, bool>,
) -> Option<WindowsSandboxModeToml> {
if entries
.get(Feature::WindowsSandboxElevated.key())
.copied()
.unwrap_or(false)
{
return Some(WindowsSandboxModeToml::Elevated);
}
if entries
.get(Feature::WindowsSandbox.key())
.copied()
.unwrap_or(false)
|| entries
.get("enable_experimental_windows_sandbox")
.copied()
.unwrap_or(false)
{
Some(WindowsSandboxModeToml::Unelevated)
} else {
None
}
}
#[cfg(target_os = "windows")]
pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool {
codex_windows_sandbox::sandbox_setup_is_complete(codex_home)
}
#[cfg(not(target_os = "windows"))]
pub fn sandbox_setup_is_complete(_codex_home: &Path) -> bool {
false
}
#[cfg(target_os = "windows")]
pub fn elevated_setup_failure_details(err: &anyhow::Error) -> Option<(String, String)> {
let failure = codex_windows_sandbox::extract_setup_failure(err)?;
let code = failure.code.as_str().to_string();
let message = codex_windows_sandbox::sanitize_setup_metric_tag_value(&failure.message);
Some((code, message))
}
#[cfg(not(target_os = "windows"))]
pub fn elevated_setup_failure_details(_err: &anyhow::Error) -> Option<(String, String)> {
None
}
#[cfg(target_os = "windows")]
pub fn elevated_setup_failure_metric_name(err: &anyhow::Error) -> &'static str {
if codex_windows_sandbox::extract_setup_failure(err).is_some_and(|failure| {
matches!(
failure.code,
codex_windows_sandbox::SetupErrorCode::OrchestratorHelperLaunchCanceled
)
}) {
"codex.windows_sandbox.elevated_setup_canceled"
} else {
"codex.windows_sandbox.elevated_setup_failure"
}
}
#[cfg(not(target_os = "windows"))]
pub fn elevated_setup_failure_metric_name(_err: &anyhow::Error) -> &'static str {
panic!("elevated_setup_failure_metric_name is only supported on Windows")
}
#[cfg(target_os = "windows")]
pub fn run_elevated_setup(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> anyhow::Result<()> {
let permissions =
codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
permission_profile,
workspace_roots,
)?;
codex_windows_sandbox::run_elevated_setup(
codex_windows_sandbox::SandboxSetupRequest {
permissions: &permissions,
command_cwd,
env_map,
codex_home,
proxy_enforced: false,
},
codex_windows_sandbox::SetupRootOverrides::default(),
)
}
#[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,
_workspace_roots: &[AbsolutePathBuf],
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
) -> anyhow::Result<()> {
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,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_windows_sandbox_legacy_preflight(
permission_profile,
workspace_roots,
codex_home,
command_cwd,
env_map,
)
}
#[cfg(target_os = "windows")]
pub fn run_setup_refresh_with_extra_read_roots(
permission_profile: &PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
command_cwd: &Path,
env_map: &HashMap<String, String>,
codex_home: &Path,
extra_read_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
codex_windows_sandbox::run_setup_refresh_with_extra_read_roots(
permission_profile,
workspace_roots,
command_cwd,
env_map,
codex_home,
extra_read_roots,
/*proxy_enforced*/ false,
)
}
#[cfg(not(target_os = "windows"))]
pub fn run_legacy_setup_preflight(
_permission_profile: &PermissionProfile,
_workspace_roots: &[AbsolutePathBuf],
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
) -> anyhow::Result<()> {
anyhow::bail!("legacy Windows sandbox setup is only supported on Windows")
}
#[cfg(not(target_os = "windows"))]
pub fn run_setup_refresh_with_extra_read_roots(
_permission_profile: &PermissionProfile,
_workspace_roots: &[AbsolutePathBuf],
_command_cwd: &Path,
_env_map: &HashMap<String, String>,
_codex_home: &Path,
_extra_read_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
anyhow::bail!("Windows sandbox read-root refresh is only supported on Windows")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowsSandboxSetupMode {
Elevated,
Unelevated,
}
#[derive(Debug, Clone)]
pub struct WindowsSandboxSetupRequest {
pub mode: WindowsSandboxSetupMode,
pub permission_profile: PermissionProfile,
pub workspace_roots: Vec<AbsolutePathBuf>,
pub command_cwd: PathBuf,
pub env_map: HashMap<String, String>,
pub codex_home: PathBuf,
}
pub async fn run_windows_sandbox_setup(request: WindowsSandboxSetupRequest) -> anyhow::Result<()> {
let start = Instant::now();
let mode = request.mode;
let originator_tag = sanitize_metric_tag_value(originator().value.as_str());
let result = run_windows_sandbox_setup_and_persist(request).await;
match result {
Ok(()) => {
emit_windows_sandbox_setup_success_metrics(
mode,
originator_tag.as_str(),
start.elapsed(),
);
Ok(())
}
Err(err) => {
emit_windows_sandbox_setup_failure_metrics(
mode,
originator_tag.as_str(),
start.elapsed(),
&err,
);
Err(err)
}
}
}
async fn run_windows_sandbox_setup_and_persist(
request: WindowsSandboxSetupRequest,
) -> anyhow::Result<()> {
let mode = request.mode;
let permission_profile = request.permission_profile;
let workspace_roots = request.workspace_roots;
let command_cwd = request.command_cwd;
let env_map = request.env_map;
let codex_home = request.codex_home;
let setup_codex_home = codex_home.clone();
let setup_result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
match mode {
WindowsSandboxSetupMode::Elevated => {
if !sandbox_setup_is_complete(setup_codex_home.as_path()) {
run_elevated_setup(
&permission_profile,
workspace_roots.as_slice(),
command_cwd.as_path(),
&env_map,
setup_codex_home.as_path(),
)?;
}
}
WindowsSandboxSetupMode::Unelevated => {
run_legacy_setup_preflight(
&permission_profile,
workspace_roots.as_slice(),
command_cwd.as_path(),
&env_map,
setup_codex_home.as_path(),
)?;
}
}
Ok(())
})
.await
.map_err(|join_err| anyhow::anyhow!("windows sandbox setup task failed: {join_err}"))?;
setup_result?;
ConfigEditsBuilder::new(codex_home.as_path())
.set_windows_sandbox_mode(windows_sandbox_setup_mode_tag(mode))
.clear_legacy_windows_sandbox_keys()
.apply()
.await
.map_err(|err| anyhow::anyhow!("failed to persist windows sandbox mode: {err}"))
}
fn emit_windows_sandbox_setup_success_metrics(
mode: WindowsSandboxSetupMode,
originator_tag: &str,
duration: std::time::Duration,
) {
let Some(metrics) = codex_otel::global() else {
return;
};
let mode_tag = windows_sandbox_setup_mode_tag(mode);
let _ = metrics.record_duration(
"codex.windows_sandbox.setup_duration_ms",
duration,
&[
("result", "success"),
("originator", originator_tag),
("mode", mode_tag),
],
);
let _ = metrics.counter(
"codex.windows_sandbox.setup_success",
/*inc*/ 1,
&[("originator", originator_tag), ("mode", mode_tag)],
);
}
fn emit_windows_sandbox_setup_failure_metrics(
mode: WindowsSandboxSetupMode,
originator_tag: &str,
duration: std::time::Duration,
_err: &anyhow::Error,
) {
let Some(metrics) = codex_otel::global() else {
return;
};
let mode_tag = windows_sandbox_setup_mode_tag(mode);
let _ = metrics.record_duration(
"codex.windows_sandbox.setup_duration_ms",
duration,
&[
("result", "failure"),
("originator", originator_tag),
("mode", mode_tag),
],
);
let _ = metrics.counter(
"codex.windows_sandbox.setup_failure",
/*inc*/ 1,
&[("originator", originator_tag), ("mode", mode_tag)],
);
if matches!(mode, WindowsSandboxSetupMode::Elevated) {
#[cfg(target_os = "windows")]
{
let mut failure_tags: Vec<(&str, &str)> = vec![("originator", originator_tag)];
let mut code_tag: Option<String> = None;
let mut message_tag: Option<String> = None;
if let Some((code, message)) = elevated_setup_failure_details(_err) {
code_tag = Some(code);
message_tag = Some(message);
}
if let Some(code) = code_tag.as_deref() {
failure_tags.push(("code", code));
}
if let Some(message) = message_tag.as_deref() {
failure_tags.push(("message", message));
}
let _ = metrics.counter(
elevated_setup_failure_metric_name(_err),
/*inc*/ 1,
&failure_tags,
);
}
} else {
let _ = metrics.counter(
"codex.windows_sandbox.legacy_setup_preflight_failed",
/*inc*/ 1,
&[("originator", originator_tag)],
);
}
}
fn windows_sandbox_setup_mode_tag(mode: WindowsSandboxSetupMode) -> &'static str {
match mode {
WindowsSandboxSetupMode::Elevated => "elevated",
WindowsSandboxSetupMode::Unelevated => "unelevated",
}
}
#[cfg(test)]
#[path = "windows_sandbox_tests.rs"]
mod tests;