1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Stage {
37 UnderDevelopment,
39 Experimental {
41 name: &'static str,
42 menu_description: &'static str,
43 announcement: &'static str,
44 },
45 Stable,
47 Deprecated,
49 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
83pub enum Feature {
84 ShellTool,
87 CodexHooks,
89 SecretAuthStorage,
91
92 CodeMode,
95 CodeModeBufferedExec,
97 CodeModeHost,
99 CodeModeOnly,
101 UnifiedExec,
103 ShellZshFork,
105 UnifiedExecZshFork,
111 TerminalResizeReflow,
113 TerminalVisualizationInstructions,
115 ApplyPatchStreamingEvents,
117 ExecPermissionApprovals,
119 RequestPermissionsTool,
121 WebSearchRequest,
123 WebSearchCached,
126 StandaloneWebSearch,
128 UseLegacyLandlock,
131 ShellSnapshot,
133 DeferredExecutor,
135 RuntimeMetrics,
137 MemoryTool,
139 ExternalAgentMemoryImport,
141 LocalThreadStoreCompression,
143 Chronicle,
145 EnableRequestCompression,
147 NetworkProxy,
149 RespectSystemProxy,
151 Collab,
153 MultiAgentV2,
155 MultiAgentMode,
157 SpawnCsv,
159 Apps,
161 EnableMcpApps,
163 AppsMcpPathOverride,
165 ToolSearch,
167 ToolSearchAlwaysDeferMcpTools,
169 NonPrefixedMcpToolNames,
171 ToolSuggest,
173 Plugins,
175 ExecutorCapabilityDiscovery,
177 PluginHooks,
179 InAppBrowser,
183 BrowserUse,
187 BrowserUseFullCdpAccess,
191 BrowserUseExternal,
195 ComputerUse,
199 RemotePlugin,
201 PluginSharing,
203 ExternalMigration,
205 ImageGeneration,
207 ResizeAllImages,
209 ItemIds,
211 ConcurrentReasoningSummaries,
213 SkillMcpDependencyInstall,
215 SkillSearch,
217 SkillEnvVarDependencyPrompt,
219 MentionsV2,
221 DefaultModeRequestUserInput,
223 GuardianApproval,
225 Goals,
227 TokenBudget,
229 RolloutBudget,
231 CurrentTimeReminder,
233 ToolCallMcpElicitation,
235 AuthElicitation,
237 Personality,
239 Artifact,
241 FastMode,
243 RealtimeConversation,
245 PreventIdleSleep,
247 RemoteCompactionV2,
249 UseAgentIdentity,
251 WorkspaceDependencies,
253
254 GhostCommit,
258 JsRepl,
260 JsReplToolsOnly,
262 SearchTool,
264 UseLinuxSandboxBwrap,
267 RequestRule,
269 WindowsSandbox,
271 WindowsSandboxElevated,
273 RemoteModels,
275 CodexGitCommit,
277 Sqlite,
279 ApplyPatchFreeform,
281 UnavailableDummyTools,
283 Steer,
286 CollaborationModes,
289 RemoteControl,
291 ImageDetailOriginal,
294 TuiAppServer,
296 WorkspaceOwnerUsageNudge,
299 ResponsesWebsockets,
301 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#[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 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 1,
440 &[
441 ("feature", feature.key),
442 ("value", &self.enabled(feature.id).to_string()),
443 ],
444 );
445 }
446 }
447 }
448
449 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
620pub 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
637pub fn is_known_feature_key(key: &str) -> bool {
639 feature_for_key(key).is_some()
640}
641
642#[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 #[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 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#[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
786pub trait FeatureConfig {
789 fn enabled(&self) -> Option<bool>;
790 fn set_enabled(&mut self, enabled: bool);
791}
792
793#[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 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;