config: support enterprise feature requirements

This commit is contained in:
Michael Bolin
2026-03-03 12:42:57 -08:00
parent 745c48b088
commit 5e667824d0
12 changed files with 362 additions and 8 deletions
@@ -8943,6 +8943,15 @@
"type": "null"
}
]
},
"featureRequirements": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
}
},
"type": "object"
@@ -81,6 +81,15 @@
"type": "null"
}
]
},
"featureRequirements": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"object",
"null"
]
}
},
"type": "object"
@@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval";
import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
@@ -608,6 +609,7 @@ pub struct ConfigRequirements {
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
pub feature_requirements: Option<BTreeMap<String, bool>>,
pub enforce_residency: Option<ResidencyRequirement>,
#[experimental("configRequirements/read.network")]
pub network: Option<NetworkRequirements>,
+1 -1
View File
@@ -163,7 +163,7 @@ Example with notification opt-out:
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home).
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), `enforceResidency`, and `network` constraints.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints.
### Example: Start or resume a thread
+17
View File
@@ -127,6 +127,9 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
}
normalized
}),
feature_requirements: requirements
.feature_requirements
.map(|requirements| requirements.entries),
enforce_residency: requirements
.enforce_residency
.map(map_residency_requirement_to_api),
@@ -212,6 +215,12 @@ mod tests {
allowed_web_search_modes: Some(vec![
codex_core::config_loader::WebSearchModeRequirement::Cached,
]),
feature_requirements: Some(codex_core::config_loader::FeatureRequirementsToml {
entries: std::collections::BTreeMap::from([
("apps".to_string(), false),
("personality".to_string(), true),
]),
}),
mcp_servers: None,
rules: None,
enforce_residency: Some(CoreResidencyRequirement::Us),
@@ -247,6 +256,13 @@ mod tests {
mapped.allowed_web_search_modes,
Some(vec![WebSearchMode::Cached, WebSearchMode::Disabled]),
);
assert_eq!(
mapped.feature_requirements,
Some(std::collections::BTreeMap::from([
("apps".to_string(), false),
("personality".to_string(), true),
])),
);
assert_eq!(
mapped.enforce_residency,
Some(codex_app_server_protocol::ResidencyRequirement::Us),
@@ -275,6 +291,7 @@ mod tests {
allowed_approval_policies: None,
allowed_sandbox_modes: None,
allowed_web_search_modes: Some(Vec::new()),
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -79,6 +79,7 @@ pub struct ConfigRequirements {
pub approval_policy: ConstrainedWithSource<AskForApproval>,
pub sandbox_policy: ConstrainedWithSource<SandboxPolicy>,
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub exec_policy: Option<Sourced<RequirementsExecPolicy>>,
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
@@ -101,6 +102,7 @@ impl Default for ConfigRequirements {
Constrained::allow_any(WebSearchMode::Cached),
None,
),
feature_requirements: None,
mcp_servers: None,
exec_policy: None,
enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None),
@@ -142,6 +144,18 @@ pub struct NetworkRequirementsToml {
pub allow_local_binding: Option<bool>,
}
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct FeatureRequirementsToml {
#[serde(flatten)]
pub entries: BTreeMap<String, bool>,
}
impl FeatureRequirementsToml {
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
/// Normalized network constraints derived from requirements TOML.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkConstraints {
@@ -233,6 +247,7 @@ pub struct ConfigRequirementsToml {
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
pub feature_requirements: Option<FeatureRequirementsToml>,
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
pub rules: Option<RequirementsExecPolicyToml>,
pub enforce_residency: Option<ResidencyRequirement>,
@@ -267,6 +282,7 @@ pub struct ConfigRequirementsWithSources {
pub allowed_approval_policies: Option<Sourced<Vec<AskForApproval>>>,
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
@@ -302,6 +318,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies,
allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
rules,
enforce_residency,
@@ -315,6 +332,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies,
allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
rules,
enforce_residency,
@@ -324,6 +342,7 @@ impl ConfigRequirementsWithSources {
allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value),
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
feature_requirements: feature_requirements.map(|sourced| sourced.value),
mcp_servers: mcp_servers.map(|sourced| sourced.value),
rules: rules.map(|sourced| sourced.value),
enforce_residency: enforce_residency.map(|sourced| sourced.value),
@@ -370,6 +389,10 @@ impl ConfigRequirementsToml {
self.allowed_approval_policies.is_none()
&& self.allowed_sandbox_modes.is_none()
&& self.allowed_web_search_modes.is_none()
&& self
.feature_requirements
.as_ref()
.is_none_or(FeatureRequirementsToml::is_empty)
&& self.mcp_servers.is_none()
&& self.rules.is_none()
&& self.enforce_residency.is_none()
@@ -385,6 +408,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
allowed_approval_policies,
allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
rules,
enforce_residency,
@@ -521,6 +545,8 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
}
None => ConstrainedWithSource::new(Constrained::allow_any(WebSearchMode::Cached), None),
};
let feature_requirements =
feature_requirements.filter(|requirements| !requirements.value.is_empty());
let enforce_residency = match enforce_residency {
Some(Sourced {
@@ -553,6 +579,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
approval_policy,
sandbox_policy,
web_search_mode,
feature_requirements,
mcp_servers,
exec_policy,
enforce_residency,
@@ -588,6 +615,7 @@ mod tests {
allowed_approval_policies,
allowed_sandbox_modes,
allowed_web_search_modes,
feature_requirements,
mcp_servers,
rules,
enforce_residency,
@@ -600,6 +628,8 @@ mod tests {
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
allowed_web_search_modes: allowed_web_search_modes
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
feature_requirements: feature_requirements
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)),
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
enforce_residency: enforce_residency
@@ -622,6 +652,9 @@ mod tests {
WebSearchModeRequirement::Cached,
WebSearchModeRequirement::Live,
];
let feature_requirements = FeatureRequirementsToml {
entries: BTreeMap::from([("personality".to_string(), true)]),
};
let enforce_residency = ResidencyRequirement::Us;
let enforce_source = source.clone();
@@ -631,6 +664,7 @@ mod tests {
allowed_approval_policies: Some(allowed_approval_policies.clone()),
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
feature_requirements: Some(feature_requirements.clone()),
mcp_servers: None,
rules: None,
enforce_residency: Some(enforce_residency),
@@ -651,6 +685,10 @@ mod tests {
allowed_web_search_modes,
enforce_source.clone(),
)),
feature_requirements: Some(Sourced::new(
feature_requirements,
enforce_source.clone(),
)),
mcp_servers: None,
rules: None,
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
@@ -683,6 +721,7 @@ mod tests {
)),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -723,6 +762,7 @@ mod tests {
)),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -809,6 +849,8 @@ mod tests {
allowed_sandbox_modes = ["read-only"]
allowed_web_search_modes = ["cached"]
enforce_residency = "us"
[feature_requirements]
personality = true
"#,
)?;
@@ -829,6 +871,13 @@ mod tests {
requirements.web_search_mode.source,
Some(source_location.clone())
);
assert_eq!(
requirements
.feature_requirements
.as_ref()
.map(|requirements| requirements.source.clone()),
Some(source_location.clone())
);
assert_eq!(requirements.enforce_residency.source, Some(source_location));
Ok(())
@@ -1038,6 +1087,32 @@ mod tests {
Ok(())
}
#[test]
fn deserialize_feature_requirements() -> Result<()> {
let toml_str = r#"
[feature_requirements]
apps = false
personality = true
"#;
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
assert_eq!(
requirements.feature_requirements,
Some(Sourced::new(
FeatureRequirementsToml {
entries: BTreeMap::from([
("apps".to_string(), false),
("personality".to_string(), true),
]),
},
RequirementSource::Unknown,
))
);
Ok(())
}
#[test]
fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> {
let toml_str = r#"
+1
View File
@@ -16,6 +16,7 @@ pub use config_requirements::ConfigRequirements;
pub use config_requirements::ConfigRequirementsToml;
pub use config_requirements::ConfigRequirementsWithSources;
pub use config_requirements::ConstrainedWithSource;
pub use config_requirements::FeatureRequirementsToml;
pub use config_requirements::McpServerIdentity;
pub use config_requirements::McpServerRequirement;
pub use config_requirements::NetworkConstraints;
+197 -1
View File
@@ -29,6 +29,7 @@ use crate::config_loader::CloudRequirementsLoader;
use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConstrainedWithSource;
use crate::config_loader::FeatureRequirementsToml;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::McpServerIdentity;
use crate::config_loader::McpServerRequirement;
@@ -39,6 +40,8 @@ use crate::features::Feature;
use crate::features::FeatureOverrides;
use crate::features::Features;
use crate::features::FeaturesToml;
use crate::features::canonical_feature_for_key;
use crate::features::feature_for_key;
use crate::git_info::resolve_root_git_project_for_trust;
use crate::model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID;
use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
@@ -810,6 +813,88 @@ where
Ok(())
}
fn feature_requirements_display(feature_requirements: &BTreeMap<Feature, bool>) -> String {
let values = feature_requirements
.iter()
.map(|(feature, enabled)| format!("{}={enabled}", feature.key()))
.collect::<Vec<_>>();
format!("[{}]", values.join(", "))
}
fn apply_feature_requirements(
configured_features: Features,
feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
) -> std::io::Result<Features> {
let Some(Sourced {
value: feature_requirements,
source,
}) = feature_requirements
else {
return Ok(configured_features);
};
let mut pinned_features = BTreeMap::new();
for (key, enabled) in feature_requirements.entries {
if let Some(feature) = canonical_feature_for_key(&key) {
pinned_features.insert(feature, enabled);
continue;
}
if let Some(feature) = feature_for_key(&key) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"invalid `feature_requirements` entry `{key}` from {source}: use canonical feature key `{}`",
feature.key()
),
));
}
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid `feature_requirements` entry `{key}` from {source}"),
));
}
if pinned_features.is_empty() {
return Ok(configured_features);
}
let pinned_features_for_normalizer = pinned_features.clone();
let constrained =
Constrained::normalized(configured_features, move |mut candidate: Features| {
for (feature, enabled) in &pinned_features_for_normalizer {
candidate.set_enabled(*feature, *enabled);
}
candidate.normalize_dependencies();
candidate
})
.map_err(std::io::Error::from)?;
let constrained_features = ConstrainedWithSource::new(constrained, Some(source.clone()));
let normalized_features = constrained_features.get();
let allowed = feature_requirements_display(&pinned_features);
for (feature, enabled) in &pinned_features {
if normalized_features.enabled(*feature) != *enabled {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
ConstraintError::InvalidValue {
field_name: "features",
candidate: format!(
"{}={}",
feature.key(),
normalized_features.enabled(*feature)
),
allowed,
requirement_source: source,
},
));
}
}
Ok(normalized_features.clone())
}
fn mcp_server_matches_requirement(
requirement: &McpServerRequirement,
server: &McpServerConfig,
@@ -1737,7 +1822,11 @@ impl Config {
web_search_request: override_tools_web_search_request,
};
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
let configured_features = Features::from_config(&cfg, &config_profile, feature_overrides);
let features = apply_feature_requirements(
configured_features,
requirements.feature_requirements.clone(),
)?;
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
let resolved_cwd = {
use std::env;
@@ -2054,6 +2143,7 @@ impl Config {
approval_policy: mut constrained_approval_policy,
sandbox_policy: mut constrained_sandbox_policy,
web_search_mode: mut constrained_web_search_mode,
feature_requirements: _,
mcp_servers,
exec_policy: _,
enforce_residency,
@@ -5390,6 +5480,7 @@ model_verbosity = "high"
allowed_web_search_modes: Some(vec![
crate::config_loader::WebSearchModeRequirement::Cached,
]),
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -6021,6 +6112,7 @@ mcp_oauth_callback_url = "https://example.com/callback"
crate::config_loader::SandboxModeRequirement::ReadOnly,
]),
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -6139,6 +6231,110 @@ trust_level = "untrusted"
);
Ok(())
}
#[tokio::test]
async fn feature_requirements_override_default_feature_values() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
entries: BTreeMap::from([
("personality".to_string(), true),
("shell_tool".to_string(), false),
]),
}),
..Default::default()
}))
}))
.build()
.await?;
assert!(config.features.enabled(Feature::Personality));
assert!(!config.features.enabled(Feature::ShellTool));
assert!(
!config
.startup_warnings
.iter()
.any(|warning| warning.contains("Configured value for `features`")),
"{:?}",
config.startup_warnings
);
Ok(())
}
#[tokio::test]
async fn explicit_feature_config_is_normalized_by_requirements() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"
[features]
personality = false
shell_tool = true
"#,
)?;
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
entries: BTreeMap::from([
("personality".to_string(), true),
("shell_tool".to_string(), false),
]),
}),
..Default::default()
}))
}))
.build()
.await?;
assert!(config.features.enabled(Feature::Personality));
assert!(!config.features.enabled(Feature::ShellTool));
assert!(
!config
.startup_warnings
.iter()
.any(|warning| warning.contains("Configured value for `features`")),
"{:?}",
config.startup_warnings
);
Ok(())
}
#[tokio::test]
async fn feature_requirements_reject_legacy_aliases() {
let codex_home = TempDir::new().expect("tempdir");
let err = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
entries: BTreeMap::from([("collab".to_string(), true)]),
}),
..Default::default()
}))
}))
.build()
.await
.expect_err("legacy aliases should be rejected");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(
err.to_string()
.contains("use canonical feature key `multi_agent`"),
"{err}"
);
}
#[test]
fn experimental_realtime_ws_base_url_loads_from_config_toml() -> std::io::Result<()> {
let cfg: ConfigToml = toml::from_str(
+1
View File
@@ -34,6 +34,7 @@ pub use codex_config::ConfigLoadError;
pub use codex_config::ConfigRequirements;
pub use codex_config::ConfigRequirementsToml;
pub use codex_config::ConstrainedWithSource;
pub use codex_config::FeatureRequirementsToml;
pub use codex_config::LoaderOverrides;
pub use codex_config::McpServerIdentity;
pub use codex_config::McpServerRequirement;
+25
View File
@@ -23,6 +23,7 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::Path;
use tempfile::tempdir;
@@ -494,6 +495,9 @@ async fn load_requirements_toml_produces_expected_constraints() -> anyhow::Resul
allowed_approval_policies = ["never", "on-request"]
allowed_web_search_modes = ["cached"]
enforce_residency = "us"
[feature_requirements]
personality = true
"#,
)
.await?;
@@ -515,6 +519,15 @@ enforce_residency = "us"
.cloned(),
Some(vec![crate::config_loader::WebSearchModeRequirement::Cached])
);
assert_eq!(
config_requirements_toml
.feature_requirements
.as_ref()
.map(|requirements| requirements.value.clone()),
Some(crate::config_loader::FeatureRequirementsToml {
entries: BTreeMap::from([("personality".to_string(), true)]),
})
);
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
assert_eq!(
config_requirements.approval_policy.value(),
@@ -552,6 +565,15 @@ enforce_residency = "us"
config_requirements.enforce_residency.value(),
Some(crate::config_loader::ResidencyRequirement::Us)
);
assert_eq!(
config_requirements
.feature_requirements
.as_ref()
.map(|requirements| requirements.value.clone()),
Some(crate::config_loader::FeatureRequirementsToml {
entries: BTreeMap::from([("personality".to_string(), true)]),
})
);
Ok(())
}
@@ -581,6 +603,7 @@ allowed_approval_policies = ["on-request"]
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -629,6 +652,7 @@ allowed_approval_policies = ["on-request"]
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
@@ -666,6 +690,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
mcp_servers: None,
rules: None,
enforce_residency: None,
+24 -5
View File
@@ -241,6 +241,14 @@ impl Features {
self
}
pub fn set_enabled(&mut self, f: Feature, enabled: bool) -> &mut Self {
if enabled {
self.enable(f)
} else {
self.disable(f)
}
}
pub fn record_legacy_usage_force(&mut self, alias: &str, feature: Feature) {
let (summary, details) = legacy_usage_notice(alias, feature);
self.legacy_usages.insert(LegacyFeatureUsage {
@@ -349,10 +357,7 @@ impl Features {
}
overrides.apply(&mut features);
if features.enabled(Feature::JsReplToolsOnly) && !features.enabled(Feature::JsRepl) {
tracing::warn!("js_repl_tools_only requires js_repl; disabling js_repl_tools_only");
features.disable(Feature::JsReplToolsOnly);
}
features.normalize_dependencies();
features
}
@@ -360,6 +365,13 @@ impl Features {
pub fn enabled_features(&self) -> Vec<Feature> {
self.enabled.iter().copied().collect()
}
pub(crate) fn normalize_dependencies(&mut self) {
if self.enabled(Feature::JsReplToolsOnly) && !self.enabled(Feature::JsRepl) {
tracing::warn!("js_repl_tools_only requires js_repl; disabling js_repl_tools_only");
self.disable(Feature::JsReplToolsOnly);
}
}
}
fn legacy_usage_notice(alias: &str, feature: Feature) -> (String, Option<String>) {
@@ -400,7 +412,7 @@ fn web_search_details() -> &'static str {
}
/// Keys accepted in `[features]` tables.
fn feature_for_key(key: &str) -> Option<Feature> {
pub(crate) fn feature_for_key(key: &str) -> Option<Feature> {
for spec in FEATURES {
if spec.key == key {
return Some(spec.id);
@@ -409,6 +421,13 @@ fn feature_for_key(key: &str) -> Option<Feature> {
legacy::feature_for_key(key)
}
pub(crate) fn canonical_feature_for_key(key: &str) -> Option<Feature> {
FEATURES
.iter()
.find(|spec| spec.key == key)
.map(|spec| spec.id)
}
/// Returns `true` if the provided string matches a known feature toggle key.
pub fn is_known_feature_key(key: &str) -> bool {
feature_for_key(key).is_some()