mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
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:
@@ -1465,6 +1465,10 @@
|
|||||||
],
|
],
|
||||||
"description": "User-level skill config entries keyed by SKILL.md path."
|
"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": {
|
"tool_output_token_limit": {
|
||||||
"description": "Token budget applied when storing tool/function outputs in the context manager.",
|
"description": "Token budget applied when storing tool/function outputs in the context manager.",
|
||||||
"format": "uint",
|
"format": "uint",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use crate::connectors;
|
|||||||
use crate::exec_policy::ExecPolicyManager;
|
use crate::exec_policy::ExecPolicyManager;
|
||||||
use crate::features::Feature;
|
use crate::features::Feature;
|
||||||
use crate::features::Features;
|
use crate::features::Features;
|
||||||
|
use crate::features::maybe_push_unstable_features_warning;
|
||||||
use crate::models_manager::manager::ModelsManager;
|
use crate::models_manager::manager::ModelsManager;
|
||||||
use crate::parse_command::parse_command;
|
use crate::parse_command::parse_command;
|
||||||
use crate::parse_turn_item;
|
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_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 auth = auth.as_ref();
|
||||||
let otel_manager = OtelManager::new(
|
let otel_manager = OtelManager::new(
|
||||||
|
|||||||
@@ -316,6 +316,9 @@ pub struct Config {
|
|||||||
/// Centralized feature flags; source of truth for feature gating.
|
/// Centralized feature flags; source of truth for feature gating.
|
||||||
pub features: Features,
|
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).
|
/// The active profile name used to derive this `Config` (if any).
|
||||||
pub active_profile: Option<String>,
|
pub active_profile: Option<String>,
|
||||||
|
|
||||||
@@ -906,6 +909,9 @@ pub struct ConfigToml {
|
|||||||
#[schemars(schema_with = "crate::config::schema::features_schema")]
|
#[schemars(schema_with = "crate::config::schema::features_schema")]
|
||||||
pub features: Option<FeaturesToml>,
|
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).
|
/// Settings for ghost snapshots (used for undo).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ghost_snapshot: Option<GhostSnapshotToml>,
|
pub ghost_snapshot: Option<GhostSnapshotToml>,
|
||||||
@@ -1564,6 +1570,9 @@ impl Config {
|
|||||||
use_experimental_unified_exec_tool,
|
use_experimental_unified_exec_tool,
|
||||||
ghost_snapshot,
|
ghost_snapshot,
|
||||||
features,
|
features,
|
||||||
|
suppress_unstable_features_warning: cfg
|
||||||
|
.suppress_unstable_features_warning
|
||||||
|
.unwrap_or(false),
|
||||||
active_profile: active_profile_name,
|
active_profile: active_profile_name,
|
||||||
active_project,
|
active_project,
|
||||||
windows_wsl_setup_acknowledged: cfg.windows_wsl_setup_acknowledged.unwrap_or(false),
|
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,
|
use_experimental_unified_exec_tool: false,
|
||||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||||
features: Features::with_defaults(),
|
features: Features::with_defaults(),
|
||||||
|
suppress_unstable_features_warning: false,
|
||||||
active_profile: Some("o3".to_string()),
|
active_profile: Some("o3".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
@@ -3814,6 +3824,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_unified_exec_tool: false,
|
use_experimental_unified_exec_tool: false,
|
||||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||||
features: Features::with_defaults(),
|
features: Features::with_defaults(),
|
||||||
|
suppress_unstable_features_warning: false,
|
||||||
active_profile: Some("gpt3".to_string()),
|
active_profile: Some("gpt3".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
@@ -3911,6 +3922,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_unified_exec_tool: false,
|
use_experimental_unified_exec_tool: false,
|
||||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||||
features: Features::with_defaults(),
|
features: Features::with_defaults(),
|
||||||
|
suppress_unstable_features_warning: false,
|
||||||
active_profile: Some("zdr".to_string()),
|
active_profile: Some("zdr".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
@@ -3994,6 +4006,7 @@ model_verbosity = "high"
|
|||||||
use_experimental_unified_exec_tool: false,
|
use_experimental_unified_exec_tool: false,
|
||||||
ghost_snapshot: GhostSnapshotConfig::default(),
|
ghost_snapshot: GhostSnapshotConfig::default(),
|
||||||
features: Features::with_defaults(),
|
features: Features::with_defaults(),
|
||||||
|
suppress_unstable_features_warning: false,
|
||||||
active_profile: Some("gpt5".to_string()),
|
active_profile: Some("gpt5".to_string()),
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
windows_wsl_setup_acknowledged: false,
|
windows_wsl_setup_acknowledged: false,
|
||||||
|
|||||||
@@ -5,14 +5,20 @@
|
|||||||
//! booleans through multiple types, call sites consult a single `Features`
|
//! booleans through multiple types, call sites consult a single `Features`
|
||||||
//! container attached to `Config`.
|
//! container attached to `Config`.
|
||||||
|
|
||||||
|
use crate::config::CONFIG_TOML_FILE;
|
||||||
|
use crate::config::Config;
|
||||||
use crate::config::ConfigToml;
|
use crate::config::ConfigToml;
|
||||||
use crate::config::profile::ConfigProfile;
|
use crate::config::profile::ConfigProfile;
|
||||||
|
use crate::protocol::Event;
|
||||||
|
use crate::protocol::EventMsg;
|
||||||
|
use crate::protocol::WarningEvent;
|
||||||
use codex_otel::OtelManager;
|
use codex_otel::OtelManager;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
use toml::Value as TomlValue;
|
||||||
|
|
||||||
mod legacy;
|
mod legacy;
|
||||||
pub(crate) use legacy::LegacyFeatureToggles;
|
pub(crate) use legacy::LegacyFeatureToggles;
|
||||||
@@ -466,3 +472,54 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
default_enabled: false,
|
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 }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ mod tools;
|
|||||||
mod truncation;
|
mod truncation;
|
||||||
mod undo;
|
mod undo;
|
||||||
mod unified_exec;
|
mod unified_exec;
|
||||||
|
mod unstable_features_warning;
|
||||||
mod user_notification;
|
mod user_notification;
|
||||||
mod user_shell_cmd;
|
mod user_shell_cmd;
|
||||||
mod view_image;
|
mod view_image;
|
||||||
|
|||||||
90
codex-rs/core/tests/suite/unstable_features_warning.rs
Normal file
90
codex-rs/core/tests/suite/unstable_features_warning.rs
Normal 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());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user