Allow disabling the in-process code-mode host fallback (#35266)

## What changed

- Allow `features.code_mode_host` to use a configuration table with
  `disable_in_process_fallback`. When enabled, failure to start the standalone
  host is returned as tool output instead of falling back to embedded V8.
- Preserve the existing fallback behavior by default and continue accepting the
  boolean feature toggle.
- Limit displayed host paths in spawn errors to 512 bytes while retaining the
  executable-bearing suffix and valid UTF-8 boundaries.

## Testing

- Cover boolean and table-based feature configuration, fallback-disabled host
  failures, and bounded ASCII and UTF-8 error paths.

GitOrigin-RevId: ab3d014e79054c2f8beef9a658915f01cca197b2
This commit is contained in:
Channing Conger
2026-07-24 23:56:57 +00:00
committed by copyberry
parent a453588416
commit cba0e2701c
14 changed files with 297 additions and 18 deletions

View File

@@ -29,6 +29,26 @@ impl FeatureConfig for CodeModeConfigToml {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CodeModeHostConfigToml {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
/// Fail instead of running embedded V8 when the standalone host is unavailable.
#[serde(skip_serializing_if = "Option::is_none")]
pub disable_in_process_fallback: Option<bool>,
}
impl FeatureConfig for CodeModeHostConfigToml {
fn enabled(&self) -> Option<bool> {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = Some(enabled);
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct NonPrefixedMcpToolNamesConfigToml {

View File

@@ -17,6 +17,7 @@ use toml::Table;
mod feature_configs;
mod legacy;
pub use feature_configs::CodeModeConfigToml;
pub use feature_configs::CodeModeHostConfigToml;
pub use feature_configs::CurrentTimeReminderConfigToml;
pub use feature_configs::CurrentTimeReminderDeliveryMode;
pub use feature_configs::CurrentTimeSource;
@@ -652,6 +653,8 @@ pub struct FeaturesToml {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code_mode: Option<FeatureToml<CodeModeConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code_mode_host: Option<FeatureToml<CodeModeHostConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub non_prefixed_mcp_tool_names: Option<FeatureToml<NonPrefixedMcpToolNamesConfigToml>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
@@ -690,6 +693,9 @@ impl FeaturesToml {
if let Some(enabled) = self.code_mode.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::CodeMode.key().to_string(), enabled);
}
if let Some(enabled) = self.code_mode_host.as_ref().and_then(FeatureToml::enabled) {
entries.insert(Feature::CodeModeHost.key().to_string(), enabled);
}
if let Some(enabled) = self
.non_prefixed_mcp_tool_names
.as_ref()
@@ -723,6 +729,7 @@ impl FeaturesToml {
self.clear_removed_compatibility_entries();
let Self {
code_mode,
code_mode_host,
non_prefixed_mcp_tool_names,
multi_agent_v2,
token_budget,
@@ -739,6 +746,8 @@ impl FeaturesToml {
let enabled = features.enabled(spec.id);
if spec.id == Feature::CodeMode {
materialize_resolved_feature_enabled(code_mode, enabled);
} else if spec.id == Feature::CodeModeHost {
materialize_resolved_feature_enabled(code_mode_host, enabled);
} else if spec.id == Feature::NonPrefixedMcpToolNames {
materialize_resolved_feature_enabled(non_prefixed_mcp_tool_names, enabled);
} else if spec.id == Feature::MultiAgentV2 {

View File

@@ -85,6 +85,42 @@ fn code_mode_only_requires_code_mode() {
assert_eq!(features.enabled(Feature::CodeMode), true);
}
#[test]
fn code_mode_host_feature_config_preserves_boolean_toggle() {
let features: FeaturesToml =
toml::from_str("code_mode_host = false").expect("features table should deserialize");
assert_eq!(features.code_mode_host, Some(FeatureToml::Enabled(false)));
assert_eq!(
features.entries(),
BTreeMap::from([("code_mode_host".to_string(), false)])
);
}
#[test]
fn code_mode_host_feature_config_deserializes_fallback_setting() {
let features: FeaturesToml = toml::from_str(
r#"
[code_mode_host]
enabled = true
disable_in_process_fallback = true
"#,
)
.expect("features table should deserialize");
assert_eq!(
features.code_mode_host,
Some(FeatureToml::Config(crate::CodeModeHostConfigToml {
enabled: Some(true),
disable_in_process_fallback: Some(true),
}))
);
assert_eq!(
features.entries(),
BTreeMap::from([("code_mode_host".to_string(), true)])
);
}
#[test]
fn from_sources_ignores_removed_terminal_resize_reflow_feature_key() {
let features_toml = FeaturesToml::from(BTreeMap::from([(
@@ -495,6 +531,10 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
features.enable(Feature::RespectSystemProxy);
let mut features_toml = FeaturesToml {
code_mode_host: Some(FeatureToml::Config(crate::CodeModeHostConfigToml {
enabled: Some(false),
disable_in_process_fallback: Some(true),
})),
multi_agent_v2: Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml {
enabled: Some(false),
min_wait_timeout_ms: Some(2500),
@@ -526,6 +566,13 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
spec.key
);
}
assert_eq!(
features_toml.code_mode_host,
Some(FeatureToml::Config(crate::CodeModeHostConfigToml {
enabled: Some(true),
disable_in_process_fallback: Some(true),
}))
);
assert_eq!(
features_toml.multi_agent_v2,
Some(FeatureToml::Config(crate::MultiAgentV2ConfigToml {