Add Windows sandbox provisioning setup command

This commit is contained in:
David Wiesen
2026-05-27 15:55:05 -07:00
parent 2e0c4f4977
commit 7e49fc51e7
7 changed files with 468 additions and 113 deletions

View File

@@ -50,6 +50,7 @@ mod marketplace_cmd;
mod mcp_cmd;
mod plugin_cmd;
mod remote_control_cmd;
mod sandbox_setup;
mod state_db_recovery;
#[cfg(not(windows))]
mod wsl_paths;
@@ -1250,6 +1251,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(),

View File

@@ -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<String>,
/// 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<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SandboxSetupLevel {
Elevated,
}
impl SandboxSetupCommand {
fn setup_level(&self) -> anyhow::Result<SandboxSetupLevel> {
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<Option<SandboxSetupCommand>> {
if !sandbox_command
.first()
.is_some_and(|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<SandboxSetupIdentity> {
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());
}
}

View File

@@ -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,

View File

@@ -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<String> = 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();

View File

@@ -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;

View File

@@ -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<codex_otel::StatsigMetricsSettings>,
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<u16>,
@@ -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,

View File

@@ -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",