diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 84f911bd44..5cb8cb10f9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1786,10 +1786,12 @@ name = "codex-config" version = "0.0.0" dependencies = [ "anyhow", + "base64 0.22.1", "codex-app-server-protocol", "codex-execpolicy", "codex-protocol", "codex-utils-absolute-path", + "core-foundation 0.9.4", "futures", "multimap", "pretty_assertions", @@ -1802,6 +1804,7 @@ dependencies = [ "toml 0.9.11+spec-1.1.0", "toml_edit 0.24.0+spec-1.1.0", "tracing", + "windows-sys 0.52.0", ] [[package]] diff --git a/codex-rs/config/Cargo.toml b/codex-rs/config/Cargo.toml index 02446ed6f3..c3a110e8d8 100644 --- a/codex-rs/config/Cargo.toml +++ b/codex-rs/config/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true workspace = true [dependencies] +base64 = { workspace = true } codex-app-server-protocol = { workspace = true } codex-execpolicy = { workspace = true } codex-protocol = { workspace = true } @@ -24,6 +25,15 @@ toml = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.9" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.52", features = [ + "Win32_System_Com", + "Win32_UI_Shell", +] } + [dev-dependencies] anyhow = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/core/src/config_loader/layer_io.rs b/codex-rs/config/src/layer_io.rs similarity index 84% rename from codex-rs/core/src/config_loader/layer_io.rs rename to codex-rs/config/src/layer_io.rs index af77bdafa5..dccafaa506 100644 --- a/codex-rs/core/src/config_loader/layer_io.rs +++ b/codex-rs/config/src/layer_io.rs @@ -1,10 +1,10 @@ -use super::LoaderOverrides; +use crate::LoaderOverrides; +use crate::config_error_from_toml; +use crate::io_error_from_config_error; #[cfg(target_os = "macos")] -use super::macos::ManagedAdminConfigLayer; +use crate::macos::ManagedAdminConfigLayer; #[cfg(target_os = "macos")] -use super::macos::load_managed_admin_config_layer; -use codex_config::config_error_from_toml; -use codex_config::io_error_from_config_error; +use crate::macos::load_managed_admin_config_layer; use codex_utils_absolute_path::AbsolutePathBuf; use std::io; use std::path::Path; @@ -16,26 +16,26 @@ use toml::Value as TomlValue; const CODEX_MANAGED_CONFIG_SYSTEM_PATH: &str = "/etc/codex/managed_config.toml"; #[derive(Debug, Clone)] -pub(super) struct MangedConfigFromFile { +pub struct ManagedConfigFromFile { pub managed_config: TomlValue, pub file: AbsolutePathBuf, } #[derive(Debug, Clone)] -pub(super) struct ManagedConfigFromMdm { +pub struct ManagedConfigFromMdm { pub managed_config: TomlValue, pub raw_toml: String, } #[derive(Debug, Clone)] -pub(super) struct LoadedConfigLayers { +pub struct LoadedConfigLayers { /// If present, data read from a file such as `/etc/codex/managed_config.toml`. - pub managed_config: Option, + pub managed_config: Option, /// If present, data read from managed preferences (macOS only). pub managed_config_from_mdm: Option, } -pub(super) async fn load_config_layers_internal( +pub async fn load_config_layers_internal( codex_home: &Path, overrides: LoaderOverrides, ) -> io::Result { @@ -59,7 +59,7 @@ pub(super) async fn load_config_layers_internal( let managed_config = read_config_from_path(&managed_config_path, /*log_missing_as_info*/ false) .await? - .map(|managed_config| MangedConfigFromFile { + .map(|managed_config| ManagedConfigFromFile { managed_config, file: managed_config_path.clone(), }); @@ -88,7 +88,7 @@ fn map_managed_admin_layer(layer: ManagedAdminConfigLayer) -> ManagedConfigFromM } } -pub(super) async fn read_config_from_path( +async fn read_config_from_path( path: impl AsRef, log_missing_as_info: bool, ) -> io::Result> { @@ -120,8 +120,7 @@ pub(super) async fn read_config_from_path( } } -/// Return the default managed config path. -pub(super) fn managed_config_default_path(codex_home: &Path) -> PathBuf { +fn managed_config_default_path(codex_home: &Path) -> PathBuf { #[cfg(unix)] { let _ = codex_home; diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 995a5b8db7..e44c8d3e39 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -3,6 +3,10 @@ mod config_requirements; mod constraint; mod diagnostics; mod fingerprint; +mod layer_io; +mod loader; +#[cfg(target_os = "macos")] +mod macos; mod merge; mod overrides; mod requirements_exec_policy; @@ -44,6 +48,15 @@ pub use diagnostics::format_config_error; pub use diagnostics::format_config_error_with_source; pub use diagnostics::io_error_from_config_error; pub use fingerprint::version_for_toml; +pub use layer_io::LoadedConfigLayers; +pub use layer_io::ManagedConfigFromFile; +pub use layer_io::ManagedConfigFromMdm; +pub use layer_io::load_config_layers_internal; +pub use loader::load_managed_admin_requirements; +pub use loader::load_requirements_from_legacy_scheme; +pub use loader::load_requirements_toml; +pub use loader::system_config_toml_file; +pub use loader::system_requirements_toml_file; pub use merge::merge_toml_values; pub use overrides::build_cli_overrides_layer; pub use requirements_exec_policy::RequirementsExecPolicy; diff --git a/codex-rs/config/src/loader.rs b/codex-rs/config/src/loader.rs new file mode 100644 index 0000000000..8756f0c555 --- /dev/null +++ b/codex-rs/config/src/loader.rs @@ -0,0 +1,235 @@ +use crate::ConfigRequirementsToml; +use crate::ConfigRequirementsWithSources; +use crate::LoadedConfigLayers; +use crate::RequirementSource; +#[cfg(target_os = "macos")] +use crate::macos::load_managed_admin_requirements_toml; +use codex_protocol::config_types::SandboxMode; +use codex_protocol::protocol::AskForApproval; +use codex_utils_absolute_path::AbsolutePathBuf; +use serde::Deserialize; +use std::io; +use std::path::Path; +#[cfg(windows)] +use std::path::PathBuf; + +pub const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml"; + +#[cfg(windows)] +const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData"; + +pub async fn load_requirements_toml( + config_requirements_toml: &mut ConfigRequirementsWithSources, + requirements_toml_file: impl AsRef, +) -> io::Result<()> { + let requirements_toml_file = + AbsolutePathBuf::from_absolute_path(requirements_toml_file.as_ref())?; + match tokio::fs::read_to_string(&requirements_toml_file).await { + Ok(contents) => { + let requirements_config: ConfigRequirementsToml = + toml::from_str(&contents).map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Error parsing requirements file {}: {err}", + requirements_toml_file.as_ref().display(), + ), + ) + })?; + config_requirements_toml.merge_unset_fields( + RequirementSource::SystemRequirementsToml { + file: requirements_toml_file.clone(), + }, + requirements_config, + ); + } + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + Err(err) => { + return Err(io::Error::new( + err.kind(), + format!( + "Failed to read requirements file {}: {err}", + requirements_toml_file.as_ref().display(), + ), + )); + } + } + + Ok(()) +} + +pub async fn load_managed_admin_requirements( + config_requirements_toml: &mut ConfigRequirementsWithSources, + managed_config_requirements_base64: Option<&str>, +) -> io::Result<()> { + #[cfg(target_os = "macos")] + { + load_managed_admin_requirements_toml( + config_requirements_toml, + managed_config_requirements_base64, + ) + .await + } + + #[cfg(not(target_os = "macos"))] + { + let _ = config_requirements_toml; + let _ = managed_config_requirements_base64; + Ok(()) + } +} + +#[cfg(unix)] +pub fn system_requirements_toml_file() -> io::Result { + AbsolutePathBuf::from_absolute_path(Path::new("/etc/codex/requirements.toml")) +} + +#[cfg(windows)] +pub fn system_requirements_toml_file() -> io::Result { + windows_system_requirements_toml_file() +} + +#[cfg(unix)] +pub fn system_config_toml_file() -> io::Result { + AbsolutePathBuf::from_absolute_path(Path::new(SYSTEM_CONFIG_TOML_FILE_UNIX)) +} + +#[cfg(windows)] +pub fn system_config_toml_file() -> io::Result { + windows_system_config_toml_file() +} + +#[cfg(windows)] +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") +} + +#[cfg(windows)] +fn windows_system_requirements_toml_file() -> io::Result { + let requirements_toml_file = windows_codex_system_dir().join("requirements.toml"); + AbsolutePathBuf::try_from(requirements_toml_file) +} + +#[cfg(windows)] +fn windows_system_config_toml_file() -> io::Result { + let config_toml_file = windows_codex_system_dir().join("config.toml"); + AbsolutePathBuf::try_from(config_toml_file) +} + +#[cfg(windows)] +fn windows_program_data_dir_from_known_folder() -> io::Result { + 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::(); + 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}" + )) + })?; + 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", + )); + } + + 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) +} + +pub async fn load_requirements_from_legacy_scheme( + config_requirements_toml: &mut ConfigRequirementsWithSources, + loaded_config_layers: LoadedConfigLayers, +) -> io::Result<()> { + let LoadedConfigLayers { + managed_config, + managed_config_from_mdm, + } = loaded_config_layers; + + for (source, config) in managed_config_from_mdm + .map(|config| { + ( + RequirementSource::LegacyManagedConfigTomlFromMdm, + config.managed_config, + ) + }) + .into_iter() + .chain(managed_config.map(|config| { + ( + RequirementSource::LegacyManagedConfigTomlFromFile { file: config.file }, + config.managed_config, + ) + })) + { + 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}"), + ) + })?; + + let requirements = ConfigRequirementsToml::from(legacy_config); + config_requirements_toml.merge_unset_fields(source, requirements); + } + + Ok(()) +} + +#[derive(Deserialize, Debug, Clone, Default, PartialEq)] +struct LegacyManagedConfigToml { + approval_policy: Option, + sandbox_mode: Option, +} + +impl From for ConfigRequirementsToml { + fn from(legacy: LegacyManagedConfigToml) -> Self { + let mut config_requirements_toml = ConfigRequirementsToml::default(); + + let LegacyManagedConfigToml { + approval_policy, + sandbox_mode, + } = legacy; + if let Some(approval_policy) = approval_policy { + config_requirements_toml.allowed_approval_policies = Some(vec![approval_policy]); + } + if let Some(sandbox_mode) = sandbox_mode { + let required_mode = sandbox_mode.into(); + let mut allowed_modes = vec![crate::SandboxModeRequirement::ReadOnly]; + if required_mode != crate::SandboxModeRequirement::ReadOnly { + allowed_modes.push(required_mode); + } + config_requirements_toml.allowed_sandbox_modes = Some(allowed_modes); + } + config_requirements_toml + } +} diff --git a/codex-rs/core/src/config_loader/macos.rs b/codex-rs/config/src/macos.rs similarity index 94% rename from codex-rs/core/src/config_loader/macos.rs rename to codex-rs/config/src/macos.rs index d94b1930b7..7769bb82e3 100644 --- a/codex-rs/core/src/config_loader/macos.rs +++ b/codex-rs/config/src/macos.rs @@ -1,6 +1,6 @@ -use super::ConfigRequirementsToml; -use super::ConfigRequirementsWithSources; -use super::RequirementSource; +use crate::ConfigRequirementsToml; +use crate::ConfigRequirementsWithSources; +use crate::RequirementSource; use base64::Engine; use base64::prelude::BASE64_STANDARD; use core_foundation::base::TCFType; @@ -16,19 +16,19 @@ const MANAGED_PREFERENCES_CONFIG_KEY: &str = "config_toml_base64"; const MANAGED_PREFERENCES_REQUIREMENTS_KEY: &str = "requirements_toml_base64"; #[derive(Debug, Clone)] -pub(super) struct ManagedAdminConfigLayer { +pub struct ManagedAdminConfigLayer { pub config: TomlValue, pub raw_toml: String, } -pub(super) fn managed_preferences_requirements_source() -> RequirementSource { +fn managed_preferences_requirements_source() -> RequirementSource { RequirementSource::MdmManagedPreferences { domain: MANAGED_PREFERENCES_APPLICATION_ID.to_string(), key: MANAGED_PREFERENCES_REQUIREMENTS_KEY.to_string(), } } -pub(crate) async fn load_managed_admin_config_layer( +pub async fn load_managed_admin_config_layer( override_base64: Option<&str>, ) -> io::Result> { if let Some(encoded) = override_base64 { @@ -61,7 +61,7 @@ fn load_managed_admin_config() -> io::Result> { .transpose() } -pub(crate) async fn load_managed_admin_requirements_toml( +pub async fn load_managed_admin_requirements_toml( target: &mut ConfigRequirementsWithSources, override_base64: Option<&str>, ) -> io::Result<()> { diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 0c426b155f..386b9a2701 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -1,27 +1,18 @@ -mod layer_io; -#[cfg(target_os = "macos")] -mod macos; - #[cfg(test)] mod tests; use crate::config::ConfigToml; -use crate::config_loader::layer_io::LoadedConfigLayers; use crate::git_info::resolve_root_git_project_for_trust; use codex_app_server_protocol::ConfigLayerSource; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigRequirementsWithSources; -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 dunce::canonicalize as normalize_path; use serde::Deserialize; use std::io; use std::path::Path; -#[cfg(windows)] -use std::path::PathBuf; use toml::Value as TomlValue; pub use codex_config::AppRequirementToml; @@ -38,6 +29,7 @@ pub use codex_config::ConfigRequirements; pub use codex_config::ConfigRequirementsToml; pub use codex_config::ConstrainedWithSource; pub use codex_config::FeatureRequirementsToml; +use codex_config::LoadedConfigLayers; pub use codex_config::LoaderOverrides; pub use codex_config::McpServerIdentity; pub use codex_config::McpServerRequirement; @@ -55,18 +47,16 @@ pub(crate) use codex_config::config_error_from_toml; pub use codex_config::format_config_error; pub use codex_config::format_config_error_with_source; pub(crate) use codex_config::io_error_from_config_error; +use codex_config::load_config_layers_internal; +use codex_config::load_managed_admin_requirements; +use codex_config::load_requirements_from_legacy_scheme; +pub(crate) use codex_config::load_requirements_toml; pub use codex_config::merge_toml_values; +use codex_config::system_config_toml_file; +use codex_config::system_requirements_toml_file; #[cfg(test)] pub(crate) use codex_config::version_for_toml; -/// On Unix systems, load default settings from this file path, if present. -/// Note that /etc/codex/ is treated as a "config folder," so subfolders such -/// as skills/ and rules/ will also be honored. -pub const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml"; - -#[cfg(windows)] -const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData"; - const DEFAULT_PROJECT_ROOT_MARKERS: &[&str] = &[".git"]; pub(crate) async fn first_layer_config_error(layers: &ConfigLayerStack) -> Option { @@ -125,8 +115,7 @@ pub async fn load_config_layers_state( .merge_unset_fields(RequirementSource::CloudRequirements, requirements); } - #[cfg(target_os = "macos")] - macos::load_managed_admin_requirements_toml( + load_managed_admin_requirements( &mut config_requirements_toml, overrides .macos_managed_config_requirements_base64 @@ -140,7 +129,7 @@ pub async fn load_config_layers_state( // Make a best-effort to support the legacy `managed_config.toml` as a // requirements specification. - let loaded_config_layers = layer_io::load_config_layers_internal(codex_home, overrides).await?; + let loaded_config_layers = load_config_layers_internal(codex_home, overrides).await?; load_requirements_from_legacy_scheme( &mut config_requirements_toml, loaded_config_layers.clone(), @@ -343,185 +332,6 @@ async fn load_config_toml_for_required_layer( Ok(create_entry(toml_value)) } -/// If available, apply requirements from the platform system -/// `requirements.toml` location to `config_requirements_toml` by filling in -/// any unset fields. -async fn load_requirements_toml( - config_requirements_toml: &mut ConfigRequirementsWithSources, - requirements_toml_file: impl AsRef, -) -> io::Result<()> { - let requirements_toml_file = - AbsolutePathBuf::from_absolute_path(requirements_toml_file.as_ref())?; - match tokio::fs::read_to_string(&requirements_toml_file).await { - Ok(contents) => { - let requirements_config: ConfigRequirementsToml = - toml::from_str(&contents).map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidData, - format!( - "Error parsing requirements file {}: {e}", - requirements_toml_file.as_ref().display(), - ), - ) - })?; - config_requirements_toml.merge_unset_fields( - RequirementSource::SystemRequirementsToml { - file: requirements_toml_file.clone(), - }, - requirements_config, - ); - } - Err(e) => { - if e.kind() != io::ErrorKind::NotFound { - return Err(io::Error::new( - e.kind(), - format!( - "Failed to read requirements file {}: {e}", - requirements_toml_file.as_ref().display(), - ), - )); - } - } - } - - Ok(()) -} - -#[cfg(unix)] -fn system_requirements_toml_file() -> io::Result { - AbsolutePathBuf::from_absolute_path(Path::new("/etc/codex/requirements.toml")) -} - -#[cfg(windows)] -fn system_requirements_toml_file() -> io::Result { - windows_system_requirements_toml_file() -} - -#[cfg(unix)] -fn system_config_toml_file() -> io::Result { - AbsolutePathBuf::from_absolute_path(Path::new(SYSTEM_CONFIG_TOML_FILE_UNIX)) -} - -#[cfg(windows)] -fn system_config_toml_file() -> io::Result { - windows_system_config_toml_file() -} - -#[cfg(windows)] -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") -} - -#[cfg(windows)] -fn windows_system_requirements_toml_file() -> io::Result { - let requirements_toml_file = windows_codex_system_dir().join("requirements.toml"); - AbsolutePathBuf::try_from(requirements_toml_file) -} - -#[cfg(windows)] -fn windows_system_config_toml_file() -> io::Result { - let config_toml_file = windows_codex_system_dir().join("config.toml"); - AbsolutePathBuf::try_from(config_toml_file) -} - -#[cfg(windows)] -fn windows_program_data_dir_from_known_folder() -> io::Result { - 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::(); - 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) -} - -async fn load_requirements_from_legacy_scheme( - config_requirements_toml: &mut ConfigRequirementsWithSources, - loaded_config_layers: LoadedConfigLayers, -) -> io::Result<()> { - // In this implementation, earlier layers cannot be overwritten by later - // layers, so list managed_config_from_mdm first because it has the highest - // precedence. - let LoadedConfigLayers { - managed_config, - managed_config_from_mdm, - } = loaded_config_layers; - - for (source, config) in managed_config_from_mdm - .map(|config| { - ( - RequirementSource::LegacyManagedConfigTomlFromMdm, - config.managed_config, - ) - }) - .into_iter() - .chain(managed_config.map(|c| { - ( - RequirementSource::LegacyManagedConfigTomlFromFile { file: c.file }, - c.managed_config, - ) - })) - { - 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}"), - ) - })?; - - let new_requirements_toml = ConfigRequirementsToml::from(legacy_config); - config_requirements_toml.merge_unset_fields(source, new_requirements_toml); - } - - Ok(()) -} - /// Reads `project_root_markers` from the [toml::Value] produced by merging /// `config.toml` from the config layers in the stack preceding /// [ConfigLayerSource::Project]. @@ -895,51 +705,12 @@ async fn load_project_layers( Ok(layers) } -/// 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 allowing only that -/// value. -#[derive(Deserialize, Debug, Clone, Default, PartialEq)] -struct LegacyManagedConfigToml { - approval_policy: Option, - sandbox_mode: Option, -} - -impl From for ConfigRequirementsToml { - fn from(legacy: LegacyManagedConfigToml) -> Self { - let mut config_requirements_toml = ConfigRequirementsToml::default(); - - let LegacyManagedConfigToml { - approval_policy, - sandbox_mode, - } = legacy; - if let Some(approval_policy) = approval_policy { - config_requirements_toml.allowed_approval_policies = Some(vec![approval_policy]); - } - 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); - } - config_requirements_toml.allowed_sandbox_modes = Some(allowed_modes); - } - config_requirements_toml - } -} - // Cannot name this `mod tests` because of tests.rs in this folder. #[cfg(test)] mod unit_tests { use super::*; - #[cfg(windows)] - use std::path::Path; + use codex_config::ManagedConfigFromFile; + use codex_protocol::protocol::SandboxPolicy; use tempfile::tempdir; #[test] @@ -979,65 +750,81 @@ foo = "xyzzy" Ok(()) } - #[test] - fn legacy_managed_config_backfill_includes_read_only_sandbox_mode() { - let legacy = LegacyManagedConfigToml { - approval_policy: None, - sandbox_mode: Some(SandboxMode::WorkspaceWrite), + #[tokio::test] + async fn legacy_managed_config_backfill_includes_read_only_sandbox_mode() { + let tmp = tempdir().expect("tempdir"); + let managed_path = AbsolutePathBuf::try_from(tmp.path().join("managed_config.toml")) + .expect("managed path"); + let loaded_layers = LoadedConfigLayers { + managed_config: Some(ManagedConfigFromFile { + managed_config: toml::toml! { + sandbox_mode = "workspace-write" + } + .into(), + file: managed_path.clone(), + }), + managed_config_from_mdm: None, }; - - let requirements = ConfigRequirementsToml::from(legacy); + let mut requirements_with_sources = ConfigRequirementsWithSources::default(); + load_requirements_from_legacy_scheme(&mut requirements_with_sources, loaded_layers) + .await + .expect("load legacy requirements"); + let requirements: ConfigRequirements = requirements_with_sources + .try_into() + .expect("requirements parse"); assert_eq!( - requirements.allowed_sandbox_modes, - Some(vec![ - SandboxModeRequirement::ReadOnly, - SandboxModeRequirement::WorkspaceWrite - ]) + requirements.sandbox_policy.get(), + &SandboxPolicy::new_read_only_policy() + ); + assert!( + requirements + .sandbox_policy + .can_set(&SandboxPolicy::new_workspace_write_policy()) + .is_ok() + ); + assert_eq!( + requirements + .sandbox_policy + .can_set(&SandboxPolicy::DangerFullAccess), + Err(codex_config::ConstraintError::InvalidValue { + field_name: "sandbox_mode", + candidate: "DangerFullAccess".into(), + allowed: "[ReadOnly, WorkspaceWrite]".into(), + requirement_source: RequirementSource::LegacyManagedConfigTomlFromFile { + file: managed_path, + }, + }) ); } #[cfg(windows)] #[test] 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() + system_requirements_toml_file() .expect("requirements.toml path") .as_path() - .ends_with(Path::new("OpenAI").join("Codex").join("requirements.toml")) + .ends_with( + std::path::Path::new("OpenAI") + .join("Codex") + .join("requirements.toml") + ) ); } #[cfg(windows)] #[test] 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() + system_config_toml_file() .expect("config.toml path") .as_path() - .ends_with(Path::new("OpenAI").join("Codex").join("config.toml")) + .ends_with( + std::path::Path::new("OpenAI") + .join("Codex") + .join("config.toml") + ) ); } }