Skip to content
39 changes: 35 additions & 4 deletions codex-rs/app-server/src/config_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,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,
/*defer_windows_network_validation*/ true,
)
.await
}

pub(crate) async fn load_latest_config_for_thread(
&self,
thread_config: &Config,
Expand Down Expand Up @@ -215,11 +226,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,
/*defer_windows_network_validation*/ false,
)
.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>,
defer_windows_network_validation: bool,
) -> std::io::Result<Config> {
let mut request_overrides = request_overrides.unwrap_or_default();
if let Some(value) = request_overrides.remove("bypass_hook_trust") {
Expand All @@ -240,17 +269,19 @@ impl ConfigManager {
)
.collect::<Vec<_>>();

let mut config = codex_core::config::ConfigBuilder::default()
let mut builder = codex_core::config::ConfigBuilder::default()
.codex_home(self.codex_home.clone())
.cli_overrides(merged_cli_overrides)
.loader_overrides(self.loader_overrides.clone())
.strict_config(self.strict_config)
.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());
if defer_windows_network_validation {
builder = builder.defer_windows_network_validation_for_bootstrap();
}
let mut config = builder.build().await?;
self.apply_runtime_feature_enablement(&mut config);
self.apply_arg0_paths(&mut config);
Ok(config)
Expand Down
59 changes: 57 additions & 2 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -298,6 +299,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 } => {
Expand Down Expand Up @@ -495,7 +502,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) => {
Expand All @@ -521,7 +528,7 @@ pub async fn run_main_with_transport_options(
{
Ok(config) => (config, true),
Err(err) => {
if strict_config {
if strict_config || is_unrecoverable_windows_network_config_error(&err) {
return Err(err);
Comment thread
iceweasel-oai marked this conversation as resolved.
}

Expand Down Expand Up @@ -1364,14 +1371,62 @@ fn analytics_rpc_transport(transport: &AppServerTransport) -> AppServerRpcTransp
#[cfg(test)]
mod tests {
use super::LogFormat;
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -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
Expand All @@ -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 {
(
Expand All @@ -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())
Comment on lines +275 to +277

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the reloaded sandbox level for command/exec proxies

When command/exec is called with a request permissionProfile, this path reloads config for the requested cwd and can pick up elevated-only Windows requirements plus an enabled network proxy, but ExecParams still uses windows_sandbox_level captured earlier from self.config. On Windows that means a project/profile-specific proxy can be started here while the exec request is still marked restricted-token, and the new compatibility check rejects the command instead of using the reloaded elevated level that made the proxy valid. Carry the reloaded config’s WindowsSandboxLevel alongside network_proxy_spec before starting/passing the proxy.

AGENTS.md reference: AGENTS.md:L102-L107

Useful? React with 👍 / 👎.

{
Some(spec) => match spec
.start_proxy(
&network_proxy_permission_profile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 15 additions & 0 deletions codex-rs/config/src/config_requirements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,13 @@ impl WindowsRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_sandbox_implementations.is_none()
}

pub fn allows_only_elevated_sandbox(&self) -> bool {
matches!(
self.allowed_sandbox_implementations.as_deref(),
Some([WindowsSandboxModeToml::Elevated])
)
}
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
Expand Down Expand Up @@ -881,6 +888,14 @@ pub struct ConfigRequirementsToml {
pub guardian_policy_config: Option<String>,
}

impl ConfigRequirementsToml {
pub fn allows_only_elevated_windows_sandbox(&self) -> bool {
self.windows
.as_ref()
.is_some_and(WindowsRequirementsToml::allows_only_elevated_sandbox)
}
}

#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct ModelsRequirementsToml {
pub new_thread: Option<NewThreadModelDefaultsToml>,
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/config/src/constraint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ pub enum ConstraintError {
requirement_source: RequirementSource,
reason: String,
},

#[error(
"network proxy configuration requires `[windows] allowed_sandbox_implementations = [\"elevated\"]` in requirements"
)]
NetworkProxyRequiresElevatedWindowsSandboxRequirement,

#[error(
"the network proxy is incompatible with the unelevated Windows sandbox; set `windows.sandbox = \"elevated\"` or disable the network proxy"
)]
NetworkProxyIncompatibleWithUnelevatedWindowsSandbox,
}

impl ConstraintError {
Expand All @@ -41,6 +51,14 @@ impl ConstraintError {
field_name: field_name.into(),
}
}

pub fn is_windows_network_configuration_error(&self) -> bool {
matches!(
self,
Self::NetworkProxyRequiresElevatedWindowsSandboxRequirement
| Self::NetworkProxyIncompatibleWithUnelevatedWindowsSandbox
)
}
}

pub type ConstraintResult<T> = Result<T, ConstraintError>;
Expand Down
Loading
Loading