From a4e42dc7342fe26c67f0b6144812591126e6a5ef Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Sun, 26 Jul 2026 12:11:08 -0500 Subject: [PATCH] fix(hosted): disable project LSP execution in reviews --- src-rust/crates/cli/src/main.rs | 12 ++++++++-- src-rust/crates/core/src/lib.rs | 40 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src-rust/crates/cli/src/main.rs b/src-rust/crates/cli/src/main.rs index bb5030c..2872be1 100644 --- a/src-rust/crates/cli/src/main.rs +++ b/src-rust/crates/cli/src/main.rs @@ -1533,17 +1533,25 @@ fn filter_tools_for_hosted_review( return tools; } - filter_read_only_tools(&tools) + filter_read_only_tools_except(&tools, &["LSP"]) } fn filter_read_only_tools( tools: &[Box], +) -> Arc>> { + filter_read_only_tools_except(tools, &[]) +} + +fn filter_read_only_tools_except( + tools: &[Box], + excluded_names: &[&str], ) -> Arc>> { use claurst_tools::PermissionLevel as PL; // Collect names of tools that are read-only, then rebuild from all_tools // (Box is not Clone so we can't directly filter-and-keep). let allowed_names: Vec = tools .iter() + .filter(|t| !excluded_names.contains(&t.name())) .filter(|t| { matches!(t.permission_level(), PL::ReadOnly | PL::None) || t.name() == "AskUserQuestion" }) @@ -5347,7 +5355,7 @@ mod tests { let names = tool_names(&filter_tools_for_hosted_review(all, &config)); assert!(names.contains(&"Read".to_string())); - for forbidden in ["Bash", "Edit", "Write", "NotebookEdit", "ApplyPatch"] { + for forbidden in ["Bash", "Edit", "Write", "NotebookEdit", "ApplyPatch", "LSP"] { assert!( !names.contains(&forbidden.to_string()), "hosted review default tools must not include {forbidden}, got {names:?}" diff --git a/src-rust/crates/core/src/lib.rs b/src-rust/crates/core/src/lib.rs index f247668..ffd7bcb 100644 --- a/src-rust/crates/core/src/lib.rs +++ b/src-rust/crates/core/src/lib.rs @@ -1656,6 +1656,7 @@ pub mod config { settings.config.provider = None; settings.config.provider_configs.clear(); settings.config.mcp_servers.clear(); + settings.config.lsp_servers.clear(); settings.config.hooks.clear(); settings.config.enable_all_mcp_servers = false; for project in settings.projects.values_mut() { @@ -2075,6 +2076,45 @@ pub mod config { ); } + #[test] + fn project_settings_do_not_merge_lsp_servers() { + let global = Settings { + config: Config { + lsp_servers: vec![crate::lsp::LspServerConfig { + name: "trusted-rust".to_string(), + command: "rust-analyzer".to_string(), + args: Vec::new(), + file_patterns: vec!["*.rs".to_string()], + initialization_options: None, + extension_to_language: HashMap::new(), + env: HashMap::new(), + }], + ..Default::default() + }, + ..Default::default() + }; + let project = Settings { + config: Config { + lsp_servers: vec![crate::lsp::LspServerConfig { + name: "project-controlled".to_string(), + command: "sh".to_string(), + args: vec!["-c".to_string(), "payload".to_string()], + file_patterns: vec!["*.rs".to_string()], + initialization_options: None, + extension_to_language: HashMap::new(), + env: HashMap::new(), + }], + ..Default::default() + }, + ..Default::default() + }; + + let merged = Settings::merge(global, Settings::sanitize_project_settings(project)); + + assert_eq!(merged.config.lsp_servers.len(), 1); + assert_eq!(merged.config.lsp_servers[0].name, "trusted-rust"); + } + #[test] fn hosted_review_config_deserializes_from_camel_case() { let settings: Settings =