use std::io;use codex_config::ConfigLayerEntry;use codex_config::ConfigLayerSource;use codex_config::config_toml::ConfigLockfileToml;use codex_config::config_toml::ConfigToml;use codex_utils_absolute_path::AbsolutePathBuf;use serde::Serialize;use serde::de::DeserializeOwned;use similar::TextDiff;pub(crate) const CONFIG_LOCK_VERSION: u32 = 1;#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]pub(crate) struct ConfigLockReplayOptions { pub allow_codex_version_mismatch: bool,}pub(crate) async fn read_config_lock_from_path( path: &AbsolutePathBuf,) -> io::Result<ConfigLockfileToml> { let contents = tokio::fs::read_to_string(path).await.map_err(|err| { config_lock_error(format!( "failed to read config lock file {}: {err}", path.display() )) })?; let lockfile: ConfigLockfileToml = toml::from_str(&contents).map_err(|err| { config_lock_error(format!( "failed to parse config lock file {}: {err}", path.display() )) })?; validate_config_lock_metadata_shape(&lockfile)?; Ok(lockfile)}pub(crate) fn config_lockfile(config: ConfigToml) -> ConfigLockfileToml { ConfigLockfileToml { version: CONFIG_LOCK_VERSION, codex_version: env!("CARGO_PKG_VERSION").to_string(), config, }}pub(crate) fn validate_config_lock_replay( expected_lock: &ConfigLockfileToml, actual_lock: &ConfigLockfileToml, options: ConfigLockReplayOptions,) -> io::Result<()> { validate_config_lock_metadata_shape(expected_lock)?; validate_config_lock_metadata_shape(actual_lock)?; if !options.allow_codex_version_mismatch && expected_lock.codex_version != actual_lock.codex_version { return Err(config_lock_error(format!( "config lock Codex version mismatch: lock was generated by {}, current version is {}; set debug.config_lockfile.allow_codex_version_mismatch=true to ignore this", expected_lock.codex_version, actual_lock.codex_version ))); } let expected_lock = config_lock_for_comparison(expected_lock, options); let actual_lock = config_lock_for_comparison(actual_lock, options); if expected_lock != actual_lock { let diff = compact_diff("config", &expected_lock, &actual_lock) .unwrap_or_else(|err| format!("failed to build config lock diff: {err}")); return Err(config_lock_error(format!( "replayed effective config does not match config lock: {diff}" ))); } Ok(())}pub(crate) fn lock_layer_from_config( lock_path: &AbsolutePathBuf, lockfile: &ConfigLockfileToml,) -> io::Result<ConfigLayerEntry> { let value = toml_value( &config_without_lock_controls(&lockfile.config), "config lock", )?; Ok(ConfigLayerEntry::new( ConfigLayerSource::User { file: lock_path.clone(), profile: None, }, value, ))}pub(crate) fn config_without_lock_controls(config: &ConfigToml) -> ConfigToml { let mut config = config.clone(); clear_config_lock_debug_controls(&mut config); config}pub(crate) fn clear_config_lock_debug_controls(config: &mut ConfigToml) { if let Some(debug) = config.debug.as_mut() { debug.config_lockfile = None; } if config .debug .as_ref() .is_some_and(|debug| debug.config_lockfile.is_none()) { config.debug = None; }}fn validate_config_lock_metadata_shape(lock: &ConfigLockfileToml) -> io::Result<()> { if lock.version != CONFIG_LOCK_VERSION { return Err(config_lock_error(format!( "unsupported config lock version {}; expected {CONFIG_LOCK_VERSION}", lock.version ))); } Ok(())}fn config_lock_for_comparison( lockfile: &ConfigLockfileToml, options: ConfigLockReplayOptions,) -> ConfigLockfileToml { let mut lockfile = lockfile.clone(); clear_config_lock_debug_controls(&mut lockfile.config); if let Some(features) = lockfile.config.features.as_mut() { features.clear_removed_compatibility_entries(); } if options.allow_codex_version_mismatch { lockfile.codex_version.clear(); } lockfile}fn config_lock_error(message: impl Into<String>) -> io::Error { io::Error::other(message.into())}fn compact_diff<T: Serialize>(root: &str, expected: &T, actual: &T) -> io::Result<String> { let expected = toml::to_string_pretty(expected).map_err(|err| { config_lock_error(format!( "failed to serialize expected {root} lock TOML: {err}" )) })?; let actual = toml::to_string_pretty(actual).map_err(|err| { config_lock_error(format!( "failed to serialize actual {root} lock TOML: {err}" )) })?; Ok(TextDiff::from_lines(&expected, &actual) .unified_diff() .context_radius(2) .header("expected", "actual") .to_string())}fn toml_value<T: Serialize>(value: &T, label: &str) -> io::Result<toml::Value> { toml::Value::try_from(value) .map_err(|err| config_lock_error(format!("failed to serialize {label}: {err}")))}pub(crate) fn toml_round_trip<T>(value: &impl Serialize, label: &'static str) -> io::Result<T>where T: DeserializeOwned + Serialize,{ let value = toml_value(value, label)?; let toml = value.clone().try_into().map_err(|err| { config_lock_error(format!("failed to convert {label} to TOML shape: {err}")) })?; let represented_value = toml_value(&toml, label)?; if represented_value != value { return Err(config_lock_error(format!( "resolved {label} cannot be fully represented as TOML" ))); } Ok(toml)}