use std::path::Path;
use zeph_config::RegistryBackendKind;
use zeph_core::config::Config;
use zeph_core::vault::{AgeVaultProvider, EnvVaultProvider, Secret, VaultProvider};
use zeph_plugins::marketplace::RegistryClient;
use zeph_plugins::marketplace::skills_sh::{DEFAULT_BASE_URL, SkillsShClient};
#[tracing::instrument(name = "registry_client.resolve_token", skip(config))]
pub(crate) async fn resolve_registry_token(
config: &Config,
vault_override: Option<&str>,
vault_key_override: Option<&Path>,
vault_path_override: Option<&Path>,
) -> anyhow::Result<Option<Secret>> {
let reg = &config.skills.registry;
if !reg.enabled {
return Ok(None);
}
let Some(key_name) = reg.auth_vault_key.as_deref() else {
return Ok(None);
};
let vault: Box<dyn VaultProvider> = match config.vault.backend {
zeph_config::VaultBackend::Env => Box::new(EnvVaultProvider),
zeph_config::VaultBackend::Age => {
let (vault_key_path, vault_secrets_path) = crate::bootstrap::resolve_vault_paths(
config,
vault_override,
vault_key_override,
vault_path_override,
);
let provider = AgeVaultProvider::load_async(&vault_key_path, &vault_secrets_path)
.await
.map_err(|e| anyhow::anyhow!("failed to load vault: {e}"))?;
Box::new(provider)
}
zeph_config::VaultBackend::Keyring => {
anyhow::bail!("keyring vault backend is not yet implemented");
}
other => anyhow::bail!("unsupported vault backend: {other}"),
};
let value = vault
.get_secret(key_name)
.await
.map_err(|e| anyhow::anyhow!("vault error resolving {key_name}: {e}"))?;
Ok(value.map(Secret::new))
}
pub(crate) fn build_registry_client(
config: &Config,
token: Option<Secret>,
) -> Box<dyn RegistryClient> {
let reg = &config.skills.registry;
match reg.backend_kind {
RegistryBackendKind::SkillsSh => {
let base_url = reg
.backend_url
.clone()
.unwrap_or_else(|| DEFAULT_BASE_URL.to_owned());
Box::new(SkillsShClient::new(
base_url,
token,
reg.registry_timeout_secs,
))
}
_ => Box::new(SkillsShClient::new(
DEFAULT_BASE_URL.to_owned(),
token,
reg.registry_timeout_secs,
)),
}
}
pub(crate) const REGISTRY_NOT_CONFIGURED_MSG: &str = "no skill/plugin registry is configured. Add `[skills.registry] enabled = true` (and a \
backend_kind/backend_url) to config.toml, or run `zeph --init` to configure it \
interactively. See `zeph skill search --help`.";
pub(crate) fn print_search_results(entries: &[zeph_plugins::marketplace::RegistryEntry]) {
if entries.is_empty() {
println!("No results.");
return;
}
println!("Found {} result(s):\n", entries.len());
for entry in entries {
if entry.tags.is_empty() {
println!(" {} — {}", entry.registry_id, entry.description);
} else {
println!(
" {} — {} [{}]",
entry.registry_id,
entry.description,
entry.tags.join(", ")
);
}
println!(" name: {}", entry.name);
if let Some(author) = &entry.author {
println!(" author: {author}");
}
if let Some(status) = &entry.security_audit_status {
println!(" security audit: {status}");
}
}
}
#[cfg(all(test, feature = "registry"))]
mod tests {
use super::*;
use zeph_plugins::marketplace::RegistryEntry;
#[tokio::test]
async fn resolve_registry_token_returns_none_without_vault_access_when_disabled() {
let mut config = Config::default();
config.skills.registry.enabled = false;
config.skills.registry.auth_vault_key = Some("ZEPH_SKILL_REGISTRY_TOKEN".to_owned());
let result = resolve_registry_token(&config, None, None, None)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn resolve_registry_token_returns_none_when_no_auth_key_configured() {
let mut config = Config::default();
config.skills.registry.enabled = true;
config.skills.registry.auth_vault_key = None;
let result = resolve_registry_token(&config, None, None, None)
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn resolve_registry_token_resolves_from_env_backend() {
let key = "ZEPH_TEST_REGISTRY_TOKEN_RESOLVE";
#[allow(unsafe_code)] unsafe {
std::env::set_var(key, "test-token-value");
}
let mut config = Config::default();
config.skills.registry.enabled = true;
config.skills.registry.auth_vault_key = Some(key.to_owned());
config.vault.backend = zeph_config::VaultBackend::Env;
let result = resolve_registry_token(&config, None, None, None)
.await
.unwrap();
#[allow(unsafe_code)]
unsafe {
std::env::remove_var(key);
}
assert_eq!(
result.map(|s| s.expose().to_owned()),
Some("test-token-value".to_owned())
);
}
#[tokio::test]
#[serial_test::serial]
async fn resolve_registry_token_age_backend_uses_vault_override_paths() {
let override_dir = tempfile::tempdir().expect("override tempdir");
let default_dir = tempfile::tempdir().expect("default tempdir");
let key_path = override_dir.path().join("vault-key.txt");
let vault_path = override_dir.path().join("secrets.age");
AgeVaultProvider::init_vault_at(&key_path, &vault_path, false).expect("init vault");
let mut vault = AgeVaultProvider::load_async(&key_path, &vault_path)
.await
.expect("load fresh vault");
vault
.set_secret_mut(
"ZEPH_TEST_REGISTRY_TOKEN_AGE".to_owned(),
"override-token-value".to_owned(),
false,
)
.expect("seed secret");
vault.save_async().await.expect("persist vault");
#[allow(unsafe_code)] unsafe {
std::env::set_var("XDG_CONFIG_HOME", default_dir.path());
}
let mut config = Config::default();
config.skills.registry.enabled = true;
config.skills.registry.auth_vault_key = Some("ZEPH_TEST_REGISTRY_TOKEN_AGE".to_owned());
config.vault.backend = zeph_config::VaultBackend::Age;
let result =
resolve_registry_token(&config, None, Some(&key_path), Some(&vault_path)).await;
#[allow(unsafe_code)]
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
}
let secret = result
.expect("override vault must load successfully")
.expect("secret must resolve from the override path");
assert_eq!(secret.expose(), "override-token-value");
}
#[tokio::test]
#[serial_test::serial]
async fn resolve_registry_token_age_backend_falls_back_to_default_vault_dir_without_override() {
let default_dir = tempfile::tempdir().expect("default tempdir");
#[allow(unsafe_code)]
unsafe {
std::env::set_var("XDG_CONFIG_HOME", default_dir.path());
}
let mut config = Config::default();
config.skills.registry.enabled = true;
config.skills.registry.auth_vault_key = Some("ZEPH_TEST_REGISTRY_TOKEN_AGE".to_owned());
config.vault.backend = zeph_config::VaultBackend::Age;
let result = resolve_registry_token(&config, None, None, None).await;
#[allow(unsafe_code)]
unsafe {
std::env::remove_var("XDG_CONFIG_HOME");
}
assert!(
result.is_err(),
"no vault at default_vault_dir() must surface as an error, proving the default \
path (not a silent override bypass) was exercised"
);
}
#[test]
fn print_search_results_handles_empty_slice() {
print_search_results(&[]);
}
#[test]
fn print_search_results_handles_populated_slice() {
let entries = vec![RegistryEntry {
registry_id: "acme/x".to_owned(),
name: "X".to_owned(),
description: "a tool".to_owned(),
tags: vec!["tag".to_owned()],
author: Some("acme".to_owned()),
security_audit_status: Some("pass".to_owned()),
}];
print_search_results(&entries);
}
}