Skip to main content

ai_usagebar/commandcode/
creds.rs

1//! Locate the Command Code OAuth credential a local harness already holds.
2//!
3//! Command Code is reachable through several agent harnesses, and each files
4//! the same OAuth credential under its own key in its own file. This module
5//! reads them; it never writes. Refreshing an expired token belongs to the
6//! CLI that owns the file, and racing it here would corrupt state shared by
7//! every harness on the machine.
8
9use std::path::{Path, PathBuf};
10
11use serde_json::Value;
12
13use crate::cache::home_dir;
14use crate::error::{AppError, Result};
15
16/// Files searched in order; the first holding a live credential wins.
17pub const AUTH_FILES: &[&str] = &[OFFICIAL_AUTH_FILE, ".pi/agent/auth.json"];
18
19/// The Command Code CLI's own file — the only one whose whole contents belong
20/// to Command Code. The others are shared harness keystores, which is why the
21/// unkeyed `apiKey` fallback below is scoped to this one.
22const OFFICIAL_AUTH_FILE: &str = ".commandcode/auth.json";
23
24/// Keys a harness may file the credential under.
25const CREDENTIAL_KEYS: &[&str] = &["command-code", "commandcode"];
26
27pub const SIGNED_OUT: &str =
28    "Command Code is not signed in. Run `commandcode` and sign in, or set COMMANDCODE_API_KEY.";
29pub const EXPIRED: &str = "Command Code sign-in expired. Run `commandcode` to sign in again.";
30
31/// A credential and where it came from.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Credential {
34    pub token: String,
35    pub source: String,
36}
37
38/// One candidate found on disk, before expiry is considered.
39struct Candidate {
40    token: String,
41    expires_ms: Option<i64>,
42}
43
44/// Default search paths, resolved against the OS home convention.
45pub fn default_paths() -> Result<Vec<PathBuf>> {
46    let home = home_dir()?;
47    Ok(AUTH_FILES.iter().map(|rel| home.join(rel)).collect())
48}
49
50/// Read whichever credential shape a harness wrote into `path`.
51///
52/// A missing or malformed file yields `None` rather than an error: one broken
53/// harness must not mask a working one further down the list.
54pub fn read_from(path: &Path) -> Option<Credential> {
55    let text = std::fs::read_to_string(path).ok()?;
56    let value: Value = serde_json::from_str(&text).ok()?;
57    let object = value.as_object()?;
58
59    // `apiKey` is unkeyed, so it is only consulted in Command Code's own file,
60    // where the whole document is Command Code's. A harness keystore holds
61    // every provider the user has signed into — pi's `auth.json` is typed
62    // `Record<providerId, Credential>` and carries their Anthropic, OpenAI and
63    // OpenRouter credentials — so a key that names no provider must not be
64    // read out of one, whatever shape a future version gives it.
65    let generic = path
66        .ends_with(OFFICIAL_AUTH_FILE)
67        .then(|| object.get("apiKey"))
68        .flatten();
69    let candidates = CREDENTIAL_KEYS
70        .iter()
71        .filter_map(|key| object.get(*key))
72        .chain(generic)
73        .filter_map(candidate);
74
75    for found in candidates {
76        if found.expires_ms.is_some_and(is_past) {
77            continue;
78        }
79        return Some(Credential {
80            token: found.token,
81            source: path.display().to_string(),
82        });
83    }
84    None
85}
86
87/// Production resolver: the env override, then configured paths, then the
88/// platform defaults. Tests use [`resolve_from`], which takes both as inputs.
89pub fn resolve(configured: Option<&[PathBuf]>) -> Result<Credential> {
90    let paths = effective_paths(configured)?;
91    resolve_from(std::env::var("COMMANDCODE_API_KEY").ok().as_deref(), &paths)
92}
93
94/// The effective search list: a configured override when it names at least one
95/// path, else the platform defaults.
96pub fn effective_paths(configured: Option<&[PathBuf]>) -> Result<Vec<PathBuf>> {
97    match configured {
98        Some(paths) if !paths.is_empty() => Ok(paths.to_vec()),
99        _ => default_paths(),
100    }
101}
102
103/// Resolve one credential from the environment, then each path in turn.
104pub fn resolve_from(env_key: Option<&str>, paths: &[PathBuf]) -> Result<Credential> {
105    if let Some(key) = env_key.map(str::trim).filter(|key| !key.is_empty()) {
106        return Ok(Credential {
107            token: key.to_string(),
108            source: "COMMANDCODE_API_KEY".to_string(),
109        });
110    }
111    for path in paths {
112        if let Some(credential) = read_from(path) {
113            return Ok(credential);
114        }
115    }
116    Err(AppError::Credentials(message_for(paths)))
117}
118
119/// Distinguish "never signed in" from "signed in, but the token lapsed" so the
120/// tooltip can tell the user which one to fix.
121fn message_for(paths: &[PathBuf]) -> String {
122    let any_expired = paths.iter().any(|path| {
123        std::fs::read_to_string(path)
124            .ok()
125            .and_then(|text| serde_json::from_str::<Value>(&text).ok())
126            .and_then(|value| {
127                let object = value.as_object()?;
128                let generic = path
129                    .ends_with(OFFICIAL_AUTH_FILE)
130                    .then(|| object.get("apiKey"))
131                    .flatten();
132                let found = CREDENTIAL_KEYS
133                    .iter()
134                    .filter_map(|key| object.get(*key))
135                    .chain(generic)
136                    .filter_map(candidate)
137                    .any(|found| found.expires_ms.is_some_and(is_past));
138                Some(found)
139            })
140            .unwrap_or(false)
141    });
142    if any_expired { EXPIRED } else { SIGNED_OUT }.to_string()
143}
144
145fn candidate(value: &Value) -> Option<Candidate> {
146    if let Some(token) = value.as_str() {
147        let token = token.trim();
148        return (!token.is_empty()).then(|| Candidate {
149            token: token.to_string(),
150            expires_ms: None,
151        });
152    }
153    let object = value.as_object()?;
154    let token = ["access", "apiKey", "key"]
155        .iter()
156        .filter_map(|field| object.get(*field))
157        .filter_map(Value::as_str)
158        .map(str::trim)
159        .find(|token| !token.is_empty())?;
160    Some(Candidate {
161        token: token.to_string(),
162        expires_ms: object.get("expires").and_then(Value::as_i64),
163    })
164}
165
166fn is_past(expires_ms: i64) -> bool {
167    expires_ms > 0 && expires_ms <= chrono::Utc::now().timestamp_millis()
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    fn write(dir: &Path, name: &str, value: Value) -> PathBuf {
175        let path = dir.join(name);
176        std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
177        path
178    }
179
180    fn future_ms() -> i64 {
181        chrono::Utc::now().timestamp_millis() + 86_400_000
182    }
183
184    fn past_ms() -> i64 {
185        chrono::Utc::now().timestamp_millis() - 86_400_000
186    }
187
188    #[test]
189    fn reads_the_official_cli_oauth_shape() {
190        let dir = tempfile::tempdir().unwrap();
191        let path = write(
192            dir.path(),
193            "auth.json",
194            serde_json::json!({
195                "command-code": {"type": "oauth", "access": "tok", "expires": future_ms()}
196            }),
197        );
198
199        assert_eq!(read_from(&path).unwrap().token, "tok");
200    }
201
202    /// A harness keystore holds every provider the user signed into — pi types
203    /// its `auth.json` as `Record<providerId, Credential>` and carries their
204    /// Anthropic, OpenAI and OpenRouter credentials there. A key that names no
205    /// provider therefore must not be read out of one: only the provider-keyed
206    /// lookup applies, and only Command Code's own file may fall back to the
207    /// unkeyed `apiKey`.
208    #[test]
209    fn the_unkeyed_apikey_is_only_read_from_command_codes_own_file() {
210        let dir = tempfile::tempdir().unwrap();
211        let body = serde_json::json!({ "apiKey": "another-providers-secret" });
212
213        let shared = dir.path().join(".pi/agent");
214        std::fs::create_dir_all(&shared).unwrap();
215        let shared = write(&shared, "auth.json", body.clone());
216        assert_eq!(
217            read_from(&shared),
218            None,
219            "an unkeyed apiKey in a shared harness keystore is not ours to read"
220        );
221
222        let own = dir.path().join(".commandcode");
223        std::fs::create_dir_all(&own).unwrap();
224        let own = write(&own, "auth.json", body);
225        assert_eq!(
226            read_from(&own).map(|c| c.token),
227            Some("another-providers-secret".to_string()),
228            "Command Code's own file still accepts a pasted key"
229        );
230    }
231
232    #[test]
233    fn reads_the_pi_oauth_shape() {
234        let dir = tempfile::tempdir().unwrap();
235        let path = write(
236            dir.path(),
237            "auth.json",
238            serde_json::json!({
239                "openai-codex": {"type": "oauth", "access": "other"},
240                "commandcode": {"type": "oauth", "access": "tok", "expires": future_ms()}
241            }),
242        );
243
244        assert_eq!(read_from(&path).unwrap().token, "tok");
245    }
246
247    #[test]
248    fn reads_a_legacy_plain_string_credential() {
249        let dir = tempfile::tempdir().unwrap();
250        let path = write(
251            dir.path(),
252            "auth.json",
253            serde_json::json!({"commandcode": "plain-token"}),
254        );
255
256        assert_eq!(read_from(&path).unwrap().token, "plain-token");
257    }
258
259    #[test]
260    fn an_expired_token_is_not_offered() {
261        let dir = tempfile::tempdir().unwrap();
262        let path = write(
263            dir.path(),
264            "auth.json",
265            serde_json::json!({
266                "command-code": {"type": "oauth", "access": "tok", "expires": past_ms()}
267            }),
268        );
269
270        assert!(read_from(&path).is_none());
271    }
272
273    #[test]
274    fn malformed_and_missing_files_are_skipped_not_fatal() {
275        let dir = tempfile::tempdir().unwrap();
276        let broken = dir.path().join("broken.json");
277        std::fs::write(&broken, "{not json").unwrap();
278
279        assert!(read_from(&broken).is_none());
280        assert!(read_from(&dir.path().join("absent.json")).is_none());
281    }
282
283    #[test]
284    fn env_key_outranks_every_file() {
285        let dir = tempfile::tempdir().unwrap();
286        let path = write(
287            dir.path(),
288            "auth.json",
289            serde_json::json!({"commandcode": {"access": "from-file"}}),
290        );
291
292        let credential = resolve_from(Some("from-env"), &[path]).unwrap();
293
294        assert_eq!(credential.token, "from-env");
295        assert_eq!(credential.source, "COMMANDCODE_API_KEY");
296    }
297
298    #[test]
299    fn a_stale_harness_does_not_mask_a_live_one() {
300        let dir = tempfile::tempdir().unwrap();
301        let stale = write(
302            dir.path(),
303            "stale.json",
304            serde_json::json!({
305                "command-code": {"access": "stale", "expires": past_ms()}
306            }),
307        );
308        let live = write(
309            dir.path(),
310            "live.json",
311            serde_json::json!({
312                "commandcode": {"access": "live", "expires": future_ms()}
313            }),
314        );
315
316        assert_eq!(resolve_from(None, &[stale, live]).unwrap().token, "live");
317    }
318
319    #[test]
320    fn all_expired_reports_expiry_and_nothing_reports_signed_out() {
321        let dir = tempfile::tempdir().unwrap();
322        let stale = write(
323            dir.path(),
324            "stale.json",
325            serde_json::json!({
326                "command-code": {"access": "stale", "expires": past_ms()}
327            }),
328        );
329
330        let expired = resolve_from(None, std::slice::from_ref(&stale)).unwrap_err();
331        assert!(expired.to_string().contains("expired"), "{expired}");
332
333        let absent = resolve_from(None, &[dir.path().join("absent.json")]).unwrap_err();
334        assert!(absent.to_string().contains("not signed in"), "{absent}");
335
336        let shared_dir = dir.path().join(".pi/agent");
337        std::fs::create_dir_all(&shared_dir).unwrap();
338        let unrelated = write(
339            &shared_dir,
340            "auth.json",
341            serde_json::json!({
342                "apiKey": {"access": "other", "expires": past_ms()}
343            }),
344        );
345        let signed_out = resolve_from(None, &[unrelated]).unwrap_err();
346        assert!(
347            signed_out.to_string().contains("not signed in"),
348            "an unrelated expired apiKey must not be treated as Command Code: {signed_out}"
349        );
350    }
351
352    #[test]
353    fn default_paths_only_include_verified_credential_files() {
354        let paths = default_paths().unwrap();
355
356        assert_eq!(
357            AUTH_FILES,
358            &[".commandcode/auth.json", ".pi/agent/auth.json"]
359        );
360        assert_eq!(paths.len(), AUTH_FILES.len());
361        assert!(paths[0].ends_with(AUTH_FILES[0]));
362        assert!(paths[1].ends_with(AUTH_FILES[1]));
363    }
364}