From c4ea7294b989827862eed6972d130233459161f4 Mon Sep 17 00:00:00 2001 From: johnl-oai Date: Wed, 2 Sep 2026 18:40:48 +0000 Subject: [PATCH] Prepare managed policy validation for Windows sandbox provisioning (#42344) ## What changed - Add a Windows sandbox service validator that loads managed configuration while impersonating the provisioning client. - Reject elevated sandbox or network settings that conflict with managed requirements, including local binding and HTTP or SOCKS proxy-port restrictions. - Add a one-shot cloud configuration loader that bypasses the disk cache so policy checks use a fresh backend response without modifying cached configuration. - Keep the provisioning integration disabled until authenticated transport can supply the policy inputs. ## Testing - Cover elevated sandbox restrictions, disabled networking, local binding, proxy-port classification, malformed policy, impersonation failure, and cache bypass behavior. GitOrigin-RevId: b76aa8959c515c6507c2eaf61af768efa1305253 --- codex-rs/Cargo.lock | 5 + codex-rs/cloud-config/src/bundle_loader.rs | 55 ++-- codex-rs/cloud-config/src/lib.rs | 1 + codex-rs/cloud-config/src/service.rs | 37 ++- codex-rs/cloud-config/src/service_tests.rs | 68 +++++ codex-rs/windows-sandbox-service/Cargo.toml | 7 + codex-rs/windows-sandbox-service/src/lib.rs | 4 + .../src/machine_policy.rs | 138 +++++++++ .../src/machine_policy_tests.rs | 277 ++++++++++++++++++ 9 files changed, 562 insertions(+), 30 deletions(-) create mode 100644 codex-rs/windows-sandbox-service/src/machine_policy.rs create mode 100644 codex-rs/windows-sandbox-service/src/machine_policy_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index dba3abd7a1..9a31b5f104 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4977,7 +4977,12 @@ name = "codex-windows-sandbox-service" version = "0.0.0" dependencies = [ "anyhow", + "codex-cloud-config", + "codex-config", + "codex-core", "codex-windows-sandbox", + "tokio", + "toml 0.9.11+spec-1.1.0", "windows-sys 0.52.0", ] diff --git a/codex-rs/cloud-config/src/bundle_loader.rs b/codex-rs/cloud-config/src/bundle_loader.rs index d95f9bdb87..d38355189f 100644 --- a/codex-rs/cloud-config/src/bundle_loader.rs +++ b/codex-rs/cloud-config/src/bundle_loader.rs @@ -88,24 +88,45 @@ pub async fn cloud_config_bundle_loader_for_storage( auth_config: AuthConfig, enable_codex_api_key_env: bool, ) -> std::io::Result { - let auth_manager = - AuthManager::shared_from_auth_config(auth_config.clone(), enable_codex_api_key_env).await?; - Ok(cloud_config_bundle_loader_from_auth_config( - auth_config, - auth_manager, - )) + let service = + cloud_config_bundle_service_for_storage(auth_config, enable_codex_api_key_env).await?; + let (loader, refresh_task) = cloud_config_bundle_loader_for_service(service); + replace_refresh_task(refresher_task_slot(), refresh_task); + Ok(loader) } -fn cloud_config_bundle_loader_from_auth_config( +/// Fetches directly from the network on each load, without reading or writing +/// the disk cache or starting a background refresher. +pub async fn cloud_config_bundle_loader_for_storage_without_cache( auth_config: AuthConfig, - auth_manager: Arc, -) -> CloudConfigBundleLoader { - cloud_config_bundle_loader( - auth_manager, - auth_config - .chatgpt_base_url - .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()), - auth_config.codex_home, - auth_config.auth_route_config.http_client_factory().clone(), - ) + enable_codex_api_key_env: bool, +) -> std::io::Result { + let service = Arc::new( + cloud_config_bundle_service_for_storage(auth_config, enable_codex_api_key_env) + .await? + .without_cache(), + ); + Ok(CloudConfigBundleLoader::from_getter(move || { + let service = Arc::clone(&service); + async move { service.load_startup_bundle_with_timeout().await } + })) +} + +async fn cloud_config_bundle_service_for_storage( + auth_config: AuthConfig, + enable_codex_api_key_env: bool, +) -> std::io::Result> { + let auth_manager = + AuthManager::shared_from_auth_config(auth_config.clone(), enable_codex_api_key_env).await?; + Ok(CloudConfigBundleService::new( + auth_manager, + Arc::new(BackendBundleClient::new( + auth_config + .chatgpt_base_url + .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()), + auth_config.auth_route_config.http_client_factory().clone(), + )), + auth_config.codex_home, + CLOUD_CONFIG_BUNDLE_TIMEOUT, + )) } diff --git a/codex-rs/cloud-config/src/lib.rs b/codex-rs/cloud-config/src/lib.rs index 0e742f5522..3bcf30f4c3 100644 --- a/codex-rs/cloud-config/src/lib.rs +++ b/codex-rs/cloud-config/src/lib.rs @@ -12,3 +12,4 @@ mod validation; pub use bundle_loader::cloud_config_bundle_loader; pub use bundle_loader::cloud_config_bundle_loader_for_storage; +pub use bundle_loader::cloud_config_bundle_loader_for_storage_without_cache; diff --git a/codex-rs/cloud-config/src/service.rs b/codex-rs/cloud-config/src/service.rs index faafe895a1..25326c4d5b 100644 --- a/codex-rs/cloud-config/src/service.rs +++ b/codex-rs/cloud-config/src/service.rs @@ -2,6 +2,7 @@ //! //! Startup loads a shared bundle from cache or backend, and background refresh //! updates both the on-disk cache and the bundle observed by future config loads. +//! One-shot network loads can disable disk-cache reads and writes. use crate::backend::BundleClient; use crate::backend::BundleRequestError; @@ -78,6 +79,7 @@ pub(crate) struct CloudConfigBundleService { auth_manager: Arc, client: Arc, cache: CloudConfigBundleCache, + cache_enabled: bool, codex_home: AbsolutePathBuf, timeout: Duration, latest_bundle: OnceCell, CloudConfigBundleLoadError>>>, @@ -98,12 +100,18 @@ where auth_manager, client, cache: CloudConfigBundleCache::new(codex_home.clone()), + cache_enabled: true, codex_home, timeout, latest_bundle: OnceCell::new(), } } + pub(crate) fn without_cache(mut self) -> Self { + self.cache_enabled = false; + self + } + pub(crate) async fn get_latest( &self, ) -> Result, CloudConfigBundleLoadError> { @@ -182,15 +190,17 @@ where return Ok(None); } - // Startup prefers a valid, identity-matched cache entry. The backend is - // only consulted on cache miss or invalid cache contents. - let (chatgpt_user_id, account_id) = auth_identity(&auth); - match self - .load_valid_cached_bundle(chatgpt_user_id.as_deref(), account_id.as_deref()) - .await - { - CachedBundleLookup::Hit(bundle) => return Ok(bundle), - CachedBundleLookup::Miss => {} + if self.cache_enabled { + // Startup prefers a valid, identity-matched cache entry. The backend is + // only consulted on cache miss or invalid cache contents. + let (chatgpt_user_id, account_id) = auth_identity(&auth); + match self + .load_valid_cached_bundle(chatgpt_user_id.as_deref(), account_id.as_deref()) + .await + { + CachedBundleLookup::Hit(bundle) => return Ok(bundle), + CachedBundleLookup::Miss => {} + } } self.fetch_remote_bundle_and_update_cache_with_retries(auth, "startup") @@ -322,10 +332,11 @@ where } let (chatgpt_user_id, account_id) = auth_identity(auth); - if let Err(err) = self - .cache - .save(chatgpt_user_id, account_id, bundle.clone()) - .await + if self.cache_enabled + && let Err(err) = self + .cache + .save(chatgpt_user_id, account_id, bundle.clone()) + .await { tracing::warn!( error = %err, diff --git a/codex-rs/cloud-config/src/service_tests.rs b/codex-rs/cloud-config/src/service_tests.rs index d1d0c347d2..3760694b1e 100644 --- a/codex-rs/cloud-config/src/service_tests.rs +++ b/codex-rs/cloud-config/src/service_tests.rs @@ -634,6 +634,74 @@ async fn get_bundle_uses_cache_when_valid() { assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 0); } +#[tokio::test] +async fn get_bundle_without_cache_ignores_and_preserves_valid_cache() { + let codex_home = tempdir().expect("tempdir"); + let cache = create_test_cache(codex_home.path()); + cache + .save( + Some("user-12345".to_string()), + Some("account-12345".to_string()), + CloudConfigBundle::default(), + ) + .await + .expect("write empty cache"); + let cached_bytes = std::fs::read(cache.path()).expect("read cache"); + let bundle = test_bundle(); + let fetcher = Arc::new(StaticBundleClient::new(bundle.clone())); + let service = CloudConfigBundleService::new( + auth_manager_with_plan("business").await, + fetcher.clone(), + codex_home.path().to_path_buf(), + CLOUD_CONFIG_BUNDLE_TIMEOUT, + ) + .without_cache(); + + assert_eq!( + service.load_startup_bundle_with_timeout().await, + Ok(Some(bundle)) + ); + assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 1); + assert_eq!( + std::fs::read(cache.path()).expect("read unchanged cache"), + cached_bytes + ); +} + +#[tokio::test(start_paused = true)] +async fn get_bundle_without_cache_fails_closed_on_request_failure() { + let codex_home = tempdir().expect("tempdir"); + create_test_cache(codex_home.path()) + .save( + Some("user-12345".to_string()), + Some("account-12345".to_string()), + test_bundle(), + ) + .await + .expect("write valid cache"); + let fetcher = Arc::new(SequenceBundleClient::new(vec![ + Err(request_error()); + CLOUD_CONFIG_BUNDLE_MAX_ATTEMPTS + ])); + let service = CloudConfigBundleService::new( + auth_manager_with_plan("business").await, + fetcher.clone(), + codex_home.path().to_path_buf(), + CLOUD_CONFIG_BUNDLE_TIMEOUT, + ) + .without_cache(); + + let err = service + .load_startup_bundle_with_timeout() + .await + .expect_err("request failure must not fall back to the cache"); + assert_eq!(err.code(), CloudConfigBundleLoadErrorCode::RequestFailed); + assert_eq!( + fetcher.request_count.load(Ordering::SeqCst), + CLOUD_CONFIG_BUNDLE_MAX_ATTEMPTS + ); +} + #[tokio::test] async fn get_bundle_ignores_cache_for_different_auth_identity() { let codex_home = tempdir().expect("tempdir"); diff --git a/codex-rs/windows-sandbox-service/Cargo.toml b/codex-rs/windows-sandbox-service/Cargo.toml index 97632ad5b2..5d6baa76b7 100644 --- a/codex-rs/windows-sandbox-service/Cargo.toml +++ b/codex-rs/windows-sandbox-service/Cargo.toml @@ -18,7 +18,14 @@ workspace = true [dependencies] anyhow = { workspace = true } +codex-cloud-config = { workspace = true } +codex-config = { workspace = true } +codex-core = { workspace = true } codex-windows-sandbox = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } + +[dev-dependencies] +toml = { workspace = true } [target.'cfg(windows)'.dependencies.windows-sys] version = "0.52" diff --git a/codex-rs/windows-sandbox-service/src/lib.rs b/codex-rs/windows-sandbox-service/src/lib.rs index 56dc54d141..04dd1c2ea3 100644 --- a/codex-rs/windows-sandbox-service/src/lib.rs +++ b/codex-rs/windows-sandbox-service/src/lib.rs @@ -2,6 +2,10 @@ use anyhow::Result; #[cfg(windows)] mod ipc; +// Provisioning remains disabled until authenticated transport connects this policy. +#[cfg(windows)] +#[allow(dead_code)] +mod machine_policy; #[cfg(windows)] mod service; diff --git a/codex-rs/windows-sandbox-service/src/machine_policy.rs b/codex-rs/windows-sandbox-service/src/machine_policy.rs new file mode 100644 index 0000000000..85c43f78ae --- /dev/null +++ b/codex-rs/windows-sandbox-service/src/machine_policy.rs @@ -0,0 +1,138 @@ +//! Validates provisioning requests against the standard managed configuration layers. + +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use codex_cloud_config::cloud_config_bundle_loader_for_storage_without_cache; +use codex_config::ConfigLoadOptions; +use codex_config::ConfigRequirementsToml; +use codex_config::types::WindowsSandboxModeToml; +use codex_core::config::bootstrap_auth_config; +use codex_core::config::load_config_toml_with_layer_stack; +use codex_windows_sandbox::WindowsSandboxProvisioningSettings; +use codex_windows_sandbox::WindowsSandboxProxyListeners; +use std::path::Path; +use std::sync::Arc; +use tokio::sync::Notify; +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::Security::ImpersonateLoggedOnUser; + +pub(crate) fn validate_provisioning_settings( + codex_home: &Path, + settings: &WindowsSandboxProvisioningSettings, + listeners: &WindowsSandboxProxyListeners, + impersonation_token: HANDLE, +) -> Result<()> { + let impersonation_failure = Arc::new(Notify::new()); + let worker_impersonation_failure = Arc::clone(&impersonation_failure); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .on_thread_start(move || { + if unsafe { ImpersonateLoggedOnUser(impersonation_token) } == 0 { + let error = std::io::Error::last_os_error(); + worker_impersonation_failure.notify_one(); + panic!( + "failed to impersonate provisioning client on configuration runtime thread: {error}" + ); + } + }) + .build() + .context("start managed configuration runtime")?; + let requirements = runtime.block_on(async { + tokio::select! { + biased; + () = impersonation_failure.notified() => { + Err(anyhow::anyhow!("configuration runtime worker failed to impersonate the provisioning client")) + }, + result = async { + let mut bootstrap_config = load_config_toml_with_layer_stack( + codex_home, + /*cwd*/ None, + Vec::new(), + ConfigLoadOptions::default(), + ) + .await + .context("load bootstrap configuration")?; + // Use the default cloud-policy endpoint unless managed requirements override it. + bootstrap_config.config_toml.chatgpt_base_url = None; + let cloud_config_bundle = cloud_config_bundle_loader_for_storage_without_cache( + bootstrap_auth_config(codex_home, &bootstrap_config) + .context("resolve cloud configuration authentication")?, + /*enable_codex_api_key_env*/ false, + ) + .await + .context("initialize cloud configuration authentication")?; + let config = load_config_toml_with_layer_stack( + codex_home, + /*cwd*/ None, + Vec::new(), + ConfigLoadOptions { + cloud_config_bundle, + ..Default::default() + }, + ) + .await + .context("load managed configuration")?; + + Ok::<_, anyhow::Error>(config.config_layer_stack.requirements_toml().clone()) + } => result, + } + })?; + + validate_requirements(settings, listeners, &requirements) + .context("enforce managed provisioning requirements") +} + +fn validate_requirements( + settings: &WindowsSandboxProvisioningSettings, + listeners: &WindowsSandboxProxyListeners, + requirements: &ConfigRequirementsToml, +) -> Result<()> { + if requirements + .windows + .as_ref() + .and_then(|windows| windows.allowed_sandbox_implementations.as_ref()) + .is_some_and(|allowed| !allowed.contains(&WindowsSandboxModeToml::Elevated)) + { + bail!("managed policy does not permit the elevated Windows sandbox"); + } + + let Some(network) = requirements.network.as_ref() else { + return Ok(()); + }; + if network.enabled == Some(false) + && (settings.allow_local_binding || !settings.proxy_ports.is_empty()) + { + bail!("managed policy disables sandbox network access"); + } + if settings.allow_local_binding && network.allow_local_binding == Some(false) { + bail!("managed policy does not permit local network binding"); + } + if let Some(required) = network.http_port + && let Some(actual) = listeners.http_ports.iter().find(|port| **port != required) + { + bail!("managed policy does not permit HTTP proxy port {actual}"); + } + if let Some(required) = network.socks_port + && let Some(actual) = listeners.socks_ports.iter().find(|port| **port != required) + { + bail!("managed policy does not permit SOCKS proxy port {actual}"); + } + // Omitting listener identities must not bypass managed port restrictions. + if network.http_port.is_some() || network.socks_port.is_some() { + for port in &settings.proxy_ports { + if !listeners.http_ports.contains(port) + && !listeners.socks_ports.contains(port) + && network.http_port != Some(*port) + && network.socks_port != Some(*port) + { + bail!("managed policy does not permit unclassified proxy port {port}"); + } + } + } + Ok(()) +} + +#[cfg(test)] +#[path = "machine_policy_tests.rs"] +mod tests; diff --git a/codex-rs/windows-sandbox-service/src/machine_policy_tests.rs b/codex-rs/windows-sandbox-service/src/machine_policy_tests.rs new file mode 100644 index 0000000000..17b92359cc --- /dev/null +++ b/codex-rs/windows-sandbox-service/src/machine_policy_tests.rs @@ -0,0 +1,277 @@ +use anyhow::Result; +use codex_config::ConfigRequirementsToml; +use codex_windows_sandbox::WindowsSandboxProvisioningSettings; +use codex_windows_sandbox::WindowsSandboxProxyListeners; + +fn validate_requirements( + settings: &WindowsSandboxProvisioningSettings, + listeners: WindowsSandboxProxyListeners, + contents: &str, +) -> Result<()> { + let requirements: ConfigRequirementsToml = toml::from_str(contents)?; + super::validate_requirements(settings, &listeners, &requirements) +} + +#[test] +fn runtime_worker_impersonation_failure_rejects_provisioning() { + let error = super::validate_provisioning_settings( + &std::env::temp_dir(), + &WindowsSandboxProvisioningSettings::default(), + &WindowsSandboxProxyListeners::default(), + /*impersonation_token*/ 0, + ) + .expect_err("runtime workers must not load configuration without impersonating the client"); + + assert!(error.to_string().contains("failed to impersonate")); +} + +#[test] +fn unmanaged_policy_allows_default_and_requested_network_settings() -> Result<()> { + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + "", + )?; + validate_requirements( + &WindowsSandboxProvisioningSettings { + proxy_ports: vec![3128, 8080, 9000], + allow_local_binding: true, + }, + WindowsSandboxProxyListeners { + http_ports: vec![3128], + socks_ports: vec![8080], + }, + "allowed_sandbox_modes = [\"workspace-write\"]", + ) +} + +#[test] +fn elevated_sandbox_is_allowed_when_included() -> Result<()> { + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + "[windows]\nallowed_sandbox_implementations = [\"unelevated\", \"elevated\"]", + ) +} + +#[test] +fn elevated_sandbox_is_rejected_when_prohibited_or_empty() { + for implementations in ["[\"unelevated\"]", "[]"] { + let policy = format!("[windows]\nallowed_sandbox_implementations = {implementations}"); + assert!( + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + &policy, + ) + .is_err() + ); + } +} + +#[test] +fn forbidden_local_binding_is_rejected() { + let settings = WindowsSandboxProvisioningSettings { + proxy_ports: Vec::new(), + allow_local_binding: true, + }; + assert!( + validate_requirements( + &settings, + WindowsSandboxProxyListeners::default(), + "[experimental_network]\nallow_local_binding = false" + ) + .is_err() + ); +} + +#[test] +fn disabled_network_rejects_proxy_ports_and_local_binding() -> Result<()> { + let policy = "[experimental_network]\nenabled = false"; + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + policy, + )?; + for settings in [ + WindowsSandboxProvisioningSettings { + proxy_ports: vec![3128], + allow_local_binding: false, + }, + WindowsSandboxProvisioningSettings { + proxy_ports: Vec::new(), + allow_local_binding: true, + }, + ] { + assert!( + validate_requirements(&settings, WindowsSandboxProxyListeners::default(), policy) + .is_err() + ); + } + Ok(()) +} + +#[test] +fn enabled_network_allows_unrestricted_proxy_ports() -> Result<()> { + validate_requirements( + &WindowsSandboxProvisioningSettings { + proxy_ports: vec![3128, 8080, 8081, 9000], + allow_local_binding: false, + }, + WindowsSandboxProxyListeners { + http_ports: vec![3128, 8080], + socks_ports: vec![8081], + }, + "[experimental_network]\nenabled = true", + ) +} + +#[test] +fn disabled_local_binding_is_allowed_when_policy_permits_binding() -> Result<()> { + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + "[experimental_network]\nallow_local_binding = true", + ) +} + +#[test] +fn managed_proxy_ports_allow_only_configured_ports() -> Result<()> { + let policy = "[experimental_network]\nhttp_port = 3128\nsocks_port = 1080"; + let allowed = WindowsSandboxProvisioningSettings { + proxy_ports: vec![1080, 3128], + allow_local_binding: false, + }; + for listeners in [ + WindowsSandboxProxyListeners { + http_ports: vec![3128], + socks_ports: vec![1080], + }, + WindowsSandboxProxyListeners::default(), + ] { + validate_requirements(&allowed, listeners, policy)?; + } + let prohibited = WindowsSandboxProvisioningSettings { + proxy_ports: vec![1080, 3128, 8080], + allow_local_binding: false, + }; + for listeners in [ + WindowsSandboxProxyListeners { + http_ports: vec![3128, 8080], + socks_ports: vec![1080], + }, + WindowsSandboxProxyListeners { + http_ports: vec![3128], + socks_ports: vec![1080], + }, + WindowsSandboxProxyListeners::default(), + ] { + assert!(validate_requirements(&prohibited, listeners, policy).is_err()); + } + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + policy, + ) +} + +#[test] +fn managed_http_port_does_not_restrict_the_unmanaged_socks_listener() -> Result<()> { + validate_requirements( + &WindowsSandboxProvisioningSettings { + proxy_ports: vec![3128, 8081], + allow_local_binding: false, + }, + WindowsSandboxProxyListeners { + http_ports: vec![3128], + socks_ports: vec![8081], + }, + "[experimental_network]\nhttp_port = 3128", + ) +} + +#[test] +fn managed_socks_port_does_not_restrict_the_unmanaged_http_listener() -> Result<()> { + validate_requirements( + &WindowsSandboxProvisioningSettings { + proxy_ports: vec![1080, 3128], + allow_local_binding: false, + }, + WindowsSandboxProxyListeners { + http_ports: vec![3128], + socks_ports: vec![1080], + }, + "[experimental_network]\nsocks_port = 1080", + ) +} + +#[test] +fn managed_proxy_ports_reject_swapped_listener_roles() { + assert!( + validate_requirements( + &WindowsSandboxProvisioningSettings { + proxy_ports: vec![1080, 3128], + allow_local_binding: false, + }, + WindowsSandboxProxyListeners { + http_ports: vec![1080], + socks_ports: vec![3128], + }, + "[experimental_network]\nhttp_port = 3128\nsocks_port = 1080", + ) + .is_err() + ); +} + +#[test] +fn managed_socks_port_rejects_a_mismatched_socks_listener() { + assert!( + validate_requirements( + &WindowsSandboxProvisioningSettings { + proxy_ports: vec![1080, 3128, 8081], + allow_local_binding: false, + }, + WindowsSandboxProxyListeners { + http_ports: vec![3128], + socks_ports: vec![1080, 8081], + }, + "[experimental_network]\nhttp_port = 3128\nsocks_port = 1080", + ) + .is_err() + ); +} + +#[test] +fn managed_socks_port_does_not_require_a_disabled_socks_listener() -> Result<()> { + validate_requirements( + &WindowsSandboxProvisioningSettings { + proxy_ports: vec![3128], + allow_local_binding: false, + }, + WindowsSandboxProxyListeners { + http_ports: vec![3128], + socks_ports: Vec::new(), + }, + "[experimental_network]\nhttp_port = 3128\nsocks_port = 1080", + ) +} + +#[test] +fn malformed_machine_policy_fails_closed() { + assert!( + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + "[experimental_network]\nhttp_port = \"invalid\"" + ) + .is_err() + ); + assert!( + validate_requirements( + &WindowsSandboxProvisioningSettings::default(), + WindowsSandboxProxyListeners::default(), + "[windows]\nallowed_sandbox_implementations = [\"invalid\"]" + ) + .is_err() + ); +}