diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 940e1e536d..90d6440cf4 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -29,6 +29,7 @@ mod mcp_cmd; use crate::mcp_cmd::McpCli; use codex_core::config::Config; use codex_core::config::ConfigOverrides; +use codex_core::features::is_known_feature_key; /// Codex CLI /// @@ -286,15 +287,25 @@ struct FeatureToggles { } impl FeatureToggles { - fn to_overrides(&self) -> Vec { + fn to_overrides(&self) -> anyhow::Result> { let mut v = Vec::new(); - for k in &self.enable { - v.push(format!("features.{k}=true")); + for feature in &self.enable { + Self::validate_feature(feature)?; + v.push(format!("features.{feature}=true")); } - for k in &self.disable { - v.push(format!("features.{k}=false")); + for feature in &self.disable { + Self::validate_feature(feature)?; + v.push(format!("features.{feature}=false")); + } + Ok(v) + } + + fn validate_feature(feature: &str) -> anyhow::Result<()> { + if is_known_feature_key(feature) { + Ok(()) + } else { + anyhow::bail!("Unknown feature flag: {feature}") } - v } } @@ -345,9 +356,8 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() } = MultitoolCli::parse(); // Fold --enable/--disable into config overrides so they flow to all subcommands. - root_config_overrides - .raw_overrides - .extend(feature_toggles.to_overrides()); + let toggle_overrides = feature_toggles.to_overrides()?; + root_config_overrides.raw_overrides.extend(toggle_overrides); match subcommand { None => { @@ -605,6 +615,7 @@ mod tests { use assert_matches::assert_matches; use codex_core::protocol::TokenUsage; use codex_protocol::ConversationId; + use pretty_assertions::assert_eq; fn finalize_from_args(args: &[&str]) -> TuiCli { let cli = MultitoolCli::try_parse_from(args).expect("parse"); @@ -781,4 +792,32 @@ mod tests { assert!(!interactive.resume_last); assert_eq!(interactive.resume_session_id, None); } + + #[test] + fn feature_toggles_known_features_generate_overrides() { + let toggles = FeatureToggles { + enable: vec!["web_search_request".to_string()], + disable: vec!["unified_exec".to_string()], + }; + let overrides = toggles.to_overrides().expect("valid features"); + assert_eq!( + overrides, + vec![ + "features.web_search_request=true".to_string(), + "features.unified_exec=false".to_string(), + ] + ); + } + + #[test] + fn feature_toggles_unknown_feature_errors() { + let toggles = FeatureToggles { + enable: vec!["does_not_exist".to_string()], + disable: Vec::new(), + }; + let err = toggles + .to_overrides() + .expect_err("feature should be rejected"); + assert_eq!(err.to_string(), "Unknown feature flag: does_not_exist"); + } } diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index c10305b800..269a445203 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -191,6 +191,11 @@ fn feature_for_key(key: &str) -> Option { legacy::feature_for_key(key) } +/// Returns `true` if the provided string matches a known feature toggle key. +pub fn is_known_feature_key(key: &str) -> bool { + feature_for_key(key).is_some() +} + /// Deserializable features table for TOML. #[derive(Deserialize, Debug, Clone, Default, PartialEq)] pub struct FeaturesToml {