Download codex-rs/config/src/loader/mod.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 72.8 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/config/src/loader/mod.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/config/src/loader/mod.rs
-
curl -L -o mod.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/config/src/loader/mod.rs
72.8 kB
| mod layer_io; | |
| mod local; | |
| mod macos; | |
| mod managed_requirements; | |
| mod project_discovery; | |
| mod tests; | |
| mod windows; | |
| use self::layer_io::LoadedConfigLayers; | |
| use crate::CONFIG_TOML_FILE; | |
| use crate::CloudConfigBundleLayers; | |
| use crate::ConfigLayerSource; | |
| use crate::ProfileV2Name; | |
| use crate::RequirementsLayerEntry; | |
| use crate::config_requirements::RequirementSource; | |
| use crate::config_requirements::SandboxModeRequirement; | |
| use crate::config_toml::ConfigToml; | |
| use crate::config_toml::ProjectConfig; | |
| use crate::diagnostics::ConfigError; | |
| use crate::diagnostics::config_error_from_toml; | |
| use crate::diagnostics::first_layer_config_error_from_entries as typed_first_layer_config_error_from_entries; | |
| use crate::diagnostics::io_error_from_config_error; | |
| use crate::merge::merge_toml_values; | |
| use crate::overrides::build_cli_overrides_layer; | |
| use crate::project_root_markers::default_project_root_markers; | |
| use crate::project_root_markers::project_root_markers_from_config; | |
| use crate::shell_environment_policy::ShellEnvironmentPolicyFilterConfigToml; | |
| use crate::state::ConfigLayerEntry; | |
| use crate::state::ConfigLayerStack; | |
| use crate::state::ConfigLoadOptions; | |
| use crate::state::LoaderOverrides; | |
| use crate::state::validate_enabled_config_layers; | |
| use crate::strict_config::config_error_from_ignored_toml_value_fields; | |
| use crate::strict_config::ignored_config_warning; | |
| use crate::strict_config::ignored_toml_value_fields; | |
| use crate::strict_config::unknown_feature_toml_value_field; | |
| use crate::thread_config::ThreadConfigContext; | |
| use crate::thread_config::ThreadConfigLoader; | |
| use codex_file_system::ExecutorFileSystem; | |
| use codex_git_utils::resolve_root_git_project_for_trust; | |
| use codex_network_proxy::is_credential_broker_provider_env_key; | |
| use codex_protocol::config_types::ApprovalsReviewer; | |
| use codex_protocol::config_types::SandboxMode; | |
| use codex_protocol::config_types::TrustLevel; | |
| use codex_protocol::protocol::AskForApproval; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use codex_utils_absolute_path::AbsolutePathBufGuard; | |
| use codex_utils_path_uri::PathUri; | |
| use dunce::canonicalize as normalize_path; | |
| use serde::Deserialize; | |
| use std::io; | |
| use std::path::Path; | |
| use std::path::PathBuf; | |
| use toml::Value as TomlValue; | |
| pub use local::LocalConfigLayers; | |
| pub use local::LocalTomlLayer; | |
| pub use local::LocalTomlLayerStack; | |
| pub use local::load_local_config_layers; | |
| pub use managed_requirements::load_managed_requirements_state; | |
| pub use windows::WindowsSystemConfigNamespaceProbe; | |
| pub use windows::probe_windows_system_config_namespace; | |
| const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml"; | |
| const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData"; | |
| // Project-local config comes from repository contents, so it should not get to | |
| // choose where a user's credentials are sent or which local commands are run. | |
| // These settings are still supported from user, system, managed, and runtime | |
| // config layers. | |
| const PROJECT_LOCAL_CONFIG_DENYLIST: &[&str] = &[ | |
| "openai_base_url", | |
| "chatgpt_base_url", | |
| "apps_mcp_product_sku", | |
| "responses_api_metadata", | |
| "model_provider", | |
| "model_providers", | |
| "notify", | |
| "profile", | |
| "profiles", | |
| "experimental_realtime_webrtc_call_base_url", | |
| "experimental_realtime_ws_base_url", | |
| "otel", | |
| ]; | |
| async fn first_layer_config_error_from_entries(layers: &[ConfigLayerEntry]) -> Option<ConfigError> { | |
| typed_first_layer_config_error_from_entries::<ConfigToml>(layers, CONFIG_TOML_FILE).await | |
| } | |
| /// To build up the set of admin-enforced constraints, requirements layers are | |
| /// collected in ascending precedence order, matching config layers, and then | |
| /// composed with config-style TOML merging plus field-specific handling for | |
| /// hooks, rules, deny-read permissions, and remote sandbox config: | |
| /// | |
| /// - system `/etc/codex/requirements.toml` (Unix) or | |
| /// `%ProgramData%\OpenAI\Codex\requirements.toml` (Windows) | |
| /// - cloud: enterprise-managed cloud config bundle requirements | |
| /// - legacy: `/etc/codex/managed_config.toml` (Unix) reinterpreted as | |
| /// requirements.toml | |
| /// - admin: managed preferences (*) | |
| /// | |
| /// 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: | |
| /// | |
| /// - package: optional default configuration supplied with the Codex package | |
| /// - admin: managed preferences (*) | |
| /// - system `/etc/codex/config.toml` (Unix) or | |
| /// `%ProgramData%\OpenAI\Codex\config.toml` (Windows) | |
| /// - cloud enterprise-managed cloud config bundle fragments | |
| /// - user `${CODEX_HOME}/config.toml` | |
| /// - profile `${CODEX_HOME}/<name>.config.toml`, when selected | |
| /// - cwd `${PWD}/config.toml` (loaded but disabled when the directory is untrusted) | |
| /// - tree parent directories up to root looking for `./.codex/config.toml` (loaded but disabled when untrusted) | |
| /// - repo `$(git rev-parse --show-toplevel)/.codex/config.toml` (loaded but disabled when untrusted) | |
| /// - runtime e.g., --config flags, model selector in UI | |
| /// | |
| /// (*) Only available on macOS via managed device profiles. | |
| /// | |
| /// See https://developers.openai.com/codex/security for details. | |
| /// | |
| /// When loading the config stack for a thread, there should be a `cwd` | |
| /// associated with it such that `cwd` should be `Some(...)`. Only for | |
| /// thread-agnostic config loading (e.g., for the app server's `/config` | |
| /// endpoint) should `cwd` be `None`. | |
| pub async fn load_config_layers_state( | |
| fs: &dyn ExecutorFileSystem, | |
| codex_home: &Path, | |
| cwd: Option<AbsolutePathBuf>, | |
| cli_overrides: &[(String, TomlValue)], | |
| options: impl Into<ConfigLoadOptions>, | |
| thread_config_loader: &dyn ThreadConfigLoader, | |
| ) -> io::Result<ConfigLayerStack> { | |
| let ConfigLoadOptions { | |
| loader_overrides: overrides, | |
| strict_config, | |
| cloud_config_bundle, | |
| } = options.into(); | |
| let packaged_defaults_layer = if let Some(file) = &overrides.packaged_defaults_path { | |
| let config = layer_io::read_config_from_path( | |
| fs, | |
| file, | |
| /*log_missing_as_info*/ false, | |
| strict_config, | |
| ) | |
| .await? | |
| .ok_or_else(|| { | |
| io::Error::new( | |
| io::ErrorKind::NotFound, | |
| format!("packaged defaults config file {} not found", file.display()), | |
| ) | |
| })?; | |
| let base_dir = file.as_path().parent().ok_or_else(|| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "packaged defaults config file {} has no parent directory", | |
| file.display() | |
| ), | |
| ) | |
| })?; | |
| ConfigLayerEntry::new( | |
| ConfigLayerSource::PackagedDefaults { file: file.clone() }, | |
| resolve_relative_paths_in_config_toml(config, base_dir)?, | |
| ) | |
| } else { | |
| let file = AbsolutePathBuf::from_absolute_path(std::env::current_exe()?)?; | |
| let raw_toml = include_str!("../../defaults.toml"); | |
| let config = toml::from_str(raw_toml).map_err(|error| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!("invalid embedded packaged defaults; this is a Codex build error: {error}"), | |
| ) | |
| })?; | |
| ConfigLayerEntry::new_with_raw_toml( | |
| ConfigLayerSource::PackagedDefaults { file }, | |
| config, | |
| raw_toml.to_owned(), | |
| AbsolutePathBuf::from_absolute_path(codex_home)?, | |
| ) | |
| }; | |
| let active_user_profile = overrides.user_config_profile.clone(); | |
| let ignore_user_config = overrides.ignore_user_config; | |
| let ignore_project_config = overrides.ignore_project_config; | |
| let ignore_user_and_project_exec_policy_rules = | |
| overrides.ignore_user_and_project_exec_policy_rules; | |
| let mut bundle_requirements_layers = Vec::new(); | |
| let mut cloud_config_layers = Vec::new(); | |
| if !overrides.ignore_managed_requirements | |
| && let Some(bundle) = cloud_config_bundle.get().await.map_err(io::Error::other)? | |
| { | |
| let cloud_config_base_dir = AbsolutePathBuf::from_absolute_path(codex_home)?; | |
| let bundle_layers = if strict_config { | |
| CloudConfigBundleLayers::from_bundle_strict_config(bundle, &cloud_config_base_dir)? | |
| } else { | |
| CloudConfigBundleLayers::from_bundle(bundle, &cloud_config_base_dir)? | |
| }; | |
| let CloudConfigBundleLayers { | |
| enterprise_managed_config, | |
| enterprise_managed_requirements, | |
| } = bundle_layers; | |
| bundle_requirements_layers = enterprise_managed_requirements; | |
| cloud_config_layers = enterprise_managed_config; | |
| } | |
| let (config_requirements_toml, loaded_config_layers, requirements_layers) = | |
| managed_requirements::load_requirements_from_sources( | |
| fs, | |
| codex_home, | |
| &overrides, | |
| strict_config, | |
| bundle_requirements_layers, | |
| ) | |
| .await?; | |
| let mut startup_warnings = loaded_config_layers.startup_warnings.clone(); | |
| let thread_config_context = ThreadConfigContext { | |
| thread_id: None, | |
| cwd: cwd.clone(), | |
| }; | |
| let thread_config_layers = thread_config_loader | |
| .load_config_layers(thread_config_context) | |
| .await | |
| .map_err(io::Error::other)?; | |
| let mut layers = Vec::<ConfigLayerEntry>::new(); | |
| layers.push(packaged_defaults_layer); | |
| let cli_overrides_layer = if cli_overrides.is_empty() { | |
| None | |
| } else { | |
| let cli_overrides_layer = build_cli_overrides_layer(cli_overrides); | |
| let base_dir = cwd | |
| .as_ref() | |
| .map(AbsolutePathBuf::as_path) | |
| .unwrap_or(codex_home); | |
| if strict_config { | |
| validate_cli_overrides_strictly(&cli_overrides_layer, base_dir)?; | |
| } | |
| Some(resolve_relative_paths_in_config_toml( | |
| cli_overrides_layer, | |
| base_dir, | |
| )?) | |
| }; | |
| // Include an entry for the "system" config folder, loading its config.toml, | |
| // if it exists. | |
| let system_config_toml_file = system_config_toml_file_with_overrides(&overrides)?; | |
| let system_layer = load_config_toml_for_required_layer( | |
| fs, | |
| &system_config_toml_file, | |
| strict_config, | |
| |config_toml| { | |
| ConfigLayerEntry::new( | |
| ConfigLayerSource::System { | |
| file: system_config_toml_file.clone(), | |
| }, | |
| config_toml, | |
| ) | |
| }, | |
| ) | |
| .await?; | |
| layers.push(system_layer); | |
| layers.extend(cloud_config_layers); | |
| // Add the base user config layer. When profile-v2 is selected, add the | |
| // profile config as a second user layer on top so the profile only needs to | |
| // contain overrides. | |
| let active_user_file = overrides.user_config_path(codex_home)?; | |
| let base_user_file = AbsolutePathBuf::resolve_path_against_base(CONFIG_TOML_FILE, codex_home); | |
| let base_user_layer = load_user_config_layer( | |
| fs, | |
| &base_user_file, | |
| /*profile*/ None, | |
| ignore_user_config, | |
| strict_config, | |
| ) | |
| .await?; | |
| if let Some(active_user_profile) = active_user_profile.as_ref() | |
| && let Some(base_user_config) = base_user_layer.config.as_table() | |
| { | |
| let legacy_profile_is_selected = base_user_config | |
| .get("profile") | |
| .and_then(TomlValue::as_str) | |
| .is_some_and(|profile| profile == active_user_profile.as_str()); | |
| let legacy_profile_table_exists = base_user_config | |
| .get("profiles") | |
| .and_then(TomlValue::as_table) | |
| .is_some_and(|profiles| profiles.contains_key(active_user_profile.as_str())); | |
| if legacy_profile_is_selected || legacy_profile_table_exists { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "--profile `{active_user_profile}` cannot be used while {} contains legacy `profile = \"{active_user_profile}\"` or `[profiles.{active_user_profile}]` config; move those settings into {} and remove the legacy profile selector/table. See https://developers.openai.com/codex/config-advanced#profiles for more information.", | |
| base_user_file.as_path().display(), | |
| active_user_file.as_path().display() | |
| ), | |
| )); | |
| } | |
| } | |
| layers.push(base_user_layer); | |
| if active_user_file != base_user_file { | |
| layers.push( | |
| load_user_config_layer( | |
| fs, | |
| &active_user_file, | |
| active_user_profile.as_ref(), | |
| ignore_user_config, | |
| strict_config, | |
| ) | |
| .await?, | |
| ); | |
| } | |
| if !ignore_project_config && let Some(cwd) = cwd { | |
| let mut merged_so_far = TomlValue::Table(toml::map::Map::new()); | |
| for layer in &layers { | |
| merge_toml_values(&mut merged_so_far, &layer.config); | |
| } | |
| if let Some(cli_overrides_layer) = cli_overrides_layer.as_ref() { | |
| merge_toml_values(&mut merged_so_far, cli_overrides_layer); | |
| } | |
| let trusted_broker_config = credential_broker_trusted_config( | |
| &merged_so_far, | |
| &thread_config_layers, | |
| &loaded_config_layers, | |
| ); | |
| // Managed config wins over CLI config. Apply it before choosing the | |
| // project root and trust, but keep its final layers above project config. | |
| project_discovery::merge_managed_config_for_discovery( | |
| &mut merged_so_far, | |
| &loaded_config_layers, | |
| codex_home, | |
| )?; | |
| let project_root_markers = match project_root_markers_from_config(&merged_so_far) { | |
| Ok(markers) => markers.unwrap_or_else(default_project_root_markers), | |
| Err(err) => { | |
| if let Some(config_error) = first_layer_config_error_from_entries(&layers).await { | |
| return Err(io_error_from_config_error( | |
| io::ErrorKind::InvalidData, | |
| config_error, | |
| /*source*/ None, | |
| )); | |
| } | |
| return Err(err); | |
| } | |
| }; | |
| let mut project_trust_context = match project_trust_context( | |
| fs, | |
| &merged_so_far, | |
| &trusted_broker_config, | |
| &cwd, | |
| &project_root_markers, | |
| codex_home, | |
| &active_user_file, | |
| ) | |
| .await | |
| { | |
| Ok(context) => context, | |
| Err(err) => { | |
| let source = err | |
| .get_ref() | |
| .and_then(|err| err.downcast_ref::<toml::de::Error>()) | |
| .cloned(); | |
| if let Some(config_error) = first_layer_config_error_from_entries(&layers).await { | |
| return Err(io_error_from_config_error( | |
| io::ErrorKind::InvalidData, | |
| config_error, | |
| source, | |
| )); | |
| } | |
| return Err(err); | |
| } | |
| }; | |
| apply_credential_broker_requirements(&mut project_trust_context, &config_requirements_toml); | |
| let project_layers = load_project_layers( | |
| fs, | |
| &cwd, | |
| &project_trust_context.project_root, | |
| &project_trust_context, | |
| codex_home, | |
| strict_config, | |
| ) | |
| .await?; | |
| layers.extend(project_layers.layers); | |
| startup_warnings.extend(project_layers.startup_warnings); | |
| } | |
| // Add a layer for runtime overrides from the CLI or UI, if any exist. | |
| if let Some(cli_overrides_layer) = cli_overrides_layer { | |
| layers.push(ConfigLayerEntry::new( | |
| ConfigLayerSource::SessionFlags, | |
| cli_overrides_layer, | |
| )); | |
| } | |
| for thread_config_layer in thread_config_layers { | |
| insert_layer_by_precedence(&mut layers, thread_config_layer); | |
| } | |
| // Make a best-effort to support the legacy `managed_config.toml` as a | |
| // config layer on top of everything else. For fields in | |
| // `managed_config.toml` that do not have an equivalent in | |
| // `ConfigRequirements`, note users can still override these values on a | |
| // per-turn basis in the TUI and VS Code. | |
| 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(|| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "Managed config file {} has no parent directory", | |
| config.file.as_path().display() | |
| ), | |
| ) | |
| })?; | |
| let managed_config = | |
| resolve_relative_paths_in_config_toml(config.managed_config, managed_parent)?; | |
| layers.push(ConfigLayerEntry::new( | |
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { file: config.file }, | |
| managed_config, | |
| )); | |
| } | |
| if let Some(config) = managed_config_from_mdm { | |
| // As a general rule, config from MDM should _not_ include relative | |
| // paths, starting with `./`, but a path starting with `~/` _is_ a | |
| // supported use case. Because resolve_relative_paths_in_config_toml() | |
| // relies on AbsolutePathBufGuard to resolve `~/`, we must supply a | |
| // value for base_dir. Preserve that same base on the layer so later | |
| // raw-TOML diagnostics parse with the same path semantics. | |
| let raw_toml_base_dir = AbsolutePathBuf::from_absolute_path(codex_home)?; | |
| let managed_config = resolve_relative_paths_in_config_toml( | |
| config.managed_config, | |
| raw_toml_base_dir.as_path(), | |
| )?; | |
| layers.push(ConfigLayerEntry::new_with_raw_toml( | |
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm, | |
| managed_config, | |
| config.raw_toml, | |
| raw_toml_base_dir, | |
| )); | |
| } | |
| if let Err(err) = validate_enabled_config_layers(&layers) { | |
| if let Some(config_error) = typed_first_layer_config_error_from_entries::< | |
| ShellEnvironmentPolicyFilterConfigToml, | |
| >(&layers, CONFIG_TOML_FILE) | |
| .await | |
| { | |
| return Err(io_error_from_config_error( | |
| io::ErrorKind::InvalidData, | |
| config_error, | |
| /*source*/ None, | |
| )); | |
| } | |
| return Err(err); | |
| } | |
| let config_layer_stack = ConfigLayerStack::new( | |
| layers, | |
| config_requirements_toml.clone().try_into()?, | |
| config_requirements_toml.into_toml(), | |
| )? | |
| .with_user_and_project_exec_policy_rules_ignored(ignore_user_and_project_exec_policy_rules); | |
| startup_warnings.extend(ignored_config_warning( | |
| &config_layer_stack, | |
| &requirements_layers, | |
| )); | |
| Ok(config_layer_stack.with_startup_warnings(startup_warnings)) | |
| } | |
| async fn load_user_config_layer( | |
| fs: &dyn ExecutorFileSystem, | |
| user_file: &AbsolutePathBuf, | |
| profile: Option<&ProfileV2Name>, | |
| ignore_user_config: bool, | |
| strict_config: bool, | |
| ) -> io::Result<ConfigLayerEntry> { | |
| let profile = profile.map(ToString::to_string); | |
| if ignore_user_config { | |
| return Ok(ConfigLayerEntry::new( | |
| ConfigLayerSource::User { | |
| file: user_file.clone(), | |
| profile, | |
| }, | |
| TomlValue::Table(toml::map::Map::new()), | |
| )); | |
| } | |
| load_config_toml_for_required_layer(fs, user_file, strict_config, |config_toml| { | |
| ConfigLayerEntry::new( | |
| ConfigLayerSource::User { | |
| file: user_file.clone(), | |
| profile: profile.clone(), | |
| }, | |
| config_toml, | |
| ) | |
| }) | |
| .await | |
| } | |
| fn insert_layer_by_precedence(layers: &mut Vec<ConfigLayerEntry>, layer: ConfigLayerEntry) { | |
| match layers | |
| .iter() | |
| .position(|existing| existing.name.precedence() > layer.name.precedence()) | |
| { | |
| Some(index) => layers.insert(index, layer), | |
| None => layers.push(layer), | |
| } | |
| } | |
| /// Attempts to load a config.toml file from `config_toml`. | |
| /// - If the file exists and is valid TOML, passes the parsed `toml::Value` to | |
| /// `create_entry` and returns the resulting layer entry. | |
| /// - If the file does not exist, uses an empty `Table` with `create_entry` and | |
| /// returns the resulting layer entry. | |
| /// - If there is an error reading the file or parsing the TOML, returns an | |
| /// error. | |
| async fn load_config_toml_for_required_layer( | |
| fs: &dyn ExecutorFileSystem, | |
| toml_file: &AbsolutePathBuf, | |
| strict_config: bool, | |
| create_entry: impl FnOnce(TomlValue) -> ConfigLayerEntry, | |
| ) -> io::Result<ConfigLayerEntry> { | |
| let loaded = load_config_toml_for_required_layer_raw(fs, toml_file, strict_config).await?; | |
| let toml_value = resolve_relative_paths_in_config_toml(loaded.toml, loaded.base_dir.as_path())?; | |
| Ok(create_entry(toml_value)) | |
| } | |
| struct LoadedTomlFile { | |
| toml: TomlValue, | |
| base_dir: AbsolutePathBuf, | |
| } | |
| async fn load_config_toml_for_required_layer_raw( | |
| fs: &dyn ExecutorFileSystem, | |
| toml_file: &AbsolutePathBuf, | |
| strict_config: bool, | |
| ) -> io::Result<LoadedTomlFile> { | |
| let config_parent = toml_file.as_path().parent().ok_or_else(|| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "Config file {} has no parent directory", | |
| toml_file.as_path().display() | |
| ), | |
| ) | |
| })?; | |
| let base_dir = AbsolutePathBuf::from_absolute_path(config_parent)?; | |
| let toml_file_uri = PathUri::from_abs_path(toml_file); | |
| let toml_value = match fs | |
| .read_file_text(&toml_file_uri, Default::default(), /*sandbox*/ None) | |
| .await | |
| { | |
| Ok(contents) => { | |
| let config: TomlValue = toml::from_str(&contents).map_err(|err| { | |
| let config_error = | |
| config_error_from_toml(toml_file.as_path(), &contents, err.clone()); | |
| io_error_from_config_error(io::ErrorKind::InvalidData, config_error, Some(err)) | |
| })?; | |
| if strict_config { | |
| validate_config_toml_strictly( | |
| toml_file.as_path(), | |
| &contents, | |
| &config, | |
| config_parent, | |
| )?; | |
| } | |
| Ok(config) | |
| } | |
| Err(e) => { | |
| if e.kind() == io::ErrorKind::NotFound { | |
| Ok(TomlValue::Table(toml::map::Map::new())) | |
| } else { | |
| Err(io::Error::new( | |
| e.kind(), | |
| format!( | |
| "Failed to read config file {}: {e}", | |
| toml_file.as_path().display() | |
| ), | |
| )) | |
| } | |
| } | |
| }?; | |
| Ok(LoadedTomlFile { | |
| toml: toml_value, | |
| base_dir, | |
| }) | |
| } | |
| fn validate_config_toml_strictly( | |
| toml_file: &Path, | |
| contents: &str, | |
| value: &TomlValue, | |
| base_dir: &Path, | |
| ) -> io::Result<()> { | |
| let _guard = AbsolutePathBufGuard::new(base_dir); | |
| if let Some(config_error) = config_error_from_ignored_toml_value_fields::<ConfigToml>( | |
| toml_file, | |
| contents, | |
| value.clone(), | |
| ) { | |
| Err(io_error_from_config_error( | |
| io::ErrorKind::InvalidData, | |
| config_error, | |
| /*source*/ None, | |
| )) | |
| } else { | |
| Ok(()) | |
| } | |
| } | |
| fn validate_cli_overrides_strictly( | |
| cli_overrides_layer: &TomlValue, | |
| base_dir: &Path, | |
| ) -> io::Result<()> { | |
| let _guard = AbsolutePathBufGuard::new(base_dir); | |
| if let Some(path) = ignored_toml_value_fields::<ConfigToml>(cli_overrides_layer.clone()).first() | |
| { | |
| let ignored_path = path.join("."); | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!("unknown configuration field `{ignored_path}` in -c/--config override"), | |
| )); | |
| } | |
| if let Some(ignored_path) = unknown_feature_toml_value_field(cli_overrides_layer) { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!("unknown configuration field `{ignored_path}` in -c/--config override"), | |
| )); | |
| } | |
| Ok(()) | |
| } | |
| /// If available, load requirements from the platform's system `requirements.toml` | |
| /// location as a requirements layer. | |
| pub async fn load_requirements_toml( | |
| fs: &dyn ExecutorFileSystem, | |
| requirements_toml_file: &AbsolutePathBuf, | |
| ) -> io::Result<Option<RequirementsLayerEntry>> { | |
| let requirements_toml_file_uri = PathUri::from_abs_path(requirements_toml_file); | |
| match fs | |
| .read_file_text( | |
| &requirements_toml_file_uri, | |
| Default::default(), | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| { | |
| Ok(contents) => { | |
| let requirements_parent = requirements_toml_file.parent().ok_or_else(|| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "Requirements file {} has no parent directory", | |
| requirements_toml_file.as_ref().display() | |
| ), | |
| ) | |
| })?; | |
| let base_dir = AbsolutePathBuf::from_absolute_path(requirements_parent)?; | |
| Ok(Some( | |
| RequirementsLayerEntry::from_toml( | |
| RequirementSource::SystemRequirementsToml { | |
| file: requirements_toml_file.clone(), | |
| }, | |
| contents, | |
| ) | |
| .with_base_dir(base_dir), | |
| )) | |
| } | |
| Err(e) => { | |
| if e.kind() != io::ErrorKind::NotFound { | |
| Err(io::Error::new( | |
| e.kind(), | |
| format!( | |
| "Failed to read requirements file {}: {e}", | |
| requirements_toml_file.as_path().display(), | |
| ), | |
| )) | |
| } else { | |
| Ok(None) | |
| } | |
| } | |
| } | |
| } | |
| fn system_requirements_toml_file() -> io::Result<AbsolutePathBuf> { | |
| AbsolutePathBuf::from_absolute_path(Path::new("/etc/codex/requirements.toml")) | |
| } | |
| fn system_requirements_toml_file() -> io::Result<AbsolutePathBuf> { | |
| windows_system_requirements_toml_file() | |
| } | |
| fn system_requirements_toml_file_with_overrides( | |
| overrides: &LoaderOverrides, | |
| ) -> io::Result<AbsolutePathBuf> { | |
| match &overrides.system_requirements_path { | |
| Some(path) => AbsolutePathBuf::from_absolute_path(path), | |
| None => system_requirements_toml_file(), | |
| } | |
| } | |
| /// Check local managed configuration sources without loading or parsing configuration. | |
| /// | |
| /// 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> { | |
| 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> { | |
| let _ = codex_home; | |
| if layer_io::managed_config_default_path(codex_home).try_exists()? { | |
| return Ok(true); | |
| } | |
| if system_requirements_path.try_exists()? { | |
| return Ok(true); | |
| } | |
| if macos::has_managed_preferences()? { | |
| return Ok(true); | |
| } | |
| Ok(false) | |
| } | |
| pub fn system_config_toml_file() -> io::Result<AbsolutePathBuf> { | |
| AbsolutePathBuf::from_absolute_path(Path::new(SYSTEM_CONFIG_TOML_FILE_UNIX)) | |
| } | |
| pub fn system_config_toml_file() -> io::Result<AbsolutePathBuf> { | |
| windows_system_config_toml_file() | |
| } | |
| fn system_config_toml_file_with_overrides( | |
| overrides: &LoaderOverrides, | |
| ) -> io::Result<AbsolutePathBuf> { | |
| match &overrides.system_config_path { | |
| Some(path) => AbsolutePathBuf::from_absolute_path(path), | |
| None => system_config_toml_file(), | |
| } | |
| } | |
| fn windows_codex_system_dir() -> PathBuf { | |
| let program_data = windows_program_data_dir_from_known_folder().unwrap_or_else(|err| { | |
| tracing::warn!( | |
| error = %err, | |
| "Failed to resolve ProgramData known folder; using default path" | |
| ); | |
| PathBuf::from(DEFAULT_PROGRAM_DATA_DIR_WINDOWS) | |
| }); | |
| program_data.join("OpenAI").join("Codex") | |
| } | |
| fn windows_system_requirements_toml_file() -> io::Result<AbsolutePathBuf> { | |
| let requirements_toml_file = windows_codex_system_dir().join("requirements.toml"); | |
| AbsolutePathBuf::try_from(requirements_toml_file) | |
| } | |
| fn windows_system_config_toml_file() -> io::Result<AbsolutePathBuf> { | |
| let config_toml_file = windows_codex_system_dir().join("config.toml"); | |
| AbsolutePathBuf::try_from(config_toml_file) | |
| } | |
| fn windows_program_data_dir_from_known_folder() -> io::Result<PathBuf> { | |
| use std::ffi::OsString; | |
| use std::os::windows::ffi::OsStringExt; | |
| use windows_sys::Win32::System::Com::CoTaskMemFree; | |
| use windows_sys::Win32::UI::Shell::FOLDERID_ProgramData; | |
| use windows_sys::Win32::UI::Shell::KF_FLAG_DEFAULT; | |
| use windows_sys::Win32::UI::Shell::SHGetKnownFolderPath; | |
| let mut path_ptr = std::ptr::null_mut::<u16>(); | |
| let known_folder_flags = u32::try_from(KF_FLAG_DEFAULT).map_err(|_| { | |
| io::Error::other(format!( | |
| "KF_FLAG_DEFAULT did not fit in u32: {KF_FLAG_DEFAULT}" | |
| )) | |
| })?; | |
| // Known folder IDs reference: | |
| // https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid | |
| // SAFETY: SHGetKnownFolderPath initializes path_ptr with a CoTaskMem-allocated, | |
| // null-terminated UTF-16 string on success. | |
| let hr = unsafe { | |
| SHGetKnownFolderPath(&FOLDERID_ProgramData, known_folder_flags, 0, &mut path_ptr) | |
| }; | |
| if hr != 0 { | |
| return Err(io::Error::other(format!( | |
| "SHGetKnownFolderPath(FOLDERID_ProgramData) failed with HRESULT {hr:#010x}" | |
| ))); | |
| } | |
| if path_ptr.is_null() { | |
| return Err(io::Error::other( | |
| "SHGetKnownFolderPath(FOLDERID_ProgramData) returned a null pointer", | |
| )); | |
| } | |
| // SAFETY: path_ptr is a valid null-terminated UTF-16 string allocated by | |
| // SHGetKnownFolderPath and must be freed with CoTaskMemFree. | |
| let path = unsafe { | |
| let mut len = 0usize; | |
| while *path_ptr.add(len) != 0 { | |
| len += 1; | |
| } | |
| let wide = std::slice::from_raw_parts(path_ptr, len); | |
| let path = PathBuf::from(OsString::from_wide(wide)); | |
| CoTaskMemFree(path_ptr.cast()); | |
| path | |
| }; | |
| Ok(path) | |
| } | |
| fn requirements_layers_from_legacy_scheme( | |
| loaded_config_layers: LoadedConfigLayers, | |
| codex_home: &Path, | |
| ) -> io::Result<Vec<RequirementsLayerEntry>> { | |
| // List the file-backed legacy layer first because requirements layers are | |
| // composed lowest-precedence to highest-precedence, and MDM has higher | |
| // precedence than the legacy managed_config.toml file. | |
| let LoadedConfigLayers { | |
| managed_config, | |
| managed_config_from_mdm, | |
| .. | |
| } = loaded_config_layers; | |
| let layer_count = | |
| usize::from(managed_config.is_some()) + usize::from(managed_config_from_mdm.is_some()); | |
| let mut layers = Vec::with_capacity(layer_count); | |
| let codex_home = AbsolutePathBuf::from_absolute_path(codex_home)?; | |
| for (source, config, base_dir) in managed_config | |
| .map(|c| { | |
| let base_dir = c.file.parent().ok_or_else(|| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "Managed config file {} has no parent directory", | |
| c.file.as_path().display() | |
| ), | |
| ) | |
| })?; | |
| Ok::<_, io::Error>(( | |
| RequirementSource::LegacyManagedConfigTomlFromFile { file: c.file }, | |
| c.managed_config, | |
| base_dir, | |
| )) | |
| }) | |
| .transpose()? | |
| .into_iter() | |
| .chain(managed_config_from_mdm.map(|config| { | |
| ( | |
| RequirementSource::LegacyManagedConfigTomlFromMdm, | |
| config.managed_config, | |
| codex_home.clone(), | |
| ) | |
| })) | |
| { | |
| let legacy_config: LegacyManagedConfigToml = | |
| config.try_into().map_err(|err: toml::de::Error| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!("Failed to parse config requirements as TOML: {err}"), | |
| ) | |
| })?; | |
| layers.push( | |
| RequirementsLayerEntry::from_toml_value( | |
| source, | |
| legacy_requirements_to_toml_value(legacy_config)?, | |
| ) | |
| .with_base_dir(base_dir), | |
| ); | |
| } | |
| Ok(layers) | |
| } | |
| fn legacy_requirements_to_toml_value(legacy: LegacyManagedConfigToml) -> io::Result<TomlValue> { | |
| let LegacyManagedConfigToml { | |
| approval_policy, | |
| approvals_reviewer, | |
| sandbox_mode, | |
| } = legacy; | |
| let mut table = toml::map::Map::new(); | |
| if let Some(approval_policy) = approval_policy { | |
| table.insert( | |
| "allowed_approval_policies".to_string(), | |
| toml_value_from_serializable(vec![approval_policy])?, | |
| ); | |
| } | |
| if let Some(approvals_reviewer) = approvals_reviewer { | |
| let mut allowed_reviewers = vec![approvals_reviewer]; | |
| if approvals_reviewer == ApprovalsReviewer::AutoReview { | |
| allowed_reviewers.push(ApprovalsReviewer::User); | |
| } | |
| table.insert( | |
| "allowed_approvals_reviewers".to_string(), | |
| toml_value_from_serializable(allowed_reviewers)?, | |
| ); | |
| } | |
| if let Some(sandbox_mode) = sandbox_mode { | |
| let required_mode: SandboxModeRequirement = sandbox_mode.into(); | |
| // Allowing read-only is a requirement for Codex to function correctly. | |
| // So in this backfill path, we append read-only if it's not already specified. | |
| let mut allowed_modes = vec![SandboxModeRequirement::ReadOnly]; | |
| if required_mode != SandboxModeRequirement::ReadOnly { | |
| allowed_modes.push(required_mode); | |
| } | |
| table.insert( | |
| "allowed_sandbox_modes".to_string(), | |
| toml_value_from_serializable(allowed_modes)?, | |
| ); | |
| } | |
| Ok(TomlValue::Table(table)) | |
| } | |
| fn toml_value_from_serializable<T: serde::Serialize>(value: T) -> io::Result<TomlValue> { | |
| TomlValue::try_from(value).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) | |
| } | |
| enum CredentialBrokerProjectState { | |
| Unconfigured, | |
| Disabled, | |
| Enabled, | |
| } | |
| fn apply_credential_broker_requirements( | |
| context: &mut ProjectTrustContext, | |
| requirements: &crate::ConfigRequirementsWithSources, | |
| ) { | |
| if context.credential_broker == CredentialBrokerProjectState::Unconfigured { | |
| return; | |
| } | |
| let enabled = requirements | |
| .feature_requirements | |
| .as_ref() | |
| .and_then(|requirements| requirements.entries.get("network_proxy")) | |
| .copied() | |
| .unwrap_or(context.credential_broker == CredentialBrokerProjectState::Enabled); | |
| context.credential_broker = if enabled | |
| && requirements | |
| .network | |
| .as_ref() | |
| .is_none_or(|network| network.enabled != Some(false)) | |
| { | |
| CredentialBrokerProjectState::Enabled | |
| } else { | |
| CredentialBrokerProjectState::Disabled | |
| }; | |
| } | |
| struct ProjectTrustContext { | |
| project_root: AbsolutePathBuf, | |
| project_root_key: String, | |
| project_root_lookup_keys: Vec<String>, | |
| checkout_root: Option<AbsolutePathBuf>, | |
| repo_root: Option<AbsolutePathBuf>, | |
| repo_root_key: Option<String>, | |
| repo_root_lookup_keys: Option<Vec<String>>, | |
| projects_trust: std::collections::HashMap<String, TrustLevel>, | |
| user_config_file: AbsolutePathBuf, | |
| credential_broker: CredentialBrokerProjectState, | |
| credential_broker_provider_env_keys: Vec<String>, | |
| } | |
| struct ProjectTrustConfigToml { | |
| projects: Option<std::collections::HashMap<String, ProjectConfig>>, | |
| } | |
| struct ProjectTrustDecision { | |
| trust_level: Option<TrustLevel>, | |
| trust_key: String, | |
| } | |
| impl ProjectTrustDecision { | |
| fn is_trusted(&self) -> bool { | |
| matches!(self.trust_level, Some(TrustLevel::Trusted)) | |
| } | |
| } | |
| impl ProjectTrustContext { | |
| fn decision_for_dir(&self, dir: &AbsolutePathBuf) -> ProjectTrustDecision { | |
| for dir_key in normalized_project_trust_keys(dir.as_path()) { | |
| if let Some((trust_key, trust_level)) = | |
| project_trust_for_lookup_key(&self.projects_trust, &dir_key) | |
| { | |
| return ProjectTrustDecision { | |
| trust_level: Some(trust_level), | |
| trust_key, | |
| }; | |
| } | |
| } | |
| for project_root_key in &self.project_root_lookup_keys { | |
| if let Some((trust_key, trust_level)) = | |
| project_trust_for_lookup_key(&self.projects_trust, project_root_key) | |
| { | |
| return ProjectTrustDecision { | |
| trust_level: Some(trust_level), | |
| trust_key, | |
| }; | |
| } | |
| } | |
| if let Some(repo_root_lookup_keys) = self.repo_root_lookup_keys.as_ref() { | |
| for repo_root_key in repo_root_lookup_keys { | |
| if let Some((trust_key, trust_level)) = | |
| project_trust_for_lookup_key(&self.projects_trust, repo_root_key) | |
| { | |
| return ProjectTrustDecision { | |
| trust_level: Some(trust_level), | |
| trust_key, | |
| }; | |
| } | |
| } | |
| } | |
| ProjectTrustDecision { | |
| trust_level: None, | |
| trust_key: self | |
| .repo_root_key | |
| .clone() | |
| .unwrap_or_else(|| self.project_root_key.clone()), | |
| } | |
| } | |
| fn disabled_reason_for_decision(&self, decision: &ProjectTrustDecision) -> Option<String> { | |
| if decision.is_trusted() { | |
| return None; | |
| } | |
| let gated_features = "project-local config, hooks, and exec policies"; | |
| let trust_key = decision.trust_key.as_str(); | |
| let user_config_file = self.user_config_file.as_path().display(); | |
| // Trust may come from managed config. Keep the explicit-untrusted prefix | |
| // stable because the remote TUI uses it to recognize existing decisions. | |
| match decision.trust_level { | |
| Some(TrustLevel::Untrusted) => Some(format!( | |
| "{trust_key} is marked as untrusted in the effective configuration. To load {gated_features}, update its trust setting. If that setting is managed by your organization, contact your administrator." | |
| )), | |
| _ => Some(format!( | |
| "To load {gated_features}, add {trust_key} as a trusted project in {user_config_file}." | |
| )), | |
| } | |
| } | |
| fn root_checkout_hooks_folder_for_dir(&self, dir: &AbsolutePathBuf) -> Option<AbsolutePathBuf> { | |
| let checkout_root = self.checkout_root.as_ref()?; | |
| let repo_root = self.repo_root.as_ref()?; | |
| // Regular checkouts resolve both paths to the same root; linked worktrees do not. | |
| if checkout_root == repo_root { | |
| return None; | |
| } | |
| let relative_dir = dir.as_path().strip_prefix(checkout_root.as_path()).ok()?; | |
| Some(repo_root.join(relative_dir).join(".codex")) | |
| } | |
| } | |
| fn project_layer_entry( | |
| dot_codex_folder: &AbsolutePathBuf, | |
| config: TomlValue, | |
| disabled_reason: Option<String>, | |
| hooks_config_folder_override: Option<AbsolutePathBuf>, | |
| ) -> ConfigLayerEntry { | |
| let source = ConfigLayerSource::Project { | |
| dot_codex_folder: dot_codex_folder.clone(), | |
| }; | |
| let entry = if let Some(reason) = disabled_reason { | |
| ConfigLayerEntry::new_disabled(source, config, reason) | |
| } else { | |
| ConfigLayerEntry::new(source, config) | |
| }; | |
| entry.with_hooks_config_folder_override(hooks_config_folder_override) | |
| } | |
| fn sanitize_project_config( | |
| config: &mut TomlValue, | |
| credential_broker: CredentialBrokerProjectState, | |
| trusted_provider_env_keys: &[String], | |
| ) -> Vec<String> { | |
| let Some(table) = config.as_table_mut() else { | |
| return Vec::new(); | |
| }; | |
| let credential_broker_configured = | |
| credential_broker != CredentialBrokerProjectState::Unconfigured; | |
| let mut ignored_keys = Vec::new(); | |
| for key in PROJECT_LOCAL_CONFIG_DENYLIST { | |
| if table.remove(*key).is_some() { | |
| ignored_keys.push((*key).to_string()); | |
| } | |
| } | |
| if let Some(features) = table.get_mut("features").and_then(TomlValue::as_table_mut) { | |
| if credential_broker == CredentialBrokerProjectState::Enabled | |
| && features.remove("shell_snapshot").is_some() | |
| { | |
| ignored_keys.push("features.shell_snapshot".to_string()); | |
| } | |
| if features.remove("respect_system_proxy").is_some() { | |
| ignored_keys.push("features.respect_system_proxy".to_string()); | |
| } | |
| if credential_broker_configured | |
| && features | |
| .get("network_proxy") | |
| .is_some_and(|network_proxy| network_proxy.as_bool().is_some()) | |
| { | |
| features.remove("network_proxy"); | |
| ignored_keys.push("features.network_proxy".to_string()); | |
| } else if let Some(network_proxy) = features | |
| .get_mut("network_proxy") | |
| .and_then(TomlValue::as_table_mut) | |
| { | |
| if network_proxy.remove("credential_broker").is_some() { | |
| ignored_keys.push("features.network_proxy.credential_broker".to_string()); | |
| } | |
| if network_proxy.remove("credentials").is_some() { | |
| ignored_keys.push("features.network_proxy.credentials".to_string()); | |
| } | |
| if credential_broker_configured && network_proxy.remove("enabled").is_some() { | |
| ignored_keys.push("features.network_proxy.enabled".to_string()); | |
| } | |
| } | |
| } | |
| if credential_broker == CredentialBrokerProjectState::Enabled | |
| && let Some(policy) = table | |
| .get_mut("shell_environment_policy") | |
| .and_then(TomlValue::as_table_mut) | |
| { | |
| if policy.remove("experimental_use_profile").is_some() { | |
| ignored_keys.push("shell_environment_policy.experimental_use_profile".to_string()); | |
| } | |
| let environment_keys_match = |left: &str, right: &str| { | |
| left == right || cfg!(windows) && left.eq_ignore_ascii_case(right) | |
| }; | |
| if let Some(overrides) = policy.get_mut("set").and_then(TomlValue::as_table_mut) { | |
| overrides.retain(|key, _| { | |
| if key.eq_ignore_ascii_case("ZDOTDIR") | |
| || key.eq_ignore_ascii_case("BASH_ENV") | |
| || is_credential_broker_provider_env_key(key) | |
| || trusted_provider_env_keys | |
| .iter() | |
| .any(|provider_key| environment_keys_match(key, provider_key)) | |
| { | |
| ignored_keys.push(format!("shell_environment_policy.set.{key}")); | |
| false | |
| } else { | |
| true | |
| } | |
| }); | |
| } | |
| } | |
| // Repository contents must not turn an ordinary key into a permission increase. | |
| if let Some(chat) = table | |
| .get_mut("tui") | |
| .and_then(|tui| tui.get_mut("keymap")) | |
| .and_then(|keymap| keymap.get_mut("chat")) | |
| .and_then(TomlValue::as_table_mut) | |
| { | |
| for key in ["previous_permission_mode", "next_permission_mode"] { | |
| if chat.remove(key).is_some() { | |
| ignored_keys.push(format!("tui.keymap.chat.{key}")); | |
| } | |
| } | |
| } | |
| ignored_keys | |
| } | |
| fn project_ignored_config_keys_warning( | |
| dot_codex_folder: &AbsolutePathBuf, | |
| ignored_keys: &[String], | |
| ) -> String { | |
| let config_path = dot_codex_folder.join(CONFIG_TOML_FILE); | |
| let ignored_keys = ignored_keys.join(", "); | |
| format!( | |
| concat!( | |
| "Ignored unsupported project-local config keys in {config_path}: {ignored_keys}. ", | |
| "If you want these settings to apply, manually set them in your ", | |
| "user-level config.toml." | |
| ), | |
| config_path = config_path.display(), | |
| ignored_keys = ignored_keys, | |
| ) | |
| } | |
| async fn project_trust_context( | |
| fs: &dyn ExecutorFileSystem, | |
| merged_config: &TomlValue, | |
| trusted_broker_config: &TomlValue, | |
| cwd: &AbsolutePathBuf, | |
| project_root_markers: &[String], | |
| config_base_dir: &Path, | |
| user_config_file: &AbsolutePathBuf, | |
| ) -> io::Result<ProjectTrustContext> { | |
| let project_trust_config: ProjectTrustConfigToml = { | |
| let _guard = AbsolutePathBufGuard::new(config_base_dir); | |
| merged_config | |
| .clone() | |
| .try_into() | |
| .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))? | |
| }; | |
| let project_root = find_project_root(fs, cwd, project_root_markers).await?; | |
| let projects = project_trust_config.projects.unwrap_or_default(); | |
| let project_root_lookup_keys = normalized_project_trust_keys(project_root.as_path()); | |
| let project_root_key = project_root_lookup_keys | |
| .first() | |
| .cloned() | |
| .unwrap_or_else(|| project_trust_key(project_root.as_path())); | |
| let checkout_root = find_git_checkout_root(fs, cwd).await; | |
| let repo_root = resolve_root_git_project_for_trust(fs, cwd).await; | |
| let repo_root_lookup_keys = repo_root | |
| .as_ref() | |
| .map(|root| normalized_project_trust_keys(root.as_path())); | |
| let repo_root_key = repo_root_lookup_keys | |
| .as_ref() | |
| .and_then(|keys| keys.first().cloned()); | |
| let projects_trust = projects | |
| .into_iter() | |
| .filter_map(|(key, project)| project.trust_level.map(|trust_level| (key, trust_level))) | |
| .collect(); | |
| let network_proxy = trusted_broker_config | |
| .get("features") | |
| .and_then(|features| features.get("network_proxy")) | |
| .and_then(TomlValue::as_table); | |
| let credential_broker_configured = network_proxy.is_some_and(|network_proxy| { | |
| network_proxy.get("credential_broker") == Some(&TomlValue::Boolean(true)) | |
| }); | |
| let credential_broker = if credential_broker_configured { | |
| if network_proxy | |
| .and_then(|network_proxy| network_proxy.get("enabled")) | |
| .and_then(TomlValue::as_bool) | |
| .unwrap_or(false) | |
| { | |
| CredentialBrokerProjectState::Enabled | |
| } else { | |
| CredentialBrokerProjectState::Disabled | |
| } | |
| } else { | |
| CredentialBrokerProjectState::Unconfigured | |
| }; | |
| let configured_providers = network_proxy | |
| .and_then(|network_proxy| network_proxy.get("credentials")) | |
| .and_then(TomlValue::as_table); | |
| let credential_broker_provider_env_keys = configured_providers | |
| .into_iter() | |
| .flat_map(|providers| providers.values()) | |
| .flat_map(|provider| { | |
| provider | |
| .get("env") | |
| .and_then(TomlValue::as_array) | |
| .into_iter() | |
| .flatten() | |
| .filter_map(TomlValue::as_str) | |
| .chain( | |
| provider | |
| .get("url_prefix_from_env") | |
| .and_then(TomlValue::as_str), | |
| ) | |
| .map(str::to_string) | |
| }) | |
| .collect::<Vec<_>>(); | |
| Ok(ProjectTrustContext { | |
| project_root, | |
| project_root_key, | |
| project_root_lookup_keys, | |
| checkout_root, | |
| repo_root, | |
| repo_root_key, | |
| repo_root_lookup_keys, | |
| projects_trust, | |
| user_config_file: user_config_file.clone(), | |
| credential_broker, | |
| credential_broker_provider_env_keys, | |
| }) | |
| } | |
| // Start before managed layers are merged: replaying credential source remaps | |
| // can discard provider fields needed to protect their environment bindings. | |
| fn credential_broker_trusted_config( | |
| merged_config: &TomlValue, | |
| thread_config_layers: &[ConfigLayerEntry], | |
| loaded_config_layers: &LoadedConfigLayers, | |
| ) -> TomlValue { | |
| let mut trusted_config = merged_config.clone(); | |
| for layer in thread_config_layers { | |
| merge_toml_values(&mut trusted_config, &layer.config); | |
| } | |
| if let Some(config) = loaded_config_layers.managed_config.as_ref() { | |
| merge_toml_values(&mut trusted_config, &config.managed_config); | |
| } | |
| if let Some(config) = loaded_config_layers.managed_config_from_mdm.as_ref() { | |
| merge_toml_values(&mut trusted_config, &config.managed_config); | |
| } | |
| trusted_config | |
| } | |
| /// Canonicalize the path and convert it to a string to be used as a key in the | |
| /// projects trust map. On Windows, strips UNC, when possible, to try to ensure | |
| /// that different paths that point to the same location have the same key. | |
| pub fn project_trust_key(path: &Path) -> String { | |
| normalized_project_trust_keys(path) | |
| .into_iter() | |
| .next() | |
| .unwrap_or_else(|| normalize_project_trust_lookup_key(path.to_string_lossy().to_string())) | |
| } | |
| /// Returns canonical and original path spellings in trust-lookup precedence order. | |
| pub fn normalized_project_trust_keys(path: &Path) -> Vec<String> { | |
| let normalized_path = normalize_project_trust_lookup_key(path.to_string_lossy().to_string()); | |
| let normalized_canonical_path = normalize_project_trust_lookup_key( | |
| normalize_path(path) | |
| .unwrap_or_else(|_| path.to_path_buf()) | |
| .to_string_lossy() | |
| .to_string(), | |
| ); | |
| if normalized_path == normalized_canonical_path { | |
| vec![normalized_canonical_path] | |
| } else { | |
| vec![normalized_canonical_path, normalized_path] | |
| } | |
| } | |
| fn normalize_project_trust_lookup_key(key: String) -> String { | |
| if cfg!(windows) { | |
| key.to_ascii_lowercase() | |
| } else { | |
| key | |
| } | |
| } | |
| fn project_trust_for_lookup_key( | |
| projects_trust: &std::collections::HashMap<String, TrustLevel>, | |
| lookup_key: &str, | |
| ) -> Option<(String, TrustLevel)> { | |
| if let Some(trust_level) = projects_trust.get(lookup_key).copied() { | |
| return Some((lookup_key.to_string(), trust_level)); | |
| } | |
| let mut normalized_matches: Vec<_> = projects_trust | |
| .iter() | |
| .filter(|(key, _)| normalize_project_trust_lookup_key((*key).clone()) == lookup_key) | |
| .collect(); | |
| normalized_matches.sort_by_key(|(key, _)| *key); | |
| normalized_matches | |
| .first() | |
| .map(|(key, trust_level)| ((**key).clone(), **trust_level)) | |
| } | |
| /// Takes a `toml::Value` parsed from a config.toml file and walks through it, | |
| /// resolving any `AbsolutePathBuf` fields against `base_dir`, returning a new | |
| /// `toml::Value` with the same shape but with paths resolved. | |
| /// | |
| /// This ensures that multiple config layers can be merged together correctly | |
| /// even if they were loaded from different directories. | |
| pub fn resolve_relative_paths_in_config_toml( | |
| value_from_config_toml: TomlValue, | |
| base_dir: &Path, | |
| ) -> io::Result<TomlValue> { | |
| // Use the serialize/deserialize round-trip to convert the | |
| // `toml::Value` into a `ConfigToml` with `AbsolutePath | |
| let _guard = AbsolutePathBufGuard::new(base_dir); | |
| let Ok(resolved) = value_from_config_toml.clone().try_into::<ConfigToml>() else { | |
| return Ok(value_from_config_toml); | |
| }; | |
| drop(_guard); | |
| let resolved_value = TomlValue::try_from(resolved).map_err(|e| { | |
| io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!("Failed to serialize resolved config: {e}"), | |
| ) | |
| })?; | |
| Ok(copy_shape_from_original( | |
| &value_from_config_toml, | |
| &resolved_value, | |
| )) | |
| } | |
| /// Ensure that every field in `original` is present in the returned | |
| /// `toml::Value`, taking the value from `resolved` where possible. This ensures | |
| /// the fields that we "removed" during the serialize/deserialize round-trip in | |
| /// `resolve_config_paths` are preserved, out of an abundance of caution. | |
| fn copy_shape_from_original(original: &TomlValue, resolved: &TomlValue) -> TomlValue { | |
| match (original, resolved) { | |
| (TomlValue::Table(original_table), TomlValue::Table(resolved_table)) => { | |
| let mut table = toml::map::Map::new(); | |
| for (key, original_value) in original_table { | |
| let resolved_value = resolved_table.get(key).unwrap_or(original_value); | |
| table.insert( | |
| key.clone(), | |
| copy_shape_from_original(original_value, resolved_value), | |
| ); | |
| } | |
| TomlValue::Table(table) | |
| } | |
| (TomlValue::Array(original_array), TomlValue::Array(resolved_array)) => { | |
| let mut items = Vec::new(); | |
| for (index, original_value) in original_array.iter().enumerate() { | |
| let resolved_value = resolved_array.get(index).unwrap_or(original_value); | |
| items.push(copy_shape_from_original(original_value, resolved_value)); | |
| } | |
| TomlValue::Array(items) | |
| } | |
| (_, resolved_value) => resolved_value.clone(), | |
| } | |
| } | |
| /// Finds the nearest ancestor with a configured project marker, or returns `cwd`. | |
| /// Callers must use markers from configuration loaded before project layers. | |
| pub async fn find_project_root( | |
| fs: &dyn ExecutorFileSystem, | |
| cwd: &AbsolutePathBuf, | |
| project_root_markers: &[String], | |
| ) -> io::Result<AbsolutePathBuf> { | |
| if project_root_markers.is_empty() { | |
| return Ok(cwd.clone()); | |
| } | |
| for ancestor in cwd.ancestors() { | |
| for marker in project_root_markers { | |
| let marker_path = ancestor.join(marker); | |
| let marker_path_uri = PathUri::from_abs_path(&marker_path); | |
| let Ok(metadata) = fs | |
| .get_metadata(&marker_path_uri, Default::default(), /*sandbox*/ None) | |
| .await | |
| else { | |
| continue; | |
| }; | |
| if marker == ".git" | |
| && metadata.is_directory | |
| && fs | |
| .get_metadata( | |
| &PathUri::from_abs_path(&marker_path.join("HEAD")), | |
| Default::default(), | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| .is_err() | |
| { | |
| continue; | |
| } | |
| return Ok(ancestor); | |
| } | |
| } | |
| Ok(cwd.clone()) | |
| } | |
| async fn find_git_checkout_root( | |
| fs: &dyn ExecutorFileSystem, | |
| cwd: &AbsolutePathBuf, | |
| ) -> Option<AbsolutePathBuf> { | |
| let cwd_uri = PathUri::from_abs_path(cwd); | |
| let base = match fs | |
| .get_metadata(&cwd_uri, Default::default(), /*sandbox*/ None) | |
| .await | |
| { | |
| Ok(metadata) if metadata.is_directory => cwd.clone(), | |
| _ => cwd.parent()?, | |
| }; | |
| for dir in base.ancestors() { | |
| let dot_git = dir.join(".git"); | |
| let dot_git_uri = PathUri::from_abs_path(&dot_git); | |
| let Ok(metadata) = fs | |
| .get_metadata(&dot_git_uri, Default::default(), /*sandbox*/ None) | |
| .await | |
| else { | |
| continue; | |
| }; | |
| if metadata.is_directory | |
| && fs | |
| .get_metadata( | |
| &PathUri::from_abs_path(&dot_git.join("HEAD")), | |
| Default::default(), | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| .is_err() | |
| { | |
| continue; | |
| } | |
| return Some(dir); | |
| } | |
| None | |
| } | |
| struct LoadedProjectLayers { | |
| layers: Vec<ConfigLayerEntry>, | |
| startup_warnings: Vec<String>, | |
| } | |
| struct DiscoveredProjectLayer { | |
| dot_codex_folder: AbsolutePathBuf, | |
| config: TomlValue, | |
| disabled_reason: Option<String>, | |
| hooks_config_folder_override: Option<AbsolutePathBuf>, | |
| load_root_checkout_hooks: bool, | |
| } | |
| struct DiscoveredProjectLayers { | |
| layers: Vec<DiscoveredProjectLayer>, | |
| startup_warnings: Vec<String>, | |
| } | |
| /// Return the appropriate list of layers (each with | |
| /// [ConfigLayerSource::Project] as the source) between `cwd` and | |
| /// `project_root`, inclusive. The list is ordered in _increasing_ precdence, | |
| /// starting from folders closest to `project_root` (which is the lowest | |
| /// precedence) to those closest to `cwd` (which is the highest precedence). | |
| /// Any warnings are stack-level startup messages, not additional config layers. | |
| async fn load_project_layers( | |
| fs: &dyn ExecutorFileSystem, | |
| cwd: &AbsolutePathBuf, | |
| project_root: &AbsolutePathBuf, | |
| trust_context: &ProjectTrustContext, | |
| codex_home: &Path, | |
| strict_config: bool, | |
| ) -> io::Result<LoadedProjectLayers> { | |
| let discovered = discover_project_layers( | |
| fs, | |
| cwd, | |
| project_root, | |
| trust_context, | |
| codex_home, | |
| strict_config, | |
| ) | |
| .await?; | |
| let mut layers = Vec::with_capacity(discovered.layers.len()); | |
| for layer in discovered.layers { | |
| let config = | |
| resolve_relative_paths_in_config_toml(layer.config, layer.dot_codex_folder.as_path())?; | |
| let config = if layer.load_root_checkout_hooks { | |
| merge_root_checkout_project_hooks( | |
| fs, | |
| config, | |
| layer.hooks_config_folder_override.as_ref(), | |
| layer.disabled_reason.is_none(), | |
| ) | |
| .await? | |
| } else { | |
| config | |
| }; | |
| layers.push(project_layer_entry( | |
| &layer.dot_codex_folder, | |
| config, | |
| layer.disabled_reason, | |
| layer.hooks_config_folder_override, | |
| )); | |
| } | |
| Ok(LoadedProjectLayers { | |
| layers, | |
| startup_warnings: discovered.startup_warnings, | |
| }) | |
| } | |
| async fn discover_project_layers( | |
| fs: &dyn ExecutorFileSystem, | |
| cwd: &AbsolutePathBuf, | |
| project_root: &AbsolutePathBuf, | |
| trust_context: &ProjectTrustContext, | |
| codex_home: &Path, | |
| strict_config: bool, | |
| ) -> io::Result<DiscoveredProjectLayers> { | |
| let codex_home_abs = AbsolutePathBuf::from_absolute_path(codex_home)?; | |
| let codex_home_normalized = | |
| normalize_path(codex_home_abs.as_path()).unwrap_or_else(|_| codex_home_abs.to_path_buf()); | |
| let mut dirs = cwd | |
| .ancestors() | |
| .scan(false, |done, a| { | |
| if *done { | |
| None | |
| } else { | |
| if &a == project_root { | |
| *done = true; | |
| } | |
| Some(a) | |
| } | |
| }) | |
| .collect::<Vec<_>>(); | |
| dirs.reverse(); | |
| let mut layers = Vec::new(); | |
| let mut startup_warnings = Vec::new(); | |
| for dir in dirs { | |
| let dot_codex_abs = dir.join(".codex"); | |
| let dot_codex_uri = PathUri::from_abs_path(&dot_codex_abs); | |
| if !fs | |
| .get_metadata(&dot_codex_uri, Default::default(), /*sandbox*/ None) | |
| .await | |
| .map(|metadata| metadata.is_directory) | |
| .unwrap_or(false) | |
| { | |
| continue; | |
| } | |
| let decision = trust_context.decision_for_dir(&dir); | |
| let disabled_reason = trust_context.disabled_reason_for_decision(&decision); | |
| let hooks_config_folder_override = trust_context.root_checkout_hooks_folder_for_dir(&dir); | |
| let dot_codex_normalized = | |
| normalize_path(dot_codex_abs.as_path()).unwrap_or_else(|_| dot_codex_abs.to_path_buf()); | |
| if dot_codex_abs == codex_home_abs || dot_codex_normalized == codex_home_normalized { | |
| continue; | |
| } | |
| let config_file = dot_codex_abs.join(CONFIG_TOML_FILE); | |
| let config_file_uri = PathUri::from_abs_path(&config_file); | |
| match fs | |
| .read_file_text(&config_file_uri, Default::default(), /*sandbox*/ None) | |
| .await | |
| { | |
| Ok(contents) => { | |
| let config: TomlValue = match toml::from_str(&contents) { | |
| Ok(config) => config, | |
| Err(e) => { | |
| if decision.is_trusted() { | |
| let config_file_display = config_file.as_path().display(); | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "Error parsing project config file {config_file_display}: {e}" | |
| ), | |
| )); | |
| } | |
| layers.push(DiscoveredProjectLayer { | |
| dot_codex_folder: dot_codex_abs, | |
| config: TomlValue::Table(toml::map::Map::new()), | |
| disabled_reason, | |
| hooks_config_folder_override, | |
| load_root_checkout_hooks: false, | |
| }); | |
| continue; | |
| } | |
| }; | |
| let mut config = config; | |
| if disabled_reason.is_none() && strict_config { | |
| validate_config_toml_strictly( | |
| config_file.as_path(), | |
| &contents, | |
| &config, | |
| dot_codex_abs.as_path(), | |
| )?; | |
| } | |
| let ignored_project_config_keys = sanitize_project_config( | |
| &mut config, | |
| trust_context.credential_broker, | |
| &trust_context.credential_broker_provider_env_keys, | |
| ); | |
| if disabled_reason.is_none() && !ignored_project_config_keys.is_empty() { | |
| startup_warnings.push(project_ignored_config_keys_warning( | |
| &dot_codex_abs, | |
| &ignored_project_config_keys, | |
| )); | |
| } | |
| layers.push(DiscoveredProjectLayer { | |
| dot_codex_folder: dot_codex_abs, | |
| config, | |
| disabled_reason, | |
| hooks_config_folder_override, | |
| load_root_checkout_hooks: true, | |
| }); | |
| } | |
| Err(err) => { | |
| if err.kind() == io::ErrorKind::NotFound { | |
| // If there is no config.toml file, record an empty entry | |
| // for this project layer, as this may still have subfolders | |
| // that are significant in the overall ConfigLayerStack. | |
| layers.push(DiscoveredProjectLayer { | |
| dot_codex_folder: dot_codex_abs, | |
| config: TomlValue::Table(toml::map::Map::new()), | |
| disabled_reason, | |
| hooks_config_folder_override, | |
| load_root_checkout_hooks: true, | |
| }); | |
| } else { | |
| let config_file_display = config_file.as_path().display(); | |
| return Err(io::Error::new( | |
| err.kind(), | |
| format!("Failed to read project config file {config_file_display}: {err}"), | |
| )); | |
| } | |
| } | |
| } | |
| } | |
| Ok(DiscoveredProjectLayers { | |
| layers, | |
| startup_warnings, | |
| }) | |
| } | |
| /// For linked worktrees, preserve ordinary worktree-local project config while | |
| /// replacing only hook declarations with the matching root-checkout layer. | |
| async fn merge_root_checkout_project_hooks( | |
| fs: &dyn ExecutorFileSystem, | |
| mut config: TomlValue, | |
| hooks_config_folder_override: Option<&AbsolutePathBuf>, | |
| is_trusted: bool, | |
| ) -> io::Result<TomlValue> { | |
| let Some(hooks_config_folder) = hooks_config_folder_override else { | |
| return Ok(config); | |
| }; | |
| let root_config = | |
| load_root_checkout_project_config(fs, hooks_config_folder, is_trusted).await?; | |
| let root_config = | |
| resolve_relative_paths_in_config_toml(root_config, hooks_config_folder.as_path())?; | |
| let Some(config_table) = config.as_table_mut() else { | |
| return Ok(config); | |
| }; | |
| config_table.remove("hooks"); | |
| if let Some(hooks) = root_config.get("hooks") { | |
| config_table.insert("hooks".to_string(), hooks.clone()); | |
| } | |
| Ok(config) | |
| } | |
| async fn load_root_checkout_project_config( | |
| fs: &dyn ExecutorFileSystem, | |
| hooks_config_folder: &AbsolutePathBuf, | |
| is_trusted: bool, | |
| ) -> io::Result<TomlValue> { | |
| let hooks_config_file = hooks_config_folder.join(CONFIG_TOML_FILE); | |
| let hooks_config_file_uri = PathUri::from_abs_path(&hooks_config_file); | |
| Ok( | |
| match fs | |
| .read_file_text( | |
| &hooks_config_file_uri, | |
| Default::default(), | |
| /*sandbox*/ None, | |
| ) | |
| .await | |
| { | |
| Ok(contents) => { | |
| let parsed: TomlValue = match toml::from_str(&contents) { | |
| Ok(parsed) => parsed, | |
| Err(err) => { | |
| if is_trusted { | |
| return Err(io::Error::new( | |
| io::ErrorKind::InvalidData, | |
| format!( | |
| "Error parsing project hooks config file {}: {err}", | |
| hooks_config_file.as_path().display() | |
| ), | |
| )); | |
| } | |
| TomlValue::Table(toml::map::Map::new()) | |
| } | |
| }; | |
| parsed | |
| } | |
| Err(err) if err.kind() == io::ErrorKind::NotFound => { | |
| TomlValue::Table(toml::map::Map::new()) | |
| } | |
| Err(err) => { | |
| return Err(io::Error::new( | |
| err.kind(), | |
| format!( | |
| "Failed to read project hooks config file {}: {err}", | |
| hooks_config_file.as_path().display() | |
| ), | |
| )); | |
| } | |
| }, | |
| ) | |
| } | |
| /// The legacy mechanism for specifying admin-enforced configuration is to read | |
| /// from a file like `/etc/codex/managed_config.toml` that has the same | |
| /// structure as `config.toml` where fields like `approval_policy` can specify | |
| /// exactly one value rather than a list of allowed values. | |
| /// | |
| /// If present, re-interpret `managed_config.toml` as a `requirements.toml` | |
| /// where each specified field is treated as a constraint. Most fields allow | |
| /// only the specified value. `approvals_reviewer = "auto_review"` also allows | |
| /// `user` so people can opt out of the auto-reviewer. | |
| struct LegacyManagedConfigToml { | |
| approval_policy: Option<AskForApproval>, | |
| approvals_reviewer: Option<ApprovalsReviewer>, | |
| sandbox_mode: Option<SandboxMode>, | |
| } | |
| // Cannot name this `mod tests` because of tests.rs in this folder. | |
| mod unit_tests { | |
| use super::*; | |
| use std::path::Path; | |
| use tempfile::tempdir; | |
| fn ensure_resolve_relative_paths_in_config_toml_preserves_all_fields() -> anyhow::Result<()> { | |
| let tmp = tempdir()?; | |
| let base_dir = tmp.path(); | |
| let contents = r#" | |
| # This is a field recognized by config.toml that is an AbsolutePathBuf in | |
| # the ConfigToml struct. | |
| model_instructions_file = "./some_file.md" | |
| # This is a field recognized by config.toml. | |
| model = "gpt-1000" | |
| # This is a field not recognized by config.toml. | |
| foo = "xyzzy" | |
| "#; | |
| let user_config: TomlValue = toml::from_str(contents)?; | |
| let normalized_toml_value = resolve_relative_paths_in_config_toml(user_config, base_dir)?; | |
| let mut expected_toml_value = toml::map::Map::new(); | |
| expected_toml_value.insert( | |
| "model_instructions_file".to_string(), | |
| TomlValue::String( | |
| AbsolutePathBuf::resolve_path_against_base("./some_file.md", base_dir) | |
| .as_path() | |
| .to_string_lossy() | |
| .to_string(), | |
| ), | |
| ); | |
| expected_toml_value.insert( | |
| "model".to_string(), | |
| TomlValue::String("gpt-1000".to_string()), | |
| ); | |
| expected_toml_value.insert("foo".to_string(), TomlValue::String("xyzzy".to_string())); | |
| assert_eq!(normalized_toml_value, TomlValue::Table(expected_toml_value)); | |
| Ok(()) | |
| } | |
| fn legacy_managed_config_backfill_includes_read_only_sandbox_mode() -> io::Result<()> { | |
| let legacy = LegacyManagedConfigToml { | |
| approval_policy: None, | |
| approvals_reviewer: None, | |
| sandbox_mode: Some(SandboxMode::WorkspaceWrite), | |
| }; | |
| assert_eq!( | |
| legacy_requirements_to_toml_value(legacy)?, | |
| TomlValue::Table(toml::map::Map::from_iter([( | |
| "allowed_sandbox_modes".to_string(), | |
| TomlValue::Array(vec![ | |
| TomlValue::String("read-only".to_string()), | |
| TomlValue::String("workspace-write".to_string()), | |
| ]), | |
| )])) | |
| ); | |
| Ok(()) | |
| } | |
| fn legacy_managed_config_backfill_allows_user_when_guardian_is_required() -> io::Result<()> { | |
| let legacy = LegacyManagedConfigToml { | |
| approval_policy: None, | |
| approvals_reviewer: Some(ApprovalsReviewer::AutoReview), | |
| sandbox_mode: None, | |
| }; | |
| assert_eq!( | |
| legacy_requirements_to_toml_value(legacy)?, | |
| TomlValue::Table(toml::map::Map::from_iter([( | |
| "allowed_approvals_reviewers".to_string(), | |
| TomlValue::Array(vec![ | |
| TomlValue::String("auto_review".to_string()), | |
| TomlValue::String("user".to_string()), | |
| ]), | |
| )])) | |
| ); | |
| Ok(()) | |
| } | |
| fn legacy_managed_config_backfill_preserves_user_only_approvals_reviewer() -> io::Result<()> { | |
| let legacy = LegacyManagedConfigToml { | |
| approval_policy: None, | |
| approvals_reviewer: Some(ApprovalsReviewer::User), | |
| sandbox_mode: None, | |
| }; | |
| assert_eq!( | |
| legacy_requirements_to_toml_value(legacy)?, | |
| TomlValue::Table(toml::map::Map::from_iter([( | |
| "allowed_approvals_reviewers".to_string(), | |
| TomlValue::Array(vec![TomlValue::String("user".to_string())]), | |
| )])) | |
| ); | |
| Ok(()) | |
| } | |
| fn windows_system_requirements_toml_file_uses_expected_suffix() { | |
| let expected = windows_program_data_dir_from_known_folder() | |
| .unwrap_or_else(|_| PathBuf::from(DEFAULT_PROGRAM_DATA_DIR_WINDOWS)) | |
| .join("OpenAI") | |
| .join("Codex") | |
| .join("requirements.toml"); | |
| assert_eq!( | |
| windows_system_requirements_toml_file() | |
| .expect("requirements.toml path") | |
| .as_path(), | |
| expected.as_path() | |
| ); | |
| assert!( | |
| windows_system_requirements_toml_file() | |
| .expect("requirements.toml path") | |
| .as_path() | |
| .ends_with(Path::new("OpenAI").join("Codex").join("requirements.toml")) | |
| ); | |
| } | |
| fn windows_system_config_toml_file_uses_expected_suffix() { | |
| let expected = windows_program_data_dir_from_known_folder() | |
| .unwrap_or_else(|_| PathBuf::from(DEFAULT_PROGRAM_DATA_DIR_WINDOWS)) | |
| .join("OpenAI") | |
| .join("Codex") | |
| .join("config.toml"); | |
| assert_eq!( | |
| windows_system_config_toml_file() | |
| .expect("config.toml path") | |
| .as_path(), | |
| expected.as_path() | |
| ); | |
| assert!( | |
| windows_system_config_toml_file() | |
| .expect("config.toml path") | |
| .as_path() | |
| .ends_with(Path::new("OpenAI").join("Codex").join("config.toml")) | |
| ); | |
| } | |
| } | |