Skip to main content

codex_features/
lib.rs

1//! Centralized feature flags and metadata.
2//!
3//! This crate defines the feature registry plus the logic used to resolve an
4//! effective feature set from config-like inputs.
5
6use codex_otel::SessionTelemetry;
7use codex_protocol::protocol::Event;
8use codex_protocol::protocol::EventMsg;
9use codex_protocol::protocol::WarningEvent;
10use schemars::JsonSchema;
11use serde::Deserialize;
12use serde::Serialize;
13use std::collections::BTreeMap;
14use std::collections::BTreeSet;
15use toml::Table;
16
17mod feature_configs;
18mod legacy;
19pub use feature_configs::CodeModeConfigToml;
20pub use feature_configs::CurrentTimeReminderConfigToml;
21pub use feature_configs::CurrentTimeReminderDeliveryMode;
22pub use feature_configs::CurrentTimeSource;
23pub use feature_configs::MultiAgentV2ConfigToml;
24pub use feature_configs::NetworkProxyConfigToml;
25pub use feature_configs::NetworkProxyDomainPermissionToml;
26pub use feature_configs::NetworkProxyModeToml;
27pub use feature_configs::NetworkProxyUnixSocketPermissionToml;
28use feature_configs::RemovedAppsMcpPathOverrideConfigToml;
29pub use feature_configs::RolloutBudgetConfigToml;
30pub use feature_configs::TokenBudgetConfigToml;
31use legacy::LegacyFeatureToggles;
32pub use legacy::legacy_feature_keys;
33
34/// High-level lifecycle stage for a feature.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Stage {
37    /// Features that are still under development, not ready for external use
38    UnderDevelopment,
39    /// Experimental features made available to users through the `/experimental` menu
40    Experimental {
41        name: &'static str,
42        menu_description: &'static str,
43        announcement: &'static str,
44    },
45    /// Stable features. The feature flag is kept for ad-hoc enabling/disabling
46    Stable,
47    /// Deprecated feature that should not be used anymore.
48    Deprecated,
49    /// The feature flag is useless but kept for backward compatibility reason.
50    Removed,
51}
52
53impl Stage {
54    pub fn experimental_menu_name(self) -> Option<&'static str> {
55        match self {
56            Stage::Experimental { name, .. } => Some(name),
57            Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None,
58        }
59    }
60
61    pub fn experimental_menu_description(self) -> Option<&'static str> {
62        match self {
63            Stage::Experimental {
64                menu_description, ..
65            } => Some(menu_description),
66            Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None,
67        }
68    }
69
70    pub fn experimental_announcement(self) -> Option<&'static str> {
71        match self {
72            Stage::Experimental {
73                announcement: "", ..
74            } => None,
75            Stage::Experimental { announcement, .. } => Some(announcement),
76            Stage::UnderDevelopment | Stage::Stable | Stage::Deprecated | Stage::Removed => None,
77        }
78    }
79}
80
81/// Unique features toggled via configuration.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
83pub enum Feature {
84    // Stable.
85    /// Enable the default shell tool.
86    ShellTool,
87    /// Enable Claude-style lifecycle hooks loaded from hooks.json files.
88    CodexHooks,
89    /// Store CLI auth in the encrypted local secrets backend when keyring storage is selected.
90    SecretAuthStorage,
91
92    // Experimental
93    /// Enable JavaScript code mode backed by the in-process V8 runtime.
94    CodeMode,
95    /// Use a 30-second default yield timeout for code mode exec calls.
96    CodeModeBufferedExec,
97    /// Run JavaScript code mode in the standalone host process.
98    CodeModeHost,
99    /// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`).
100    CodeModeOnly,
101    /// Use the single unified PTY-backed exec tool.
102    UnifiedExec,
103    /// Route shell tool execution through the zsh exec bridge.
104    ShellZshFork,
105    /// Allow unified exec to compose with the zsh exec bridge.
106    ///
107    /// This flag is only a composition gate. Enabling it by itself must not turn
108    /// on either `unified_exec` or `shell_zsh_fork` because those features have
109    /// separate rollout and enterprise controls.
110    UnifiedExecZshFork,
111    /// Removed compatibility flag. Transcript scrollback reflow on terminal resize is always on.
112    TerminalResizeReflow,
113    /// Add terminal-specific visualization guidance to TUI developer instructions.
114    TerminalVisualizationInstructions,
115    /// Stream structured progress while apply_patch input is being generated.
116    ApplyPatchStreamingEvents,
117    /// Allow exec tools to request additional permissions while staying sandboxed.
118    ExecPermissionApprovals,
119    /// Expose the built-in request_permissions tool.
120    RequestPermissionsTool,
121    /// Allow the model to request web searches that fetch live content.
122    WebSearchRequest,
123    /// Allow the model to request web searches that fetch cached content.
124    /// Takes precedence over `WebSearchRequest`.
125    WebSearchCached,
126    /// Expose the extension-backed standalone web search tool.
127    StandaloneWebSearch,
128    /// Use the legacy Landlock Linux sandbox fallback instead of the default
129    /// bubblewrap pipeline.
130    UseLegacyLandlock,
131    /// Experimental shell snapshotting.
132    ShellSnapshot,
133    /// Allow turns to start while selected executors are still starting.
134    DeferredExecutor,
135    /// Enable runtime metrics snapshots via a manual reader.
136    RuntimeMetrics,
137    /// Enable startup memory extraction and file-backed memory consolidation.
138    MemoryTool,
139    /// Enable importing project-scoped memory from external agents.
140    ExternalAgentMemoryImport,
141    /// Compress cold local thread-store rollout files.
142    LocalThreadStoreCompression,
143    /// Enable the Chronicle sidecar for passive screen-context memories.
144    Chronicle,
145    /// Compress request bodies (zstd) when sending streaming requests to codex-backend.
146    EnableRequestCompression,
147    /// Start the managed network proxy for sandboxed sessions.
148    NetworkProxy,
149    /// Respect host system proxy settings for Codex-owned network clients.
150    RespectSystemProxy,
151    /// Enable collab tools.
152    Collab,
153    /// Enable task-path-based multi-agent routing.
154    MultiAgentV2,
155    /// Removed compatibility flag retained as a no-op.
156    MultiAgentMode,
157    /// Removed compatibility flag for the deleted agent-job tools.
158    SpawnCsv,
159    /// Enable apps.
160    Apps,
161    /// Enable MCP apps.
162    EnableMcpApps,
163    /// Removed compatibility flag for the legacy Apps MCP path override.
164    AppsMcpPathOverride,
165    /// Removed compatibility flag retained as a no-op now that tool_search is always enabled.
166    ToolSearch,
167    /// Removed compatibility flag. MCP tools are always deferred when tool_search is available.
168    ToolSearchAlwaysDeferMcpTools,
169    /// Expose MCP model-visible namespaces without the legacy `mcp__` prefix.
170    NonPrefixedMcpToolNames,
171    /// Enable discoverable tool suggestions for apps.
172    ToolSuggest,
173    /// Enable plugins.
174    Plugins,
175    /// Discover selected-root plugin and skill manifests through one high-level exec-server RPC.
176    ExecutorCapabilityDiscovery,
177    /// Removed compatibility flag for plugin-bundled lifecycle hooks.
178    PluginHooks,
179    /// Allow the in-app browser pane in desktop apps.
180    ///
181    /// Requirements-only gate: this should be set from requirements, not user config.
182    InAppBrowser,
183    /// Allow Browser Use agent integration in desktop apps.
184    ///
185    /// Requirements-only gate: this should be set from requirements, not user config.
186    BrowserUse,
187    /// Allow Browser Use integration to access the full Chrome DevTools Protocol surface.
188    ///
189    /// Requirements-only gate: this should be set from requirements, not user config.
190    BrowserUseFullCdpAccess,
191    /// Allow Browser Use integration with external browsers.
192    ///
193    /// Requirements-only gate: this should be set from requirements, not user config.
194    BrowserUseExternal,
195    /// Allow Codex Computer Use.
196    ///
197    /// Requirements-only gate: this should be set from requirements, not user config.
198    ComputerUse,
199    /// Enable the PS-backed remote plugin catalog.
200    RemotePlugin,
201    /// Enable remote plugin sharing flows.
202    PluginSharing,
203    /// Removed compatibility flag retained as a no-op.
204    ExternalMigration,
205    /// Enable extension-backed image generation.
206    ImageGeneration,
207    /// Removed compatibility flag for always-on centralized image preparation.
208    ResizeAllImages,
209    /// Removed compatibility flag for always-on response item IDs.
210    ItemIds,
211    /// Request sequential cutoff reasoning summary delivery.
212    ConcurrentReasoningSummaries,
213    /// Allow prompting and installing missing MCP dependencies.
214    SkillMcpDependencyInstall,
215    /// Run cheap skill-search methods in shadow mode and emit experiment metrics.
216    SkillSearch,
217    /// Removed compatibility flag for deleted skill env var dependency prompting.
218    SkillEnvVarDependencyPrompt,
219    /// Enable the unified mention popup used by default in the TUI.
220    MentionsV2,
221    /// Allow request_user_input in Default collaboration mode.
222    DefaultModeRequestUserInput,
223    /// Enable automatic review for approval prompts.
224    GuardianApproval,
225    /// Enable persisted thread goals and automatic goal continuation.
226    Goals,
227    /// Add current context-window metadata to model-visible context.
228    TokenBudget,
229    /// Track and report a shared token budget across a session's agent threads.
230    RolloutBudget,
231    /// Add current-time reminders to model-visible context.
232    CurrentTimeReminder,
233    /// Route MCP tool approval prompts through the MCP elicitation request path.
234    ToolCallMcpElicitation,
235    /// Prompt Codex Apps connector auth failures through MCP URL elicitations.
236    AuthElicitation,
237    /// Enable personality selection in the TUI.
238    Personality,
239    /// Enable native artifact tools.
240    Artifact,
241    /// Enable Fast mode selection in the TUI and request layer.
242    FastMode,
243    /// Enable experimental realtime voice conversation mode in the TUI.
244    RealtimeConversation,
245    /// Prevent idle system sleep while a turn is actively running.
246    PreventIdleSleep,
247    /// Enable remote compaction v2 over the normal Responses API.
248    RemoteCompactionV2,
249    /// Use Agent Identity for ChatGPT-authenticated sessions.
250    UseAgentIdentity,
251    /// Enable workspace dependency support.
252    WorkspaceDependencies,
253
254    // Removed
255    /// Removed compatibility flag retained as a no-op so old configs can
256    /// still parse `undo`.
257    GhostCommit,
258    /// Removed compatibility flag for the deleted JavaScript REPL feature.
259    JsRepl,
260    /// Removed compatibility flag for the deleted JavaScript REPL tool-only mode.
261    JsReplToolsOnly,
262    /// Legacy search-tool feature flag kept for backward compatibility.
263    SearchTool,
264    /// Removed legacy Linux bubblewrap opt-in flag retained as a no-op so old
265    /// wrappers and config can still parse it.
266    UseLinuxSandboxBwrap,
267    /// Allow the model to request approval and propose exec rules.
268    RequestRule,
269    /// Enable Windows sandbox (restricted token) on Windows.
270    WindowsSandbox,
271    /// Use the elevated Windows sandbox pipeline (setup + runner).
272    WindowsSandboxElevated,
273    /// Legacy remote models flag kept for backward compatibility.
274    RemoteModels,
275    /// Removed legacy git commit attribution guidance flag.
276    CodexGitCommit,
277    /// Persist rollout metadata to a local SQLite database.
278    Sqlite,
279    /// Removed compatibility flag for the deleted apply_patch fallback feature.
280    ApplyPatchFreeform,
281    /// Removed compatibility flag for the deleted unavailable-tool placeholder backfill.
282    UnavailableDummyTools,
283    /// Steer feature flag - when enabled, Enter submits immediately instead of queuing.
284    /// Kept for config backward compatibility; behavior is always steer-enabled.
285    Steer,
286    /// Enable collaboration modes (Plan, Default).
287    /// Kept for config backward compatibility; behavior is always collaboration-modes-enabled.
288    CollaborationModes,
289    /// Removed compatibility flag for the deleted remote control feature.
290    RemoteControl,
291    /// Removed compatibility flag retained as a no-op so old wrappers can
292    /// still pass `--enable image_detail_original`.
293    ImageDetailOriginal,
294    /// Removed compatibility flag. The TUI now always uses the app-server implementation.
295    TuiAppServer,
296    /// Removed compatibility flag retained as a no-op now that workspace owner
297    /// usage nudges are always enabled.
298    WorkspaceOwnerUsageNudge,
299    /// Legacy rollout flag for Responses API WebSocket transport experiments.
300    ResponsesWebsockets,
301    /// Legacy rollout flag for Responses API WebSocket transport v2 experiments.
302    ResponsesWebsocketsV2,
303}
304
305impl Feature {
306    pub fn key(self) -> &'static str {
307        self.info().key
308    }
309
310    pub fn stage(self) -> Stage {
311        self.info().stage
312    }
313
314    pub fn default_enabled(self) -> bool {
315        self.info().default_enabled
316    }
317
318    fn info(self) -> &'static FeatureSpec {
319        FEATURES
320            .iter()
321            .find(|spec| spec.id == self)
322            .unwrap_or_else(|| unreachable!("missing FeatureSpec for {self:?}"))
323    }
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
327pub struct LegacyFeatureUsage {
328    pub alias: String,
329    pub feature: Feature,
330    pub summary: String,
331    pub details: Option<String>,
332}
333
334/// Holds the effective set of enabled features.
335#[derive(Debug, Clone, Default, PartialEq)]
336pub struct Features {
337    enabled: BTreeSet<Feature>,
338    legacy_usages: BTreeSet<LegacyFeatureUsage>,
339}
340
341#[derive(Debug, Clone, Default)]
342pub struct FeatureOverrides {
343    pub web_search_request: Option<bool>,
344}
345
346#[derive(Debug, Clone, Copy, Default)]
347pub struct FeatureConfigSource<'a> {
348    pub features: Option<&'a FeaturesToml>,
349    pub experimental_use_unified_exec_tool: Option<bool>,
350}
351
352impl FeatureOverrides {
353    fn apply(self, features: &mut Features) {
354        if let Some(enabled) = self.web_search_request {
355            if enabled {
356                features.enable(Feature::WebSearchRequest);
357            } else {
358                features.disable(Feature::WebSearchRequest);
359            }
360            features.record_legacy_usage("web_search_request", Feature::WebSearchRequest);
361        }
362    }
363}
364
365impl Features {
366    /// Starts with built-in defaults.
367    pub fn with_defaults() -> Self {
368        let mut set = BTreeSet::new();
369        for spec in FEATURES {
370            if spec.default_enabled {
371                set.insert(spec.id);
372            }
373        }
374        Self {
375            enabled: set,
376            legacy_usages: BTreeSet::new(),
377        }
378    }
379
380    pub fn enabled(&self, f: Feature) -> bool {
381        self.enabled.contains(&f)
382    }
383
384    pub fn apps_enabled_for_auth(&self, has_chatgpt_auth: bool) -> bool {
385        self.enabled(Feature::Apps) && has_chatgpt_auth
386    }
387
388    pub fn use_legacy_landlock(&self) -> bool {
389        self.enabled(Feature::UseLegacyLandlock)
390    }
391
392    pub fn enable(&mut self, f: Feature) -> &mut Self {
393        self.enabled.insert(f);
394        self
395    }
396
397    pub fn disable(&mut self, f: Feature) -> &mut Self {
398        self.enabled.remove(&f);
399        self
400    }
401
402    pub fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self {
403        if enabled {
404            self.enable(f)
405        } else {
406            self.disable(f)
407        }
408    }
409
410    pub fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature) {
411        let (summary, details) = legacy_usage_notice(alias, feature);
412        self.legacy_usages.insert(LegacyFeatureUsage {
413            alias: alias.to_string(),
414            feature,
415            summary,
416            details,
417        });
418    }
419
420    pub fn record_legacy_usage(&mut self, alias: &str, feature: Feature) {
421        if alias == feature.key() {
422            return;
423        }
424        self.record_legacy_usage_force(alias, feature);
425    }
426
427    pub fn legacy_feature_usages(&self) -> impl Iterator<Item = &LegacyFeatureUsage> + '_ {
428        self.legacy_usages.iter()
429    }
430
431    pub fn emit_metrics(&self, otel: &SessionTelemetry) {
432        for feature in FEATURES {
433            if matches!(feature.stage, Stage::Removed) {
434                continue;
435            }
436            if self.enabled(feature.id) != feature.default_enabled {
437                otel.counter(
438                    "codex.feature.state",
439                    /*inc*/ 1,
440                    &[
441                        ("feature", feature.key),
442                        ("value", &self.enabled(feature.id).to_string()),
443                    ],
444                );
445            }
446        }
447    }
448
449    /// Apply a table of key -> bool toggles (e.g. from TOML).
450    pub fn apply_map(&mut self, m: &BTreeMap<String, bool>) {
451        for (k, v) in m {
452            match k.as_str() {
453                "web_search_request" => {
454                    self.record_legacy_usage_force(
455                        "features.web_search_request",
456                        Feature::WebSearchRequest,
457                    );
458                }
459                "web_search_cached" => {
460                    self.record_legacy_usage_force(
461                        "features.web_search_cached",
462                        Feature::WebSearchCached,
463                    );
464                }
465                "tui_app_server" => {
466                    continue;
467                }
468                "undo" => {
469                    continue;
470                }
471                "js_repl" => {
472                    continue;
473                }
474                "js_repl_tools_only" => {
475                    continue;
476                }
477                "remote_control" => {
478                    continue;
479                }
480                "apply_patch_freeform" => {
481                    continue;
482                }
483                "tool_search" | "tool_search_always_defer_mcp_tools" | "apps_mcp_path_override" => {
484                    continue;
485                }
486                "image_detail_original" | "resize_all_images" | "item_ids" => {
487                    continue;
488                }
489                "plugin_hooks" => {
490                    continue;
491                }
492                "skill_env_var_dependency_prompt" => {
493                    continue;
494                }
495                "terminal_resize_reflow" => {
496                    continue;
497                }
498                "use_legacy_landlock" => {
499                    self.record_legacy_usage_force(
500                        "features.use_legacy_landlock",
501                        Feature::UseLegacyLandlock,
502                    );
503                }
504                _ => {}
505            }
506            if k == "imagegenext" && m.contains_key(Feature::ImageGeneration.key()) {
507                self.record_legacy_usage(k, Feature::ImageGeneration);
508                continue;
509            }
510            match feature_for_key(k) {
511                Some(feat) => {
512                    if matches!(feat, Feature::TuiAppServer) {
513                        continue;
514                    }
515                    if k != feat.key() {
516                        self.record_legacy_usage(k.as_str(), feat);
517                    }
518                    if *v {
519                        self.enable(feat);
520                    } else {
521                        self.disable(feat);
522                    }
523                }
524                None => {
525                    tracing::warn!("unknown feature key in config: {k}");
526                }
527            }
528        }
529    }
530
531    pub fn from_sources(
532        base: FeatureConfigSource<'_>,
533        profile: FeatureConfigSource<'_>,
534        overrides: FeatureOverrides,
535    ) -> Self {
536        let mut features = Features::with_defaults();
537
538        for source in [base, profile] {
539            LegacyFeatureToggles {
540                experimental_use_unified_exec_tool: source.experimental_use_unified_exec_tool,
541            }
542            .apply(&mut features);
543
544            if let Some(feature_entries) = source.features {
545                features.apply_toml(feature_entries);
546            }
547        }
548
549        overrides.apply(&mut features);
550        features.normalize_dependencies();
551
552        features
553    }
554
555    pub fn enabled_features(&self) -> Vec<Feature> {
556        self.enabled.iter().copied().collect()
557    }
558
559    pub fn normalize_dependencies(&mut self) {
560        if self.enabled(Feature::CodeModeOnly) && !self.enabled(Feature::CodeMode) {
561            self.enable(Feature::CodeMode);
562        }
563    }
564}
565
566fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option<String>) {
567    let canonical = feature.key();
568    match feature {
569        Feature::WebSearchRequest | Feature::WebSearchCached => {
570            let label = match alias {
571                "web_search" => "[features].web_search",
572                "features.web_search_request" | "web_search_request" => {
573                    "[features].web_search_request"
574                }
575                "features.web_search_cached" | "web_search_cached" => {
576                    "[features].web_search_cached"
577                }
578                _ => alias,
579            };
580            let summary =
581                format!("`{label}` is deprecated because web search is enabled by default.");
582            (summary, Some(web_search_details().to_string()))
583        }
584        Feature::UseLegacyLandlock => {
585            let label = match alias {
586                "features.use_legacy_landlock" | "use_legacy_landlock" => {
587                    "[features].use_legacy_landlock"
588                }
589                _ => alias,
590            };
591            let summary = format!("`{label}` is deprecated and will be removed soon.");
592            let details =
593                "Remove this setting to stop opting into the legacy Linux sandbox behavior."
594                    .to_string();
595            (summary, Some(details))
596        }
597        _ => {
598            let label = if alias.contains('.') || alias.starts_with('[') {
599                alias.to_string()
600            } else {
601                format!("[features].{alias}")
602            };
603            let summary = format!("`{label}` is deprecated. Use `[features].{canonical}` instead.");
604            let details = if alias == canonical {
605                None
606            } else {
607                Some(format!(
608                    "Enable it with `--enable {canonical}` or `[features].{canonical}` in config.toml. See https://developers.openai.com/codex/config-basic#feature-flags for details."
609                ))
610            };
611            (summary, details)
612        }
613    }
614}
615
616fn web_search_details() -> &'static str {
617    "Set `web_search` to `\"live\"`, `\"indexed\"`, `\"cached\"`, or `\"disabled\"` at the top level (or under a profile) in config.toml if you want to override it."
618}
619
620/// Keys accepted in `[features]` tables.
621pub fn feature_for_key(key: &str) -> Option<Feature> {
622    for spec in FEATURES {
623        if spec.key == key {
624            return Some(spec.id);
625        }
626    }
627    legacy::feature_for_key(key)
628}
629
630pub fn canonical_feature_for_key(key: &str) -> Option<Feature> {
631    FEATURES
632        .iter()
633        .find(|spec| spec.key == key)
634        .map(|spec| spec.id)
635}
636
637/// Returns `true` if the provided string matches a known feature toggle key.
638pub fn is_known_feature_key(key: &str) -> bool {
639    feature_for_key(key).is_some()
640}
641
642/// Deserializable features table for TOML.
643#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
644pub struct FeaturesToml {
645    #[serde(default, skip_serializing_if = "Option::is_none")]
646    pub code_mode: Option<FeatureToml<CodeModeConfigToml>>,
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub token_budget: Option<FeatureToml<TokenBudgetConfigToml>>,
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub rollout_budget: Option<FeatureToml<RolloutBudgetConfigToml>>,
653    #[serde(default, skip_serializing_if = "Option::is_none")]
654    pub current_time_reminder: Option<FeatureToml<CurrentTimeReminderConfigToml>>,
655    #[serde(default, rename = "apps_mcp_path_override", skip_serializing)]
656    #[schemars(skip)]
657    removed_apps_mcp_path_override: Option<FeatureToml<RemovedAppsMcpPathOverrideConfigToml>>,
658    pub network_proxy: Option<FeatureToml<NetworkProxyConfigToml>>,
659    /// Boolean feature toggles keyed by canonical or legacy feature name.
660    #[serde(flatten)]
661    entries: BTreeMap<String, bool>,
662}
663
664impl Features {
665    fn apply_toml(&mut self, features: &FeaturesToml) {
666        let entries = features.entries();
667        self.apply_map(&entries);
668    }
669}
670
671impl FeaturesToml {
672    /// Removes compatibility-only inputs that no longer affect runtime
673    /// behavior or belong in newly materialized config.
674    pub fn clear_removed_compatibility_entries(&mut self) {
675        self.removed_apps_mcp_path_override = None;
676        self.entries.remove("apps_mcp_path_override");
677    }
678
679    pub fn entries(&self) -> BTreeMap<String, bool> {
680        let mut entries = self.entries.clone();
681        if let Some(enabled) = self.code_mode.as_ref().and_then(FeatureToml::enabled) {
682            entries.insert(Feature::CodeMode.key().to_string(), enabled);
683        }
684        if let Some(enabled) = self.multi_agent_v2.as_ref().and_then(FeatureToml::enabled) {
685            entries.insert(Feature::MultiAgentV2.key().to_string(), enabled);
686        }
687        if let Some(enabled) = self.token_budget.as_ref().and_then(FeatureToml::enabled) {
688            entries.insert(Feature::TokenBudget.key().to_string(), enabled);
689        }
690        if let Some(enabled) = self.rollout_budget.as_ref().and_then(FeatureToml::enabled) {
691            entries.insert(Feature::RolloutBudget.key().to_string(), enabled);
692        }
693        if let Some(enabled) = self
694            .current_time_reminder
695            .as_ref()
696            .and_then(FeatureToml::enabled)
697        {
698            entries.insert(Feature::CurrentTimeReminder.key().to_string(), enabled);
699        }
700        if let Some(enabled) = self.network_proxy.as_ref().and_then(FeatureToml::enabled) {
701            entries.insert(Feature::NetworkProxy.key().to_string(), enabled);
702        }
703        entries
704    }
705
706    pub fn materialize_resolved_enabled(&mut self, features: &Features) {
707        self.clear_removed_compatibility_entries();
708        let Self {
709            code_mode,
710            multi_agent_v2,
711            token_budget,
712            rollout_budget,
713            current_time_reminder,
714            removed_apps_mcp_path_override: _,
715            network_proxy,
716            entries,
717        } = self;
718        for key in legacy::legacy_feature_keys() {
719            entries.remove(key);
720        }
721        for spec in FEATURES {
722            let enabled = features.enabled(spec.id);
723            if spec.id == Feature::CodeMode {
724                materialize_resolved_feature_enabled(code_mode, enabled);
725            } else if spec.id == Feature::MultiAgentV2 {
726                materialize_resolved_feature_enabled(multi_agent_v2, enabled);
727            } else if spec.id == Feature::TokenBudget {
728                materialize_resolved_feature_enabled(token_budget, enabled);
729            } else if spec.id == Feature::RolloutBudget {
730                materialize_resolved_feature_enabled(rollout_budget, enabled);
731            } else if spec.id == Feature::CurrentTimeReminder {
732                materialize_resolved_feature_enabled(current_time_reminder, enabled);
733            } else if spec.id == Feature::NetworkProxy {
734                materialize_resolved_feature_enabled(network_proxy, enabled);
735            } else {
736                entries.insert(spec.key.to_string(), enabled);
737            }
738        }
739    }
740}
741
742fn materialize_resolved_feature_enabled<T: FeatureConfig>(
743    feature: &mut Option<FeatureToml<T>>,
744    enabled: bool,
745) {
746    match feature {
747        Some(feature) => feature.set_enabled(enabled),
748        None => *feature = Some(FeatureToml::Enabled(enabled)),
749    }
750}
751
752impl From<BTreeMap<String, bool>> for FeaturesToml {
753    fn from(entries: BTreeMap<String, bool>) -> Self {
754        Self {
755            entries,
756            ..Default::default()
757        }
758    }
759}
760
761// To be used for features that need more configuration than just enabled/disabled and
762// require a custom config struct under `[features]`.
763#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
764#[serde(untagged)]
765pub enum FeatureToml<T> {
766    Enabled(bool),
767    Config(T),
768}
769
770impl<T: FeatureConfig> FeatureToml<T> {
771    pub fn enabled(&self) -> Option<bool> {
772        match self {
773            Self::Enabled(enabled) => Some(*enabled),
774            Self::Config(config) => config.enabled(),
775        }
776    }
777
778    pub fn set_enabled(&mut self, enabled: bool) {
779        match self {
780            Self::Enabled(value) => *value = enabled,
781            Self::Config(config) => config.set_enabled(enabled),
782        }
783    }
784}
785
786// A trait to be implemented by custom feature config structs when defining a feature that needs more configuration than
787// just enabled/disabled.
788pub trait FeatureConfig {
789    fn enabled(&self) -> Option<bool>;
790    fn set_enabled(&mut self, enabled: bool);
791}
792
793/// Single, easy-to-read registry of all feature definitions.
794#[derive(Debug, Clone, Copy)]
795pub struct FeatureSpec {
796    pub id: Feature,
797    pub key: &'static str,
798    pub stage: Stage,
799    pub default_enabled: bool,
800}
801
802pub const FEATURES: &[FeatureSpec] = &[
803    // Stable features.
804    FeatureSpec {
805        id: Feature::GhostCommit,
806        key: "undo",
807        stage: Stage::Removed,
808        default_enabled: false,
809    },
810    FeatureSpec {
811        id: Feature::ShellTool,
812        key: "shell_tool",
813        stage: Stage::Stable,
814        default_enabled: true,
815    },
816    FeatureSpec {
817        id: Feature::SecretAuthStorage,
818        key: "secret_auth_storage",
819        stage: Stage::Stable,
820        default_enabled: cfg!(windows),
821    },
822    FeatureSpec {
823        id: Feature::UnifiedExec,
824        key: "unified_exec",
825        stage: Stage::Stable,
826        default_enabled: !cfg!(windows),
827    },
828    FeatureSpec {
829        id: Feature::ShellZshFork,
830        key: "shell_zsh_fork",
831        stage: Stage::UnderDevelopment,
832        default_enabled: false,
833    },
834    FeatureSpec {
835        id: Feature::UnifiedExecZshFork,
836        key: "unified_exec_zsh_fork",
837        stage: Stage::UnderDevelopment,
838        default_enabled: false,
839    },
840    FeatureSpec {
841        id: Feature::ShellSnapshot,
842        key: "shell_snapshot",
843        stage: Stage::Stable,
844        default_enabled: true,
845    },
846    FeatureSpec {
847        id: Feature::DeferredExecutor,
848        key: "deferred_executor",
849        stage: Stage::UnderDevelopment,
850        default_enabled: false,
851    },
852    FeatureSpec {
853        id: Feature::JsRepl,
854        key: "js_repl",
855        stage: Stage::Removed,
856        default_enabled: false,
857    },
858    FeatureSpec {
859        id: Feature::CodeMode,
860        key: "code_mode",
861        stage: Stage::UnderDevelopment,
862        default_enabled: false,
863    },
864    FeatureSpec {
865        id: Feature::CodeModeBufferedExec,
866        key: "code_mode_buffered_exec",
867        stage: Stage::UnderDevelopment,
868        default_enabled: false,
869    },
870    FeatureSpec {
871        id: Feature::CodeModeHost,
872        key: "code_mode_host",
873        stage: Stage::Stable,
874        default_enabled: true,
875    },
876    FeatureSpec {
877        id: Feature::CodeModeOnly,
878        key: "code_mode_only",
879        stage: Stage::UnderDevelopment,
880        default_enabled: false,
881    },
882    FeatureSpec {
883        id: Feature::JsReplToolsOnly,
884        key: "js_repl_tools_only",
885        stage: Stage::Removed,
886        default_enabled: false,
887    },
888    FeatureSpec {
889        id: Feature::TerminalResizeReflow,
890        key: "terminal_resize_reflow",
891        stage: Stage::Removed,
892        default_enabled: true,
893    },
894    FeatureSpec {
895        id: Feature::WebSearchRequest,
896        key: "web_search_request",
897        stage: Stage::Deprecated,
898        default_enabled: false,
899    },
900    FeatureSpec {
901        id: Feature::WebSearchCached,
902        key: "web_search_cached",
903        stage: Stage::Deprecated,
904        default_enabled: false,
905    },
906    FeatureSpec {
907        id: Feature::StandaloneWebSearch,
908        key: "standalone_web_search",
909        stage: Stage::UnderDevelopment,
910        default_enabled: false,
911    },
912    FeatureSpec {
913        id: Feature::SearchTool,
914        key: "search_tool",
915        stage: Stage::Removed,
916        default_enabled: false,
917    },
918    FeatureSpec {
919        id: Feature::CodexGitCommit,
920        key: "codex_git_commit",
921        stage: Stage::Removed,
922        default_enabled: false,
923    },
924    FeatureSpec {
925        id: Feature::RuntimeMetrics,
926        key: "runtime_metrics",
927        stage: Stage::UnderDevelopment,
928        default_enabled: false,
929    },
930    FeatureSpec {
931        id: Feature::Sqlite,
932        key: "sqlite",
933        stage: Stage::Removed,
934        default_enabled: true,
935    },
936    FeatureSpec {
937        id: Feature::MemoryTool,
938        key: "memories",
939        stage: Stage::Stable,
940        default_enabled: false,
941    },
942    FeatureSpec {
943        id: Feature::ExternalAgentMemoryImport,
944        key: "external_agent_memory_import",
945        stage: Stage::UnderDevelopment,
946        default_enabled: false,
947    },
948    FeatureSpec {
949        id: Feature::LocalThreadStoreCompression,
950        key: "local_thread_store_compression",
951        stage: Stage::UnderDevelopment,
952        default_enabled: false,
953    },
954    FeatureSpec {
955        id: Feature::Chronicle,
956        key: "chronicle",
957        stage: Stage::UnderDevelopment,
958        default_enabled: false,
959    },
960    FeatureSpec {
961        id: Feature::ApplyPatchFreeform,
962        key: "apply_patch_freeform",
963        stage: Stage::Removed,
964        default_enabled: false,
965    },
966    FeatureSpec {
967        id: Feature::ApplyPatchStreamingEvents,
968        key: "apply_patch_streaming_events",
969        stage: Stage::UnderDevelopment,
970        default_enabled: false,
971    },
972    FeatureSpec {
973        id: Feature::ExecPermissionApprovals,
974        key: "exec_permission_approvals",
975        stage: Stage::UnderDevelopment,
976        default_enabled: false,
977    },
978    FeatureSpec {
979        id: Feature::CodexHooks,
980        key: "hooks",
981        stage: Stage::Stable,
982        default_enabled: true,
983    },
984    FeatureSpec {
985        id: Feature::RequestPermissionsTool,
986        key: "request_permissions_tool",
987        stage: Stage::UnderDevelopment,
988        default_enabled: false,
989    },
990    FeatureSpec {
991        id: Feature::UseLinuxSandboxBwrap,
992        key: "use_linux_sandbox_bwrap",
993        stage: Stage::Removed,
994        default_enabled: false,
995    },
996    FeatureSpec {
997        id: Feature::UseLegacyLandlock,
998        key: "use_legacy_landlock",
999        stage: Stage::Deprecated,
1000        default_enabled: false,
1001    },
1002    FeatureSpec {
1003        id: Feature::RequestRule,
1004        key: "request_rule",
1005        stage: Stage::Removed,
1006        default_enabled: false,
1007    },
1008    FeatureSpec {
1009        id: Feature::WindowsSandbox,
1010        key: "experimental_windows_sandbox",
1011        stage: Stage::Removed,
1012        default_enabled: false,
1013    },
1014    FeatureSpec {
1015        id: Feature::WindowsSandboxElevated,
1016        key: "elevated_windows_sandbox",
1017        stage: Stage::Removed,
1018        default_enabled: false,
1019    },
1020    FeatureSpec {
1021        id: Feature::RemoteModels,
1022        key: "remote_models",
1023        stage: Stage::Removed,
1024        default_enabled: false,
1025    },
1026    FeatureSpec {
1027        id: Feature::EnableRequestCompression,
1028        key: "enable_request_compression",
1029        stage: Stage::Stable,
1030        default_enabled: true,
1031    },
1032    FeatureSpec {
1033        id: Feature::NetworkProxy,
1034        key: "network_proxy",
1035        stage: Stage::Experimental {
1036            name: "Network proxy",
1037            menu_description: "Apply network proxy restrictions to sandboxed sessions that already have network access.",
1038            announcement: "NEW: Network proxy can now be enabled from /experimental. Restart Codex after enabling it.",
1039        },
1040        default_enabled: false,
1041    },
1042    FeatureSpec {
1043        id: Feature::RespectSystemProxy,
1044        key: "respect_system_proxy",
1045        stage: Stage::UnderDevelopment,
1046        default_enabled: false,
1047    },
1048    FeatureSpec {
1049        id: Feature::Collab,
1050        key: "multi_agent",
1051        stage: Stage::Stable,
1052        default_enabled: true,
1053    },
1054    FeatureSpec {
1055        id: Feature::MultiAgentV2,
1056        key: "multi_agent_v2",
1057        stage: Stage::Stable,
1058        default_enabled: false,
1059    },
1060    FeatureSpec {
1061        id: Feature::MultiAgentMode,
1062        key: "multi_agent_mode",
1063        stage: Stage::Removed,
1064        default_enabled: false,
1065    },
1066    FeatureSpec {
1067        id: Feature::SpawnCsv,
1068        key: "enable_fanout",
1069        stage: Stage::Removed,
1070        default_enabled: false,
1071    },
1072    FeatureSpec {
1073        id: Feature::Apps,
1074        key: "apps",
1075        stage: Stage::Stable,
1076        default_enabled: true,
1077    },
1078    FeatureSpec {
1079        id: Feature::EnableMcpApps,
1080        key: "enable_mcp_apps",
1081        stage: Stage::UnderDevelopment,
1082        default_enabled: false,
1083    },
1084    FeatureSpec {
1085        id: Feature::AppsMcpPathOverride,
1086        key: "apps_mcp_path_override",
1087        stage: Stage::Removed,
1088        default_enabled: false,
1089    },
1090    FeatureSpec {
1091        id: Feature::ToolSearch,
1092        key: "tool_search",
1093        stage: Stage::Removed,
1094        default_enabled: false,
1095    },
1096    FeatureSpec {
1097        id: Feature::ToolSearchAlwaysDeferMcpTools,
1098        key: "tool_search_always_defer_mcp_tools",
1099        stage: Stage::Removed,
1100        default_enabled: true,
1101    },
1102    FeatureSpec {
1103        id: Feature::NonPrefixedMcpToolNames,
1104        key: "non_prefixed_mcp_tool_names",
1105        stage: Stage::UnderDevelopment,
1106        default_enabled: false,
1107    },
1108    FeatureSpec {
1109        id: Feature::UnavailableDummyTools,
1110        key: "unavailable_dummy_tools",
1111        stage: Stage::Removed,
1112        default_enabled: false,
1113    },
1114    FeatureSpec {
1115        id: Feature::ToolSuggest,
1116        key: "tool_suggest",
1117        stage: Stage::Stable,
1118        default_enabled: true,
1119    },
1120    FeatureSpec {
1121        id: Feature::Plugins,
1122        key: "plugins",
1123        stage: Stage::Stable,
1124        default_enabled: true,
1125    },
1126    FeatureSpec {
1127        id: Feature::ExecutorCapabilityDiscovery,
1128        key: "executor_capability_discovery",
1129        stage: Stage::UnderDevelopment,
1130        default_enabled: false,
1131    },
1132    FeatureSpec {
1133        id: Feature::PluginHooks,
1134        key: "plugin_hooks",
1135        stage: Stage::Removed,
1136        default_enabled: false,
1137    },
1138    FeatureSpec {
1139        id: Feature::InAppBrowser,
1140        key: "in_app_browser",
1141        stage: Stage::Stable,
1142        default_enabled: true,
1143    },
1144    FeatureSpec {
1145        id: Feature::BrowserUse,
1146        key: "browser_use",
1147        stage: Stage::Stable,
1148        default_enabled: true,
1149    },
1150    FeatureSpec {
1151        id: Feature::BrowserUseFullCdpAccess,
1152        key: "browser_use_full_cdp_access",
1153        stage: Stage::Stable,
1154        default_enabled: true,
1155    },
1156    FeatureSpec {
1157        id: Feature::BrowserUseExternal,
1158        key: "browser_use_external",
1159        stage: Stage::Stable,
1160        default_enabled: true,
1161    },
1162    FeatureSpec {
1163        id: Feature::ComputerUse,
1164        key: "computer_use",
1165        stage: Stage::Stable,
1166        default_enabled: true,
1167    },
1168    FeatureSpec {
1169        id: Feature::RemotePlugin,
1170        key: "remote_plugin",
1171        stage: Stage::Stable,
1172        default_enabled: true,
1173    },
1174    FeatureSpec {
1175        id: Feature::PluginSharing,
1176        key: "plugin_sharing",
1177        stage: Stage::Stable,
1178        default_enabled: true,
1179    },
1180    FeatureSpec {
1181        id: Feature::ExternalMigration,
1182        key: "external_migration",
1183        stage: Stage::Removed,
1184        default_enabled: false,
1185    },
1186    FeatureSpec {
1187        id: Feature::ImageGeneration,
1188        key: "image_generation",
1189        stage: Stage::Stable,
1190        default_enabled: true,
1191    },
1192    FeatureSpec {
1193        id: Feature::ResizeAllImages,
1194        key: "resize_all_images",
1195        stage: Stage::Removed,
1196        default_enabled: true,
1197    },
1198    FeatureSpec {
1199        id: Feature::ItemIds,
1200        key: "item_ids",
1201        stage: Stage::Removed,
1202        default_enabled: true,
1203    },
1204    FeatureSpec {
1205        id: Feature::ConcurrentReasoningSummaries,
1206        key: "concurrent_reasoning_summaries",
1207        stage: Stage::UnderDevelopment,
1208        default_enabled: false,
1209    },
1210    FeatureSpec {
1211        id: Feature::SkillMcpDependencyInstall,
1212        key: "skill_mcp_dependency_install",
1213        stage: Stage::Stable,
1214        default_enabled: true,
1215    },
1216    FeatureSpec {
1217        id: Feature::SkillSearch,
1218        key: "skill_search",
1219        stage: Stage::Stable,
1220        default_enabled: true,
1221    },
1222    FeatureSpec {
1223        id: Feature::SkillEnvVarDependencyPrompt,
1224        key: "skill_env_var_dependency_prompt",
1225        stage: Stage::Removed,
1226        default_enabled: false,
1227    },
1228    FeatureSpec {
1229        id: Feature::MentionsV2,
1230        key: "mentions_v2",
1231        stage: Stage::Stable,
1232        default_enabled: true,
1233    },
1234    FeatureSpec {
1235        id: Feature::Steer,
1236        key: "steer",
1237        stage: Stage::Removed,
1238        default_enabled: true,
1239    },
1240    FeatureSpec {
1241        id: Feature::DefaultModeRequestUserInput,
1242        key: "default_mode_request_user_input",
1243        stage: Stage::UnderDevelopment,
1244        default_enabled: false,
1245    },
1246    FeatureSpec {
1247        id: Feature::TerminalVisualizationInstructions,
1248        key: "terminal_visualization_instructions",
1249        stage: Stage::UnderDevelopment,
1250        default_enabled: false,
1251    },
1252    FeatureSpec {
1253        id: Feature::GuardianApproval,
1254        key: "guardian_approval",
1255        stage: Stage::Stable,
1256        default_enabled: true,
1257    },
1258    FeatureSpec {
1259        id: Feature::Goals,
1260        key: "goals",
1261        stage: Stage::Stable,
1262        default_enabled: true,
1263    },
1264    FeatureSpec {
1265        id: Feature::TokenBudget,
1266        key: "token_budget",
1267        stage: Stage::UnderDevelopment,
1268        default_enabled: false,
1269    },
1270    FeatureSpec {
1271        id: Feature::RolloutBudget,
1272        key: "rollout_budget",
1273        stage: Stage::UnderDevelopment,
1274        default_enabled: false,
1275    },
1276    FeatureSpec {
1277        id: Feature::CurrentTimeReminder,
1278        key: "current_time_reminder",
1279        stage: Stage::UnderDevelopment,
1280        default_enabled: false,
1281    },
1282    FeatureSpec {
1283        id: Feature::CollaborationModes,
1284        key: "collaboration_modes",
1285        stage: Stage::Removed,
1286        default_enabled: true,
1287    },
1288    FeatureSpec {
1289        id: Feature::ToolCallMcpElicitation,
1290        key: "tool_call_mcp_elicitation",
1291        stage: Stage::Stable,
1292        default_enabled: true,
1293    },
1294    FeatureSpec {
1295        id: Feature::AuthElicitation,
1296        key: "auth_elicitation",
1297        stage: Stage::Stable,
1298        default_enabled: true,
1299    },
1300    FeatureSpec {
1301        id: Feature::Personality,
1302        key: "personality",
1303        stage: Stage::Stable,
1304        default_enabled: true,
1305    },
1306    FeatureSpec {
1307        id: Feature::Artifact,
1308        key: "artifact",
1309        stage: Stage::UnderDevelopment,
1310        default_enabled: false,
1311    },
1312    FeatureSpec {
1313        id: Feature::FastMode,
1314        key: "fast_mode",
1315        stage: Stage::Stable,
1316        default_enabled: true,
1317    },
1318    FeatureSpec {
1319        id: Feature::RealtimeConversation,
1320        key: "realtime_conversation",
1321        stage: Stage::UnderDevelopment,
1322        default_enabled: false,
1323    },
1324    FeatureSpec {
1325        id: Feature::RemoteControl,
1326        key: "remote_control",
1327        stage: Stage::Removed,
1328        default_enabled: false,
1329    },
1330    FeatureSpec {
1331        id: Feature::ImageDetailOriginal,
1332        key: "image_detail_original",
1333        stage: Stage::Removed,
1334        default_enabled: false,
1335    },
1336    FeatureSpec {
1337        id: Feature::TuiAppServer,
1338        key: "tui_app_server",
1339        stage: Stage::Removed,
1340        default_enabled: true,
1341    },
1342    FeatureSpec {
1343        id: Feature::PreventIdleSleep,
1344        key: "prevent_idle_sleep",
1345        stage: if cfg!(any(
1346            target_os = "macos",
1347            target_os = "linux",
1348            target_os = "windows"
1349        )) {
1350            Stage::Experimental {
1351                name: "Prevent sleep while running",
1352                menu_description: "Keep your computer awake while Codex is running a thread.",
1353                announcement: "NEW: Prevent sleep while running is now available in /experimental.",
1354            }
1355        } else {
1356            Stage::UnderDevelopment
1357        },
1358        default_enabled: false,
1359    },
1360    FeatureSpec {
1361        id: Feature::WorkspaceOwnerUsageNudge,
1362        key: "workspace_owner_usage_nudge",
1363        stage: Stage::Removed,
1364        default_enabled: false,
1365    },
1366    FeatureSpec {
1367        id: Feature::ResponsesWebsockets,
1368        key: "responses_websockets",
1369        stage: Stage::Removed,
1370        default_enabled: false,
1371    },
1372    FeatureSpec {
1373        id: Feature::ResponsesWebsocketsV2,
1374        key: "responses_websockets_v2",
1375        stage: Stage::Removed,
1376        default_enabled: false,
1377    },
1378    FeatureSpec {
1379        id: Feature::RemoteCompactionV2,
1380        key: "remote_compaction_v2",
1381        stage: Stage::Stable,
1382        default_enabled: true,
1383    },
1384    FeatureSpec {
1385        id: Feature::UseAgentIdentity,
1386        key: "use_agent_identity",
1387        stage: Stage::UnderDevelopment,
1388        default_enabled: false,
1389    },
1390    FeatureSpec {
1391        id: Feature::WorkspaceDependencies,
1392        key: "workspace_dependencies",
1393        stage: Stage::Stable,
1394        default_enabled: true,
1395    },
1396];
1397
1398pub fn unstable_features_warning_event(
1399    effective_features: Option<&Table>,
1400    suppress_unstable_features_warning: bool,
1401    features: &Features,
1402    config_path: &str,
1403) -> Option<Event> {
1404    if suppress_unstable_features_warning {
1405        return None;
1406    }
1407
1408    let mut under_development_feature_keys = Vec::new();
1409    if let Some(table) = effective_features {
1410        for (key, value) in table {
1411            let is_enabled = value.as_bool() == Some(true)
1412                || value
1413                    .as_table()
1414                    .and_then(|table| table.get("enabled"))
1415                    .and_then(toml::Value::as_bool)
1416                    == Some(true);
1417            if !is_enabled {
1418                continue;
1419            }
1420            let Some(spec) = FEATURES.iter().find(|spec| spec.key == key.as_str()) else {
1421                continue;
1422            };
1423            if !features.enabled(spec.id) {
1424                continue;
1425            }
1426            if matches!(spec.stage, Stage::UnderDevelopment) {
1427                under_development_feature_keys.push(spec.key.to_string());
1428            }
1429        }
1430    }
1431
1432    if under_development_feature_keys.is_empty() {
1433        return None;
1434    }
1435
1436    under_development_feature_keys.sort();
1437    let under_development_feature_keys = under_development_feature_keys.join(", ");
1438    let message = format!(
1439        "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}."
1440    );
1441    Some(Event {
1442        id: String::new(),
1443        msg: EventMsg::Warning(WarningEvent { message }),
1444    })
1445}
1446
1447#[cfg(test)]
1448mod tests;