Select the elevated Windows sandbox for network proxies (#33445)

## Why

Windows firewall enforcement is tied to the logon-user sandbox identities. Proxy-enforced commands therefore need the elevated backend even when the configured sandbox mode is the default restricted-token mode.

## What changed

- Select the elevated Windows sandbox backend whenever a command uses the managed network proxy.
- Remove configuration and startup checks that required users or managed requirements to select the elevated sandbox explicitly.
- Start and expose managed network proxy state consistently on Windows.

## Testing

- Verify that proxy enforcement selects the elevated backend for the restricted-token sandbox mode.

GitOrigin-RevId: e01ac052587721c468a8fbcf4e2c7c5a185a3e05
This commit is contained in:
iceweasel-oai
2026-07-15 23:19:00 +00:00
committed by copyberry
parent 7d1218a997
commit 4bc2c723ef
23 changed files with 58 additions and 496 deletions
+2 -98
View File
@@ -99,92 +99,6 @@ use serde::Deserialize;
use tempfile::tempdir;
use super::*;
async fn load_config_with_elevated_only_windows_sandbox_requirement(
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
) -> std::io::Result<Config> {
let requirements_toml = ConfigRequirementsToml {
windows: Some(codex_config::WindowsRequirementsToml {
allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]),
}),
..Default::default()
};
let mut requirements_with_sources = codex_config::ConfigRequirementsWithSources::default();
requirements_with_sources.merge_unset_fields(
codex_config::RequirementSource::Unknown,
requirements_toml.clone(),
);
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
let config_layer_stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)?;
Config::load_config_with_layer_stack(
codex_exec_server::LOCAL_FS.as_ref(),
cfg,
overrides,
codex_home,
config_layer_stack,
)
.await
}
#[test]
fn windows_network_proxy_validation() {
let elevated_only = ConfigRequirementsToml {
windows: Some(codex_config::WindowsRequirementsToml {
allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]),
}),
..Default::default()
};
for (is_windows, requirements, network_proxy_enabled, allowed) in [
(true, ConfigRequirementsToml::default(), true, false),
(true, ConfigRequirementsToml::default(), false, true),
(true, elevated_only, true, true),
(
true,
ConfigRequirementsToml {
windows: Some(codex_config::WindowsRequirementsToml {
allowed_sandbox_implementations: Some(vec![
WindowsSandboxModeToml::Elevated,
WindowsSandboxModeToml::Unelevated,
]),
}),
..Default::default()
},
true,
false,
),
(false, ConfigRequirementsToml::default(), true, true),
] {
assert_eq!(
validate_windows_network_proxy_requirements_for_platform(
is_windows,
&requirements,
network_proxy_enabled,
)
.is_ok(),
allowed
);
}
for (is_windows, sandbox_level, network_proxy_enabled, allowed) in [
(true, WindowsSandboxLevel::Disabled, true, false),
(true, WindowsSandboxLevel::RestrictedToken, true, false),
(true, WindowsSandboxLevel::Elevated, true, true),
(true, WindowsSandboxLevel::RestrictedToken, false, true),
(false, WindowsSandboxLevel::RestrictedToken, true, true),
] {
assert_eq!(
validate_windows_sandbox_network_proxy_compatibility_for_platform(
is_windows,
sandbox_level,
network_proxy_enabled,
)
.is_ok(),
allowed
);
}
}
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use core_test_support::TempDirExt;
@@ -1652,7 +1566,7 @@ async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> s
..Default::default()
},
};
let config = load_config_with_elevated_only_windows_sandbox_requirement(
let config = Config::load_from_base_config_with_overrides(
base_config,
ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
@@ -1697,13 +1611,6 @@ sandbox = "elevated"
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"[windows]
allowed_sandbox_implementations = ["elevated"]
"#,
),
)
.cli_overrides(vec![
(
"features.network_proxy.enabled".to_string(),
@@ -1836,9 +1743,6 @@ async fn experimental_network_requirements_enable_proxy_without_feature() -> std
r#"
[experimental_network]
enabled = true
[windows]
allowed_sandbox_implementations = ["elevated"]
"#,
),
)
@@ -1862,7 +1766,7 @@ allowed_sandbox_implementations = ["elevated"]
async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let config = load_config_with_elevated_only_windows_sandbox_requirement(
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
features: Some(toml::from_str("network_proxy = true").expect("valid features")),
default_permissions: Some("dev".to_string()),
+6 -109
View File
@@ -601,56 +601,6 @@ fn build_network_proxy_spec(
})
}
pub fn validate_windows_sandbox_network_proxy_compatibility(
windows_sandbox_level: WindowsSandboxLevel,
network_proxy_configured: bool,
) -> std::io::Result<()> {
validate_windows_sandbox_network_proxy_compatibility_for_platform(
cfg!(target_os = "windows"),
windows_sandbox_level,
network_proxy_configured,
)
}
fn validate_windows_sandbox_network_proxy_compatibility_for_platform(
is_windows: bool,
windows_sandbox_level: WindowsSandboxLevel,
network_proxy_configured: bool,
) -> std::io::Result<()> {
if is_windows
&& network_proxy_configured
&& windows_sandbox_level != WindowsSandboxLevel::Elevated
{
return Err(ConstraintError::NetworkProxyIncompatibleWithUnelevatedWindowsSandbox.into());
}
Ok(())
}
fn validate_windows_network_proxy_requirements(
requirements: &ConfigRequirementsToml,
network_proxy_configured: bool,
) -> std::io::Result<()> {
validate_windows_network_proxy_requirements_for_platform(
cfg!(target_os = "windows"),
requirements,
network_proxy_configured,
)
}
fn validate_windows_network_proxy_requirements_for_platform(
is_windows: bool,
requirements: &ConfigRequirementsToml,
network_proxy_configured: bool,
) -> std::io::Result<()> {
if is_windows
&& network_proxy_configured
&& !requirements.allows_only_elevated_windows_sandbox()
{
return Err(ConstraintError::NetworkProxyRequiresElevatedWindowsSandboxRequirement.into());
}
Ok(())
}
/// Configured thread persistence backend.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ThreadStoreConfig {
@@ -1295,12 +1245,6 @@ impl AuthManagerConfig for Config {
}
}
#[derive(Clone, Copy)]
enum ConfigBuildPhase {
CloudConfigBootstrap,
Authoritative,
}
#[derive(Clone, Default)]
pub struct ConfigBuilder {
codex_home: Option<PathBuf>,
@@ -1359,15 +1303,10 @@ impl ConfigBuilder {
pub async fn build(self) -> std::io::Result<Config> {
// Keep the large config-loading future off small runtime thread stacks.
Box::pin(self.build_inner(ConfigBuildPhase::Authoritative)).await
Box::pin(self.build_inner()).await
}
/// Builds the preliminary config needed to initialize cloud config loading.
pub async fn build_for_cloud_config_bootstrap(self) -> std::io::Result<Config> {
Box::pin(self.build_inner(ConfigBuildPhase::CloudConfigBootstrap)).await
}
async fn build_inner(self, build_phase: ConfigBuildPhase) -> std::io::Result<Config> {
async fn build_inner(self) -> std::io::Result<Config> {
let Self {
codex_home,
cli_overrides,
@@ -1452,13 +1391,12 @@ impl ConfigBuilder {
config_layer_stack.requirements().clone(),
config_layer_stack.requirements_toml().clone(),
)?;
let mut config = Config::load_config_with_layer_stack_and_options(
let mut config = Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
lock_config_toml,
harness_overrides,
codex_home,
lock_config_layer_stack,
build_phase,
)
.await?;
config.config_lock_toml = Some(Arc::new(expected_lock_config));
@@ -1467,13 +1405,12 @@ impl ConfigBuilder {
save_fields_resolved_from_model_catalog;
return Ok(config);
}
Config::load_config_with_layer_stack_and_options(
Config::load_config_with_layer_stack(
LOCAL_FS.as_ref(),
config_toml,
harness_overrides,
codex_home,
config_layer_stack,
build_phase,
)
.await
}
@@ -3079,25 +3016,6 @@ impl Config {
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
config_layer_stack: ConfigLayerStack,
) -> std::io::Result<Self> {
Self::load_config_with_layer_stack_and_options(
fs,
cfg,
overrides,
codex_home,
config_layer_stack,
ConfigBuildPhase::Authoritative,
)
.await
}
async fn load_config_with_layer_stack_and_options(
fs: &dyn ExecutorFileSystem,
cfg: ConfigToml,
overrides: ConfigOverrides,
codex_home: AbsolutePathBuf,
config_layer_stack: ConfigLayerStack,
build_phase: ConfigBuildPhase,
) -> std::io::Result<Self> {
// Keep the large config-construction future off small test thread stacks.
Box::pin(async move {
@@ -3895,17 +3813,6 @@ impl Config {
network_requirements,
&network_permission_profile,
)?;
if matches!(build_phase, ConfigBuildPhase::Authoritative) {
let network_proxy_enabled = network.as_ref().is_some_and(NetworkProxySpec::enabled);
validate_windows_network_proxy_requirements(
config_layer_stack.requirements_toml(),
network_proxy_enabled,
)?;
validate_windows_sandbox_network_proxy_compatibility(
windows_sandbox_level,
network_proxy_enabled,
)?;
}
let mut helper_readable_roots = get_readable_roots_required_for_codex_runtime(
&codex_home,
zsh_path.as_ref(),
@@ -4299,21 +4206,11 @@ impl Config {
NetworkProxyConfig::default()
};
let network = build_network_proxy_spec(
build_network_proxy_spec(
configured_network_proxy_config,
self.config_layer_stack.requirements().network.clone(),
permission_profile,
)?;
let network_proxy_enabled = network.as_ref().is_some_and(NetworkProxySpec::enabled);
validate_windows_network_proxy_requirements(
self.config_layer_stack.requirements_toml(),
network_proxy_enabled,
)?;
validate_windows_sandbox_network_proxy_compatibility(
WindowsSandboxLevel::from_config(self),
network_proxy_enabled,
)?;
Ok(network)
)
}
pub fn bundled_skills_enabled(&self) -> bool {
@@ -73,7 +73,7 @@ impl ConfigReloader for StaticNetworkProxyReloader {
}
impl NetworkProxySpec {
pub fn enabled(&self) -> bool {
pub(crate) fn enabled(&self) -> bool {
self.config.enabled
}
@@ -215,26 +215,22 @@ pub(crate) fn push_xml_escaped_text(rendered: &mut String, value: &str) {
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct NetworkContext {
enabled: bool,
allowed_domains: Vec<String>,
denied_domains: Vec<String>,
}
impl NetworkContext {
pub(crate) fn new(enabled: bool, allowed: Vec<String>, denied: Vec<String>) -> Self {
pub(crate) fn new(allowed_domains: Vec<String>, denied_domains: Vec<String>) -> Self {
Self {
enabled,
allowed_domains: allowed,
denied_domains: denied,
allowed_domains,
denied_domains,
}
}
pub(super) fn render(&self) -> String {
let mut rendered = format!("<network enabled=\"{}\">", self.enabled);
if self.enabled {
Self::push_rendered_domain_element(&mut rendered, "allowed", &self.allowed_domains);
Self::push_rendered_domain_element(&mut rendered, "denied", &self.denied_domains);
}
let mut rendered = "<network enabled=\"true\">".to_string();
Self::push_rendered_domain_element(&mut rendered, "allowed", &self.allowed_domains);
Self::push_rendered_domain_element(&mut rendered, "denied", &self.denied_domains);
rendered.push_str("</network>");
rendered
}
@@ -374,7 +374,6 @@ fn network_from_turn_context(turn_context: &TurnContext) -> Option<NetworkContex
.as_ref()?;
Some(NetworkContext::new(
turn_context.network.is_some(),
network
.domains
.as_ref()
@@ -115,7 +115,6 @@ fn serialize_environment_context_with_foreign_windows_cwd() {
#[test]
fn serialize_environment_context_with_network() {
let network = NetworkContext::new(
/*enabled*/ true,
vec!["api.example.com".to_string(), "*.openai.com".to_string()],
vec!["blocked.example.com".to_string()],
);
@@ -60,7 +60,6 @@ fn snapshots() -> Result<()> {
current_date: Some("2026-06-19".to_string()),
timezone: Some("UTC".to_string()),
network: Some(NetworkContext::new(
/*enabled*/ true,
vec!["old.example.com".to_string()],
vec![],
)),
@@ -74,7 +73,6 @@ fn snapshots() -> Result<()> {
current_date: Some("2026-06-20".to_string()),
timezone: Some("America/Los_Angeles".to_string()),
network: Some(NetworkContext::new(
/*enabled*/ true,
vec!["new.example.com".to_string()],
vec!["blocked.example.com".to_string()],
)),
+3 -9
View File
@@ -17,7 +17,6 @@ use tokio::io::BufReader;
use tokio::process::Child;
use tokio_util::sync::CancellationToken;
use crate::config::validate_windows_sandbox_network_proxy_compatibility;
use crate::sandboxing::ExecOptions;
use crate::sandboxing::ExecRequest;
use crate::sandboxing::SandboxPermissions;
@@ -412,13 +411,10 @@ pub fn build_exec_request(
)
})
.map_err(CodexErr::from)?;
validate_windows_sandbox_network_proxy_compatibility(
let use_windows_elevated_backend = windows_sandbox_uses_elevated_backend(
exec_req.windows_sandbox_level,
exec_req.network.is_some(),
)
.map_err(CodexErr::Io)?;
let use_windows_elevated_backend =
windows_sandbox_uses_elevated_backend(exec_req.windows_sandbox_level);
);
exec_req.windows_sandbox_filesystem_overrides = if use_windows_elevated_backend {
resolve_windows_elevated_filesystem_overrides(
exec_req.sandbox,
@@ -662,9 +658,7 @@ async fn exec_windows_sandbox(
let command_path = command.first().cloned();
let sandbox_level = windows_sandbox_level;
let proxy_enforced = network.is_some();
validate_windows_sandbox_network_proxy_compatibility(sandbox_level, proxy_enforced)
.map_err(CodexErr::Io)?;
let use_elevated = windows_sandbox_uses_elevated_backend(sandbox_level);
let use_elevated = windows_sandbox_uses_elevated_backend(sandbox_level, proxy_enforced);
let additional_deny_write_paths = windows_sandbox_filesystem_overrides
.map(|overrides| overrides.additional_deny_write_paths.clone())
.unwrap_or_default();
+7 -1
View File
@@ -397,12 +397,18 @@ fn windows_restricted_token_supports_read_only_profiles() {
}
#[test]
fn windows_backend_selection_uses_configured_sandbox_level() {
fn windows_proxy_enforcement_uses_elevated_backend() {
assert!(!windows_sandbox_uses_elevated_backend(
WindowsSandboxLevel::RestrictedToken,
/*proxy_enforced*/ false,
));
assert!(windows_sandbox_uses_elevated_backend(
WindowsSandboxLevel::RestrictedToken,
/*proxy_enforced*/ true,
));
assert!(windows_sandbox_uses_elevated_backend(
WindowsSandboxLevel::Elevated,
/*proxy_enforced*/ false,
));
}
-4
View File
@@ -1044,10 +1044,6 @@ impl Session {
spec
}
};
if cfg!(target_os = "windows") && !spec.enabled() {
self.services.network_proxy.store(None);
return;
}
if let Some(started_proxy) = self.services.network_proxy.load_full() {
if let Err(err) = spec.apply_to_started_proxy(started_proxy.as_ref()).await {
warn!("failed to refresh managed network proxy for sandbox change: {err}");
+1 -6
View File
@@ -987,12 +987,7 @@ impl Session {
)
});
let (network_proxy, session_network_proxy) =
if let Some(spec) = config
.permissions
.network
.as_ref()
.filter(|spec| !cfg!(target_os = "windows") || spec.enabled())
{
if let Some(spec) = config.permissions.network.as_ref() {
let current_exec_policy = exec_policy.current();
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
spec,
+14 -75
View File
@@ -934,10 +934,7 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
let (session, _turn_context) = make_session_and_context().await;
let initial_permission_profile = PermissionProfile::workspace_write();
let mut network_config = NetworkProxyConfig {
enabled: true,
..Default::default()
};
let mut network_config = NetworkProxyConfig::default();
network_config.set_allowed_domains(vec!["evil.com".to_string()]);
let requirements = NetworkConstraints {
domains: Some(NetworkDomainPermissionsToml {
@@ -1195,34 +1192,6 @@ async fn workspace_write_turns_continue_to_expose_managed_network_proxy() -> any
Ok(())
}
#[cfg(target_os = "windows")]
#[tokio::test]
async fn workspace_write_turns_do_not_expose_disabled_managed_network_proxy() -> anyhow::Result<()>
{
let permission_profile = PermissionProfile::workspace_write();
let network_spec = crate::config::NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
Some(NetworkConstraints {
enabled: Some(false),
..Default::default()
}),
&permission_profile,
)?;
let session = make_session_with_config(move |config| {
config
.permissions
.set_permission_profile(permission_profile)
.expect("test setup should allow permission profile");
config.permissions.network = Some(network_spec);
})
.await?;
let turn_context = session.new_default_turn().await;
assert!(turn_context.network.is_none());
Ok(())
}
#[tokio::test]
async fn user_shell_commands_do_not_inherit_managed_network_proxy() -> anyhow::Result<()> {
let permission_profile = PermissionProfile::workspace_write();
@@ -4746,17 +4715,9 @@ async fn active_profile_update_rebuilds_network_proxy_config() -> std::io::Resul
codex_home.path().join(codex_config::CONFIG_TOML_FILE),
toml::to_string(&base_config).expect("serialize config"),
)?;
let elevated_only_windows_sandbox_requirement = || {
codex_config::test_support::CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"[windows]
allowed_sandbox_implementations = ["elevated"]
"#,
)
};
let locked_config = Arc::new(
ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.cloud_config_bundle(elevated_only_windows_sandbox_requirement())
.harness_overrides(ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
..Default::default()
@@ -4775,7 +4736,6 @@ allowed_sandbox_implementations = ["elevated"]
);
let selected_config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.cloud_config_bundle(elevated_only_windows_sandbox_requirement())
.harness_overrides(ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
default_permissions: Some("web-enabled".to_string()),
@@ -7981,32 +7941,17 @@ async fn spawn_task_does_not_update_previous_turn_settings_for_non_run_turn_task
}
#[tokio::test]
async fn record_context_updates_emits_disabled_network_transition() {
let (session, mut previous_context) = make_session_and_context().await;
let permission_profile = PermissionProfile::workspace_write();
let network_config = NetworkProxyConfig {
enabled: true,
..Default::default()
};
let network_spec = crate::config::NetworkProxySpec::from_config_and_constraints(
network_config,
/*requirements*/ None,
&permission_profile,
)
.expect("build network proxy spec");
let started_proxy = network_spec
.start_proxy(
&permission_profile,
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
/*enable_network_approval_flow*/ false,
crate::config::NetworkProxyAuditMetadata::default(),
async fn record_context_updates_emits_environment_item_for_network_changes() {
let (session, previous_context) = make_session_and_context().await;
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
&session.services.models_manager,
)
.await
.expect("start network proxy");
previous_context.network = Some(started_proxy.proxy());
.await;
let mut config = (*previous_context.config).clone();
let mut config = (*current_context.config).clone();
let mut requirements = config.config_layer_stack.requirements().clone();
requirements.network = Some(Sourced::new(
NetworkConstraints {
@@ -8041,15 +7986,7 @@ async fn record_context_updates_emits_disabled_network_transition() {
config.config_layer_stack.requirements_toml().clone(),
)
.expect("rebuild config layer stack with network requirements");
previous_context.config = Arc::new(config);
let previous_context = Arc::new(previous_context);
let mut current_context = previous_context
.with_model(
previous_context.model_info.slug.clone(),
&session.services.models_manager,
)
.await;
current_context.network = None;
current_context.config = Arc::new(config);
let update_items =
record_context_update_items(&session, previous_context, current_context).await;
@@ -8058,7 +7995,9 @@ async fn record_context_updates_emits_disabled_network_transition() {
.into_iter()
.find(|text| text.contains("<environment_context>"))
.expect("environment update item should be emitted");
assert!(environment_update.contains("<network enabled=\"false\"></network>"));
assert!(environment_update.contains(
"<network enabled=\"true\"><allowed>api.example.com</allowed><denied>blocked.example.com</denied></network>"
));
}
#[tokio::test]
@@ -980,15 +980,6 @@ impl UnifiedExecProcessManager {
mut spawn_lifecycle: SpawnLifecycleHandle,
environment: &codex_exec_server::Environment,
) -> Result<UnifiedExecProcess, UnifiedExecError> {
#[cfg(target_os = "windows")]
if !environment.is_remote() {
crate::config::validate_windows_sandbox_network_proxy_compatibility(
request.windows_sandbox_level,
request.network.is_some(),
)
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
}
let inherited_fds = spawn_lifecycle.inherited_fds();
#[cfg(target_os = "windows")]
-3
View File
@@ -223,9 +223,6 @@ pub fn managed_network_requirements_loader() -> CloudConfigBundleLoader {
[experimental_network]
enabled = true
allow_local_binding = true
[windows]
allowed_sandbox_implementations = ["elevated"]
"#,
)
}