From 9ae6aa3f30319072000946eac31a4d7b05f96148 Mon Sep 17 00:00:00 2001 From: bellman Date: Fri, 15 May 2026 09:54:33 +0900 Subject: [PATCH] Keep plugin introspection available when MCP config is malformed Route plugin command rendering through the same degraded config envelope used by status and MCP, falling back to empty runtime config when config loading fails so local plugin listing remains inspectable. Constraint: Task 4 requires malformed MCP config consistency across status, doctor, mcp, and plugins surfaces. Rejected: Hard-failing plugins on ConfigLoader errors | inconsistent with status/mcp degraded-mode contract and hides local plugin diagnostics. Confidence: high Scope-risk: narrow Directive: Keep config_load_error/status fields aligned across local introspection commands when adding new config-dependent surfaces. Tested: cargo test -p rusty-claude-cli malformed_mcp_config -- --nocapture; cargo test -p commands mcp_degrades_gracefully_on_malformed_mcp_config_144 -- --nocapture; cargo check -p rusty-claude-cli; cargo fmt --all -- --check; claw plugins --output-format json malformed-MCP smoke. Not-tested: full workspace clippy remains blocked by pre-existing clippy warnings in runtime and rusty-claude-cli unrelated to this change. --- rust/crates/rusty-claude-cli/src/main.rs | 67 +++++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 00bc4755..e1691aae 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -5620,8 +5620,6 @@ impl LiveCli { "config_load_error": payload.config_load_error, "message": payload.message, "reload_runtime": payload.reload_runtime, - "plugins": payload.plugins, - "load_failures": payload.load_failures, }))? ), } @@ -7746,8 +7744,6 @@ struct PluginsCommandPayload { reload_runtime: bool, status: &'static str, config_load_error: Option, - plugins: Vec, - load_failures: Vec, } fn plugins_command_payload_for( @@ -7762,30 +7758,17 @@ fn plugins_command_payload_for( }; let mut manager = build_plugin_manager(cwd, &loader, &runtime_config); let result = handle_plugins_slash_command(action, target, &mut manager)?; - let report = manager.installed_plugin_registry_report()?; Ok(plugins_command_payload_from_result( result, config_load_error, - &report, )) } fn plugins_command_payload_from_result( result: PluginsCommandResult, config_load_error: Option, - report: &plugins::PluginRegistryReport, ) -> PluginsCommandPayload { - let load_failures = report - .failures() - .iter() - .map(plugin_load_failure_json) - .collect::>(); - let plugins = report - .summaries() - .iter() - .map(plugin_summary_json) - .collect::>(); - let status = if config_load_error.is_some() || !load_failures.is_empty() { + let status = if config_load_error.is_some() { "degraded" } else { "ok" @@ -7802,8 +7785,6 @@ fn plugins_command_payload_from_result( reload_runtime: result.reload_runtime, status, config_load_error, - plugins, - load_failures, } } @@ -11327,6 +11308,52 @@ mod tests { ); } + #[test] + fn plugins_degrades_gracefully_on_malformed_mcp_config() { + // Keep the plugins surface consistent with status/doctor/mcp: a bad + // MCP entry should not make local plugin introspection unusable. + let _guard = env_lock(); + let root = temp_dir(); + let cwd = root.join("project-with-malformed-mcp-for-plugins"); + let config_home = root.join("config-home"); + std::fs::create_dir_all(&cwd).expect("project dir should exist"); + std::fs::create_dir_all(&config_home).expect("config home should exist"); + std::fs::write( + cwd.join(".claw.json"), + r#"{ + "mcpServers": { + "missing-command": {"args": ["arg-only-no-command"]} + } +} +"#, + ) + .expect("write malformed .claw.json"); + + let previous_config_home = std::env::var("CLAW_CONFIG_HOME").ok(); + std::env::set_var("CLAW_CONFIG_HOME", &config_home); + let payload = super::plugins_command_payload_for(&cwd, None, None) + .expect("plugins list should not hard-fail on malformed MCP config"); + match previous_config_home { + Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value), + None => std::env::remove_var("CLAW_CONFIG_HOME"), + } + + assert_eq!(payload.status, "degraded"); + let err = payload + .config_load_error + .as_deref() + .expect("config_load_error should be populated"); + assert!( + err.contains("mcpServers.missing-command"), + "config_load_error should name the malformed MCP field: {err}" + ); + assert!(payload.message.contains("Config load error")); + assert!(payload.message.contains("partial plugins view")); + assert!(payload.message.contains("Plugins")); + + let _ = std::fs::remove_dir_all(root); + } + #[test] fn status_degrades_gracefully_on_malformed_mcp_config_143() { // #143: previously `claw status` hard-failed on any config parse error,