AkurAI Build
Menu

Public repository / 312f7f9361137f01d24979c8fb377c6e0937a87f

AkurAI-PassVault / crates/passvault-cli/src/cmd_tool.rs

Latest change 58558732228cea9b646049555021e899ee300f35 - Checkpoint WIP + add AkurAI Build CI/CD pipeline by Ólafur Búi Ólafsson

use akurai_json::Value;
use akurai_storage::BTree;
use passvault_core::crypto::Crypto;
use passvault_mcp::tools::ToolRegistry;
use std::io::Read;
use std::sync::{Arc, Mutex};

pub fn run(name: &str, confirmed: bool) -> Result<(), String> {
    if matches!(name, "delete_entry" | "delete_folder") && !confirmed {
        return Err(format!("{name} requires --confirm"));
    }

    let registry = registry()?;
    let mut input = String::new();
    std::io::stdin()
        .read_to_string(&mut input)
        .map_err(|error| format!("read stdin: {error}"))?;
    let arguments = if input.trim().is_empty() {
        Vec::new()
    } else {
        match akurai_json::parse(&input).map_err(|error| format!("invalid JSON input: {error}"))? {
            Value::Object(arguments) => arguments,
            _ => return Err("stdin must contain one JSON object".into()),
        }
    };

    let mut result = registry.execute(name, &arguments)?;
    redact(&mut result);
    println!("{}", result.to_json());
    Ok(())
}

pub fn list() -> Result<(), String> {
    let registry = registry()?;
    println!("{}", Value::Array(registry.list_tools()).to_json());
    Ok(())
}

fn registry() -> Result<ToolRegistry, String> {
    let db_path =
        std::env::var("AKURAI_PASSVAULT_DB").unwrap_or_else(|_| "data/passvault.db".into());
    let key = std::env::var("AKURAI_PASSVAULT_KEY")
        .map_err(|_| "AKURAI_PASSVAULT_KEY must be set".to_string())?;
    let db = BTree::open(&db_path).map_err(|error| format!("open {db_path}: {error}"))?;
    let crypto = Crypto::from_hex_key(&key).map_err(|error| format!("crypto init: {error}"))?;
    Ok(ToolRegistry::new(
        Arc::new(Mutex::new(db)),
        Arc::new(crypto),
    ))
}

fn redact(value: &mut Value) {
    match value {
        Value::Object(fields) => {
            for (key, value) in fields {
                if matches!(key.as_str(), "password" | "token" | "secret") {
                    *value = Value::Str("[REDACTED]".into());
                } else {
                    redact(value);
                }
            }
        }
        Value::Array(values) => {
            for value in values {
                redact(value);
            }
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn redacts_secret_fields_recursively() {
        let mut value = Value::Object(vec![(
            "nested".into(),
            Value::Array(vec![Value::Object(vec![(
                "password".into(),
                Value::Str("do-not-print".into()),
            )])]),
        )]);
        redact(&mut value);
        let output = value.to_json();
        assert!(!output.contains("do-not-print"));
        assert!(output.contains("[REDACTED]"));
    }
}