Add an authenticated Windows sandbox provisioning client (#42337)

## What changed

- Add a client for the packaged Windows sandbox provisioning service that verifies the named-pipe server belongs to the running `CodexSandboxService` process before exchanging a versioned provisioning request.
- Treat an absent, busy, timed-out, or disconnected service as unavailable while surfacing provisioning and authentication failures.
- Send the complete `WindowsSandboxProvisioningSettings` and classify inherited HTTP and SOCKS proxy listeners separately for managed-policy validation.
- Share bounded frame-readiness handling between runner and provisioning IPC.

## Testing

- Extend proxy environment tests to cover network-enabled profiles, mixed HTTP and SOCKS listeners, explicit proxy-port overrides, case-insensitive schemes, and unclassified protocols.

GitOrigin-RevId: 6ff6741fc3ecd76314a7a045523a8d7a7e7af686
This commit is contained in:
johnl-oai
2026-09-02 18:40:46 +00:00
committed by copyberry
parent 301a7c5e01
commit dcfcb570b2
7 changed files with 491 additions and 58 deletions

View File

@@ -89,6 +89,7 @@ features = [
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",
"Win32_System_Registry",
"Win32_System_Services",
]
version = "0.52"

View File

@@ -21,7 +21,6 @@ use anyhow::Context;
use anyhow::Result;
use std::ffi::c_void;
use std::fs::File;
use std::os::windows::io::AsRawHandle;
use std::os::windows::io::FromRawHandle;
use std::path::Path;
use std::ptr;
@@ -39,7 +38,6 @@ use windows_sys::Win32::Foundation::GetLastError;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
use windows_sys::Win32::System::IO::CancelSynchronousIo;
use windows_sys::Win32::System::Pipes::PeekNamedPipe;
use windows_sys::Win32::System::Threading::CreateProcessWithLogonW;
use windows_sys::Win32::System::Threading::GetCurrentProcess;
use windows_sys::Win32::System::Threading::GetCurrentThread;
@@ -50,7 +48,6 @@ use windows_sys::Win32::System::Threading::WaitForSingleObject;
const RUNNER_SPAWN_READY_TIMEOUT: Duration = Duration::from_secs(15);
const RUNNER_PIPE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
const RUNNER_SPAWN_READY_POLL_INTERVAL: Duration = Duration::from_millis(5);
const RUNNER_ERROR_MODE_FLAGS: u32 = 0x0001 | 0x0002;
const WAIT_OBJECT_0: u32 = 0;
@@ -159,7 +156,11 @@ impl RunnerTransport {
}
pub(crate) fn read_spawn_ready(&mut self) -> Result<()> {
wait_for_complete_frame(&self.pipe_read, RUNNER_SPAWN_READY_TIMEOUT)?;
crate::framed_io::wait_for_complete_frame(
&self.pipe_read,
Instant::now() + RUNNER_SPAWN_READY_TIMEOUT,
)
.context("wait for runner spawn_ready")?;
let msg = read_frame(&mut self.pipe_read)?
.ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?;
match msg.message {
@@ -446,52 +447,6 @@ pub(crate) fn spawn_runner_transport(
Ok(transport)
}
fn wait_for_complete_frame(pipe_read: &File, timeout: Duration) -> Result<()> {
let handle = pipe_read.as_raw_handle() as HANDLE;
let deadline = Instant::now() + timeout;
let mut len_buf = [0u8; 4];
loop {
let mut bytes_read = 0u32;
let mut total_available = 0u32;
let ok = unsafe {
PeekNamedPipe(
handle,
len_buf.as_mut_ptr() as *mut c_void,
len_buf.len() as u32,
&mut bytes_read,
&mut total_available,
ptr::null_mut(),
)
};
if ok == 0 {
let err = unsafe { GetLastError() } as i32;
return Err(anyhow::anyhow!(
"PeekNamedPipe failed while waiting for spawn_ready: {err}"
));
}
if bytes_read == len_buf.len() as u32 {
let frame_len = u32::from_le_bytes(len_buf) as usize;
let total_len = frame_len
.checked_add(len_buf.len())
.ok_or_else(|| anyhow::anyhow!("runner frame length overflow"))?;
if total_available as usize >= total_len {
return Ok(());
}
}
if Instant::now() >= deadline {
return Err(anyhow::anyhow!(
"timed out after {}ms waiting for runner spawn_ready",
timeout.as_millis()
));
}
std::thread::sleep(RUNNER_SPAWN_READY_POLL_INTERVAL);
}
}
#[cfg(test)]
mod tests {
use super::RunnerLogonError;

View File

@@ -3,11 +3,19 @@
use anyhow::Result;
use serde::Serialize;
use serde::de::DeserializeOwned;
use std::fs::File;
use std::io;
use std::io::Read;
use std::io::Write;
use std::os::windows::io::AsRawHandle;
use std::ptr;
use std::time::Duration;
use std::time::Instant;
use windows_sys::Win32::System::Pipes::PeekNamedPipe;
/// Bound the memory used by an individual untrusted IPC frame.
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
const FRAME_POLL_INTERVAL: Duration = Duration::from_millis(5);
pub(crate) fn write_frame<W: Write, T: Serialize>(mut writer: W, message: &T) -> Result<()> {
let payload = serde_json::to_vec(message)?;
@@ -37,3 +45,47 @@ pub(crate) fn read_frame<R: Read, T: DeserializeOwned>(mut reader: R) -> Result<
let message = serde_json::from_slice(&payload)?;
Ok(Some(message))
}
pub(crate) fn wait_for_complete_frame(pipe: &File, deadline: Instant) -> io::Result<()> {
let mut len_buf = [0_u8; 4];
loop {
let mut bytes_read = 0_u32;
let mut total_available = 0_u32;
let ok = unsafe {
PeekNamedPipe(
pipe.as_raw_handle() as _,
len_buf.as_mut_ptr().cast(),
len_buf.len() as u32,
&mut bytes_read,
&mut total_available,
ptr::null_mut(),
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
if bytes_read == len_buf.len() as u32 {
let frame_len = u32::from_le_bytes(len_buf) as usize;
if frame_len > MAX_FRAME_LEN {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("frame too large: {frame_len}"),
));
}
if total_available as usize >= len_buf.len() + frame_len {
return Ok(());
}
}
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"timed out waiting for a complete IPC frame",
));
}
std::thread::sleep(remaining.min(FRAME_POLL_INTERVAL));
}
}

View File

@@ -8,6 +8,9 @@ mod ssh_config_dependencies;
use std::fmt;
use std::sync::Arc;
use serde::Deserialize;
use serde::Serialize;
/// Cancellation hook used by Windows sandbox capture backends.
#[derive(Clone)]
pub struct WindowsSandboxCancellationToken {
@@ -38,7 +41,8 @@ impl fmt::Debug for WindowsSandboxCancellationToken {
pub use codex_protocol::config_types::WindowsSandboxProxySettingsMode;
/// Network settings installed by an administrator during managed Windows sandbox setup.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct WindowsSandboxProvisioningSettings {
/// Loopback proxy ports permitted for the offline sandbox identity.
pub proxy_ports: Vec<u16>,
@@ -79,6 +83,8 @@ mod path_normalization;
#[cfg(target_os = "windows")]
mod process;
#[cfg(target_os = "windows")]
mod provisioning_client;
#[cfg(target_os = "windows")]
mod provisioning_protocol;
#[cfg(target_os = "windows")]
mod resolved_permissions;
@@ -259,6 +265,10 @@ pub use process::read_handle_loop;
#[cfg(target_os = "windows")]
pub use process::spawn_process_with_pipes;
#[cfg(target_os = "windows")]
pub use provisioning_client::WindowsSandboxProvisioningOutcome;
#[cfg(target_os = "windows")]
pub use provisioning_client::provision_windows_sandbox_via_service;
#[cfg(target_os = "windows")]
pub use provisioning_protocol::FramedProvisioningMessage;
#[cfg(target_os = "windows")]
pub use provisioning_protocol::PROVISIONING_PROTOCOL_VERSION;
@@ -271,6 +281,8 @@ pub use provisioning_protocol::SandboxProvisioningRequest;
#[cfg(target_os = "windows")]
pub use provisioning_protocol::SandboxProvisioningResponse;
#[cfg(target_os = "windows")]
pub use provisioning_protocol::WindowsSandboxProxyListeners;
#[cfg(target_os = "windows")]
pub use provisioning_protocol::read_provisioning_frame;
#[cfg(target_os = "windows")]
pub use provisioning_protocol::write_provisioning_frame;

View File

@@ -0,0 +1,277 @@
//! Authenticated client for the packaged Windows sandbox provisioning service.
use crate::WindowsSandboxProvisioningSettings;
use crate::WindowsSandboxProxyListeners;
use std::collections::HashMap;
use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::mem::size_of;
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use std::path::Path;
use std::ptr;
use std::time::Duration;
use std::time::Instant;
use anyhow::Context;
use anyhow::anyhow;
use anyhow::bail;
use codex_protocol::models::PermissionProfile;
use windows_sys::Win32::Foundation::ERROR_BROKEN_PIPE;
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows_sys::Win32::Foundation::ERROR_NO_DATA;
use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY;
use windows_sys::Win32::Foundation::ERROR_PIPE_NOT_CONNECTED;
use windows_sys::Win32::Foundation::ERROR_SEM_TIMEOUT;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::SC_HANDLE;
use windows_sys::Win32::Storage::FileSystem::SECURITY_IMPERSONATION;
use windows_sys::Win32::Storage::FileSystem::SECURITY_SQOS_PRESENT;
use windows_sys::Win32::System::Pipes::GetNamedPipeServerProcessId;
use windows_sys::Win32::System::Pipes::WaitNamedPipeW;
use windows_sys::Win32::System::Services;
const SERVICE_NAME: &str = "CodexSandboxService";
const PROVISIONING_TIMEOUT: Duration = Duration::from_secs(120);
impl WindowsSandboxProvisioningSettings {
/// Derives the full firewall settings using the same environment handling as elevated setup.
pub fn from_environment(
permission_profile: &PermissionProfile,
env_map: &HashMap<String, String>,
) -> Self {
let network_identity = if permission_profile.network_sandbox_policy().is_enabled() {
crate::setup::SandboxNetworkIdentity::Online
} else {
crate::setup::SandboxNetworkIdentity::Offline
};
let settings = crate::setup::offline_proxy_settings_from_env(env_map, network_identity);
Self {
proxy_ports: settings.proxy_ports,
allow_local_binding: settings.allow_local_binding,
}
}
}
impl WindowsSandboxProxyListeners {
/// Identifies known listener protocols without restricting the ports allowed by setup.
pub fn from_environment(
permission_profile: &PermissionProfile,
env_map: &HashMap<String, String>,
) -> Self {
if permission_profile.network_sandbox_policy().is_enabled() {
return Self::default();
}
let mut listeners = Self::default();
for value in crate::setup::PROXY_ENV_KEYS
.iter()
.filter_map(|key| env_map.get(*key))
{
let Some((scheme, _)) = value.trim().split_once("://") else {
continue;
};
let Some(port) = crate::setup::loopback_proxy_port_from_url(value) else {
continue;
};
match scheme.to_ascii_lowercase().as_str() {
"http" | "https" => listeners.http_ports.push(port),
"socks4" | "socks4a" | "socks5" | "socks5h" => {
listeners.socks_ports.push(port);
}
_ => {}
}
}
listeners.http_ports.sort_unstable();
listeners.http_ports.dedup();
listeners.socks_ports.sort_unstable();
listeners.socks_ports.dedup();
listeners
}
}
/// Result of attempting setup through the packaged provisioning service.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum WindowsSandboxProvisioningOutcome {
/// The service completed elevated provisioning.
Provisioned,
/// The service was absent, did not support this caller, or timed out.
Unavailable,
}
/// Provisions the elevated Windows sandbox through the authenticated packaged service.
pub fn provision_windows_sandbox_via_service(
codex_home: &Path,
settings: WindowsSandboxProvisioningSettings,
listeners: WindowsSandboxProxyListeners,
) -> anyhow::Result<WindowsSandboxProvisioningOutcome> {
let request = crate::FramedProvisioningMessage {
version: crate::PROVISIONING_PROTOCOL_VERSION,
message: crate::ProvisioningMessage::ProvisionSandboxRequest {
payload: crate::SandboxProvisioningRequest {
codex_home: codex_home
.to_str()
.context("sandbox provisioning home is not valid UTF-8")?
.to_owned(),
settings,
listeners,
},
},
};
let deadline = Instant::now() + PROVISIONING_TIMEOUT;
let Some(mut pipe) = connect(deadline)? else {
return Ok(WindowsSandboxProvisioningOutcome::Unavailable);
};
let response = (|| -> anyhow::Result<crate::FramedProvisioningMessage> {
verify_server(pipe.as_raw_handle() as HANDLE)
.context("authenticate provisioning pipe server")?;
crate::write_provisioning_frame(&mut pipe, &request)
.context("send sandbox provisioning request")?;
crate::framed_io::wait_for_complete_frame(&pipe, deadline)
.context("wait for sandbox provisioning response")?;
crate::read_provisioning_frame(&mut pipe)
.context("read sandbox provisioning response")?
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::UnexpectedEof,
"sandbox provisioning service closed the pipe without a response",
)
})
.context("read sandbox provisioning response")
})();
let response = match response {
Ok(response) => response,
Err(error)
if error.downcast_ref::<io::Error>().is_some_and(|error| {
matches!(
error.kind(),
io::ErrorKind::UnexpectedEof | io::ErrorKind::TimedOut
) || matches!(
error.raw_os_error(),
Some(code)
if code == ERROR_BROKEN_PIPE as i32
|| code == ERROR_NO_DATA as i32
|| code == ERROR_PIPE_NOT_CONNECTED as i32
)
}) =>
{
return Ok(WindowsSandboxProvisioningOutcome::Unavailable);
}
Err(error) => return Err(error),
};
if response.version != crate::PROVISIONING_PROTOCOL_VERSION {
return Ok(WindowsSandboxProvisioningOutcome::Unavailable);
}
let crate::ProvisioningMessage::ProvisionSandboxResponse { payload } = response.message else {
bail!("unexpected sandbox provisioning response message");
};
match payload {
crate::SandboxProvisioningResponse::Ok => {
Ok(WindowsSandboxProvisioningOutcome::Provisioned)
}
crate::SandboxProvisioningResponse::Error { message } => Err(anyhow!(message)),
}
}
fn connect(deadline: Instant) -> anyhow::Result<Option<File>> {
let open_pipe = || {
OpenOptions::new()
.read(true)
.write(true)
.custom_flags(SECURITY_SQOS_PRESENT | SECURITY_IMPERSONATION)
.open(crate::SANDBOX_PROVISIONING_PIPE_NAME)
};
let pipe_name = crate::to_wide(crate::SANDBOX_PROVISIONING_PIPE_NAME);
loop {
match open_pipe() {
Ok(pipe) => return Ok(Some(pipe)),
Err(error) if error.raw_os_error() == Some(ERROR_FILE_NOT_FOUND as i32) => {
return Ok(None);
}
Err(error) if error.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Ok(None);
}
let wait_ms = u32::try_from(remaining.as_millis())
.unwrap_or(u32::MAX)
.max(1);
if unsafe { WaitNamedPipeW(pipe_name.as_ptr(), wait_ms) } == 0 {
let error = io::Error::last_os_error();
if matches!(
error.raw_os_error(),
Some(code)
if code == ERROR_FILE_NOT_FOUND as i32
|| code == ERROR_SEM_TIMEOUT as i32
) {
return Ok(None);
}
return Err(error).context("wait for sandbox provisioning pipe");
}
}
Err(error) => return Err(error).context("open sandbox provisioning pipe"),
}
}
}
fn verify_server(pipe: HANDLE) -> anyhow::Result<()> {
let mut pipe_process_id = 0;
if unsafe { GetNamedPipeServerProcessId(pipe, &mut pipe_process_id) } == 0 {
return Err(io::Error::last_os_error()).context("identify provisioning pipe server");
}
let manager =
unsafe { Services::OpenSCManagerW(ptr::null(), ptr::null(), Services::SC_MANAGER_CONNECT) };
if manager == 0 {
return Err(io::Error::last_os_error()).context("open service control manager");
}
let manager = ServiceHandle(manager);
let service_name = crate::to_wide(SERVICE_NAME);
let service = unsafe {
Services::OpenServiceW(
manager.0,
service_name.as_ptr(),
Services::SERVICE_QUERY_STATUS,
)
};
if service == 0 {
return Err(io::Error::last_os_error()).context("open sandbox provisioning service");
}
let service = ServiceHandle(service);
let mut status: Services::SERVICE_STATUS_PROCESS = unsafe { std::mem::zeroed() };
let mut bytes_needed = 0;
if unsafe {
Services::QueryServiceStatusEx(
service.0,
Services::SC_STATUS_PROCESS_INFO,
ptr::from_mut(&mut status).cast(),
size_of::<Services::SERVICE_STATUS_PROCESS>() as u32,
&mut bytes_needed,
)
} == 0
{
return Err(io::Error::last_os_error()).context("query sandbox provisioning service");
}
if status.dwCurrentState != Services::SERVICE_RUNNING
|| status.dwProcessId == 0
|| status.dwProcessId != pipe_process_id
{
bail!("the provisioning pipe server does not match the running service");
}
Ok(())
}
struct ServiceHandle(SC_HANDLE);
impl Drop for ServiceHandle {
fn drop(&mut self) {
if self.0 != 0 {
unsafe { Services::CloseServiceHandle(self.0) };
}
}
}

View File

@@ -1,5 +1,6 @@
//! Dedicated protocol exchanged with the Windows sandbox provisioning service.
use crate::WindowsSandboxProvisioningSettings;
use anyhow::Result;
use serde::Deserialize;
use serde::Serialize;
@@ -37,9 +38,16 @@ pub enum ProvisioningMessage {
#[serde(deny_unknown_fields)]
pub struct SandboxProvisioningRequest {
pub codex_home: String,
pub http_port: Option<u16>,
pub socks_port: Option<u16>,
pub allow_local_binding: bool,
pub settings: WindowsSandboxProvisioningSettings,
pub listeners: WindowsSandboxProxyListeners,
}
/// Known proxy protocols used for managed-policy validation, separate from firewall settings.
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct WindowsSandboxProxyListeners {
pub http_ports: Vec<u16>,
pub socks_ports: Vec<u16>,
}
/// Result returned by the sandbox provisioning service.

View File

@@ -726,7 +726,7 @@ impl SandboxNetworkIdentity {
}
}
const PROXY_ENV_KEYS: &[&str] = &[
pub(crate) const PROXY_ENV_KEYS: &[&str] = &[
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
@@ -793,7 +793,7 @@ pub(crate) fn proxy_ports_from_env(env_map: &HashMap<String, String>) -> Vec<u16
ports.into_iter().collect()
}
fn loopback_proxy_port_from_url(url: &str) -> Option<u16> {
pub(crate) fn loopback_proxy_port_from_url(url: &str) -> Option<u16> {
let authority = url.trim().split_once("://")?.1.split('/').next()?;
let host_port = authority.rsplit_once('@').map_or(authority, |(_, hp)| hp);
@@ -1360,6 +1360,8 @@ mod tests {
use super::profile_read_roots;
use super::proxy_ports_from_env;
use super::verify_setup_completed;
use crate::WindowsSandboxProvisioningSettings;
use crate::WindowsSandboxProxyListeners;
use crate::helper_materialization::BIN_DIRNAME;
use crate::helper_materialization::RESOURCES_DIRNAME;
use crate::helper_materialization::helper_bin_dir;
@@ -1822,6 +1824,26 @@ mod tests {
);
assert_eq!(proxy_ports_from_env(&env), vec![1081, 8080, 43128, 43129]);
assert_eq!(
WindowsSandboxProvisioningSettings::from_environment(
&PermissionProfile::workspace_write(),
&env,
),
WindowsSandboxProvisioningSettings {
proxy_ports: vec![1081, 8080, 43128, 43129],
allow_local_binding: false,
}
);
assert_eq!(
WindowsSandboxProxyListeners::from_environment(
&PermissionProfile::workspace_write(),
&env,
),
WindowsSandboxProxyListeners {
http_ports: vec![8080],
socks_ports: vec![1081],
}
);
}
#[test]
@@ -1843,6 +1865,20 @@ mod tests {
allow_local_binding: false,
}
);
let permission_profile = PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
);
assert_eq!(
WindowsSandboxProvisioningSettings::from_environment(&permission_profile, &env),
WindowsSandboxProvisioningSettings::default()
);
assert_eq!(
WindowsSandboxProxyListeners::from_environment(&permission_profile, &env),
WindowsSandboxProxyListeners::default()
);
}
#[test]
@@ -1862,12 +1898,104 @@ mod tests {
);
assert_eq!(
offline_proxy_settings_from_env(&env, super::SandboxNetworkIdentity::Offline),
super::OfflineProxySettings {
WindowsSandboxProvisioningSettings::from_environment(
&PermissionProfile::workspace_write(),
&env,
),
WindowsSandboxProvisioningSettings {
proxy_ports: vec![1081, 8080],
allow_local_binding: true,
}
);
assert_eq!(
WindowsSandboxProxyListeners::from_environment(
&PermissionProfile::workspace_write(),
&env,
),
WindowsSandboxProxyListeners {
http_ports: vec![8080],
socks_ports: vec![1081],
}
);
env.remove("ALL_PROXY");
for (all_proxy, socks_ports) in [
("HTTP://localhost:8080", vec![]),
("socks5h://[::1]:8080", vec![8080]),
] {
env.insert("all_proxy".to_string(), all_proxy.to_string());
assert_eq!(
WindowsSandboxProxyListeners::from_environment(
&PermissionProfile::workspace_write(),
&env,
),
WindowsSandboxProxyListeners {
http_ports: vec![8080],
socks_ports,
}
);
}
}
#[test]
fn provisioning_settings_preserve_all_inherited_proxy_ports() {
for (proxy_env, proxy_ports, http_ports, socks_ports) in [
(
vec![("ALL_PROXY", "socks5h://127.0.0.1:1081")],
vec![1081],
vec![],
vec![1081],
),
(
vec![
("HTTP_PROXY", "http://127.0.0.1:8080"),
("HTTPS_PROXY", "http://127.0.0.1:3128"),
],
vec![3128, 8080],
vec![3128, 8080],
vec![],
),
(
vec![
("HTTP_PROXY", "http://127.0.0.1:8080"),
(WINDOWS_SANDBOX_PROXY_PORTS_ENV_KEY, "8080,1081"),
],
vec![1081, 8080],
vec![8080],
vec![],
),
(
vec![("ALL_PROXY", "ftp://127.0.0.1:3128")],
vec![3128],
vec![],
vec![],
),
] {
let env = proxy_env
.into_iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect();
assert_eq!(
WindowsSandboxProvisioningSettings::from_environment(
&PermissionProfile::workspace_write(),
&env,
),
WindowsSandboxProvisioningSettings {
proxy_ports,
allow_local_binding: false,
}
);
assert_eq!(
WindowsSandboxProxyListeners::from_environment(
&PermissionProfile::workspace_write(),
&env,
),
WindowsSandboxProxyListeners {
http_ports,
socks_ports,
}
);
}
}
#[test]