mirror of
https://github.com/openai/codex.git
synced 2026-06-02 19:31:59 +00:00
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.
This commit is contained in:
@@ -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(),
|
||||
|
||||
206
codex-rs/cli/src/sandbox_setup.rs
Normal file
206
codex-rs/cli/src/sandbox_setup.rs
Normal 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_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<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());
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,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,
|
||||
@@ -180,6 +185,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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -241,6 +241,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;
|
||||
|
||||
@@ -203,6 +203,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)?;
|
||||
@@ -496,10 +497,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>,
|
||||
@@ -808,6 +817,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| {
|
||||
@@ -819,6 +829,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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user