mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Add Windows sandbox wrapper entrypoint
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2202,6 +2202,7 @@ dependencies = [
|
||||
"codex-shell-escalation",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-home-dir",
|
||||
"codex-windows-sandbox",
|
||||
"dotenvy",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
251
codex-rs/windows-sandbox-rs/src/wrapper.rs
Normal file
251
codex-rs/windows-sandbox-rs/src/wrapper.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user