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
This commit is contained in:
johnl-oai
2026-09-02 18:40:48 +00:00
committed by copyberry
parent add870a4bf
commit c4ea7294b9
9 changed files with 562 additions and 30 deletions

5
codex-rs/Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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<CloudConfigBundleLoader> {
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<AuthManager>,
) -> 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<CloudConfigBundleLoader> {
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<CloudConfigBundleService<BackendBundleClient>> {
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,
))
}

View File

@@ -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;

View File

@@ -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<C> {
auth_manager: Arc<AuthManager>,
client: Arc<C>,
cache: CloudConfigBundleCache,
cache_enabled: bool,
codex_home: AbsolutePathBuf,
timeout: Duration,
latest_bundle: OnceCell<Mutex<Result<Option<CloudConfigBundle>, 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<Option<CloudConfigBundle>, 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,

View File

@@ -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");

View File

@@ -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"

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()
);
}