diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4d89989627..65de5641b4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3298,6 +3298,7 @@ version = "0.0.0" dependencies = [ "base64 0.22.1", "codex-file-system", + "codex-mxc-sandbox", "codex-network-proxy", "codex-protocol", "codex-shell-command", diff --git a/codex-rs/exec-server-protocol/Cargo.toml b/codex-rs/exec-server-protocol/Cargo.toml index 9dac711e1a..62841ed973 100644 --- a/codex-rs/exec-server-protocol/Cargo.toml +++ b/codex-rs/exec-server-protocol/Cargo.toml @@ -22,5 +22,8 @@ codex-utils-path-uri = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["arbitrary_precision", "raw_value"] } +[target.'cfg(windows)'.dependencies] +codex-mxc-sandbox = { workspace = true } + [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index 8ef964028a..0dc0d51e74 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -217,6 +217,10 @@ impl EnvironmentInfo { /// Returns information about the current local exec-server process. pub fn local() -> Self { + #[cfg(windows)] + let windows_mxc = codex_mxc_sandbox::is_available(); + #[cfg(not(windows))] + let windows_mxc = false; let cwd = std::env::current_dir().ok(); let temporary_directories = Self::local_temporary_directories_with_cwd(cwd.as_deref()); let normalize_temp_path = |path: std::ffi::OsString| { @@ -246,7 +250,7 @@ impl EnvironmentInfo { http_header_env_vars: true, sandboxed_file_streaming: true, shell_snapshot_v2: cfg!(unix), - windows_mxc: false, + windows_mxc, }, } } diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index 15bff80842..26c59008e5 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -5,6 +5,7 @@ use crate::process_telemetry::ProcessTelemetry; use codex_exec_server_protocol::JSONRPCErrorError; use codex_network_proxy::CUSTOM_CA_ENV_KEYS; use codex_network_proxy::ManagedNetworkSandboxContext; +use codex_network_proxy::ManagedProxyRouting; use codex_network_proxy::NetworkPolicyAuditObserver; use codex_network_proxy::NetworkPolicyDecider; use codex_network_proxy::NetworkProxy; @@ -14,6 +15,7 @@ use codex_network_proxy::RemoteNetworkProxyLaunchConfig; use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path; #[cfg(target_os = "windows")] use codex_network_proxy::strip_managed_proxy_env; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxDirectSpawnTransformRequest; @@ -87,17 +89,9 @@ pub(crate) async fn prepare_exec_request_with_telemetry( if let Some(sandbox) = params.sandbox.as_ref() && sandbox.windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Mxc { - if params.tty || params.arg0.is_some() { + if params.arg0.is_some() || sandbox.windows_sandbox_private_desktop { return Err(invalid_params( - "MXC currently supports ordinary pipe launches only".to_owned(), - )); - } - if params.enforce_managed_network - || params.managed_network.is_some() - || params.network_proxy.is_some() - { - return Err(invalid_params( - "MXC managed networking is not supported yet".to_owned(), + "MXC custom argv0 and private-desktop launches are not supported".to_owned(), )); } if !codex_sandboxing::windows_mxc_available() { @@ -126,6 +120,15 @@ pub(crate) async fn prepare_exec_request_with_telemetry( prepare_managed_network( params.managed_network.as_ref(), network_proxy, + if params + .sandbox + .as_ref() + .is_some_and(|sandbox| sandbox.windows_sandbox_level == WindowsSandboxLevel::Mxc) + { + ManagedProxyRouting::DedicatedListeners + } else { + ManagedProxyRouting::SharedIngress + }, env, network_policy_decider, network_policy_audit_observer, @@ -328,6 +331,7 @@ pub(crate) async fn prepare_exec_request_with_telemetry( async fn prepare_managed_network( managed_network: Option<&ManagedNetworkSandboxContext>, network_proxy: Option<&RemoteNetworkProxyLaunchConfig>, + routing: ManagedProxyRouting, env: HashMap, network_policy_decider: Option>, network_policy_audit_observer: Option, @@ -363,7 +367,9 @@ async fn prepare_managed_network( registration_id: registration.executor_registration_id.clone(), }), }); - let mut builder = NetworkProxy::builder().state(Arc::new(state)); + let mut builder = NetworkProxy::builder() + .state(Arc::new(state)) + .managed_proxy_routing(routing); if let Some(network_policy_decider) = network_policy_decider { builder = builder.policy_decider_arc(network_policy_decider); } @@ -376,15 +382,19 @@ async fn prepare_managed_network( .await .map_err(|err| internal_error(format!("failed to start executor network proxy: {err}")))?; #[cfg(target_os = "windows")] - let network_proxy_restricting_sid = Some( - proxy - .network_proxy_restricting_sid(/*environment_id*/ None) - .ok_or_else(|| { - internal_error( - "managed Windows proxy route is missing its restricting SID".to_string(), - ) - })?, - ); + let network_proxy_restricting_sid = if routing == ManagedProxyRouting::SharedIngress { + Some( + proxy + .network_proxy_restricting_sid(/*environment_id*/ None) + .ok_or_else(|| { + internal_error( + "managed Windows proxy route is missing its restricting SID".to_string(), + ) + })?, + ) + } else { + None + }; #[cfg(not(target_os = "windows"))] let network_proxy_restricting_sid = None; let prepared = proxy diff --git a/codex-rs/exec-server/src/process_sandbox_tests.rs b/codex-rs/exec-server/src/process_sandbox_tests.rs index d2b65555d0..e110481e60 100644 --- a/codex-rs/exec-server/src/process_sandbox_tests.rs +++ b/codex-rs/exec-server/src/process_sandbox_tests.rs @@ -133,29 +133,17 @@ async fn sandbox_request_wraps_native_argv_on_executor() { let mut params = params; params.sandbox.as_mut().unwrap().windows_sandbox_level = codex_protocol::config_types::WindowsSandboxLevel::Mxc; - for (tty, managed, message) in [ - (false, false, "native MXC is unavailable on this executor"), - ( - true, - false, - "MXC currently supports ordinary pipe launches only", - ), - (false, true, "MXC managed networking is not supported yet"), - ] { - params.tty = tty; - params.enforce_managed_network = managed; - let error = prepare_exec_request( - ¶ms, - HashMap::new(), - Some(&runtime_paths), - /*network_policy_decider*/ None, - /*network_policy_audit_observer*/ None, - ) - .await - .err() - .expect("unsupported MXC must fail closed"); - assert_eq!(error.message, message); - } + let error = prepare_exec_request( + ¶ms, + HashMap::new(), + Some(&runtime_paths), + /*network_policy_decider*/ None, + /*network_policy_audit_observer*/ None, + ) + .await + .err() + .expect("unsupported MXC must fail closed"); + assert_eq!(error.message, "native MXC is unavailable on this executor"); } #[cfg(unix)] diff --git a/codex-rs/exec-server/tests/accepted_websocket.rs b/codex-rs/exec-server/tests/accepted_websocket.rs index bd31df7c74..a3caf7b3de 100644 --- a/codex-rs/exec-server/tests/accepted_websocket.rs +++ b/codex-rs/exec-server/tests/accepted_websocket.rs @@ -187,9 +187,10 @@ async fn accepted_websocket_interoperates_and_recovers_with_real_direct_executor .default_environment() .context("direct executor environment should be installed")?; + let expected_info = EnvironmentInfo::local(); assert_eq!( timeout(TEST_TIMEOUT, environment.force_info()).await??, - EnvironmentInfo::local() + expected_info ); let files = tempfile::tempdir()?; let large_file_path = files.path().join("large-response.bin"); @@ -340,7 +341,7 @@ async fn accepted_websocket_interoperates_and_recovers_with_real_direct_executor )); assert_eq!( timeout(TEST_TIMEOUT, environment.force_info()).await??, - EnvironmentInfo::local() + expected_info ); let recovered_read = timeout(Duration::from_secs(5), pending_read) diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index b982301949..ee58834c73 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -1297,6 +1297,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re async fn assert_remote_windows_sandbox_process_write( windows_sandbox_level: WindowsSandboxLevel, expected_sandbox_type: codex_sandboxing::SandboxType, + tty: bool, ) -> Result<()> { if !selected_windows_sandbox_available(windows_sandbox_level) { eprintln!("skipping MXC enforcement test: native MXC is unavailable on this host"); @@ -1332,8 +1333,8 @@ async fn assert_remote_windows_sandbox_process_write( shell_snapshot: None, env_policy: /*env_policy*/ None, env: Default::default(), - tty: false, - pipe_stdin: true, + tty, + pipe_stdin: !tty, arg0: None, sandbox: Some(sandbox), enforce_managed_network: false, @@ -1347,7 +1348,8 @@ async fn assert_remote_windows_sandbox_process_write( }; assert_eq!(session.sandbox_type, Some(expected_sandbox_type)); - let write_response = session.process.write(b"hello\n".to_vec()).await?; + let input = if tty { b"hello\r" } else { b"hello\n" }; + let write_response = session.process.write(input.to_vec()).await?; assert_eq!(write_response.status, WriteStatus::Accepted); let StartedExecProcess { process, .. } = session; let wake_rx = process.subscribe_wake(); @@ -1766,13 +1768,21 @@ async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<() #[test_case( WindowsSandboxLevel::RestrictedToken, - codex_sandboxing::SandboxType::WindowsRestrictedToken; + codex_sandboxing::SandboxType::WindowsRestrictedToken, + false; "restricted_token" )] #[test_case( WindowsSandboxLevel::Mxc, - codex_sandboxing::SandboxType::WindowsMxc; - "mxc" + codex_sandboxing::SandboxType::WindowsMxc, + false; + "mxc_pipe" +)] +#[test_case( + WindowsSandboxLevel::Mxc, + codex_sandboxing::SandboxType::WindowsMxc, + true; + "mxc_conpty" )] #[cfg_attr(not(windows), ignore = "Windows-only exec-server sandbox process test")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1780,8 +1790,10 @@ async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<() async fn remote_windows_sandbox_process_accepts_process_write( windows_sandbox_level: WindowsSandboxLevel, expected_sandbox_type: codex_sandboxing::SandboxType, + tty: bool, ) -> Result<()> { - assert_remote_windows_sandbox_process_write(windows_sandbox_level, expected_sandbox_type).await + assert_remote_windows_sandbox_process_write(windows_sandbox_level, expected_sandbox_type, tty) + .await } #[test_case(false ; "local")]