use ai_model_vault::audit::AuditEventType;
use ai_model_vault::blockchain::{AuditProof, BlockchainAudit};
use ai_model_vault::{Result, VaultConfig, VaultError};
use crate::cli::args::ChainCommands;
fn parse_event_type(raw: &str) -> Result<AuditEventType> {
let upper = raw.to_uppercase();
serde_json::from_value(serde_json::Value::String(upper)).map_err(|_| {
VaultError::ConfigError(format!(
"unknown event type '{raw}' -- expected one of: MODEL_STORED, \
MODEL_RETRIEVED, MODEL_DELETED, AUTH_SUCCESS, AUTH_FAILURE, \
SECURITY_VIOLATION, VAULT_CREATED, CONFIG_CHANGED"
))
})
}
fn chain_disabled() -> VaultError {
VaultError::ConfigError(
"blockchain audit trail is not enabled for this vault -- set \
security.blockchain_audit = true in config.toml (it also requires \
security.audit_log = true). Entries are recorded from the moment it \
is enabled; history written before then is not in the chain."
.to_string(),
)
}
pub fn handle_chain(command: ChainCommands, config: VaultConfig) -> Result<()> {
if !config.security.blockchain_audit {
return Err(chain_disabled());
}
let chain_dir = config.get_audit_chain_dir();
if !chain_dir.exists() {
return Err(VaultError::ConfigError(format!(
"blockchain audit trail is enabled but nothing has been recorded yet \
({} does not exist). It is created on the first audited operation.",
chain_dir.display()
)));
}
let chain = BlockchainAudit::new(&chain_dir, config.security.blockchain_block_size)?;
match command {
ChainCommands::Status => status(&chain),
ChainCommands::Verify => verify(&chain),
ChainCommands::Proof {
block,
entry,
output,
} => proof(&chain, block, entry, output),
ChainCommands::VerifyProof { proof: path } => verify_proof(&path),
ChainCommands::Search {
model,
event,
limit,
} => search(&chain, model, event, limit),
}
}
fn status(chain: &BlockchainAudit) -> Result<()> {
println!("Blockchain audit trail");
println!(" Height: {} block(s)", chain.height());
println!(" Block size: {} entry/entries", chain.block_size());
if let Some(latest) = chain.latest() {
println!(" Latest: #{} {}", latest.index, latest.hash);
println!(" Entries: {} in latest block", latest.entries.len());
}
let pending = chain.pending_count();
println!(" Pending: {pending} entry/entries");
if pending > 0 {
println!(
"\n⚠️ {pending} entry/entries are in memory and not yet on disk. They are\n \
not covered by `aim chain verify` and would be lost if this process\n \
died now. They are written when the block fills (block size {}) or\n \
on clean exit.",
chain.block_size()
);
}
Ok(())
}
fn verify(chain: &BlockchainAudit) -> Result<()> {
let result = chain.verify_chain();
println!(
"Verified {} of {} block(s)",
result.blocks_verified, result.blocks_total
);
if result.valid {
println!("✅ Chain intact — hash links, Merkle roots, and block hashes all check out");
return Ok(());
}
println!("❌ Chain verification FAILED");
for issue in &result.issues {
println!(" - {issue}");
}
Err(VaultError::IntegrityError(format!(
"audit chain verification failed with {} issue(s)",
result.issues.len()
)))
}
fn proof(
chain: &BlockchainAudit,
block: u64,
entry: usize,
output: Option<std::path::PathBuf>,
) -> Result<()> {
let proof = chain.generate_proof(block, entry)?;
let json = serde_json::to_string_pretty(&proof)?;
match output {
Some(path) => {
std::fs::write(&path, json)?;
println!("Proof written to {}", path.display());
}
None => println!("{json}"),
}
Ok(())
}
fn verify_proof(path: &std::path::Path) -> Result<()> {
let contents = std::fs::read_to_string(path)?;
let proof: AuditProof = serde_json::from_str(&contents)?;
let result = BlockchainAudit::verify_proof(&proof);
if result.valid {
println!("✅ Proof valid");
println!(" Entry is in block #{}", proof.block_index);
println!(" Chain of custody reaches genesis {}", proof.genesis_hash);
println!(
"\nNote: this checks the proof's own consistency. It does not prove the\n\
genesis hash belongs to your vault — compare it against\n`aim chain status` on the vault you trust."
);
return Ok(());
}
println!("❌ Proof INVALID");
for issue in &result.issues {
println!(" - {issue}");
}
Err(VaultError::IntegrityError(format!(
"proof verification failed with {} issue(s)",
result.issues.len()
)))
}
fn search(
chain: &BlockchainAudit,
model: Option<String>,
event: Option<String>,
limit: usize,
) -> Result<()> {
let event_type = event.as_deref().map(parse_event_type).transpose()?;
let results = chain.search(model.as_deref(), event_type, None, None, limit)?;
if results.is_empty() {
println!("No matching entries.");
return Ok(());
}
println!("{} matching entr(ies):\n", results.len());
for (block_idx, entry_idx, entry) in &results {
let audit = &entry.audit;
println!(
" block {block_idx} entry {entry_idx} {} {:?}{}",
audit.timestamp.to_rfc3339(),
audit.event_type,
if audit.success { "" } else { " (FAILED)" }
);
println!(" {}", audit.description);
println!(" proof: aim chain proof --block {block_idx} --entry {entry_idx}");
}
Ok(())
}