codineer_runtime/credentials/
env_resolver.rs1use super::{CredentialError, CredentialResolver, ResolvedCredential};
2
3#[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 #[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 #[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 #[must_use]
50 pub const fn xai() -> Self {
51 Self::new("env", "Environment Variables", "XAI_API_KEY", None, 100)
52 }
53
54 #[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}