greentic_llm/credentials/mod.rs
1//! Pluggable credential source for LLM providers.
2//!
3//! The [`CredentialSource`] trait abstracts where API keys come from so the
4//! runtime can swap implementations without touching call sites. Today we
5//! ship [`EnvCredentialSource`] (reads `GREENTIC_LLM_*` env vars). Consumers with richer credential stores (e.g. the designer's admin-managed tenants) implement their own [`CredentialSource`].
6//!
7//! [`Credential`] deliberately does **not** implement `Serialize` so it
8//! cannot be accidentally persisted to disk or sent over the wire. The
9//! `api_key` field is zeroized on drop and redacted in `Debug` output.
10
11mod env_source;
12
13pub use env_source::EnvCredentialSource;
14
15use crate::capabilities::ProviderKind;
16use chrono::{DateTime, Utc};
17use zeroize::ZeroizeOnDrop;
18
19/// Resolved credential for a single provider.
20///
21/// `api_key` is held as a `String` and zeroized on drop. `base_url` and
22/// `expires_at` are skipped from zeroization because they contain no
23/// secret material.
24#[derive(Clone, ZeroizeOnDrop)]
25pub struct Credential {
26 pub api_key: String,
27 #[zeroize(skip)]
28 pub base_url: Option<String>,
29 #[zeroize(skip)]
30 pub expires_at: Option<DateTime<Utc>>,
31}
32
33impl std::fmt::Debug for Credential {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.debug_struct("Credential")
36 .field("api_key", &"[REDACTED]")
37 .field("base_url", &self.base_url)
38 .field("expires_at", &self.expires_at)
39 .finish()
40 }
41}
42
43/// Errors returned by every [`CredentialSource`] implementation.
44#[derive(Debug, thiserror::Error)]
45pub enum CredError {
46 #[error("missing credential for provider {0:?}")]
47 Missing(ProviderKind),
48 /// Reserved for credential sources with expiring tokens (none ship today).
49 #[error("credential expired for provider {0:?}")]
50 Expired(ProviderKind),
51 #[error(transparent)]
52 Other(#[from] anyhow::Error),
53}
54
55/// Pluggable source of provider credentials.
56///
57/// Implementations resolve a [`ProviderKind`] to a [`Credential`]. The
58/// optional `invalidate` hook lets sources backed by a remote store
59/// (e.g. a future token-cache implementation) drop cached entries when the runtime
60/// observes a 401 from the upstream provider.
61#[async_trait::async_trait]
62pub trait CredentialSource: Send + Sync {
63 async fn get_credential(&self, provider: ProviderKind) -> Result<Credential, CredError>;
64
65 async fn invalidate(&self, _provider: ProviderKind) -> Result<(), CredError> {
66 Ok(())
67 }
68}