diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs index 715822fbfe..8386b1edf7 100644 --- a/codex-rs/config/src/schema.rs +++ b/codex-rs/config/src/schema.rs @@ -45,12 +45,22 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { } validation .properties - .insert(feature.key.to_string(), schema_gen.subschema_for::()); + .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::()); + .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 a30a3ed925..9ce7965375 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -218,18 +218,6 @@ }, "type": "object" }, - "AppsMcpPathOverrideConfigToml": { - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean" - }, - "path": { - "type": "string" - } - }, - "type": "object" - }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ @@ -359,244 +347,244 @@ "description": "Optional feature toggles scoped to this profile.", "properties": { "apply_patch_freeform": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "apply_patch_streaming_events": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "apps_mcp_path_override": { "$ref": "#/definitions/FeatureToml_for_AppsMcpPathOverrideConfigToml" }, "browser_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "browser_use_external": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "child_agents_md": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "chronicle": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "code_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "code_mode_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "codex_git_commit": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "codex_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "collab": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "collaboration_modes": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "computer_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "connectors": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "default_mode_request_user_input": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "elevated_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_fanout": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_mcp_apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_request_compression": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "exec_permission_approvals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "experimental_use_freeform_apply_patch": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "experimental_use_unified_exec_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "external_migration": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "fast_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "goals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "guardian_approval": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "image_detail_original": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "image_generation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "in_app_browser": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "include_apply_patch_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "js_repl": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "js_repl_tools_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "memories": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "memory_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "multi_agent": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "multi_agent_v2": { "$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml" }, "personality": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "plugin_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "plugins": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "prevent_idle_sleep": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "realtime_conversation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "remote_control": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "remote_models": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "remote_plugin": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "request_permissions": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "request_permissions_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "request_rule": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "responses_websockets": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "responses_websockets_v2": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "runtime_metrics": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "search_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "shell_snapshot": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "shell_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "shell_zsh_fork": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "skill_env_var_dependency_prompt": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "skill_mcp_dependency_install": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "sqlite": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "steer": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "telepathy": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "terminal_resize_reflow": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_call_mcp_elicitation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_search_always_defer_mcp_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_suggest": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tui_app_server": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "unavailable_dummy_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "undo": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "unified_exec": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "use_legacy_landlock": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "use_linux_sandbox_bwrap": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "web_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "web_search_cached": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "web_search_request": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "workspace_dependencies": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "workspace_owner_usage_nudge": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" } }, "type": "object" @@ -765,13 +753,75 @@ }, "type": "object" }, + "FeatureConfigTable_for_AppsMcpPathOverrideConfigToml": { + "properties": { + "enabled": { + "type": "boolean" + }, + "hint": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "type": "object" + }, + "FeatureConfigTable_for_MultiAgentV2ConfigToml": { + "properties": { + "enabled": { + "type": "boolean" + }, + "hide_spawn_agent_metadata": { + "type": "boolean" + }, + "hint": { + "type": "string" + }, + "max_concurrent_threads_per_session": { + "format": "uint", + "minimum": 1.0, + "type": "integer" + }, + "min_wait_timeout_ms": { + "format": "int64", + "maximum": 3600000.0, + "minimum": 1.0, + "type": "integer" + }, + "root_agent_usage_hint_text": { + "type": "string" + }, + "subagent_usage_hint_text": { + "type": "string" + }, + "usage_hint_enabled": { + "type": "boolean" + }, + "usage_hint_text": { + "type": "string" + } + }, + "type": "object" + }, + "FeatureConfigTable_for_NoExtraFeatureConfigToml": { + "properties": { + "enabled": { + "type": "boolean" + }, + "hint": { + "type": "string" + } + }, + "type": "object" + }, "FeatureToml_for_AppsMcpPathOverrideConfigToml": { "anyOf": [ { "type": "boolean" }, { - "$ref": "#/definitions/AppsMcpPathOverrideConfigToml" + "$ref": "#/definitions/FeatureConfigTable_for_AppsMcpPathOverrideConfigToml" } ] }, @@ -781,7 +831,17 @@ "type": "boolean" }, { - "$ref": "#/definitions/MultiAgentV2ConfigToml" + "$ref": "#/definitions/FeatureConfigTable_for_MultiAgentV2ConfigToml" + } + ] + }, + "FeatureToml_for_NoExtraFeatureConfigToml": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/FeatureConfigTable_for_NoExtraFeatureConfigToml" } ] }, @@ -1408,41 +1468,6 @@ }, "type": "object" }, - "MultiAgentV2ConfigToml": { - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean" - }, - "hide_spawn_agent_metadata": { - "type": "boolean" - }, - "max_concurrent_threads_per_session": { - "format": "uint", - "minimum": 1.0, - "type": "integer" - }, - "min_wait_timeout_ms": { - "format": "int64", - "maximum": 3600000.0, - "minimum": 1.0, - "type": "integer" - }, - "root_agent_usage_hint_text": { - "type": "string" - }, - "subagent_usage_hint_text": { - "type": "string" - }, - "usage_hint_enabled": { - "type": "boolean" - }, - "usage_hint_text": { - "type": "string" - } - }, - "type": "object" - }, "NetworkDomainPermissionToml": { "enum": [ "allow", @@ -3825,244 +3850,244 @@ "description": "Centralized feature flags (new). Prefer this over individual toggles.", "properties": { "apply_patch_freeform": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "apply_patch_streaming_events": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "apps_mcp_path_override": { "$ref": "#/definitions/FeatureToml_for_AppsMcpPathOverrideConfigToml" }, "browser_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "browser_use_external": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "child_agents_md": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "chronicle": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "code_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "code_mode_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "codex_git_commit": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "codex_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "collab": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "collaboration_modes": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "computer_use": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "connectors": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "default_mode_request_user_input": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "elevated_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_fanout": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_mcp_apps": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "enable_request_compression": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "exec_permission_approvals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "experimental_use_freeform_apply_patch": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "experimental_use_unified_exec_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "experimental_windows_sandbox": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "external_migration": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "fast_mode": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "goals": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "guardian_approval": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "image_detail_original": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "image_generation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "in_app_browser": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "include_apply_patch_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "js_repl": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "js_repl_tools_only": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "memories": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "memory_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "multi_agent": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "multi_agent_v2": { "$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml" }, "personality": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "plugin_hooks": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "plugins": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "prevent_idle_sleep": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "realtime_conversation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "remote_control": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "remote_models": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "remote_plugin": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "request_permissions": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "request_permissions_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "request_rule": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "responses_websockets": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "responses_websockets_v2": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "runtime_metrics": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "search_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "shell_snapshot": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "shell_tool": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "shell_zsh_fork": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "skill_env_var_dependency_prompt": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "skill_mcp_dependency_install": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "sqlite": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "steer": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "telepathy": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "terminal_resize_reflow": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_call_mcp_elicitation": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_search_always_defer_mcp_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tool_suggest": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "tui_app_server": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "unavailable_dummy_tools": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "undo": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "unified_exec": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "use_legacy_landlock": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "use_linux_sandbox_bwrap": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "web_search": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "web_search_cached": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "web_search_request": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "workspace_dependencies": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" }, "workspace_owner_usage_nudge": { - "type": "boolean" + "$ref": "#/definitions/FeatureToml_for_NoExtraFeatureConfigToml" } }, "type": "object" @@ -4497,4 +4522,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 20b2a923f8..eebf4a9901 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -62,8 +62,8 @@ use codex_exec_server::LOCAL_FS; use codex_features::AppsMcpPathOverrideConfigToml; use codex_features::Feature; use codex_features::FeatureConfigSource; +use codex_features::FeatureConfigTable; use codex_features::FeatureOverrides; -use codex_features::FeatureToml; use codex_features::Features; use codex_features::FeaturesToml; use codex_features::MultiAgentV2ConfigToml; @@ -1876,44 +1876,66 @@ fn resolve_web_search_config( fn resolve_multi_agent_v2_config( config_toml: &ConfigToml, config_profile: &ConfigProfile, -) -> MultiAgentV2Config { - let base = multi_agent_v2_toml_config(config_toml.features.as_ref()); - let profile = multi_agent_v2_toml_config(config_profile.features.as_ref()); +) -> std::io::Result { + let base = multi_agent_v2_toml_config(config_toml.features.as_ref())?; + let profile = multi_agent_v2_toml_config(config_profile.features.as_ref())?; let default = MultiAgentV2Config::default(); let max_concurrent_threads_per_session = profile + .as_ref() .and_then(|config| config.max_concurrent_threads_per_session) - .or_else(|| base.and_then(|config| config.max_concurrent_threads_per_session)) + .or_else(|| { + base.as_ref() + .and_then(|config| config.max_concurrent_threads_per_session) + }) .unwrap_or(default.max_concurrent_threads_per_session); let min_wait_timeout_ms = profile + .as_ref() .and_then(|config| config.min_wait_timeout_ms) - .or_else(|| base.and_then(|config| config.min_wait_timeout_ms)) + .or_else(|| base.as_ref().and_then(|config| config.min_wait_timeout_ms)) .unwrap_or(default.min_wait_timeout_ms); let usage_hint_enabled = profile + .as_ref() .and_then(|config| config.usage_hint_enabled) - .or_else(|| base.and_then(|config| config.usage_hint_enabled)) + .or_else(|| base.as_ref().and_then(|config| config.usage_hint_enabled)) .unwrap_or(default.usage_hint_enabled); let usage_hint_text = profile + .as_ref() .and_then(|config| config.usage_hint_text.as_ref()) - .or_else(|| base.and_then(|config| config.usage_hint_text.as_ref())) + .or_else(|| { + base.as_ref() + .and_then(|config| config.usage_hint_text.as_ref()) + }) .cloned() .or(default.usage_hint_text); let root_agent_usage_hint_text = profile + .as_ref() .and_then(|config| config.root_agent_usage_hint_text.as_ref()) - .or_else(|| base.and_then(|config| config.root_agent_usage_hint_text.as_ref())) + .or_else(|| { + base.as_ref() + .and_then(|config| config.root_agent_usage_hint_text.as_ref()) + }) .cloned() .or(default.root_agent_usage_hint_text); let subagent_usage_hint_text = profile + .as_ref() .and_then(|config| config.subagent_usage_hint_text.as_ref()) - .or_else(|| base.and_then(|config| config.subagent_usage_hint_text.as_ref())) + .or_else(|| { + base.as_ref() + .and_then(|config| config.subagent_usage_hint_text.as_ref()) + }) .cloned() .or(default.subagent_usage_hint_text); let hide_spawn_agent_metadata = profile + .as_ref() .and_then(|config| config.hide_spawn_agent_metadata) - .or_else(|| base.and_then(|config| config.hide_spawn_agent_metadata)) + .or_else(|| { + base.as_ref() + .and_then(|config| config.hide_spawn_agent_metadata) + }) .unwrap_or(default.hide_spawn_agent_metadata); - MultiAgentV2Config { + Ok(MultiAgentV2Config { max_concurrent_threads_per_session, min_wait_timeout_ms, usage_hint_enabled, @@ -1921,7 +1943,7 @@ fn resolve_multi_agent_v2_config( root_agent_usage_hint_text, subagent_usage_hint_text, hide_spawn_agent_metadata, - } + }) } fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalResizeReflowConfig { @@ -1938,19 +1960,37 @@ fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalRe } } -fn multi_agent_v2_toml_config(features: Option<&FeaturesToml>) -> Option<&MultiAgentV2ConfigToml> { - match features?.multi_agent_v2.as_ref()? { - FeatureToml::Enabled(_) => None, - FeatureToml::Config(config) => Some(config), - } +fn multi_agent_v2_toml_config( + features: Option<&FeaturesToml>, +) -> std::io::Result> { + typed_feature_config::(features, Feature::MultiAgentV2.key()) + .map(|config| config.map(|config| config.extra)) } fn apps_mcp_path_override_toml_config( features: Option<&FeaturesToml>, -) -> Option<&AppsMcpPathOverrideConfigToml> { - match features?.apps_mcp_path_override.as_ref()? { - FeatureToml::Enabled(_) => None, - FeatureToml::Config(config) => Some(config), +) -> std::io::Result> { + typed_feature_config::( + features, + Feature::AppsMcpPathOverride.key(), + ) + .map(|config| config.map(|config| config.extra)) +} + +fn typed_feature_config( + features: Option<&FeaturesToml>, + key: &'static str, +) -> std::io::Result>> +where + T: serde::de::DeserializeOwned, +{ + match features.and_then(|features| features.typed_config(key)) { + Some(Ok(config)) => Ok(Some(config)), + Some(Err(err)) => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid features.{key} config: {err}"), + )), + None => Ok(None), } } @@ -2476,13 +2516,14 @@ impl Config { let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features) .unwrap_or(WebSearchMode::Cached); let web_search_config = resolve_web_search_config(&cfg, &config_profile); - let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile); + let multi_agent_v2 = resolve_multi_agent_v2_config(&cfg, &config_profile)?; let apps_mcp_path_override = if features.enabled(Feature::AppsMcpPathOverride) { - let base = apps_mcp_path_override_toml_config(cfg.features.as_ref()); - let profile = apps_mcp_path_override_toml_config(config_profile.features.as_ref()); + let base = apps_mcp_path_override_toml_config(cfg.features.as_ref())?; + let profile = apps_mcp_path_override_toml_config(config_profile.features.as_ref())?; profile + .as_ref() .and_then(|config| config.path.as_ref()) - .or_else(|| base.and_then(|config| config.path.as_ref())) + .or_else(|| base.as_ref().and_then(|config| config.path.as_ref())) .cloned() } else { None diff --git a/codex-rs/core/src/session/config_lock.rs b/codex-rs/core/src/session/config_lock.rs index d1f190510a..40da3629de 100644 --- a/codex-rs/core/src/session/config_lock.rs +++ b/codex-rs/core/src/session/config_lock.rs @@ -4,7 +4,6 @@ use codex_config::config_toml::ConfigToml; use codex_config::types::MemoriesToml; use codex_features::AppsMcpPathOverrideConfigToml; use codex_features::Feature; -use codex_features::FeatureToml; use codex_features::FeaturesToml; use codex_features::MultiAgentV2ConfigToml; use codex_protocol::ThreadId; @@ -143,14 +142,20 @@ fn save_config_resolved_fields( .features .get_or_insert_with(FeaturesToml::default); features.materialize_resolved_enabled(config.features.get()); - let mut multi_agent_v2: MultiAgentV2ConfigToml = + let 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)); - features.apps_mcp_path_override = Some(FeatureToml::Config(AppsMcpPathOverrideConfigToml { - enabled: Some(config.features.enabled(Feature::AppsMcpPathOverride)), - path: config.apps_mcp_path_override.clone(), - })); + features.materialize_resolved_config( + Feature::MultiAgentV2, + config.features.enabled(Feature::MultiAgentV2), + multi_agent_v2, + )?; + features.materialize_resolved_config( + Feature::AppsMcpPathOverride, + config.features.enabled(Feature::AppsMcpPathOverride), + AppsMcpPathOverrideConfigToml { + path: config.apps_mcp_path_override.clone(), + }, + )?; lock_config.memories = Some(resolved_config_to_toml::( &config.memories, "memories", @@ -253,20 +258,31 @@ mod tests { } let multi_agent_v2 = features - .multi_agent_v2 - .as_ref() + .get(Feature::MultiAgentV2.key()) .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(_), - usage_hint_enabled: Some(_), - hide_spawn_agent_metadata: Some(_), + FeatureToml::Config(FeatureConfigTable { + common: CommonFeatureConfigToml { + enabled: Some(false), + .. + }, .. }) )); + let multi_agent_v2 = features + .typed_config::(Feature::MultiAgentV2.key()) + .expect("multi_agent_v2 config should parse") + .expect("multi_agent_v2 config should deserialize"); + assert!( + multi_agent_v2 + .extra + .max_concurrent_threads_per_session + .is_some() + ); + assert!(multi_agent_v2.extra.min_wait_timeout_ms.is_some()); + assert!(multi_agent_v2.extra.usage_hint_enabled.is_some()); + assert!(multi_agent_v2.extra.hide_spawn_agent_metadata.is_some()); assert_eq!(lockfile.version, crate::config_lock::CONFIG_LOCK_VERSION); } diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index 4f3eb5b11c..8a822af2f2 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -1,4 +1,3 @@ -use crate::FeatureConfig; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -6,8 +5,6 @@ use serde::Serialize; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] #[serde(deny_unknown_fields)] pub struct MultiAgentV2ConfigToml { - #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 1))] pub max_concurrent_threads_per_session: Option, @@ -26,31 +23,9 @@ pub struct MultiAgentV2ConfigToml { pub hide_spawn_agent_metadata: Option, } -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)] #[serde(deny_unknown_fields)] pub struct AppsMcpPathOverrideConfigToml { - #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, } - -impl FeatureConfig for AppsMcpPathOverrideConfigToml { - fn enabled(&self) -> Option { - self.enabled.or(self.path.as_ref().map(|_| true)) - } - - fn set_enabled(&mut self, enabled: bool) { - self.enabled = Some(enabled); - } -} diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index bf384672a1..a2b5673579 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -3,1173 +3,32 @@ //! This crate defines the feature registry plus the logic used to resolve an //! effective feature set from config-like inputs. -use codex_otel::SessionTelemetry; -use codex_protocol::protocol::Event; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::WarningEvent; -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use toml::Table; - mod feature_configs; mod legacy; +mod machinery; +mod registry; + pub use feature_configs::AppsMcpPathOverrideConfigToml; pub use feature_configs::MultiAgentV2ConfigToml; -use legacy::LegacyFeatureToggles; pub use legacy::legacy_feature_keys; - -/// High-level lifecycle stage for a feature. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Stage { - /// Features that are still under development, not ready for external use - UnderDevelopment, - /// Experimental features made available to users through the `/experimental` menu - Experimental { - name: &'static str, - menu_description: &'static str, - announcement: &'static str, - }, - /// Stable features. The feature flag is kept for ad-hoc enabling/disabling - Stable, - /// Deprecated feature that should not be used anymore. - Deprecated, - /// The feature flag is useless but kept for backward compatibility reason. - Removed, -} - -impl Stage { - pub fn experimental_menu_name(self) -> Option<&'static str> { - match self { - Stage::Experimental { name, .. } => Some(name), - Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None, - } - } - - pub fn experimental_menu_description(self) -> Option<&'static str> { - match self { - Stage::Experimental { - menu_description, .. - } => Some(menu_description), - Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None, - } - } - - pub fn experimental_announcement(self) -> Option<&'static str> { - match self { - Stage::Experimental { - announcement: "", .. - } => None, - Stage::Experimental { announcement, .. } => Some(announcement), - Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None, - } - } -} - -/// Unique features toggled via configuration. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Feature { - // Stable. - /// Removed compatibility flag retained as a no-op so old configs can - /// still parse `undo`. - GhostCommit, - /// Enable the default shell tool. - ShellTool, - /// Enable Claude-style lifecycle hooks loaded from hooks.json files. - CodexHooks, - - // Experimental - /// Removed compatibility flag for the deleted JavaScript REPL feature. - JsRepl, - /// Enable JavaScript code mode backed by the in-process V8 runtime. - CodeMode, - /// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`). - CodeModeOnly, - /// Removed compatibility flag for the deleted JavaScript REPL tool-only mode. - JsReplToolsOnly, - /// Use the single unified PTY-backed exec tool. - UnifiedExec, - /// Route shell tool execution through the zsh exec bridge. - ShellZshFork, - /// Reflow transcript scrollback when the terminal is resized. - TerminalResizeReflow, - /// Include the freeform apply_patch tool. - ApplyPatchFreeform, - /// Stream structured progress while apply_patch input is being generated. - ApplyPatchStreamingEvents, - /// Allow exec tools to request additional permissions while staying sandboxed. - ExecPermissionApprovals, - /// Expose the built-in request_permissions tool. - RequestPermissionsTool, - /// Allow the model to request web searches that fetch live content. - WebSearchRequest, - /// Allow the model to request web searches that fetch cached content. - /// Takes precedence over `WebSearchRequest`. - WebSearchCached, - /// Legacy search-tool feature flag kept for backward compatibility. - SearchTool, - /// Removed legacy Linux bubblewrap opt-in flag retained as a no-op so old - /// wrappers and config can still parse it. - UseLinuxSandboxBwrap, - /// Use the legacy Landlock Linux sandbox fallback instead of the default - /// bubblewrap pipeline. - UseLegacyLandlock, - /// Allow the model to request approval and propose exec rules. - RequestRule, - /// Enable Windows sandbox (restricted token) on Windows. - WindowsSandbox, - /// Use the elevated Windows sandbox pipeline (setup + runner). - WindowsSandboxElevated, - /// Legacy remote models flag kept for backward compatibility. - RemoteModels, - /// Experimental shell snapshotting. - ShellSnapshot, - /// Enable git commit attribution guidance via model instructions. - CodexGitCommit, - /// Enable runtime metrics snapshots via a manual reader. - RuntimeMetrics, - /// Persist rollout metadata to a local SQLite database. - Sqlite, - /// Enable startup memory extraction and file-backed memory consolidation. - MemoryTool, - /// Enable the Chronicle sidecar for passive screen-context memories. - Chronicle, - /// Append additional AGENTS.md guidance to user instructions. - ChildAgentsMd, - /// Compress request bodies (zstd) when sending streaming requests to codex-backend. - EnableRequestCompression, - /// Enable collab tools. - Collab, - /// Enable task-path-based multi-agent routing. - MultiAgentV2, - /// Enable CSV-backed agent job tools. - SpawnCsv, - /// Enable apps. - Apps, - /// Enable MCP apps. - EnableMcpApps, - /// Use the new path for the built-in apps MCP server. - AppsMcpPathOverride, - /// Enable the tool_search tool for apps. - ToolSearch, - /// Always defer MCP tools behind tool_search instead of exposing small sets directly. - ToolSearchAlwaysDeferMcpTools, - /// Expose placeholder tools for unavailable historical tool calls. - UnavailableDummyTools, - /// Enable discoverable tool suggestions for apps. - ToolSuggest, - /// Enable plugins. - Plugins, - /// Enable plugin-bundled lifecycle hooks. - PluginHooks, - /// Allow the in-app browser pane in desktop apps. - /// - /// Requirements-only gate: this should be set from requirements, not user config. - InAppBrowser, - /// Allow Browser Use agent integration in desktop apps. - /// - /// Requirements-only gate: this should be set from requirements, not user config. - BrowserUse, - /// Allow Browser Use integration with external browsers. - /// - /// Requirements-only gate: this should be set from requirements, not user config. - BrowserUseExternal, - /// Allow Codex Computer Use. - /// - /// Requirements-only gate: this should be set from requirements, not user config. - ComputerUse, - /// Temporary internal-only flag for PS-backed remote plugin catalog development. - RemotePlugin, - /// Show the startup prompt for migrating external agent config into Codex. - ExternalMigration, - /// Allow the model to invoke the built-in image generation tool. - ImageGeneration, - /// Allow prompting and installing missing MCP dependencies. - SkillMcpDependencyInstall, - /// Prompt for missing skill env var dependencies. - SkillEnvVarDependencyPrompt, - /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. - /// Kept for config backward compatibility; behavior is always steer-enabled. - Steer, - /// Allow request_user_input in Default collaboration mode. - DefaultModeRequestUserInput, - /// Enable automatic review for approval prompts. - GuardianApproval, - /// Enable persisted thread goals and automatic goal continuation. - Goals, - /// Enable collaboration modes (Plan, Default). - /// Kept for config backward compatibility; behavior is always collaboration-modes-enabled. - CollaborationModes, - /// Route MCP tool approval prompts through the MCP elicitation request path. - ToolCallMcpElicitation, - /// Enable personality selection in the TUI. - Personality, - /// Enable native artifact tools. - Artifact, - /// Enable Fast mode selection in the TUI and request layer. - FastMode, - /// Enable experimental realtime voice conversation mode in the TUI. - RealtimeConversation, - /// Connect app-server to the ChatGPT remote control service. - RemoteControl, - /// Removed compatibility flag retained as a no-op so old wrappers can - /// still pass `--enable image_detail_original`. - ImageDetailOriginal, - /// Removed compatibility flag. The TUI now always uses the app-server implementation. - TuiAppServer, - /// Prevent idle system sleep while a turn is actively running. - PreventIdleSleep, - /// Enable workspace-specific owner nudge copy and prompts in the TUI. - WorkspaceOwnerUsageNudge, - /// Legacy rollout flag for Responses API WebSocket transport experiments. - ResponsesWebsockets, - /// Legacy rollout flag for Responses API WebSocket transport v2 experiments. - ResponsesWebsocketsV2, - /// Enable workspace dependency support. - WorkspaceDependencies, -} - -impl Feature { - pub fn key(self) -> &'static str { - self.info().key - } - - pub fn stage(self) -> Stage { - self.info().stage - } - - pub fn default_enabled(self) -> bool { - self.info().default_enabled - } - - fn info(self) -> &'static FeatureSpec { - FEATURES - .iter() - .find(|spec| spec.id == self) - .unwrap_or_else(|| unreachable!("missing FeatureSpec for {self:?}")) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct LegacyFeatureUsage { - pub alias: String, - pub feature: Feature, - pub summary: String, - pub details: Option, -} - -/// Holds the effective set of enabled features. -#[derive(Debug, Clone, Default, PartialEq)] -pub struct Features { - enabled: BTreeSet, - legacy_usages: BTreeSet, -} - -#[derive(Debug, Clone, Default)] -pub struct FeatureOverrides { - pub include_apply_patch_tool: Option, - pub web_search_request: Option, -} - -#[derive(Debug, Clone, Copy, Default)] -pub struct FeatureConfigSource<'a> { - pub features: Option<&'a FeaturesToml>, - pub include_apply_patch_tool: Option, - pub experimental_use_freeform_apply_patch: Option, - pub experimental_use_unified_exec_tool: Option, -} - -impl FeatureOverrides { - fn apply(self, features: &mut Features) { - LegacyFeatureToggles { - include_apply_patch_tool: self.include_apply_patch_tool, - ..Default::default() - } - .apply(features); - if let Some(enabled) = self.web_search_request { - if enabled { - features.enable(Feature::WebSearchRequest); - } else { - features.disable(Feature::WebSearchRequest); - } - features.record_legacy_usage("web_search_request", Feature::WebSearchRequest); - } - } -} - -impl Features { - /// Starts with built-in defaults. - pub fn with_defaults() -> Self { - let mut set = BTreeSet::new(); - for spec in FEATURES { - if spec.default_enabled { - set.insert(spec.id); - } - } - Self { - enabled: set, - legacy_usages: BTreeSet::new(), - } - } - - pub fn enabled(&self, f: Feature) -> bool { - self.enabled.contains(&f) - } - - pub fn apps_enabled_for_auth(&self, has_chatgpt_auth: bool) -> bool { - self.enabled(Feature::Apps) && has_chatgpt_auth - } - - pub fn use_legacy_landlock(&self) -> bool { - self.enabled(Feature::UseLegacyLandlock) - } - - pub fn enable(&mut self, f: Feature) -> &mut Self { - self.enabled.insert(f); - self - } - - pub fn disable(&mut self, f: Feature) -> &mut Self { - self.enabled.remove(&f); - self - } - - pub fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self { - if enabled { - self.enable(f) - } else { - self.disable(f) - } - } - - pub fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature) { - let (summary, details) = legacy_usage_notice(alias, feature); - self.legacy_usages.insert(LegacyFeatureUsage { - alias: alias.to_string(), - feature, - summary, - details, - }); - } - - pub fn record_legacy_usage(&mut self, alias: &str, feature: Feature) { - if alias == feature.key() { - return; - } - self.record_legacy_usage_force(alias, feature); - } - - pub fn legacy_feature_usages(&self) -> impl Iterator + '_ { - self.legacy_usages.iter() - } - - pub fn emit_metrics(&self, otel: &SessionTelemetry) { - for feature in FEATURES { - if matches!(feature.stage, Stage::Removed) { - continue; - } - if self.enabled(feature.id) != feature.default_enabled { - otel.counter( - "codex.feature.state", - /*inc*/ 1, - &[ - ("feature", feature.key), - ("value", &self.enabled(feature.id).to_string()), - ], - ); - } - } - } - - /// Apply a table of key -> bool toggles (e.g. from TOML). - pub fn apply_map(&mut self, m: &BTreeMap) { - for (k, v) in m { - match k.as_str() { - "web_search_request" => { - self.record_legacy_usage_force( - "features.web_search_request", - Feature::WebSearchRequest, - ); - } - "web_search_cached" => { - self.record_legacy_usage_force( - "features.web_search_cached", - Feature::WebSearchCached, - ); - } - "tui_app_server" => { - continue; - } - "undo" => { - continue; - } - "js_repl" => { - continue; - } - "js_repl_tools_only" => { - continue; - } - "image_detail_original" => { - continue; - } - "use_legacy_landlock" => { - self.record_legacy_usage_force( - "features.use_legacy_landlock", - Feature::UseLegacyLandlock, - ); - } - _ => {} - } - match feature_for_key(k) { - Some(feat) => { - if matches!(feat, Feature::TuiAppServer) { - continue; - } - if k != feat.key() { - self.record_legacy_usage(k.as_str(), feat); - } - if *v { - self.enable(feat); - } else { - self.disable(feat); - } - } - None => { - tracing::warn!("unknown feature key in config: {k}"); - } - } - } - } - - pub fn from_sources( - base: FeatureConfigSource<'_>, - profile: FeatureConfigSource<'_>, - overrides: FeatureOverrides, - ) -> Self { - let mut features = Features::with_defaults(); - - for source in [base, profile] { - LegacyFeatureToggles { - include_apply_patch_tool: source.include_apply_patch_tool, - experimental_use_freeform_apply_patch: source.experimental_use_freeform_apply_patch, - experimental_use_unified_exec_tool: source.experimental_use_unified_exec_tool, - } - .apply(&mut features); - - if let Some(feature_entries) = source.features { - features.apply_toml(feature_entries); - } - } - - overrides.apply(&mut features); - features.normalize_dependencies(); - - features - } - - pub fn enabled_features(&self) -> Vec { - self.enabled.iter().copied().collect() - } - - pub fn normalize_dependencies(&mut self) { - if self.enabled(Feature::SpawnCsv) && !self.enabled(Feature::Collab) { - self.enable(Feature::Collab); - } - if self.enabled(Feature::CodeModeOnly) && !self.enabled(Feature::CodeMode) { - self.enable(Feature::CodeMode); - } - } -} - -fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option) { - let canonical = feature.key(); - match feature { - Feature::WebSearchRequest | Feature::WebSearchCached => { - let label = match alias { - "web_search" => "[features].web_search", - "features.web_search_request" | "web_search_request" => { - "[features].web_search_request" - } - "features.web_search_cached" | "web_search_cached" => { - "[features].web_search_cached" - } - _ => alias, - }; - let summary = - format!("`{label}` is deprecated because web search is enabled by default."); - (summary, Some(web_search_details().to_string())) - } - Feature::UseLegacyLandlock => { - let label = match alias { - "features.use_legacy_landlock" | "use_legacy_landlock" => { - "[features].use_legacy_landlock" - } - _ => alias, - }; - let summary = format!("`{label}` is deprecated and will be removed soon."); - let details = - "Remove this setting to stop opting into the legacy Linux sandbox behavior." - .to_string(); - (summary, Some(details)) - } - _ => { - let label = if alias.contains('.') || alias.starts_with('[') { - alias.to_string() - } else { - format!("[features].{alias}") - }; - let summary = format!("`{label}` is deprecated. Use `[features].{canonical}` instead."); - let details = if alias == canonical { - None - } else { - Some(format!( - "Enable it with `--enable {canonical}` or `[features].{canonical}` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details." - )) - }; - (summary, details) - } - } -} - -fn web_search_details() -> &'static str { - "Set `web_search` to `\"live\"`, `\"cached\"`, or `\"disabled\"` at the top level (or under a profile) in config.toml if you want to override it." -} - -/// Keys accepted in `[features]` tables. -pub fn feature_for_key(key: &str) -> Option { - for spec in FEATURES { - if spec.key == key { - return Some(spec.id); - } - } - legacy::feature_for_key(key) -} - -pub fn canonical_feature_for_key(key: &str) -> Option { - FEATURES - .iter() - .find(|spec| spec.key == key) - .map(|spec| spec.id) -} - -/// 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(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)] -pub struct FeaturesToml { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub multi_agent_v2: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub apps_mcp_path_override: Option>, - /// Boolean feature toggles keyed by canonical or legacy feature name. - #[serde(flatten)] - entries: BTreeMap, -} - -impl Features { - fn apply_toml(&mut self, features: &FeaturesToml) { - let entries = features.entries(); - self.apply_map(&entries); - } -} - -impl FeaturesToml { - pub fn entries(&self) -> BTreeMap { - let mut entries = self.entries.clone(); - if let Some(enabled) = self.multi_agent_v2.as_ref().and_then(FeatureToml::enabled) { - entries.insert(Feature::MultiAgentV2.key().to_string(), enabled); - } - if let Some(enabled) = self - .apps_mcp_path_override - .as_ref() - .and_then(FeatureToml::enabled) - { - entries.insert(Feature::AppsMcpPathOverride.key().to_string(), enabled); - } - entries - } - - pub fn materialize_resolved_enabled(&mut self, features: &Features) { - let Self { - multi_agent_v2, - apps_mcp_path_override, - 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::MultiAgentV2 { - materialize_resolved_feature_enabled(multi_agent_v2, enabled); - } else if spec.id == Feature::AppsMcpPathOverride { - materialize_resolved_feature_enabled(apps_mcp_path_override, 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 { - fn from(entries: BTreeMap) -> Self { - Self { - entries, - ..Default::default() - } - } -} - -// 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)] -#[serde(untagged)] -pub enum FeatureToml { - Enabled(bool), - Config(T), -} - -impl FeatureToml { - pub fn enabled(&self) -> Option { - match self { - Self::Enabled(enabled) => Some(*enabled), - 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. -#[derive(Debug, Clone, Copy)] -pub struct FeatureSpec { - pub id: Feature, - pub key: &'static str, - pub stage: Stage, - pub default_enabled: bool, -} - -pub const FEATURES: &[FeatureSpec] = &[ - // Stable features. - FeatureSpec { - id: Feature::GhostCommit, - key: "undo", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ShellTool, - key: "shell_tool", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::UnifiedExec, - key: "unified_exec", - stage: Stage::Stable, - default_enabled: !cfg!(windows), - }, - FeatureSpec { - id: Feature::ShellZshFork, - key: "shell_zsh_fork", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ShellSnapshot, - key: "shell_snapshot", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::JsRepl, - key: "js_repl", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::CodeMode, - key: "code_mode", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::CodeModeOnly, - key: "code_mode_only", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::JsReplToolsOnly, - key: "js_repl_tools_only", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::TerminalResizeReflow, - key: "terminal_resize_reflow", - stage: Stage::Experimental { - name: "Terminal resize reflow", - menu_description: "Rebuild Codex-owned transcript scrollback when the terminal width changes.", - announcement: "", - }, - default_enabled: true, - }, - FeatureSpec { - id: Feature::WebSearchRequest, - key: "web_search_request", - stage: Stage::Deprecated, - default_enabled: false, - }, - FeatureSpec { - id: Feature::WebSearchCached, - key: "web_search_cached", - stage: Stage::Deprecated, - default_enabled: false, - }, - FeatureSpec { - id: Feature::SearchTool, - key: "search_tool", - stage: Stage::Removed, - default_enabled: false, - }, - // Experimental program. Rendered in the `/experimental` menu for users. - FeatureSpec { - id: Feature::CodexGitCommit, - key: "codex_git_commit", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::RuntimeMetrics, - key: "runtime_metrics", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::Sqlite, - key: "sqlite", - stage: Stage::Removed, - default_enabled: true, - }, - FeatureSpec { - id: Feature::MemoryTool, - key: "memories", - stage: Stage::Experimental { - name: "Memories", - menu_description: "Allow Codex to create new memories from conversations and bring relevant memories into new conversations.", - announcement: "NEW: Codex can now generate and uses memories. Try is now with `/memories`", - }, - default_enabled: false, - }, - FeatureSpec { - id: Feature::Chronicle, - key: "chronicle", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ChildAgentsMd, - key: "child_agents_md", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ApplyPatchFreeform, - key: "apply_patch_freeform", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ApplyPatchStreamingEvents, - key: "apply_patch_streaming_events", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ExecPermissionApprovals, - key: "exec_permission_approvals", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::CodexHooks, - key: "hooks", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::RequestPermissionsTool, - key: "request_permissions_tool", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::UseLinuxSandboxBwrap, - key: "use_linux_sandbox_bwrap", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::UseLegacyLandlock, - key: "use_legacy_landlock", - stage: Stage::Deprecated, - default_enabled: false, - }, - FeatureSpec { - id: Feature::RequestRule, - key: "request_rule", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::WindowsSandbox, - key: "experimental_windows_sandbox", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::WindowsSandboxElevated, - key: "elevated_windows_sandbox", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::RemoteModels, - key: "remote_models", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::EnableRequestCompression, - key: "enable_request_compression", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::Collab, - key: "multi_agent", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::MultiAgentV2, - key: "multi_agent_v2", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::SpawnCsv, - key: "enable_fanout", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::Apps, - key: "apps", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::EnableMcpApps, - key: "enable_mcp_apps", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::AppsMcpPathOverride, - key: "apps_mcp_path_override", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ToolSearch, - key: "tool_search", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::ToolSearchAlwaysDeferMcpTools, - key: "tool_search_always_defer_mcp_tools", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::UnavailableDummyTools, - key: "unavailable_dummy_tools", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::ToolSuggest, - key: "tool_suggest", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::Plugins, - key: "plugins", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::PluginHooks, - key: "plugin_hooks", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::InAppBrowser, - key: "in_app_browser", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::BrowserUse, - key: "browser_use", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::BrowserUseExternal, - key: "browser_use_external", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::ComputerUse, - key: "computer_use", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::RemotePlugin, - key: "remote_plugin", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ExternalMigration, - key: "external_migration", - stage: Stage::Experimental { - name: "External migration", - menu_description: "Show a startup prompt when Codex detects migratable external agent config for this machine or project.", - announcement: "", - }, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ImageGeneration, - key: "image_generation", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::SkillMcpDependencyInstall, - key: "skill_mcp_dependency_install", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::SkillEnvVarDependencyPrompt, - key: "skill_env_var_dependency_prompt", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::Steer, - key: "steer", - stage: Stage::Removed, - default_enabled: true, - }, - FeatureSpec { - id: Feature::DefaultModeRequestUserInput, - key: "default_mode_request_user_input", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::GuardianApproval, - key: "guardian_approval", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::Goals, - key: "goals", - stage: Stage::Experimental { - name: "Goals", - menu_description: "Set a persistent goal Codex can continue over time", - announcement: "", - }, - default_enabled: false, - }, - FeatureSpec { - id: Feature::CollaborationModes, - key: "collaboration_modes", - stage: Stage::Removed, - default_enabled: true, - }, - FeatureSpec { - id: Feature::ToolCallMcpElicitation, - key: "tool_call_mcp_elicitation", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::Personality, - key: "personality", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::Artifact, - key: "artifact", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::FastMode, - key: "fast_mode", - stage: Stage::Stable, - default_enabled: true, - }, - FeatureSpec { - id: Feature::RealtimeConversation, - key: "realtime_conversation", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::RemoteControl, - key: "remote_control", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ImageDetailOriginal, - key: "image_detail_original", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::TuiAppServer, - key: "tui_app_server", - stage: Stage::Removed, - default_enabled: true, - }, - FeatureSpec { - id: Feature::PreventIdleSleep, - key: "prevent_idle_sleep", - stage: if cfg!(any( - target_os = "macos", - target_os = "linux", - target_os = "windows" - )) { - Stage::Experimental { - name: "Prevent sleep while running", - menu_description: "Keep your computer awake while Codex is running a thread.", - announcement: "NEW: Prevent sleep while running is now available in /experimental.", - } - } else { - Stage::UnderDevelopment - }, - default_enabled: false, - }, - FeatureSpec { - id: Feature::WorkspaceOwnerUsageNudge, - key: "workspace_owner_usage_nudge", - stage: Stage::UnderDevelopment, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ResponsesWebsockets, - key: "responses_websockets", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::ResponsesWebsocketsV2, - key: "responses_websockets_v2", - stage: Stage::Removed, - default_enabled: false, - }, - FeatureSpec { - id: Feature::WorkspaceDependencies, - key: "workspace_dependencies", - stage: Stage::Stable, - default_enabled: true, - }, -]; - -pub fn unstable_features_warning_event( - effective_features: Option<&Table>, - suppress_unstable_features_warning: bool, - features: &Features, - config_path: &str, -) -> Option { - if suppress_unstable_features_warning { - return None; - } - - 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) { - continue; - } - let Some(spec) = FEATURES.iter().find(|spec| spec.key == key.as_str()) else { - continue; - }; - if !features.enabled(spec.id) { - continue; - } - if matches!(spec.stage, Stage::UnderDevelopment) { - under_development_feature_keys.push(spec.key.to_string()); - } - } - } - - if under_development_feature_keys.is_empty() { - return None; - } - - let under_development_feature_keys = under_development_feature_keys.join(", "); - let message = format!( - "Under-development features enabled: {under_development_feature_keys}. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in {config_path}." - ); - Some(Event { - id: String::new(), - msg: EventMsg::Warning(WarningEvent { message }), - }) -} +pub use machinery::CommonFeatureConfigToml; +pub use machinery::Feature; +pub use machinery::FeatureConfigSource; +pub use machinery::FeatureConfigTable; +pub use machinery::FeatureOverrides; +pub use machinery::FeatureToml; +pub use machinery::Features; +pub use machinery::FeaturesToml; +pub use machinery::LegacyFeatureUsage; +pub use machinery::NoExtraFeatureConfigToml; +pub use machinery::RawFeatureConfigExtras; +pub use machinery::Stage; +pub use machinery::canonical_feature_for_key; +pub use machinery::feature_for_key; +pub use machinery::is_known_feature_key; +pub use machinery::unstable_features_warning_event; +pub use registry::FEATURES; +pub use registry::FeatureSpec; #[cfg(test)] mod tests; diff --git a/codex-rs/features/src/machinery.rs b/codex-rs/features/src/machinery.rs new file mode 100644 index 0000000000..15140ecb15 --- /dev/null +++ b/codex-rs/features/src/machinery.rs @@ -0,0 +1,807 @@ +use crate::legacy::LegacyFeatureToggles; +use crate::registry::FEATURES; +use crate::registry::FeatureSpec; +use codex_otel::SessionTelemetry; +use codex_protocol::protocol::Event; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::WarningEvent; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde::de::DeserializeOwned; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use toml::Table; +use toml::Value as TomlValue; + +/// High-level lifecycle stage for a feature. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Stage { + /// Features that are still under development, not ready for external use + UnderDevelopment, + /// Experimental features made available to users through the `/experimental` menu + Experimental { + name: &'static str, + menu_description: &'static str, + announcement: &'static str, + }, + /// Stable features. The feature flag is kept for ad-hoc enabling/disabling + Stable, + /// Deprecated feature that should not be used anymore. + Deprecated, + /// The feature flag is useless but kept for backward compatibility reason. + Removed, +} + +impl Stage { + pub fn experimental_menu_name(self) -> Option<&'static str> { + match self { + Stage::Experimental { name, .. } => Some(name), + Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None, + } + } + + pub fn experimental_menu_description(self) -> Option<&'static str> { + match self { + Stage::Experimental { + menu_description, .. + } => Some(menu_description), + Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None, + } + } + + pub fn experimental_announcement(self) -> Option<&'static str> { + match self { + Stage::Experimental { + announcement: "", .. + } => None, + Stage::Experimental { announcement, .. } => Some(announcement), + Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None, + } + } +} + +/// Unique features toggled via configuration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Feature { + // Stable. + /// Removed compatibility flag retained as a no-op so old configs can + /// still parse `undo`. + GhostCommit, + /// Enable the default shell tool. + ShellTool, + /// Enable Claude-style lifecycle hooks loaded from hooks.json files. + CodexHooks, + + // Experimental + /// Removed compatibility flag for the deleted JavaScript REPL feature. + JsRepl, + /// Enable JavaScript code mode backed by the in-process V8 runtime. + CodeMode, + /// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`). + CodeModeOnly, + /// Removed compatibility flag for the deleted JavaScript REPL tool-only mode. + JsReplToolsOnly, + /// Use the single unified PTY-backed exec tool. + UnifiedExec, + /// Route shell tool execution through the zsh exec bridge. + ShellZshFork, + /// Reflow transcript scrollback when the terminal is resized. + TerminalResizeReflow, + /// Include the freeform apply_patch tool. + ApplyPatchFreeform, + /// Stream structured progress while apply_patch input is being generated. + ApplyPatchStreamingEvents, + /// Allow exec tools to request additional permissions while staying sandboxed. + ExecPermissionApprovals, + /// Expose the built-in request_permissions tool. + RequestPermissionsTool, + /// Allow the model to request web searches that fetch live content. + WebSearchRequest, + /// Allow the model to request web searches that fetch cached content. + /// Takes precedence over `WebSearchRequest`. + WebSearchCached, + /// Legacy search-tool feature flag kept for backward compatibility. + SearchTool, + /// Removed legacy Linux bubblewrap opt-in flag retained as a no-op so old + /// wrappers and config can still parse it. + UseLinuxSandboxBwrap, + /// Use the legacy Landlock Linux sandbox fallback instead of the default + /// bubblewrap pipeline. + UseLegacyLandlock, + /// Allow the model to request approval and propose exec rules. + RequestRule, + /// Enable Windows sandbox (restricted token) on Windows. + WindowsSandbox, + /// Use the elevated Windows sandbox pipeline (setup + runner). + WindowsSandboxElevated, + /// Legacy remote models flag kept for backward compatibility. + RemoteModels, + /// Experimental shell snapshotting. + ShellSnapshot, + /// Enable git commit attribution guidance via model instructions. + CodexGitCommit, + /// Enable runtime metrics snapshots via a manual reader. + RuntimeMetrics, + /// Persist rollout metadata to a local SQLite database. + Sqlite, + /// Enable startup memory extraction and file-backed memory consolidation. + MemoryTool, + /// Enable the Chronicle sidecar for passive screen-context memories. + Chronicle, + /// Append additional AGENTS.md guidance to user instructions. + ChildAgentsMd, + /// Compress request bodies (zstd) when sending streaming requests to codex-backend. + EnableRequestCompression, + /// Enable collab tools. + Collab, + /// Enable task-path-based multi-agent routing. + MultiAgentV2, + /// Enable CSV-backed agent job tools. + SpawnCsv, + /// Enable apps. + Apps, + /// Enable MCP apps. + EnableMcpApps, + /// Use the new path for the built-in apps MCP server. + AppsMcpPathOverride, + /// Enable the tool_search tool for apps. + ToolSearch, + /// Always defer MCP tools behind tool_search instead of exposing small sets directly. + ToolSearchAlwaysDeferMcpTools, + /// Expose placeholder tools for unavailable historical tool calls. + UnavailableDummyTools, + /// Enable discoverable tool suggestions for apps. + ToolSuggest, + /// Enable plugins. + Plugins, + /// Enable plugin-bundled lifecycle hooks. + PluginHooks, + /// Allow the in-app browser pane in desktop apps. + /// + /// Requirements-only gate: this should be set from requirements, not user config. + InAppBrowser, + /// Allow Browser Use agent integration in desktop apps. + /// + /// Requirements-only gate: this should be set from requirements, not user config. + BrowserUse, + /// Allow Browser Use integration with external browsers. + /// + /// Requirements-only gate: this should be set from requirements, not user config. + BrowserUseExternal, + /// Allow Codex Computer Use. + /// + /// Requirements-only gate: this should be set from requirements, not user config. + ComputerUse, + /// Temporary internal-only flag for PS-backed remote plugin catalog development. + RemotePlugin, + /// Show the startup prompt for migrating external agent config into Codex. + ExternalMigration, + /// Allow the model to invoke the built-in image generation tool. + ImageGeneration, + /// Allow prompting and installing missing MCP dependencies. + SkillMcpDependencyInstall, + /// Prompt for missing skill env var dependencies. + SkillEnvVarDependencyPrompt, + /// Steer feature flag - when enabled, Enter submits immediately instead of queuing. + /// Kept for config backward compatibility; behavior is always steer-enabled. + Steer, + /// Allow request_user_input in Default collaboration mode. + DefaultModeRequestUserInput, + /// Enable automatic review for approval prompts. + GuardianApproval, + /// Enable persisted thread goals and automatic goal continuation. + Goals, + /// Enable collaboration modes (Plan, Default). + /// Kept for config backward compatibility; behavior is always collaboration-modes-enabled. + CollaborationModes, + /// Route MCP tool approval prompts through the MCP elicitation request path. + ToolCallMcpElicitation, + /// Enable personality selection in the TUI. + Personality, + /// Enable native artifact tools. + Artifact, + /// Enable Fast mode selection in the TUI and request layer. + FastMode, + /// Enable experimental realtime voice conversation mode in the TUI. + RealtimeConversation, + /// Connect app-server to the ChatGPT remote control service. + RemoteControl, + /// Removed compatibility flag retained as a no-op so old wrappers can + /// still pass `--enable image_detail_original`. + ImageDetailOriginal, + /// Removed compatibility flag. The TUI now always uses the app-server implementation. + TuiAppServer, + /// Prevent idle system sleep while a turn is actively running. + PreventIdleSleep, + /// Enable workspace-specific owner nudge copy and prompts in the TUI. + WorkspaceOwnerUsageNudge, + /// Legacy rollout flag for Responses API WebSocket transport experiments. + ResponsesWebsockets, + /// Legacy rollout flag for Responses API WebSocket transport v2 experiments. + ResponsesWebsocketsV2, + /// Enable workspace dependency support. + WorkspaceDependencies, +} + +impl Feature { + pub fn key(self) -> &'static str { + self.info().key + } + + pub fn stage(self) -> Stage { + self.info().stage + } + + pub fn default_enabled(self) -> bool { + self.info().default_enabled + } + + pub(crate) fn info(self) -> &'static FeatureSpec { + FEATURES + .iter() + .find(|spec| spec.id == self) + .unwrap_or_else(|| unreachable!("missing FeatureSpec for {self:?}")) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct LegacyFeatureUsage { + pub alias: String, + pub feature: Feature, + pub summary: String, + pub details: Option, +} + +/// Holds the effective set of enabled features. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Features { + enabled: BTreeSet, + legacy_usages: BTreeSet, +} + +#[derive(Debug, Clone, Default)] +pub struct FeatureOverrides { + pub include_apply_patch_tool: Option, + pub web_search_request: Option, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct FeatureConfigSource<'a> { + pub features: Option<&'a FeaturesToml>, + pub include_apply_patch_tool: Option, + pub experimental_use_freeform_apply_patch: Option, + pub experimental_use_unified_exec_tool: Option, +} + +impl FeatureOverrides { + fn apply(self, features: &mut Features) { + LegacyFeatureToggles { + include_apply_patch_tool: self.include_apply_patch_tool, + ..Default::default() + } + .apply(features); + if let Some(enabled) = self.web_search_request { + if enabled { + features.enable(Feature::WebSearchRequest); + } else { + features.disable(Feature::WebSearchRequest); + } + features.record_legacy_usage("web_search_request", Feature::WebSearchRequest); + } + } +} + +impl Features { + /// Starts with built-in defaults. + pub fn with_defaults() -> Self { + let mut set = BTreeSet::new(); + for spec in FEATURES { + if spec.default_enabled { + set.insert(spec.id); + } + } + Self { + enabled: set, + legacy_usages: BTreeSet::new(), + } + } + + pub fn enabled(&self, f: Feature) -> bool { + self.enabled.contains(&f) + } + + pub fn apps_enabled_for_auth(&self, has_chatgpt_auth: bool) -> bool { + self.enabled(Feature::Apps) && has_chatgpt_auth + } + + pub fn use_legacy_landlock(&self) -> bool { + self.enabled(Feature::UseLegacyLandlock) + } + + pub fn enable(&mut self, f: Feature) -> &mut Self { + self.enabled.insert(f); + self + } + + pub fn disable(&mut self, f: Feature) -> &mut Self { + self.enabled.remove(&f); + self + } + + pub fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self { + if enabled { + self.enable(f) + } else { + self.disable(f) + } + } + + pub fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature) { + let (summary, details) = legacy_usage_notice(alias, feature); + self.legacy_usages.insert(LegacyFeatureUsage { + alias: alias.to_string(), + feature, + summary, + details, + }); + } + + pub fn record_legacy_usage(&mut self, alias: &str, feature: Feature) { + if alias == feature.key() { + return; + } + self.record_legacy_usage_force(alias, feature); + } + + pub fn legacy_feature_usages(&self) -> impl Iterator + '_ { + self.legacy_usages.iter() + } + + pub fn emit_metrics(&self, otel: &SessionTelemetry) { + for feature in FEATURES { + if matches!(feature.stage, Stage::Removed) { + continue; + } + if self.enabled(feature.id) != feature.default_enabled { + otel.counter( + "codex.feature.state", + /*inc*/ 1, + &[ + ("feature", feature.key), + ("value", &self.enabled(feature.id).to_string()), + ], + ); + } + } + } + + /// Apply a table of key -> bool toggles (e.g. from TOML). + pub fn apply_map(&mut self, entries: &BTreeMap) { + for (key, enabled) in entries { + match key.as_str() { + "web_search_request" => { + self.record_legacy_usage_force( + "features.web_search_request", + Feature::WebSearchRequest, + ); + } + "web_search_cached" => { + self.record_legacy_usage_force( + "features.web_search_cached", + Feature::WebSearchCached, + ); + } + "tui_app_server" + | "undo" + | "js_repl" + | "js_repl_tools_only" + | "image_detail_original" => { + continue; + } + "use_legacy_landlock" => { + self.record_legacy_usage_force( + "features.use_legacy_landlock", + Feature::UseLegacyLandlock, + ); + } + _ => {} + } + match feature_for_key(key) { + Some(feature) => { + if matches!(feature, Feature::TuiAppServer) { + continue; + } + if key != feature.key() { + self.record_legacy_usage(key.as_str(), feature); + } + self.set_enabled(feature, *enabled); + } + None => { + tracing::warn!("unknown feature key in config: {key}"); + } + } + } + } + + pub fn from_sources( + base: FeatureConfigSource<'_>, + profile: FeatureConfigSource<'_>, + overrides: FeatureOverrides, + ) -> Self { + let mut features = Features::with_defaults(); + + for source in [base, profile] { + LegacyFeatureToggles { + include_apply_patch_tool: source.include_apply_patch_tool, + experimental_use_freeform_apply_patch: source.experimental_use_freeform_apply_patch, + experimental_use_unified_exec_tool: source.experimental_use_unified_exec_tool, + } + .apply(&mut features); + + if let Some(feature_entries) = source.features { + features.apply_toml(feature_entries); + } + } + + overrides.apply(&mut features); + features.normalize_dependencies(); + + features + } + + pub fn enabled_features(&self) -> Vec { + self.enabled.iter().copied().collect() + } + + pub fn normalize_dependencies(&mut self) { + if self.enabled(Feature::SpawnCsv) && !self.enabled(Feature::Collab) { + self.enable(Feature::Collab); + } + if self.enabled(Feature::CodeModeOnly) && !self.enabled(Feature::CodeMode) { + self.enable(Feature::CodeMode); + } + } + + fn apply_toml(&mut self, features: &FeaturesToml) { + let entries = features.entries(); + self.apply_map(&entries); + } +} + +fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option) { + let canonical = feature.key(); + match feature { + Feature::WebSearchRequest | Feature::WebSearchCached => { + let label = match alias { + "web_search" => "[features].web_search", + "features.web_search_request" | "web_search_request" => { + "[features].web_search_request" + } + "features.web_search_cached" | "web_search_cached" => { + "[features].web_search_cached" + } + _ => alias, + }; + let summary = + format!("`{label}` is deprecated because web search is enabled by default."); + (summary, Some(web_search_details().to_string())) + } + Feature::UseLegacyLandlock => { + let label = match alias { + "features.use_legacy_landlock" | "use_legacy_landlock" => { + "[features].use_legacy_landlock" + } + _ => alias, + }; + let summary = format!("`{label}` is deprecated and will be removed soon."); + let details = + "Remove this setting to stop opting into the legacy Linux sandbox behavior." + .to_string(); + (summary, Some(details)) + } + _ => { + let label = if alias.contains('.') || alias.starts_with('[') { + alias.to_string() + } else { + format!("[features].{alias}") + }; + let summary = format!("`{label}` is deprecated. Use `[features].{canonical}` instead."); + let details = if alias == canonical { + None + } else { + Some(format!( + "Enable it with `--enable {canonical}` or `[features].{canonical}` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details." + )) + }; + (summary, details) + } + } +} + +fn web_search_details() -> &'static str { + "Set `web_search` to `\"live\"`, `\"cached\"`, or `\"disabled\"` at the top level (or under a profile) in config.toml if you want to override it." +} + +/// Keys accepted in `[features]` tables. +pub fn feature_for_key(key: &str) -> Option { + for spec in FEATURES { + if spec.key == key { + return Some(spec.id); + } + } + crate::legacy::feature_for_key(key) +} + +pub fn canonical_feature_for_key(key: &str) -> Option { + FEATURES + .iter() + .find(|spec| spec.key == key) + .map(|spec| spec.id) +} + +/// 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(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +pub struct FeaturesToml { + #[serde(flatten)] + entries: BTreeMap, +} + +impl FeaturesToml { + #[cfg(test)] + pub(crate) fn from_entries(entries: BTreeMap) -> Self { + Self { entries } + } + + pub fn entries(&self) -> BTreeMap { + self.entries + .iter() + .filter_map(|(key, feature)| { + feature_enabled_in_config(key, feature).map(|enabled| (key.clone(), enabled)) + }) + .collect() + } + + pub fn get(&self, key: &str) -> Option<&FeatureToml> { + self.entries.get(key) + } + + pub fn typed_config( + &self, + key: &str, + ) -> Option, toml::de::Error>> + where + T: DeserializeOwned, + { + self.get(key).and_then(FeatureToml::typed_config::) + } + + pub fn hint(&self, key: &str) -> Option<&str> { + self.get(key).and_then(FeatureToml::hint) + } + + pub fn insert(&mut self, key: String, feature: FeatureToml) { + self.entries.insert(key, feature); + } + + pub fn materialize_resolved_enabled(&mut self, features: &Features) { + for key in crate::legacy::legacy_feature_keys() { + self.entries.remove(key); + } + for spec in FEATURES { + let enabled = features.enabled(spec.id); + materialize_resolved_feature_enabled(&mut self.entries, spec.key, enabled); + } + } + + pub fn materialize_resolved_config( + &mut self, + feature: Feature, + enabled: bool, + extra: T, + ) -> Result<(), toml::ser::Error> + where + T: Serialize, + { + let key = feature.key().to_string(); + let hint = self.hint(&key).map(ToOwned::to_owned); + let feature = FeatureToml::Config( + FeatureConfigTable { + common: CommonFeatureConfigToml { + enabled: Some(enabled), + hint, + }, + extra, + } + .into_raw()?, + ); + self.insert(key, feature); + Ok(()) + } +} + +fn materialize_resolved_feature_enabled( + features: &mut BTreeMap, + key: &str, + enabled: bool, +) { + match features.get_mut(key) { + Some(feature) => feature.set_enabled(enabled), + None => { + features.insert(key.to_string(), FeatureToml::Enabled(enabled)); + } + } +} + +fn feature_enabled_in_config(key: &str, feature: &FeatureToml) -> Option { + match feature { + FeatureToml::Enabled(enabled) => Some(*enabled), + FeatureToml::Config(config) => config.common.enabled.or_else(|| { + if key == Feature::AppsMcpPathOverride.key() { + config.extra.contains_key("path").then_some(true) + } else { + None + } + }), + } +} + +impl From> for FeaturesToml { + fn from(entries: BTreeMap) -> Self { + Self { + entries: entries + .into_iter() + .map(|(key, enabled)| (key, FeatureToml::Enabled(enabled))) + .collect(), + } + } +} + +pub type RawFeatureConfigExtras = BTreeMap; + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CommonFeatureConfigToml { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hint: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct NoExtraFeatureConfigToml {} + +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] +pub struct FeatureConfigTable { + #[serde(flatten)] + pub common: CommonFeatureConfigToml, + #[serde(flatten)] + pub extra: T, +} + +// To be used for feature entries under `[features]` that can be either a bare +// boolean toggle or a table with shared fields plus feature-specific extras. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[serde(untagged)] +pub enum FeatureToml { + Enabled(bool), + Config(FeatureConfigTable), +} + +impl FeatureToml { + pub fn enabled(&self) -> Option { + match self { + Self::Enabled(enabled) => Some(*enabled), + Self::Config(config) => config.common.enabled, + } + } + + pub fn hint(&self) -> Option<&str> { + match self { + Self::Enabled(_) => None, + Self::Config(config) => config.common.hint.as_deref(), + } + } + + pub fn set_enabled(&mut self, enabled: bool) { + match self { + Self::Enabled(value) => *value = enabled, + Self::Config(config) => config.common.enabled = Some(enabled), + } + } +} + +impl FeatureToml { + pub fn typed_config(&self) -> Option, toml::de::Error>> + where + T: DeserializeOwned, + { + match self { + Self::Enabled(_) => None, + Self::Config(config) => Some(config.clone().typed()), + } + } +} + +impl FeatureConfigTable { + pub fn into_raw(self) -> Result, toml::ser::Error> + where + T: Serialize, + { + let extra = match TomlValue::try_from(self.extra)? { + TomlValue::Table(table) => table.into_iter().collect(), + other => { + unreachable!("feature config extras must serialize as a TOML table: {other:?}") + } + }; + Ok(FeatureConfigTable { + common: self.common, + extra, + }) + } +} + +impl FeatureConfigTable { + pub fn typed(self) -> Result, toml::de::Error> + where + T: DeserializeOwned, + { + Ok(FeatureConfigTable { + common: self.common, + extra: TomlValue::Table(self.extra.into_iter().collect()).try_into()?, + }) + } +} + +pub fn unstable_features_warning_event( + effective_features: Option<&Table>, + suppress_unstable_features_warning: bool, + features: &Features, + config_path: &str, +) -> Option { + if suppress_unstable_features_warning { + return None; + } + + let mut under_development_feature_keys = Vec::new(); + if let Some(table) = effective_features { + for (key, value) in table { + if configured_feature_enabled_in_effective_table(key, value) != Some(true) { + continue; + } + let Some(spec) = FEATURES.iter().find(|spec| spec.key == key.as_str()) else { + continue; + }; + if !features.enabled(spec.id) { + continue; + } + if matches!(spec.stage, Stage::UnderDevelopment) { + under_development_feature_keys.push(spec.key.to_string()); + } + } + } + + if under_development_feature_keys.is_empty() { + return None; + } + + let under_development_feature_keys = under_development_feature_keys.join(", "); + let message = format!( + "Under-development features enabled: {under_development_feature_keys}. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in {config_path}." + ); + Some(Event { + id: String::new(), + msg: EventMsg::Warning(WarningEvent { message }), + }) +} + +fn configured_feature_enabled_in_effective_table(key: &str, value: &TomlValue) -> Option { + let feature: FeatureToml = value.clone().try_into().ok()?; + feature_enabled_in_config(key, &feature) +} diff --git a/codex-rs/features/src/registry.rs b/codex-rs/features/src/registry.rs new file mode 100644 index 0000000000..35cae91882 --- /dev/null +++ b/codex-rs/features/src/registry.rs @@ -0,0 +1,464 @@ +use crate::machinery::Feature; +use crate::machinery::Stage; + +/// Single, easy-to-read registry of all feature definitions. +#[derive(Debug, Clone, Copy)] +pub struct FeatureSpec { + pub id: Feature, + pub key: &'static str, + pub stage: Stage, + pub default_enabled: bool, +} + +pub const FEATURES: &[FeatureSpec] = &[ + // Stable features. + FeatureSpec { + id: Feature::GhostCommit, + key: "undo", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ShellTool, + key: "shell_tool", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::UnifiedExec, + key: "unified_exec", + stage: Stage::Stable, + default_enabled: !cfg!(windows), + }, + FeatureSpec { + id: Feature::ShellZshFork, + key: "shell_zsh_fork", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ShellSnapshot, + key: "shell_snapshot", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::JsRepl, + key: "js_repl", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::CodeMode, + key: "code_mode", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::CodeModeOnly, + key: "code_mode_only", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::JsReplToolsOnly, + key: "js_repl_tools_only", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::TerminalResizeReflow, + key: "terminal_resize_reflow", + stage: Stage::Experimental { + name: "Terminal resize reflow", + menu_description: "Rebuild Codex-owned transcript scrollback when the terminal width changes.", + announcement: "", + }, + default_enabled: true, + }, + FeatureSpec { + id: Feature::WebSearchRequest, + key: "web_search_request", + stage: Stage::Deprecated, + default_enabled: false, + }, + FeatureSpec { + id: Feature::WebSearchCached, + key: "web_search_cached", + stage: Stage::Deprecated, + default_enabled: false, + }, + FeatureSpec { + id: Feature::SearchTool, + key: "search_tool", + stage: Stage::Removed, + default_enabled: false, + }, + // Experimental program. Rendered in the `/experimental` menu for users. + FeatureSpec { + id: Feature::CodexGitCommit, + key: "codex_git_commit", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::RuntimeMetrics, + key: "runtime_metrics", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::Sqlite, + key: "sqlite", + stage: Stage::Removed, + default_enabled: true, + }, + FeatureSpec { + id: Feature::MemoryTool, + key: "memories", + stage: Stage::Experimental { + name: "Memories", + menu_description: "Allow Codex to create new memories from conversations and bring relevant memories into new conversations.", + announcement: "NEW: Codex can now generate and uses memories. Try is now with `/memories`", + }, + default_enabled: false, + }, + FeatureSpec { + id: Feature::Chronicle, + key: "chronicle", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ChildAgentsMd, + key: "child_agents_md", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ApplyPatchFreeform, + key: "apply_patch_freeform", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ApplyPatchStreamingEvents, + key: "apply_patch_streaming_events", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ExecPermissionApprovals, + key: "exec_permission_approvals", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::CodexHooks, + key: "hooks", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::RequestPermissionsTool, + key: "request_permissions_tool", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::UseLinuxSandboxBwrap, + key: "use_linux_sandbox_bwrap", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::UseLegacyLandlock, + key: "use_legacy_landlock", + stage: Stage::Deprecated, + default_enabled: false, + }, + FeatureSpec { + id: Feature::RequestRule, + key: "request_rule", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::WindowsSandbox, + key: "experimental_windows_sandbox", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::WindowsSandboxElevated, + key: "elevated_windows_sandbox", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::RemoteModels, + key: "remote_models", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::EnableRequestCompression, + key: "enable_request_compression", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::Collab, + key: "multi_agent", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::MultiAgentV2, + key: "multi_agent_v2", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::SpawnCsv, + key: "enable_fanout", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::Apps, + key: "apps", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::EnableMcpApps, + key: "enable_mcp_apps", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::AppsMcpPathOverride, + key: "apps_mcp_path_override", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ToolSearch, + key: "tool_search", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::ToolSearchAlwaysDeferMcpTools, + key: "tool_search_always_defer_mcp_tools", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::UnavailableDummyTools, + key: "unavailable_dummy_tools", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::ToolSuggest, + key: "tool_suggest", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::Plugins, + key: "plugins", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::PluginHooks, + key: "plugin_hooks", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::InAppBrowser, + key: "in_app_browser", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::BrowserUse, + key: "browser_use", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::BrowserUseExternal, + key: "browser_use_external", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::ComputerUse, + key: "computer_use", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::RemotePlugin, + key: "remote_plugin", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ExternalMigration, + key: "external_migration", + stage: Stage::Experimental { + name: "External migration", + menu_description: "Show a startup prompt when Codex detects migratable external agent config for this machine or project.", + announcement: "", + }, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ImageGeneration, + key: "image_generation", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::SkillMcpDependencyInstall, + key: "skill_mcp_dependency_install", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::SkillEnvVarDependencyPrompt, + key: "skill_env_var_dependency_prompt", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::Steer, + key: "steer", + stage: Stage::Removed, + default_enabled: true, + }, + FeatureSpec { + id: Feature::DefaultModeRequestUserInput, + key: "default_mode_request_user_input", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::GuardianApproval, + key: "guardian_approval", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::Goals, + key: "goals", + stage: Stage::Experimental { + name: "Goals", + menu_description: "Set a persistent goal Codex can continue over time", + announcement: "", + }, + default_enabled: false, + }, + FeatureSpec { + id: Feature::CollaborationModes, + key: "collaboration_modes", + stage: Stage::Removed, + default_enabled: true, + }, + FeatureSpec { + id: Feature::ToolCallMcpElicitation, + key: "tool_call_mcp_elicitation", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::Personality, + key: "personality", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::Artifact, + key: "artifact", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::FastMode, + key: "fast_mode", + stage: Stage::Stable, + default_enabled: true, + }, + FeatureSpec { + id: Feature::RealtimeConversation, + key: "realtime_conversation", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::RemoteControl, + key: "remote_control", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ImageDetailOriginal, + key: "image_detail_original", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::TuiAppServer, + key: "tui_app_server", + stage: Stage::Removed, + default_enabled: true, + }, + FeatureSpec { + id: Feature::PreventIdleSleep, + key: "prevent_idle_sleep", + stage: if cfg!(any( + target_os = "macos", + target_os = "linux", + target_os = "windows" + )) { + Stage::Experimental { + name: "Prevent sleep while running", + menu_description: "Keep your computer awake while Codex is running a thread.", + announcement: "NEW: Prevent sleep while running is now available in /experimental.", + } + } else { + Stage::UnderDevelopment + }, + default_enabled: false, + }, + FeatureSpec { + id: Feature::WorkspaceOwnerUsageNudge, + key: "workspace_owner_usage_nudge", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ResponsesWebsockets, + key: "responses_websockets", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::ResponsesWebsocketsV2, + key: "responses_websockets_v2", + stage: Stage::Removed, + default_enabled: false, + }, + FeatureSpec { + id: Feature::WorkspaceDependencies, + key: "workspace_dependencies", + stage: Stage::Stable, + default_enabled: true, + }, +]; diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index 6235c1c3e5..9dfa633256 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -1,5 +1,7 @@ +use crate::CommonFeatureConfigToml; use crate::Feature; use crate::FeatureConfigSource; +use crate::FeatureConfigTable; use crate::FeatureOverrides; use crate::FeatureToml; use crate::Features; @@ -312,19 +314,9 @@ fn apps_require_feature_flag_and_chatgpt_auth() { #[test] 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 mut profile_entries = BTreeMap::new(); - profile_entries.insert("code_mode_only".to_string(), true); - let profile_features = FeaturesToml { - entries: profile_entries, - ..Default::default() - }; + let base_features = FeaturesToml::from(BTreeMap::from([("plugins".to_string(), true)])); + let profile_features = + FeaturesToml::from(BTreeMap::from([("code_mode_only".to_string(), true)])); let features = Features::from_sources( FeatureConfigSource { @@ -416,7 +408,10 @@ multi_agent_v2 = true features.entries(), BTreeMap::from([("multi_agent_v2".to_string(), true)]) ); - assert_eq!(features.multi_agent_v2, Some(FeatureToml::Enabled(true))); + assert_eq!( + features.get("multi_agent_v2"), + Some(&FeatureToml::Enabled(true)) + ); } #[test] @@ -425,6 +420,7 @@ fn multi_agent_v2_feature_config_deserializes_table() { r#" [multi_agent_v2] enabled = true +hint = "Use this feature carefully." max_concurrent_threads_per_session = 4 min_wait_timeout_ms = 2500 usage_hint_enabled = false @@ -441,9 +437,49 @@ hide_spawn_agent_metadata = true BTreeMap::from([("multi_agent_v2".to_string(), true)]) ); assert_eq!( - features.multi_agent_v2, - Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml { - enabled: Some(true), + features.get("multi_agent_v2"), + Some(&FeatureToml::Config(FeatureConfigTable { + common: CommonFeatureConfigToml { + enabled: Some(true), + hint: Some("Use this feature carefully.".to_string()), + }, + extra: BTreeMap::from([ + ( + "hide_spawn_agent_metadata".to_string(), + TomlValue::Boolean(true), + ), + ( + "max_concurrent_threads_per_session".to_string(), + TomlValue::Integer(4), + ), + ("min_wait_timeout_ms".to_string(), TomlValue::Integer(2500)), + ( + "root_agent_usage_hint_text".to_string(), + TomlValue::String("Root guidance.".to_string()), + ), + ( + "subagent_usage_hint_text".to_string(), + TomlValue::String("Subagent guidance.".to_string()), + ), + ("usage_hint_enabled".to_string(), TomlValue::Boolean(false),), + ( + "usage_hint_text".to_string(), + TomlValue::String("Custom delegation guidance.".to_string()), + ), + ]), + })) + ); + let typed = features + .typed_config::("multi_agent_v2") + .expect("table feature should expose a typed config") + .expect("multi_agent_v2 config should deserialize"); + assert_eq!( + typed.common.hint.as_deref(), + Some("Use this feature carefully.") + ); + assert_eq!( + typed.extra, + crate::MultiAgentV2ConfigToml { max_concurrent_threads_per_session: Some(4), min_wait_timeout_ms: Some(2500), usage_hint_enabled: Some(false), @@ -451,7 +487,7 @@ hide_spawn_agent_metadata = true root_agent_usage_hint_text: Some("Root guidance.".to_string()), subagent_usage_hint_text: Some("Subagent guidance.".to_string()), hide_spawn_agent_metadata: Some(true), - })) + } ); } @@ -476,20 +512,30 @@ usage_hint_enabled = false assert_eq!(features.enabled(Feature::MultiAgentV2), false); assert_eq!(features_toml.entries(), BTreeMap::new()); assert_eq!( - features_toml.multi_agent_v2, - Some(crate::FeatureToml::Config(crate::MultiAgentV2ConfigToml { - enabled: None, - max_concurrent_threads_per_session: None, - min_wait_timeout_ms: None, - usage_hint_enabled: Some(false), - usage_hint_text: None, - root_agent_usage_hint_text: None, - subagent_usage_hint_text: None, - hide_spawn_agent_metadata: None, + features_toml.get("multi_agent_v2"), + Some(&crate::FeatureToml::Config(FeatureConfigTable { + common: CommonFeatureConfigToml::default(), + extra: BTreeMap::from([("usage_hint_enabled".to_string(), TomlValue::Boolean(false),)]), })) ); } +#[test] +fn apps_mcp_path_override_path_still_enables_feature() { + let features_toml: FeaturesToml = toml::from_str( + r#" +[apps_mcp_path_override] +path = "/custom/mcp" +"#, + ) + .expect("features table should deserialize"); + + assert_eq!( + features_toml.entries(), + BTreeMap::from([("apps_mcp_path_override".to_string(), true)]) + ); +} + #[test] fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config() { let mut features = Features::with_defaults(); @@ -497,15 +543,25 @@ 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)]), - ..Default::default() - }; + let mut features_toml = FeaturesToml::from_entries(BTreeMap::from([ + ( + "multi_agent_v2".to_string(), + FeatureToml::Config(FeatureConfigTable { + common: CommonFeatureConfigToml { + enabled: Some(false), + hint: Some("Preserve me.".to_string()), + }, + extra: BTreeMap::from([( + "min_wait_timeout_ms".to_string(), + TomlValue::Integer(2500), + )]), + }), + ), + ( + "include_apply_patch_tool".to_string(), + FeatureToml::Enabled(true), + ), + ])); features_toml.materialize_resolved_enabled(&features); @@ -520,11 +576,13 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config( ); } assert_eq!( - features_toml.multi_agent_v2, - Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml { - enabled: Some(true), - min_wait_timeout_ms: Some(2500), - ..Default::default() + features_toml.get("multi_agent_v2"), + Some(&FeatureToml::Config(FeatureConfigTable { + common: CommonFeatureConfigToml { + enabled: Some(true), + hint: Some("Preserve me.".to_string()), + }, + extra: BTreeMap::from([("min_wait_timeout_ms".to_string(), TomlValue::Integer(2500),)]), })) ); let replayed = Features::from_sources( @@ -538,10 +596,60 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config( assert_eq!(replayed.enabled(Feature::ApplyPatchFreeform), false); } +#[test] +fn generic_feature_config_deserializes_hint_without_per_feature_wiring() { + let features: FeaturesToml = toml::from_str( + r#" +[some_feature] +enabled = true +hint = "Remember {{ features.multi_agent_v2.max_concurrent_threads_per_session }}" +"#, + ) + .expect("features table should deserialize"); + + assert_eq!( + features.entries(), + BTreeMap::from([("some_feature".to_string(), true)]) + ); + assert_eq!( + features.get("some_feature"), + Some(&FeatureToml::Config(FeatureConfigTable { + common: CommonFeatureConfigToml { + enabled: Some(true), + hint: Some( + "Remember {{ features.multi_agent_v2.max_concurrent_threads_per_session }}" + .to_string(), + ), + }, + extra: BTreeMap::new(), + })) + ); + assert_eq!( + features.hint("some_feature"), + Some("Remember {{ features.multi_agent_v2.max_concurrent_threads_per_session }}") + ); +} + #[test] fn unstable_warning_event_only_mentions_enabled_under_development_features() { let mut configured_features = Table::new(); - configured_features.insert("child_agents_md".to_string(), TomlValue::Boolean(true)); + configured_features.insert( + "child_agents_md".to_string(), + TomlValue::Table(Table::from_iter([ + ("enabled".to_string(), TomlValue::Boolean(true)), + ( + "hint".to_string(), + TomlValue::String("Use child_agents_md".to_string()), + ), + ])), + ); + configured_features.insert( + "code_mode".to_string(), + TomlValue::Table(Table::from_iter([( + "enabled".to_string(), + TomlValue::Boolean(false), + )])), + ); configured_features.insert("personality".to_string(), TomlValue::Boolean(true)); configured_features.insert("unknown".to_string(), TomlValue::Boolean(true)); @@ -560,6 +668,8 @@ 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("unknown")); assert!(message.contains("/tmp/config.toml")); }