use crate::protocol::{AuthConfig, ProtocolManifest};
#[cfg(feature = "keyring")]
use keyring::Entry;
use std::env;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CredentialSourceKind {
Explicit,
ManifestEnv,
ConventionalEnv,
Keyring,
None,
}
#[derive(Clone)]
pub struct ResolvedCredential {
secret: Option<String>,
pub source_kind: CredentialSourceKind,
pub source_name: Option<String>,
pub required_envs: Vec<String>,
pub conventional_envs: Vec<String>,
}
impl fmt::Debug for ResolvedCredential {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResolvedCredential")
.field("secret", &self.secret.as_ref().map(|_| "<redacted>"))
.field("source_kind", &self.source_kind)
.field("source_name", &self.source_name)
.field("required_envs", &self.required_envs)
.field("conventional_envs", &self.conventional_envs)
.finish()
}
}
impl ResolvedCredential {
pub fn secret(&self) -> Option<&str> {
self.secret.as_deref()
}
pub fn missing(required_envs: Vec<String>, conventional_envs: Vec<String>) -> Self {
Self {
secret: None,
source_kind: CredentialSourceKind::None,
source_name: None,
required_envs,
conventional_envs,
}
}
#[cfg(test)]
pub(crate) fn resolved_explicit(secret: &str) -> Self {
Self {
secret: Some(secret.to_string()),
source_kind: CredentialSourceKind::Explicit,
source_name: Some("explicit".to_string()),
required_envs: Vec::new(),
conventional_envs: Vec::new(),
}
}
}
pub fn primary_auth(manifest: &ProtocolManifest) -> Option<&AuthConfig> {
manifest.endpoint.auth.as_ref().or(manifest.auth.as_ref())
}
pub fn shadowed_auth(manifest: &ProtocolManifest) -> Option<&AuthConfig> {
let endpoint = manifest.endpoint.auth.as_ref()?;
let top = manifest.auth.as_ref()?;
let same = endpoint.auth_type == top.auth_type
&& endpoint.token_env == top.token_env
&& endpoint.key_env == top.key_env;
if same {
None
} else {
Some(top)
}
}
pub fn required_envs(manifest: &ProtocolManifest) -> Vec<String> {
let Some(auth) = primary_auth(manifest) else {
return Vec::new();
};
let mut out = Vec::new();
if let Some(env) = auth.token_env.as_ref().or(auth.key_env.as_ref()) {
let trimmed = env.trim();
if !trimmed.is_empty() {
out.push(trimmed.to_string());
}
}
out
}
pub fn provider_id(manifest: &ProtocolManifest) -> &str {
manifest.provider_id.as_deref().unwrap_or(&manifest.id)
}
pub fn conventional_envs(provider_id: &str) -> Vec<String> {
let normalized = provider_id.to_uppercase().replace('-', "_");
vec![format!("{normalized}_API_KEY")]
}
pub fn resolve_credential(
manifest: &ProtocolManifest,
explicit: Option<&str>,
) -> ResolvedCredential {
let required_envs = required_envs(manifest);
let conventional_envs = conventional_envs(provider_id(manifest));
if let Some(value) = explicit.map(str::trim).filter(|value| !value.is_empty()) {
return ResolvedCredential {
secret: Some(value.to_string()),
source_kind: CredentialSourceKind::Explicit,
source_name: Some("explicit".to_string()),
required_envs,
conventional_envs,
};
}
for name in &required_envs {
if let Some(value) = env_value(name) {
return ResolvedCredential {
secret: Some(value),
source_kind: CredentialSourceKind::ManifestEnv,
source_name: Some(name.clone()),
required_envs,
conventional_envs,
};
}
}
for name in &conventional_envs {
if let Some(value) = env_value(name) {
return ResolvedCredential {
secret: Some(value),
source_kind: CredentialSourceKind::ConventionalEnv,
source_name: Some(name.clone()),
required_envs,
conventional_envs,
};
}
}
#[cfg(feature = "keyring")]
{
let id = provider_id(manifest);
if let Some(value) = keyring_value(id) {
return ResolvedCredential {
secret: Some(value),
source_kind: CredentialSourceKind::Keyring,
source_name: Some(format!("ai-protocol/{id}")),
required_envs,
conventional_envs,
};
}
}
ResolvedCredential::missing(required_envs, conventional_envs)
}
fn env_value(name: &str) -> Option<String> {
env::var(name)
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
#[cfg(feature = "keyring")]
fn keyring_value(provider_id: &str) -> Option<String> {
Entry::new("ai-protocol", provider_id)
.ok()
.and_then(|entry| entry.get_password().ok())
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard {
key: &'static str,
old: Option<String>,
}
impl EnvGuard {
fn set(key: &'static str, value: Option<&str>) -> Self {
let old = env::var(key).ok();
match value {
Some(value) => env::set_var(key, value),
None => env::remove_var(key),
}
Self { key, old }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match self.old.as_ref() {
Some(value) => env::set_var(self.key, value),
None => env::remove_var(self.key),
}
}
}
fn manifest() -> ProtocolManifest {
serde_yaml::from_str(
r#"
id: replicate
protocol_version: "1.5"
name: "Replicate"
status: "stable"
category: "ai_provider"
official_url: "https://example.com"
support_contact: "https://example.com/support"
endpoint:
base_url: "https://api.example.com/v1"
auth:
type: "bearer"
token_env: "REPLICATE_API_TOKEN"
capabilities: [chat]
"#,
)
.expect("manifest")
}
#[test]
fn explicit_credential_wins() {
let _lock = ENV_LOCK.lock().expect("env lock");
let _token = EnvGuard::set("REPLICATE_API_TOKEN", Some("env-token"));
let _key = EnvGuard::set("REPLICATE_API_KEY", Some("env-key"));
let resolved = resolve_credential(&manifest(), Some(" explicit "));
assert_eq!(resolved.secret(), Some("explicit"));
assert_eq!(resolved.source_kind, CredentialSourceKind::Explicit);
assert_eq!(resolved.source_name.as_deref(), Some("explicit"));
}
#[test]
fn manifest_env_wins_over_conventional_env() {
let _lock = ENV_LOCK.lock().expect("env lock");
let _token = EnvGuard::set("REPLICATE_API_TOKEN", Some("manifest-token"));
let _key = EnvGuard::set("REPLICATE_API_KEY", Some("conventional-key"));
let resolved = resolve_credential(&manifest(), None);
assert_eq!(resolved.secret(), Some("manifest-token"));
assert_eq!(resolved.source_kind, CredentialSourceKind::ManifestEnv);
assert_eq!(resolved.source_name.as_deref(), Some("REPLICATE_API_TOKEN"));
}
#[test]
fn conventional_env_is_fallback() {
let _lock = ENV_LOCK.lock().expect("env lock");
let _token = EnvGuard::set("REPLICATE_API_TOKEN", None);
let _key = EnvGuard::set("REPLICATE_API_KEY", Some("conventional-key"));
let resolved = resolve_credential(&manifest(), None);
assert_eq!(resolved.secret(), Some("conventional-key"));
assert_eq!(resolved.source_kind, CredentialSourceKind::ConventionalEnv);
assert_eq!(resolved.source_name.as_deref(), Some("REPLICATE_API_KEY"));
}
#[test]
fn debug_redacts_secret() {
let _lock = ENV_LOCK.lock().expect("env lock");
let _token = EnvGuard::set("REPLICATE_API_TOKEN", Some("manifest-token"));
let resolved = resolve_credential(&manifest(), None);
let debug = format!("{resolved:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("manifest-token"));
}
#[test]
fn missing_credential_lists_required_and_conventional_envs() {
let _lock = ENV_LOCK.lock().expect("env lock");
let _token = EnvGuard::set("REPLICATE_API_TOKEN", None);
let _key = EnvGuard::set("REPLICATE_API_KEY", None);
let resolved = resolve_credential(&manifest(), None);
assert!(resolved.secret().is_none());
assert_eq!(resolved.source_kind, CredentialSourceKind::None);
assert!(resolved.source_name.is_none());
assert_eq!(resolved.required_envs, vec!["REPLICATE_API_TOKEN"]);
assert_eq!(resolved.conventional_envs, vec!["REPLICATE_API_KEY"]);
}
#[test]
fn shadowed_auth_returns_none_when_only_endpoint_present() {
let manifest: ProtocolManifest = serde_yaml::from_str(
r#"
id: replicate
protocol_version: "2.0"
name: "Replicate"
status: "stable"
category: "ai_provider"
official_url: "https://example.com"
support_contact: "https://example.com/support"
endpoint:
base_url: "https://api.example.com/v1"
auth:
type: "bearer"
token_env: "REPLICATE_API_TOKEN"
capabilities: [chat]
"#,
)
.expect("manifest");
assert!(shadowed_auth(&manifest).is_none());
}
#[test]
fn shadowed_auth_returns_none_when_blocks_match() {
let manifest: ProtocolManifest = serde_yaml::from_str(
r#"
id: replicate
protocol_version: "1.5"
name: "Replicate"
status: "stable"
category: "ai_provider"
official_url: "https://example.com"
support_contact: "https://example.com/support"
endpoint:
base_url: "https://api.example.com/v1"
auth:
type: "bearer"
token_env: "REPLICATE_API_TOKEN"
auth:
type: "bearer"
token_env: "REPLICATE_API_TOKEN"
capabilities: [chat]
"#,
)
.expect("manifest");
assert!(shadowed_auth(&manifest).is_none());
}
#[test]
fn shadowed_auth_flags_divergent_blocks() {
let manifest: ProtocolManifest = serde_yaml::from_str(
r#"
id: replicate
protocol_version: "1.5"
name: "Replicate"
status: "stable"
category: "ai_provider"
official_url: "https://example.com"
support_contact: "https://example.com/support"
endpoint:
base_url: "https://api.example.com/v1"
auth:
type: "bearer"
token_env: "REPLICATE_API_TOKEN"
auth:
type: "api_key"
key_env: "REPLICATE_LEGACY_KEY"
header: "X-Legacy-Key"
capabilities: [chat]
"#,
)
.expect("manifest");
let shadowed = shadowed_auth(&manifest).expect("divergence detected");
assert_eq!(shadowed.auth_type, "api_key");
assert_eq!(shadowed.key_env.as_deref(), Some("REPLICATE_LEGACY_KEY"));
let primary = primary_auth(&manifest).expect("primary auth");
assert_eq!(primary.auth_type, "bearer");
assert_eq!(primary.token_env.as_deref(), Some("REPLICATE_API_TOKEN"));
assert_eq!(required_envs(&manifest), vec!["REPLICATE_API_TOKEN"]);
}
#[cfg(feature = "keyring")]
#[test]
#[ignore]
fn keyring_value_is_resolved() {
let _lock = ENV_LOCK.lock().expect("env lock");
let _token = EnvGuard::set("REPLICATE_API_TOKEN", None);
let _key = EnvGuard::set("REPLICATE_API_KEY", None);
let resolved = resolve_credential(&manifest(), None);
match resolved.source_kind {
CredentialSourceKind::Keyring => {
assert!(resolved.secret().is_some());
assert_eq!(
resolved.source_name.as_deref(),
Some("ai-protocol/replicate")
);
}
CredentialSourceKind::None => {
eprintln!("keyring entry not found; populate `ai-protocol/replicate` then re-run");
}
other => panic!("unexpected source_kind: {other:?}"),
}
}
}