use zeroize::Zeroizing;
use tsafe_core::agent::request_password_from_agent;
use tsafe_core::vault::Vault;
use crate::errors::{McpError, McpErrorKind};
use crate::session::Session;
pub fn open_vault(session: &Session) -> Result<Vault, McpError> {
let password = match request_password_from_agent(&session.profile) {
Ok(Some(pw)) => pw,
Ok(None) => {
if session.require_agent {
return Err(agent_not_running_error(&session.profile));
}
return Err(McpError::new(
McpErrorKind::AgentNotRunning,
"no agent available (TSAFE_MCP_REQUIRE_AGENT=0 set; pass --no-require-agent to confirm)",
));
}
Err(e) => {
return Err(McpError::new(
McpErrorKind::AgentNotRunning,
format!("agent rpc failed: {e}"),
));
}
};
let password = Zeroizing::new(password);
Vault::open_read_only(&session.vault_path, password.as_bytes()).map_err(|e| {
McpError::new(
McpErrorKind::InternalError,
format!("vault open failed: {e}"),
)
})
}
pub fn lookup_key(vault: &Vault, key: &str) -> Result<Zeroizing<String>, McpError> {
vault.get(key).map_err(|e| {
let msg = format!("{e}");
if msg.to_lowercase().contains("not found") {
McpError::new(McpErrorKind::KeyMissing, format!("key '{key}'"))
} else {
McpError::new(
McpErrorKind::InternalError,
format!("lookup '{key}': {msg}"),
)
}
})
}
pub fn vault_has_key(vault: &Vault, key: &str) -> bool {
vault.get(key).is_ok()
}
fn agent_not_running_error(profile: &str) -> McpError {
McpError::new(
McpErrorKind::AgentNotRunning,
format!(
"tsafe-agent not running. Run `tsafe agent unlock --profile {profile}` and reload the host."
),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn session_without_agent() -> Session {
Session {
profile: "demo".to_string(),
allowed_globs: vec!["demo/*".to_string()],
denied_globs: vec![],
contract: None,
allow_reveal: false,
audit_source: "mcp:test:1".to_string(),
pid: 1,
require_agent: true,
vault_path: PathBuf::from("nonexistent.tsafe"),
}
}
#[test]
fn open_vault_without_agent_returns_agent_not_running() {
let tmp = tempfile::tempdir().unwrap();
let tmp_str = tmp.path().to_str().unwrap().to_string();
temp_env::with_vars(
[
("TSAFE_AGENT_SOCK", None::<&str>),
("XDG_RUNTIME_DIR", None::<&str>),
("TSAFE_VAULT_DIR", Some(tmp_str.as_str())),
],
|| {
let s = session_without_agent();
match open_vault(&s) {
Ok(_) => panic!("expected AgentNotRunning, got Ok"),
Err(e) => {
assert_eq!(e.kind, McpErrorKind::AgentNotRunning);
assert!(e.message.contains("tsafe-agent not running"));
}
}
},
);
}
#[test]
fn lookup_key_returns_value_for_present_key() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("demo.vault");
let mut v = Vault::create(&path, b"pw").unwrap();
v.set("demo/foo", "value-foo", Default::default()).unwrap();
drop(v);
let vault = Vault::open_read_only(&path, b"pw").unwrap();
let value = lookup_key(&vault, "demo/foo").expect("present key resolves");
assert_eq!(value.as_str(), "value-foo");
}
#[test]
fn lookup_key_missing_returns_key_missing() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("demo.vault");
let v = Vault::create(&path, b"pw").unwrap();
drop(v);
let vault = Vault::open_read_only(&path, b"pw").unwrap();
let err = lookup_key(&vault, "does/not/exist").expect_err("missing must error");
assert_eq!(err.kind, McpErrorKind::KeyMissing);
assert!(err.message.to_lowercase().contains("does/not/exist"));
}
#[test]
fn vault_has_key_distinguishes_present_and_missing() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("demo.vault");
let mut v = Vault::create(&path, b"pw").unwrap();
v.set("demo/here", "x", Default::default()).unwrap();
drop(v);
let vault = Vault::open_read_only(&path, b"pw").unwrap();
assert!(vault_has_key(&vault, "demo/here"));
assert!(!vault_has_key(&vault, "demo/missing"));
}
}