mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +00:00
Add configurable gating for the sleep tool (#41243)
## What changed - Add a stable `sleep_tool` feature that can enable or disable registration of the built-in sleep tool independently of the clock tool. - Support structured `features.sleep_tool` configuration with `model_driven` and `always_on` modes. The default `model_driven` mode preserves the existing model and `current_time_reminder` behavior, while `always_on` registers sleep whenever the feature is enabled. - Preserve nested sleep-tool configuration across CLI overrides, config merges, and feature toggles, and expose the settings in the generated config schema. ## Testing - Cover feature-map overrides, both selection modes, legacy clock settings, config merging and editing, and invalid mode rejection. GitOrigin-RevId: 18eadc582f8a4445958c29d063e5be495276703e
This commit is contained in:
1 parent
41d3dc56a0
commit
ec9620c231
12 files changed
+313
-14
No files matched your search
@@ -67,7 +67,10 @@ pub fn is_structured_feature_path<S: AsRef<str>>(path: &[S]) -> bool {
|
|||||||
};
|
};
|
||||||
|
|
||||||
features.as_ref() == "features"
|
features.as_ref() == "features"
|
||||||
&& matches!(feature.as_ref(), "multi_agent_v2" | "network_proxy")
|
&& matches!(
|
||||||
|
feature.as_ref(),
|
||||||
|
"multi_agent_v2" | "network_proxy" | "sleep_tool"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &mut Vec<String>) {
|
fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &mut Vec<String>) {
|
||||||
|
|||||||
@@ -255,6 +255,38 @@ fn multi_agent_v2_cli_overrides_preserve_boolean_and_nested_configuration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sleep_tool_overrides_preserve_disabled_state_and_mode() {
|
||||||
|
for feature_path in ["features", "profiles.work.features"] {
|
||||||
|
let disabled = (
|
||||||
|
format!("{feature_path}.sleep_tool"),
|
||||||
|
TomlValue::Boolean(false),
|
||||||
|
);
|
||||||
|
let mode = (
|
||||||
|
format!("{feature_path}.sleep_tool.mode"),
|
||||||
|
TomlValue::String("always_on".to_string()),
|
||||||
|
);
|
||||||
|
let expected = parse_toml(&format!(
|
||||||
|
"[{feature_path}.sleep_tool]\nenabled = false\nmode = \"always_on\"\n",
|
||||||
|
));
|
||||||
|
for overrides in [vec![disabled.clone(), mode.clone()], vec![mode, disabled]] {
|
||||||
|
assert_eq!(crate::build_cli_overrides_layer(&overrides), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
let boolean_layer = parse_toml(&format!("[{feature_path}]\nsleep_tool = false\n"));
|
||||||
|
let table_layer = parse_toml(&format!(
|
||||||
|
"[{feature_path}.sleep_tool]\nmode = \"always_on\"\n",
|
||||||
|
));
|
||||||
|
for (mut base, overlay) in [
|
||||||
|
(boolean_layer.clone(), table_layer.clone()),
|
||||||
|
(table_layer, boolean_layer),
|
||||||
|
] {
|
||||||
|
merge_toml_values(&mut base, &overlay);
|
||||||
|
assert_eq!(base, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn network_proxy_feature_overrides_preserve_credential_broker_configuration() {
|
fn network_proxy_feature_overrides_preserve_credential_broker_configuration() {
|
||||||
let enabled = (
|
let enabled = (
|
||||||
|
|||||||
@@ -122,6 +122,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if feature.id == codex_features::Feature::SleepTool {
|
||||||
|
validation.properties.insert(
|
||||||
|
feature.key.to_string(),
|
||||||
|
schema_gen.subschema_for::<codex_features::FeatureToml<
|
||||||
|
codex_features::SleepToolConfigToml,
|
||||||
|
>>(),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if feature.id == codex_features::Feature::AppsMcpPathOverride {
|
if feature.id == codex_features::Feature::AppsMcpPathOverride {
|
||||||
validation.properties.insert(
|
validation.properties.insert(
|
||||||
feature.key.to_string(),
|
feature.key.to_string(),
|
||||||
|
|||||||
@@ -907,6 +907,9 @@
|
|||||||
"skip_host_skill_discovery": {
|
"skip_host_skill_discovery": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"sleep_tool": {
|
||||||
|
"$ref": "#/definitions/FeatureToml_for_SleepToolConfigToml"
|
||||||
|
},
|
||||||
"sqlite": {
|
"sqlite": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -1262,6 +1265,16 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"FeatureToml_for_SleepToolConfigToml": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/SleepToolConfigToml"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"FeatureToml_for_TokenBudgetConfigToml": {
|
"FeatureToml_for_TokenBudgetConfigToml": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3499,6 +3512,37 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SleepToolConfigToml": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"mode": {
|
||||||
|
"$ref": "#/definitions/SleepToolMode"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SleepToolMode": {
|
||||||
|
"description": "How the sleep tool is selected when its feature gate is enabled.",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"description": "Preserve the existing model and legacy clock configuration defaults.",
|
||||||
|
"enum": [
|
||||||
|
"model_driven"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Register sleep regardless of the model or legacy clock configuration.",
|
||||||
|
"enum": [
|
||||||
|
"always_on"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"ThreadStoreToml": {
|
"ThreadStoreToml": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
{
|
{
|
||||||
@@ -6063,6 +6107,9 @@
|
|||||||
"skip_host_skill_discovery": {
|
"skip_host_skill_discovery": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"sleep_tool": {
|
||||||
|
"$ref": "#/definitions/FeatureToml_for_SleepToolConfigToml"
|
||||||
|
},
|
||||||
"sqlite": {
|
"sqlite": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -136,6 +136,33 @@ fn multi_agent_v2_feature_toggle_preserves_nested_configuration() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sleep_tool_feature_toggle_preserves_mode() {
|
||||||
|
let tmp = tempdir().expect("tmpdir");
|
||||||
|
let codex_home = tmp.path();
|
||||||
|
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||||
|
std::fs::write(
|
||||||
|
&config_path,
|
||||||
|
"[features.sleep_tool]\nmode = \"always_on\"\n",
|
||||||
|
)
|
||||||
|
.expect("write config");
|
||||||
|
|
||||||
|
for enabled in [false, true] {
|
||||||
|
ConfigEditsBuilder::new(codex_home)
|
||||||
|
.set_feature_enabled("sleep_tool", enabled)
|
||||||
|
.apply_blocking()
|
||||||
|
.expect("toggle feature");
|
||||||
|
let actual: TomlValue =
|
||||||
|
toml::from_str(&std::fs::read_to_string(&config_path).expect("read config"))
|
||||||
|
.expect("parse config");
|
||||||
|
let expected: TomlValue = toml::from_str(&format!(
|
||||||
|
"[features.sleep_tool]\nenabled = {enabled}\nmode = \"always_on\"\n",
|
||||||
|
))
|
||||||
|
.expect("parse expected config");
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn network_proxy_feature_toggle_preserves_credential_broker_configuration() {
|
fn network_proxy_feature_toggle_preserves_credential_broker_configuration() {
|
||||||
let tmp = tempdir().expect("tmpdir");
|
let tmp = tempdir().expect("tmpdir");
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ use codex_features::Features;
|
|||||||
use codex_features::FeaturesToml;
|
use codex_features::FeaturesToml;
|
||||||
use codex_features::MultiAgentV2ConfigToml;
|
use codex_features::MultiAgentV2ConfigToml;
|
||||||
use codex_features::NetworkProxyConfigToml;
|
use codex_features::NetworkProxyConfigToml;
|
||||||
|
use codex_features::SleepToolMode;
|
||||||
use codex_features::TokenBudgetConfigToml;
|
use codex_features::TokenBudgetConfigToml;
|
||||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||||
use codex_http_client::HttpClientFactory;
|
use codex_http_client::HttpClientFactory;
|
||||||
@@ -1045,6 +1046,8 @@ pub struct Config {
|
|||||||
pub rollout_budget: Option<RolloutBudgetConfig>,
|
pub rollout_budget: Option<RolloutBudgetConfig>,
|
||||||
/// Current-time reminder and clock tool configuration, when enabled.
|
/// Current-time reminder and clock tool configuration, when enabled.
|
||||||
pub current_time_reminder: Option<CurrentTimeReminderConfig>,
|
pub current_time_reminder: Option<CurrentTimeReminderConfig>,
|
||||||
|
/// How the sleep tool is selected when its feature gate is enabled.
|
||||||
|
pub sleep_tool_mode: SleepToolMode,
|
||||||
|
|
||||||
/// Centralized feature flags; source of truth for feature gating.
|
/// Centralized feature flags; source of truth for feature gating.
|
||||||
pub features: ManagedFeatures,
|
pub features: ManagedFeatures,
|
||||||
@@ -3684,6 +3687,15 @@ impl Config {
|
|||||||
let token_budget = resolve_token_budget_config(&cfg, &features)?;
|
let token_budget = resolve_token_budget_config(&cfg, &features)?;
|
||||||
let rollout_budget = resolve_rollout_budget_config(&cfg, &features)?;
|
let rollout_budget = resolve_rollout_budget_config(&cfg, &features)?;
|
||||||
let current_time_reminder = resolve_current_time_reminder_config(&cfg, &features)?;
|
let current_time_reminder = resolve_current_time_reminder_config(&cfg, &features)?;
|
||||||
|
let sleep_tool_mode = cfg
|
||||||
|
.features
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|features| features.sleep_tool.as_ref())
|
||||||
|
.and_then(|feature| match feature {
|
||||||
|
FeatureToml::Enabled(_) => None,
|
||||||
|
FeatureToml::Config(config) => config.mode,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
|
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
|
||||||
|
|
||||||
let agent_roles =
|
let agent_roles =
|
||||||
@@ -4282,6 +4294,7 @@ impl Config {
|
|||||||
token_budget,
|
token_budget,
|
||||||
rollout_budget,
|
rollout_budget,
|
||||||
current_time_reminder,
|
current_time_reminder,
|
||||||
|
sleep_tool_mode,
|
||||||
features,
|
features,
|
||||||
suppress_unstable_features_warning: cfg
|
suppress_unstable_features_warning: cfg
|
||||||
.suppress_unstable_features_warning
|
.suppress_unstable_features_warning
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ use crate::tools::router::ToolRouter;
|
|||||||
use crate::tools::tool_namespaces_info::collect_tool_namespaces_info;
|
use crate::tools::tool_namespaces_info::collect_tool_namespaces_info;
|
||||||
use codex_extension_api::ExtensionData;
|
use codex_extension_api::ExtensionData;
|
||||||
use codex_features::Feature;
|
use codex_features::Feature;
|
||||||
|
use codex_features::SleepToolMode;
|
||||||
use codex_login::AuthManager;
|
use codex_login::AuthManager;
|
||||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||||
use codex_protocol::account::PlanType;
|
use codex_protocol::account::PlanType;
|
||||||
@@ -1153,23 +1154,31 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
|
|||||||
registry.add(GetContextRemainingHandler);
|
registry.add(GetContextRemainingHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if features.enabled(Feature::CurrentTimeReminder) {
|
let current_time_reminder_enabled = features.enabled(Feature::CurrentTimeReminder);
|
||||||
registry.add(CurrentTimeHandler);
|
let model_has_clock = context
|
||||||
if turn_context
|
|
||||||
.config
|
|
||||||
.current_time_reminder
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|config| config.sleep_tool)
|
|
||||||
{
|
|
||||||
registry.add(SleepHandler);
|
|
||||||
}
|
|
||||||
} else if context
|
|
||||||
.model_info
|
.model_info
|
||||||
.experimental_supported_tools
|
.experimental_supported_tools
|
||||||
.iter()
|
.iter()
|
||||||
.any(|tool| tool == "clock")
|
.any(|tool| tool == "clock");
|
||||||
{
|
if current_time_reminder_enabled || model_has_clock {
|
||||||
registry.add(CurrentTimeHandler);
|
registry.add(CurrentTimeHandler);
|
||||||
|
}
|
||||||
|
if features.enabled(Feature::SleepTool)
|
||||||
|
&& match turn_context.config.sleep_tool_mode {
|
||||||
|
SleepToolMode::AlwaysOn => true,
|
||||||
|
SleepToolMode::ModelDriven => {
|
||||||
|
if current_time_reminder_enabled {
|
||||||
|
turn_context
|
||||||
|
.config
|
||||||
|
.current_time_reminder
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|config| config.sleep_tool)
|
||||||
|
} else {
|
||||||
|
model_has_clock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
registry.add(SleepHandler);
|
registry.add(SleepHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::AtomicI64;
|
use std::sync::atomic::AtomicI64;
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
@@ -428,6 +429,117 @@ async fn system_time_source_adds_current_time_reminder(clock_setup: ClockSetup)
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_case(ReasoningEffort::High, true; "model_enabled")]
|
||||||
|
#[test_case(ReasoningEffort::High, false; "model_disabled")]
|
||||||
|
#[test_case(ReasoningEffort::Persistent, true; "persistent_enabled")]
|
||||||
|
#[test_case(ReasoningEffort::Persistent, false; "persistent_disabled")]
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn client_feature_map_can_disable_sleep_tool(
|
||||||
|
reasoning_effort: ReasoningEffort,
|
||||||
|
sleep_tool_enabled: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
let reminders_enabled = reasoning_effort == ReasoningEffort::Persistent;
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let responses = mount_sse_once(
|
||||||
|
&server,
|
||||||
|
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let test = test_codex()
|
||||||
|
.with_model_info_override("gpt-5.5", |model_info| {
|
||||||
|
model_info
|
||||||
|
.experimental_supported_tools
|
||||||
|
.push("clock".to_string());
|
||||||
|
})
|
||||||
|
.with_config(move |config| {
|
||||||
|
config.model_reasoning_effort = Some(reasoning_effort);
|
||||||
|
let mut features = config.features.get().clone();
|
||||||
|
features.apply_map(&BTreeMap::from([(
|
||||||
|
"sleep_tool".to_string(),
|
||||||
|
sleep_tool_enabled,
|
||||||
|
)]));
|
||||||
|
config
|
||||||
|
.features
|
||||||
|
.set(features)
|
||||||
|
.expect("test features should be allowed");
|
||||||
|
})
|
||||||
|
.build_with_auto_env(&server)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test.submit_text_turn("what time is it?").await?;
|
||||||
|
|
||||||
|
let request = responses.single_request();
|
||||||
|
assert_eq!(
|
||||||
|
["curr_time", "sleep"].map(|name| request.tool_by_name("clock", name).is_some()),
|
||||||
|
[true, sleep_tool_enabled]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
current_time_reminders(&request).is_empty(),
|
||||||
|
!reminders_enabled
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case("[features]\nsleep_tool = true", Some(true), [true, true]; "boolean_enabled")]
|
||||||
|
#[test_case("[features]\nsleep_tool = false", Some(true), [true, false]; "boolean_disabled")]
|
||||||
|
#[test_case("[features.sleep_tool]\nmode = 'always_on'", None, [false, true]; "always_on_without_model_clock")]
|
||||||
|
#[test_case("[features.sleep_tool]\nenabled = false\nmode = 'always_on'", Some(true), [true, false]; "disabled_overrides_always_on")]
|
||||||
|
#[test_case("[features.sleep_tool]\nmode = 'model_driven'", None, [false, false]; "model_driven_without_model_clock")]
|
||||||
|
#[test_case("[features.sleep_tool]\nmode = 'model_driven'", Some(false), [true, false]; "model_driven_preserves_legacy_disable")]
|
||||||
|
#[test_case("[features.sleep_tool]\nmode = 'always_on'", Some(false), [true, true]; "always_on_overrides_legacy_disable")]
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn sleep_tool_configuration_controls_registration(
|
||||||
|
sleep_config: &str,
|
||||||
|
legacy_sleep_tool: Option<bool>,
|
||||||
|
expected_tools: [bool; 2],
|
||||||
|
) -> Result<()> {
|
||||||
|
skip_if_no_network!(Ok(()));
|
||||||
|
|
||||||
|
let server = start_mock_server().await;
|
||||||
|
let responses = mount_sse_once(
|
||||||
|
&server,
|
||||||
|
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let mut config_toml = sleep_config.to_string();
|
||||||
|
if let Some(sleep_tool) = legacy_sleep_tool {
|
||||||
|
config_toml.push_str(&format!(
|
||||||
|
"\n[features.current_time_reminder]\nenabled = true\nsleep_tool = {sleep_tool}\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let test = test_codex()
|
||||||
|
.with_model_info_override("gpt-5.5", |model_info| {
|
||||||
|
model_info
|
||||||
|
.experimental_supported_tools
|
||||||
|
.retain(|tool| tool != "clock");
|
||||||
|
})
|
||||||
|
.with_pre_build_hook(move |home| {
|
||||||
|
std::fs::write(home.join("config.toml"), config_toml)
|
||||||
|
.expect("sleep tool configuration should be written");
|
||||||
|
})
|
||||||
|
.with_config(|config| {
|
||||||
|
config.model_reasoning_effort = Some(ReasoningEffort::High);
|
||||||
|
})
|
||||||
|
.build_with_auto_env(&server)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test.submit_text_turn("check the available clock tools")
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let request = responses.single_request();
|
||||||
|
assert_eq!(
|
||||||
|
["curr_time", "sleep"].map(|name| request.tool_by_name("clock", name).is_some()),
|
||||||
|
expected_tools
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
current_time_reminders(&request).is_empty(),
|
||||||
|
legacy_sleep_tool.is_none()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn current_time_reminder_is_refreshed_after_compaction() -> Result<()> {
|
async fn current_time_reminder_is_refreshed_after_compaction() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
|
|||||||
@@ -396,6 +396,32 @@ impl FeatureConfig for CurrentTimeReminderConfigToml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How the sleep tool is selected when its feature gate is enabled.
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SleepToolMode {
|
||||||
|
/// Preserve the existing model and legacy clock configuration defaults.
|
||||||
|
#[default]
|
||||||
|
ModelDriven,
|
||||||
|
/// Register sleep regardless of the model or legacy clock configuration.
|
||||||
|
AlwaysOn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct SleepToolConfigToml {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub mode: Option<SleepToolMode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureConfig for SleepToolConfigToml {
|
||||||
|
fn enabled(&self) -> Option<bool> {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub(crate) struct RemovedAppsMcpPathOverrideConfigToml {
|
pub(crate) struct RemovedAppsMcpPathOverrideConfigToml {
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ pub use feature_configs::NetworkProxyUnixSocketPermissionToml;
|
|||||||
pub use feature_configs::NonPrefixedMcpToolNamesConfigToml;
|
pub use feature_configs::NonPrefixedMcpToolNamesConfigToml;
|
||||||
use feature_configs::RemovedAppsMcpPathOverrideConfigToml;
|
use feature_configs::RemovedAppsMcpPathOverrideConfigToml;
|
||||||
pub use feature_configs::RolloutBudgetConfigToml;
|
pub use feature_configs::RolloutBudgetConfigToml;
|
||||||
|
pub use feature_configs::SleepToolConfigToml;
|
||||||
|
pub use feature_configs::SleepToolMode;
|
||||||
pub use feature_configs::TokenBudgetConfigToml;
|
pub use feature_configs::TokenBudgetConfigToml;
|
||||||
pub use feature_configs::ToolRegistryConfigToml;
|
pub use feature_configs::ToolRegistryConfigToml;
|
||||||
use legacy::LegacyFeatureToggles;
|
use legacy::LegacyFeatureToggles;
|
||||||
@@ -95,6 +97,8 @@ pub enum Feature {
|
|||||||
ShellTool,
|
ShellTool,
|
||||||
/// Enable the built-in local image viewer.
|
/// Enable the built-in local image viewer.
|
||||||
ViewImage,
|
ViewImage,
|
||||||
|
/// Allow registration of the built-in sleep tool.
|
||||||
|
SleepTool,
|
||||||
/// Enable Claude-style lifecycle hooks loaded from hooks.json files.
|
/// Enable Claude-style lifecycle hooks loaded from hooks.json files.
|
||||||
CodexHooks,
|
CodexHooks,
|
||||||
/// Store CLI auth in the encrypted local secrets backend when keyring storage is selected.
|
/// Store CLI auth in the encrypted local secrets backend when keyring storage is selected.
|
||||||
@@ -747,6 +751,8 @@ pub struct FeaturesToml {
|
|||||||
pub rollout_budget: Option<FeatureToml<RolloutBudgetConfigToml>>,
|
pub rollout_budget: Option<FeatureToml<RolloutBudgetConfigToml>>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub current_time_reminder: Option<FeatureToml<CurrentTimeReminderConfigToml>>,
|
pub current_time_reminder: Option<FeatureToml<CurrentTimeReminderConfigToml>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sleep_tool: Option<FeatureToml<SleepToolConfigToml>>,
|
||||||
#[serde(default, rename = "apps_mcp_path_override", skip_serializing)]
|
#[serde(default, rename = "apps_mcp_path_override", skip_serializing)]
|
||||||
#[schemars(skip)]
|
#[schemars(skip)]
|
||||||
removed_apps_mcp_path_override: Option<FeatureToml<RemovedAppsMcpPathOverrideConfigToml>>,
|
removed_apps_mcp_path_override: Option<FeatureToml<RemovedAppsMcpPathOverrideConfigToml>>,
|
||||||
@@ -801,6 +807,9 @@ impl FeaturesToml {
|
|||||||
if let Some(enabled) = self.network_proxy.as_ref().and_then(FeatureToml::enabled) {
|
if let Some(enabled) = self.network_proxy.as_ref().and_then(FeatureToml::enabled) {
|
||||||
entries.insert(Feature::NetworkProxy.key().to_string(), enabled);
|
entries.insert(Feature::NetworkProxy.key().to_string(), enabled);
|
||||||
}
|
}
|
||||||
|
if let Some(enabled) = self.sleep_tool.as_ref().and_then(FeatureToml::enabled) {
|
||||||
|
entries.insert(Feature::SleepTool.key().to_string(), enabled);
|
||||||
|
}
|
||||||
entries
|
entries
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,6 +882,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
stage: Stage::Stable,
|
stage: Stage::Stable,
|
||||||
default_enabled: true,
|
default_enabled: true,
|
||||||
},
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::SleepTool,
|
||||||
|
key: "sleep_tool",
|
||||||
|
stage: Stage::Stable,
|
||||||
|
default_enabled: true,
|
||||||
|
},
|
||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
id: Feature::SecretAuthStorage,
|
id: Feature::SecretAuthStorage,
|
||||||
key: "secret_auth_storage",
|
key: "secret_auth_storage",
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ fn transcript_v2_resolves_explicit_config_overrides() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sleep_tool_config_rejects_unknown_mode() {
|
||||||
|
assert!(toml::from_str::<FeaturesToml>("[sleep_tool]\nmode = 'off'").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn under_development_features_are_disabled_by_default() {
|
fn under_development_features_are_disabled_by_default() {
|
||||||
for spec in crate::FEATURES {
|
for spec in crate::FEATURES {
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
|||||||
token_budget: None,
|
token_budget: None,
|
||||||
rollout_budget: None,
|
rollout_budget: None,
|
||||||
current_time_reminder: None,
|
current_time_reminder: None,
|
||||||
|
sleep_tool_mode: Default::default(),
|
||||||
features: Default::default(),
|
features: Default::default(),
|
||||||
suppress_unstable_features_warning: false,
|
suppress_unstable_features_warning: false,
|
||||||
active_project: ProjectConfig { trust_level: None },
|
active_project: ProjectConfig { trust_level: None },
|
||||||
|
|||||||
Reference in new issue
Block a user