Stop loading legacy managed config on Windows (#38947)

## What changed

- Ignore the default `CODEX_HOME/managed_config.toml` on Windows and exclude it
  from local managed-configuration detection.
- Emit a startup warning when the deprecated file exists, directing users to
  `%ProgramData%\OpenAI\Codex\requirements.toml` for enforced settings or
  `config.toml` for defaults.
- Preserve explicit managed-config path overrides and Unix legacy-file support.

## Testing

- Add Windows tests covering ignored legacy settings, the startup warning, and
  managed-configuration detection through `requirements.toml`.

GitOrigin-RevId: a61d9d9912b13817ba82807a486c9ed92e49c7bc
This commit is contained in:
iceweasel-oai
2026-08-17 05:44:20 +00:00
committed by copyberry
parent e38290846c
commit c6058ccaa9
3 changed files with 158 additions and 21 deletions

View File

@@ -37,6 +37,8 @@ pub(super) struct LoadedConfigLayers {
pub managed_config: Option<MangedConfigFromFile>,
/// If present, data read from managed preferences (macOS only).
pub managed_config_from_mdm: Option<ManagedConfigFromMdm>,
/// User-facing warnings discovered while loading managed configuration.
pub startup_warnings: Vec<String>,
}
pub(super) async fn load_config_layers_internal(
@@ -58,21 +60,54 @@ pub(super) async fn load_config_layers_internal(
..
} = overrides;
#[cfg(windows)]
let ignore_default_managed_config = managed_config_path.is_none();
#[cfg(not(windows))]
let ignore_default_managed_config = false;
let managed_config_path = AbsolutePathBuf::from_absolute_path(
managed_config_path.unwrap_or_else(|| managed_config_default_path(codex_home)),
)?;
let managed_config = read_config_from_path(
fs,
&managed_config_path,
/*log_missing_as_info*/ false,
strict_config,
)
.await?
.map(|loaded| MangedConfigFromFile {
managed_config: loaded,
file: managed_config_path.clone(),
});
#[cfg(windows)]
let startup_warnings = if ignore_default_managed_config {
let path_uri = PathUri::from_abs_path(&managed_config_path);
match fs.get_metadata(&path_uri, /*sandbox*/ None).await {
Ok(_) => vec![format!(
"Ignoring deprecated managed config file at {}; CODEX_HOME/managed_config.toml is no longer supported on Windows. Use %ProgramData%\\OpenAI\\Codex\\requirements.toml for enforced settings or config.toml for defaults.",
managed_config_path.as_path().display()
)],
Err(err) if err.kind() == io::ErrorKind::NotFound => Vec::new(),
Err(err) => {
tracing::debug!(
error = %err,
path = %managed_config_path.as_path().display(),
"Failed to check deprecated managed config file"
);
Vec::new()
}
}
} else {
Vec::new()
};
#[cfg(not(windows))]
let startup_warnings = Vec::new();
let managed_config = if ignore_default_managed_config {
None
} else {
read_config_from_path(
fs,
&managed_config_path,
/*log_missing_as_info*/ false,
strict_config,
)
.await?
.map(|loaded| MangedConfigFromFile {
managed_config: loaded,
file: managed_config_path.clone(),
})
};
#[cfg(target_os = "macos")]
let managed_preferences = load_managed_admin_config_layer(
@@ -89,6 +124,7 @@ pub(super) async fn load_config_layers_internal(
Ok(LoadedConfigLayers {
managed_config,
managed_config_from_mdm: managed_preferences,
startup_warnings,
})
}
@@ -168,7 +204,9 @@ fn validate_config_toml_strictly(
Ok(())
}
/// Return the default managed config path.
/// Return the legacy managed config path.
///
/// On Windows, the default path is only checked so callers can warn that it is ignored.
pub(super) fn managed_config_default_path(codex_home: &Path) -> PathBuf {
#[cfg(unix)]
{

View File

@@ -94,11 +94,12 @@ async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> O
/// - system `/etc/codex/requirements.toml` (Unix) or
/// `%ProgramData%\OpenAI\Codex\requirements.toml` (Windows)
/// - cloud: enterprise-managed cloud config bundle requirements
/// - legacy: managed_config.toml reinterpreted as requirements.toml
/// - legacy: `/etc/codex/managed_config.toml` (Unix) reinterpreted as
/// requirements.toml
/// - admin: managed preferences (*)
///
/// For backwards compatibility, we also load from
/// `managed_config.toml` and map it to `requirements.toml`.
/// For backwards compatibility, Unix continues to load
/// `/etc/codex/managed_config.toml` and map it to `requirements.toml`.
///
/// Configuration is built up from multiple layers in the following order:
///
@@ -233,10 +234,12 @@ pub async fn load_config_layers_state(
let loaded_config_layers =
layer_io::load_config_layers_internal(fs, codex_home, overrides.clone(), strict_config)
.await?;
let mut startup_warnings = (!loaded_config_layers.startup_warnings.is_empty())
.then(|| loaded_config_layers.startup_warnings.clone());
if !ignore_managed_requirements {
requirements_layers.extend(system_requirements_layer);
requirements_layers.extend(bundle_requirements_layers);
// Continue to support the legacy `managed_config.toml` locations as
// Continue to support loaded legacy `managed_config.toml` sources as
// requirements layers for backwards compatibility.
requirements_layers.extend(requirements_layers_from_legacy_scheme(
loaded_config_layers.clone(),
@@ -353,7 +356,6 @@ pub async fn load_config_layers_state(
);
}
let mut startup_warnings = None;
if !ignore_project_config && let Some(cwd) = cwd {
let mut merged_so_far = TomlValue::Table(toml::map::Map::new());
for layer in &layers {
@@ -412,7 +414,9 @@ pub async fn load_config_layers_state(
)
.await?;
layers.extend(project_layers.layers);
startup_warnings = Some(project_layers.startup_warnings);
startup_warnings
.get_or_insert_with(Vec::new)
.extend(project_layers.startup_warnings);
}
// Add a layer for runtime overrides from the CLI or UI, if any exist.
@@ -435,6 +439,7 @@ pub async fn load_config_layers_state(
let LoadedConfigLayers {
managed_config,
managed_config_from_mdm,
..
} = loaded_config_layers;
if let Some(config) = managed_config {
let managed_parent = config.file.as_path().parent().ok_or_else(|| {
@@ -736,9 +741,26 @@ fn system_requirements_toml_file_with_overrides(
/// Filesystem or managed-preference errors are returned so callers can conservatively avoid
/// assuming that administrator-controlled configuration is absent.
pub fn has_local_managed_configuration(codex_home: &Path) -> io::Result<bool> {
if layer_io::managed_config_default_path(codex_home).try_exists()?
|| system_requirements_toml_file()?.as_path().try_exists()?
{
let system_requirements_file = system_requirements_toml_file()?;
has_local_managed_configuration_with_system_requirements_path(
codex_home,
system_requirements_file.as_path(),
)
}
fn has_local_managed_configuration_with_system_requirements_path(
codex_home: &Path,
system_requirements_path: &Path,
) -> io::Result<bool> {
#[cfg(windows)]
let _ = codex_home;
#[cfg(not(windows))]
if layer_io::managed_config_default_path(codex_home).try_exists()? {
return Ok(true);
}
if system_requirements_path.try_exists()? {
return Ok(true);
}
@@ -852,6 +874,7 @@ fn requirements_layers_from_legacy_scheme(
let LoadedConfigLayers {
managed_config,
managed_config_from_mdm,
..
} = loaded_config_layers;
let layer_count =

View File

@@ -221,6 +221,82 @@ async fn missing_packaged_defaults_file_returns_an_error() {
);
}
#[cfg(windows)]
#[tokio::test]
async fn default_windows_managed_config_is_ignored_with_warning() {
let tmp = tempdir().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
std::fs::create_dir_all(&codex_home).expect("create codex home");
let managed_config_path = codex_home.join("managed_config.toml");
std::fs::write(
&managed_config_path,
r#"
model = "legacy-model"
approval_policy = "never"
sandbox_mode = "danger-full-access"
"#,
)
.expect("write default legacy managed config");
std::fs::write(codex_home.join(CONFIG_TOML_FILE), r#"model = "user-model""#)
.expect("write user config");
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.managed_config_path = None;
overrides.system_config_path = Some(tmp.path().join("system-config.toml"));
overrides.system_requirements_path = Some(tmp.path().join("requirements.toml"));
let stack = load_config_layers_state(
&TestFileSystem,
&codex_home,
/*cwd*/ None,
&[],
overrides,
&crate::NoopThreadConfigLoader,
)
.await
.expect("load config layers");
assert_eq!(
stack.effective_config().get("model"),
Some(&TomlValue::String("user-model".to_string()))
);
assert_eq!(stack.requirements_toml().allowed_approval_policies, None);
assert_eq!(stack.requirements_toml().allowed_sandbox_modes, None);
assert!(stack.all_layers_low_to_high().all(|layer| !matches!(
&layer.name,
ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
)));
let expected_warnings = vec![format!(
"Ignoring deprecated managed config file at {}; CODEX_HOME/managed_config.toml is no longer supported on Windows. Use %ProgramData%\\OpenAI\\Codex\\requirements.toml for enforced settings or config.toml for defaults.",
managed_config_path.display()
)];
assert_eq!(stack.startup_warnings(), Some(expected_warnings.as_slice()));
}
#[cfg(windows)]
#[test]
fn windows_local_managed_configuration_ignores_legacy_file_but_detects_requirements() {
let tmp = tempdir().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
std::fs::create_dir_all(&codex_home).expect("create codex home");
std::fs::write(codex_home.join("managed_config.toml"), "")
.expect("write default legacy managed config");
let system_requirements_path = tmp.path().join("requirements.toml");
let legacy_only = has_local_managed_configuration_with_system_requirements_path(
&codex_home,
&system_requirements_path,
)
.expect("check legacy-only managed configuration");
std::fs::write(&system_requirements_path, "").expect("write system requirements");
let with_system_requirements = has_local_managed_configuration_with_system_requirements_path(
&codex_home,
&system_requirements_path,
)
.expect("check system managed configuration");
assert_eq!((legacy_only, with_system_requirements), (false, true));
}
#[tokio::test]
async fn profile_v2_rejects_matching_legacy_profile_in_base_user_config() {
let tmp = tempdir().expect("tempdir");