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}