Add Windows sandbox wrapper entrypoint

This commit is contained in:
David Wiesen
2026-06-11 12:55:24 -07:00
parent 132590db69
commit b295c376dd
6 changed files with 292 additions and 5 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2202,6 +2202,7 @@ dependencies = [
"codex-shell-escalation",
"codex-utils-absolute-path",
"codex-utils-home-dir",
"codex-windows-sandbox",
"dotenvy",
"pretty_assertions",
"tempfile",

View File

@@ -26,5 +26,8 @@ dotenvy = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
[target.'cfg(windows)'.dependencies]
codex-windows-sandbox = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }

View File

@@ -9,6 +9,8 @@ use codex_exec_server::CODEX_FS_HELPER_ARG1;
use codex_install_context::InstallContext;
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
use codex_utils_home_dir::find_codex_home;
#[cfg(target_os = "windows")]
use codex_windows_sandbox::CODEX_WINDOWS_SANDBOX_ARG1;
#[cfg(unix)]
use std::os::unix::fs::symlink;
use tempfile::TempDir;
@@ -96,6 +98,10 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
}
let argv1 = args.next().unwrap_or_default();
#[cfg(target_os = "windows")]
if argv1 == CODEX_WINDOWS_SANDBOX_ARG1 {
codex_windows_sandbox::run_windows_sandbox_wrapper_main();
}
if argv1 == CODEX_FS_HELPER_ARG1 {
codex_exec_server::run_fs_helper_main();
}

View File

@@ -96,22 +96,33 @@ pub fn resolve_current_exe_for_launch(codex_home: &Path, fallback_executable: &s
Ok(path) => path,
Err(_) => return PathBuf::from(fallback_executable),
};
resolve_exe_for_launch(&source, codex_home)
}
/// Returns the executable path that should be launched from a Windows sandbox.
///
/// Windows sandbox launch setup may grant access to helper binaries under
/// CODEX_HOME/.sandbox-bin. Callers that already know the intended helper
/// binary should pass it here instead of relying on `current_exe()`, which can
/// name a host process rather than the Codex helper in embedded exec-server
/// scenarios.
pub fn resolve_exe_for_launch(source: &Path, codex_home: &Path) -> PathBuf {
let Some(file_name) = source.file_name() else {
return source;
return source.to_path_buf();
};
let destination = helper_bin_dir(codex_home).join(file_name);
match copy_from_source_if_needed(&source, &destination) {
match copy_from_source_if_needed(source, &destination) {
Ok(_) => destination,
Err(err) => {
let sandbox_log_dir = crate::sandbox_dir(codex_home);
log_note(
&format!(
"helper copy failed for current executable: {err:#}; falling back to legacy path {}",
"helper copy failed for executable: {err:#}; falling back to legacy path {}",
source.display()
),
Some(&sandbox_log_dir),
);
source
source.to_path_buf()
}
}
}

View File

@@ -78,6 +78,8 @@ mod wfp_setup;
mod winutil;
#[cfg(target_os = "windows")]
mod workspace_acl;
#[cfg(target_os = "windows")]
mod wrapper;
mod deny_read_resolver;
@@ -178,6 +180,8 @@ pub use filesystem_overrides::unsupported_windows_restricted_token_sandbox_reaso
#[cfg(target_os = "windows")]
pub use helper_materialization::resolve_current_exe_for_launch;
#[cfg(target_os = "windows")]
pub use helper_materialization::resolve_exe_for_launch;
#[cfg(target_os = "windows")]
pub use hide_users::hide_current_user_profile_dir;
#[cfg(target_os = "windows")]
pub use hide_users::hide_newly_created_users;
@@ -320,6 +324,16 @@ pub use winutil::string_from_sid_bytes;
pub use winutil::to_wide;
#[cfg(target_os = "windows")]
pub use workspace_acl::is_command_cwd_root;
#[cfg(target_os = "windows")]
pub use wrapper::CODEX_WINDOWS_SANDBOX_ARG1;
#[cfg(target_os = "windows")]
pub use wrapper::WindowsSandboxWrapperRequest;
#[cfg(target_os = "windows")]
pub use wrapper::create_windows_sandbox_command_args_for_request_file;
#[cfg(target_os = "windows")]
pub use wrapper::create_windows_sandbox_wrapper_request_for_permission_profile;
#[cfg(target_os = "windows")]
pub use wrapper::run_windows_sandbox_wrapper_main;
#[cfg(not(target_os = "windows"))]
pub use stub::CaptureResult;
@@ -477,13 +491,14 @@ mod windows_impl {
codex_home: &Path,
command: Vec<String>,
cwd: &Path,
mut env_map: HashMap<String, String>,
env_map: HashMap<String, String>,
timeout_ms: Option<u64>,
cancellation: Option<WindowsSandboxCancellationToken>,
additional_deny_read_paths: &[AbsolutePathBuf],
additional_deny_write_paths: &[AbsolutePathBuf],
use_private_desktop: bool,
) -> Result<CaptureResult> {
let mut env_map = env_map;
let additional_deny_read_paths = additional_deny_read_paths
.iter()
.map(AbsolutePathBuf::to_path_buf)

View File

@@ -0,0 +1,251 @@
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;
use serde::Deserialize;
use serde::Serialize;
pub const CODEX_WINDOWS_SANDBOX_ARG1: &str = "--run-as-windows-sandbox";
const REQUEST_FILE_FLAG: &str = "--request-file";
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WindowsSandboxWrapperRequest {
pub codex_home: PathBuf,
pub command_cwd: AbsolutePathBuf,
pub permission_profile: PermissionProfile,
pub windows_sandbox_level: WindowsSandboxLevel,
pub windows_sandbox_private_desktop: bool,
pub command: Vec<String>,
}
pub fn create_windows_sandbox_wrapper_request_for_permission_profile(
command: Vec<String>,
command_cwd: AbsolutePathBuf,
permission_profile: PermissionProfile,
windows_sandbox_level: WindowsSandboxLevel,
windows_sandbox_private_desktop: bool,
codex_home: PathBuf,
) -> WindowsSandboxWrapperRequest {
WindowsSandboxWrapperRequest {
codex_home,
command_cwd,
permission_profile,
windows_sandbox_level,
windows_sandbox_private_desktop,
command,
}
}
pub fn create_windows_sandbox_command_args_for_request_file(request_file: &Path) -> Vec<String> {
vec![
CODEX_WINDOWS_SANDBOX_ARG1.to_string(),
REQUEST_FILE_FLAG.to_string(),
request_file.to_string_lossy().into_owned(),
]
}
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_file = parse_windows_sandbox_wrapper_args(args)?;
let request_json = std::fs::read(&request_file).with_context(|| {
format!(
"failed to read windows sandbox wrapper request {}",
request_file.display()
)
})?;
let _ = std::fs::remove_file(&request_file);
let request: WindowsSandboxWrapperRequest = serde_json::from_slice(&request_json)
.context("failed to parse windows sandbox wrapper request")?;
run_windows_sandbox_wrapper_request(request).await
}
async fn run_windows_sandbox_wrapper_request(request: WindowsSandboxWrapperRequest) -> Result<i32> {
if !request.codex_home.is_absolute() {
bail!(
"windows sandbox wrapper codex_home must be absolute: {}",
request.codex_home.display()
);
}
if request.command.is_empty() {
bail!("missing sandboxed command in windows sandbox wrapper request");
}
let env = std::env::vars().collect::<HashMap<_, _>>();
let workspace_roots = vec![request.command_cwd.clone()];
let spawned = match request.windows_sandbox_level {
WindowsSandboxLevel::Elevated => {
let overrides = crate::resolve_windows_elevated_filesystem_overrides(
/*windows_sandbox_active*/ true,
&request.permission_profile,
&request.command_cwd,
/*use_windows_elevated_backend*/ true,
)
.map_err(anyhow::Error::msg)?
.unwrap_or_default();
crate::unified_exec::spawn_windows_sandbox_session_elevated_for_permission_profile(
&request.permission_profile,
workspace_roots.as_slice(),
request.codex_home.as_path(),
request.command,
request.command_cwd.as_path(),
env,
/*timeout_ms*/ None,
overrides.read_roots_override.as_deref(),
overrides.read_roots_include_platform_defaults,
overrides.write_roots_override.as_deref(),
&overrides.additional_deny_read_paths,
&overrides.additional_deny_write_paths,
/*tty*/ false,
/*stdin_open*/ true,
request.windows_sandbox_private_desktop,
)
.await
}
WindowsSandboxLevel::RestrictedToken | WindowsSandboxLevel::Disabled => {
let overrides = crate::resolve_windows_restricted_token_filesystem_overrides(
/*windows_sandbox_active*/ true,
&request.permission_profile,
&request.command_cwd,
request.windows_sandbox_level,
)
.map_err(anyhow::Error::msg)?
.unwrap_or_default();
crate::unified_exec::spawn_windows_sandbox_session_legacy(
&request.permission_profile,
workspace_roots.as_slice(),
request.codex_home.as_path(),
request.command,
request.command_cwd.as_path(),
env,
/*timeout_ms*/ None,
&overrides.additional_deny_read_paths,
&overrides.additional_deny_write_paths,
/*tty*/ false,
/*stdin_open*/ true,
request.windows_sandbox_private_desktop,
)
.await
}
}?;
Ok(crate::stdio_bridge::forward_sandbox_session_stdio(spawned).await)
}
fn parse_windows_sandbox_wrapper_args(args: Vec<String>) -> Result<PathBuf> {
let mut args = args.into_iter();
let Some(flag) = args.next() else {
bail!("missing required argument {REQUEST_FILE_FLAG}");
};
if flag != REQUEST_FILE_FLAG {
bail!("expected {REQUEST_FILE_FLAG}, got {flag}");
}
let request_file = PathBuf::from(next_flag_value(&mut args, REQUEST_FILE_FLAG)?);
if !request_file.is_absolute() {
bail!(
"{REQUEST_FILE_FLAG} must be absolute: {}",
request_file.display()
);
}
if let Some(arg) = args.next() {
bail!("unexpected windows sandbox wrapper argument: {arg}");
}
Ok(request_file)
}
fn next_flag_value(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<String> {
args.next()
.ok_or_else(|| anyhow!("missing value for {flag}"))
}
#[cfg(test)]
mod tests {
use codex_protocol::permissions::NetworkSandboxPolicy;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn windows_wrapper_args_reference_request_file_only() -> Result<()> {
let args = create_windows_sandbox_command_args_for_request_file(Path::new(
r"C:\codex-home\.sandbox\request.json",
));
assert_eq!(
args,
vec![
CODEX_WINDOWS_SANDBOX_ARG1.to_string(),
REQUEST_FILE_FLAG.to_string(),
r"C:\codex-home\.sandbox\request.json".to_string(),
]
);
assert_eq!(
parse_windows_sandbox_wrapper_args(args.into_iter().skip(1).collect())?,
PathBuf::from(r"C:\codex-home\.sandbox\request.json")
);
Ok(())
}
#[test]
fn windows_wrapper_request_round_trips() -> Result<()> {
let permission_profile = PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
};
let request = create_windows_sandbox_wrapper_request_for_permission_profile(
vec![
"helper.exe".to_string(),
"--codex-run-as-fs-helper".to_string(),
],
AbsolutePathBuf::from_absolute_path(Path::new(r"C:\work"))?,
permission_profile.clone(),
WindowsSandboxLevel::RestrictedToken,
/*windows_sandbox_private_desktop*/ true,
PathBuf::from(r"C:\codex-home"),
);
let request: WindowsSandboxWrapperRequest =
serde_json::from_slice(&serde_json::to_vec(&request)?)?;
assert_eq!(
request.command,
vec![
"helper.exe".to_string(),
"--codex-run-as-fs-helper".to_string()
]
);
assert_eq!(request.permission_profile, permission_profile);
assert_eq!(
request.windows_sandbox_level,
WindowsSandboxLevel::RestrictedToken
);
assert_eq!(request.windows_sandbox_private_desktop, true);
Ok(())
}
}