use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use cortex_ledger::verify_chain;
use cortex_store::Pool;
use serde_json::{json, Value};
use crate::{GateId, ToolError, ToolHandler};
pub struct CortexAuditVerifyTool {
#[allow(dead_code)]
pool: Arc<Mutex<Pool>>,
event_log: PathBuf,
}
impl std::fmt::Debug for CortexAuditVerifyTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CortexAuditVerifyTool")
.field("event_log", &self.event_log)
.finish_non_exhaustive()
}
}
impl CortexAuditVerifyTool {
#[must_use]
pub fn new(pool: Arc<Mutex<Pool>>, event_log: PathBuf) -> Self {
Self { pool, event_log }
}
}
impl ToolHandler for CortexAuditVerifyTool {
fn name(&self) -> &'static str {
"cortex_audit_verify"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::HealthRead]
}
fn call(&self, _params: Value) -> Result<Value, ToolError> {
tracing::info!("cortex_audit_verify called via MCP");
match verify_chain(&self.event_log) {
Ok(report) => {
let issues: Vec<String> = report
.failures
.iter()
.map(|f| {
let event_part = f
.event_id
.as_ref()
.map_or_else(|| String::from("(no event id)"), ToString::to_string);
format!("line {}: event {} — {:?}", f.line, event_part, f.reason)
})
.collect();
Ok(json!({
"ok": report.ok(),
"chain_length": report.rows_scanned,
"issues": issues,
}))
}
Err(err) => {
Ok(json!({
"ok": false,
"chain_length": 0,
"issues": [format!("chain verification failed: {err}")],
}))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
fn make_tool(event_log: PathBuf) -> CortexAuditVerifyTool {
let pool = cortex_store::Pool::open_in_memory().expect("in-memory sqlite");
CortexAuditVerifyTool::new(Arc::new(Mutex::new(pool)), event_log)
}
#[test]
fn name_and_gate() {
let tool = make_tool(PathBuf::from("/nonexistent/event.jsonl"));
assert_eq!(tool.name(), "cortex_audit_verify");
assert!(!tool.gate_set().is_empty());
assert_eq!(tool.gate_set(), &[GateId::HealthRead]);
}
#[test]
fn missing_event_log_returns_ok_false() {
let tool = make_tool(PathBuf::from("/nonexistent/event.jsonl"));
let result = tool.call(serde_json::Value::Null).unwrap();
assert_eq!(result["ok"], false);
assert_eq!(result["chain_length"], 0);
let issues = result["issues"].as_array().unwrap();
assert!(
!issues.is_empty(),
"missing file should produce at least one issue"
);
}
}