vios_app 0.1.1

Small JSON vaults: Argon2id + AES-GCM, with optional AAD binding.
Documentation
use chrono::Utc;
use clap::Parser;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::{
    env, fs, io,
    path::{Path, PathBuf},
};

#[derive(Parser, Debug)]
#[command(
    name = "vaultseal_register",
    about = "Produce a VaultSeal JSON for a run TS"
)]
struct Args {
    /// Explicit TS to use (otherwise read vault_dev_log/RUN_TS)
    #[arg(long)]
    ts: Option<String>,

    /// Optional extra file to hash and include as an additional agent record
    #[arg(long)]
    input: Option<PathBuf>,

    /// Optional extra path to also write the VaultSeal JSON
    #[arg(long)]
    out: Option<PathBuf>,
}

#[derive(Serialize)]
struct AgentSeal {
    agent: String,
    file_path: String,
    sha256: String,
}

#[derive(Serialize)]
struct VaultSeal {
    ts: String,
    generated_at: String,
    agents: Vec<AgentSeal>,
    combined_sha256: String,
}

fn read_ts_from_args_or_runfile(
    arg_ts: Option<String>,
) -> Result<String, Box<dyn std::error::Error>> {
    if let Some(ts) = arg_ts {
        return Ok(ts);
    }
    // Allow `--ts=<value>` via raw env too, just in case user still passes it that way
    if let Some(ts_arg) = env::args().find(|a| a.starts_with("--ts=")) {
        return Ok(ts_arg.trim_start_matches("--ts=").to_string());
    }
    let ts = fs::read_to_string("vault_dev_log/RUN_TS")?
        .trim()
        .to_string();
    Ok(ts)
}

fn load_agent_file(
    agent: &str,
    ts: &str,
) -> Result<(PathBuf, Vec<u8>, String), Box<dyn std::error::Error>> {
    let flat = PathBuf::from(format!("vault_dev_log/{agent}_{ts}.json"));
    let nested = PathBuf::from(format!("vault_dev_log/{agent}/{ts}.json"));

    let path = if flat.exists() {
        flat
    } else if nested.exists() {
        nested
    } else {
        return Err(format!(
            "Missing agent log for '{agent}': expected vault_dev_log/{agent}_{ts}.json or vault_dev_log/{agent}/{ts}.json"
        )
        .into());
    };

    let data = fs::read(&path)?;
    let mut h = Sha256::new();
    h.update(&data);
    let file_hash = hex::encode(h.finalize());
    Ok((path, data, file_hash))
}

fn hash_file(path: &Path) -> Result<(Vec<u8>, String), Box<dyn std::error::Error>> {
    let data = fs::read(path)?;
    let mut h = Sha256::new();
    h.update(&data);
    Ok((data.clone(), hex::encode(h.finalize())))
}

fn safe_write_file(path: &Path, bytes: &[u8]) -> io::Result<()> {
    if path.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            "Target is a directory",
        ));
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, bytes)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();

    // ---- 1) Resolve TS
    let ts = read_ts_from_args_or_runfile(args.ts)?;

    // ---- 2) Expected agents
    let agents = [
        "Naorix",
        "EchoLoom",
        "SealMind",
        "WhisperGen",
        "VaultCrafter",
        "REEMSmith",
        "Scribe-001",
        "Overseer-7",
    ];

    // ---- 3) Load agent logs, compute per-agent + combined hash
    let mut per_agent: Vec<AgentSeal> = Vec::new();
    let mut combined_hasher = Sha256::new();
    let mut found_any = false;

    for agent in agents {
        match load_agent_file(agent, &ts) {
            Ok((path, data, file_hash)) => {
                found_any = true;
                combined_hasher.update(&data);
                per_agent.push(AgentSeal {
                    agent: agent.to_string(),
                    file_path: path.to_string_lossy().into_owned(),
                    sha256: file_hash,
                });
            }
            Err(e) => eprintln!("(warn) {e}"),
        }
    }

    // Optional: include a manual input file as another β€œagent”
    if let Some(input_path) = args.input.as_ref() {
        let (data, file_hash) = hash_file(input_path)?;
        combined_hasher.update(&data);
        per_agent.push(AgentSeal {
            agent: "ManualInput".to_string(),
            file_path: input_path.to_string_lossy().into_owned(),
            sha256: file_hash,
        });
        found_any = true;
    }

    if !found_any {
        return Err("No agent logs found and no --input provided; nothing to seal.".into());
    }

    let seal = VaultSeal {
        ts: ts.clone(),
        generated_at: Utc::now().to_rfc3339(),
        agents: per_agent,
        combined_sha256: hex::encode(combined_hasher.finalize()),
    };
    let seal_json = serde_json::to_string_pretty(&seal)?;

    // ---- 4) Primary outputs: .vaultseal/VaultSeal_<TS>.json + LATEST
    let seal_dir = PathBuf::from(".vaultseal");
    fs::create_dir_all(&seal_dir)?;
    let out_path = seal_dir.join(format!("VaultSeal_{ts}.json"));
    safe_write_file(&out_path, seal_json.as_bytes())
        .map_err(|e| format!("writing {} failed: {}", out_path.display(), e))?;

    safe_write_file(&seal_dir.join("LATEST"), format!("{ts}\n").as_bytes())
        .map_err(|e| format!("writing .vaultseal/LATEST failed: {e}"))?;

    // ---- 5) Audit copy: vault_dev_log/VaultSeal_<TS>.json (skip if that path is a dir)
    let audit_path = PathBuf::from(format!("vault_dev_log/VaultSeal_{ts}.json"));
    match safe_write_file(&audit_path, seal_json.as_bytes()) {
        Ok(_) => {}
        Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
            eprintln!(
                "(audit) {} is a directory; skipping audit copy",
                audit_path.display()
            );
        }
        Err(e) => return Err(format!("writing {} failed: {}", audit_path.display(), e).into()),
    }

    // ---- 6) Optional extra output path
    if let Some(user_out) = args.out.as_ref() {
        if let Some(parent) = user_out.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(user_out, &seal_json)?;
        println!("πŸ“ Also wrote: {}", user_out.display());
    }

    println!("βœ… VaultSeal created");
    println!("   β€’ {}", out_path.display());
    println!("   β€’ {}", audit_path.display());
    println!("πŸ”‘ Combined SHA256: {}", seal.combined_sha256);
    Ok(())
}