use zeph_config::RegistryBackendKind;
use zeph_core::config::Config;
use zeph_core::vault::{
AgeVaultProvider, EnvVaultProvider, Secret, VaultProvider, default_vault_dir,
};
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) -> 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 dir = default_vault_dir();
let provider =
AgeVaultProvider::load_async(&dir.join("vault-key.txt"), &dir.join("secrets.age"))
.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).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).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).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())
);
}
#[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);
}
}