mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why Filesystem helpers intentionally run with a minimal environment that excludes proxy variables. After filesystem operations started using the Windows sandbox wrapper, the wrapper derived an empty proxy configuration from that helper environment and compared it with the persistent sandbox setup marker. When the marker contained proxy ports, every filesystem operation appeared to require a firewall update, which could launch elevated setup, show a UAC or loader dialog, and fail operations such as `apply_patch` with error 1223. Filesystem helpers do not use network access, so they should preserve the proxy/firewall state established by normal sandboxed process launches. ## What changed - Add an explicit Windows sandbox proxy-settings mode for reconciling or preserving persistent proxy state. - Use preserve mode for filesystem helpers while normal process launches continue to reconcile proxy settings from their environment. - Carry the selected proxy state consistently through setup validation, elevated setup, and non-elevated ACL refreshes. - Cover wrapper argument propagation and marker-derived proxy preservation. ## Validation - `cargo build -p codex-cli --bin codex` - `just test -p codex-windows-sandbox preserving_proxy_settings_uses_the_existing_marker` - `just test -p codex-windows-sandbox windows_wrapper_args_round_trip` - `just test -p codex-windows-sandbox setup_request_prefers_explicit_proxy_settings` - `just test -p codex-sandboxing transform_for_direct_spawn_windows` - `just test -p codex-exec-server fs_sandbox::tests` - Ran the same sandboxed `fs/writeFile` reproduction against published `0.142.0-alpha.6` and the new CLI. The published CLI launched elevated setup and failed with `ShellExecuteExW ... 1223`; the new CLI completed without elevation. Related to #28359.
333 lines
13 KiB
Rust
333 lines
13 KiB
Rust
//! Internal `codex.exe --run-as-windows-sandbox` wrapper.
|
|
//!
|
|
//! This gives direct-spawn callers an argv-shaped Windows sandbox launcher,
|
|
//! analogous to the macOS seatbelt and Linux sandbox wrapper paths. The wrapper
|
|
//! parses sandbox metadata from argv, launches the requested inner command in a
|
|
//! Windows sandbox session, and forwards stdio to that inner command.
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use anyhow::anyhow;
|
|
use anyhow::bail;
|
|
use codex_protocol::config_types::WindowsSandboxLevel;
|
|
use codex_protocol::models::PermissionProfile;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
|
|
pub const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox";
|
|
|
|
const COMMAND_CWD_FLAG: &str = "--command-cwd";
|
|
const CODEX_HOME_FLAG: &str = "--codex-home";
|
|
const DENY_READ_PATHS_JSON_FLAG: &str = "--deny-read-paths-json";
|
|
const DENY_WRITE_PATHS_JSON_FLAG: &str = "--deny-write-paths-json";
|
|
const ENV_JSON_FLAG: &str = "--env-json";
|
|
const PERMISSION_PROFILE_FLAG: &str = "--permission-profile";
|
|
const PRIVATE_DESKTOP_FLAG: &str = "--windows-sandbox-private-desktop";
|
|
const PRESERVE_PROXY_SETTINGS_FLAG: &str = "--preserve-proxy-settings";
|
|
const PROXY_ENFORCED_FLAG: &str = "--proxy-enforced";
|
|
const READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG: &str = "--read-roots-include-platform-defaults";
|
|
const READ_ROOTS_JSON_FLAG: &str = "--read-roots-json";
|
|
const SANDBOX_LEVEL_FLAG: &str = "--windows-sandbox-level";
|
|
const WRITE_ROOTS_JSON_FLAG: &str = "--write-roots-json";
|
|
const WORKSPACE_ROOT_FLAG: &str = "--workspace-root";
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn create_windows_sandbox_command_args_for_permission_profile(
|
|
command: Vec<String>,
|
|
command_cwd: &AbsolutePathBuf,
|
|
workspace_roots: &[AbsolutePathBuf],
|
|
env_map: &HashMap<String, String>,
|
|
permission_profile: &PermissionProfile,
|
|
windows_sandbox_level: WindowsSandboxLevel,
|
|
windows_sandbox_private_desktop: bool,
|
|
proxy_enforced: bool,
|
|
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
|
|
read_roots_override: Option<&[PathBuf]>,
|
|
read_roots_include_platform_defaults: bool,
|
|
write_roots_override: Option<&[PathBuf]>,
|
|
deny_read_paths_override: &[AbsolutePathBuf],
|
|
deny_write_paths_override: &[AbsolutePathBuf],
|
|
codex_home: &Path,
|
|
) -> Vec<String> {
|
|
let permission_profile_json = serde_json::to_string(permission_profile)
|
|
.unwrap_or_else(|err| panic!("failed to serialize permission profile: {err}"));
|
|
let env_json = serde_json::to_string(env_map)
|
|
.unwrap_or_else(|err| panic!("failed to serialize env: {err}"));
|
|
let mut args = vec![
|
|
CODEX_WINDOWS_SANDBOX_ARG1.to_string(),
|
|
CODEX_HOME_FLAG.to_string(),
|
|
codex_home.to_string_lossy().into_owned(),
|
|
COMMAND_CWD_FLAG.to_string(),
|
|
command_cwd.as_path().to_string_lossy().into_owned(),
|
|
PERMISSION_PROFILE_FLAG.to_string(),
|
|
permission_profile_json,
|
|
ENV_JSON_FLAG.to_string(),
|
|
env_json,
|
|
SANDBOX_LEVEL_FLAG.to_string(),
|
|
windows_sandbox_level.to_string(),
|
|
];
|
|
let workspace_roots = if workspace_roots.is_empty() {
|
|
std::slice::from_ref(command_cwd)
|
|
} else {
|
|
workspace_roots
|
|
};
|
|
for root in workspace_roots {
|
|
args.push(WORKSPACE_ROOT_FLAG.to_string());
|
|
args.push(root.as_path().to_string_lossy().into_owned());
|
|
}
|
|
if windows_sandbox_private_desktop {
|
|
args.push(PRIVATE_DESKTOP_FLAG.to_string());
|
|
}
|
|
if proxy_enforced {
|
|
args.push(PROXY_ENFORCED_FLAG.to_string());
|
|
}
|
|
if proxy_settings_mode == crate::WindowsSandboxProxySettingsMode::Preserve {
|
|
args.push(PRESERVE_PROXY_SETTINGS_FLAG.to_string());
|
|
}
|
|
if let Some(read_roots_override) = read_roots_override {
|
|
push_json_arg(&mut args, READ_ROOTS_JSON_FLAG, &read_roots_override);
|
|
}
|
|
if read_roots_include_platform_defaults {
|
|
args.push(READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG.to_string());
|
|
}
|
|
if let Some(write_roots_override) = write_roots_override {
|
|
push_json_arg(&mut args, WRITE_ROOTS_JSON_FLAG, &write_roots_override);
|
|
}
|
|
if !deny_read_paths_override.is_empty() {
|
|
push_json_arg(
|
|
&mut args,
|
|
DENY_READ_PATHS_JSON_FLAG,
|
|
&deny_read_paths_override,
|
|
);
|
|
}
|
|
if !deny_write_paths_override.is_empty() {
|
|
push_json_arg(
|
|
&mut args,
|
|
DENY_WRITE_PATHS_JSON_FLAG,
|
|
&deny_write_paths_override,
|
|
);
|
|
}
|
|
args.push("--".to_string());
|
|
args.extend(command);
|
|
args
|
|
}
|
|
|
|
fn push_json_arg<T: serde::Serialize>(args: &mut Vec<String>, flag: &str, value: &T) {
|
|
args.push(flag.to_string());
|
|
args.push(
|
|
serde_json::to_string(value)
|
|
.unwrap_or_else(|err| panic!("failed to serialize {flag}: {err}")),
|
|
);
|
|
}
|
|
|
|
pub fn run_windows_sandbox_wrapper_main() -> ! {
|
|
let args = std::env::args().skip(2).collect::<Vec<_>>();
|
|
let runtime = match tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
{
|
|
Ok(runtime) => runtime,
|
|
Err(err) => {
|
|
eprintln!("windows sandbox failed to build runtime: {err}");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
let exit_code = match runtime.block_on(run_windows_sandbox_wrapper_args(args)) {
|
|
Ok(exit_code) => exit_code,
|
|
Err(err) => {
|
|
eprintln!("windows sandbox failed: {err:#}");
|
|
1
|
|
}
|
|
};
|
|
std::process::exit(exit_code);
|
|
}
|
|
|
|
async fn run_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<i32> {
|
|
let request = parse_windows_sandbox_wrapper_args(args)?;
|
|
run_windows_sandbox_wrapper_request(request).await
|
|
}
|
|
|
|
struct WindowsSandboxWrapperRequest {
|
|
codex_home: PathBuf,
|
|
command_cwd: AbsolutePathBuf,
|
|
workspace_roots: Vec<AbsolutePathBuf>,
|
|
env_map: HashMap<String, String>,
|
|
permission_profile: PermissionProfile,
|
|
windows_sandbox_level: WindowsSandboxLevel,
|
|
windows_sandbox_private_desktop: bool,
|
|
proxy_enforced: bool,
|
|
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
|
|
read_roots_override: Option<Vec<PathBuf>>,
|
|
read_roots_include_platform_defaults: bool,
|
|
write_roots_override: Option<Vec<PathBuf>>,
|
|
deny_read_paths_override: Vec<AbsolutePathBuf>,
|
|
deny_write_paths_override: Vec<AbsolutePathBuf>,
|
|
command: Vec<String>,
|
|
}
|
|
|
|
async fn run_windows_sandbox_wrapper_request(request: WindowsSandboxWrapperRequest) -> Result<i32> {
|
|
if request.command.is_empty() {
|
|
bail!("missing sandboxed command in windows sandbox wrapper request");
|
|
}
|
|
let spawned =
|
|
crate::spawn_windows_sandbox_session_for_level(crate::WindowsSandboxSessionRequest {
|
|
permission_profile: &request.permission_profile,
|
|
workspace_roots: request.workspace_roots.as_slice(),
|
|
codex_home: request.codex_home.as_path(),
|
|
command: request.command,
|
|
cwd: request.command_cwd.as_path(),
|
|
env_map: request.env_map,
|
|
windows_sandbox_level: request.windows_sandbox_level,
|
|
proxy_enforced: request.proxy_enforced,
|
|
proxy_settings_mode: request.proxy_settings_mode,
|
|
timeout_ms: None,
|
|
read_roots_override: request.read_roots_override.as_deref(),
|
|
read_roots_include_platform_defaults: request.read_roots_include_platform_defaults,
|
|
write_roots_override: request.write_roots_override.as_deref(),
|
|
deny_read_paths_override: request.deny_read_paths_override.as_slice(),
|
|
deny_write_paths_override: request.deny_write_paths_override.as_slice(),
|
|
tty: false,
|
|
stdin_open: true,
|
|
use_private_desktop: request.windows_sandbox_private_desktop,
|
|
})
|
|
.await?;
|
|
|
|
Ok(crate::forward_sandbox_session_stdio(spawned).await)
|
|
}
|
|
|
|
fn parse_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<WindowsSandboxWrapperRequest> {
|
|
let mut args = args.into_iter();
|
|
let mut codex_home = None;
|
|
let mut command_cwd = None;
|
|
let mut workspace_roots = Vec::new();
|
|
let mut env_map = None;
|
|
let mut permission_profile = None;
|
|
let mut windows_sandbox_level = None;
|
|
let mut windows_sandbox_private_desktop = false;
|
|
let mut proxy_enforced = false;
|
|
let mut proxy_settings_mode = crate::WindowsSandboxProxySettingsMode::Reconcile;
|
|
let mut read_roots_override = None;
|
|
let mut read_roots_include_platform_defaults = false;
|
|
let mut write_roots_override = None;
|
|
let mut deny_read_paths_override = Vec::new();
|
|
let mut deny_write_paths_override = Vec::new();
|
|
let mut command = None;
|
|
|
|
while let Some(arg) = args.next() {
|
|
match arg.as_str() {
|
|
CODEX_HOME_FLAG => codex_home = Some(PathBuf::from(next_flag_value(&mut args, &arg)?)),
|
|
COMMAND_CWD_FLAG => {
|
|
command_cwd = Some(absolute_path_arg(next_flag_value(&mut args, &arg)?, &arg)?);
|
|
}
|
|
WORKSPACE_ROOT_FLAG => {
|
|
workspace_roots.push(absolute_path_arg(next_flag_value(&mut args, &arg)?, &arg)?);
|
|
}
|
|
ENV_JSON_FLAG => {
|
|
let value = next_flag_value(&mut args, &arg)?;
|
|
env_map = Some(serde_json::from_str(&value).context("failed to parse env json")?);
|
|
}
|
|
DENY_READ_PATHS_JSON_FLAG => {
|
|
deny_read_paths_override =
|
|
json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?;
|
|
}
|
|
DENY_WRITE_PATHS_JSON_FLAG => {
|
|
deny_write_paths_override =
|
|
json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?;
|
|
}
|
|
PERMISSION_PROFILE_FLAG => {
|
|
let value = next_flag_value(&mut args, &arg)?;
|
|
permission_profile = Some(
|
|
serde_json::from_str(&value).context("failed to parse permission profile")?,
|
|
);
|
|
}
|
|
SANDBOX_LEVEL_FLAG => {
|
|
let value = next_flag_value(&mut args, &arg)?;
|
|
windows_sandbox_level = Some(parse_windows_sandbox_level(&value)?);
|
|
}
|
|
PRIVATE_DESKTOP_FLAG => windows_sandbox_private_desktop = true,
|
|
PRESERVE_PROXY_SETTINGS_FLAG => {
|
|
proxy_settings_mode = crate::WindowsSandboxProxySettingsMode::Preserve;
|
|
}
|
|
PROXY_ENFORCED_FLAG => proxy_enforced = true,
|
|
READ_ROOTS_INCLUDE_PLATFORM_DEFAULTS_FLAG => {
|
|
read_roots_include_platform_defaults = true;
|
|
}
|
|
READ_ROOTS_JSON_FLAG => {
|
|
read_roots_override =
|
|
Some(json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?);
|
|
}
|
|
WRITE_ROOTS_JSON_FLAG => {
|
|
write_roots_override =
|
|
Some(json_flag_value(next_flag_value(&mut args, &arg)?, &arg)?);
|
|
}
|
|
"--" => {
|
|
command = Some(args.collect::<Vec<_>>());
|
|
break;
|
|
}
|
|
_ => bail!("unexpected windows sandbox wrapper argument: {arg}"),
|
|
}
|
|
}
|
|
|
|
let codex_home = codex_home.ok_or_else(|| anyhow!("missing required {CODEX_HOME_FLAG}"))?;
|
|
if !codex_home.is_absolute() {
|
|
bail!(
|
|
"{CODEX_HOME_FLAG} must be absolute: {}",
|
|
codex_home.display()
|
|
);
|
|
}
|
|
let command_cwd = command_cwd.ok_or_else(|| anyhow!("missing required {COMMAND_CWD_FLAG}"))?;
|
|
if workspace_roots.is_empty() {
|
|
workspace_roots.push(command_cwd.clone());
|
|
}
|
|
Ok(WindowsSandboxWrapperRequest {
|
|
codex_home,
|
|
command_cwd,
|
|
workspace_roots,
|
|
env_map: env_map.ok_or_else(|| anyhow!("missing required {ENV_JSON_FLAG}"))?,
|
|
permission_profile: permission_profile
|
|
.ok_or_else(|| anyhow!("missing required {PERMISSION_PROFILE_FLAG}"))?,
|
|
windows_sandbox_level: windows_sandbox_level
|
|
.ok_or_else(|| anyhow!("missing required {SANDBOX_LEVEL_FLAG}"))?,
|
|
windows_sandbox_private_desktop,
|
|
proxy_enforced,
|
|
proxy_settings_mode,
|
|
read_roots_override,
|
|
read_roots_include_platform_defaults,
|
|
write_roots_override,
|
|
deny_read_paths_override,
|
|
deny_write_paths_override,
|
|
command: command.ok_or_else(|| anyhow!("missing sandboxed command separator --"))?,
|
|
})
|
|
}
|
|
|
|
fn next_flag_value(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<String> {
|
|
args.next()
|
|
.ok_or_else(|| anyhow!("missing value for {flag}"))
|
|
}
|
|
|
|
fn absolute_path_arg(value: String, flag: &str) -> Result<AbsolutePathBuf> {
|
|
let path = PathBuf::from(value);
|
|
AbsolutePathBuf::from_absolute_path(path.as_path())
|
|
.with_context(|| format!("{flag} must be absolute: {}", path.display()))
|
|
}
|
|
|
|
fn json_flag_value<T: serde::de::DeserializeOwned>(value: String, flag: &str) -> Result<T> {
|
|
serde_json::from_str(&value).with_context(|| format!("failed to parse {flag}"))
|
|
}
|
|
|
|
fn parse_windows_sandbox_level(value: &str) -> Result<WindowsSandboxLevel> {
|
|
match value {
|
|
"disabled" => Ok(WindowsSandboxLevel::Disabled),
|
|
"restricted-token" => Ok(WindowsSandboxLevel::RestrictedToken),
|
|
"elevated" => Ok(WindowsSandboxLevel::Elevated),
|
|
_ => bail!("invalid windows sandbox level: {value}"),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "wrapper_tests.rs"]
|
|
mod tests;
|