Warn users on enabling underdevelopment features (#9954)

<img width="938" height="73" alt="image"
src="https://github.com/user-attachments/assets/a2d5ac46-92c5-4828-b35e-0965c30cdf36"
/>
This commit is contained in:
Ahmed Ibrahim
2026-01-26 17:58:05 -08:00
committed by GitHub
parent a641a6427c
commit c900de271a
6 changed files with 167 additions and 0 deletions

View File

@@ -1465,6 +1465,10 @@
],
"description": "User-level skill config entries keyed by SKILL.md path."
},
"suppress_unstable_features_warning": {
"description": "Suppress warnings about unstable (under development) features.",
"type": "boolean"
},
"tool_output_token_limit": {
"description": "Token budget applied when storing tool/function outputs in the context manager.",
"format": "uint",

View File

@@ -22,6 +22,7 @@ use crate::connectors;
use crate::exec_policy::ExecPolicyManager;
use crate::features::Feature;
use crate::features::Features;
use crate::features::maybe_push_unstable_features_warning;
use crate::models_manager::manager::ModelsManager;
use crate::parse_command::parse_command;
use crate::parse_turn_item;
@@ -754,6 +755,7 @@ impl Session {
});
}
maybe_push_chat_wire_api_deprecation(&config, &mut post_session_configured_events);
maybe_push_unstable_features_warning(&config, &mut post_session_configured_events);
let auth = auth.as_ref();
let otel_manager = OtelManager::new(

View File

@@ -316,6 +316,9 @@ pub struct Config {
/// Centralized feature flags; source of truth for feature gating.
pub features: Features,
/// When `true`, suppress warnings about unstable (under development) features.
pub suppress_unstable_features_warning: bool,
/// The active profile name used to derive this `Config` (if any).
pub active_profile: Option<String>,
@@ -906,6 +909,9 @@ pub struct ConfigToml {
#[schemars(schema_with = "crate::config::schema::features_schema")]
pub features: Option<FeaturesToml>,
/// Suppress warnings about unstable (under development) features.
pub suppress_unstable_features_warning: Option<bool>,
/// Settings for ghost snapshots (used for undo).
#[serde(default)]
pub ghost_snapshot: Option<GhostSnapshotToml>,
@@ -1564,6 +1570,9 @@ impl Config {
use_experimental_unified_exec_tool,
ghost_snapshot,
features,
suppress_unstable_features_warning: cfg
.suppress_unstable_features_warning
.unwrap_or(false),
active_profile: active_profile_name,
active_project,
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
@@ -3732,6 +3741,7 @@ model_verbosity = "high"
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
suppress_unstable_features_warning: false,
active_profile: Some("o3".to_string()),
active_project: ProjectConfig { trust_level: None },
windows_wsl_setup_acknowledged: false,
@@ -3814,6 +3824,7 @@ model_verbosity = "high"
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
suppress_unstable_features_warning: false,
active_profile: Some("gpt3".to_string()),
active_project: ProjectConfig { trust_level: None },
windows_wsl_setup_acknowledged: false,
@@ -3911,6 +3922,7 @@ model_verbosity = "high"
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
suppress_unstable_features_warning: false,
active_profile: Some("zdr".to_string()),
active_project: ProjectConfig { trust_level: None },
windows_wsl_setup_acknowledged: false,
@@ -3994,6 +4006,7 @@ model_verbosity = "high"
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
suppress_unstable_features_warning: false,
active_profile: Some("gpt5".to_string()),
active_project: ProjectConfig { trust_level: None },
windows_wsl_setup_acknowledged: false,

View File

@@ -5,14 +5,20 @@
//! booleans through multiple types, call sites consult a single `Features`
//! container attached to `Config`.
use crate::config::CONFIG_TOML_FILE;
use crate::config::Config;
use crate::config::ConfigToml;
use crate::config::profile::ConfigProfile;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::WarningEvent;
use codex_otel::OtelManager;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use toml::Value as TomlValue;
mod legacy;
pub(crate) use legacy::LegacyFeatureToggles;
@@ -466,3 +472,54 @@ pub const FEATURES: &[FeatureSpec] = &[
default_enabled: false,
},
];
/// Push a warning event if any under-development features are enabled.
pub fn maybe_push_unstable_features_warning(
config: &Config,
post_session_configured_events: &mut Vec<Event>,
) {
if config.suppress_unstable_features_warning {
return;
}
let mut under_development_feature_keys = Vec::new();
if let Some(table) = config
.config_layer_stack
.effective_config()
.get("features")
.and_then(TomlValue::as_table)
{
for (key, value) in table {
if value.as_bool() != Some(true) {
continue;
}
let Some(spec) = FEATURES.iter().find(|spec| spec.key == key.as_str()) else {
continue;
};
if !config.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;
}
let under_development_feature_keys = under_development_feature_keys.join(", ");
let config_path = config
.codex_home
.join(CONFIG_TOML_FILE)
.display()
.to_string();
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}."
);
post_session_configured_events.push(Event {
id: "".to_owned(),
msg: EventMsg::Warning(WarningEvent { message }),
});
}

View File

@@ -74,6 +74,7 @@ mod tools;
mod truncation;
mod undo;
mod unified_exec;
mod unstable_features_warning;
mod user_notification;
mod user_shell_cmd;
mod view_image;

View File

@@ -0,0 +1,90 @@
#![allow(clippy::unwrap_used, clippy::expect_used)]
use codex_core::AuthManager;
use codex_core::CodexAuth;
use codex_core::NewThread;
use codex_core::ThreadManager;
use codex_core::config::CONFIG_TOML_FILE;
use codex_core::features::Feature;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InitialHistory;
use codex_core::protocol::WarningEvent;
use codex_utils_absolute_path::AbsolutePathBuf;
use core::time::Duration;
use core_test_support::load_default_config_for_test;
use core_test_support::wait_for_event;
use tempfile::TempDir;
use tokio::time::timeout;
use toml::toml;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn emits_warning_when_unstable_features_enabled_via_config() {
let home = TempDir::new().expect("tempdir");
let mut config = load_default_config_for_test(&home).await;
config.features.enable(Feature::ChildAgentsMd);
let user_config_path =
AbsolutePathBuf::from_absolute_path(config.codex_home.join(CONFIG_TOML_FILE))
.expect("absolute user config path");
config.config_layer_stack = config.config_layer_stack.with_user_config(
&user_config_path,
toml! { features = { child_agents_md = true } }.into(),
);
let thread_manager = ThreadManager::with_models_provider(
CodexAuth::from_api_key("test"),
config.model_provider.clone(),
);
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
let NewThread {
thread: conversation,
..
} = thread_manager
.resume_thread_with_history(config, InitialHistory::New, auth_manager)
.await
.expect("spawn conversation");
let warning = wait_for_event(&conversation, |ev| matches!(ev, EventMsg::Warning(_))).await;
let EventMsg::Warning(WarningEvent { message }) = warning else {
panic!("expected warning event");
};
assert!(message.contains("child_agents_md"));
assert!(message.contains("Under-development features enabled"));
assert!(message.contains("suppress_unstable_features_warning = true"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn suppresses_warning_when_configured() {
let home = TempDir::new().expect("tempdir");
let mut config = load_default_config_for_test(&home).await;
config.features.enable(Feature::ChildAgentsMd);
config.suppress_unstable_features_warning = true;
let user_config_path =
AbsolutePathBuf::from_absolute_path(config.codex_home.join(CONFIG_TOML_FILE))
.expect("absolute user config path");
config.config_layer_stack = config.config_layer_stack.with_user_config(
&user_config_path,
toml! { features = { child_agents_md = true } }.into(),
);
let thread_manager = ThreadManager::with_models_provider(
CodexAuth::from_api_key("test"),
config.model_provider.clone(),
);
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
let NewThread {
thread: conversation,
..
} = thread_manager
.resume_thread_with_history(config, InitialHistory::New, auth_manager)
.await
.expect("spawn conversation");
let warning = timeout(
Duration::from_millis(150),
wait_for_event(&conversation, |ev| matches!(ev, EventMsg::Warning(_))),
)
.await;
assert!(warning.is_err());
}