mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Require the elevated Windows sandbox for network proxies (#32857)
## Why Network proxy enforcement on Windows requires the elevated sandbox backend. Silently selecting that backend for an unelevated configuration makes the effective sandbox differ from the configured mode. ## What changed - Reject enabled network proxy configurations on Windows unless managed requirements allow only the elevated sandbox and `windows.sandbox` resolves to `"elevated"`. - Preserve the configured sandbox backend during execution and validate proxy compatibility at config loading, sandbox setup, and process launch boundaries. - Defer this validation during cloud-config bootstrap so authoritative managed requirements can be loaded first. - Do not start or expose disabled Windows proxies, and render transitions to disabled networking as `<network enabled="false"></network>`. ## Testing - Cover Windows requirement and sandbox compatibility matrices, bootstrap behavior, backend selection, disabled proxy handling, and environment-context updates. GitOrigin-RevId: bfa1dda98594e0db61883f8e7f65bd560e3453e6
This commit is contained in:
@@ -24,6 +24,12 @@ use toml::Value as TomlValue;
|
||||
use tracing::instrument;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ConfigLoadPhase {
|
||||
CloudConfigBootstrap,
|
||||
Authoritative,
|
||||
}
|
||||
|
||||
/// Shared app-server entry point for loading effective Codex configuration.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ConfigManager {
|
||||
@@ -148,6 +154,17 @@ impl ConfigManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn load_config_for_cloud_config_bootstrap(&self) -> std::io::Result<Config> {
|
||||
self.load_with_cli_overrides_inner(
|
||||
&self.current_cli_overrides(),
|
||||
/*request_overrides*/ None,
|
||||
ConfigOverrides::default(),
|
||||
/*fallback_cwd*/ None,
|
||||
ConfigLoadPhase::CloudConfigBootstrap,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn load_latest_config_for_thread(
|
||||
&self,
|
||||
thread_config: &Config,
|
||||
@@ -215,11 +232,29 @@ impl ConfigManager {
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub(crate) async fn load_with_cli_overrides(
|
||||
&self,
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
typesafe_overrides: ConfigOverrides,
|
||||
fallback_cwd: Option<PathBuf>,
|
||||
) -> std::io::Result<Config> {
|
||||
self.load_with_cli_overrides_inner(
|
||||
cli_overrides,
|
||||
request_overrides,
|
||||
typesafe_overrides,
|
||||
fallback_cwd,
|
||||
ConfigLoadPhase::Authoritative,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_with_cli_overrides_inner(
|
||||
&self,
|
||||
cli_overrides: &[(String, TomlValue)],
|
||||
request_overrides: Option<HashMap<String, serde_json::Value>>,
|
||||
mut typesafe_overrides: ConfigOverrides,
|
||||
fallback_cwd: Option<PathBuf>,
|
||||
load_phase: ConfigLoadPhase,
|
||||
) -> std::io::Result<Config> {
|
||||
let mut request_overrides = request_overrides.unwrap_or_default();
|
||||
if let Some(value) = request_overrides.remove("bypass_hook_trust") {
|
||||
@@ -240,7 +275,7 @@ impl ConfigManager {
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut config = codex_core::config::ConfigBuilder::default()
|
||||
let builder = codex_core::config::ConfigBuilder::default()
|
||||
.codex_home(self.codex_home.clone())
|
||||
.cli_overrides(merged_cli_overrides)
|
||||
.loader_overrides(self.loader_overrides.clone())
|
||||
@@ -248,9 +283,13 @@ impl ConfigManager {
|
||||
.harness_overrides(typesafe_overrides)
|
||||
.fallback_cwd(fallback_cwd)
|
||||
.cloud_config_bundle(self.current_cloud_config_bundle())
|
||||
.thread_config_loader(self.current_thread_config_loader())
|
||||
.build()
|
||||
.await?;
|
||||
.thread_config_loader(self.current_thread_config_loader());
|
||||
let mut config = match load_phase {
|
||||
ConfigLoadPhase::CloudConfigBootstrap => {
|
||||
builder.build_for_cloud_config_bootstrap().await?
|
||||
}
|
||||
ConfigLoadPhase::Authoritative => builder.build().await?,
|
||||
};
|
||||
self.apply_runtime_feature_enablement(&mut config);
|
||||
self.apply_arg0_paths(&mut config);
|
||||
Ok(config)
|
||||
|
||||
@@ -54,6 +54,7 @@ use codex_app_server_protocol::TextPosition as AppTextPosition;
|
||||
use codex_app_server_protocol::TextRange as AppTextRange;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLoadError;
|
||||
use codex_config::ConstraintError;
|
||||
use codex_config::TextRange as CoreTextRange;
|
||||
use codex_core::ExecPolicyError;
|
||||
use codex_core::check_execpolicy_for_warnings;
|
||||
@@ -300,6 +301,12 @@ fn config_error_location(err: &std::io::Error) -> Option<(String, AppTextRange)>
|
||||
})
|
||||
}
|
||||
|
||||
fn is_unrecoverable_windows_network_config_error(err: &std::io::Error) -> bool {
|
||||
err.get_ref()
|
||||
.and_then(|source| source.downcast_ref::<ConstraintError>())
|
||||
.is_some_and(ConstraintError::is_windows_network_configuration_error)
|
||||
}
|
||||
|
||||
fn exec_policy_warning_location(err: &ExecPolicyError) -> (Option<String>, Option<AppTextRange>) {
|
||||
match err {
|
||||
ExecPolicyError::ParsePolicy { path, source } => {
|
||||
@@ -497,7 +504,7 @@ pub async fn run_main_with_transport_options(
|
||||
Arc::new(NoopThreadConfigLoader),
|
||||
);
|
||||
match config_manager
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
.load_config_for_cloud_config_bootstrap()
|
||||
.await
|
||||
{
|
||||
Ok(config) => {
|
||||
@@ -523,7 +530,7 @@ pub async fn run_main_with_transport_options(
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
if strict_config {
|
||||
if strict_config || is_unrecoverable_windows_network_config_error(&err) {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
@@ -1323,14 +1330,63 @@ fn analytics_rpc_transport(transport: &AppServerTransport) -> AppServerRpcTransp
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::LogFormat;
|
||||
#[cfg(target_os = "windows")]
|
||||
use super::is_unrecoverable_windows_network_config_error;
|
||||
#[cfg(debug_assertions)]
|
||||
use super::loader_overrides_with_test_user_config_file;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::config_manager::ConfigManager;
|
||||
#[cfg(debug_assertions)]
|
||||
use codex_config::LoaderOverrides;
|
||||
#[cfg(debug_assertions)]
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tokio::test]
|
||||
async fn cloud_config_bootstrap_defers_network_validation_and_preserves_cli_overrides() {
|
||||
let codex_home = tempfile::tempdir().expect("create Codex home");
|
||||
std::fs::write(
|
||||
codex_home.path().join(codex_config::CONFIG_TOML_FILE),
|
||||
r#"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
|
||||
[features]
|
||||
network_proxy = true
|
||||
|
||||
[windows]
|
||||
sandbox = "elevated"
|
||||
"#,
|
||||
)
|
||||
.expect("write config");
|
||||
let chatgpt_base_url = "https://cloud-config.example.test".to_string();
|
||||
let config_manager = ConfigManager::new_for_tests(
|
||||
codex_home.path().to_path_buf(),
|
||||
vec![(
|
||||
"chatgpt_base_url".to_string(),
|
||||
toml::Value::String(chatgpt_base_url.clone()),
|
||||
)],
|
||||
codex_config::LoaderOverrides::without_managed_config_for_tests(),
|
||||
codex_config::CloudConfigBundleLoader::default(),
|
||||
);
|
||||
|
||||
let config = config_manager
|
||||
.load_config_for_cloud_config_bootstrap()
|
||||
.await
|
||||
.expect("bootstrap config should defer Windows network validation");
|
||||
assert_eq!(config.chatgpt_base_url, chatgpt_base_url);
|
||||
assert!(config.permissions.network.is_some());
|
||||
|
||||
let err = config_manager
|
||||
.load_latest_config(/*fallback_cwd*/ None)
|
||||
.await
|
||||
.expect_err("authoritative config should enforce Windows network validation");
|
||||
assert!(is_unrecoverable_windows_network_config_error(&err));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_format_from_env_value_matches_json_values_case_insensitively() {
|
||||
assert_eq!(LogFormat::from_env_value(Some("json")), LogFormat::Json);
|
||||
|
||||
@@ -204,6 +204,7 @@ impl CommandExecRequestProcessor {
|
||||
network_proxy_permission_profile,
|
||||
managed_network_requirements_enabled,
|
||||
windows_sandbox_workspace_roots,
|
||||
windows_sandbox_level,
|
||||
) = if let Some(permission_profile) = permission_profile {
|
||||
let overrides = ConfigOverrides {
|
||||
cwd: Some(cwd.to_path_buf()),
|
||||
@@ -232,6 +233,7 @@ impl CommandExecRequestProcessor {
|
||||
config.permissions.permission_profile().clone(),
|
||||
config.managed_network_requirements_enabled(),
|
||||
config.effective_workspace_roots(),
|
||||
WindowsSandboxLevel::from_config(&config),
|
||||
)
|
||||
} else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) {
|
||||
self.config
|
||||
@@ -258,6 +260,7 @@ impl CommandExecRequestProcessor {
|
||||
self.config.permissions.permission_profile().clone(),
|
||||
self.config.managed_network_requirements_enabled(),
|
||||
self.config.effective_workspace_roots(),
|
||||
windows_sandbox_level,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
@@ -266,9 +269,13 @@ impl CommandExecRequestProcessor {
|
||||
self.config.permissions.permission_profile().clone(),
|
||||
self.config.managed_network_requirements_enabled(),
|
||||
self.config.effective_workspace_roots(),
|
||||
windows_sandbox_level,
|
||||
)
|
||||
};
|
||||
let started_network_proxy = match network_proxy_spec.as_ref() {
|
||||
let started_network_proxy = match network_proxy_spec
|
||||
.as_ref()
|
||||
.filter(|spec| !cfg!(target_os = "windows") || spec.enabled())
|
||||
{
|
||||
Some(spec) => match spec
|
||||
.start_proxy(
|
||||
&network_proxy_permission_profile,
|
||||
|
||||
@@ -63,6 +63,19 @@ impl WindowsSandboxRequestProcessor {
|
||||
config.config_layer_stack.requirements(),
|
||||
params.mode,
|
||||
)?;
|
||||
let target_windows_sandbox_level = match setup_mode {
|
||||
CoreWindowsSandboxSetupMode::Elevated => WindowsSandboxLevel::Elevated,
|
||||
CoreWindowsSandboxSetupMode::Unelevated => WindowsSandboxLevel::RestrictedToken,
|
||||
};
|
||||
codex_core::config::validate_windows_sandbox_network_proxy_compatibility(
|
||||
target_windows_sandbox_level,
|
||||
config
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.is_some_and(codex_core::config::NetworkProxySpec::enabled),
|
||||
)
|
||||
.map_err(|err| invalid_request(err.to_string()))?;
|
||||
|
||||
self.outgoing
|
||||
.send_response(
|
||||
|
||||
Reference in New Issue
Block a user