mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
fix: ignore remote control config override
This commit is contained in:
@@ -180,7 +180,7 @@ Example with notification opt-out:
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `remote_control`, `tool_search`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `remote_control`, `tool_search`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default. Exception: `remote_control` ignores `config.toml` feature entries so runtime enablement can own the app-server connection state.
|
||||
- `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
|
||||
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||
|
||||
@@ -325,6 +325,50 @@ async fn experimental_feature_enablement_set_allows_remote_control() -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_feature_enablement_set_enables_remote_control_ignored_from_config_toml()
|
||||
-> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
"[features]\nremote_control = true\n",
|
||||
)?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let ConfigReadResponse { config, .. } = read_config(&mut mcp, /*cwd*/ None).await?;
|
||||
assert_eq!(
|
||||
config
|
||||
.additional
|
||||
.get("features")
|
||||
.and_then(|features| features.get("remote_control")),
|
||||
Some(&json!(false))
|
||||
);
|
||||
|
||||
let actual = set_experimental_feature_enablement(
|
||||
&mut mcp,
|
||||
BTreeMap::from([("remote_control".to_string(), true)]),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
actual,
|
||||
ExperimentalFeatureEnablementSetResponse {
|
||||
enablement: BTreeMap::from([("remote_control".to_string(), true)]),
|
||||
}
|
||||
);
|
||||
|
||||
let ConfigReadResponse { config, .. } = read_config(&mut mcp, /*cwd*/ None).await?;
|
||||
assert_eq!(
|
||||
config
|
||||
.additional
|
||||
.get("features")
|
||||
.and_then(|features| features.get("remote_control")),
|
||||
Some(&json!(true))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_feature_enablement_set_empty_map_is_no_op() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::merge::merge_toml_values;
|
||||
use codex_app_server_protocol::ConfigLayer;
|
||||
use codex_app_server_protocol::ConfigLayerMetadata;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_features::feature_for_key;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
@@ -301,7 +302,8 @@ impl ConfigLayerStack {
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
) {
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
let config = effective_layer_config(layer);
|
||||
merge_toml_values(&mut merged, &config);
|
||||
}
|
||||
merged
|
||||
}
|
||||
@@ -317,7 +319,8 @@ impl ConfigLayerStack {
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
) {
|
||||
let config = normalized_with_key_aliases(&layer.config, &[]);
|
||||
let config = effective_layer_config(layer);
|
||||
let config = normalized_with_key_aliases(&config, &[]);
|
||||
record_origins(&config, &layer.metadata(), &mut path, &mut origins);
|
||||
}
|
||||
|
||||
@@ -354,6 +357,26 @@ impl ConfigLayerStack {
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_layer_config(layer: &ConfigLayerEntry) -> TomlValue {
|
||||
let mut config = layer.config.clone();
|
||||
if matches!(layer.name, ConfigLayerSource::SessionFlags) {
|
||||
return config;
|
||||
}
|
||||
remove_config_toml_only_feature_overrides(&mut config);
|
||||
config
|
||||
}
|
||||
|
||||
fn remove_config_toml_only_feature_overrides(config: &mut TomlValue) {
|
||||
let Some(features) = config.get_mut("features").and_then(TomlValue::as_table_mut) else {
|
||||
return;
|
||||
};
|
||||
|
||||
features.retain(|key, _| match feature_for_key(key) {
|
||||
Some(feature) => feature.can_override_from_config_toml(),
|
||||
None => true,
|
||||
});
|
||||
}
|
||||
|
||||
/// Ensures precedence ordering of config layers is correct. Returns the index
|
||||
/// of the user config layer, if any (at most one should exist).
|
||||
fn verify_layer_ordering(layers: &[ConfigLayerEntry]) -> std::io::Result<Option<usize>> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
@@ -32,3 +33,64 @@ no_memories_if_mcp_or_web_search = true
|
||||
"legacy key should be canonicalized before origin recording"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_layers_ignore_remote_control_feature_override() {
|
||||
let layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::System {
|
||||
file: AbsolutePathBuf::resolve_path_against_base("config.toml", std::env::temp_dir()),
|
||||
},
|
||||
toml::from_str(
|
||||
r#"
|
||||
[features]
|
||||
plugins = true
|
||||
remote_control = true
|
||||
"#,
|
||||
)
|
||||
.expect("config TOML should parse"),
|
||||
);
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("single layer stack should be valid");
|
||||
|
||||
let effective = stack.effective_config();
|
||||
let features = effective
|
||||
.get("features")
|
||||
.and_then(TomlValue::as_table)
|
||||
.expect("features table should be present");
|
||||
assert_eq!(features.get("plugins"), Some(&TomlValue::Boolean(true)));
|
||||
assert_eq!(features.get("remote_control"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_flags_preserve_remote_control_feature_override() {
|
||||
let layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::from_str(
|
||||
r#"
|
||||
[features]
|
||||
remote_control = true
|
||||
"#,
|
||||
)
|
||||
.expect("config TOML should parse"),
|
||||
);
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("single layer stack should be valid");
|
||||
|
||||
let effective = stack.effective_config();
|
||||
let features = effective
|
||||
.get("features")
|
||||
.and_then(TomlValue::as_table)
|
||||
.expect("features table should be present");
|
||||
assert_eq!(
|
||||
features.get("remote_control"),
|
||||
Some(&TomlValue::Boolean(true))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4176,6 +4176,48 @@ async fn feature_table_overrides_legacy_flags() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn config_toml_remote_control_feature_is_ignored() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cfg = ConfigToml {
|
||||
features: Some(FeaturesToml::from(BTreeMap::from([(
|
||||
"remote_control".to_string(),
|
||||
true,
|
||||
)]))),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
cfg,
|
||||
ConfigOverrides::default(),
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::RemoteControl));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cli_remote_control_feature_override_is_preserved() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
// `codex --enable remote_control` is folded into this session override
|
||||
// before config loading.
|
||||
.cli_overrides(vec![(
|
||||
"features.remote_control".to_string(),
|
||||
toml::Value::Boolean(true),
|
||||
)])
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(config.features.enabled(Feature::RemoteControl));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn legacy_toggles_map_to_features() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -9128,6 +9170,28 @@ async fn browser_feature_requirements_are_valid() -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_feature_requirements_are_valid() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
feature_requirements: Some(codex_config::FeatureRequirementsToml {
|
||||
entries: BTreeMap::from([("remote_control".to_string(), true)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(config.features.enabled(Feature::RemoteControl));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn debug_config_lockfile_export_settings_load_from_nested_table() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -258,6 +258,9 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
|
||||
if let Some(features) = cfg.features.as_ref() {
|
||||
for (key, enabled) in features.entries() {
|
||||
if let Some(feature) = feature_for_key(&key) {
|
||||
if !feature.can_override_from_config_toml() {
|
||||
continue;
|
||||
}
|
||||
explicit_settings.push((format!("features.{key}"), feature, enabled));
|
||||
}
|
||||
}
|
||||
@@ -280,6 +283,9 @@ fn explicit_feature_settings_in_config(cfg: &ConfigToml) -> Vec<(String, Feature
|
||||
if let Some(features) = profile.features.as_ref() {
|
||||
for (key, enabled) in features.entries() {
|
||||
if let Some(feature) = feature_for_key(&key) {
|
||||
if !feature.can_override_from_config_toml() {
|
||||
continue;
|
||||
}
|
||||
explicit_settings.push((
|
||||
format!("profiles.{profile_name}.features.{key}"),
|
||||
feature,
|
||||
|
||||
@@ -223,6 +223,9 @@ pub enum Feature {
|
||||
/// Enable experimental realtime voice conversation mode in the TUI.
|
||||
RealtimeConversation,
|
||||
/// Connect app-server to the ChatGPT remote control service.
|
||||
///
|
||||
/// Runtime/requirements-only gate: this should not be overridden from
|
||||
/// config.toml.
|
||||
RemoteControl,
|
||||
/// Removed compatibility flag retained as a no-op so old wrappers can
|
||||
/// still pass `--enable image_detail_original`.
|
||||
@@ -258,6 +261,12 @@ impl Feature {
|
||||
self.info().default_enabled
|
||||
}
|
||||
|
||||
/// Whether `[features]` entries in config.toml are allowed to override this
|
||||
/// feature's effective state.
|
||||
pub fn can_override_from_config_toml(self) -> bool {
|
||||
!matches!(self, Feature::RemoteControl)
|
||||
}
|
||||
|
||||
fn info(self) -> &'static FeatureSpec {
|
||||
FEATURES
|
||||
.iter()
|
||||
@@ -630,6 +639,8 @@ impl FeaturesToml {
|
||||
materialize_resolved_feature_enabled(apps_mcp_path_override, enabled);
|
||||
} else if spec.id == Feature::NetworkProxy {
|
||||
materialize_resolved_feature_enabled(network_proxy, enabled);
|
||||
} else if !spec.id.can_override_from_config_toml() {
|
||||
entries.remove(spec.key);
|
||||
} else {
|
||||
entries.insert(spec.key.to_string(), enabled);
|
||||
}
|
||||
|
||||
@@ -567,6 +567,10 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
|
||||
let entries = features_toml.entries();
|
||||
assert_eq!(entries.get("include_apply_patch_tool"), None);
|
||||
for spec in crate::FEATURES {
|
||||
if !spec.id.can_override_from_config_toml() {
|
||||
assert_eq!(entries.get(spec.key), None, "{}", spec.key);
|
||||
continue;
|
||||
}
|
||||
assert_eq!(
|
||||
entries.get(spec.key),
|
||||
Some(&features.enabled(spec.id)),
|
||||
|
||||
Reference in New Issue
Block a user