@@ -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