Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
206 changes: 206 additions & 0 deletions codex-rs/cli/src/sandbox_setup.rs
Original file line number Diff line number Diff line change
@@ -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_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()
Comment on lines +85 to +87
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear legacy sandbox keys when provisioning

If the target CODEX_HOME already contains any legacy Windows sandbox feature key (for example from a previous opt-in/opt-out), resolve_windows_sandbox_mode gives those keys precedence or ignores [windows].sandbox, while this command only writes the new key. The normal setup path calls clear_legacy_windows_sandbox_keys() before persisting the mode, so managed provisioning can report success but leave the desktop/TUI still seeing the old legacy setting and prompting or running with the wrong sandbox mode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these users won't have legacy keys. I decided to skip this for simplicity

.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());
}
}
10 changes: 10 additions & 0 deletions codex-rs/core/src/windows_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading
Loading