Skip to main content

codineer_runtime/credentials/
env_resolver.rs

1use super::{CredentialError, CredentialResolver, ResolvedCredential};
2
3/// Resolves credentials from environment variables.
4///
5/// Supports a primary API key variable and an optional secondary bearer token variable.
6/// When both are set, produces `ApiKeyAndBearer`. When only one is set, produces
7/// the corresponding single-credential variant.
8#[derive(Debug, Clone)]
9pub struct EnvVarResolver {
10    id: &'static str,
11    display_name: &'static str,
12    api_key_env: &'static str,
13    bearer_env: Option<&'static str>,
14    priority: u16,
15}
16
17impl EnvVarResolver {
18    /// Generic constructor.
19    #[must_use]
20    pub const fn new(
21        id: &'static str,
22        display_name: &'static str,
23        api_key_env: &'static str,
24        bearer_env: Option<&'static str>,
25        priority: u16,
26    ) -> Self {
27        Self {
28            id,
29            display_name,
30            api_key_env,
31            bearer_env,
32            priority,
33        }
34    }
35
36    /// Resolver for Anthropic credentials (`ANTHROPIC_API_KEY` + `ANTHROPIC_AUTH_TOKEN`).
37    #[must_use]
38    pub const fn anthropic() -> Self {
39        Self::new(
40            "env",
41            "Environment Variables",
42            "ANTHROPIC_API_KEY",
43            Some("ANTHROPIC_AUTH_TOKEN"),
44            100,
45        )
46    }
47
48    /// Resolver for xAI credentials (`XAI_API_KEY`).
49    #[must_use]
50    pub const fn xai() -> Self {
51        Self::new("env", "Environment Variables", "XAI_API_KEY", None, 100)
52    }
53
54    /// Resolver for OpenAI credentials (`OPENAI_API_KEY`).
55    #[must_use]
56    pub const fn openai() -> Self {
57        Self::new("env", "Environment Variables", "OPENAI_API_KEY", None, 100)
58    }
59}
60
61fn read_env_non_empty(key: &str) -> Option<String> {
62    match std::env::var(key) {
63        Ok(value) if !value.is_empty() => Some(value),
64        _ => None,
65    }
66}
67
68impl CredentialResolver for EnvVarResolver {
69    fn id(&self) -> &str {
70        self.id
71    }
72
73    fn display_name(&self) -> &str {
74        self.display_name
75    }
76
77    fn priority(&self) -> u16 {
78        self.priority
79    }
80
81    fn resolve(&self) -> Result<Option<ResolvedCredential>, CredentialError> {
82        let api_key = read_env_non_empty(self.api_key_env);
83        let bearer = self.bearer_env.and_then(read_env_non_empty);
84
85        match (api_key, bearer) {
86            (Some(api_key), Some(bearer_token)) => Ok(Some(ResolvedCredential::ApiKeyAndBearer {
87                api_key,
88                bearer_token,
89            })),
90            (Some(api_key), None) => Ok(Some(ResolvedCredential::ApiKey(api_key))),
91            (None, Some(bearer_token)) => Ok(Some(ResolvedCredential::BearerToken(bearer_token))),
92            (None, None) => Ok(None),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
102        crate::test_env_lock()
103    }
104
105    #[test]
106    fn anthropic_resolves_api_key() {
107        let _guard = env_lock();
108        std::env::set_var("ANTHROPIC_API_KEY", "sk-test");
109        std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
110
111        let resolver = EnvVarResolver::anthropic();
112        let cred = resolver.resolve().unwrap();
113        assert_eq!(cred, Some(ResolvedCredential::ApiKey("sk-test".into())));
114
115        std::env::remove_var("ANTHROPIC_API_KEY");
116    }
117
118    #[test]
119    fn anthropic_resolves_both() {
120        let _guard = env_lock();
121        std::env::set_var("ANTHROPIC_API_KEY", "sk-key");
122        std::env::set_var("ANTHROPIC_AUTH_TOKEN", "bearer-tok");
123
124        let resolver = EnvVarResolver::anthropic();
125        let cred = resolver.resolve().unwrap();
126        assert_eq!(
127            cred,
128            Some(ResolvedCredential::ApiKeyAndBearer {
129                api_key: "sk-key".into(),
130                bearer_token: "bearer-tok".into(),
131            })
132        );
133
134        std::env::remove_var("ANTHROPIC_API_KEY");
135        std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
136    }
137
138    #[test]
139    fn anthropic_returns_none_when_unset() {
140        let _guard = env_lock();
141        std::env::remove_var("ANTHROPIC_API_KEY");
142        std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
143
144        let resolver = EnvVarResolver::anthropic();
145        assert_eq!(resolver.resolve().unwrap(), None);
146    }
147
148    #[test]
149    fn anthropic_bearer_only() {
150        let _guard = env_lock();
151        std::env::remove_var("ANTHROPIC_API_KEY");
152        std::env::set_var("ANTHROPIC_AUTH_TOKEN", "tok");
153
154        let resolver = EnvVarResolver::anthropic();
155        let cred = resolver.resolve().unwrap();
156        assert_eq!(cred, Some(ResolvedCredential::BearerToken("tok".into())));
157
158        std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
159    }
160
161    #[test]
162    fn empty_values_treated_as_absent() {
163        let _guard = env_lock();
164        std::env::set_var("ANTHROPIC_API_KEY", "");
165        std::env::set_var("ANTHROPIC_AUTH_TOKEN", "");
166
167        let resolver = EnvVarResolver::anthropic();
168        assert_eq!(resolver.resolve().unwrap(), None);
169
170        std::env::remove_var("ANTHROPIC_API_KEY");
171        std::env::remove_var("ANTHROPIC_AUTH_TOKEN");
172    }
173
174    #[test]
175    fn xai_resolver_reads_correct_env() {
176        let _guard = env_lock();
177        std::env::set_var("XAI_API_KEY", "xai-test");
178
179        let resolver = EnvVarResolver::xai();
180        let cred = resolver.resolve().unwrap();
181        assert_eq!(cred, Some(ResolvedCredential::ApiKey("xai-test".into())));
182        assert_eq!(resolver.priority(), 100);
183
184        std::env::remove_var("XAI_API_KEY");
185    }
186
187    #[test]
188    fn does_not_support_login() {
189        let resolver = EnvVarResolver::anthropic();
190        assert!(!resolver.supports_login());
191    }
192}