Menu
Public repository / 2c509452a4982321959914cab8d52b881e8368de
AkurAI-PassVault / crates/passvault-mcp/src/tools.rs
Latest change 2c509452a4982321959914cab8d52b881e8368de - AKURAI-PASSVAULT-6 cargo fmt --all by Ólafur Búi Ólafsson
use akurai_json::Value;
use akurai_storage::BTree;
use passvault_core::crypto::Crypto;
use passvault_core::entities::{normalize_entry_name, Folder, VaultEntry};
use passvault_core::types::Timestamp;
use std::sync::{Arc, Mutex};
fn upper_bound(prefix: &[u8]) -> Vec<u8> {
let mut bound = prefix.to_vec();
bound.push(0xff);
bound
}
fn now() -> Timestamp {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
pub struct ToolRegistry {
db: Arc<Mutex<BTree>>,
crypto: Arc<Crypto>,
}
impl ToolRegistry {
pub fn new(db: Arc<Mutex<BTree>>, crypto: Arc<Crypto>) -> Self {
ToolRegistry { db, crypto }
}
fn json_rpc_result(&self, id: Option<Value>, result: Value) -> String {
let mut resp = Vec::new();
resp.push(("jsonrpc".into(), Value::Str("2.0".into())));
resp.push(("result".into(), result));
if let Some(id_val) = id {
resp.push(("id".into(), id_val));
}
Value::Object(resp).to_json()
}
fn json_rpc_error(&self, id: Option<Value>, code: i64, message: &str) -> String {
let mut resp = Vec::new();
resp.push(("jsonrpc".into(), Value::Str("2.0".into())));
resp.push((
"error".into(),
Value::Object(vec![
("code".into(), Value::Int(code)),
("message".into(), Value::Str(message.into())),
]),
));
if let Some(id_val) = id {
resp.push(("id".into(), id_val));
}
Value::Object(resp).to_json()
}
pub fn handle_request(&self, body: &str) -> String {
let req = match akurai_json::parse(body) {
Ok(v) => v,
Err(e) => return self.json_rpc_error(None, -32700, &format!("Parse error: {e}")),
};
let method = req.get("method").and_then(|v| v.as_str()).unwrap_or("");
let id = req.get("id").cloned();
// JSON-RPC notifications carry no `id` and MUST NOT receive a response body.
// Return an empty string; the HTTP layer turns this into `202 Accepted` with no
// body (per the MCP Streamable-HTTP spec). Strict clients (codex/rmcp) error out
// if a notification gets a JSON-RPC response.
if id.is_none() || method.starts_with("notifications/") {
return String::new();
}
match method {
"initialize" => self.json_rpc_result(
id,
Value::Object(vec![
("protocolVersion".into(), Value::Str("2024-11-05".into())),
(
"capabilities".into(),
Value::Object(vec![("tools".into(), Value::Object(vec![]))]),
),
(
"serverInfo".into(),
Value::Object(vec![
("name".into(), Value::Str("akurai-passvault".into())),
("version".into(), Value::Str("0.0.1".into())),
]),
),
]),
),
"tools/list" => {
let tools = self.list_tools();
self.json_rpc_result(
id,
Value::Object(vec![("tools".into(), Value::Array(tools))]),
)
}
"tools/get" => {
let params = match req.get("params") {
Some(Value::Object(pairs)) => pairs.clone(),
_ => vec![],
};
let tool_name = params
.iter()
.find(|(k, _)| k == "name")
.and_then(|(_, v)| v.as_str())
.unwrap_or("");
let tools = self.list_tools();
let tool = tools
.into_iter()
.find(|t| t.get("name").and_then(|v| v.as_str()) == Some(tool_name));
match tool {
Some(t) => self.json_rpc_result(id, Value::Object(vec![("tool".into(), t)])),
None => self.json_rpc_error(id, -32602, &format!("Unknown tool: {tool_name}")),
}
}
"tools/call" => {
let params = match req.get("params") {
Some(Value::Object(pairs)) => pairs.clone(),
_ => vec![],
};
let tool_name = params
.iter()
.find(|(k, _)| k == "name")
.and_then(|(_, v)| v.as_str())
.unwrap_or("");
let arguments: Vec<(String, Value)> = params
.iter()
.find(|(k, _)| k == "arguments")
.and_then(|(_, v)| match v {
Value::Object(pairs) => Some(pairs.clone()),
_ => None,
})
.unwrap_or_default();
match self.execute(tool_name, &arguments) {
Ok(result) => self.json_rpc_result(
id,
Value::Object(vec![(
"content".into(),
Value::Array(vec![Value::Object(vec![
("type".into(), Value::Str("text".into())),
("text".into(), Value::Str(result.to_json())),
])]),
)]),
),
Err(e) => self.json_rpc_error(id, -32603, &format!("Tool error: {e}")),
}
}
_ => self.json_rpc_error(id, -32601, &format!("Method not found: {method}")),
}
}
pub fn list_tools(&self) -> Vec<Value> {
vec![
self.tool_def(
"list_entries",
"List all vault entries (name, uri, username)",
vec![],
),
self.tool_def(
"get_entry",
"Get a single entry by ID with all fields",
vec![("id", "number", "Entry ID", true)],
),
self.tool_def(
"create_entry",
"Create a new vault entry",
vec![
("name", "string", "Entry name", true),
("username", "string", "Username", false),
("password", "string", "Password", false),
("uri", "string", "URI/URL", false),
("notes", "string", "Notes", false),
("folderId", "number", "Folder ID", false),
],
),
self.tool_def(
"update_entry",
"Update an existing entry",
vec![
("id", "number", "Entry ID", true),
("name", "string", "Entry name", false),
("username", "string", "Username", false),
("password", "string", "Password", false),
("uri", "string", "URI/URL", false),
("notes", "string", "Notes", false),
("folderId", "number", "Folder ID", false),
],
),
self.tool_def(
"delete_entry",
"Delete a vault entry",
vec![("id", "number", "Entry ID", true)],
),
self.tool_def(
"generate_password",
"Generate a random password",
vec![
("length", "number", "Password length (default 24)", false),
(
"symbols",
"boolean",
"Include symbols (default true)",
false,
),
],
),
self.tool_def(
"search_entries",
"Search entries by name, URI, or username",
vec![("query", "string", "Search query", true)],
),
self.tool_def("list_folders", "List all folders", vec![]),
self.tool_def(
"create_folder",
"Create a new folder",
vec![("name", "string", "Folder name", true)],
),
self.tool_def(
"delete_folder",
"Delete a folder",
vec![("id", "number", "Folder ID", true)],
),
self.tool_def(
"system_status",
"Report PassVault storage health and record counts",
vec![],
),
self.tool_def(
"provision_workspace_secret",
"Create a workspace secret reference without returning the value",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("name", "string", "Secret name", true),
("value", "string", "Secret value", true),
("notes", "string", "Metadata notes", false),
],
),
self.tool_def(
"get_workspace_secret_metadata",
"Get secret metadata (name, rotation status) — never the value",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Secret ID", true),
],
),
self.tool_def(
"get_workspace_secret_value",
"Get secret value — requires a grant with sufficient visibility",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Secret ID", true),
("identityId", "number", "Service identity ID", true),
("purpose", "string", "Access purpose for audit", true),
],
),
self.tool_def(
"list_workspace_secrets",
"List all secrets in a workspace (metadata only)",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
],
),
self.tool_def(
"delete_workspace_secret",
"Delete a workspace secret (idempotent)",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Secret ID", true),
],
),
self.tool_def(
"create_service_identity",
"Create a service identity for workspace secret access",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("name", "string", "Identity name", true),
],
),
self.tool_def(
"list_service_identities",
"List service identities in a workspace",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
],
),
self.tool_def(
"delete_service_identity",
"Delete a service identity and its grants (idempotent)",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Identity ID", true),
],
),
self.tool_def(
"create_secret_grant",
"Grant a service identity access to a secret with a visibility level",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("identityId", "number", "Identity ID", true),
("secretId", "number", "Secret ID", true),
(
"visibility",
"string",
"metadata | hidden_programmatic | forbidden_display",
true,
),
],
),
self.tool_def(
"list_secret_grants",
"List grants, optionally filtered by secretId",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
(
"secretId",
"number",
"Filter by secret ID (optional)",
false,
),
],
),
self.tool_def(
"delete_secret_grant",
"Delete a grant (idempotent)",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Grant ID", true),
],
),
self.tool_def(
"rotate_secret",
"Rotate a secret value with optional overlap window for rollback",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Secret ID", true),
("newValue", "string", "New secret value", true),
(
"overlapSeconds",
"number",
"Overlap window before cutover (optional)",
false,
),
],
),
self.tool_def(
"rollback_secret",
"Rollback a rotated secret to its previous value",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Secret ID", true),
],
),
self.tool_def(
"revoke_workspace_secret",
"Deactivate a secret (idempotent)",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
("id", "number", "Secret ID", true),
],
),
self.tool_def(
"deprovision_workspace",
"Remove all workspace-scoped data (idempotent)",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
],
),
self.tool_def(
"list_audit_entries",
"List audit entries for a workspace",
vec![
("organizationId", "string", "Organization ID", true),
("workspaceId", "string", "Workspace ID", true),
],
),
]
}
fn tool_def(
&self,
name: &str,
description: &str,
params: Vec<(&str, &str, &str, bool)>,
) -> Value {
let mut properties = Vec::new();
let mut required = Vec::new();
for (pname, ptype, pdesc, is_required) in params {
properties.push((
pname.to_string(),
Value::Object(vec![
("type".into(), Value::Str(ptype.into())),
("description".into(), Value::Str(pdesc.into())),
]),
));
if is_required {
required.push(Value::Str(pname.to_string()));
}
}
Value::Object(vec![
("name".into(), Value::Str(name.into())),
("description".into(), Value::Str(description.into())),
(
"inputSchema".into(),
Value::Object(vec![
("type".into(), Value::Str("object".into())),
("properties".into(), Value::Object(properties)),
("required".into(), Value::Array(required)),
]),
),
])
}
pub fn execute(&self, tool_name: &str, args: &[(String, Value)]) -> Result<Value, String> {
match tool_name {
"list_entries" => self.list_entries(),
"get_entry" => self.get_entry(args),
"create_entry" => self.create_entry(args),
"update_entry" => self.update_entry(args),
"delete_entry" => self.delete_entry(args),
"generate_password" => self.generate_password(args),
"search_entries" => self.search_entries(args),
"list_folders" => self.list_folders(),
"create_folder" => self.create_folder(args),
"delete_folder" => self.delete_folder(args),
"system_status" => self.system_status(),
"provision_workspace_secret" => self.provision_workspace_secret(args),
"get_workspace_secret_metadata" => self.get_workspace_secret_metadata(args),
"get_workspace_secret_value" => self.get_workspace_secret_value(args),
"list_workspace_secrets" => self.list_workspace_secrets(args),
"delete_workspace_secret" => self.delete_workspace_secret(args),
"create_service_identity" => self.create_service_identity(args),
"list_service_identities" => self.list_service_identities(args),
"delete_service_identity" => self.delete_service_identity(args),
"create_secret_grant" => self.create_secret_grant(args),
"list_secret_grants" => self.list_secret_grants(args),
"delete_secret_grant" => self.delete_secret_grant(args),
"rotate_secret" => self.rotate_secret(args),
"rollback_secret" => self.rollback_secret(args),
"revoke_workspace_secret" => self.revoke_workspace_secret(args),
"deprovision_workspace" => self.deprovision_workspace(args),
"list_audit_entries" => self.list_audit_entries(args),
_ => Err(format!("unknown tool: {tool_name}")),
}
}
fn system_status(&self) -> Result<Value, String> {
let entries = self.scan_entries()?.len() as i64;
let folders = match self.list_folders()? {
Value::Array(values) => values.len() as i64,
_ => 0,
};
Ok(Value::Object(vec![
("status".into(), Value::Str("ok".into())),
("entries".into(), Value::Int(entries)),
("folders".into(), Value::Int(folders)),
]))
}
fn scan_entries(&self) -> Result<Vec<(i64, VaultEntry)>, String> {
let prefix = b"vault:entry:";
let start = prefix.to_vec();
let end = upper_bound(prefix);
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let entries = db.range(&start, &end).map_err(|e| format!("range: {e}"))?;
let mut results = Vec::new();
for (key, val) in entries {
let key_str = String::from_utf8_lossy(&key).to_string();
if key_str.contains("counter") {
continue;
}
let id = key_str
.strip_prefix("vault:entry:")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let decrypted = self
.crypto
.decrypt(&val)
.map_err(|e| format!("decrypt: {e}"))?;
if let Ok(v) = akurai_json::parse(&String::from_utf8_lossy(&decrypted)) {
if let Ok(entry) = VaultEntry::from_value(&v) {
results.push((id, entry));
}
}
}
Ok(results)
}
fn get_entry_raw(&self, id: i64) -> Result<VaultEntry, String> {
let key = format!("vault:entry:{id}");
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let bytes = db
.get(key.as_bytes())
.map_err(|e| format!("storage: {e}"))?
.ok_or_else(|| "entry not found".to_string())?;
let decrypted = self
.crypto
.decrypt(&bytes)
.map_err(|e| format!("decrypt: {e}"))?;
let v = akurai_json::parse(&String::from_utf8_lossy(&decrypted))
.map_err(|e| format!("json: {e}"))?;
VaultEntry::from_value(&v).map_err(|e| format!("parse: {e}"))
}
fn list_entries(&self) -> Result<Value, String> {
let entries = self.scan_entries()?;
Ok(Value::Array(
entries
.into_iter()
.map(|(_, e)| e.summary_value())
.collect(),
))
}
fn get_entry(&self, args: &[(String, Value)]) -> Result<Value, String> {
let id = get_i64(args, "id").ok_or("missing id")?;
let entry = self.get_entry_raw(id)?;
Ok(entry.to_value())
}
fn create_entry(&self, args: &[(String, Value)]) -> Result<Value, String> {
let name = normalize_entry_name(&get_str(args, "name").ok_or("name is required")?)
.map_err(|e| e.to_string())?;
let username = get_str(args, "username").unwrap_or_default();
let password = get_str(args, "password").unwrap_or_default();
let uri = get_str(args, "uri").unwrap_or_default();
let notes = get_str(args, "notes").unwrap_or_default();
let folder_id = get_i64(args, "folderId");
let ts = now();
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let next_id = next_counter(&mut db, "vault:entry:counter");
let entry = VaultEntry {
id: next_id,
name,
username,
password,
uri,
notes,
folder_id,
created_at: ts,
updated_at: ts,
};
let encrypted = self
.crypto
.encrypt(entry.to_value().to_json().as_bytes())
.map_err(|e| format!("encrypt: {e}"))?;
db.insert(format!("vault:entry:{next_id}").as_bytes(), &encrypted)
.map_err(|e| format!("insert: {e}"))?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(entry.to_value())
}
fn update_entry(&self, args: &[(String, Value)]) -> Result<Value, String> {
let id = get_i64(args, "id").ok_or("missing id")?;
let mut entry = self.get_entry_raw(id)?;
if let Some(v) = get_str(args, "name") {
entry.name = normalize_entry_name(&v).map_err(|e| e.to_string())?;
}
if let Some(v) = get_str(args, "username") {
entry.username = v;
}
if let Some(v) = get_str(args, "password") {
entry.password = v;
}
if let Some(v) = get_str(args, "uri") {
entry.uri = v;
}
if let Some(v) = get_str(args, "notes") {
entry.notes = v;
}
if let Some(value) = get_arg(args, "folderId") {
entry.folder_id = value.as_i64();
}
entry.updated_at = now();
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let encrypted = self
.crypto
.encrypt(entry.to_value().to_json().as_bytes())
.map_err(|e| format!("encrypt: {e}"))?;
db.insert(format!("vault:entry:{id}").as_bytes(), &encrypted)
.map_err(|e| format!("insert: {e}"))?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(entry.to_value())
}
fn delete_entry(&self, args: &[(String, Value)]) -> Result<Value, String> {
let id = get_i64(args, "id").ok_or("missing id")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let removed = db
.delete(format!("vault:entry:{id}").as_bytes())
.map_err(|e| format!("delete: {e}"))?;
if !removed {
return Err(format!("entry {id} not found"));
}
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![("deleted".into(), Value::Bool(true))]))
}
fn generate_password(&self, args: &[(String, Value)]) -> Result<Value, String> {
let length = get_i64(args, "length").unwrap_or(24).clamp(8, 128) as usize;
let symbols = get_bool(args, "symbols").unwrap_or(true);
let mut charset = Vec::new();
charset.extend(b'a'..=b'z');
charset.extend(b'A'..=b'Z');
charset.extend(b'0'..=b'9');
if symbols {
charset.extend(b"!@#$%^&*()_+-=[]{}|;:,.<>?");
}
use rand::Rng;
let password: String = (0..length)
.map(|_| charset[rand::thread_rng().gen_range(0..charset.len())] as char)
.collect();
Ok(Value::Object(vec![(
"password".into(),
Value::Str(password),
)]))
}
fn search_entries(&self, args: &[(String, Value)]) -> Result<Value, String> {
let query = get_str(args, "query")
.ok_or("missing query")?
.to_lowercase();
let entries = self.scan_entries()?;
let results: Vec<Value> = entries
.into_iter()
.filter(|(_, e)| {
e.name.to_lowercase().contains(&query)
|| e.uri.to_lowercase().contains(&query)
|| e.username.to_lowercase().contains(&query)
|| e.notes.to_lowercase().contains(&query)
})
.map(|(_, e)| e.summary_value())
.collect();
Ok(Value::Array(results))
}
fn list_folders(&self) -> Result<Value, String> {
let prefix = b"vault:folder:";
let start = prefix.to_vec();
let end = upper_bound(prefix);
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let entries = db.range(&start, &end).map_err(|e| format!("range: {e}"))?;
let folders: Vec<Value> = entries
.into_iter()
.filter_map(|(_, val)| {
let v = akurai_json::parse(&String::from_utf8_lossy(&val)).ok()?;
Folder::from_value(&v).ok().map(|f| f.to_value())
})
.collect();
Ok(Value::Array(folders))
}
fn create_folder(&self, args: &[(String, Value)]) -> Result<Value, String> {
let name = get_str(args, "name").ok_or("name is required")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let next_id = next_counter(&mut db, "vault:folder:counter");
let folder = Folder {
id: next_id,
name,
created_at: now(),
};
db.insert(
format!("vault:folder:{next_id}").as_bytes(),
folder.to_value().to_json().as_bytes(),
)
.map_err(|e| format!("insert: {e}"))?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(folder.to_value())
}
fn delete_folder(&self, args: &[(String, Value)]) -> Result<Value, String> {
let id = get_i64(args, "id").ok_or("missing id")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let removed = db
.delete(format!("vault:folder:{id}").as_bytes())
.map_err(|e| format!("delete: {e}"))?;
if !removed {
return Err(format!("folder {id} not found"));
}
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![("deleted".into(), Value::Bool(true))]))
}
// ── Workspace key helpers ──────────────────────────────────────────
fn ws_key(&self, org: &str, ws: &str, suffix: &str) -> String {
format!("ws:{org}:{ws}:{suffix}")
}
fn ws_counter(&self, db: &mut BTree, org: &str, ws: &str, kind: &str) -> i64 {
next_counter(db, &format!("ws:{org}:{ws}:{kind}:counter"))
}
#[allow(clippy::type_complexity)]
fn ws_scan_prefix(
&self,
db: &mut BTree,
org: &str,
ws: &str,
kind: &str,
) -> Result<KeyValuePairs, String> {
let prefix = format!("ws:{org}:{ws}:{kind}:");
let start = prefix.as_bytes().to_vec();
let end = upper_bound(&start);
db.range(&start, &end).map_err(|e| format!("range: {e}"))
}
#[allow(clippy::too_many_arguments)]
fn write_audit(
&self,
db: &mut BTree,
org: &str,
ws: &str,
actor: &str,
action: &str,
target: &str,
purpose: &str,
result: &str,
) -> Result<(), String> {
let id = self.ws_counter(db, org, ws, "audit");
let entry = passvault_core::entities::AuditEntry {
id,
organization_id: org.to_string(),
workspace_id: ws.to_string(),
actor: actor.to_string(),
action: action.to_string(),
target: target.to_string(),
purpose: purpose.to_string(),
result: result.to_string(),
timestamp: now(),
};
db.insert(
self.ws_key(org, ws, &format!("audit:{id}")).as_bytes(),
entry.to_value().to_json().as_bytes(),
)
.map_err(|e| format!("audit write: {e}"))
}
// ── Service Identity tools ─────────────────────────────────────────
fn create_service_identity(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let name = get_str(args, "name").ok_or("missing name")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let id = self.ws_counter(&mut db, &org, &ws, "identity");
let identity = passvault_core::entities::ServiceIdentity {
id,
organization_id: org.clone(),
workspace_id: ws.clone(),
name,
created_at: now(),
};
let key = self.ws_key(&org, &ws, &format!("identity:{id}"));
db.insert(key.as_bytes(), identity.to_value().to_json().as_bytes())
.map_err(|e| format!("insert: {e}"))?;
self.write_audit(
&mut db,
&org,
&ws,
"system",
"create_identity",
&identity.name,
"provisioning",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(identity.to_value())
}
fn list_service_identities(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let rows = self.ws_scan_prefix(&mut db, &org, &ws, "identity")?;
let identities: Vec<Value> = rows
.into_iter()
.filter_map(|(_, val)| {
let v = akurai_json::parse(&String::from_utf8_lossy(&val)).ok()?;
passvault_core::entities::ServiceIdentity::from_value(&v)
.ok()
.map(|i| i.to_value())
})
.collect();
Ok(Value::Array(identities))
}
fn delete_service_identity(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let removed = db
.delete(self.ws_key(&org, &ws, &format!("identity:{id}")).as_bytes())
.map_err(|e| format!("delete: {e}"))?;
if !removed {
return Ok(Value::Object(vec![("deleted".into(), Value::Bool(false))]));
}
// Remove all grants for this identity
let grant_rows = self.ws_scan_prefix(&mut db, &org, &ws, "grant")?;
for (key, val) in &grant_rows {
if let Ok(v) = akurai_json::parse(&String::from_utf8_lossy(val)) {
if let Ok(g) = passvault_core::entities::SecretGrant::from_value(&v) {
if g.identity_id == id {
let _ = db.delete(key).map_err(|e| format!("delete grant: {e}"));
}
}
}
}
self.write_audit(
&mut db,
&org,
&ws,
"system",
"delete_identity",
&format!("identity:{id}"),
"deprovisioning",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![("deleted".into(), Value::Bool(true))]))
}
// ── Secret Grant tools ─────────────────────────────────────────────
fn create_secret_grant(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let identity_id = get_i64(args, "identityId").ok_or("missing identityId")?;
let secret_id = get_i64(args, "secretId").ok_or("missing secretId")?;
let visibility = get_str(args, "visibility").unwrap_or_else(|| "metadata".to_string());
// Validate visibility
if passvault_core::types::GrantVisibility::parse(&visibility).is_none() {
return Err(format!("invalid visibility: {visibility}"));
}
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let id = self.ws_counter(&mut db, &org, &ws, "grant");
let grant = passvault_core::entities::SecretGrant {
id,
organization_id: org.clone(),
workspace_id: ws.clone(),
identity_id,
secret_id,
visibility,
created_at: now(),
};
db.insert(
self.ws_key(&org, &ws, &format!("grant:{id}")).as_bytes(),
grant.to_value().to_json().as_bytes(),
)
.map_err(|e| format!("insert: {e}"))?;
self.write_audit(
&mut db,
&org,
&ws,
"system",
"create_grant",
&format!("identity:{identity_id}->secret:{secret_id}"),
"provisioning",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(grant.to_value())
}
fn list_secret_grants(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let secret_id = get_i64(args, "secretId");
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let rows = self.ws_scan_prefix(&mut db, &org, &ws, "grant")?;
let grants: Vec<Value> = rows
.into_iter()
.filter_map(|(_, val)| {
let v = akurai_json::parse(&String::from_utf8_lossy(&val)).ok()?;
let g = passvault_core::entities::SecretGrant::from_value(&v).ok()?;
if let Some(sid) = secret_id {
if g.secret_id != sid {
return None;
}
}
Some(g.to_value())
})
.collect();
Ok(Value::Array(grants))
}
fn delete_secret_grant(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let removed = db
.delete(self.ws_key(&org, &ws, &format!("grant:{id}")).as_bytes())
.map_err(|e| format!("delete: {e}"))?;
if !removed {
return Ok(Value::Object(vec![("deleted".into(), Value::Bool(false))]));
}
self.write_audit(
&mut db,
&org,
&ws,
"system",
"delete_grant",
&format!("grant:{id}"),
"deprovisioning",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![("deleted".into(), Value::Bool(true))]))
}
// ── Managed Workspace Secret tools ─────────────────────────────────
fn provision_workspace_secret(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let name = get_str(args, "name").ok_or("missing name")?;
let value = get_str(args, "value").ok_or("missing value")?;
let notes = get_str(args, "notes").unwrap_or_default();
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let id = self.ws_counter(&mut db, &org, &ws, "secret");
let secret = passvault_core::entities::WorkspaceSecret {
id,
organization_id: org.clone(),
workspace_id: ws.clone(),
name: name.clone(),
value,
notes,
active: true,
rotation_id: None,
created_at: now(),
updated_at: now(),
};
let encrypted = self
.crypto
.encrypt(secret.to_value().to_json().as_bytes())
.map_err(|e| format!("encrypt: {e}"))?;
db.insert(
self.ws_key(&org, &ws, &format!("secret:{id}")).as_bytes(),
&encrypted,
)
.map_err(|e| format!("insert: {e}"))?;
self.write_audit(
&mut db,
&org,
&ws,
"system",
"provision",
&name,
"provisioning",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
// Return metadata only — never the value
let meta = secret.metadata_value();
Ok(meta)
}
fn get_workspace_secret_metadata(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let secret = self.get_ws_secret_raw(&org, &ws, id)?;
Ok(secret.metadata_value())
}
fn get_workspace_secret_value(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let identity_id = get_i64(args, "identityId").ok_or("missing identityId")?;
let purpose = get_str(args, "purpose").ok_or("missing purpose")?;
// Check grant visibility
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let grant_rows = self.ws_scan_prefix(&mut db, &org, &ws, "grant")?;
let mut allowed = false;
let mut visibility = "metadata".to_string();
for (_, val) in &grant_rows {
if let Ok(v) = akurai_json::parse(&String::from_utf8_lossy(val)) {
if let Ok(g) = passvault_core::entities::SecretGrant::from_value(&v) {
if g.identity_id == identity_id && g.secret_id == id {
allowed = true;
visibility = g.visibility.clone();
break;
}
}
}
}
drop(db);
if !allowed {
let mut db2 = self.db.lock().map_err(|e| format!("lock: {e}"))?;
self.write_audit(
&mut db2,
&org,
&ws,
&format!("identity:{identity_id}"),
"retrieve_denied",
&format!("secret:{id}"),
&purpose,
"denied",
)?;
db2.commit().ok();
return Err("access denied: no grant for this identity/secret pair".to_string());
}
if visibility == "metadata" {
return Err("access denied: grant is metadata-only".to_string());
}
let secret = self.get_ws_secret_raw(&org, &ws, id)?;
let mut db2 = self.db.lock().map_err(|e| format!("lock: {e}"))?;
self.write_audit(
&mut db2,
&org,
&ws,
&format!("identity:{identity_id}"),
"retrieve",
&secret.name,
&purpose,
"allowed",
)?;
db2.commit().ok();
let mut resp = secret.to_value();
if let Value::Object(ref mut pairs) = resp {
pairs.push(("_grantVisibility".into(), Value::Str(visibility.clone())));
}
Ok(resp)
}
fn list_workspace_secrets(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let rows = self.ws_scan_prefix(&mut db, &org, &ws, "secret")?;
let secrets: Vec<Value> = rows
.into_iter()
.filter_map(|(_, encrypted)| {
let decrypted = self.crypto.decrypt(&encrypted).ok()?;
let v = akurai_json::parse(&String::from_utf8_lossy(&decrypted)).ok()?;
passvault_core::entities::WorkspaceSecret::from_value(&v)
.ok()
.map(|s| s.metadata_value())
})
.collect();
Ok(Value::Array(secrets))
}
fn delete_workspace_secret(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let removed = db
.delete(self.ws_key(&org, &ws, &format!("secret:{id}")).as_bytes())
.map_err(|e| format!("delete: {e}"))?;
let _ = db.delete(self.ws_key(&org, &ws, &format!("rotation:{id}")).as_bytes());
let _ = db.delete(
self.ws_key(&org, &ws, &format!("secret_prev:{id}"))
.as_bytes(),
);
self.write_audit(
&mut db,
&org,
&ws,
"system",
"delete_secret",
&format!("secret:{id}"),
"deprovisioning",
if removed { "allowed" } else { "noop" },
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![(
"deleted".into(),
Value::Bool(removed),
)]))
}
// ── Rotation tools ─────────────────────────────────────────────────
fn rotate_secret(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let new_value = get_str(args, "newValue").ok_or("missing newValue")?;
let overlap_secs = get_i64(args, "overlapSeconds");
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let key = self.ws_key(&org, &ws, &format!("secret:{id}"));
let current_bytes = db
.get(key.as_bytes())
.map_err(|e| format!("storage: {e}"))?
.ok_or("secret not found")?;
let decrypted = self
.crypto
.decrypt(¤t_bytes)
.map_err(|e| format!("decrypt: {e}"))?;
let v = akurai_json::parse(&String::from_utf8_lossy(&decrypted))
.map_err(|e| format!("json: {e}"))?;
let mut secret = passvault_core::entities::WorkspaceSecret::from_value(&v)
.map_err(|e| format!("parse: {e}"))?;
// Hash current value for rollback tracking
let prev_hash = {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
secret.value.hash(&mut hasher);
format!("{:x}", hasher.finish())
};
// Save previous version for rollback
db.insert(
self.ws_key(&org, &ws, &format!("secret_prev:{id}"))
.as_bytes(),
¤t_bytes,
)
.map_err(|e| format!("save prev: {e}"))?;
// Update secret with new value
secret.value = new_value;
secret.updated_at = now();
let rotation_id = self.ws_counter(&mut db, &org, &ws, "rotation");
secret.rotation_id = Some(rotation_id);
let phase = if overlap_secs.is_some() {
"overlap"
} else {
"active"
};
let overlap_until = overlap_secs.map(|secs| now() + secs);
let rotation = passvault_core::entities::RotationState {
id: rotation_id,
secret_id: id,
previous_value_hash: Some(prev_hash),
phase: phase.to_string(),
activated_at: now(),
overlap_until,
created_at: now(),
};
let encrypted = self
.crypto
.encrypt(secret.to_value().to_json().as_bytes())
.map_err(|e| format!("encrypt: {e}"))?;
db.insert(key.as_bytes(), &encrypted)
.map_err(|e| format!("insert: {e}"))?;
db.insert(
self.ws_key(&org, &ws, &format!("rotation:{id}")).as_bytes(),
rotation.to_value().to_json().as_bytes(),
)
.map_err(|e| format!("rotation insert: {e}"))?;
self.write_audit(
&mut db,
&org,
&ws,
"system",
"rotate",
&secret.name,
"rotation",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![
("rotationId".into(), Value::Int(rotation_id)),
("phase".into(), Value::Str(phase.to_string())),
]))
}
fn rollback_secret(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let rotation_key = self.ws_key(&org, &ws, &format!("rotation:{id}"));
let rotation_bytes = db
.get(rotation_key.as_bytes())
.map_err(|e| format!("storage: {e}"))?
.ok_or("no rotation found for this secret")?;
let rot_v = akurai_json::parse(&String::from_utf8_lossy(&rotation_bytes))
.map_err(|e| format!("json: {e}"))?;
let rotation = passvault_core::entities::RotationState::from_value(&rot_v)
.map_err(|e| format!("parse: {e}"))?;
if rotation.phase == "rolled_back" {
return Err("already rolled back".to_string());
}
let prev_key = self.ws_key(&org, &ws, &format!("secret_prev:{id}"));
let prev_bytes = db
.get(prev_key.as_bytes())
.map_err(|e| format!("storage: {e}"))?
.ok_or("no previous version available")?;
let decrypted = self
.crypto
.decrypt(&prev_bytes)
.map_err(|e| format!("decrypt: {e}"))?;
let v = akurai_json::parse(&String::from_utf8_lossy(&decrypted))
.map_err(|e| format!("json: {e}"))?;
let mut secret = passvault_core::entities::WorkspaceSecret::from_value(&v)
.map_err(|e| format!("parse: {e}"))?;
secret.updated_at = now();
let encrypted = self
.crypto
.encrypt(secret.to_value().to_json().as_bytes())
.map_err(|e| format!("encrypt: {e}"))?;
let secret_key = self.ws_key(&org, &ws, &format!("secret:{id}"));
db.insert(secret_key.as_bytes(), &encrypted)
.map_err(|e| format!("insert: {e}"))?;
let mut updated_rotation = rotation;
updated_rotation.phase = "rolled_back".to_string();
db.insert(
rotation_key.as_bytes(),
updated_rotation.to_value().to_json().as_bytes(),
)
.map_err(|e| format!("rotation update: {e}"))?;
let _ = db.delete(prev_key.as_bytes());
self.write_audit(
&mut db,
&org,
&ws,
"system",
"rollback",
&secret.name,
"rotation",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![(
"rolledBack".into(),
Value::Bool(true),
)]))
}
fn revoke_workspace_secret(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let id = get_i64(args, "id").ok_or("missing id")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let key = self.ws_key(&org, &ws, &format!("secret:{id}"));
let bytes = match db
.get(key.as_bytes())
.map_err(|e| format!("storage: {e}"))?
{
Some(b) => b,
None => return Ok(Value::Object(vec![("revoked".into(), Value::Bool(false))])),
};
let decrypted = self
.crypto
.decrypt(&bytes)
.map_err(|e| format!("decrypt: {e}"))?;
let v = akurai_json::parse(&String::from_utf8_lossy(&decrypted))
.map_err(|e| format!("json: {e}"))?;
let mut secret = passvault_core::entities::WorkspaceSecret::from_value(&v)
.map_err(|e| format!("parse: {e}"))?;
secret.active = false;
secret.updated_at = now();
let encrypted = self
.crypto
.encrypt(secret.to_value().to_json().as_bytes())
.map_err(|e| format!("encrypt: {e}"))?;
db.insert(key.as_bytes(), &encrypted)
.map_err(|e| format!("insert: {e}"))?;
self.write_audit(
&mut db,
&org,
&ws,
"system",
"revoke",
&secret.name,
"deprovisioning",
"allowed",
)?;
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![("revoked".into(), Value::Bool(true))]))
}
/// Deprovision an entire workspace — idempotent removal of all workspace-scoped data.
fn deprovision_workspace(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let prefix = format!("ws:{org}:{ws}:");
let start = prefix.as_bytes().to_vec();
let end = upper_bound(&start);
let rows: Vec<Vec<u8>> = db
.range(&start, &end)
.map_err(|e| format!("range: {e}"))?
.into_iter()
.map(|(k, _)| k)
.collect();
let count = rows.len() as i64;
for key in &rows {
let _ = db.delete(key);
}
let audit_id = now();
let audit = passvault_core::entities::AuditEntry {
id: audit_id,
organization_id: org.clone(),
workspace_id: ws.clone(),
actor: "system".to_string(),
action: "deprovision".to_string(),
target: "workspace".to_string(),
purpose: "deprovisioning".to_string(),
result: "allowed".to_string(),
timestamp: now(),
};
db.insert(
format!("deprovision_audit:{org}:{ws}:{audit_id}").as_bytes(),
audit.to_value().to_json().as_bytes(),
)
.ok();
db.commit().map_err(|e| format!("commit: {e}"))?;
Ok(Value::Object(vec![
("removedKeys".into(), Value::Int(count)),
("deprovisioned".into(), Value::Bool(true)),
]))
}
// ── Audit tools ────────────────────────────────────────────────────
fn list_audit_entries(&self, args: &[(String, Value)]) -> Result<Value, String> {
let org = get_str(args, "organizationId").ok_or("missing organizationId")?;
let ws = get_str(args, "workspaceId").ok_or("missing workspaceId")?;
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let rows = self.ws_scan_prefix(&mut db, &org, &ws, "audit")?;
let mut entries: Vec<Value> = rows
.into_iter()
.filter_map(|(_, val)| {
let v = akurai_json::parse(&String::from_utf8_lossy(&val)).ok()?;
passvault_core::entities::AuditEntry::from_value(&v)
.ok()
.map(|a| a.to_value())
})
.collect();
// Also check deprovision audit entries at the global key
let depro_prefix = format!("deprovision_audit:{org}:{ws}:");
let depro_start = depro_prefix.as_bytes().to_vec();
let depro_end = upper_bound(&depro_start);
if let Ok(depro_rows) = db.range(&depro_start, &depro_end) {
for (_, val) in depro_rows {
if let Ok(v) = akurai_json::parse(&String::from_utf8_lossy(&val)) {
if let Ok(a) = passvault_core::entities::AuditEntry::from_value(&v) {
entries.push(a.to_value());
}
}
}
}
entries.sort_by(|a, b| {
let ats = a.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0);
let bts = b.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0);
bts.cmp(&ats)
});
Ok(Value::Array(entries))
}
fn get_ws_secret_raw(
&self,
org: &str,
ws: &str,
id: i64,
) -> Result<passvault_core::entities::WorkspaceSecret, String> {
let key = self.ws_key(org, ws, &format!("secret:{id}"));
let mut db = self.db.lock().map_err(|e| format!("lock: {e}"))?;
let bytes = db
.get(key.as_bytes())
.map_err(|e| format!("storage: {e}"))?
.ok_or("secret not found")?;
let decrypted = self
.crypto
.decrypt(&bytes)
.map_err(|e| format!("decrypt: {e}"))?;
let v = akurai_json::parse(&String::from_utf8_lossy(&decrypted))
.map_err(|e| format!("json: {e}"))?;
passvault_core::entities::WorkspaceSecret::from_value(&v).map_err(|e| format!("parse: {e}"))
}
}
type KeyValuePairs = Vec<(Vec<u8>, Vec<u8>)>;
fn get_arg<'a>(args: &'a [(String, Value)], name: &str) -> Option<&'a Value> {
args.iter().find(|(k, _)| k == name).map(|(_, v)| v)
}
fn get_str(args: &[(String, Value)], name: &str) -> Option<String> {
get_arg(args, name).and_then(|v| v.as_str().map(String::from))
}
fn get_i64(args: &[(String, Value)], name: &str) -> Option<i64> {
get_arg(args, name).and_then(|v| v.as_i64())
}
fn get_bool(args: &[(String, Value)], name: &str) -> Option<bool> {
get_arg(args, name).and_then(|v| v.as_bool())
}
fn next_counter(db: &mut BTree, key: &str) -> i64 {
let next = db
.get(key.as_bytes())
.ok()
.flatten()
.and_then(|v| std::str::from_utf8(&v).ok()?.parse::<i64>().ok())
.unwrap_or(0)
+ 1;
let _ = db.insert(key.as_bytes(), next.to_string().as_bytes());
next
}