diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 2508338d49..5cb8112097 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -1417,8 +1417,7 @@ impl ConfigRequirementsToml { /// Applies the requirements whose values replace config values. /// - /// This projection is shared by config/read and config-lock export so - /// both surfaces describe the same behavior as the final runtime config. + /// This projection keeps config/read aligned with the final runtime config. pub fn apply_exact_to_config(&self, config: &mut ConfigToml) { macro_rules! apply_exact { ($field:ident) => { diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index fd888debfe..a4ecf0a3e6 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -46,7 +46,6 @@ use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::BaseInstructionsProvenance; use codex_protocol::models::PermissionProfile; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -327,9 +326,6 @@ pub struct ConfigToml { /// Defaults to `$CODEX_HOME/log`. pub log_dir: Option, - /// Debugging and reproducibility settings. - pub debug: Option, - /// Optional URI-based file opener. If set, citations to files in the model /// output will be hyperlinked using the specified URI scheme. pub file_opener: Option, @@ -518,42 +514,6 @@ pub struct ConfigToml { pub oss_provider: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct ConfigLockfileToml { - pub version: u32, - pub codex_version: String, - - /// Origin of the effective base instructions captured in the lockfile. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_instructions_provenance: Option, - - /// Replayable effective config captured in the lockfile. - pub config: ConfigToml, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct DebugToml { - pub config_lockfile: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -#[schemars(deny_unknown_fields)] -pub struct DebugConfigLockToml { - /// Directory where Codex writes effective session config lock files. - pub export_dir: Option, - - /// Lockfile to replay as the authoritative effective config. - pub load_path: Option, - - /// Allow replaying a lock generated by a different Codex version. - pub allow_codex_version_mismatch: Option, - - /// Save fields resolved from the model catalog/session configuration. - pub save_fields_resolved_from_model_catalog: Option, -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ThreadStoreToml { diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index f9a046df44..0a543e8bb0 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -943,45 +943,6 @@ ], "type": "string" }, - "DebugConfigLockToml": { - "additionalProperties": false, - "properties": { - "allow_codex_version_mismatch": { - "description": "Allow replaying a lock generated by a different Codex version.", - "type": "boolean" - }, - "export_dir": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Directory where Codex writes effective session config lock files." - }, - "load_path": { - "allOf": [ - { - "$ref": "#/definitions/AbsolutePathBuf" - } - ], - "description": "Lockfile to replay as the authoritative effective config." - }, - "save_fields_resolved_from_model_catalog": { - "description": "Save fields resolved from the model catalog/session configuration.", - "type": "boolean" - } - }, - "type": "object" - }, - "DebugToml": { - "additionalProperties": false, - "properties": { - "config_lockfile": { - "$ref": "#/definitions/DebugConfigLockToml" - } - }, - "type": "object" - }, "ExperimentalRequestUserInput": { "additionalProperties": false, "properties": { @@ -5040,14 +5001,6 @@ "description": "Compact prompt used for history compaction.", "type": "string" }, - "debug": { - "allOf": [ - { - "$ref": "#/definitions/DebugToml" - } - ], - "description": "Debugging and reproducibility settings." - }, "default_permissions": { "description": "Default permissions profile to apply. Names starting with `:` refer to built-in profiles; other names are resolved from the `[permissions]` table.", "type": "string" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index b341bcc26c..7c94456a8d 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -10592,77 +10592,6 @@ browser_use_full_cdp_access = false Ok(()) } -#[tokio::test] -async fn debug_config_lockfile_export_settings_load_from_nested_table() -> std::io::Result<()> { - let codex_home = TempDir::new()?; - std::fs::write( - codex_home.path().join(CONFIG_TOML_FILE), - r#"[debug.config_lockfile] -export_dir = "locks" -allow_codex_version_mismatch = true -save_fields_resolved_from_model_catalog = false -"#, - )?; - - let config = ConfigBuilder::without_managed_config_for_tests() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .build() - .await?; - - assert_eq!( - config.config_lock_export_dir, - Some(AbsolutePathBuf::resolve_path_against_base( - "locks", - codex_home.path() - )) - ); - assert!(config.config_lock_allow_codex_version_mismatch); - assert!(!config.config_lock_save_fields_resolved_from_model_catalog); - - Ok(()) -} - -#[tokio::test] -async fn debug_config_lockfile_load_path_loads_lock_from_nested_table() -> std::io::Result<()> { - let codex_home = TempDir::new()?; - let lock_path = codex_home.path().join("session.config.lock.toml"); - std::fs::write( - &lock_path, - format!( - r#"version = {} -codex_version = "older-version" - -[config] -"#, - crate::config_lock::CONFIG_LOCK_VERSION - ), - )?; - std::fs::write( - codex_home.path().join(CONFIG_TOML_FILE), - format!( - r#"[debug.config_lockfile] -load_path = '{}' -allow_codex_version_mismatch = true -save_fields_resolved_from_model_catalog = false -"#, - lock_path.display() - ), - )?; - - let config = ConfigBuilder::without_managed_config_for_tests() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .build() - .await?; - - assert!(config.config_lock_toml.is_some()); - assert!(config.config_lock_allow_codex_version_mismatch); - assert!(!config.config_lock_save_fields_resolved_from_model_catalog); - - Ok(()) -} - #[tokio::test] async fn explicit_feature_config_is_normalized_by_requirements() -> std::io::Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 70476fcd1a..695bb1eae7 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -21,7 +21,6 @@ use codex_config::ResidencyRequirement; use codex_config::SandboxModeRequirement; use codex_config::Sourced; use codex_config::ThreadConfigLoader; -use codex_config::config_toml::ConfigLockfileToml; use codex_config::config_toml::ConfigToml; use codex_config::config_toml::DEFAULT_PROJECT_DOC_MAX_BYTES; use codex_config::config_toml::ProjectConfig; @@ -142,9 +141,6 @@ use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; use crate::config::permissions::network_proxy_config_for_profile_selection; use crate::config::permissions::validate_user_permission_profile_names; -use crate::config_lock::config_without_lock_controls; -use crate::config_lock::lock_layer_from_config; -use crate::config_lock::read_config_lock_from_path; use crate::responses_metadata::validate_extra_metadata; use codex_network_proxy::NetworkProxyConfig; use toml::Value as TomlValue; @@ -915,20 +911,6 @@ pub struct Config { /// Directory where Codex writes log files (defaults to `$CODEX_HOME/log`). pub log_dir: PathBuf, - /// Directory where Codex writes effective session config lock files. - pub config_lock_export_dir: Option, - - /// Whether config lock replay ignores Codex version drift between the - /// lock metadata and the regenerated lock. - pub config_lock_allow_codex_version_mismatch: bool, - - /// Whether config lock creation saves values resolved from the model - /// catalog/session configuration. - pub config_lock_save_fields_resolved_from_model_catalog: bool, - - /// Effective config lock used for strict replay validation. - pub config_lock_toml: Option>, - /// Settings that govern if and what will be written to `~/.codex/history.jsonl`. pub history: History, @@ -1520,45 +1502,6 @@ impl ConfigBuilder { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, err)); } }; - let config_lock_settings = config_toml - .debug - .as_ref() - .and_then(|debug| debug.config_lockfile.as_ref()); - if let Some(config_lock_load_path) = - config_lock_settings.and_then(|config_lock| config_lock.load_path.as_ref()) - { - let allow_codex_version_mismatch = config_lock_settings - .and_then(|config_lock| config_lock.allow_codex_version_mismatch) - .unwrap_or(false); - let save_fields_resolved_from_model_catalog = config_lock_settings - .and_then(|config_lock| config_lock.save_fields_resolved_from_model_catalog) - .unwrap_or(true); - let lockfile_toml = read_config_lock_from_path(config_lock_load_path).await?; - let expected_lock_config = lockfile_toml.clone(); - let lock_layer = lock_layer_from_config(config_lock_load_path, &lockfile_toml)?; - let lock_config_toml = config_without_lock_controls(&lockfile_toml.config); - let lock_config_layer_stack = ConfigLayerStack::new( - vec![lock_layer], - config_layer_stack.requirements().clone(), - config_layer_stack.requirements_toml().clone(), - )?; - let mut config = Config::load_config_with_layer_stack( - LOCAL_FS.as_ref(), - lock_config_toml, - harness_overrides, - codex_home, - lock_config_layer_stack, - ) - .await?; - if let Some(provenance) = lockfile_toml.base_instructions_provenance { - config.base_instructions_provenance = Some(provenance); - } - config.config_lock_toml = Some(Arc::new(expected_lock_config)); - config.config_lock_allow_codex_version_mismatch = allow_codex_version_mismatch; - config.config_lock_save_fields_resolved_from_model_catalog = - save_fields_resolved_from_model_catalog; - return Ok(config); - } Config::load_config_with_layer_stack( LOCAL_FS.as_ref(), config_toml, @@ -4223,24 +4166,6 @@ impl Config { codex_home, sqlite: codex_state::SqliteConfig::from_sqlite_home(sqlite_home), log_dir, - config_lock_export_dir: cfg - .debug - .as_ref() - .and_then(|debug| debug.config_lockfile.as_ref()) - .and_then(|config_lock| config_lock.export_dir.clone()), - config_lock_allow_codex_version_mismatch: cfg - .debug - .as_ref() - .and_then(|debug| debug.config_lockfile.as_ref()) - .and_then(|config_lock| config_lock.allow_codex_version_mismatch) - .unwrap_or(false), - config_lock_save_fields_resolved_from_model_catalog: cfg - .debug - .as_ref() - .and_then(|debug| debug.config_lockfile.as_ref()) - .and_then(|config_lock| config_lock.save_fields_resolved_from_model_catalog) - .unwrap_or(true), - config_lock_toml: None, config_layer_stack, history, ephemeral: ephemeral.unwrap_or_default(), diff --git a/codex-rs/core/src/config_lock.rs b/codex-rs/core/src/config_lock.rs deleted file mode 100644 index 73f57f94d6..0000000000 --- a/codex-rs/core/src/config_lock.rs +++ /dev/null @@ -1,183 +0,0 @@ -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 { - 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(), - base_instructions_provenance: None, - 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 mut actual_lock = config_lock_for_comparison(actual_lock, options); - if expected_lock.base_instructions_provenance.is_none() { - actual_lock.base_instructions_provenance = None; - } - 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 { - 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) -> io::Error { - io::Error::other(message.into()) -} - -fn compact_diff(root: &str, expected: &T, actual: &T) -> io::Result { - 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(value: &T, label: &str) -> io::Result { - toml::Value::try_from(value) - .map_err(|err| config_lock_error(format!("failed to serialize {label}: {err}"))) -} - -pub(crate) fn toml_round_trip(value: &impl Serialize, label: &'static str) -> io::Result -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) -} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index db14427c50..d8db808548 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -25,7 +25,6 @@ mod compact_remote; mod compact_remote_history; mod compact_remote_v2; mod compact_token_budget; -mod config_lock; mod user_message_admission; pub use codex_thread::BackgroundTerminalInfo; pub use codex_thread::CodexThread; diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs deleted file mode 100644 index 56ee5c59d2..0000000000 --- a/codex-rs/core/src/session/config_lock.rs +++ /dev/null @@ -1,641 +0,0 @@ -use anyhow::Context; -use codex_config::config_toml::ConfigLockfileToml; -use codex_config::config_toml::ConfigToml; -use codex_config::config_toml::OrchestratorFeatureToml; -use codex_config::config_toml::OrchestratorToml; -use codex_config::types::MemoriesToml; -use codex_features::CurrentTimeReminderConfigToml; -use codex_features::Feature; -use codex_features::FeatureToml; -use codex_features::FeaturesToml; -use codex_features::MultiAgentV2ConfigToml; -use codex_features::RolloutBudgetConfigToml; -use codex_features::TokenBudgetConfigToml; -use codex_features::ToolRegistryConfigToml; -use codex_protocol::ThreadId; -use codex_protocol::models::BaseInstructionsProvenance; - -use crate::config::Config; -use crate::config_lock::ConfigLockReplayOptions; -use crate::config_lock::clear_config_lock_debug_controls; -use crate::config_lock::config_lockfile; -use crate::config_lock::toml_round_trip; -use crate::config_lock::validate_config_lock_replay; - -use super::SessionConfiguration; - -pub(crate) async fn validate_config_lock_if_configured( - session_configuration: &SessionConfiguration, - base_instructions_provenance: Option<&BaseInstructionsProvenance>, -) -> anyhow::Result<()> { - if session_configuration.session_source.is_non_root_agent() { - return Ok(()); - } - let Some(expected) = session_configuration - .original_config_do_not_use - .config_lock_toml - .as_ref() - else { - return Ok(()); - }; - let mut actual = session_configuration.to_config_lockfile_toml()?; - if actual.config.instructions.is_some() { - actual.base_instructions_provenance = base_instructions_provenance.cloned(); - } - let config = session_configuration.original_config_do_not_use.as_ref(); - let options = ConfigLockReplayOptions { - allow_codex_version_mismatch: config.config_lock_allow_codex_version_mismatch, - }; - validate_config_lock_replay(expected, &actual, options) - .context("config lock replay validation failed")?; - Ok(()) -} - -pub(crate) async fn export_config_lock_if_configured( - session_configuration: &SessionConfiguration, - conversation_id: ThreadId, - base_instructions_provenance: Option<&BaseInstructionsProvenance>, -) -> anyhow::Result<()> { - let config = session_configuration.original_config_do_not_use.as_ref(); - let Some(export_dir) = config.config_lock_export_dir.as_ref() else { - return Ok(()); - }; - - let mut lock = session_configuration.to_config_lockfile_toml()?; - if lock.config.instructions.is_some() { - lock.base_instructions_provenance = base_instructions_provenance.cloned(); - } - let lock = toml::to_string_pretty(&lock).context("failed to serialize config lock")?; - let path = export_dir.join(format!("{conversation_id}.config.lock.toml")); - - tokio::fs::create_dir_all(export_dir) - .await - .with_context(|| { - format!( - "failed to create config lock export directory {}", - export_dir.display() - ) - })?; - tokio::fs::write(&path, lock) - .await - .with_context(|| format!("failed to write config lock to {}", path.display()))?; - - Ok(()) -} - -impl SessionConfiguration { - pub(crate) fn to_config_lockfile_toml(&self) -> anyhow::Result { - Ok(config_lockfile(session_configuration_to_lock_config_toml( - self, - )?)) - } -} - -fn session_configuration_to_lock_config_toml( - sc: &SessionConfiguration, -) -> anyhow::Result { - let config = sc.original_config_do_not_use.as_ref(); - // Start from the resolved layer stack, then patch in values that are only - // known after session setup. Export and replay validation both use this - // path, so every field here is part of the lockfile contract. - let mut lock_config: ConfigToml = config - .config_layer_stack - .effective_config() - .try_into() - .context("failed to deserialize effective config for config lock")?; - if config.config_lock_save_fields_resolved_from_model_catalog { - save_session_resolved_fields(sc, &mut lock_config); - } - - save_config_resolved_fields(config, &mut lock_config)?; - drop_lockfile_inputs(&mut lock_config); - // Apply exact managed values last so cleanup cannot discard their runtime-effective values. - config - .config_layer_stack - .requirements_toml() - .apply_exact_to_config(&mut lock_config); - - Ok(lock_config) -} - -/// Saves values chosen during session construction from the model catalog, -/// collaboration mode, and resolved prompt setup. -/// -/// These values are not always present in the raw layer stack, so copy them -/// from the live session when the lockfile should be fully self-contained. -fn save_session_resolved_fields(sc: &SessionConfiguration, lock_config: &mut ConfigToml) { - lock_config.model = Some(sc.collaboration_mode.model().to_string()); - lock_config.model_reasoning_effort = sc.collaboration_mode.reasoning_effort(); - lock_config.model_reasoning_summary = sc.model_reasoning_summary; - lock_config.service_tier = sc.service_tier.clone(); - lock_config.instructions = Some(sc.base_instructions.clone()); - lock_config.developer_instructions = sc.developer_instructions.clone(); - lock_config.compact_prompt = sc.compact_prompt.clone(); - lock_config.personality = sc.personality; - lock_config.approval_policy = Some(sc.approval_policy.value()); - lock_config.approvals_reviewer = Some(sc.approvals_reviewer); -} - -/// Saves values stored on `Config` after higher-level resolution, -/// normalization, defaulting, or feature materialization. -/// -/// Persist the resolved representation so replay compares against the behavior -/// Codex actually ran with, not only the user-authored TOML inputs. -fn save_config_resolved_fields( - config: &Config, - lock_config: &mut ConfigToml, -) -> anyhow::Result<()> { - lock_config.web_search = Some(config.web_search_mode.value()); - lock_config.model_provider = Some(config.model_provider_id.clone()); - lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort.clone(); - lock_config.model_verbosity = config.model_verbosity; - lock_config.include_permissions_instructions = Some(config.include_permissions_instructions); - lock_config.include_apps_instructions = Some(config.include_apps_instructions); - lock_config.include_collaboration_mode_instructions = - Some(config.include_collaboration_mode_instructions); - lock_config.include_environment_context = Some(config.include_environment_context); - lock_config.background_terminal_max_timeout = Some(config.background_terminal_max_timeout); - - // Feature aliases and feature configs need to be written in their resolved - // form; otherwise replay can drift when a legacy key maps to the same - // runtime feature. - let features = lock_config - .features - .get_or_insert_with(FeaturesToml::default); - features.materialize_resolved_enabled(config.features.get()); - if config.tool_registry.error_on_tool_collisions - || config.tool_registry.turn_metadata_includes_tool_info - || features.tool_registry.is_some() - { - features.tool_registry = Some(ToolRegistryConfigToml { - error_on_tool_collisions: Some(config.tool_registry.error_on_tool_collisions), - turn_metadata_includes_tool_info: Some( - config.tool_registry.turn_metadata_includes_tool_info, - ), - }); - } - let mut multi_agent_v2: MultiAgentV2ConfigToml = - resolved_config_to_toml(&config.multi_agent_v2, "features.multi_agent_v2")?; - multi_agent_v2.enabled = Some(config.features.enabled(Feature::MultiAgentV2)); - features.multi_agent_v2 = Some(FeatureToml::Config(multi_agent_v2)); - if let Some(token_budget) = config.token_budget.as_ref() - && super::token_budget::has_explicit_settings(config) - { - let mut token_budget: TokenBudgetConfigToml = - resolved_config_to_toml(token_budget, "features.token_budget")?; - token_budget.enabled = Some(config.features.enabled(Feature::TokenBudget)); - features.token_budget = Some(FeatureToml::Config(token_budget)); - } - if let Some(rollout_budget) = config.rollout_budget.as_ref() { - let mut rollout_budget: RolloutBudgetConfigToml = - resolved_config_to_toml(rollout_budget, "features.rollout_budget")?; - rollout_budget.enabled = Some(config.features.enabled(Feature::RolloutBudget)); - features.rollout_budget = Some(FeatureToml::Config(rollout_budget)); - } - if let Some(current_time_reminder) = config.current_time_reminder.as_ref() { - let mut current_time_reminder: CurrentTimeReminderConfigToml = - resolved_config_to_toml(current_time_reminder, "features.current_time_reminder")?; - current_time_reminder.enabled = Some(config.features.enabled(Feature::CurrentTimeReminder)); - features.current_time_reminder = Some(FeatureToml::Config(current_time_reminder)); - } - lock_config.memories = Some(resolved_config_to_toml::( - &config.memories, - "memories", - )?); - - let agents = lock_config.agents.get_or_insert_with(Default::default); - agents.enabled = Some(config.agents_enabled); - agents.max_concurrent_threads_per_session = config.agent_max_threads; - agents.max_depth = Some(config.agent_max_depth); - agents.default_subagent_model = config.agent_default_subagent_model.clone(); - agents.default_subagent_reasoning_effort = - config.agent_default_subagent_reasoning_effort.clone(); - agents.interrupt_message = Some(config.agent_interrupt_message_enabled); - - lock_config - .skills - .get_or_insert_with(Default::default) - .include_instructions = Some(config.include_skill_instructions); - lock_config - .orchestrator - .get_or_insert_with(OrchestratorToml::default) - .skills - .get_or_insert_with(OrchestratorFeatureToml::default) - .enabled = Some(config.orchestrator_skills_enabled); - lock_config - .orchestrator - .get_or_insert_with(OrchestratorToml::default) - .mcp - .get_or_insert_with(Default::default) - .enabled = Some(config.orchestrator_mcp_enabled); - - Ok(()) -} - -fn drop_lockfile_inputs(lock_config: &mut ConfigToml) { - // The lockfile should contain replayable values, not the profile, - // debug-control, file-include, and environment-specific inputs that - // produced those values in the original session. - lock_config.profile = None; - lock_config.profiles.clear(); - clear_config_lock_debug_controls(lock_config); - lock_config.model_instructions_file = None; - lock_config.experimental_compact_prompt_file = None; - lock_config.model_catalog_json = None; - lock_config.sandbox_mode = None; - lock_config.sandbox_workspace_write = None; - lock_config.default_permissions = None; - lock_config.permissions = None; - lock_config.experimental_use_unified_exec_tool = None; -} - -fn resolved_config_to_toml( - value: &impl serde::Serialize, - label: &'static str, -) -> anyhow::Result -where - Toml: serde::de::DeserializeOwned + serde::Serialize, -{ - toml_round_trip(value, label).map_err(anyhow::Error::from) -} - -#[cfg(test)] -mod tests { - use super::*; - use codex_config::test_support::CloudConfigBundleFixture; - use codex_models_manager::bundled_models_response; - use pretty_assertions::assert_eq; - use std::path::Path; - use std::sync::Arc; - - fn write_model_catalog(path: &Path) { - let mut catalog = bundled_models_response() - .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); - catalog.models = catalog.models.into_iter().take(1).collect(); - std::fs::write( - path, - serde_json::to_string(&catalog).expect("serialize model catalog"), - ) - .expect("write model catalog"); - } - - #[tokio::test] - async fn lock_contains_prompts_and_materializes_features() { - let mut sc = crate::session::tests::make_session_configuration_for_tests().await; - let mut config = (*sc.original_config_do_not_use).clone(); - config.tool_registry.error_on_tool_collisions = true; - config.tool_registry.turn_metadata_includes_tool_info = true; - config.multi_agent_v2.subagent_developer_instructions = - Some("Locked subagent developer instructions.".to_string()); - config.token_budget = Some(crate::config::TokenBudgetConfig { - reminder_threshold_tokens: Some(16_000), - reminder_message_template: "Locked reminder: {n_remaining} tokens.".to_string(), - guidance_message: Some("Locked context-window guidance.".to_string()), - auto_compact_fallback_prompt: Some("Write notes before rollover.".to_string()), - auto_compact_fallback_buffer_tokens: Some(8_000), - }); - config - .features - .enable(Feature::TokenBudget) - .expect("token_budget should be enableable in tests"); - config.rollout_budget = Some(crate::config::RolloutBudgetConfig { - limit_tokens: 100_000, - reminder_at_remaining_tokens: vec![50_000, 25_000, 10_000], - sampling_token_weight: 1.0, - prefill_token_weight: 0.25, - }); - config - .features - .enable(Feature::RolloutBudget) - .expect("rollout_budget should be enableable in tests"); - config.current_time_reminder = Some(crate::config::CurrentTimeReminderConfig::default()); - config - .features - .enable(Feature::CurrentTimeReminder) - .expect("current_time_reminder should be enableable in tests"); - sc.original_config_do_not_use = Arc::new(config); - sc.base_instructions = "resolved instructions".to_string(); - sc.developer_instructions = Some("resolved developer instructions".to_string()); - sc.compact_prompt = Some("resolved compact prompt".to_string()); - - let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize"); - let lock = &lockfile.config; - - assert_eq!(lock.instructions, Some(sc.base_instructions.clone())); - assert_eq!(lock.developer_instructions, sc.developer_instructions); - assert_eq!(lock.compact_prompt, sc.compact_prompt); - assert_eq!(lock.model, Some(sc.collaboration_mode.model().to_string())); - assert_eq!( - lock.model_reasoning_effort, - sc.collaboration_mode.reasoning_effort() - ); - assert_eq!(lock.profile, None); - assert!(lock.profiles.is_empty()); - assert!( - lock.debug - .as_ref() - .is_none_or(|debug| debug.config_lockfile.is_none()) - ); - assert!(lock.memories.is_some()); - let features = lock - .features - .as_ref() - .expect("lock should materialize feature states"); - assert_eq!( - features.tool_registry, - Some(ToolRegistryConfigToml { - error_on_tool_collisions: Some(true), - turn_metadata_includes_tool_info: Some(true), - }) - ); - let feature_entries = features.entries(); - for spec in codex_features::FEATURES { - assert_eq!( - feature_entries.get(spec.key), - Some(&sc.original_config_do_not_use.features.enabled(spec.id)), - "{}", - spec.key - ); - } - assert_eq!( - features.code_mode, - Some(FeatureToml::Enabled( - sc.original_config_do_not_use - .features - .enabled(Feature::CodeMode) - )) - ); - - let multi_agent_v2 = features - .multi_agent_v2 - .as_ref() - .expect("multi_agent_v2 config should be materialized"); - assert!(matches!( - multi_agent_v2, - FeatureToml::Config(MultiAgentV2ConfigToml { - enabled: Some(false), - max_concurrent_threads_per_session: Some(_), - min_wait_timeout_ms: Some(_), - max_wait_timeout_ms: Some(_), - default_wait_timeout_ms: Some(_), - subagent_developer_instructions: Some(instructions), - hide_spawn_agent_metadata: Some(_), - .. - }) if instructions == "Locked subagent developer instructions." - )); - - assert_eq!( - features.token_budget, - Some(FeatureToml::Config(TokenBudgetConfigToml { - enabled: Some(true), - reminder_threshold_tokens: Some(16_000), - reminder_message_template: Some( - "Locked reminder: {n_remaining} tokens.".to_string() - ), - guidance_message: Some("Locked context-window guidance.".to_string()), - auto_compact_fallback_prompt: Some("Write notes before rollover.".to_string()), - auto_compact_fallback_buffer_tokens: Some(8_000), - })) - ); - - assert_eq!( - features.rollout_budget, - Some(FeatureToml::Config(RolloutBudgetConfigToml { - enabled: Some(true), - limit_tokens: Some(100_000), - reminder_at_remaining_tokens: Some(vec![50_000, 25_000, 10_000]), - sampling_token_weight: Some(1.0), - prefill_token_weight: Some(0.25), - })) - ); - assert_eq!( - features.current_time_reminder, - Some(FeatureToml::Config(CurrentTimeReminderConfigToml { - enabled: Some(true), - reminder_interval_seconds: Some(1), - clock_source: Some(codex_features::CurrentTimeSource::System), - delivery_mode: Some(codex_features::CurrentTimeReminderDeliveryMode::AnyInference), - sleep_tool: Some(false), - })) - ); - - assert_eq!(lockfile.version, crate::config_lock::CONFIG_LOCK_VERSION); - } - - #[tokio::test] - async fn lock_preserves_model_owned_token_budget_defaults() { - let mut sc = crate::session::tests::make_session_configuration_for_tests().await; - let mut config = (*sc.original_config_do_not_use).clone(); - config.token_budget = Some(crate::config::TokenBudgetConfig::default()); - config - .features - .enable(Feature::TokenBudget) - .expect("token_budget should be enableable in tests"); - sc.original_config_do_not_use = Arc::new(config); - - let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize"); - let features = lockfile - .config - .features - .as_ref() - .expect("lock should materialize feature states"); - - assert_eq!( - features.token_budget.as_ref(), - Some(&FeatureToml::Enabled(true)) - ); - assert_eq!(features.tool_registry, None); - } - - #[tokio::test] - async fn lock_skips_session_values_when_model_catalog_fields_are_not_saved() { - let mut sc = crate::session::tests::make_session_configuration_for_tests().await; - let mut config = (*sc.original_config_do_not_use).clone(); - config.config_lock_save_fields_resolved_from_model_catalog = false; - sc.original_config_do_not_use = Arc::new(config); - sc.base_instructions = "catalog instructions".to_string(); - sc.developer_instructions = Some("catalog developer instructions".to_string()); - sc.compact_prompt = Some("catalog compact prompt".to_string()); - sc.service_tier = Some("flex".to_string()); - - let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize"); - let lock = &lockfile.config; - - assert_eq!(lock.model, None); - assert_eq!(lock.model_reasoning_effort, None); - assert_eq!(lock.model_reasoning_summary, None); - assert_eq!(lock.service_tier, None); - assert_eq!(lock.instructions, None); - assert_eq!(lock.developer_instructions, None); - assert_eq!(lock.compact_prompt, None); - assert_eq!(lock.personality, None); - assert_eq!(lock.approval_policy, None); - assert_eq!(lock.approvals_reviewer, None); - } - - #[tokio::test] - async fn lock_contains_exact_managed_requirements() { - let codex_home = tempfile::tempdir().expect("create temp dir"); - let sqlite_home = codex_home.path().join("managed-state"); - let log_dir = codex_home.path().join("managed-logs"); - let catalog_path = codex_home.path().join("managed-models.json"); - write_model_catalog(&catalog_path); - let requirements = format!( - r#" -sqlite_home = {:?} -log_dir = {:?} -model_catalog_json = {:?} -check_for_update_on_startup = false -allow_login_shell = false - -[feedback] -enabled = false - -[windows] -sandbox_private_desktop = false -"#, - sqlite_home.display(), - log_dir.display(), - catalog_path.display(), - ); - let mut config = crate::config::ConfigBuilder::without_managed_config_for_tests() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .cloud_config_bundle( - CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements), - ) - .build() - .await - .expect("config should load"); - config.config_lock_save_fields_resolved_from_model_catalog = false; - let mut sc = crate::session::tests::make_session_configuration_for_tests().await; - sc.original_config_do_not_use = Arc::new(config); - - let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize"); - let lock = &lockfile.config; - - assert_eq!(lock.sqlite_home.as_deref(), Some(sqlite_home.as_path())); - assert_eq!(lock.log_dir.as_deref(), Some(log_dir.as_path())); - assert_eq!( - lock.model_catalog_json.as_deref(), - Some(catalog_path.as_path()) - ); - assert_eq!(lock.check_for_update_on_startup, Some(false)); - assert_eq!(lock.allow_login_shell, Some(false)); - assert_eq!( - lock.feedback.as_ref().and_then(|feedback| feedback.enabled), - Some(false) - ); - assert_eq!( - lock.windows - .as_ref() - .and_then(|windows| windows.sandbox_private_desktop), - Some(false) - ); - } - - #[tokio::test] - async fn lock_drops_unmanaged_model_catalog_input() { - let codex_home = tempfile::tempdir().expect("create temp dir"); - let catalog_path = codex_home.path().join("user-models.json"); - write_model_catalog(&catalog_path); - let config = crate::config::ConfigBuilder::without_managed_config_for_tests() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .cli_overrides(vec![( - "model_catalog_json".to_string(), - toml::Value::String(catalog_path.display().to_string()), - )]) - .build() - .await - .expect("config should load"); - let mut sc = crate::session::tests::make_session_configuration_for_tests().await; - sc.original_config_do_not_use = Arc::new(config); - - let lockfile = sc.to_config_lockfile_toml().expect("lock should serialize"); - - assert_eq!(lockfile.config.model_catalog_json, None); - } - - #[tokio::test] - async fn lock_validation_reports_config_diff() { - let sc = crate::session::tests::make_session_configuration_for_tests().await; - let expected = sc.to_config_lockfile_toml().expect("lock should serialize"); - let mut actual = expected.clone(); - actual.config.model = Some("different-model".to_string()); - - let error = - validate_config_lock_replay(&expected, &actual, ConfigLockReplayOptions::default()) - .expect_err("config drift should fail"); - let message = error.to_string(); - assert!( - message.contains("replayed effective config does not match config lock"), - "{message}" - ); - assert!(message.contains("model = "), "{message}"); - } - - #[tokio::test] - async fn lock_validation_ignores_removed_apps_mcp_path_override() { - let sc = crate::session::tests::make_session_configuration_for_tests().await; - let actual = sc.to_config_lockfile_toml().expect("lock should serialize"); - let mut expected_value = toml::Value::try_from(&actual).expect("lock should become TOML"); - expected_value["config"]["features"] - .as_table_mut() - .expect("features should be a table") - .insert( - "apps_mcp_path_override".to_string(), - toml::Value::Table(toml::Table::from_iter([ - ("enabled".to_string(), toml::Value::Boolean(true)), - ( - "path".to_string(), - toml::Value::String("/custom/mcp".to_string()), - ), - ])), - ); - let expected: ConfigLockfileToml = expected_value - .try_into() - .expect("lock with removed input should deserialize"); - - validate_config_lock_replay(&expected, &actual, ConfigLockReplayOptions::default()) - .expect("removed compatibility input should not cause lock drift"); - } - - #[tokio::test] - async fn lock_validation_rejects_codex_version_mismatch_by_default() { - let sc = crate::session::tests::make_session_configuration_for_tests().await; - let mut expected = sc.to_config_lockfile_toml().expect("lock should serialize"); - expected.codex_version = "older-version".to_string(); - let actual = sc.to_config_lockfile_toml().expect("lock should serialize"); - - let error = - validate_config_lock_replay(&expected, &actual, ConfigLockReplayOptions::default()) - .expect_err("version drift should fail"); - let message = error.to_string(); - assert!( - message.contains("config lock Codex version mismatch"), - "{message}" - ); - assert!( - message.contains("debug.config_lockfile.allow_codex_version_mismatch=true"), - "{message}" - ); - } - - #[tokio::test] - async fn lock_validation_can_ignore_codex_version_mismatch() { - let sc = crate::session::tests::make_session_configuration_for_tests().await; - let mut expected = sc.to_config_lockfile_toml().expect("lock should serialize"); - expected.codex_version = "older-version".to_string(); - let actual = sc.to_config_lockfile_toml().expect("lock should serialize"); - - validate_config_lock_replay( - &expected, - &actual, - ConfigLockReplayOptions { - allow_codex_version_mismatch: true, - }, - ) - .expect("version drift should be ignored"); - } -} diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 3abbba529c..98f2be3b64 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -204,7 +204,6 @@ use codex_protocol::error::Result as CodexResult; use codex_protocol::exec_output::StreamOutput; mod code_mode_warning; -mod config_lock; pub(crate) mod context_window; mod extension_metrics; mod handlers; @@ -227,8 +226,6 @@ pub(crate) mod turn; pub(crate) mod turn_context; mod world_state; use self::code_mode_warning::unsupported_code_mode_warning; -use self::config_lock::export_config_lock_if_configured; -use self::config_lock::validate_config_lock_if_configured; #[cfg(test)] use self::handlers::submission_dispatch_span; use self::handlers::submission_loop; @@ -683,11 +680,6 @@ impl Session { .get_model_info(model.as_str(), &config.to_models_manager_config()) .await; let configured_config = Arc::clone(&config); - if config.config_lock_export_dir.is_some() - && config.config_lock_save_fields_resolved_from_model_catalog - { - self::token_budget::apply_model_defaults(Arc::make_mut(&mut config), &model_info); - } let multi_agent_version = config.multi_agent_version_override().or_else(|| { resolve_multi_agent_version(&conversation_history, inherited_multi_agent_version) }); @@ -735,7 +727,6 @@ impl Session { developer_instructions: config.developer_instructions.clone(), personality: config.personality, base_instructions, - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: session_permission_profile_state_from_config(&config)?, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 206ec5d5e7..391d976bf8 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -85,9 +85,6 @@ pub(crate) struct SessionConfiguration { /// Base instructions for the session. pub(super) base_instructions: String, - /// Compact prompt override. - pub(super) compact_prompt: Option, - /// When to escalate for approval for execution pub(super) approval_policy: Constrained, pub(super) approvals_reviewer: ApprovalsReviewer, @@ -1100,17 +1097,6 @@ impl Session { ); } session_configuration.thread_name = thread_name.clone(); - validate_config_lock_if_configured( - &session_configuration, - base_instructions_provenance.as_ref(), - ) - .await?; - export_config_lock_if_configured( - &session_configuration, - thread_id, - base_instructions_provenance.as_ref(), - ) - .await?; let mut state = SessionState::new_with_auto_compact_window_ids( session_configuration.clone(), initial_auto_compact_window_ids, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index a3b9fb9023..3a2263764c 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -4099,7 +4099,6 @@ async fn set_rate_limits_retains_previous_credits() { .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), @@ -4209,7 +4208,6 @@ async fn set_rate_limits_updates_plan_type_when_present() { .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), @@ -4758,7 +4756,6 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), @@ -5555,7 +5552,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() { .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), @@ -5697,7 +5693,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), @@ -5977,7 +5972,6 @@ async fn make_session_with_config_and_rx( .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), @@ -6092,7 +6086,6 @@ async fn make_session_with_history_source_and_agent_control_and_rx( .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), @@ -7957,7 +7950,6 @@ where .base_instructions .clone() .unwrap_or_else(|| model_info.get_model_instructions(config.personality)), - compact_prompt: config.compact_prompt.clone(), approval_policy: config.permissions.approval_policy.clone(), approvals_reviewer: config.approvals_reviewer, permission_profile_state: config.permissions.permission_profile_state().clone(), diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 98ed4018c7..d1f3153c0a 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -619,7 +619,6 @@ async fn review_uses_updated_turn_permissions_and_approval_policy() { .with_config(|config| { config.review_model = Some("gpt-5.4".to_string()); config.model_context_window = Some(128_000); - config.config_lock_export_dir = Some(config.codex_home.join("review-config-locks")); config.service_tier = Some(ServiceTier::Fast.request_value().to_string()); config .features diff --git a/codex-rs/core/tests/suite/token_budget.rs b/codex-rs/core/tests/suite/token_budget.rs index a1c05edd34..4a602b2d76 100644 --- a/codex-rs/core/tests/suite/token_budget.rs +++ b/codex-rs/core/tests/suite/token_budget.rs @@ -1,13 +1,10 @@ use anyhow::Result; -use codex_config::config_toml::ConfigLockfileToml; use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; use codex_core::config::Config; use codex_core::config::TokenBudgetConfig; use codex_extension_api::ExtensionRegistryBuilder; use codex_features::Feature; -use codex_features::FeatureToml; -use codex_features::TokenBudgetConfigToml; use codex_model_provider_info::built_in_model_providers; use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::items::TurnItem; @@ -58,7 +55,6 @@ use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use std::time::Duration; -use tempfile::TempDir; const CONFIGURED_CONTEXT_WINDOW: i64 = 128_000; const AUTO_COMPACT_FALLBACK_PROMPT: &str = "Save the important state before rollover."; @@ -382,126 +378,6 @@ async fn token_budget_explicit_default_template_overrides_model_defaults() -> Re Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn token_budget_model_defaults_survive_config_lock_replay() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = start_mock_server().await; - let responses = mount_sse_sequence( - &server, - vec![sse_completed("resp-1"), sse_completed("resp-2")], - ) - .await; - let home = Arc::new(TempDir::new()?); - let initial = test_codex() - .with_home(Arc::clone(&home)) - .with_model_info_override("gpt-5.2", |model_info| { - model_info - .model_messages - .as_mut() - .expect("bundled model should have model messages") - .token_budget = Some(model_token_budget_config()); - }) - .with_config(|config| { - config.model_context_window = Some(CONFIGURED_CONTEXT_WINDOW); - config.config_lock_export_dir = Some(config.codex_home.join("memento-config-locks")); - config - .features - .enable(Feature::TokenBudget) - .expect("test config should allow token budget"); - }) - .build_with_auto_env(&server) - .await?; - initial - .submit_turn("inspect guidance before lock replay") - .await?; - - let lock_path = initial - .codex_home_path() - .join("memento-config-locks") - .join(format!( - "{}.config.lock.toml", - initial.session_configured.thread_id - )); - let lock: ConfigLockfileToml = toml::from_str(&std::fs::read_to_string(&lock_path)?)?; - assert_eq!( - lock.config - .features - .as_ref() - .and_then(|features| features.token_budget.as_ref()), - Some(&FeatureToml::Config(TokenBudgetConfigToml { - enabled: Some(true), - reminder_threshold_tokens: Some(6_144), - reminder_message_template: Some( - "Model reminder: {n_remaining} tokens remain.".to_string() - ), - guidance_message: Some("Use the model-owned context-window guidance.".to_string()), - auto_compact_fallback_prompt: Some(AUTO_COMPACT_FALLBACK_PROMPT.to_string()), - auto_compact_fallback_buffer_tokens: Some(16_384), - })) - ); - let replay = test_codex() - .with_home(home) - .with_pre_build_hook(move |home| { - std::fs::write( - home.join("config.toml"), - format!("[debug.config_lockfile]\nload_path = {lock_path:?}\n"), - ) - .expect("write exported config lock replay settings"); - }) - .with_model_info_override("gpt-5.2", |model_info| { - let mut updated_defaults = model_token_budget_config(); - updated_defaults.reminder_threshold_tokens = 4_096; - updated_defaults.reminder_message_template = - "Updated model reminder: {n_remaining} tokens remain.".to_string(); - updated_defaults.guidance_message = - "Use the updated model-owned context-window guidance.".to_string(); - updated_defaults.auto_compact_fallback_prompt = - "Use the updated fallback before rollover.".to_string(); - updated_defaults.auto_compact_fallback_buffer_tokens = 8_192; - model_info - .model_messages - .as_mut() - .expect("bundled model should have model messages") - .token_budget = Some(updated_defaults); - }) - .with_config(|config| { - config.model_context_window = Some(CONFIGURED_CONTEXT_WINDOW); - }) - .build_with_auto_env(&server) - .await?; - core_test_support::submit_thread_settings( - &replay.codex, - ThreadSettingsOverrides { - model: Some("gpt-5.4".to_string()), - ..Default::default() - }, - ) - .await?; - replay - .submit_text_turn("inspect guidance after lock replay and model switch") - .await?; - - let requests = responses.requests(); - assert_eq!(requests.len(), 2); - for request in &requests { - assert!( - request.body_contains_text("Use the model-owned context-window guidance."), - "exporting and replaying a config lock must preserve model-owned guidance" - ); - } - assert_eq!(requests[1].body_json()["model"], "gpt-5.4"); - assert!( - requests[1] - .message_input_texts("developer") - .iter() - .any(|text| text.contains("")), - "replaying model-owned instructions must not override the new model's template" - ); - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn token_budget_defaults_follow_the_active_model() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index b3e25d3358..0fa2cbf8fc 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -37,10 +37,6 @@ impl FeatureConfig for CodeModeConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] @@ -57,10 +53,6 @@ impl FeatureConfig for CodeModeHostConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] @@ -77,10 +69,6 @@ impl FeatureConfig for NonPrefixedMcpToolNamesConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] @@ -134,10 +122,6 @@ impl FeatureConfig for MultiAgentV2ConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] @@ -172,10 +156,6 @@ impl FeatureConfig for TokenBudgetConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] @@ -201,10 +181,6 @@ impl FeatureConfig for RolloutBudgetConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)] @@ -246,10 +222,6 @@ impl FeatureConfig for CurrentTimeReminderConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] @@ -294,10 +266,6 @@ impl FeatureConfig for NetworkProxyConfigToml { fn enabled(&self) -> Option { self.enabled } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } } #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)] diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 8daf9e10d1..d0f32c447f 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -706,13 +706,6 @@ impl Features { } impl FeaturesToml { - /// Removes compatibility-only inputs that no longer affect runtime - /// behavior or belong in newly materialized config. - pub fn clear_removed_compatibility_entries(&mut self) { - self.removed_apps_mcp_path_override = None; - self.entries.remove("apps_mcp_path_override"); - } - pub fn entries(&self) -> BTreeMap { let mut entries = self.entries.clone(); if let Some(enabled) = self.code_mode.as_ref().and_then(FeatureToml::enabled) { @@ -749,58 +742,6 @@ impl FeaturesToml { } entries } - - pub fn materialize_resolved_enabled(&mut self, features: &Features) { - self.clear_removed_compatibility_entries(); - let Self { - tool_registry: _, - code_mode, - code_mode_host, - non_prefixed_mcp_tool_names, - multi_agent_v2, - token_budget, - rollout_budget, - current_time_reminder, - removed_apps_mcp_path_override: _, - network_proxy, - entries, - } = self; - for key in legacy::legacy_feature_keys() { - entries.remove(key); - } - for spec in FEATURES { - let enabled = features.enabled(spec.id); - if spec.id == Feature::CodeMode { - materialize_resolved_feature_enabled(code_mode, enabled); - } else if spec.id == Feature::CodeModeHost { - materialize_resolved_feature_enabled(code_mode_host, enabled); - } else if spec.id == Feature::NonPrefixedMcpToolNames { - materialize_resolved_feature_enabled(non_prefixed_mcp_tool_names, enabled); - } else if spec.id == Feature::MultiAgentV2 { - materialize_resolved_feature_enabled(multi_agent_v2, enabled); - } else if spec.id == Feature::TokenBudget { - materialize_resolved_feature_enabled(token_budget, enabled); - } else if spec.id == Feature::RolloutBudget { - materialize_resolved_feature_enabled(rollout_budget, enabled); - } else if spec.id == Feature::CurrentTimeReminder { - materialize_resolved_feature_enabled(current_time_reminder, enabled); - } else if spec.id == Feature::NetworkProxy { - materialize_resolved_feature_enabled(network_proxy, enabled); - } else { - entries.insert(spec.key.to_string(), enabled); - } - } - } -} - -fn materialize_resolved_feature_enabled( - feature: &mut Option>, - enabled: bool, -) { - match feature { - Some(feature) => feature.set_enabled(enabled), - None => *feature = Some(FeatureToml::Enabled(enabled)), - } } impl From> for FeaturesToml { @@ -828,20 +769,12 @@ impl FeatureToml { Self::Config(config) => config.enabled(), } } - - pub fn set_enabled(&mut self, enabled: bool) { - match self { - Self::Enabled(value) => *value = enabled, - Self::Config(config) => config.set_enabled(enabled), - } - } } // A trait to be implemented by custom feature config structs when defining a feature that needs more configuration than // just enabled/disabled. pub trait FeatureConfig { fn enabled(&self) -> Option; - fn set_enabled(&mut self, enabled: bool); } /// Single, easy-to-read registry of all feature definitions. diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 7bea00c383..b94eca17d1 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -542,108 +542,6 @@ server_names = ["history", "notes"] ); } -#[test] -fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config() { - let mut features = Features::with_defaults(); - features.enable(Feature::CodeMode); - features.enable(Feature::MultiAgentV2); - features.enable(Feature::NetworkProxy); - features.enable(Feature::NonPrefixedMcpToolNames); - features.enable(Feature::RespectSystemProxy); - - let mut features_toml = FeaturesToml { - tool_registry: Some(crate::ToolRegistryConfigToml { - error_on_tool_collisions: Some(true), - turn_metadata_includes_tool_info: Some(true), - }), - code_mode_host: Some(FeatureToml::Config(crate::CodeModeHostConfigToml { - enabled: Some(false), - disable_in_process_fallback: Some(true), - })), - multi_agent_v2: Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml { - enabled: Some(false), - min_wait_timeout_ms: Some(2500), - subagent_developer_instructions: Some("Delegate carefully.".to_string()), - ..Default::default() - })), - network_proxy: Some(FeatureToml::Config(crate::NetworkProxyConfigToml { - enabled: Some(false), - proxy_url: Some("http://127.0.0.1:43128".to_string()), - ..Default::default() - })), - non_prefixed_mcp_tool_names: Some(FeatureToml::Config( - crate::NonPrefixedMcpToolNamesConfigToml { - enabled: Some(false), - server_names: Some(vec!["history".to_string(), "notes".to_string()]), - }, - )), - entries: BTreeMap::new(), - ..Default::default() - }; - - features_toml.materialize_resolved_enabled(&features); - - assert_eq!( - features_toml.tool_registry, - Some(crate::ToolRegistryConfigToml { - error_on_tool_collisions: Some(true), - turn_metadata_includes_tool_info: Some(true), - }) - ); - let entries = features_toml.entries(); - assert!(!entries.contains_key("tool_registry")); - for spec in crate::FEATURES { - assert_eq!( - entries.get(spec.key), - Some(&features.enabled(spec.id)), - "{}", - spec.key - ); - } - assert_eq!( - features_toml.code_mode_host, - Some(FeatureToml::Config(crate::CodeModeHostConfigToml { - enabled: Some(true), - disable_in_process_fallback: Some(true), - })) - ); - assert_eq!( - features_toml.multi_agent_v2, - Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml { - enabled: Some(true), - min_wait_timeout_ms: Some(2500), - subagent_developer_instructions: Some("Delegate carefully.".to_string()), - ..Default::default() - })) - ); - assert_eq!( - features_toml.network_proxy, - Some(FeatureToml::Config(crate::NetworkProxyConfigToml { - enabled: Some(true), - proxy_url: Some("http://127.0.0.1:43128".to_string()), - ..Default::default() - })) - ); - assert_eq!( - features_toml.non_prefixed_mcp_tool_names, - Some(FeatureToml::Config( - crate::NonPrefixedMcpToolNamesConfigToml { - enabled: Some(true), - server_names: Some(vec!["history".to_string(), "notes".to_string()]), - } - )) - ); - let replayed = Features::from_sources( - FeatureConfigSource { - features: Some(&features_toml), - ..Default::default() - }, - FeatureConfigSource::default(), - FeatureOverrides::default(), - ); - assert_eq!(replayed.enabled(Feature::ApplyPatchFreeform), false); -} - #[test] fn unstable_warning_event_only_mentions_enabled_under_development_features() { let mut configured_features = Table::new(); diff --git a/codex-rs/thread-manager-sample/src/main.rs b/codex-rs/thread-manager-sample/src/main.rs index d36508ce55..8c5ace272b 100644 --- a/codex-rs/thread-manager-sample/src/main.rs +++ b/codex-rs/thread-manager-sample/src/main.rs @@ -260,10 +260,6 @@ fn new_config(model: Option, arg0_paths: Arg0DispatchPaths) -> anyhow::R memories: MemoriesConfig::default(), sqlite: SqliteConfig::from_sqlite_home(codex_home.clone()), log_dir: codex_home.join("log").to_path_buf(), - config_lock_export_dir: None, - config_lock_allow_codex_version_mismatch: false, - config_lock_save_fields_resolved_from_model_catalog: true, - config_lock_toml: None, codex_home, history: History::default(), ephemeral: true,