Compare commits

...

1 Commits

Author SHA1 Message Date
Anton Panasenko
dd8dbe8747 fix: ignore remote control config override 2026-05-11 21:23:13 -07:00
8 changed files with 217 additions and 3 deletions

View File

@@ -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`).

View File

@@ -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()?;

View File

@@ -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>> {

View File

@@ -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))
);
}

View File

@@ -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()?;

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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)),