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 = match configured {
91        Some(paths) if !paths.is_empty() => paths.to_vec(),
92        _ => default_paths()?,
93    };
94    resolve_from(std::env::var("COMMANDCODE_API_KEY").ok().as_deref(), &paths)
95}
96
97/// Resolve one credential from the environment, then each path in turn.
98pub fn resolve_from(env_key: Option<&str>, paths: &[PathBuf]) -> Result<Credential> {
99    if let Some(key) = env_key.map(str::trim).filter(|key| !key.is_empty()) {
100        return Ok(Credential {
101            token: key.to_string(),
102            source: "COMMANDCODE_API_KEY".to_string(),
103        });
104    }
105    for path in paths {
106        if let Some(credential) = read_from(path) {
107            return Ok(credential);
108        }
109    }
110    Err(AppError::Credentials(message_for(paths)))
111}
112
113/// Distinguish "never signed in" from "signed in, but the token lapsed" so the
114/// tooltip can tell the user which one to fix.
115fn message_for(paths: &[PathBuf]) -> String {
116    let any_expired = paths.iter().any(|path| {
117        std::fs::read_to_string(path)
118            .ok()
119            .and_then(|text| serde_json::from_str::<Value>(&text).ok())
120            .and_then(|value| {
121                let object = value.as_object()?;
122                let generic = path
123                    .ends_with(OFFICIAL_AUTH_FILE)
124                    .then(|| object.get("apiKey"))
125                    .flatten();
126                let found = CREDENTIAL_KEYS
127                    .iter()
128                    .filter_map(|key| object.get(*key))
129                    .chain(generic)
130                    .filter_map(candidate)
131                    .any(|found| found.expires_ms.is_some_and(is_past));
132                Some(found)
133            })
134            .unwrap_or(false)
135    });
136    if any_expired { EXPIRED } else { SIGNED_OUT }.to_string()
137}
138
139fn candidate(value: &Value) -> Option<Candidate> {
140    if let Some(token) = value.as_str() {
141        let token = token.trim();
142        return (!token.is_empty()).then(|| Candidate {
143            token: token.to_string(),
144            expires_ms: None,
145        });
146    }
147    let object = value.as_object()?;
148    let token = ["access", "apiKey", "key"]
149        .iter()
150        .filter_map(|field| object.get(*field))
151        .filter_map(Value::as_str)
152        .map(str::trim)
153        .find(|token| !token.is_empty())?;
154    Some(Candidate {
155        token: token.to_string(),
156        expires_ms: object.get("expires").and_then(Value::as_i64),
157    })
158}
159
160fn is_past(expires_ms: i64) -> bool {
161    expires_ms > 0 && expires_ms <= chrono::Utc::now().timestamp_millis()
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    fn write(dir: &Path, name: &str, value: Value) -> PathBuf {
169        let path = dir.join(name);
170        std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
171        path
172    }
173
174    fn future_ms() -> i64 {
175        chrono::Utc::now().timestamp_millis() + 86_400_000
176    }
177
178    fn past_ms() -> i64 {
179        chrono::Utc::now().timestamp_millis() - 86_400_000
180    }
181
182    #[test]
183    fn reads_the_official_cli_oauth_shape() {
184        let dir = tempfile::tempdir().unwrap();
185        let path = write(
186            dir.path(),
187            "auth.json",
188            serde_json::json!({
189                "command-code": {"type": "oauth", "access": "tok", "expires": future_ms()}
190            }),
191        );
192
193        assert_eq!(read_from(&path).unwrap().token, "tok");
194    }
195
196    /// A harness keystore holds every provider the user signed into — pi types
197    /// its `auth.json` as `Record<providerId, Credential>` and carries their
198    /// Anthropic, OpenAI and OpenRouter credentials there. A key that names no
199    /// provider therefore must not be read out of one: only the provider-keyed
200    /// lookup applies, and only Command Code's own file may fall back to the
201    /// unkeyed `apiKey`.
202    #[test]
203    fn the_unkeyed_apikey_is_only_read_from_command_codes_own_file() {
204        let dir = tempfile::tempdir().unwrap();
205        let body = serde_json::json!({ "apiKey": "another-providers-secret" });
206
207        let shared = dir.path().join(".pi/agent");
208        std::fs::create_dir_all(&shared).unwrap();
209        let shared = write(&shared, "auth.json", body.clone());
210        assert_eq!(
211            read_from(&shared),
212            None,
213            "an unkeyed apiKey in a shared harness keystore is not ours to read"
214        );
215
216        let own = dir.path().join(".commandcode");
217        std::fs::create_dir_all(&own).unwrap();
218        let own = write(&own, "auth.json", body);
219        assert_eq!(
220            read_from(&own).map(|c| c.token),
221            Some("another-providers-secret".to_string()),
222            "Command Code's own file still accepts a pasted key"
223        );
224    }
225
226    #[test]
227    fn reads_the_pi_oauth_shape() {
228        let dir = tempfile::tempdir().unwrap();
229        let path = write(
230            dir.path(),
231            "auth.json",
232            serde_json::json!({
233                "openai-codex": {"type": "oauth", "access": "other"},
234                "commandcode": {"type": "oauth", "access": "tok", "expires": future_ms()}
235            }),
236        );
237
238        assert_eq!(read_from(&path).unwrap().token, "tok");
239    }
240
241    #[test]
242    fn reads_a_legacy_plain_string_credential() {
243        let dir = tempfile::tempdir().unwrap();
244        let path = write(
245            dir.path(),
246            "auth.json",
247            serde_json::json!({"commandcode": "plain-token"}),
248        );
249
250        assert_eq!(read_from(&path).unwrap().token, "plain-token");
251    }
252
253    #[test]
254    fn an_expired_token_is_not_offered() {
255        let dir = tempfile::tempdir().unwrap();
256        let path = write(
257            dir.path(),
258            "auth.json",
259            serde_json::json!({
260                "command-code": {"type": "oauth", "access": "tok", "expires": past_ms()}
261            }),
262        );
263
264        assert!(read_from(&path).is_none());
265    }
266
267    #[test]
268    fn malformed_and_missing_files_are_skipped_not_fatal() {
269        let dir = tempfile::tempdir().unwrap();
270        let broken = dir.path().join("broken.json");
271        std::fs::write(&broken, "{not json").unwrap();
272
273        assert!(read_from(&broken).is_none());
274        assert!(read_from(&dir.path().join("absent.json")).is_none());
275    }
276
277    #[test]
278    fn env_key_outranks_every_file() {
279        let dir = tempfile::tempdir().unwrap();
280        let path = write(
281            dir.path(),
282            "auth.json",
283            serde_json::json!({"commandcode": {"access": "from-file"}}),
284        );
285
286        let credential = resolve_from(Some("from-env"), &[path]).unwrap();
287
288        assert_eq!(credential.token, "from-env");
289        assert_eq!(credential.source, "COMMANDCODE_API_KEY");
290    }
291
292    #[test]
293    fn a_stale_harness_does_not_mask_a_live_one() {
294        let dir = tempfile::tempdir().unwrap();
295        let stale = write(
296            dir.path(),
297            "stale.json",
298            serde_json::json!({
299                "command-code": {"access": "stale", "expires": past_ms()}
300            }),
301        );
302        let live = write(
303            dir.path(),
304            "live.json",
305            serde_json::json!({
306                "commandcode": {"access": "live", "expires": future_ms()}
307            }),
308        );
309
310        assert_eq!(resolve_from(None, &[stale, live]).unwrap().token, "live");
311    }
312
313    #[test]
314    fn all_expired_reports_expiry_and_nothing_reports_signed_out() {
315        let dir = tempfile::tempdir().unwrap();
316        let stale = write(
317            dir.path(),
318            "stale.json",
319            serde_json::json!({
320                "command-code": {"access": "stale", "expires": past_ms()}
321            }),
322        );
323
324        let expired = resolve_from(None, std::slice::from_ref(&stale)).unwrap_err();
325        assert!(expired.to_string().contains("expired"), "{expired}");
326
327        let absent = resolve_from(None, &[dir.path().join("absent.json")]).unwrap_err();
328        assert!(absent.to_string().contains("not signed in"), "{absent}");
329
330        let shared_dir = dir.path().join(".pi/agent");
331        std::fs::create_dir_all(&shared_dir).unwrap();
332        let unrelated = write(
333            &shared_dir,
334            "auth.json",
335            serde_json::json!({
336                "apiKey": {"access": "other", "expires": past_ms()}
337            }),
338        );
339        let signed_out = resolve_from(None, &[unrelated]).unwrap_err();
340        assert!(
341            signed_out.to_string().contains("not signed in"),
342            "an unrelated expired apiKey must not be treated as Command Code: {signed_out}"
343        );
344    }
345
346    #[test]
347    fn default_paths_only_include_verified_credential_files() {
348        let paths = default_paths().unwrap();
349
350        assert_eq!(
351            AUTH_FILES,
352            &[".commandcode/auth.json", ".pi/agent/auth.json"]
353        );
354        assert_eq!(paths.len(), AUTH_FILES.len());
355        assert!(paths[0].ends_with(AUTH_FILES[0]));
356        assert!(paths[1].ends_with(AUTH_FILES[1]));
357    }
358}