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: "kimi",
142        name: "Kimi (Moonshot AI)",
143        base_url: "https://api.moonshot.ai/v1",
144        env_vars: &["MOONSHOT_API_KEY", "KIMI_API_KEY"],
145    },
146];
147
148/// Proxy endpoint allowlist for OpenAI-compatible providers (and the local
149/// endpoint). The typed broker proxy may only reach these relative paths —
150/// a signed proxy request can never be steered at other same-host endpoints
151/// (key-management, billing, admin APIs, …).
152pub const OPENAI_COMPAT_ALLOWED_PATHS: &[&str] = &["/models", "/chat/completions"];
153
154/// The per-provider proxy path allowlist. Every static provider in the table
155/// is OpenAI-compatible today, so they share one catalog; the lookup is
156/// per-provider so a future non-uniform provider gets its own list instead of
157/// a widened shared one. Unknown providers get an empty allowlist (fail closed).
158pub fn allowed_proxy_paths(key: &str) -> &'static [&'static str] {
159    if key == LOCAL_PROVIDER_KEY || static_provider(key).is_some() {
160        OPENAI_COMPAT_ALLOWED_PATHS
161    } else {
162        &[]
163    }
164}
165
166/// Look up a static provider spec by key.
167pub fn static_provider(key: &str) -> Option<&'static StaticProviderSpec> {
168    STATIC_PROVIDERS.iter().find(|s| s.key == key)
169}
170
171/// True if `key` names a static-key provider (not `local`, not OAuth).
172pub fn is_static_provider(key: &str) -> bool {
173    static_provider(key).is_some()
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use std::collections::HashSet;
180    use std::str::FromStr;
181
182    /// Registry invariant: provider keys are unique.
183    #[test]
184    fn static_provider_keys_are_unique() {
185        let mut seen = HashSet::new();
186        for spec in STATIC_PROVIDERS {
187            assert!(
188                seen.insert(spec.key),
189                "duplicate static provider key: {}",
190                spec.key
191            );
192        }
193    }
194
195    /// Registry invariant: pinned base URLs are HTTPS — the broker must never
196    /// send a bearer key over plaintext to a remote host.
197    #[test]
198    fn static_provider_base_urls_are_https() {
199        for spec in STATIC_PROVIDERS {
200            assert!(
201                spec.base_url.starts_with("https://"),
202                "{} base_url must be https: {}",
203                spec.key,
204                spec.base_url
205            );
206        }
207    }
208
209    /// Cross-registry invariant: static provider keys never collide with
210    /// canonical OAuth provider IDs or the local pseudo-provider.
211    #[test]
212    fn static_keys_do_not_collide_with_oauth_ids_or_local() {
213        for spec in STATIC_PROVIDERS {
214            assert!(
215                crate::core::auth::provider::OAuthProviderId::from_str(spec.key).is_err(),
216                "static provider key {} collides with an OAuth provider id",
217                spec.key
218            );
219            assert_ne!(spec.key, LOCAL_PROVIDER_KEY);
220        }
221    }
222
223    #[test]
224    fn lookup_finds_known_and_rejects_unknown() {
225        assert_eq!(static_provider("groq").unwrap().name, "Groq");
226        assert!(static_provider("local").is_none());
227        assert!(static_provider("anthropic").is_none());
228        assert!(!is_static_provider("no-such-provider"));
229    }
230
231    /// Allowlist invariant: every proxyable provider gets exactly the
232    /// cataloged OpenAI-compatible paths; unknown keys get nothing.
233    #[test]
234    fn allowed_proxy_paths_fail_closed() {
235        for spec in STATIC_PROVIDERS {
236            assert_eq!(allowed_proxy_paths(spec.key), OPENAI_COMPAT_ALLOWED_PATHS);
237        }
238        assert_eq!(
239            allowed_proxy_paths(LOCAL_PROVIDER_KEY),
240            OPENAI_COMPAT_ALLOWED_PATHS
241        );
242        assert!(allowed_proxy_paths("anthropic").is_empty());
243        assert!(allowed_proxy_paths("no-such-provider").is_empty());
244        for path in OPENAI_COMPAT_ALLOWED_PATHS {
245            assert!(path.starts_with('/'), "catalog paths are relative: {path}");
246        }
247    }
248}