From 39f2d9dd446cfcfc8bd7b983fb6b1dccb168b508 Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Thu, 5 Mar 2026 02:07:31 -0500 Subject: [PATCH] fix(reliability): validate fallback API key mapping --- src/config/schema.rs | 57 ++++++++++++++++++++++++++ src/providers/mod.rs | 2 +- tests/reliability_fallback_api_keys.rs | 1 + 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/config/schema.rs b/src/config/schema.rs index e90024d55..48879a911 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -5952,6 +5952,29 @@ impl Config { anyhow::bail!("gateway.host must not be empty"); } + // Reliability + let configured_fallbacks = self + .reliability + .fallback_providers + .iter() + .map(|provider| provider.trim()) + .filter(|provider| !provider.is_empty()) + .collect::>(); + for (entry, api_key) in &self.reliability.fallback_api_keys { + let normalized_entry = entry.trim(); + if normalized_entry.is_empty() { + anyhow::bail!("reliability.fallback_api_keys contains an empty key"); + } + if api_key.trim().is_empty() { + anyhow::bail!("reliability.fallback_api_keys.{normalized_entry} must not be empty"); + } + if !configured_fallbacks.contains(normalized_entry) { + anyhow::bail!( + "reliability.fallback_api_keys.{normalized_entry} has no matching entry in reliability.fallback_providers" + ); + } + } + // Autonomy if self.autonomy.max_actions_per_hour == 0 { anyhow::bail!("autonomy.max_actions_per_hour must be greater than 0"); @@ -10412,6 +10435,40 @@ baseline_syscalls = ["read", "write", "openat", "close"] assert!(err.to_string().contains("gated_domains")); } + #[test] + async fn reliability_validation_rejects_empty_fallback_api_key_value() { + let mut config = Config::default(); + config.reliability.fallback_providers = vec!["openrouter".to_string()]; + config + .reliability + .fallback_api_keys + .insert("openrouter".to_string(), " ".to_string()); + + let err = config + .validate() + .expect_err("expected fallback_api_keys empty value validation failure"); + assert!(err + .to_string() + .contains("reliability.fallback_api_keys.openrouter must not be empty")); + } + + #[test] + async fn reliability_validation_rejects_unmapped_fallback_api_key_entry() { + let mut config = Config::default(); + config.reliability.fallback_providers = vec!["openrouter".to_string()]; + config + .reliability + .fallback_api_keys + .insert("anthropic".to_string(), "sk-ant-test".to_string()); + + let err = config + .validate() + .expect_err("expected fallback_api_keys mapping validation failure"); + assert!(err + .to_string() + .contains("reliability.fallback_api_keys.anthropic has no matching entry")); + } + #[test] async fn security_validation_rejects_unknown_domain_category() { let mut config = Config::default(); diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 92a0b002c..38291f9e4 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -1442,7 +1442,7 @@ pub fn create_resilient_provider_with_options( Ok(provider) => providers.push((fallback.clone(), provider)), Err(_error) => { tracing::warn!( - fallback_provider = fallback, + fallback_provider = provider_name, "Ignoring invalid fallback provider during initialization" ); } diff --git a/tests/reliability_fallback_api_keys.rs b/tests/reliability_fallback_api_keys.rs index a9e9551aa..a88378b61 100644 --- a/tests/reliability_fallback_api_keys.rs +++ b/tests/reliability_fallback_api_keys.rs @@ -90,6 +90,7 @@ async fn fallback_api_keys_support_multiple_custom_endpoints() { assert_eq!(reply, "response-from-fallback-two"); + primary_server.verify().await; fallback_server_one.verify().await; fallback_server_two.verify().await; }