Skip to main content

agentic_tools_utils/
secrets.rs

1//! Token and secret resolution utilities.
2//!
3//! This module provides utilities for resolving API tokens from
4//! environment variables and CLI tools.
5
6use std::process::Command;
7use thiserror::Error;
8
9/// Errors that can occur during secret resolution.
10#[derive(Debug, Error)]
11pub enum SecretsError {
12    /// Token was not found in any source
13    #[error("Token not found in env or gh")]
14    NotFound,
15    /// External command failed
16    #[error("gh command failed: {0}")]
17    CommandFailed(String),
18    /// I/O error
19    #[error(transparent)]
20    Io(#[from] std::io::Error),
21}
22
23/// Resolve a GitHub token from environment or `gh auth token`.
24///
25/// Resolution order:
26/// 1. `GITHUB_TOKEN` environment variable
27/// 2. `GH_TOKEN` environment variable
28/// 3. `gh auth token` command output
29///
30/// # Errors
31///
32/// Returns `SecretsError::NotFound` if no token could be found.
33/// Returns `SecretsError::CommandFailed` if gh exists but returns an error.
34/// Returns `SecretsError::Io` if gh cannot be executed.
35pub fn resolve_github_token() -> Result<String, SecretsError> {
36    // Check GITHUB_TOKEN first
37    if let Ok(v) = std::env::var("GITHUB_TOKEN")
38        && !v.trim().is_empty()
39    {
40        return Ok(v.trim().to_string());
41    }
42
43    // Check GH_TOKEN
44    if let Ok(v) = std::env::var("GH_TOKEN")
45        && !v.trim().is_empty()
46    {
47        return Ok(v.trim().to_string());
48    }
49
50    // Try gh auth token
51    let out = Command::new("gh").args(["auth", "token"]).output();
52    match out {
53        Ok(o) if o.status.success() => {
54            let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
55            if s.is_empty() {
56                Err(SecretsError::NotFound)
57            } else {
58                Ok(s)
59            }
60        }
61        Ok(o) => Err(SecretsError::CommandFailed(
62            String::from_utf8_lossy(&o.stderr).to_string(),
63        )),
64        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
65            // gh not installed, that's fine
66            Err(SecretsError::NotFound)
67        }
68        Err(e) => Err(SecretsError::Io(e)),
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    // Note: These tests are limited since we can't easily mock environment
77    // variables or external commands. Full testing would require:
78    // - Setting env vars (affects other tests)
79    // - Mocking Command (complex)
80    //
81    // The current tests verify basic error types.
82
83    #[test]
84    fn secrets_error_display() {
85        let e = SecretsError::NotFound;
86        assert!(e.to_string().contains("not found"));
87
88        let e = SecretsError::CommandFailed("bad things".into());
89        assert!(e.to_string().contains("bad things"));
90    }
91}