Compare commits

...

2 Commits

Author SHA1 Message Date
jif-oai
e337e9b216 rendering 2026-05-02 12:10:17 +01:00
jif-oai
d2aa463545 feat: generic hints for configs 2026-05-02 12:04:36 +01:00
12 changed files with 1974 additions and 1481 deletions

View File

@@ -45,12 +45,22 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
}
validation
.properties
.insert(feature.key.to_string(), schema_gen.subschema_for::<bool>());
.insert(
feature.key.to_string(),
schema_gen.subschema_for::<codex_features::FeatureToml<
codex_features::NoExtraFeatureConfigToml,
>>(),
);
}
for legacy_key in legacy_feature_keys() {
validation
.properties
.insert(legacy_key.to_string(), schema_gen.subschema_for::<bool>());
.insert(
legacy_key.to_string(),
schema_gen.subschema_for::<codex_features::FeatureToml<
codex_features::NoExtraFeatureConfigToml,
>>(),
);
}
validation.additional_properties = Some(Box::new(Schema::Bool(false)));
object.object = Some(Box::new(validation));

View File

@@ -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"
}
}

View File

@@ -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<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())?;
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<Option<MultiAgentV2ConfigToml>> {
typed_feature_config::<MultiAgentV2ConfigToml>(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<Option<AppsMcpPathOverrideConfigToml>> {
typed_feature_config::<AppsMcpPathOverrideConfigToml>(
features,
Feature::AppsMcpPathOverride.key(),
)
.map(|config| config.map(|config| config.extra))
}
fn typed_feature_config<T>(
features: Option<&FeaturesToml>,
key: &'static str,
) -> std::io::Result<Option<FeatureConfigTable<T>>>
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

View File

@@ -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::<MemoriesToml>(
&config.memories,
"memories",
@@ -208,6 +213,9 @@ where
#[cfg(test)]
mod tests {
use super::*;
use codex_features::CommonFeatureConfigToml;
use codex_features::FeatureConfigTable;
use codex_features::FeatureToml;
use pretty_assertions::assert_eq;
use std::sync::Arc;
@@ -253,20 +261,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::<MultiAgentV2ConfigToml>(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);
}

View File

@@ -0,0 +1,136 @@
use std::collections::BTreeMap;
use codex_features::FEATURES;
use codex_protocol::models::ResponseItem;
use codex_utils_template::Template;
use codex_utils_template::TemplateError;
use codex_utils_template::TemplateRenderError;
use toml::Value as TomlValue;
use tracing::warn;
use crate::context_manager::updates::build_developer_update_item;
use super::session::SessionConfiguration;
use super::turn_context::TurnContext;
pub(super) fn render_feature_hint_messages(
session_configuration: &SessionConfiguration,
turn_context: &TurnContext,
) -> Vec<ResponseItem> {
let lockfile = match session_configuration.to_config_lockfile_toml() {
Ok(lockfile) => lockfile,
Err(err) => {
warn!(error = %err, "failed to build config lock for feature hint rendering");
return Vec::new();
}
};
let Some(features_toml) = lockfile.config.features.as_ref() else {
return Vec::new();
};
let template_variables = match build_template_variables(&lockfile.config) {
Ok(variables) => variables,
Err(err) => {
warn!(error = %err, "failed to serialize resolved config for feature hint rendering");
return Vec::new();
}
};
FEATURES
.iter()
.filter(|spec| turn_context.features.enabled(spec.id))
.filter_map(|spec| {
let hint = features_toml.hint(spec.key)?;
let rendered = match render_feature_hint(hint, &template_variables) {
Ok(rendered) => rendered,
Err(err) => {
warn!(
feature = spec.key,
error = %err,
"failed to render feature hint"
);
return None;
}
};
if rendered.is_empty() {
return None;
}
build_developer_update_item(vec![rendered])
})
.collect()
}
fn render_feature_hint(
source: &str,
template_variables: &BTreeMap<String, String>,
) -> Result<String, TemplateError> {
let template = Template::parse(source)?;
let variables = template
.placeholders()
.map(|placeholder| {
template_variables
.get(placeholder)
.map(|value| (placeholder, value.as_str()))
.ok_or_else(|| TemplateRenderError::MissingValue {
name: placeholder.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?;
template.render(variables).map_err(Into::into)
}
fn build_template_variables(
config: &codex_config::config_toml::ConfigToml,
) -> Result<BTreeMap<String, String>, toml::ser::Error> {
let value = TomlValue::try_from(config)?;
let mut variables = BTreeMap::new();
flatten_toml_value(/*prefix*/ None, &value, &mut variables);
Ok(variables)
}
fn flatten_toml_value(
prefix: Option<&str>,
value: &TomlValue,
variables: &mut BTreeMap<String, String>,
) {
match value {
TomlValue::Table(table) => {
for (key, value) in table {
let key = match prefix {
Some(prefix) => format!("{prefix}.{key}"),
None => key.clone(),
};
flatten_toml_value(Some(&key), value, variables);
}
}
TomlValue::String(value) => insert_template_variable(prefix, value.clone(), variables),
TomlValue::Integer(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Float(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Boolean(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Datetime(value) => {
insert_template_variable(prefix, value.to_string(), variables);
}
TomlValue::Array(value) => {
insert_template_variable(
prefix,
TomlValue::Array(value.clone()).to_string(),
variables,
);
}
}
}
fn insert_template_variable(
key: Option<&str>,
value: String,
variables: &mut BTreeMap<String, String>,
) {
if let Some(key) = key {
variables.insert(key.to_string(), value);
}
}

View File

@@ -184,6 +184,7 @@ use codex_protocol::error::Result as CodexResult;
use codex_protocol::exec_output::StreamOutput;
mod config_lock;
mod feature_hints;
mod handlers;
mod mcp;
mod multi_agents;
@@ -2540,6 +2541,7 @@ impl Session {
collaboration_mode,
base_instructions,
session_source,
session_configuration,
) = {
let state = self.state.lock().await;
(
@@ -2548,6 +2550,7 @@ impl Session {
state.session_configuration.collaboration_mode.clone(),
state.session_configuration.base_instructions.clone(),
state.session_configuration.session_source.clone(),
state.session_configuration.clone(),
)
};
if let Some(model_switch_message) =
@@ -2702,13 +2705,16 @@ impl Session {
let multi_agent_v2_usage_hint_text =
multi_agents::usage_hint_text(turn_context, &session_source);
let feature_hint_messages =
feature_hints::render_feature_hint_messages(&session_configuration, turn_context);
let mut items = Vec::with_capacity(4);
let mut items = Vec::with_capacity(4 + feature_hint_messages.len());
if let Some(developer_message) =
crate::context_manager::updates::build_developer_update_item(developer_sections)
{
items.push(developer_message);
}
items.extend(feature_hint_messages);
if let Some(usage_hint_text) = multi_agent_v2_usage_hint_text
&& let Some(usage_hint_message) =
crate::context_manager::updates::build_developer_update_item(vec![

View File

@@ -5688,6 +5688,47 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_feature_dis
);
}
#[tokio::test]
async fn build_initial_context_renders_feature_hint_developer_message_with_resolved_config_values()
{
let (session, turn_context, _rx_event) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"),
Vec::new(),
|config| {
let _ = config.features.enable(Feature::MemoryTool);
config.agent_max_threads = Some(7);
let user_config_path = match config.config_layer_stack.get_user_layer() {
Some(layer) => match &layer.name {
codex_config::ConfigLayerSource::User { file } => file.clone(),
other => panic!("expected user config layer, got {other:?}"),
},
None => panic!("expected user config layer"),
};
config.config_layer_stack = config.config_layer_stack.with_user_config(
&user_config_path,
toml::toml! {
[features.memories]
enabled = true
hint = "This is a super hint {{ agents.max_threads }}"
}
.into(),
);
},
)
.await;
let initial_context = session.build_initial_context(turn_context.as_ref()).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
developer_messages
.iter()
.any(|message| message.as_slice() == ["This is a super hint 7"]),
"expected rendered feature hint developer message, got {developer_messages:?}"
);
}
#[tokio::test]
async fn configured_multi_agent_v2_usage_hint_texts_use_effective_enabled_feature_state() {
let (mut session, _turn_context) =

View File

@@ -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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1))]
pub max_concurrent_threads_per_session: Option<usize>,
@@ -26,31 +23,9 @@ pub struct MultiAgentV2ConfigToml {
pub hide_spawn_agent_metadata: Option<bool>,
}
impl FeatureConfig for MultiAgentV2ConfigToml {
fn enabled(&self) -> Option<bool> {
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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
impl FeatureConfig for AppsMcpPathOverrideConfigToml {
fn enabled(&self) -> Option<bool> {
self.enabled.or(self.path.as_ref().map(|_| true))
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = Some(enabled);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<String>,
}
/// Holds the effective set of enabled features.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Features {
enabled: BTreeSet<Feature>,
legacy_usages: BTreeSet<LegacyFeatureUsage>,
}
#[derive(Debug, Clone, Default)]
pub struct FeatureOverrides {
pub include_apply_patch_tool: Option<bool>,
pub web_search_request: Option<bool>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FeatureConfigSource<'a> {
pub features: Option<&'a FeaturesToml>,
pub include_apply_patch_tool: Option<bool>,
pub experimental_use_freeform_apply_patch: Option<bool>,
pub experimental_use_unified_exec_tool: Option<bool>,
}
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<Item = &LegacyFeatureUsage> + '_ {
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<String, bool>) {
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<Feature> {
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<String>) {
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<Feature> {
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<Feature> {
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<String, FeatureToml>,
}
impl FeaturesToml {
#[cfg(test)]
pub(crate) fn from_entries(entries: BTreeMap<String, FeatureToml>) -> Self {
Self { entries }
}
pub fn entries(&self) -> BTreeMap<String, bool> {
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<T>(
&self,
key: &str,
) -> Option<Result<FeatureConfigTable<T>, toml::de::Error>>
where
T: DeserializeOwned,
{
self.get(key).and_then(FeatureToml::typed_config::<T>)
}
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<T>(
&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<String, FeatureToml>,
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<bool> {
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<BTreeMap<String, bool>> for FeaturesToml {
fn from(entries: BTreeMap<String, bool>) -> Self {
Self {
entries: entries
.into_iter()
.map(|(key, enabled)| (key, FeatureToml::Enabled(enabled)))
.collect(),
}
}
}
pub type RawFeatureConfigExtras = BTreeMap<String, TomlValue>;
#[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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
}
#[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<T = RawFeatureConfigExtras> {
#[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<T = RawFeatureConfigExtras> {
Enabled(bool),
Config(FeatureConfigTable<T>),
}
impl<T> FeatureToml<T> {
pub fn enabled(&self) -> Option<bool> {
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<RawFeatureConfigExtras> {
pub fn typed_config<T>(&self) -> Option<Result<FeatureConfigTable<T>, toml::de::Error>>
where
T: DeserializeOwned,
{
match self {
Self::Enabled(_) => None,
Self::Config(config) => Some(config.clone().typed()),
}
}
}
impl<T> FeatureConfigTable<T> {
pub fn into_raw(self) -> Result<FeatureConfigTable<RawFeatureConfigExtras>, 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<RawFeatureConfigExtras> {
pub fn typed<T>(self) -> Result<FeatureConfigTable<T>, 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<Event> {
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<bool> {
let feature: FeatureToml = value.clone().try_into().ok()?;
feature_enabled_in_config(key, &feature)
}

View File

@@ -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,
},
];

View File

@@ -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::<crate::MultiAgentV2ConfigToml>("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"));
}