mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Support Windows sandboxing in the exec server (#34423)
## Why Sandboxed process launch through the exec server was unsupported on Windows. ## What changed - Add a shared native process launcher that selects the Windows sandbox session backend when required and otherwise launches PTY or pipe-based processes. - Pass Windows permission, workspace-root, proxy, filesystem-override, and desktop settings through exec-server process preparation. - Preserve piped stdin and inherited file descriptors across the shared launch path. ## Testing - Verify that a remote Windows sandbox process accepts stdin while blocking a write under a read-only policy. - Enable workspace-root sandbox tests on native Windows while continuing to skip environments that cannot emulate Windows sandbox semantics. GitOrigin-RevId: 6719ccbe87ad0d5a57af56503e3f23f894c60a71
This commit is contained in:
@@ -1004,92 +1004,6 @@ impl UnifiedExecProcessManager {
|
||||
) -> Result<UnifiedExecProcess, UnifiedExecError> {
|
||||
let inherited_fds = spawn_lifecycle.inherited_fds();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken {
|
||||
// TODO(anp): Keep PathUri through the Windows sandbox launch boundary.
|
||||
let native_cwd =
|
||||
request
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.map_err(|_| UnifiedExecError::ForeignPath {
|
||||
path: request.cwd.clone(),
|
||||
})?;
|
||||
let codex_home = crate::config::find_codex_home().map_err(|err| {
|
||||
UnifiedExecError::create_process(format!(
|
||||
"windows sandbox: failed to resolve codex_home: {err}"
|
||||
))
|
||||
})?;
|
||||
let additional_deny_write_paths = request
|
||||
.windows_sandbox_filesystem_overrides
|
||||
.as_ref()
|
||||
.map(|overrides| overrides.additional_deny_write_paths.clone())
|
||||
.unwrap_or_default();
|
||||
let additional_deny_read_paths = request
|
||||
.windows_sandbox_filesystem_overrides
|
||||
.as_ref()
|
||||
.map(|overrides| overrides.additional_deny_read_paths.clone())
|
||||
.unwrap_or_default();
|
||||
let elevated_read_roots_override = request
|
||||
.windows_sandbox_filesystem_overrides
|
||||
.as_ref()
|
||||
.and_then(|overrides| overrides.read_roots_override.clone());
|
||||
let elevated_read_roots_include_platform_defaults = request
|
||||
.windows_sandbox_filesystem_overrides
|
||||
.as_ref()
|
||||
.is_some_and(|overrides| overrides.read_roots_include_platform_defaults);
|
||||
let elevated_write_roots_override = request
|
||||
.windows_sandbox_filesystem_overrides
|
||||
.as_ref()
|
||||
.and_then(|overrides| overrides.write_roots_override.clone());
|
||||
let spawned = match request.windows_sandbox_level {
|
||||
codex_protocol::config_types::WindowsSandboxLevel::Elevated => {
|
||||
codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile(
|
||||
&request.permission_profile,
|
||||
request.windows_sandbox_workspace_roots.as_slice(),
|
||||
codex_home.as_ref(),
|
||||
request.command.clone(),
|
||||
native_cwd.as_path(),
|
||||
request.env.clone(),
|
||||
request.network.is_some(),
|
||||
None,
|
||||
elevated_read_roots_override.as_deref(),
|
||||
elevated_read_roots_include_platform_defaults,
|
||||
elevated_write_roots_override.as_deref(),
|
||||
&additional_deny_read_paths,
|
||||
&additional_deny_write_paths,
|
||||
tty,
|
||||
tty,
|
||||
request.windows_sandbox_private_desktop,
|
||||
)
|
||||
.await
|
||||
}
|
||||
codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken
|
||||
| codex_protocol::config_types::WindowsSandboxLevel::Disabled => {
|
||||
codex_windows_sandbox::spawn_windows_sandbox_session_legacy(
|
||||
&request.permission_profile,
|
||||
request.windows_sandbox_workspace_roots.as_slice(),
|
||||
codex_home.as_ref(),
|
||||
request.command.clone(),
|
||||
native_cwd.as_path(),
|
||||
request.env.clone(),
|
||||
None,
|
||||
&additional_deny_read_paths,
|
||||
&additional_deny_write_paths,
|
||||
tty,
|
||||
tty,
|
||||
request.windows_sandbox_private_desktop,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
spawn_lifecycle.after_spawn();
|
||||
return UnifiedExecProcess::from_spawned(
|
||||
spawned.map_err(|err| UnifiedExecError::create_process(err.to_string()))?,
|
||||
request.sandbox,
|
||||
spawn_lifecycle,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if environment.is_remote() {
|
||||
if !inherited_fds.is_empty() {
|
||||
return Err(UnifiedExecError::create_process(
|
||||
@@ -1114,35 +1028,39 @@ impl UnifiedExecProcessManager {
|
||||
path: request.cwd.clone(),
|
||||
})?;
|
||||
|
||||
let (program, args) = request
|
||||
.command
|
||||
.split_first()
|
||||
.ok_or(UnifiedExecError::MissingCommandLine)?;
|
||||
let spawn_result = if tty {
|
||||
codex_utils_pty::pty::spawn_process_with_inherited_fds(
|
||||
program,
|
||||
args,
|
||||
native_cwd.as_path(),
|
||||
&request.env,
|
||||
&request.arg0,
|
||||
codex_utils_pty::TerminalSize::default(),
|
||||
&inherited_fds,
|
||||
)
|
||||
.await
|
||||
if request.command.is_empty() {
|
||||
return Err(UnifiedExecError::MissingCommandLine);
|
||||
}
|
||||
let windows_sandbox = if request.sandbox
|
||||
== codex_sandboxing::SandboxType::WindowsRestrictedToken
|
||||
{
|
||||
Some(codex_sandboxing::WindowsSandboxSpawnRequest {
|
||||
permission_profile: &request.permission_profile,
|
||||
workspace_roots: &request.windows_sandbox_workspace_roots,
|
||||
windows_sandbox_level: request.windows_sandbox_level,
|
||||
proxy_enforced: request.network.is_some(),
|
||||
proxy_settings_mode: codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
|
||||
filesystem_overrides: request.windows_sandbox_filesystem_overrides.as_ref(),
|
||||
use_private_desktop: request.windows_sandbox_private_desktop,
|
||||
})
|
||||
} else {
|
||||
codex_utils_pty::pipe::spawn_process_no_stdin_with_inherited_fds(
|
||||
program,
|
||||
args,
|
||||
native_cwd.as_path(),
|
||||
&request.env,
|
||||
&request.arg0,
|
||||
&inherited_fds,
|
||||
)
|
||||
.await
|
||||
None
|
||||
};
|
||||
let spawn_result = codex_sandboxing::spawn_process(codex_sandboxing::SpawnRequest {
|
||||
command: &request.command,
|
||||
cwd: native_cwd.as_path(),
|
||||
env: &request.env,
|
||||
arg0: &request.arg0,
|
||||
sandbox: request.sandbox,
|
||||
windows_sandbox,
|
||||
tty,
|
||||
stdin_open: tty,
|
||||
inherited_fds: &inherited_fds,
|
||||
})
|
||||
.await;
|
||||
spawn_lifecycle.after_spawn();
|
||||
let spawned =
|
||||
spawn_result.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||
spawn_lifecycle.after_spawn();
|
||||
UnifiedExecProcess::from_spawned(spawned, request.sandbox, spawn_lifecycle).await
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ use codex_features::Feature;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
#[cfg(windows)]
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::TestTargetOs;
|
||||
use core_test_support::responses::ResponseMock;
|
||||
use core_test_support::responses::ev_apply_patch_custom_tool_call;
|
||||
@@ -15,7 +17,7 @@ use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_target_windows;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_target_os;
|
||||
@@ -36,12 +38,19 @@ fn workspace_roots_profile() -> PermissionProfile {
|
||||
|
||||
async fn workspace_roots_test(server: &MockServer) -> Result<TestCodex> {
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
config.cwd = dunce::canonicalize(config.cwd.as_path())
|
||||
.expect("test workspace should be canonicalizable")
|
||||
.abs();
|
||||
}
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
config.workspace_roots = vec![config.cwd.clone()];
|
||||
config.set_windows_sandbox_enabled(/*value*/ true);
|
||||
});
|
||||
builder.build_with_auto_env(server).await
|
||||
}
|
||||
@@ -60,10 +69,7 @@ fn command_arguments(path: &str, contents: &str) -> Result<String> {
|
||||
TestTargetOs::Linux | TestTargetOs::MacOs => {
|
||||
("bash", format!("printf %s '{contents}' > '{path}'"))
|
||||
}
|
||||
TestTargetOs::Windows => (
|
||||
"powershell",
|
||||
format!("Set-Content -NoNewline -Path '{path}' -Value '{contents}'"),
|
||||
),
|
||||
TestTargetOs::Windows => ("cmd", format!("echo {contents}>{path}")),
|
||||
};
|
||||
Ok(serde_json::to_string(&json!({
|
||||
"cmd": command,
|
||||
@@ -134,9 +140,9 @@ async fn workspace_roots_allow_file_and_command_writes() -> Result<()> {
|
||||
const PATCH_CONTENTS: &str = "workspace root patch access";
|
||||
const COMMAND_CONTENTS: &str = "workspace root command access";
|
||||
|
||||
skip_if_target_windows!(
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
"sandboxed process launch is not supported by the exec-server Windows backend"
|
||||
"Wine does not emulate Windows restricted-token and ACL sandbox semantics"
|
||||
);
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -173,7 +179,10 @@ async fn workspace_roots_allow_file_and_command_writes() -> Result<()> {
|
||||
read_file(&test, &patch_path).await?,
|
||||
format!("{PATCH_CONTENTS}\n")
|
||||
);
|
||||
assert_eq!(read_file(&test, &command_path).await?, COMMAND_CONTENTS);
|
||||
assert_eq!(
|
||||
read_file(&test, &command_path).await?.trim_end(),
|
||||
COMMAND_CONTENTS
|
||||
);
|
||||
|
||||
remove_files(&test, &[&patch_path, &command_path]).await
|
||||
}
|
||||
@@ -183,19 +192,24 @@ async fn workspace_roots_deny_file_and_command_writes_outside_roots() -> Result<
|
||||
const PATCH_CONTENTS: &str = "outside workspace root patch";
|
||||
const COMMAND_CONTENTS: &str = "outside workspace root command";
|
||||
|
||||
skip_if_target_windows!(
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
"sandboxed process launch is not supported by the exec-server Windows backend"
|
||||
"Wine does not emulate Windows restricted-token and ACL sandbox semantics"
|
||||
);
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = workspace_roots_test(&server).await?;
|
||||
let patch_path = outside_workspace_path(&test, "outside-patch.txt")?;
|
||||
let command_path = outside_workspace_path(&test, "outside-command.txt")?;
|
||||
let patch_path_display = patch_path.inferred_native_path_string();
|
||||
let patch_relative_path = format!(
|
||||
"../{}",
|
||||
patch_path
|
||||
.basename()
|
||||
.context("outside patch path should have a file name")?
|
||||
);
|
||||
let command_path_display = command_path.inferred_native_path_string();
|
||||
let patch = format!(
|
||||
"*** Begin Patch\n*** Add File: {patch_path_display}\n+{PATCH_CONTENTS}\n*** End Patch\n"
|
||||
"*** Begin Patch\n*** Add File: {patch_relative_path}\n+{PATCH_CONTENTS}\n*** End Patch\n"
|
||||
);
|
||||
|
||||
let response_mock =
|
||||
@@ -222,8 +236,9 @@ async fn workspace_roots_deny_file_and_command_writes_outside_roots() -> Result<
|
||||
.context("denied command result should be present")?;
|
||||
let command_output = command_output.context("denied command output should be present")?;
|
||||
assert!(
|
||||
command_output.contains(&command_path_display),
|
||||
"denied command output should identify {command_path_display}, got {command_output:?}"
|
||||
command_output.contains("Access is denied")
|
||||
|| command_output.contains(&command_path_display),
|
||||
"outside command should be denied, got {command_output:?}"
|
||||
);
|
||||
assert!(
|
||||
test.fs()
|
||||
|
||||
Reference in New Issue
Block a user