Codex Handbook
features/src/lib.rs 1328 lines
//! Centralized feature flags and metadata.//!//! 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;pub use feature_configs::CodeModeConfigToml;pub use feature_configs::MultiAgentV2ConfigToml;pub use feature_configs::NetworkProxyConfigToml;pub use feature_configs::NetworkProxyDomainPermissionToml;pub use feature_configs::NetworkProxyModeToml;pub use feature_configs::NetworkProxyUnixSocketPermissionToml;use feature_configs::RemovedAppsMcpPathOverrideConfigToml;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.    /// Enable the default shell tool.    ShellTool,    /// Enable Claude-style lifecycle hooks loaded from hooks.json files.    CodexHooks,    /// Store CLI auth in the encrypted local secrets backend when keyring storage is selected.    SecretAuthStorage,    // Experimental    /// Enable JavaScript code mode backed by the in-process V8 runtime.    CodeMode,    /// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`).    CodeModeOnly,    /// Use the single unified PTY-backed exec tool.    UnifiedExec,    /// Route shell tool execution through the zsh exec bridge.    ShellZshFork,    /// Allow unified exec to compose with the zsh exec bridge.    ///    /// This flag is only a composition gate. Enabling it by itself must not turn    /// on either `unified_exec` or `shell_zsh_fork` because those features have    /// separate rollout and enterprise controls.    UnifiedExecZshFork,    /// Removed compatibility flag. Transcript scrollback reflow on terminal resize is always on.    TerminalResizeReflow,    /// Add terminal-specific visualization guidance to TUI developer instructions.    TerminalVisualizationInstructions,    /// 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,    /// Expose the extension-backed standalone web search tool.    StandaloneWebSearch,    /// Use the legacy Landlock Linux sandbox fallback instead of the default    /// bubblewrap pipeline.    UseLegacyLandlock,    /// Experimental shell snapshotting.    ShellSnapshot,    /// Enable runtime metrics snapshots via a manual reader.    RuntimeMetrics,    /// Enable startup memory extraction and file-backed memory consolidation.    MemoryTool,    /// Compress cold local thread-store rollout files.    LocalThreadStoreCompression,    /// 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,    /// Start the managed network proxy for sandboxed sessions.    NetworkProxy,    /// 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,    /// Removed compatibility flag for the legacy Apps MCP path override.    AppsMcpPathOverride,    /// Removed compatibility flag retained as a no-op now that tool_search is always enabled.    ToolSearch,    /// Always defer MCP tools behind tool_search instead of exposing small sets directly.    ToolSearchAlwaysDeferMcpTools,    /// Expose MCP model-visible namespaces without the legacy `mcp__` prefix.    NonPrefixedMcpToolNames,    /// Enable discoverable tool suggestions for apps.    ToolSuggest,    /// Enable plugins.    Plugins,    /// Removed compatibility flag for 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,    /// Enable remote plugin sharing flows.    PluginSharing,    /// Removed compatibility flag retained as a no-op.    ExternalMigration,    /// Allow the model to invoke the built-in image generation tool.    ImageGeneration,    /// Replace hosted image generation with the standalone image-generation extension.    ImageGenExt,    /// Resize all inline data-URL images before recording them in history.    ResizeAllImages,    /// Allow prompting and installing missing MCP dependencies.    SkillMcpDependencyInstall,    /// Removed compatibility flag for deleted skill env var dependency prompting.    SkillEnvVarDependencyPrompt,    /// Enable the unified mention popup used by default in the TUI.    MentionsV2,    /// 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,    /// Add current context-window metadata to model-visible context.    TokenBudget,    /// Expose an input-interruptible sleep tool.    SleepTool,    /// Route MCP tool approval prompts through the MCP elicitation request path.    ToolCallMcpElicitation,    /// Prompt Codex Apps connector auth failures through MCP URL elicitations.    AuthElicitation,    /// 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,    /// Prevent idle system sleep while a turn is actively running.    PreventIdleSleep,    /// Enable remote compaction v2 over the normal Responses API.    RemoteCompactionV2,    /// Enable workspace dependency support.    WorkspaceDependencies,    // Removed    /// Removed compatibility flag retained as a no-op so old configs can    /// still parse `undo`.    GhostCommit,    /// Removed compatibility flag for the deleted JavaScript REPL feature.    JsRepl,    /// Removed compatibility flag for the deleted JavaScript REPL tool-only mode.    JsReplToolsOnly,    /// 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,    /// 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,    /// Removed legacy git commit attribution guidance flag.    CodexGitCommit,    /// Persist rollout metadata to a local SQLite database.    Sqlite,    /// Removed compatibility flag for the deleted apply_patch fallback feature.    ApplyPatchFreeform,    /// Removed compatibility flag for the deleted unavailable-tool placeholder backfill.    UnavailableDummyTools,    /// Steer feature flag - when enabled, Enter submits immediately instead of queuing.    /// Kept for config backward compatibility; behavior is always steer-enabled.    Steer,    /// Enable collaboration modes (Plan, Default).    /// Kept for config backward compatibility; behavior is always collaboration-modes-enabled.    CollaborationModes,    /// Removed compatibility flag for the deleted remote control feature.    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,    /// Removed compatibility flag retained as a no-op now that workspace owner    /// usage nudges are always enabled.    WorkspaceOwnerUsageNudge,    /// Legacy rollout flag for Responses API WebSocket transport experiments.    ResponsesWebsockets,    /// Legacy rollout flag for Responses API WebSocket transport v2 experiments.    ResponsesWebsocketsV2,}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<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 web_search_request: Option<bool>,}#[derive(Debug, Clone, Copy, Default)]pub struct FeatureConfigSource<'a> {    pub features: Option<&'a FeaturesToml>,    pub experimental_use_unified_exec_tool: Option<bool>,}impl FeatureOverrides {    fn apply(self, features: &mut 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, m: &BTreeMap<String, bool>) {        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;                }                "remote_control" => {                    continue;                }                "apply_patch_freeform" => {                    continue;                }                "tool_search" | "apps_mcp_path_override" => {                    continue;                }                "image_detail_original" => {                    continue;                }                "plugin_hooks" => {                    continue;                }                "skill_env_var_dependency_prompt" => {                    continue;                }                "terminal_resize_reflow" => {                    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 {                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 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);        }    }    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, JsonSchema)]pub struct FeaturesToml {    #[serde(default, skip_serializing_if = "Option::is_none")]    pub code_mode: Option<FeatureToml<CodeModeConfigToml>>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,    #[serde(default, rename = "apps_mcp_path_override", skip_serializing)]    #[schemars(skip)]    removed_apps_mcp_path_override: Option<FeatureToml<RemovedAppsMcpPathOverrideConfigToml>>,    pub network_proxy: Option<FeatureToml<NetworkProxyConfigToml>>,    /// Boolean feature toggles keyed by canonical or legacy feature name.    #[serde(flatten)]    entries: BTreeMap<String, bool>,}impl Features {    fn apply_toml(&mut self, features: &FeaturesToml) {        let entries = features.entries();        self.apply_map(&entries);    }}impl FeaturesToml {    /// Removes compatibility-only inputs that no longer affect runtime    /// behavior or belong in newly materialized config.    pub fn clear_removed_compatibility_entries(&mut self) {        self.removed_apps_mcp_path_override = None;        self.entries.remove("apps_mcp_path_override");    }    pub fn entries(&self) -> BTreeMap<String, bool> {        let mut entries = self.entries.clone();        if let Some(enabled) = self.code_mode.as_ref().and_then(FeatureToml::enabled) {            entries.insert(Feature::CodeMode.key().to_string(), enabled);        }        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.network_proxy.as_ref().and_then(FeatureToml::enabled) {            entries.insert(Feature::NetworkProxy.key().to_string(), enabled);        }        entries    }    pub fn materialize_resolved_enabled(&mut self, features: &Features) {        self.clear_removed_compatibility_entries();        let Self {            code_mode,            multi_agent_v2,            removed_apps_mcp_path_override: _,            network_proxy,            entries,        } = self;        for key in legacy::legacy_feature_keys() {            entries.remove(key);        }        for spec in FEATURES {            let enabled = features.enabled(spec.id);            if spec.id == Feature::CodeMode {                materialize_resolved_feature_enabled(code_mode, enabled);            } else if spec.id == Feature::MultiAgentV2 {                materialize_resolved_feature_enabled(multi_agent_v2, enabled);            } else if spec.id == Feature::NetworkProxy {                materialize_resolved_feature_enabled(network_proxy, enabled);            } else {                entries.insert(spec.key.to_string(), enabled);            }        }    }}fn materialize_resolved_feature_enabled<T: FeatureConfig>(    feature: &mut Option<FeatureToml<T>>,    enabled: bool,) {    match feature {        Some(feature) => feature.set_enabled(enabled),        None => *feature = Some(FeatureToml::Enabled(enabled)),    }}impl From<BTreeMap<String, bool>> for FeaturesToml {    fn from(entries: BTreeMap<String, bool>) -> 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<T> {    Enabled(bool),    Config(T),}impl<T: FeatureConfig> FeatureToml<T> {    pub fn enabled(&self) -> Option<bool> {        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<bool>;    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::SecretAuthStorage,        key: "secret_auth_storage",        stage: Stage::Stable,        default_enabled: cfg!(windows),    },    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::UnifiedExecZshFork,        key: "unified_exec_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::Removed,        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::StandaloneWebSearch,        key: "standalone_web_search",        stage: Stage::UnderDevelopment,        default_enabled: false,    },    FeatureSpec {        id: Feature::SearchTool,        key: "search_tool",        stage: Stage::Removed,        default_enabled: false,    },    FeatureSpec {        id: Feature::CodexGitCommit,        key: "codex_git_commit",        stage: Stage::Removed,        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 use memories. Try it now with `/memories`",        },        default_enabled: false,    },    FeatureSpec {        id: Feature::LocalThreadStoreCompression,        key: "local_thread_store_compression",        stage: Stage::UnderDevelopment,        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::Removed,        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::NetworkProxy,        key: "network_proxy",        stage: Stage::Experimental {            name: "Network proxy",            menu_description: "Apply network proxy restrictions to sandboxed sessions that already have network access.",            announcement: "NEW: Network proxy can now be enabled from /experimental. Restart Codex after enabling it.",        },        default_enabled: false,    },    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::Removed,        default_enabled: false,    },    FeatureSpec {        id: Feature::ToolSearch,        key: "tool_search",        stage: Stage::Removed,        default_enabled: false,    },    FeatureSpec {        id: Feature::ToolSearchAlwaysDeferMcpTools,        key: "tool_search_always_defer_mcp_tools",        stage: Stage::UnderDevelopment,        default_enabled: false,    },    FeatureSpec {        id: Feature::NonPrefixedMcpToolNames,        key: "non_prefixed_mcp_tool_names",        stage: Stage::UnderDevelopment,        default_enabled: false,    },    FeatureSpec {        id: Feature::UnavailableDummyTools,        key: "unavailable_dummy_tools",        stage: Stage::Removed,        default_enabled: false,    },    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::Removed,        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::PluginSharing,        key: "plugin_sharing",        stage: Stage::Stable,        default_enabled: true,    },    FeatureSpec {        id: Feature::ExternalMigration,        key: "external_migration",        stage: Stage::Removed,        default_enabled: false,    },    FeatureSpec {        id: Feature::ImageGeneration,        key: "image_generation",        stage: Stage::Stable,        default_enabled: true,    },    FeatureSpec {        id: Feature::ImageGenExt,        key: "imagegenext",        stage: Stage::UnderDevelopment,        default_enabled: false,    },    FeatureSpec {        id: Feature::ResizeAllImages,        key: "resize_all_images",        stage: Stage::UnderDevelopment,        default_enabled: false,    },    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::Removed,        default_enabled: false,    },    FeatureSpec {        id: Feature::MentionsV2,        key: "mentions_v2",        stage: Stage::Stable,        default_enabled: true,    },    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::TerminalVisualizationInstructions,        key: "terminal_visualization_instructions",        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::Stable,        default_enabled: true,    },    FeatureSpec {        id: Feature::TokenBudget,        key: "token_budget",        stage: Stage::UnderDevelopment,        default_enabled: false,    },    FeatureSpec {        id: Feature::SleepTool,        key: "sleep_tool",        stage: Stage::UnderDevelopment,        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::AuthElicitation,        key: "auth_elicitation",        stage: Stage::UnderDevelopment,        default_enabled: false,    },    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::Removed,        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::Removed,        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::RemoteCompactionV2,        key: "remote_compaction_v2",        stage: Stage::Stable,        default_enabled: true,    },    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<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 {            let is_enabled = value.as_bool() == Some(true)                || value                    .as_table()                    .and_then(|table| table.get("enabled"))                    .and_then(toml::Value::as_bool)                    == Some(true);            if !is_enabled {                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;    }    under_development_feature_keys.sort();    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 }),    })}#[cfg(test)]mod tests;