diff --git a/codex-rs/config/src/loader/layer_io.rs b/codex-rs/config/src/loader/layer_io.rs index 947911a893..d704961274 100644 --- a/codex-rs/config/src/loader/layer_io.rs +++ b/codex-rs/config/src/loader/layer_io.rs @@ -37,6 +37,8 @@ pub(super) struct LoadedConfigLayers { pub managed_config: Option, /// If present, data read from managed preferences (macOS only). pub managed_config_from_mdm: Option, + /// User-facing warnings discovered while loading managed configuration. + pub startup_warnings: Vec, } 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)] { diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index f601250f9d..44058d1fee 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -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 { - 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 { + #[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 = diff --git a/codex-rs/config/src/loader/tests.rs b/codex-rs/config/src/loader/tests.rs index 8df26eb6cf..7bca3a4050 100644 --- a/codex-rs/config/src/loader/tests.rs +++ b/codex-rs/config/src/loader/tests.rs @@ -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");