magi-rs 0.9.0

Magi Agent: a terminal AI assistant in Rust with sandboxed tool execution, OAuth login, and encrypted local memory (authenticated encryption with error-correcting FEC via the cryptovault crate).
Documentation
// Author: Julian Bolivar
// Version: 1.0.0
// Date: 2026-07-14
//! Errores de dominio del subsistema Vault.
//!
//! Sigue el patrón de [`crate::memory::error::MemoryError`]: los tipos foráneos
//! (`cryptovault`, `rusqlite`) se **stringifican** en lugar de envolverse, para
//! mantener los tipos externos fuera de la API pública del vault y conservar
//! `Send + Sync` sin acoplarse a sus versiones.
//!
//! **Invariante de seguridad:** ningún mensaje de error contiene jamás el
//! *valor* de un secreto — solo su *nombre* o la etapa que falló.

use thiserror::Error;

/// Errores de dominio del subsistema Vault.
///
/// Cada variante nombra una etapa de fallo distinguible por el llamador; el
/// AEAD subyacente impide falsificación sin importar cuál se exponga.
#[derive(Debug, Error)]
pub enum VaultError {
    /// El desenvolvimiento (`unwrap`) de la DEK falló el tag AEAD tras la
    /// corrección FEC — es decir, la clave maestra es **incorrecta**.
    ///
    /// Es **reintentable** y **nunca** dispara un borrado de datos: ver la
    /// política de nunca-borrar (REQ-V35).
    #[error("incorrect passphrase")]
    WrongPassphrase,

    /// `vault_meta` está presente pero es irrecuperable **incluso tras** la
    /// corrección FEC (corrupción más allá de la capacidad del códec).
    ///
    /// Requiere una acción **explícita** del usuario; el sistema jamás se
    /// auto-repara destruyendo datos.
    #[error("vault metadata is corrupt and unrecoverable")]
    VaultMetaCorrupt,

    /// No existe un secreto con el nombre indicado. El nombre no es material
    /// sensible, por lo que puede figurar en el mensaje.
    #[error("secret not found: {0}")]
    SecretNotFound(String),

    /// Fallo criptográfico propagado desde `cryptovault` (mensaje ya
    /// sanitizado por el crate — sin oráculos de decode ni de timing).
    #[error("crypto error: {0}")]
    Crypto(String),

    /// Fallo a nivel de almacenamiento SQLite.
    #[error("storage error: {0}")]
    Storage(String),

    /// No hay TTY y no se proveyó la passphrase por `-p`/`MAGI_PASSPHRASE`
    /// (REQ-V40): la passphrase **jamás** se lee de un pipe. Reintentable.
    #[error("no passphrase: use -p or MAGI_PASSPHRASE in non-interactive environments")]
    PassphraseUnavailable,

    /// La passphrase no alcanza el piso duro de fortaleza (REQ-V18). El mensaje
    /// lleva los motivos + tips, **jamás** la passphrase.
    #[error("passphrase rejected: {0}")]
    WeakPassphrase(String),

    /// Error de E/S del terminal (prompt oculto / eco). Mensaje del `io::Error`,
    /// sin material sensible.
    #[error("I/O error: {0}")]
    Io(String),

    /// El usuario no confirmó una operación destructiva (REQ-V22). El CLI
    /// sale con código de salida distinto de cero para que los scripts lo
    /// detecten; no es un fallo del sistema.
    #[error("operation cancelled")]
    Aborted,

    /// El valor supera `cryptovault::MAX_PLAINTEXT_LEN` (MAGI run 4,
    /// Caspar). Lleva el límite, nunca el valor.
    #[error("value exceeds {0} bytes")]
    ValueTooLarge(usize),
}

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

    #[test]
    fn test_wrong_passphrase_display_is_user_facing_and_leaks_nothing() {
        let e = VaultError::WrongPassphrase;
        assert_eq!(e.to_string(), "incorrect passphrase");
    }

    #[test]
    fn test_vault_meta_corrupt_is_distinct_variant() {
        let e = VaultError::VaultMetaCorrupt;
        assert!(e.to_string().to_lowercase().contains("corrupt"));
    }

    #[test]
    fn test_secret_not_found_includes_name_only() {
        let e = VaultError::SecretNotFound("OPENAI_API_KEY".to_string());
        let msg = e.to_string();
        assert!(msg.contains("OPENAI_API_KEY"));
    }

    #[test]
    fn test_aborted_display_is_user_facing_and_stable() {
        assert_eq!(VaultError::Aborted.to_string(), "operation cancelled");
    }

    #[test]
    fn test_value_too_large_includes_the_limit_only() {
        let e = VaultError::ValueTooLarge(10 * 1024 * 1024);
        assert!(e.to_string().contains("10485760"));
    }
}