Skip to content

Commit de881b7

Browse files
authored
fix(core): ignore project MCP server settings
Ignore project-local MCP server definitions before hierarchical settings merge while preserving trusted global MCP server settings, including per-project MCP servers, during field-wise project settings merges.
1 parent b339e1d commit de881b7

1 file changed

Lines changed: 122 additions & 4 deletions

File tree

  • src-rust/crates/core/src

src-rust/crates/core/src/lib.rs

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,13 +1606,14 @@ pub mod config {
16061606
}
16071607

16081608
/// Load settings from all config levels and merge them.
1609-
/// Priority: project > global.
1609+
/// Priority: project > global, except project-local executable MCP
1610+
/// server definitions are ignored before merging.
16101611
pub async fn load_hierarchical(cwd: &std::path::Path) -> Self {
16111612
// 1. Load global settings.
16121613
let mut merged = Self::load().await.unwrap_or_default();
1613-
// 2. Find and merge project settings (project wins).
1614+
// 2. Find and merge project settings (safe project fields win).
16141615
if let Some(project_settings) = Self::find_project_settings(cwd).await {
1615-
merged = Self::merge(merged, project_settings);
1616+
merged = Self::merge(merged, Self::sanitize_project_settings(project_settings));
16161617
}
16171618
merged
16181619
}
@@ -1645,6 +1646,21 @@ pub mod config {
16451646
None
16461647
}
16471648

1649+
/// Remove project-local settings that can execute commands during startup.
1650+
///
1651+
/// Repository settings are untrusted until the user explicitly chooses to
1652+
/// trust a project. Keep non-executable project preferences, but never let a
1653+
/// repository contribute MCP server definitions that are auto-connected at
1654+
/// startup.
1655+
fn sanitize_project_settings(mut settings: Self) -> Self {
1656+
settings.config.mcp_servers.clear();
1657+
settings.config.enable_all_mcp_servers = false;
1658+
for project in settings.projects.values_mut() {
1659+
project.mcp_servers.clear();
1660+
}
1661+
settings
1662+
}
1663+
16481664
/// Merge two settings with `override_settings` taking priority.
16491665
/// Simple strategy: override wins for all scalar fields; Vecs are
16501666
/// concatenated (deduped); HashMaps are merged (override wins on collision).
@@ -1657,6 +1673,28 @@ pub mod config {
16571673
for (k, v) in over { base.insert(k, v); }
16581674
base
16591675
}
1676+
fn merge_project_settings(
1677+
mut base: HashMap<String, ProjectSettings>,
1678+
over: HashMap<String, ProjectSettings>,
1679+
) -> HashMap<String, ProjectSettings> {
1680+
for (key, project) in over {
1681+
match base.get_mut(&key) {
1682+
Some(existing) => {
1683+
existing.allowed_tools.extend(project.allowed_tools);
1684+
existing.allowed_tools.dedup();
1685+
existing.custom_system_prompt =
1686+
project.custom_system_prompt.or(existing.custom_system_prompt.take());
1687+
// Keep trusted global per-project MCP servers. Project settings are
1688+
// sanitized before this merge, so repo-provided MCP servers are empty
1689+
// and must not erase trusted global entries.
1690+
}
1691+
None => {
1692+
base.insert(key, project);
1693+
}
1694+
}
1695+
}
1696+
base
1697+
}
16601698
// Merge the embedded Config structs.
16611699
let merged_config = Config {
16621700
api_key: over.config.api_key.or(base.config.api_key),
@@ -1710,7 +1748,7 @@ pub mod config {
17101748
Self {
17111749
config: merged_config,
17121750
version: over.version.or(base.version),
1713-
projects: merge_map(base.projects, over.projects),
1751+
projects: merge_project_settings(base.projects, over.projects),
17141752
remote_control_at_startup: over.remote_control_at_startup || base.remote_control_at_startup,
17151753
permission_rules: { let mut v = base.permission_rules; v.extend(over.permission_rules); v },
17161754
enabled_plugins: { let mut s = base.enabled_plugins; s.extend(over.enabled_plugins); s },
@@ -1818,6 +1856,86 @@ pub mod config {
18181856
}
18191857
result
18201858
}
1859+
1860+
#[cfg(test)]
1861+
mod tests {
1862+
use super::*;
1863+
1864+
#[test]
1865+
fn project_settings_do_not_merge_mcp_servers() {
1866+
let global = Settings {
1867+
config: Config {
1868+
mcp_servers: vec![McpServerConfig {
1869+
name: "global".to_string(),
1870+
command: Some("trusted-command".to_string()),
1871+
args: vec!["--global".to_string()],
1872+
env: HashMap::new(),
1873+
url: None,
1874+
server_type: "stdio".to_string(),
1875+
}],
1876+
enable_all_mcp_servers: true,
1877+
..Default::default()
1878+
},
1879+
projects: HashMap::from([(
1880+
"repo".to_string(),
1881+
ProjectSettings {
1882+
allowed_tools: Vec::new(),
1883+
mcp_servers: vec![McpServerConfig {
1884+
name: "trusted-project".to_string(),
1885+
command: Some("trusted-project-command".to_string()),
1886+
args: Vec::new(),
1887+
env: HashMap::new(),
1888+
url: None,
1889+
server_type: "stdio".to_string(),
1890+
}],
1891+
custom_system_prompt: None,
1892+
},
1893+
)]),
1894+
..Default::default()
1895+
};
1896+
1897+
let project = Settings {
1898+
config: Config {
1899+
model: Some("project-model".to_string()),
1900+
mcp_servers: vec![McpServerConfig {
1901+
name: "project".to_string(),
1902+
command: Some("sh".to_string()),
1903+
args: vec!["-c".to_string(), "payload".to_string()],
1904+
env: HashMap::from([("STEAL_ME".to_string(), "secret".to_string())]),
1905+
url: None,
1906+
server_type: "stdio".to_string(),
1907+
}],
1908+
enable_all_mcp_servers: true,
1909+
..Default::default()
1910+
},
1911+
projects: HashMap::from([(
1912+
"repo".to_string(),
1913+
ProjectSettings {
1914+
allowed_tools: Vec::new(),
1915+
mcp_servers: vec![McpServerConfig {
1916+
name: "legacy-project".to_string(),
1917+
command: Some("sh".to_string()),
1918+
args: vec!["-c".to_string(), "payload".to_string()],
1919+
env: HashMap::new(),
1920+
url: None,
1921+
server_type: "stdio".to_string(),
1922+
}],
1923+
custom_system_prompt: None,
1924+
},
1925+
)]),
1926+
..Default::default()
1927+
};
1928+
1929+
let merged = Settings::merge(global, Settings::sanitize_project_settings(project));
1930+
1931+
assert_eq!(merged.config.model.as_deref(), Some("project-model"));
1932+
assert_eq!(merged.config.mcp_servers.len(), 1);
1933+
assert_eq!(merged.config.mcp_servers[0].name, "global");
1934+
assert!(merged.config.enable_all_mcp_servers);
1935+
assert_eq!(merged.projects["repo"].mcp_servers.len(), 1);
1936+
assert_eq!(merged.projects["repo"].mcp_servers[0].name, "trusted-project");
1937+
}
1938+
}
18211939
}
18221940

18231941
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)