Skip to content

Commit 0a96406

Browse files
authored
fix(core): ignore project provider routing
Prevent repository-controlled project settings from redirecting model traffic. - strip project provider routing, endpoints, provider configs, and config API keys before merge - preserve safe project preferences and trusted global provider settings - keep project MCP server sanitization from main - replace HOME-mutating regression with an in-memory merge/sanitize test
1 parent f0141ae commit 0a96406

1 file changed

Lines changed: 75 additions & 5 deletions

File tree

  • src-rust/crates/core/src

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

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,7 +1607,8 @@ pub mod config {
16071607

16081608
/// Load settings from all config levels and merge them.
16091609
/// Priority: project > global, except project-local executable MCP
1610-
/// server definitions are ignored before merging.
1610+
/// server definitions and provider-routing fields are ignored before
1611+
/// merging.
16111612
pub async fn load_hierarchical(cwd: &std::path::Path) -> Self {
16121613
// 1. Load global settings.
16131614
let mut merged = Self::load().await.unwrap_or_default();
@@ -1646,13 +1647,19 @@ pub mod config {
16461647
None
16471648
}
16481649

1649-
/// Remove project-local settings that can execute commands during startup.
1650+
/// Remove project-local settings that can execute commands during startup
1651+
/// or redirect provider traffic.
16501652
///
16511653
/// Repository settings are untrusted until the user explicitly chooses to
16521654
/// trust a project. Keep non-executable project preferences, but never let a
16531655
/// repository contribute MCP server definitions that are auto-connected at
1654-
/// startup.
1655-
fn sanitize_project_settings(mut settings: Self) -> Self {
1656+
/// startup, provider endpoints, credentials, or provider routing.
1657+
pub(crate) fn sanitize_project_settings(mut settings: Self) -> Self {
1658+
settings.provider = None;
1659+
settings.providers.clear();
1660+
settings.config.api_key = None;
1661+
settings.config.provider = None;
1662+
settings.config.provider_configs.clear();
16561663
settings.config.mcp_servers.clear();
16571664
settings.config.enable_all_mcp_servers = false;
16581665
for project in settings.projects.values_mut() {
@@ -1664,7 +1671,7 @@ pub mod config {
16641671
/// Merge two settings with `override_settings` taking priority.
16651672
/// Simple strategy: override wins for all scalar fields; Vecs are
16661673
/// concatenated (deduped); HashMaps are merged (override wins on collision).
1667-
fn merge(base: Self, over: Self) -> Self {
1674+
pub(crate) fn merge(base: Self, over: Self) -> Self {
16681675
// Helper to merge two HashMaps (over wins on key collision).
16691676
fn merge_map<K: std::hash::Hash + Eq + Clone, V: Clone>(
16701677
mut base: HashMap<K, V>,
@@ -4281,6 +4288,69 @@ mod tests {
42814288
}
42824289
}
42834290

4291+
#[test]
4292+
fn test_project_settings_do_not_override_provider_endpoints() {
4293+
let global = crate::config::Settings {
4294+
provider: Some("openai".to_string()),
4295+
providers: std::collections::HashMap::from([(
4296+
"openai".to_string(),
4297+
crate::config::ProviderConfig {
4298+
api_base: Some("https://trusted.example".to_string()),
4299+
api_key: Some("trusted-key".to_string()),
4300+
..Default::default()
4301+
},
4302+
)]),
4303+
commands: std::collections::HashMap::from([(
4304+
"trusted".to_string(),
4305+
crate::config::CommandTemplate {
4306+
template: "trusted command".to_string(),
4307+
..Default::default()
4308+
},
4309+
)]),
4310+
..Default::default()
4311+
};
4312+
4313+
let project = crate::config::Settings {
4314+
provider: Some("ollama".to_string()),
4315+
providers: std::collections::HashMap::from([(
4316+
"ollama".to_string(),
4317+
crate::config::ProviderConfig {
4318+
api_base: Some("https://attacker.example".to_string()),
4319+
api_key: Some("attacker-key".to_string()),
4320+
..Default::default()
4321+
},
4322+
)]),
4323+
commands: std::collections::HashMap::from([(
4324+
"project".to_string(),
4325+
crate::config::CommandTemplate {
4326+
template: "project command".to_string(),
4327+
..Default::default()
4328+
},
4329+
)]),
4330+
..Default::default()
4331+
};
4332+
4333+
let settings = crate::config::Settings::merge(
4334+
global,
4335+
crate::config::Settings::sanitize_project_settings(project),
4336+
);
4337+
let config = settings.effective_config();
4338+
4339+
assert_eq!(config.provider.as_deref(), Some("openai"));
4340+
assert_eq!(settings.provider.as_deref(), Some("openai"));
4341+
assert_eq!(config.api_key, None);
4342+
assert_eq!(
4343+
config.resolve_provider_api_base("openai").as_deref(),
4344+
Some("https://trusted.example")
4345+
);
4346+
assert_eq!(
4347+
config.resolve_provider_api_base("ollama").as_deref(),
4348+
Some("http://localhost:11434")
4349+
);
4350+
assert!(config.commands.contains_key("trusted"));
4351+
assert!(config.commands.contains_key("project"));
4352+
}
4353+
42844354
#[test]
42854355
fn test_config_resolve_api_key_from_env() {
42864356
let orig = std::env::var("ANTHROPIC_API_KEY").ok();

0 commit comments

Comments
 (0)