Skip to content
15 changes: 15 additions & 0 deletions codex-rs/app-server/src/config_manager_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,21 @@ impl ConfigManager {
format!("Invalid configuration: {err}"),
)
})?;
let materialized_config_validation =
codex_core::config::validate_materialized_config_from_layer_stack(
self.codex_home().to_path_buf(),
updated_layers.clone(),
codex_core::config::ConfigOverrides::default(),
)
.await;
if let Err(err) = materialized_config_validation
&& crate::is_unrecoverable_config_error(&err)
{
return Err(ConfigManagerError::write(
ConfigWriteErrorCode::ConfigValidationError,
err.to_string(),
));
}

if !config_edits.is_empty() {
ConfigEditsBuilder::for_config_path(provided_path.as_path())
Expand Down
82 changes: 82 additions & 0 deletions codex-rs/app-server/src/config_manager_service_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,88 @@ personality = true
);
}

#[tokio::test]
async fn write_value_ignores_unrelated_materialized_config_errors() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let config_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&config_path, "model_provider = \"missing\"\n")?;

let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: Some(config_path.display().to_string()),
key_path: "features.personality".to_string(),
value: serde_json::json!(true),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await?;

let actual: TomlValue = toml::from_str(&std::fs::read_to_string(&config_path)?)?;
let expected: TomlValue = toml::from_str(
r#"
model_provider = "missing"

[features]
personality = true
"#,
)?;
assert_eq!(actual, expected);

Ok(())
}

#[cfg(target_os = "windows")]
#[tokio::test]
async fn batch_write_rejects_unelevated_windows_sandbox_with_network_proxy() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let config_path = tmp.path().join(CONFIG_TOML_FILE);
let original_config = r#"
default_permissions = "networked"

[features]
network_proxy = true

[windows]
sandbox = "elevated"

[permissions.networked.filesystem]
":root" = "read"

[permissions.networked.network]
enabled = true
"#;
std::fs::write(&config_path, original_config)?;

let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.batch_write(ConfigBatchWriteParams {
edits: vec![codex_app_server_protocol::ConfigEdit {
key_path: "windows.sandbox".to_string(),
value: serde_json::json!("unelevated"),
merge_strategy: MergeStrategy::Replace,
}],
file_path: Some(config_path.display().to_string()),
expected_version: None,
reload_user_config: false,
})
.await
.expect_err("unelevated Windows sandbox should be rejected with network proxy");

assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert!(
error
.to_string()
.contains("The network proxy requires the elevated Windows sandbox backend"),
"{error}"
);
assert_eq!(std::fs::read_to_string(&config_path)?, original_config);
Ok(())
}

#[tokio::test]
async fn read_reports_managed_overrides_user_and_session_flags() {
let tmp = tempdir().expect("tempdir");
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,10 @@ fn config_error_location(err: &std::io::Error) -> Option<(String, AppTextRange)>
})
}

pub(crate) fn is_unrecoverable_config_error(err: &std::io::Error) -> bool {
codex_core::config::is_windows_sandbox_network_proxy_incompatible_error(err)
}

fn exec_policy_warning_location(err: &ExecPolicyError) -> (Option<String>, Option<AppTextRange>) {
match err {
ExecPolicyError::ParsePolicy { path, source } => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ 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(),
)
.map_err(|err| invalid_request(err.to_string()))?;

self.outgoing
.send_response(
Expand Down
50 changes: 50 additions & 0 deletions codex-rs/app-server/tests/suite/strict_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,53 @@ foo = "bar"

Ok(())
}

#[cfg(target_os = "windows")]
#[test]
fn non_strict_config_falls_back_for_unelevated_windows_sandbox_with_network_proxy() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"
default_permissions = "networked"

[features]
network_proxy = true

[windows]
sandbox = "unelevated"

[permissions.networked.filesystem]
":minimal" = "read"

[permissions.networked.network]
enabled = true
"#,
)?;

let output = Command::new(codex_utils_cargo_bin::cargo_bin("codex-app-server")?)
.env("CODEX_HOME", codex_home.path())
.env(
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
codex_home.path().join("managed_config.toml"),
)
.args(["--listen", "off"])
.output()?;

assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr)?;
assert!(
stderr.contains("Invalid configuration; using defaults."),
"expected invalid config warning in stderr, got: {stderr}"
);
assert!(
stderr.contains("The network proxy requires the elevated Windows sandbox backend"),
"expected Windows sandbox compatibility warning in stderr, got: {stderr}"
);
assert!(
stderr.contains("no transport configured; use --listen or enable remote control"),
"expected app server to reach transport validation after falling back, got: {stderr}"
);

Ok(())
}
185 changes: 185 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,191 @@ enabled = true
Ok(())
}

#[test]
fn windows_sandbox_network_proxy_compatibility_rejects_network_proxy_with_unelevated_windows() {
let err = validate_windows_sandbox_network_proxy_compatibility_for_platform(
/*is_windows*/ true,
WindowsSandboxLevel::RestrictedToken,
/*network_proxy_configured*/ true,
)
.expect_err("unelevated Windows sandbox should reject configured network proxy");

assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
err.to_string(),
WINDOWS_SANDBOX_NETWORK_PROXY_INCOMPATIBLE_ERROR
);
}

#[test]
fn windows_sandbox_network_proxy_compatibility_allows_compatible_combinations() {
for (windows_sandbox_level, network_proxy_configured) in [
(WindowsSandboxLevel::Disabled, true),
(WindowsSandboxLevel::Elevated, true),
(WindowsSandboxLevel::RestrictedToken, false),
] {
validate_windows_sandbox_network_proxy_compatibility_for_platform(
/*is_windows*/ true,
windows_sandbox_level,
network_proxy_configured,
)
.expect("compatible Windows sandbox and network proxy settings");
}
validate_windows_sandbox_network_proxy_compatibility_for_platform(
/*is_windows*/ false,
WindowsSandboxLevel::RestrictedToken,
/*network_proxy_configured*/ true,
)
.expect("Windows sandbox compatibility should only be enforced on Windows");
}

#[cfg(target_os = "windows")]
#[tokio::test]
async fn disabled_experimental_network_requirements_reject_unelevated_windows_sandbox()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
[windows]
sandbox = "unelevated"
"#,
)?;

let err = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[experimental_network]
enabled = false
"#,
),
)
.build()
.await
.expect_err("disabled experimental_network requirements still configure network proxy");

assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
err.to_string(),
WINDOWS_SANDBOX_NETWORK_PROXY_INCOMPATIBLE_ERROR
);
Ok(())
}

#[cfg(target_os = "windows")]
#[tokio::test]
async fn network_proxy_feature_rejects_unelevated_windows_sandbox() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;

let err = 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()),
permissions: Some(PermissionsToml {
entries: BTreeMap::from([(
"dev".to_string(),
PermissionProfileToml {
description: None,
extends: None,
workspace_roots: None,
filesystem: Some(FilesystemPermissionsToml {
glob_scan_max_depth: None,
entries: BTreeMap::from([(
":minimal".to_string(),
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
)]),
}),
network: Some(NetworkToml {
enabled: Some(true),
..Default::default()
}),
},
)]),
}),
windows: Some(WindowsToml {
sandbox: Some(WindowsSandboxModeToml::Unelevated),
sandbox_private_desktop: None,
}),
..Default::default()
},
ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
..Default::default()
},
codex_home.abs(),
)
.await
.expect_err("network_proxy feature should reject unelevated Windows sandbox");

assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
err.to_string(),
WINDOWS_SANDBOX_NETWORK_PROXY_INCOMPATIBLE_ERROR
);
Ok(())
}

#[cfg(target_os = "windows")]
#[tokio::test]
async fn active_profile_network_proxy_rejects_unelevated_windows_sandbox() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;

std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
default_permissions = ":read-only"

[features]
network_proxy = true

[windows]
sandbox = "unelevated"

[permissions.dev.filesystem]
":minimal" = "read"

[permissions.dev.network]
enabled = true
"#,
)?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
..Default::default()
})
.build()
.await?;

assert_eq!(config.permissions.network, None);
let err = config
.network_proxy_spec_for_active_permission_profile(
&ActivePermissionProfile::new("dev"),
&PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: Vec::new(),
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Enabled,
},
)
.expect_err("inactive network proxy profile should be rejected with unelevated sandbox");

assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
err.to_string(),
WINDOWS_SANDBOX_NETWORK_PROXY_INCOMPATIBLE_ERROR
);
Ok(())
}

#[tokio::test]
async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
Expand Down
Loading
Loading