Skip to main content

agent_core/core/auth/
static_providers.rs

1//! Static-key provider connection metadata, owned by the credential broker.
2//!
3//! This table is the broker-side source of truth for where a static API key
4//! may be discovered (env vars / login config) and where broker-proxied
5//! requests for that provider are allowed to go. Runtime code never resolves
6//! these secrets itself — it references providers by `key` and the broker
7//! applies the credential.
8//!
9//! The engine-side model catalog (`runtime::openai::registry`) joins on `key`
10//! for model listings; base URLs and credential discovery live here so the
11//! broker can pin proxy destinations and never accept attacker-supplied URLs
12//! for static-key providers.
13
14/// Connection/credential metadata for one static-key provider.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct StaticProviderSpec {
17    /// Canonical provider key (also the login-config `provider.<key>` name).
18    pub key: &'static str,
19    /// Human-readable name (safe for UI/status output).
20    pub name: &'static str,
21    /// Pinned HTTPS base URL. Broker-proxied requests are joined onto this;
22    /// clients cannot override it for static-key providers.
23    pub base_url: &'static str,
24    /// Environment variables the broker may discover the key from.
25    pub env_vars: &'static [&'static str],
26}
27
28/// The local OpenAI-compatible endpoint pseudo-provider (Ollama, LM Studio…).
29/// Its endpoint URL is non-secret user configuration; its optional key is
30/// still broker-owned.
31pub const LOCAL_PROVIDER_KEY: &str = "local";
32
33/// Default URL for the local endpoint when nothing is configured.
34pub const LOCAL_DEFAULT_BASE_URL: &str = "http://localhost:11434/v1";
35
36/// All known static-key providers. Broker-owned metadata; no secrets here.
37pub const STATIC_PROVIDERS: &[StaticProviderSpec] = &[
38    StaticProviderSpec {
39        key: "groq",
40        name: "Groq",
41        base_url: "https://api.groq.com/openai/v1",
42        env_vars: &["GROQ_API_KEY"],
43    },
44    StaticProviderSpec {
45        key: "cerebras",
46        name: "Cerebras",
47        base_url: "https://api.cerebras.ai/v1",
48        env_vars: &["CEREBRAS_API_KEY"],
49    },
50    StaticProviderSpec {
51        key: "nvidia",
52        name: "NVIDIA NIM",
53        base_url: "https://integrate.api.nvidia.com/v1",
54        env_vars: &["NVIDIA_API_KEY"],
55    },
56    StaticProviderSpec {
57        key: "sambanova",
58        name: "SambaNova",
59        base_url: "https://api.sambanova.ai/v1",
60        env_vars: &["SAMBANOVA_API_KEY"],
61    },
62    StaticProviderSpec {
63        key: "openrouter",
64        name: "OpenRouter",
65        base_url: "https://openrouter.ai/api/v1",
66        env_vars: &["OPENROUTER_API_KEY"],
67    },
68    StaticProviderSpec {
69        key: "google",
70        name: "Google AI Studio",
71        base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
72        env_vars: &["GOOGLE_API_KEY"],
73    },
74    StaticProviderSpec {
75        key: "deepinfra",
76        name: "DeepInfra",
77        base_url: "https://api.deepinfra.com/v1/openai",
78        env_vars: &["DEEPINFRA_API_KEY", "DEEPINFRA_TOKEN"],
79    },
80    StaticProviderSpec {
81        key: "huggingface",
82        name: "HuggingFace",
83        base_url: "https://router.huggingface.co/v1",
84        env_vars: &["HUGGINGFACE_API_KEY", "HF_TOKEN"],
85    },
86    StaticProviderSpec {
87        key: "fireworks",
88        name: "Fireworks AI",
89        base_url: "https://api.fireworks.ai/inference/v1",
90        env_vars: &["FIREWORKS_API_KEY"],
91    },
92    StaticProviderSpec {
93        key: "hyperbolic",
94        name: "Hyperbolic",
95        base_url: "https://api.hyperbolic.xyz/v1",
96        env_vars: &["HYPERBOLIC_API_KEY"],
97    },
98    StaticProviderSpec {
99        key: "scaleway",
100        name: "Scaleway",
101        base_url: "https://api.scaleway.ai/v1",
102        env_vars: &["SCALEWAY_API_KEY"],
103    },
104    StaticProviderSpec {
105        key: "siliconflow",
106        name: "SiliconFlow",
107        base_url: "https://api.siliconflow.cn/v1",
108        env_vars: &["SILICONFLOW_API_KEY"],
109    },
110    StaticProviderSpec {
111        key: "together",
112        name: "Together AI",
113        base_url: "https://api.together.xyz/v1",
114        env_vars: &["TOGETHER_API_KEY"],
115    },
116    StaticProviderSpec {
117        key: "chutes",
118        name: "Chutes AI",
119        base_url: "https://llm.chutes.ai/v1",
120        env_vars: &["CHUTES_API_KEY"],
121    },
122    StaticProviderSpec {
123        key: "codestral",
124        name: "Codestral (Mistral)",
125        base_url: "https://api.mistral.ai/v1",
126        env_vars: &["CODESTRAL_API_KEY"],
127    },
128    StaticProviderSpec {
129        key: "perplexity",
130        name: "Perplexity",
131        base_url: "https://api.perplexity.ai",
132        env_vars: &["PERPLEXITY_API_KEY", "PPLX_API_KEY"],
133    },
134    StaticProviderSpec {
135        key: "ovhcloud",
136        name: "OVHcloud",
137        base_url: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1",
138        env_vars: &["OVH_AI_ENDPOINTS_ACCESS_TOKEN"],
139    },
140    StaticProviderSpec {
141        key: "inferx",
142        name: "InferX",
143        base_url: "https://model.inferx.net/endpoints/v1",
144        env_vars: &["INFERX_API_KEY"],
145    },
146    StaticProviderSpec {
147        key: "kimi",
148        name: "Kimi (Moonshot AI)",
149        base_url: "https://api.moonshot.ai/v1",
150        env_vars: &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
151    },
152];
153
154/// Proxy endpoint allowlist for OpenAI-compatible providers (and the local
155/// endpoint). The typed broker proxy may only reach these relative paths —
156/// a signed proxy request can never be steered at other same-host endpoints
157/// (key-management, billing, admin APIs, …).
158pub const OPENAI_COMPAT_ALLOWED_PATHS: &[&str] = &["/models", "/chat/completions"];
159
160/// The per-provider proxy path allowlist. Every static provider in the table
161/// is OpenAI-compatible today, so they share one catalog; the lookup is
162/// per-provider so a future non-uniform provider gets its own list instead of
163/// a widened shared one. Unknown providers get an empty allowlist (fail closed).
164pub fn allowed_proxy_paths(key: &str) -> &'static [&'static str] {
165    if key == LOCAL_PROVIDER_KEY || static_provider(key).is_some() {
166        OPENAI_COMPAT_ALLOWED_PATHS
167    } else {
168        &[]
169    }
170}
171
172/// Look up a static provider spec by key.
173pub fn static_provider(key: &str) -> Option<&'static StaticProviderSpec> {
174    STATIC_PROVIDERS.iter().find(|s| s.key == key)
175}
176
177/// True if `key` names a static-key provider (not `local`, not OAuth).
178pub fn is_static_provider(key: &str) -> bool {
179    static_provider(key).is_some()
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use std::collections::HashSet;
186    use std::str::FromStr;
187
188    /// Registry invariant: provider keys are unique.
189    #[test]
190    fn static_provider_keys_are_unique() {
191        let mut seen = HashSet::new();
192        for spec in STATIC_PROVIDERS {
193            assert!(
194                seen.insert(spec.key),
195                "duplicate static provider key: {}",
196                spec.key
197            );
198        }
199    }
200
201    /// Registry invariant: pinned base URLs are HTTPS — the broker must never
202    /// send a bearer key over plaintext to a remote host.
203    #[test]
204    fn static_provider_base_urls_are_https() {
205        for spec in STATIC_PROVIDERS {
206            assert!(
207                spec.base_url.starts_with("https://"),
208                "{} base_url must be https: {}",
209                spec.key,
210                spec.base_url
211            );
212        }
213    }
214
215    /// Cross-registry invariant: static provider keys never collide with
216    /// canonical OAuth provider IDs or the local pseudo-provider.
217    #[test]
218    fn static_keys_do_not_collide_with_oauth_ids_or_local() {
219        for spec in STATIC_PROVIDERS {
220            assert!(
221                crate::core::auth::provider::OAuthProviderId::from_str(spec.key).is_err(),
222                "static provider key {} collides with an OAuth provider id",
223                spec.key
224            );
225            assert_ne!(spec.key, LOCAL_PROVIDER_KEY);
226        }
227    }
228
229    #[test]
230    fn lookup_finds_known_and_rejects_unknown() {
231        assert_eq!(static_provider("groq").unwrap().name, "Groq");
232        assert!(static_provider("local").is_none());
233        assert!(static_provider("anthropic").is_none());
234        assert!(!is_static_provider("no-such-provider"));
235    }
236
237    /// Allowlist invariant: every proxyable provider gets exactly the
238    /// cataloged OpenAI-compatible paths; unknown keys get nothing.
239    #[test]
240    fn allowed_proxy_paths_fail_closed() {
241        for spec in STATIC_PROVIDERS {
242            assert_eq!(allowed_proxy_paths(spec.key), OPENAI_COMPAT_ALLOWED_PATHS);
243        }
244        assert_eq!(
245            allowed_proxy_paths(LOCAL_PROVIDER_KEY),
246            OPENAI_COMPAT_ALLOWED_PATHS
247        );
248        assert!(allowed_proxy_paths("anthropic").is_empty());
249        assert!(allowed_proxy_paths("no-such-provider").is_empty());
250        for path in OPENAI_COMPAT_ALLOWED_PATHS {
251            assert!(path.starts_with('/'), "catalog paths are relative: {path}");
252        }
253    }
254}