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:
iceweasel-oai
2026-07-20 22:35:14 +00:00
committed by copyberry
parent 60272096bc
commit 35c2278dd5
16 changed files with 494 additions and 254 deletions
+1
View File
@@ -3870,6 +3870,7 @@ dependencies = [
"codex-utils-absolute-path",
"codex-utils-home-dir",
"codex-utils-path-uri",
"codex-utils-pty",
"codex-windows-sandbox",
"dunce",
"libc",
+12 -3
View File
@@ -275,13 +275,22 @@ impl CommandExecManager {
&env,
&arg0,
size.unwrap_or_default(),
&[],
)
.await
} else if stream_stdin {
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0).await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(program, args, cwd.as_path(), &env, &arg0)
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0, &[])
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
program,
args,
cwd.as_path(),
&env,
&arg0,
&[],
)
.await
};
let spawned = match spawned {
Ok(spawned) => spawned,
@@ -312,13 +312,22 @@ impl ProcessExecManager {
&env,
&arg0,
size.unwrap_or_default(),
&[],
)
.await
} else if stream_stdin {
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0).await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(program, args, cwd.as_path(), &env, &arg0)
codex_utils_pty::spawn_pipe_process(program, args, cwd.as_path(), &env, &arg0, &[])
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
program,
args,
cwd.as_path(),
&env,
&arg0,
&[],
)
.await
};
let spawned = match spawned {
Ok(spawned) => spawned,
+29 -111
View File
@@ -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
}
+29 -14
View File
@@ -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()
+15 -34
View File
@@ -18,7 +18,6 @@ use codex_sandboxing::SandboxType;
use codex_sandboxing::is_likely_sandbox_denied;
use codex_utils_pty::ExecCommandSession;
use codex_utils_pty::ProcessSignal as PtyProcessSignal;
use codex_utils_pty::TerminalSize;
use tokio::sync::Mutex;
use tokio::sync::Notify;
use tokio::sync::mpsc;
@@ -241,10 +240,9 @@ impl LocalProcess {
let process_id = params.process_id.clone();
let prepared =
prepare_exec_request(&params, child_env(&params), self.runtime_paths.as_ref()).await?;
let (program, args) = prepared
.command
.split_first()
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
if prepared.command.is_empty() {
return Err(invalid_params("argv must not be empty".to_string()));
}
let start = Arc::new(ProcessStart);
{
@@ -260,35 +258,18 @@ impl LocalProcess {
);
}
let spawned_result = if params.tty {
codex_utils_pty::spawn_pty_process(
program,
args,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
TerminalSize::default(),
)
.await
} else if params.pipe_stdin {
codex_utils_pty::spawn_pipe_process(
program,
args,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
)
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
program,
args,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
)
.await
};
let spawned_result = codex_sandboxing::spawn_process(codex_sandboxing::SpawnRequest {
command: &prepared.command,
cwd: prepared.cwd.as_path(),
env: &prepared.env,
arg0: &prepared.arg0,
sandbox: prepared.sandbox,
windows_sandbox: prepared.windows_sandbox_spawn_request(),
tty: params.tty,
stdin_open: params.tty || params.pipe_stdin,
inherited_fds: &[],
})
.await;
let spawned = match spawned_result {
Ok(spawned) => spawned,
Err(err) => {
+104 -41
View File
@@ -16,6 +16,12 @@ use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
use codex_sandboxing::WindowsSandboxFilesystemOverrides;
use codex_sandboxing::WindowsSandboxProxySettingsMode;
use codex_sandboxing::WindowsSandboxSpawnRequest;
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
use codex_sandboxing::windows_sandbox_uses_elevated_backend;
use codex_sandboxing::with_managed_mitm_ca_readable_root;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
@@ -32,6 +38,32 @@ pub(crate) struct PreparedExecRequest {
pub(crate) arg0: Option<String>,
pub(crate) sandbox: SandboxType,
pub(crate) network_proxy_handle: Option<NetworkProxyHandle>,
windows_sandbox: Option<PreparedWindowsSandboxRequest>,
}
struct PreparedWindowsSandboxRequest {
permission_profile: PermissionProfile,
workspace_roots: Vec<AbsolutePathBuf>,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
proxy_enforced: bool,
filesystem_overrides: Option<WindowsSandboxFilesystemOverrides>,
use_private_desktop: bool,
}
impl PreparedExecRequest {
pub(crate) fn windows_sandbox_spawn_request(&self) -> Option<WindowsSandboxSpawnRequest<'_>> {
self.windows_sandbox
.as_ref()
.map(|request| WindowsSandboxSpawnRequest {
permission_profile: &request.permission_profile,
workspace_roots: &request.workspace_roots,
windows_sandbox_level: request.windows_sandbox_level,
proxy_enforced: request.proxy_enforced,
proxy_settings_mode: WindowsSandboxProxySettingsMode::Reconcile,
filesystem_overrides: request.filesystem_overrides.as_ref(),
use_private_desktop: request.use_private_desktop,
})
}
}
pub(crate) async fn prepare_exec_request(
@@ -53,6 +85,7 @@ pub(crate) async fn prepare_exec_request(
arg0: params.arg0.clone(),
sandbox: SandboxType::None,
network_proxy_handle,
windows_sandbox: None,
});
};
let runtime_paths = runtime_paths
@@ -112,54 +145,83 @@ pub(crate) async fn prepare_exec_request(
sandbox_context.windows_sandbox_level,
params.enforce_managed_network,
);
match sandbox {
SandboxType::None => {
return Err(invalid_params(
"sandbox intent cannot be enforced on this executor".to_string(),
));
}
SandboxType::WindowsRestrictedToken => {
// TODO(jif): Launch generic remote commands through the Windows sandbox session API
// while preserving argv and TTY behavior and passing the child environment out of band.
return Err(invalid_params(
"sandboxed remote process launch is not supported on Windows".to_string(),
));
}
SandboxType::MacosSeatbelt | SandboxType::LinuxSeccomp => {}
if sandbox == SandboxType::None {
return Err(invalid_params(
"sandbox intent cannot be enforced on this executor".to_string(),
));
}
let (program, args) = params
.argv
.split_first()
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
let request = sandbox_manager
.transform_for_direct_spawn(SandboxDirectSpawnTransformRequest {
workspace_roots,
windows_sandbox_proxy_settings_mode:
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
transform: SandboxTransformRequest {
// TODO(jif): Preserve params.arg0 for the inner command across the sandbox
// wrapper, or reject sandboxed requests with a custom arg0.
command: SandboxCommand {
program: program.into(),
args: args.to_vec(),
cwd: params.cwd.clone(),
env,
managed_network,
additional_permissions: None,
},
permissions: &permissions,
sandbox,
enforce_managed_network: params.enforce_managed_network,
environment_id: None,
network: None,
sandbox_policy_cwd,
codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
let transform_request = SandboxDirectSpawnTransformRequest {
workspace_roots,
windows_sandbox_proxy_settings_mode:
codex_sandboxing::WindowsSandboxProxySettingsMode::Reconcile,
transform: SandboxTransformRequest {
// TODO(jif): Preserve params.arg0 for the inner command across the sandbox
// wrapper, or reject sandboxed requests with a custom arg0.
command: SandboxCommand {
program: program.into(),
args: args.to_vec(),
cwd: params.cwd.clone(),
env,
managed_network,
additional_permissions: None,
},
})
permissions: &permissions,
sandbox,
enforce_managed_network: params.enforce_managed_network,
environment_id: None,
network: None,
sandbox_policy_cwd,
codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
},
};
let mut request = if sandbox == SandboxType::WindowsRestrictedToken {
// The shared launcher invokes the native Windows session spawner directly.
sandbox_manager.transform(transform_request.transform)
} else {
sandbox_manager.transform_for_direct_spawn(transform_request)
}
.map_err(|err| invalid_params(format!("failed to prepare process sandbox: {err}")))?;
let windows_sandbox = if sandbox == SandboxType::WindowsRestrictedToken {
request.arg0 = params.arg0.clone();
let proxy_enforced = params.enforce_managed_network;
let use_elevated = windows_sandbox_uses_elevated_backend(
sandbox_context.windows_sandbox_level,
proxy_enforced,
);
let filesystem_overrides = if use_elevated {
resolve_windows_elevated_filesystem_overrides(
sandbox,
&permissions,
&native_sandbox_policy_cwd,
use_elevated,
)
} else {
resolve_windows_restricted_token_filesystem_overrides(
sandbox,
&permissions,
&native_sandbox_policy_cwd,
sandbox_context.windows_sandbox_level,
)
}
.map_err(|err| invalid_params(format!("failed to prepare process sandbox: {err}")))?;
Some(PreparedWindowsSandboxRequest {
permission_profile: permissions,
workspace_roots: native_workspace_roots,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
proxy_enforced,
filesystem_overrides,
use_private_desktop: sandbox_context.windows_sandbox_private_desktop,
})
} else {
None
};
Ok(PreparedExecRequest {
command: request.command,
cwd: native_path(&request.cwd, "cwd")?,
@@ -167,6 +229,7 @@ pub(crate) async fn prepare_exec_request(
arg0: request.arg0,
sandbox: request.sandbox,
network_proxy_handle,
windows_sandbox,
})
}
@@ -7,7 +7,9 @@ use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
#[cfg(unix)]
#[cfg(windows)]
use codex_protocol::config_types::WindowsSandboxLevel;
#[cfg(any(unix, windows))]
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
@@ -18,9 +20,9 @@ use tokio::time::timeout;
use super::prepare_exec_request;
use crate::ExecParams;
#[cfg(unix)]
#[cfg(any(unix, windows))]
use crate::ExecServerRuntimePaths;
#[cfg(unix)]
#[cfg(any(unix, windows))]
use crate::FileSystemSandboxContext;
use crate::ProcessId;
@@ -272,3 +274,50 @@ async fn disabled_remote_proxy_config_is_rejected_before_exporting_ports() {
.contains("executor-local network proxy launch requires an enabled proxy")
);
}
#[cfg(windows)]
#[tokio::test]
async fn managed_network_selects_elevated_windows_spawn() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current directory")
.try_into()
.expect("absolute cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let self_exe = std::env::current_exe().expect("current executable");
let runtime_paths = ExecServerRuntimePaths::new(self_exe, None).expect("runtime paths");
let permissions = PermissionProfile::read_only();
let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
permissions.clone(),
cwd_uri.clone(),
);
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
let params = ExecParams {
process_id: ProcessId::from("process-managed-network"),
argv: vec!["cmd.exe".to_string(), "/c".to_string(), "exit".to_string()],
cwd: cwd_uri,
env_policy: None,
env: HashMap::new(),
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: Some(sandbox),
enforce_managed_network: true,
managed_network: None,
network_proxy: None,
};
let prepared = prepare_exec_request(&params, HashMap::new(), Some(&runtime_paths))
.await
.expect("prepare sandboxed request");
let spawn = prepared
.windows_sandbox_spawn_request()
.expect("Windows sandbox spawn request");
assert_eq!(
spawn.windows_sandbox_level,
WindowsSandboxLevel::RestrictedToken
);
assert!(spawn.proxy_enforced);
assert_eq!(spawn.permission_profile, &permissions);
assert_eq!(spawn.workspace_roots, std::slice::from_ref(&cwd));
}
+69 -1
View File
@@ -13,13 +13,14 @@ use codex_exec_server::ExecOutputStream;
use codex_exec_server::ExecParams;
use codex_exec_server::ExecProcess;
use codex_exec_server::ExecProcessEvent;
#[cfg(unix)]
#[cfg(any(unix, windows))]
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::ProcessId;
use codex_exec_server::ProcessSignal;
use codex_exec_server::ReadResponse;
use codex_exec_server::StartedExecProcess;
use codex_exec_server::WriteStatus;
use codex_protocol::config_types::WindowsSandboxLevel;
#[cfg(unix)]
use codex_protocol::models::PermissionProfile;
#[cfg(unix)]
@@ -34,6 +35,7 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath;
#[cfg(unix)]
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -699,6 +701,65 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
Ok(())
}
async fn assert_remote_windows_sandbox_process_write() -> Result<()> {
let context = create_process_context(/*use_remote*/ true).await?;
let workspace = TempDir::new()?;
let blocked_file = workspace.path().join("blocked.txt");
let cwd = PathUri::from_host_native_path(workspace.path())?;
let mut sandbox = FileSystemSandboxContext::from_legacy_sandbox_policy(
SandboxPolicy::new_read_only_policy(),
cwd.clone(),
)?;
sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken;
let session = match context
.backend
.start(ExecParams {
process_id: ProcessId::from("proc-windows-sandbox-stdin"),
argv: vec![
r"C:\Windows\System32\cmd.exe".to_string(),
"/D".to_string(),
"/V:ON".to_string(),
"/S".to_string(),
"/C".to_string(),
format!(
"set /P line= & echo blocked > \"{}\" & echo from-stdin:!line!",
blocked_file.display()
),
],
cwd,
env_policy: /*env_policy*/ None,
env: Default::default(),
tty: false,
pipe_stdin: true,
arg0: None,
sandbox: Some(sandbox),
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await
{
Ok(session) => session,
Err(err) => return Err(err.into()),
};
let write_response = session.process.write(b"hello\n".to_vec()).await?;
assert_eq!(write_response.status, WriteStatus::Accepted);
let StartedExecProcess { process, .. } = session;
let wake_rx = process.subscribe_wake();
let (output, exit_code, closed) = collect_process_output_from_reads(process, wake_rx).await?;
assert!(
output.contains("from-stdin:hello"),
"unexpected output: {output:?}"
);
assert_eq!(exit_code, Some(0));
assert!(closed);
assert!(!blocked_file.exists());
Ok(())
}
async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool) -> Result<()> {
let context = create_process_context(use_remote).await?;
let process_id = "proc-stdin-closed".to_string();
@@ -1098,6 +1159,13 @@ async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<()
assert_exec_process_write_then_read_without_tty(use_remote).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[cfg_attr(not(windows), ignore = "Windows-only exec-server sandbox process test")]
#[serial_test::serial(remote_exec_server)]
async fn remote_windows_sandbox_process_accepts_process_write() -> Result<()> {
assert_remote_windows_sandbox_process_write().await
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[cfg_attr(not(unix), ignore = "Unix-only exec-server process test")]
+2
View File
@@ -13,10 +13,12 @@ doctest = false
workspace = true
[dependencies]
anyhow = { workspace = true }
codex-network-proxy = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
codex-utils-pty = { workspace = true }
codex-windows-sandbox = { workspace = true }
dunce = { workspace = true }
libc = { workspace = true }
+4
View File
@@ -6,6 +6,7 @@ mod manager;
pub mod policy_transforms;
#[cfg(target_os = "macos")]
pub mod seatbelt;
mod spawn;
mod windows;
#[cfg(target_os = "linux")]
@@ -25,6 +26,9 @@ pub use manager::SandboxablePreference;
pub use manager::compatibility_sandbox_policy_for_permission_profile;
pub use manager::get_platform_sandbox;
pub use manager::with_managed_mitm_ca_readable_root;
pub use spawn::SpawnRequest;
pub use spawn::WindowsSandboxSpawnRequest;
pub use spawn::spawn_process;
pub use windows::WindowsSandboxFilesystemOverrides;
pub use windows::permission_profile_supports_windows_restricted_token_sandbox;
pub use windows::resolve_windows_elevated_filesystem_overrides;
+125
View File
@@ -0,0 +1,125 @@
use std::collections::HashMap;
use std::path::Path;
use anyhow::Context;
use anyhow::Result;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::SpawnedProcess;
use codex_utils_pty::TerminalSize;
use crate::SandboxType;
use crate::WindowsSandboxFilesystemOverrides;
use crate::WindowsSandboxProxySettingsMode;
/// Windows-specific inputs for an executor-native process spawn.
pub struct WindowsSandboxSpawnRequest<'a> {
pub permission_profile: &'a PermissionProfile,
pub workspace_roots: &'a [AbsolutePathBuf],
pub windows_sandbox_level: WindowsSandboxLevel,
pub proxy_enforced: bool,
pub proxy_settings_mode: WindowsSandboxProxySettingsMode,
pub filesystem_overrides: Option<&'a WindowsSandboxFilesystemOverrides>,
pub use_private_desktop: bool,
}
/// Executor-native process launch request shared by local and exec-server execution.
pub struct SpawnRequest<'a> {
pub command: &'a [String],
pub cwd: &'a Path,
pub env: &'a HashMap<String, String>,
pub arg0: &'a Option<String>,
pub sandbox: SandboxType,
pub windows_sandbox: Option<WindowsSandboxSpawnRequest<'a>>,
pub tty: bool,
pub stdin_open: bool,
pub inherited_fds: &'a [i32],
}
/// Spawn a process using the backend selected by the prepared sandbox request.
pub async fn spawn_process(request: SpawnRequest<'_>) -> Result<SpawnedProcess> {
if request.sandbox == SandboxType::WindowsRestrictedToken {
#[cfg(target_os = "windows")]
{
let windows = request
.windows_sandbox
.context("missing Windows sandbox spawn request")?;
let codex_home = codex_utils_home_dir::find_codex_home()
.context("windows sandbox: failed to resolve codex_home")?;
let empty_paths = &[];
let overrides = windows.filesystem_overrides;
return codex_windows_sandbox::spawn_windows_sandbox_session_for_level(
codex_windows_sandbox::WindowsSandboxSessionRequest {
permission_profile: windows.permission_profile,
workspace_roots: windows.workspace_roots,
codex_home: codex_home.as_path(),
command: request.command.to_vec(),
cwd: request.cwd,
env_map: request.env.clone(),
windows_sandbox_level: windows.windows_sandbox_level,
proxy_enforced: windows.proxy_enforced,
proxy_settings_mode: windows.proxy_settings_mode,
timeout_ms: None,
read_roots_override: overrides
.and_then(|value| value.read_roots_override.as_deref()),
read_roots_include_platform_defaults: overrides
.is_some_and(|value| value.read_roots_include_platform_defaults),
write_roots_override: overrides
.and_then(|value| value.write_roots_override.as_deref()),
deny_read_paths_override: overrides.map_or(empty_paths, |value| {
value.additional_deny_read_paths.as_slice()
}),
deny_write_paths_override: overrides.map_or(empty_paths, |value| {
value.additional_deny_write_paths.as_slice()
}),
tty: request.tty,
stdin_open: request.stdin_open,
use_private_desktop: windows.use_private_desktop,
},
)
.await;
}
#[cfg(not(target_os = "windows"))]
anyhow::bail!("Windows sandbox process spawn is unavailable on this platform");
}
let (program, args) = request
.command
.split_first()
.context("missing program for process spawn")?;
if request.tty {
codex_utils_pty::pty::spawn_process(
program,
args,
request.cwd,
request.env,
request.arg0,
TerminalSize::default(),
request.inherited_fds,
)
.await
} else if request.stdin_open {
codex_utils_pty::pipe::spawn_process(
program,
args,
request.cwd,
request.env,
request.arg0,
request.inherited_fds,
)
.await
} else {
codex_utils_pty::pipe::spawn_process_no_stdin(
program,
args,
request.cwd,
request.env,
request.arg0,
request.inherited_fds,
)
.await
}
}
+14 -14
View File
@@ -263,31 +263,31 @@ async fn spawn_process_with_stdin_mode(
})
}
/// Spawn a process using regular pipes (no PTY), returning handles for stdin, split output, and exit.
/// Spawn a process using regular pipes and preserve selected inherited file
/// descriptors across exec on Unix.
pub async fn spawn_process(
program: &str,
args: &[String],
cwd: &Path,
env: &HashMap<String, String>,
arg0: &Option<String>,
inherited_fds: &[i32],
) -> Result<SpawnedProcess> {
spawn_process_with_stdin_mode(program, args, cwd, env, arg0, PipeStdinMode::Piped, &[]).await
}
/// Spawn a process using regular pipes, but close stdin immediately.
pub async fn spawn_process_no_stdin(
program: &str,
args: &[String],
cwd: &Path,
env: &HashMap<String, String>,
arg0: &Option<String>,
) -> Result<SpawnedProcess> {
spawn_process_no_stdin_with_inherited_fds(program, args, cwd, env, arg0, &[]).await
spawn_process_with_stdin_mode(
program,
args,
cwd,
env,
arg0,
PipeStdinMode::Piped,
inherited_fds,
)
.await
}
/// Spawn a process using regular pipes, close stdin immediately, and preserve
/// selected inherited file descriptors across exec on Unix.
pub async fn spawn_process_no_stdin_with_inherited_fds(
pub async fn spawn_process_no_stdin(
program: &str,
args: &[String],
cwd: &Path,
+2 -14
View File
@@ -122,7 +122,8 @@ fn platform_native_pty_system() -> Box<dyn portable_pty::PtySystem + Send> {
}
}
/// Spawn a process attached to a PTY, returning handles for stdin, split output, and exit.
/// Spawn a process attached to a PTY, preserving selected inherited file
/// descriptors across exec on Unix.
pub async fn spawn_process(
program: &str,
args: &[String],
@@ -130,19 +131,6 @@ pub async fn spawn_process(
env: &HashMap<String, String>,
arg0: &Option<String>,
size: TerminalSize,
) -> Result<SpawnedProcess> {
spawn_process_with_inherited_fds(program, args, cwd, env, arg0, size, &[]).await
}
/// Spawn a process attached to a PTY, preserving any inherited file
/// descriptors listed in `inherited_fds` across exec on Unix.
pub async fn spawn_process_with_inherited_fds(
program: &str,
args: &[String],
cwd: &Path,
env: &HashMap<String, String>,
arg0: &Option<String>,
size: TerminalSize,
inherited_fds: &[i32],
) -> Result<SpawnedProcess> {
if program.is_empty() {
+22 -16
View File
@@ -7,10 +7,6 @@ use crate::ProcessDriver;
use crate::SpawnedProcess;
use crate::TerminalSize;
use crate::combine_output_receivers;
#[cfg(unix)]
use crate::pipe::spawn_process_no_stdin_with_inherited_fds;
#[cfg(unix)]
use crate::pty::spawn_process_with_inherited_fds;
use crate::spawn_from_driver;
use crate::spawn_pipe_process;
use crate::spawn_pipe_process_no_stdin;
@@ -365,6 +361,7 @@ async fn pty_python_repl_emits_output_and_exits() -> anyhow::Result<()> {
&env_map,
&None,
TerminalSize::default(),
&[],
)
.await?;
let (session, mut output_rx, exit_rx) = combine_spawned_output(spawned);
@@ -421,7 +418,7 @@ async fn pipe_process_round_trips_stdin() -> anyhow::Result<()> {
)
};
let env_map: HashMap<String, String> = std::env::vars().collect();
let spawned = spawn_pipe_process(&program, &args, Path::new("."), &env_map, &None).await?;
let spawned = spawn_pipe_process(&program, &args, Path::new("."), &env_map, &None, &[]).await?;
let (session, output_rx, exit_rx) = combine_spawned_output(spawned);
let writer = session.writer_sender();
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
@@ -454,7 +451,7 @@ async fn pipe_process_detaches_from_parent_session() -> anyhow::Result<()> {
let env_map: HashMap<String, String> = std::env::vars().collect();
let script = "echo $$; sleep 0.2";
let (program, args) = shell_command(script);
let spawned = spawn_pipe_process(&program, &args, Path::new("."), &env_map, &None).await?;
let spawned = spawn_pipe_process(&program, &args, Path::new("."), &env_map, &None, &[]).await?;
let (_session, mut output_rx, exit_rx) = combine_spawned_output(spawned);
let pid_bytes =
@@ -493,8 +490,15 @@ async fn pipe_and_pty_share_interface() -> anyhow::Result<()> {
let (pipe_program, pipe_args) = shell_command(&echo_sleep_command("pipe_ok"));
let (pty_program, pty_args) = shell_command(&echo_sleep_command("pty_ok"));
let pipe =
spawn_pipe_process(&pipe_program, &pipe_args, Path::new("."), &env_map, &None).await?;
let pipe = spawn_pipe_process(
&pipe_program,
&pipe_args,
Path::new("."),
&env_map,
&None,
&[],
)
.await?;
let pty = spawn_pty_process(
&pty_program,
&pty_args,
@@ -502,6 +506,7 @@ async fn pipe_and_pty_share_interface() -> anyhow::Result<()> {
&env_map,
&None,
TerminalSize::default(),
&[],
)
.await?;
let (_pipe_session, pipe_output_rx, pipe_exit_rx) = combine_spawned_output(pipe);
@@ -537,7 +542,7 @@ async fn pipe_drains_stderr_without_stdout_activity() -> anyhow::Result<()> {
let script = "import sys\nchunk = 'E' * 65536\nfor _ in range(64):\n sys.stderr.write(chunk)\n sys.stderr.flush()\n";
let args = vec!["-c".to_string(), script.to_string()];
let env_map: HashMap<String, String> = std::env::vars().collect();
let spawned = spawn_pipe_process(&python, &args, Path::new("."), &env_map, &None).await?;
let spawned = spawn_pipe_process(&python, &args, Path::new("."), &env_map, &None, &[]).await?;
let (_session, output_rx, exit_rx) = combine_spawned_output(spawned);
let (output, code) = collect_output_until_exit(output_rx, exit_rx, /*timeout_ms*/ 10_000).await;
@@ -553,7 +558,7 @@ async fn pipe_process_can_expose_split_stdout_and_stderr() -> anyhow::Result<()>
let env_map: HashMap<String, String> = std::env::vars().collect();
let (program, args) = shell_command(&split_stdout_stderr_command());
let spawned =
spawn_pipe_process_no_stdin(&program, &args, Path::new("."), &env_map, &None).await?;
spawn_pipe_process_no_stdin(&program, &args, Path::new("."), &env_map, &None, &[]).await?;
let SpawnedProcess {
session: _session,
stdout_rx,
@@ -747,7 +752,7 @@ async fn pipe_terminate_aborts_detached_readers() -> anyhow::Result<()> {
let script =
"setsid sh -c 'i=0; while [ $i -lt 200 ]; do echo tick; sleep 0.01; i=$((i+1)); done' &";
let (program, args) = shell_command(script);
let spawned = spawn_pipe_process(&program, &args, Path::new("."), &env_map, &None).await?;
let spawned = spawn_pipe_process(&program, &args, Path::new("."), &env_map, &None, &[]).await?;
let (session, mut output_rx, _exit_rx) = combine_spawned_output(spawned);
let _ = tokio::time::timeout(tokio::time::Duration::from_millis(500), output_rx.recv())
@@ -787,6 +792,7 @@ async fn pty_terminate_kills_background_children_in_same_process_group() -> anyh
&env_map,
&None,
TerminalSize::default(),
&[],
)
.await?;
let (session, mut output_rx, _exit_rx) = combine_spawned_output(spawned);
@@ -841,7 +847,7 @@ async fn pty_spawn_can_preserve_inherited_fds() -> anyhow::Result<()> {
);
let script = "printf __preserved__ >\"/dev/fd/$PRESERVED_FD\"";
let spawned = spawn_process_with_inherited_fds(
let spawned = spawn_pty_process(
"/bin/sh",
&["-c".to_string(), script.to_string()],
Path::new("."),
@@ -893,7 +899,7 @@ async fn pty_preserving_inherited_fds_keeps_python_repl_running() -> anyhow::Res
preserved_fd.as_raw_fd().to_string(),
);
let spawned = spawn_process_with_inherited_fds(
let spawned = spawn_pty_process(
&python,
&[],
Path::new("."),
@@ -959,7 +965,7 @@ async fn pty_spawn_with_inherited_fds_reports_exec_failures() -> anyhow::Result<
let write_end = unsafe { std::fs::File::from_raw_fd(fds[1]) };
let env_map: HashMap<String, String> = std::env::vars().collect();
let spawn_result = spawn_process_with_inherited_fds(
let spawn_result = spawn_pty_process(
"/definitely/missing/command",
&[],
Path::new("."),
@@ -1008,7 +1014,7 @@ async fn pty_spawn_with_inherited_fds_supports_resize() -> anyhow::Result<()> {
let env_map: HashMap<String, String> = std::env::vars().collect();
let script = "stty -echo; printf 'start:%s\\n' \"$(stty size)\"; IFS= read _line; printf 'after:%s\\n' \"$(stty size)\"";
let spawned = spawn_process_with_inherited_fds(
let spawned = spawn_pty_process(
"/bin/sh",
&["-c".to_string(), script.to_string()],
Path::new("."),
@@ -1079,7 +1085,7 @@ async fn pipe_spawn_no_stdin_can_preserve_inherited_fds() -> anyhow::Result<()>
);
let script = "printf __pipe_preserved__ >\"/dev/fd/$PRESERVED_FD\"";
let spawned = spawn_process_no_stdin_with_inherited_fds(
let spawned = spawn_pipe_process_no_stdin(
"/bin/sh",
&["-c".to_string(), script.to_string()],
Path::new("."),
+2
View File
@@ -74,6 +74,7 @@ async fn conpty_delivers_input_to_foreground_children() -> anyhow::Result<()> {
&env,
/*arg0*/ &None,
TerminalSize::default(),
&[],
)
.await?;
let (session, mut output_rx, exit_rx) = combine_spawned_output(spawned);
@@ -126,6 +127,7 @@ async fn conpty_ctrl_c_interrupts_powershell_foreground_child() -> anyhow::Resul
&env,
/*arg0*/ &None,
TerminalSize::default(),
&[],
)
.await?;
let (session, mut output_rx, exit_rx) = combine_spawned_output(spawned);