From 20a2927b9d37974077b0f678711407121960e2d1 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 6 May 2026 15:17:33 -0700 Subject: [PATCH] Allow enabled tables for feature flags --- codex-rs/config/src/schema.rs | 14 +- codex-rs/core/config.schema.json | 350 ++++++++++++----------- codex-rs/core/src/config/config_tests.rs | 31 ++ codex-rs/features/src/lib.rs | 49 +++- codex-rs/features/src/tests.rs | 67 +++-- 5 files changed, 320 insertions(+), 191 deletions(-) diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs index 715822fbfe56..213050485cc5 100644 --- a/codex-rs/config/src/schema.rs +++ b/codex-rs/config/src/schema.rs @@ -43,14 +43,16 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { ); continue; } - validation - .properties - .insert(feature.key.to_string(), schema_gen.subschema_for::()); + validation.properties.insert( + feature.key.to_string(), + schema_gen.subschema_for::(), + ); } for legacy_key in legacy_feature_keys() { - validation - .properties - .insert(legacy_key.to_string(), schema_gen.subschema_for::()); + validation.properties.insert( + legacy_key.to_string(), + schema_gen.subschema_for::(), + ); } validation.additional_properties = Some(Box::new(Schema::Bool(false))); object.object = Some(Box::new(validation)); diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index fe3e48a2dffe..fc4fa5845fcb 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -359,256 +359,256 @@ "description": "Optional feature toggles scoped to this profile.", "properties": { "apply_patch_freeform": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "apply_patch_streaming_events": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "apps_mcp_path_override": { "$ref": "#/definitions/FeatureToml_for_AppsMcpPathOverrideConfigToml" }, "auth_elicitation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "browser_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "browser_use_external": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "builtin_mcp": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "child_agents_md": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "chronicle": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "code_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "code_mode_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "codex_git_commit": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "codex_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "collab": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "collaboration_modes": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "computer_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "connectors": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "default_mode_request_user_input": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "elevated_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_fanout": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_mcp_apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_request_compression": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "exec_permission_approvals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "experimental_use_freeform_apply_patch": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "experimental_use_unified_exec_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "external_migration": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "fast_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "goals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "guardian_approval": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "image_detail_original": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "image_generation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "in_app_browser": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "include_apply_patch_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "js_repl": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "js_repl_tools_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "memories": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "memory_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "multi_agent": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "multi_agent_v2": { "$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml" }, "personality": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "plugin_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "plugins": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "prevent_idle_sleep": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "realtime_conversation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_compaction_v2": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_control": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_models": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_plugin": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "request_permissions": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "request_permissions_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "request_rule": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "responses_websocket_response_processed": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "responses_websockets": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "responses_websockets_v2": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "runtime_metrics": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "search_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "shell_snapshot": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "shell_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "shell_zsh_fork": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "skill_env_var_dependency_prompt": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "skill_mcp_dependency_install": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "sqlite": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "steer": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "telepathy": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "terminal_resize_reflow": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_call_mcp_elicitation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_search_always_defer_mcp_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_suggest": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tui_app_server": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "unavailable_dummy_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "undo": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "unified_exec": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "use_legacy_landlock": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "use_linux_sandbox_bwrap": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "web_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "web_search_cached": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "web_search_request": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "workspace_dependencies": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "workspace_owner_usage_nudge": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" } }, "type": "object" @@ -786,6 +786,28 @@ }, "type": "object" }, + "FeatureToggleConfigToml": { + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "FeatureToggleToml": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/FeatureToggleConfigToml" + } + ] + }, "FeatureToml_for_AppsMcpPathOverrideConfigToml": { "anyOf": [ { @@ -3917,256 +3939,256 @@ "description": "Centralized feature flags (new). Prefer this over individual toggles.", "properties": { "apply_patch_freeform": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "apply_patch_streaming_events": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "apps_mcp_path_override": { "$ref": "#/definitions/FeatureToml_for_AppsMcpPathOverrideConfigToml" }, "auth_elicitation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "browser_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "browser_use_external": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "builtin_mcp": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "child_agents_md": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "chronicle": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "code_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "code_mode_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "codex_git_commit": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "codex_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "collab": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "collaboration_modes": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "computer_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "connectors": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "default_mode_request_user_input": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "elevated_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_fanout": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_mcp_apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "enable_request_compression": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "exec_permission_approvals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "experimental_use_freeform_apply_patch": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "experimental_use_unified_exec_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "external_migration": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "fast_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "goals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "guardian_approval": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "image_detail_original": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "image_generation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "in_app_browser": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "include_apply_patch_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "js_repl": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "js_repl_tools_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "memories": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "memory_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "multi_agent": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "multi_agent_v2": { "$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml" }, "personality": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "plugin_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "plugins": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "prevent_idle_sleep": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "realtime_conversation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_compaction_v2": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_control": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_models": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "remote_plugin": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "request_permissions": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "request_permissions_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "request_rule": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "responses_websocket_response_processed": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "responses_websockets": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "responses_websockets_v2": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "runtime_metrics": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "search_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "shell_snapshot": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "shell_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "shell_zsh_fork": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "skill_env_var_dependency_prompt": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "skill_mcp_dependency_install": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "sqlite": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "steer": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "telepathy": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "terminal_resize_reflow": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_call_mcp_elicitation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_search_always_defer_mcp_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tool_suggest": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "tui_app_server": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "unavailable_dummy_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "undo": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "unified_exec": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "use_legacy_landlock": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "use_linux_sandbox_bwrap": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "web_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "web_search_cached": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "web_search_request": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "workspace_dependencies": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" }, "workspace_owner_usage_nudge": { - "type": "boolean" + "$ref": "#/definitions/FeatureToggleToml" } }, "type": "object" diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 9b785ff3373f..f45a90db7b52 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8822,6 +8822,37 @@ shell_tool = true Ok(()) } +#[tokio::test] +async fn simple_features_can_be_configured_with_enabled_tables() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"profile = "dev" + +[features.code_mode] +enabled = true + +[features.personality] +enabled = false + +[profiles.dev.features.shell_tool] +enabled = 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!(config.features.enabled(Feature::CodeMode)); + assert!(!config.features.enabled(Feature::Personality)); + assert!(!config.features.enabled(Feature::ShellTool)); + + Ok(()) +} + #[tokio::test] async fn approvals_reviewer_defaults_to_manual_only_without_guardian_feature() -> std::io::Result<()> { diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 6c8ce49894de..63a8baba5365 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -13,6 +13,7 @@ use serde::Serialize; use std::collections::BTreeMap; use std::collections::BTreeSet; use toml::Table; +use toml::Value as TomlValue; mod feature_configs; mod legacy; @@ -576,7 +577,7 @@ pub struct FeaturesToml { pub apps_mcp_path_override: Option>, /// Boolean feature toggles keyed by canonical or legacy feature name. #[serde(flatten)] - entries: BTreeMap, + entries: BTreeMap, } impl Features { @@ -588,7 +589,11 @@ impl Features { impl FeaturesToml { pub fn entries(&self) -> BTreeMap { - let mut entries = self.entries.clone(); + let mut entries = self + .entries + .iter() + .map(|(key, feature)| (key.clone(), feature.enabled())) + .collect::>(); if let Some(enabled) = self.multi_agent_v2.as_ref().and_then(FeatureToml::enabled) { entries.insert(Feature::MultiAgentV2.key().to_string(), enabled); } @@ -618,7 +623,7 @@ impl FeaturesToml { } else if spec.id == Feature::AppsMcpPathOverride { materialize_resolved_feature_enabled(apps_mcp_path_override, enabled); } else { - entries.insert(spec.key.to_string(), enabled); + entries.insert(spec.key.to_string(), FeatureToggleToml::Enabled(enabled)); } } } @@ -637,12 +642,38 @@ fn materialize_resolved_feature_enabled( impl From> for FeaturesToml { fn from(entries: BTreeMap) -> Self { Self { - entries, + entries: entries + .into_iter() + .map(|(key, enabled)| (key, FeatureToggleToml::Enabled(enabled))) + .collect(), ..Default::default() } } } +// To be used for features that only need enabled/disabled configuration. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[serde(untagged)] +pub enum FeatureToggleToml { + Enabled(bool), + Config(FeatureToggleConfigToml), +} + +impl FeatureToggleToml { + fn enabled(&self) -> bool { + match self { + Self::Enabled(enabled) => *enabled, + Self::Config(config) => config.enabled, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct FeatureToggleConfigToml { + pub enabled: bool, +} + // To be used for features that need more configuration than just enabled/disabled and // require a custom config struct under `[features]`. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] @@ -1174,7 +1205,15 @@ pub fn unstable_features_warning_event( let mut under_development_feature_keys = Vec::new(); if let Some(table) = effective_features { for (key, value) in table { - if value.as_bool() != Some(true) { + let enabled = match value { + TomlValue::Boolean(enabled) => *enabled, + TomlValue::Table(table) => table + .get("enabled") + .and_then(TomlValue::as_bool) + .unwrap_or(false), + _ => false, + }; + if !enabled { continue; } let Some(spec) = FEATURES.iter().find(|spec| spec.key == key.as_str()) else { diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 5464fa7a61a4..cbef7102b8e5 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -357,17 +357,11 @@ fn apps_require_feature_flag_and_chatgpt_auth() { fn from_sources_applies_base_profile_and_overrides() { let mut base_entries = BTreeMap::new(); base_entries.insert("plugins".to_string(), true); - let base_features = FeaturesToml { - entries: base_entries, - ..Default::default() - }; + let base_features = FeaturesToml::from(base_entries); let mut profile_entries = BTreeMap::new(); profile_entries.insert("code_mode_only".to_string(), true); - let profile_features = FeaturesToml { - entries: profile_entries, - ..Default::default() - }; + let profile_features = FeaturesToml::from(profile_entries); let features = Features::from_sources( FeatureConfigSource { @@ -498,6 +492,38 @@ hide_spawn_agent_metadata = true ); } +#[test] +fn simple_feature_config_deserializes_enabled_table() { + let features: FeaturesToml = toml::from_str( + r#" +code_mode.enabled = true + +[personality] +enabled = false +"#, + ) + .expect("features table should deserialize"); + + assert_eq!( + features.entries(), + BTreeMap::from([ + ("code_mode".to_string(), true), + ("personality".to_string(), false), + ]) + ); + + let resolved = Features::from_sources( + FeatureConfigSource { + features: Some(&features), + ..Default::default() + }, + FeatureConfigSource::default(), + FeatureOverrides::default(), + ); + assert_eq!(resolved.enabled(Feature::CodeMode), true); + assert_eq!(resolved.enabled(Feature::Personality), false); +} + #[test] fn multi_agent_v2_feature_config_usage_hint_enabled_does_not_enable_feature() { let features_toml: FeaturesToml = toml::from_str( @@ -540,15 +566,15 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config( features.enable(Feature::MultiAgentV2); features.disable(Feature::ToolSearch); - let mut features_toml = FeaturesToml { - multi_agent_v2: Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml { - enabled: Some(false), - min_wait_timeout_ms: Some(2500), - ..Default::default() - })), - entries: BTreeMap::from([("include_apply_patch_tool".to_string(), true)]), + let mut features_toml = FeaturesToml::from(BTreeMap::from([( + "include_apply_patch_tool".to_string(), + true, + )])); + features_toml.multi_agent_v2 = Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml { + enabled: Some(false), + min_wait_timeout_ms: Some(2500), ..Default::default() - }; + })); features_toml.materialize_resolved_enabled(&features); @@ -587,9 +613,17 @@ fn unstable_warning_event_only_mentions_enabled_under_development_features() { configured_features.insert("child_agents_md".to_string(), TomlValue::Boolean(true)); configured_features.insert("personality".to_string(), TomlValue::Boolean(true)); configured_features.insert("unknown".to_string(), TomlValue::Boolean(true)); + configured_features.insert( + "code_mode".to_string(), + TomlValue::Table(Table::from_iter([( + "enabled".to_string(), + TomlValue::Boolean(true), + )])), + ); let mut features = Features::with_defaults(); features.enable(Feature::ChildAgentsMd); + features.enable(Feature::CodeMode); let warning = unstable_features_warning_event( Some(&configured_features), @@ -603,6 +637,7 @@ fn unstable_warning_event_only_mentions_enabled_under_development_features() { panic!("expected warning event"); }; assert!(message.contains("child_agents_md")); + assert!(message.contains("code_mode")); assert!(!message.contains("personality")); assert!(message.contains("/tmp/config.toml")); }