pub const fn is_supported() -> bool {
cfg!(feature = "keychain")
}
pub fn probe(service: &str) -> Result<(), String> {
imp::probe(service)
}
pub fn get(service: &str, account: &str) -> Result<Option<String>, String> {
imp::get(service, account)
}
pub fn set(service: &str, account: &str, secret: &str) -> Result<(), String> {
imp::set(service, account, secret)
}
pub fn delete(service: &str, account: &str) -> Result<bool, String> {
imp::delete(service, account)
}
#[cfg(feature = "keychain")]
mod imp {
pub(super) fn probe(service: &str) -> Result<(), String> {
if keyring_core::get_default_store().is_some() {
return Ok(());
}
describe(keyring::Entry::new(service, PROBE_ACCOUNT).map(drop))
}
const PROBE_ACCOUNT: &str = "__leviath_probe__";
pub(super) fn get(service: &str, account: &str) -> Result<Option<String>, String> {
interpret_get(raw_get(service, account))
}
pub(super) fn set(service: &str, account: &str, secret: &str) -> Result<(), String> {
describe(raw_set(service, account, secret))
}
pub(super) fn delete(service: &str, account: &str) -> Result<bool, String> {
interpret_delete(raw_delete(service, account))
}
fn raw_get(service: &str, account: &str) -> Result<String, keyring::Error> {
keyring_core::Entry::new(service, account).and_then(|e| e.get_password())
}
fn raw_set(service: &str, account: &str, secret: &str) -> Result<(), keyring::Error> {
keyring_core::Entry::new(service, account).and_then(|e| e.set_password(secret))
}
fn raw_delete(service: &str, account: &str) -> Result<(), keyring::Error> {
keyring_core::Entry::new(service, account).and_then(|e| e.delete_credential())
}
fn interpret_get(result: Result<String, keyring::Error>) -> Result<Option<String>, String> {
match result {
Ok(secret) => Ok(Some(secret)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(unavailable(e)),
}
}
fn interpret_delete(result: Result<(), keyring::Error>) -> Result<bool, String> {
match result {
Ok(()) => Ok(true),
Err(keyring::Error::NoEntry) => Ok(false),
Err(e) => Err(unavailable(e)),
}
}
fn describe(result: Result<(), keyring::Error>) -> Result<(), String> {
result.map_err(unavailable)
}
fn unavailable(e: keyring::Error) -> String {
format!(
"OS credential store unavailable: {e}. Set `[security] credential_store = \"file\"` \
in ~/.leviath/config.toml to keep secrets in Leviath's own 0600 files instead."
)
}
#[cfg(test)]
mod tests {
use super::*;
static STORE: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_mock_store() -> std::sync::MutexGuard<'static, ()> {
let guard = STORE.lock().expect("store lock");
keyring_core::set_default_store(keyring_core::mock::Store::new().expect("mock store"));
guard
}
const SVC: &str = "dev.leviath.test";
#[test]
fn a_secret_survives_a_set_get_delete_round_trip() {
let _guard = with_mock_store();
assert_eq!(get(SVC, "provider/anthropic").unwrap(), None);
set(SVC, "provider/anthropic", "sk-ant-secret").unwrap();
assert_eq!(
get(SVC, "provider/anthropic").unwrap().as_deref(),
Some("sk-ant-secret")
);
set(SVC, "provider/anthropic", "sk-ant-rotated").unwrap();
assert_eq!(
get(SVC, "provider/anthropic").unwrap().as_deref(),
Some("sk-ant-rotated")
);
assert!(delete(SVC, "provider/anthropic").unwrap(), "it existed");
assert_eq!(get(SVC, "provider/anthropic").unwrap(), None, "and is gone");
assert!(
!delete(SVC, "provider/anthropic").unwrap(),
"deleting again reports nothing was removed"
);
}
#[test]
fn the_public_api_delegates_to_this_implementation() {
let _guard = with_mock_store();
const SVC: &str = "dev.leviath.test.shim";
crate::keychain::probe(SVC).expect("a mock store is available");
assert_eq!(crate::keychain::get(SVC, "provider/none").unwrap(), None);
crate::keychain::set(SVC, "provider/none", "v").unwrap();
assert_eq!(
crate::keychain::get(SVC, "provider/none")
.unwrap()
.as_deref(),
Some("v")
);
assert!(crate::keychain::delete(SVC, "provider/none").unwrap());
}
#[test]
fn accounts_are_independent() {
let _guard = with_mock_store();
set(SVC, "provider/openai", "one").unwrap();
set(SVC, "mcp/github", "two").unwrap();
assert_eq!(get(SVC, "provider/openai").unwrap().as_deref(), Some("one"));
assert_eq!(get(SVC, "mcp/github").unwrap().as_deref(), Some("two"));
}
#[test]
fn a_missing_entry_is_none_but_a_broken_store_is_an_error() {
assert_eq!(interpret_get(Ok("s".into())).unwrap().as_deref(), Some("s"));
assert_eq!(interpret_get(Err(keyring::Error::NoEntry)).unwrap(), None);
assert!(interpret_get(Err(keyring::Error::NoDefaultStore)).is_err());
assert!(interpret_delete(Ok(())).unwrap());
assert!(!interpret_delete(Err(keyring::Error::NoEntry)).unwrap());
assert!(interpret_delete(Err(keyring::Error::NoDefaultStore)).is_err());
assert!(describe(Ok(())).is_ok());
assert!(describe(Err(keyring::Error::NoDefaultStore)).is_err());
}
#[test]
fn failures_name_the_file_fallback() {
let msg = unavailable(keyring::Error::NoDefaultStore);
assert!(msg.contains("OS credential store unavailable"), "{msg}");
assert!(msg.contains("credential_store = \"file\""), "{msg}");
}
#[test]
fn every_operation_reports_when_there_is_no_store() {
let _guard = STORE.lock().expect("store lock");
keyring_core::unset_default_store();
assert!(get(SVC, "provider/anthropic").is_err());
assert!(set(SVC, "provider/anthropic", "x").is_err());
assert!(delete(SVC, "provider/anthropic").is_err());
}
#[test]
fn the_platform_probe_answers_on_any_platform() {
let _guard = STORE.lock().expect("store lock");
keyring_core::unset_default_store();
let _ = probe(SVC);
keyring_core::set_default_store(keyring_core::mock::Store::new().expect("mock store"));
}
#[test]
fn the_probe_keeps_a_store_that_is_already_installed() {
let _guard = with_mock_store();
probe(SVC).expect("an installed store is a usable store");
set(SVC, "provider/probe-check", "v").unwrap();
assert_eq!(
get(SVC, "provider/probe-check").unwrap().as_deref(),
Some("v")
);
}
}
}
#[cfg(not(feature = "keychain"))]
mod imp {
const UNSUPPORTED: &str = "this build of Leviath has no OS credential store support \
(the `keychain` feature is disabled). Set `[security] credential_store = \"file\"` \
in ~/.leviath/config.toml to keep secrets in Leviath's own 0600 files instead.";
pub(super) fn probe(_service: &str) -> Result<(), String> {
Err(UNSUPPORTED.to_string())
}
pub(super) fn get(_service: &str, _account: &str) -> Result<Option<String>, String> {
Err(UNSUPPORTED.to_string())
}
pub(super) fn set(_service: &str, _account: &str, _secret: &str) -> Result<(), String> {
Err(UNSUPPORTED.to_string())
}
pub(super) fn delete(_service: &str, _account: &str) -> Result<bool, String> {
Err(UNSUPPORTED.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn support_matches_the_feature() {
assert_eq!(is_supported(), cfg!(feature = "keychain"));
}
}