mirror of
https://github.com/openai/codex.git
synced 2026-04-23 07:51:51 +03:00
Compare commits
6 Commits
exec-env-p
...
daniels-oa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1427b3c00e | ||
|
|
778a040fd9 | ||
|
|
47f66b49a7 | ||
|
|
6afcc5bebb | ||
|
|
30ccb91420 | ||
|
|
f068d7d784 |
@@ -761,6 +761,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -803,6 +804,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -856,6 +858,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -912,6 +915,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -939,6 +943,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -986,6 +991,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1032,6 +1038,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1082,6 +1089,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1133,6 +1141,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1184,6 +1193,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1272,6 +1282,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1295,6 +1306,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: 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 features: Option<Sourced<RequirementsFeaturesToml>>,
|
||||
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,
|
||||
),
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
exec_policy: None,
|
||||
enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None),
|
||||
@@ -142,6 +144,17 @@ pub struct NetworkRequirementsToml {
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
/// Raw `[features]` entries from managed requirements.
|
||||
///
|
||||
/// The keys stay as strings here so `codex-config` does not need its own copy
|
||||
/// of the feature registry; `codex-core` validates them against the canonical
|
||||
/// runtime feature list when building the effective config.
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct RequirementsFeaturesToml {
|
||||
#[serde(flatten)]
|
||||
pub entries: BTreeMap<String, bool>,
|
||||
}
|
||||
|
||||
/// Normalized network constraints derived from requirements TOML.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct NetworkConstraints {
|
||||
@@ -233,6 +246,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 features: Option<RequirementsFeaturesToml>,
|
||||
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
|
||||
pub rules: Option<RequirementsExecPolicyToml>,
|
||||
pub enforce_residency: Option<ResidencyRequirement>,
|
||||
@@ -267,6 +281,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 features: Option<Sourced<RequirementsFeaturesToml>>,
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
|
||||
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
|
||||
@@ -302,6 +317,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
features,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
@@ -315,6 +331,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
features,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
@@ -324,6 +341,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),
|
||||
features: features.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 +388,7 @@ impl ConfigRequirementsToml {
|
||||
self.allowed_approval_policies.is_none()
|
||||
&& self.allowed_sandbox_modes.is_none()
|
||||
&& self.allowed_web_search_modes.is_none()
|
||||
&& self.features.is_none()
|
||||
&& self.mcp_servers.is_none()
|
||||
&& self.rules.is_none()
|
||||
&& self.enforce_residency.is_none()
|
||||
@@ -385,6 +404,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
features,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
@@ -553,6 +573,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
web_search_mode,
|
||||
features,
|
||||
mcp_servers,
|
||||
exec_policy,
|
||||
enforce_residency,
|
||||
@@ -588,6 +609,7 @@ mod tests {
|
||||
allowed_approval_policies,
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
features,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
@@ -600,6 +622,7 @@ mod tests {
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
allowed_web_search_modes: allowed_web_search_modes
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
features: features.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 +645,9 @@ mod tests {
|
||||
WebSearchModeRequirement::Cached,
|
||||
WebSearchModeRequirement::Live,
|
||||
];
|
||||
let features = RequirementsFeaturesToml {
|
||||
entries: BTreeMap::from([(String::from("unified_exec"), false)]),
|
||||
};
|
||||
let enforce_residency = ResidencyRequirement::Us;
|
||||
let enforce_source = source.clone();
|
||||
|
||||
@@ -631,6 +657,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()),
|
||||
features: Some(features.clone()),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(enforce_residency),
|
||||
@@ -651,6 +678,7 @@ mod tests {
|
||||
allowed_web_search_modes,
|
||||
enforce_source.clone(),
|
||||
)),
|
||||
features: Some(Sourced::new(features, enforce_source.clone())),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
|
||||
@@ -683,6 +711,7 @@ mod tests {
|
||||
)),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -723,6 +752,7 @@ mod tests {
|
||||
)),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -1126,6 +1156,28 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_feature_requirements() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
[features]
|
||||
unified_exec = false
|
||||
"#;
|
||||
let requirements: ConfigRequirements =
|
||||
with_unknown_source(from_str(toml_str)?).try_into()?;
|
||||
|
||||
assert_eq!(
|
||||
requirements.features,
|
||||
Some(Sourced::new(
|
||||
RequirementsFeaturesToml {
|
||||
entries: BTreeMap::from([(String::from("unified_exec"), false)]),
|
||||
},
|
||||
RequirementSource::Unknown,
|
||||
))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_exec_policy_requirements() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
|
||||
@@ -21,6 +21,7 @@ pub use config_requirements::McpServerRequirement;
|
||||
pub use config_requirements::NetworkConstraints;
|
||||
pub use config_requirements::NetworkRequirementsToml;
|
||||
pub use config_requirements::RequirementSource;
|
||||
pub use config_requirements::RequirementsFeaturesToml;
|
||||
pub use config_requirements::ResidencyRequirement;
|
||||
pub use config_requirements::SandboxModeRequirement;
|
||||
pub use config_requirements::Sourced;
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::config_loader::ConstrainedWithSource;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::config_loader::McpServerIdentity;
|
||||
use crate::config_loader::McpServerRequirement;
|
||||
use crate::config_loader::RequirementsFeaturesToml;
|
||||
use crate::config_loader::ResidencyRequirement;
|
||||
use crate::config_loader::Sourced;
|
||||
use crate::config_loader::load_config_layers_state;
|
||||
@@ -39,6 +40,9 @@ use crate::features::Feature;
|
||||
use crate::features::FeatureOverrides;
|
||||
use crate::features::Features;
|
||||
use crate::features::FeaturesToml;
|
||||
use crate::features::Stage;
|
||||
use crate::features::canonical_feature_for_alias;
|
||||
use crate::features::canonical_feature_spec;
|
||||
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 +814,133 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies disable-only managed feature requirements after local config,
|
||||
/// profile settings, and CLI/legacy feature overrides have been merged.
|
||||
///
|
||||
/// This lives in `codex-core` instead of `codex-config` so validation can rely
|
||||
/// on the single canonical feature registry in `features.rs` without mirroring
|
||||
/// that registry across crates.
|
||||
fn apply_requirement_feature_constraints(
|
||||
features: &mut Features,
|
||||
requirement_features: Option<&Sourced<RequirementsFeaturesToml>>,
|
||||
cfg: &ConfigToml,
|
||||
config_profile: &ConfigProfile,
|
||||
feature_overrides: &FeatureOverrides,
|
||||
startup_warnings: &mut Vec<String>,
|
||||
) -> std::io::Result<()> {
|
||||
let Some(requirement_features) = requirement_features else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (key, enabled) in &requirement_features.value.entries {
|
||||
if *enabled {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"requirements features can only disable features; `{key}` was set to true (set by {})",
|
||||
requirement_features.source
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let Some(spec) = canonical_feature_spec(key) else {
|
||||
if let Some(feature) = canonical_feature_for_alias(key) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"requirements features uses legacy key `{key}` (set by {}); use canonical key `{}` instead",
|
||||
requirement_features.source,
|
||||
feature.key()
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"requirements features contains unknown feature key `{key}` (set by {})",
|
||||
requirement_features.source
|
||||
),
|
||||
));
|
||||
};
|
||||
|
||||
if matches!(spec.stage, Stage::Removed) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"requirements features contains removed feature key `{key}` (set by {})",
|
||||
requirement_features.source
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if features.enabled(spec.id)
|
||||
&& feature_is_explicitly_enabled(spec.id, cfg, config_profile, feature_overrides)
|
||||
{
|
||||
startup_warnings.push(format!(
|
||||
"Configured value for `features.{key}` is disallowed by requirements; forcing false."
|
||||
));
|
||||
}
|
||||
features.disable(spec.id);
|
||||
}
|
||||
|
||||
features.normalize_dependencies();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn feature_is_explicitly_enabled(
|
||||
feature: Feature,
|
||||
cfg: &ConfigToml,
|
||||
config_profile: &ConfigProfile,
|
||||
feature_overrides: &FeatureOverrides,
|
||||
) -> bool {
|
||||
feature_map_explicitly_enables(cfg.features.as_ref(), feature)
|
||||
|| feature_map_explicitly_enables(config_profile.features.as_ref(), feature)
|
||||
|| legacy_feature_setting_explicitly_enables(
|
||||
feature,
|
||||
cfg,
|
||||
config_profile,
|
||||
feature_overrides,
|
||||
)
|
||||
}
|
||||
|
||||
fn feature_map_explicitly_enables(features: Option<&FeaturesToml>, feature: Feature) -> bool {
|
||||
features.is_some_and(|features| {
|
||||
features.entries.iter().any(|(key, enabled)| {
|
||||
*enabled
|
||||
&& (canonical_feature_spec(key).map(|spec| spec.id) == Some(feature)
|
||||
|| canonical_feature_for_alias(key) == Some(feature))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn legacy_feature_setting_explicitly_enables(
|
||||
feature: Feature,
|
||||
cfg: &ConfigToml,
|
||||
config_profile: &ConfigProfile,
|
||||
feature_overrides: &FeatureOverrides,
|
||||
) -> bool {
|
||||
match feature {
|
||||
Feature::ApplyPatchFreeform => {
|
||||
config_profile.include_apply_patch_tool == Some(true)
|
||||
|| cfg.experimental_use_freeform_apply_patch == Some(true)
|
||||
|| config_profile.experimental_use_freeform_apply_patch == Some(true)
|
||||
|| feature_overrides.include_apply_patch_tool == Some(true)
|
||||
}
|
||||
Feature::UnifiedExec => {
|
||||
cfg.experimental_use_unified_exec_tool == Some(true)
|
||||
|| config_profile.experimental_use_unified_exec_tool == Some(true)
|
||||
}
|
||||
Feature::WebSearchRequest => {
|
||||
cfg.tools.as_ref().and_then(|tools| tools.web_search) == Some(true)
|
||||
|| config_profile.tools_web_search == Some(true)
|
||||
|| feature_overrides.web_search_request == Some(true)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_server_matches_requirement(
|
||||
requirement: &McpServerRequirement,
|
||||
server: &McpServerConfig,
|
||||
@@ -1739,7 +1870,15 @@ impl Config {
|
||||
web_search_request: override_tools_web_search_request,
|
||||
};
|
||||
|
||||
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
|
||||
let mut features = Features::from_config(&cfg, &config_profile, feature_overrides.clone());
|
||||
apply_requirement_feature_constraints(
|
||||
&mut features,
|
||||
requirements.features.as_ref(),
|
||||
&cfg,
|
||||
&config_profile,
|
||||
&feature_overrides,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
|
||||
let resolved_cwd = {
|
||||
use std::env;
|
||||
@@ -2058,6 +2197,7 @@ impl Config {
|
||||
approval_policy: mut constrained_approval_policy,
|
||||
sandbox_policy: mut constrained_sandbox_policy,
|
||||
web_search_mode: mut constrained_web_search_mode,
|
||||
features: _,
|
||||
mcp_servers,
|
||||
exec_policy: _,
|
||||
enforce_residency,
|
||||
@@ -5394,6 +5534,7 @@ model_verbosity = "high"
|
||||
allowed_web_search_modes: Some(vec![
|
||||
crate::config_loader::WebSearchModeRequirement::Cached,
|
||||
]),
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -5419,6 +5560,7 @@ model_verbosity = "high"
|
||||
constrained,
|
||||
Some(requirement_source),
|
||||
),
|
||||
features: None,
|
||||
..Default::default()
|
||||
};
|
||||
let config_layer_stack = crate::config_loader::ConfigLayerStack::new(
|
||||
@@ -5998,6 +6140,7 @@ mcp_oauth_callback_url = "https://example.com/callback"
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
]),
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -6232,6 +6375,180 @@ speaker = "Desk Speakers"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_feature_requirements_disable_default_feature_without_warning()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
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 {
|
||||
features: Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("unified_exec".to_string(), false)].into_iter().collect(),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::UnifiedExec));
|
||||
assert!(!config.use_experimental_unified_exec_tool);
|
||||
assert!(
|
||||
!config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("features.unified_exec")),
|
||||
"{:?}",
|
||||
config.startup_warnings
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_feature_requirements_warn_on_explicit_local_override() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[features]
|
||||
unified_exec = 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 {
|
||||
features: Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("unified_exec".to_string(), false)].into_iter().collect(),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::UnifiedExec));
|
||||
assert!(!config.use_experimental_unified_exec_tool);
|
||||
assert!(
|
||||
config
|
||||
.startup_warnings
|
||||
.iter()
|
||||
.any(|warning| warning.contains("features.unified_exec"))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_feature_requirements_re_normalize_feature_dependencies() -> std::io::Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[features]
|
||||
js_repl = true
|
||||
js_repl_tools_only = 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 {
|
||||
features: Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("js_repl".to_string(), false)].into_iter().collect(),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::JsRepl));
|
||||
assert!(!config.features.enabled(Feature::JsReplToolsOnly));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_feature_requirements_reject_true_values() {
|
||||
let codex_home = TempDir::new().expect("tempdir");
|
||||
|
||||
let err = 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 {
|
||||
features: Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("unified_exec".to_string(), true)].into_iter().collect(),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("requirements feature true should fail");
|
||||
|
||||
assert!(err.to_string().contains("can only disable features"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_feature_requirements_reject_unknown_feature_keys() {
|
||||
let codex_home = TempDir::new().expect("tempdir");
|
||||
|
||||
let err = 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 {
|
||||
features: Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("definitely_fake_feature".to_string(), false)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("unknown feature key should fail");
|
||||
|
||||
assert!(err.to_string().contains("unknown feature key"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_feature_requirements_reject_legacy_aliases() {
|
||||
let codex_home = TempDir::new().expect("tempdir");
|
||||
|
||||
let err = 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 {
|
||||
features: Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("experimental_use_unified_exec_tool".to_string(), false)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("legacy alias should fail");
|
||||
|
||||
assert!(err.to_string().contains("legacy key"));
|
||||
assert!(err.to_string().contains("unified_exec"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -40,6 +40,7 @@ pub use codex_config::McpServerRequirement;
|
||||
pub use codex_config::NetworkConstraints;
|
||||
pub use codex_config::NetworkRequirementsToml;
|
||||
pub use codex_config::RequirementSource;
|
||||
pub use codex_config::RequirementsFeaturesToml;
|
||||
pub use codex_config::ResidencyRequirement;
|
||||
pub use codex_config::SandboxModeRequirement;
|
||||
pub use codex_config::Sourced;
|
||||
|
||||
@@ -494,6 +494,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"
|
||||
|
||||
[features]
|
||||
unified_exec = false
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
@@ -515,6 +518,24 @@ enforce_residency = "us"
|
||||
.cloned(),
|
||||
Some(vec![crate::config_loader::WebSearchModeRequirement::Cached])
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements_toml
|
||||
.features
|
||||
.as_ref()
|
||||
.map(|requirements| requirements.value.clone()),
|
||||
Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("unified_exec".to_string(), false)].into_iter().collect(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements_toml
|
||||
.features
|
||||
.as_ref()
|
||||
.map(|requirements| requirements.source.clone()),
|
||||
Some(RequirementSource::SystemRequirementsToml {
|
||||
file: AbsolutePathBuf::from_absolute_path(&requirements_file)?,
|
||||
})
|
||||
);
|
||||
let config_requirements: ConfigRequirements = config_requirements_toml.try_into()?;
|
||||
assert_eq!(
|
||||
config_requirements.approval_policy.value(),
|
||||
@@ -552,6 +573,15 @@ enforce_residency = "us"
|
||||
config_requirements.enforce_residency.value(),
|
||||
Some(crate::config_loader::ResidencyRequirement::Us)
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements
|
||||
.features
|
||||
.as_ref()
|
||||
.map(|requirements| requirements.value.clone()),
|
||||
Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("unified_exec".to_string(), false)].into_iter().collect(),
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -581,6 +611,7 @@ allowed_approval_policies = ["on-request"]
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -629,6 +660,7 @@ allowed_approval_policies = ["on-request"]
|
||||
allowed_approval_policies: Some(vec![AskForApproval::Never]),
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -666,6 +698,9 @@ 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,
|
||||
features: Some(crate::config_loader::RequirementsFeaturesToml {
|
||||
entries: [("unified_exec".to_string(), false)].into_iter().collect(),
|
||||
}),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
@@ -687,6 +722,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
||||
layers.requirements_toml().allowed_approval_policies,
|
||||
expected.allowed_approval_policies
|
||||
);
|
||||
assert_eq!(layers.requirements_toml().features, expected.features);
|
||||
assert_eq!(
|
||||
layers
|
||||
.requirements()
|
||||
|
||||
@@ -351,10 +351,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
|
||||
}
|
||||
@@ -362,6 +359,16 @@ impl Features {
|
||||
pub fn enabled_features(&self) -> Vec<Feature> {
|
||||
self.enabled.iter().copied().collect()
|
||||
}
|
||||
|
||||
/// Clears feature flags whose prerequisites were disabled later in config
|
||||
/// resolution, such as when managed requirements turn off a base feature
|
||||
/// after local feature maps have already been applied.
|
||||
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>) {
|
||||
@@ -402,7 +409,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);
|
||||
@@ -411,6 +418,14 @@ fn feature_for_key(key: &str) -> Option<Feature> {
|
||||
legacy::feature_for_key(key)
|
||||
}
|
||||
|
||||
pub(crate) fn canonical_feature_spec(key: &str) -> Option<&'static FeatureSpec> {
|
||||
FEATURES.iter().find(|spec| spec.key == key)
|
||||
}
|
||||
|
||||
pub(crate) fn canonical_feature_for_alias(key: &str) -> Option<Feature> {
|
||||
legacy::canonical_feature_for_alias(key)
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
||||
@@ -47,14 +47,15 @@ pub(crate) fn legacy_feature_keys() -> impl Iterator<Item = &'static str> {
|
||||
ALIASES.iter().map(|alias| alias.legacy_key)
|
||||
}
|
||||
|
||||
pub(crate) fn feature_for_key(key: &str) -> Option<Feature> {
|
||||
pub(crate) fn canonical_feature_for_alias(key: &str) -> Option<Feature> {
|
||||
ALIASES
|
||||
.iter()
|
||||
.find(|alias| alias.legacy_key == key)
|
||||
.map(|alias| {
|
||||
log_alias(alias.legacy_key, alias.feature);
|
||||
alias.feature
|
||||
})
|
||||
.map(|alias| alias.feature)
|
||||
}
|
||||
|
||||
pub(crate) fn feature_for_key(key: &str) -> Option<Feature> {
|
||||
canonical_feature_for_alias(key).inspect(|feature| log_alias(key, *feature))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -527,6 +527,7 @@ mod tests {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
|
||||
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
|
||||
features: None,
|
||||
mcp_servers: Some(BTreeMap::from([(
|
||||
"docs".to_string(),
|
||||
McpServerRequirement {
|
||||
@@ -652,6 +653,7 @@ approval_policy = "never"
|
||||
allowed_approval_policies: None,
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: Some(Vec::new()),
|
||||
features: None,
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
|
||||
Reference in New Issue
Block a user