cortex_mcp/tools/
audit_verify.rs1use std::path::PathBuf;
16use std::sync::{Arc, Mutex};
17
18use cortex_ledger::verify_chain;
19use cortex_store::Pool;
20use serde_json::{json, Value};
21
22use crate::{GateId, ToolError, ToolHandler};
23
24pub struct CortexAuditVerifyTool {
43 #[allow(dead_code)]
46 pool: Arc<Mutex<Pool>>,
47 event_log: PathBuf,
49}
50
51impl std::fmt::Debug for CortexAuditVerifyTool {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("CortexAuditVerifyTool")
54 .field("event_log", &self.event_log)
55 .finish_non_exhaustive()
56 }
57}
58
59impl CortexAuditVerifyTool {
60 #[must_use]
62 pub fn new(pool: Arc<Mutex<Pool>>, event_log: PathBuf) -> Self {
63 Self { pool, event_log }
64 }
65}
66
67impl ToolHandler for CortexAuditVerifyTool {
68 fn name(&self) -> &'static str {
69 "cortex_audit_verify"
70 }
71
72 fn gate_set(&self) -> &'static [GateId] {
73 &[GateId::HealthRead]
74 }
75
76 fn call(&self, _params: Value) -> Result<Value, ToolError> {
77 tracing::info!("cortex_audit_verify called via MCP");
78
79 match verify_chain(&self.event_log) {
80 Ok(report) => {
81 let issues: Vec<String> = report
82 .failures
83 .iter()
84 .map(|f| {
85 let event_part = f
86 .event_id
87 .as_ref()
88 .map_or_else(|| String::from("(no event id)"), ToString::to_string);
89 format!("line {}: event {} — {:?}", f.line, event_part, f.reason)
90 })
91 .collect();
92
93 Ok(json!({
94 "ok": report.ok(),
95 "chain_length": report.rows_scanned,
96 "issues": issues,
97 }))
98 }
99 Err(err) => {
100 Ok(json!({
104 "ok": false,
105 "chain_length": 0,
106 "issues": [format!("chain verification failed: {err}")],
107 }))
108 }
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::sync::{Arc, Mutex};
117
118 fn make_tool(event_log: PathBuf) -> CortexAuditVerifyTool {
119 let pool = cortex_store::Pool::open_in_memory().expect("in-memory sqlite");
121 CortexAuditVerifyTool::new(Arc::new(Mutex::new(pool)), event_log)
122 }
123
124 #[test]
125 fn name_and_gate() {
126 let tool = make_tool(PathBuf::from("/nonexistent/event.jsonl"));
127 assert_eq!(tool.name(), "cortex_audit_verify");
128 assert!(!tool.gate_set().is_empty());
129 assert_eq!(tool.gate_set(), &[GateId::HealthRead]);
130 }
131
132 #[test]
133 fn missing_event_log_returns_ok_false() {
134 let tool = make_tool(PathBuf::from("/nonexistent/event.jsonl"));
135 let result = tool.call(serde_json::Value::Null).unwrap();
136 assert_eq!(result["ok"], false);
137 assert_eq!(result["chain_length"], 0);
138 let issues = result["issues"].as_array().unwrap();
139 assert!(
140 !issues.is_empty(),
141 "missing file should produce at least one issue"
142 );
143 }
144}