1use super::tools::{Tool, ToolResult};
23use crate::verification::{ProofLedger, ProofLedgerError};
24use serde::{Deserialize, Serialize};
25use serde_json::{json, Value};
26use std::collections::HashMap;
27use std::path::{Path, PathBuf};
28use std::sync::Arc;
29use tokio::sync::RwLock;
30
31pub struct DeltaToolHandler {
33 ledger: Arc<RwLock<ProofLedger>>,
35 #[allow(dead_code)]
37 ledger_path: PathBuf,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct DeltaAnchorInput {
43 pub content: String,
45 pub url: String,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub metadata: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct DeltaVerifyInput {
55 pub hash: String,
57 pub content: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct DeltaLookupInput {
64 pub hash: String,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct DeltaListByUrlInput {
71 pub url: String,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DeltaAnchorOutput {
78 pub hash: String,
80 pub short_hash: String,
82 pub url: String,
84 pub timestamp: String,
86 pub citation: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct DeltaVerifyOutput {
93 pub verified: bool,
95 pub original_hash: String,
97 pub current_hash: String,
99 pub status: String,
101 pub recommendation: String,
103}
104
105impl DeltaToolHandler {
106 pub fn new<P: AsRef<Path>>(ledger_path: P) -> Result<Self, ProofLedgerError> {
112 let path = ledger_path.as_ref().to_path_buf();
113 let ledger = ProofLedger::new(&path)?;
114
115 #[allow(clippy::arc_with_non_send_sync)]
116 Ok(Self {
117 ledger: Arc::new(RwLock::new(ledger)),
118 ledger_path: path,
119 })
120 }
121
122 pub fn in_memory() -> Result<Self, ProofLedgerError> {
124 let ledger = ProofLedger::in_memory()?;
125
126 #[allow(clippy::arc_with_non_send_sync)]
127 Ok(Self {
128 ledger: Arc::new(RwLock::new(ledger)),
129 ledger_path: PathBuf::from(":memory:"),
130 })
131 }
132
133 pub fn tool_definitions() -> Vec<Tool> {
135 vec![
136 Tool::with_schema(
137 "delta_anchor",
138 "Anchor content to Protocol Delta's immutable citation ledger. \
139 Returns a SHA-256 hash that can be used as a verifiable citation. \
140 Use this BEFORE making claims based on external sources.",
141 json!({
142 "type": "object",
143 "properties": {
144 "content": {
145 "type": "string",
146 "description": "The exact content/text to anchor (will be hashed)"
147 },
148 "url": {
149 "type": "string",
150 "description": "Source URL where the content was retrieved from"
151 },
152 "metadata": {
153 "type": "string",
154 "description": "Optional JSON metadata (source type, confidence, etc.)"
155 }
156 },
157 "required": ["content", "url"]
158 }),
159 ),
160 Tool::with_schema(
161 "delta_verify",
162 "Verify that content matches an anchored citation. \
163 Use this to detect if a source has changed (content drift) \
164 since it was originally cited.",
165 json!({
166 "type": "object",
167 "properties": {
168 "hash": {
169 "type": "string",
170 "description": "The original SHA-256 hash from the citation"
171 },
172 "content": {
173 "type": "string",
174 "description": "The current content to verify against the anchor"
175 }
176 },
177 "required": ["hash", "content"]
178 }),
179 ),
180 Tool::with_schema(
181 "delta_lookup",
182 "Look up an anchor by its hash. Returns the original URL, \
183 timestamp, and content snippet for a given citation hash.",
184 json!({
185 "type": "object",
186 "properties": {
187 "hash": {
188 "type": "string",
189 "description": "The SHA-256 hash to look up"
190 }
191 },
192 "required": ["hash"]
193 }),
194 ),
195 Tool::with_schema(
196 "delta_list_by_url",
197 "List all anchored citations from a specific URL. \
198 Useful for finding historical citations from a source.",
199 json!({
200 "type": "object",
201 "properties": {
202 "url": {
203 "type": "string",
204 "description": "The URL to search for"
205 }
206 },
207 "required": ["url"]
208 }),
209 ),
210 Tool::with_schema(
211 "delta_stats",
212 "Get statistics about the Protocol Delta ledger. \
213 Returns total anchors, ledger path, and status.",
214 json!({
215 "type": "object",
216 "properties": {},
217 "required": []
218 }),
219 ),
220 ]
221 }
222
223 pub async fn handle_anchor(
225 &self,
226 args: &HashMap<String, Value>,
227 ) -> Result<ToolResult, ProofLedgerError> {
228 let content = args
229 .get("content")
230 .and_then(|v| v.as_str())
231 .ok_or_else(|| {
232 ProofLedgerError::Io(std::io::Error::new(
233 std::io::ErrorKind::InvalidInput,
234 "Missing 'content' argument",
235 ))
236 })?;
237
238 let url = args.get("url").and_then(|v| v.as_str()).ok_or_else(|| {
239 ProofLedgerError::Io(std::io::Error::new(
240 std::io::ErrorKind::InvalidInput,
241 "Missing 'url' argument",
242 ))
243 })?;
244
245 let metadata = args
246 .get("metadata")
247 .and_then(|v| v.as_str())
248 .map(|s| s.to_string());
249
250 let ledger = self.ledger.read().await;
251 let hash = ledger.anchor(content, url, metadata)?;
252
253 let output = DeltaAnchorOutput {
254 hash: hash.clone(),
255 short_hash: hash[..8].to_string(),
256 url: url.to_string(),
257 timestamp: chrono::Utc::now().to_rfc3339(),
258 citation: format!(
259 "[sha256:{}...] (Anchored {}) → {}",
260 &hash[..8],
261 chrono::Utc::now().format("%Y-%m-%d"),
262 url
263 ),
264 };
265
266 let json_output = serde_json::to_string_pretty(&output).unwrap_or_else(|_| hash.clone());
267
268 Ok(ToolResult::text(format!(
269 "ANCHORED: Content successfully bound to immutable ledger.\n\n\
270 Citation ID: {}\n\
271 Short Hash: {}\n\
272 Source: {}\n\n\
273 Use this citation format in reports:\n\
274 {}\n\n\
275 Raw JSON:\n{}",
276 output.hash, output.short_hash, output.url, output.citation, json_output
277 )))
278 }
279
280 pub async fn handle_verify(
282 &self,
283 args: &HashMap<String, Value>,
284 ) -> Result<ToolResult, ProofLedgerError> {
285 let hash = args.get("hash").and_then(|v| v.as_str()).ok_or_else(|| {
286 ProofLedgerError::Io(std::io::Error::new(
287 std::io::ErrorKind::InvalidInput,
288 "Missing 'hash' argument",
289 ))
290 })?;
291
292 let content = args
293 .get("content")
294 .and_then(|v| v.as_str())
295 .ok_or_else(|| {
296 ProofLedgerError::Io(std::io::Error::new(
297 std::io::ErrorKind::InvalidInput,
298 "Missing 'content' argument",
299 ))
300 })?;
301
302 let ledger = self.ledger.read().await;
303 let result = ledger.verify(hash, content)?;
304
305 let output = DeltaVerifyOutput {
306 verified: result.verified,
307 original_hash: result.original_hash.clone(),
308 current_hash: result.current_hash.clone(),
309 status: if result.verified {
310 "VERIFIED".to_string()
311 } else {
312 "DRIFT_DETECTED".to_string()
313 },
314 recommendation: if result.verified {
315 "Citation is valid. Content matches the original anchor.".to_string()
316 } else {
317 format!(
318 "WARNING: Content has changed since anchoring. \
319 Original hash: {}..., Current hash: {}... \
320 Consider re-anchoring or flagging this citation as outdated.",
321 &result.original_hash[..8],
322 &result.current_hash[..8]
323 )
324 },
325 };
326
327 let json_output = serde_json::to_string_pretty(&output)
328 .unwrap_or_else(|_| format!("verified: {}", result.verified));
329
330 let status_icon = if result.verified { "✓" } else { "⚠" };
331
332 Ok(ToolResult::text(format!(
333 "{} {}\n\n\
334 Original Hash: {}...\n\
335 Current Hash: {}...\n\
336 Match: {}\n\n\
337 Recommendation: {}\n\n\
338 Raw JSON:\n{}",
339 status_icon,
340 output.status,
341 &output.original_hash[..8],
342 &output.current_hash[..8],
343 output.verified,
344 output.recommendation,
345 json_output
346 )))
347 }
348
349 pub async fn handle_lookup(
351 &self,
352 args: &HashMap<String, Value>,
353 ) -> Result<ToolResult, ProofLedgerError> {
354 let hash = args.get("hash").and_then(|v| v.as_str()).ok_or_else(|| {
355 ProofLedgerError::Io(std::io::Error::new(
356 std::io::ErrorKind::InvalidInput,
357 "Missing 'hash' argument",
358 ))
359 })?;
360
361 let ledger = self.ledger.read().await;
362 let anchor = ledger.get_anchor(hash)?;
363
364 let json_output = serde_json::to_string_pretty(&anchor)
365 .unwrap_or_else(|_| format!("hash: {}", anchor.hash));
366
367 Ok(ToolResult::text(format!(
368 "ANCHOR FOUND\n\n\
369 Hash: {}\n\
370 URL: {}\n\
371 Timestamp: {}\n\
372 Snippet: {}...\n\n\
373 Raw JSON:\n{}",
374 anchor.hash,
375 anchor.url,
376 anchor.timestamp.to_rfc3339(),
377 &anchor.content_snippet[..anchor.content_snippet.len().min(100)],
378 json_output
379 )))
380 }
381
382 pub async fn handle_list_by_url(
384 &self,
385 args: &HashMap<String, Value>,
386 ) -> Result<ToolResult, ProofLedgerError> {
387 let url = args.get("url").and_then(|v| v.as_str()).ok_or_else(|| {
388 ProofLedgerError::Io(std::io::Error::new(
389 std::io::ErrorKind::InvalidInput,
390 "Missing 'url' argument",
391 ))
392 })?;
393
394 let ledger = self.ledger.read().await;
395 let anchors = ledger.list_by_url(url)?;
396
397 if anchors.is_empty() {
398 return Ok(ToolResult::text(format!(
399 "No anchors found for URL: {}\n\n\
400 This URL has no citations in the ledger. \
401 Use delta_anchor to create the first citation.",
402 url
403 )));
404 }
405
406 let mut output = format!("ANCHORS FOR URL: {}\nTotal: {}\n\n", url, anchors.len());
407
408 for (i, anchor) in anchors.iter().enumerate() {
409 output.push_str(&format!(
410 "{}. [{}...] - {}\n Snippet: {}...\n\n",
411 i + 1,
412 &anchor.hash[..8],
413 anchor.timestamp.format("%Y-%m-%d %H:%M:%S UTC"),
414 &anchor.content_snippet[..anchor.content_snippet.len().min(60)]
415 ));
416 }
417
418 let json_output =
419 serde_json::to_string_pretty(&anchors).unwrap_or_else(|_| "[]".to_string());
420
421 output.push_str(&format!("\nRaw JSON:\n{}", json_output));
422
423 Ok(ToolResult::text(output))
424 }
425
426 pub async fn handle_stats(&self) -> Result<ToolResult, ProofLedgerError> {
428 let ledger = self.ledger.read().await;
429 let count = ledger.count()?;
430 let path = ledger.ledger_path();
431
432 let stats = json!({
433 "total_anchors": count,
434 "ledger_path": path.to_string_lossy(),
435 "status": "operational",
436 "protocol_version": "delta_v2"
437 });
438
439 let json_output = serde_json::to_string_pretty(&stats).unwrap_or_else(|_| "{}".to_string());
440
441 Ok(ToolResult::text(format!(
442 "PROTOCOL DELTA LEDGER STATUS\n\n\
443 Total Anchors: {}\n\
444 Ledger Path: {}\n\
445 Status: Operational\n\
446 Protocol Version: Delta V2 (Amber)\n\n\
447 Raw JSON:\n{}",
448 count,
449 path.display(),
450 json_output
451 )))
452 }
453
454 pub async fn handle_tool(
456 &self,
457 name: &str,
458 args: &HashMap<String, Value>,
459 ) -> Result<ToolResult, ProofLedgerError> {
460 match name {
461 "delta_anchor" => self.handle_anchor(args).await,
462 "delta_verify" => self.handle_verify(args).await,
463 "delta_lookup" => self.handle_lookup(args).await,
464 "delta_list_by_url" => self.handle_list_by_url(args).await,
465 "delta_stats" => self.handle_stats().await,
466 _ => Ok(ToolResult::error(format!("Unknown tool: {}", name))),
467 }
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use crate::mcp::tools::ToolResultContent;
475
476 #[tokio::test]
477 async fn test_delta_anchor() {
478 let handler = DeltaToolHandler::in_memory().unwrap();
479
480 let mut args = HashMap::new();
481 args.insert("content".to_string(), json!("Test content for anchoring"));
482 args.insert("url".to_string(), json!("https://example.com/test"));
483
484 let result = handler.handle_anchor(&args).await.unwrap();
485
486 assert!(result.is_error.is_none() || !result.is_error.unwrap());
487 if let ToolResultContent::Text { text } = &result.content[0] {
488 assert!(text.contains("ANCHORED"));
489 assert!(text.contains("sha256"));
490 }
491 }
492
493 #[tokio::test]
494 async fn test_delta_verify_success() {
495 let handler = DeltaToolHandler::in_memory().unwrap();
496
497 let mut anchor_args = HashMap::new();
499 anchor_args.insert("content".to_string(), json!("Immutable content"));
500 anchor_args.insert("url".to_string(), json!("https://example.com"));
501
502 let anchor_result = handler.handle_anchor(&anchor_args).await.unwrap();
503
504 let hash = if let ToolResultContent::Text { text } = &anchor_result.content[0] {
506 text.lines()
508 .find(|l: &&str| l.starts_with("Citation ID:"))
509 .and_then(|l: &str| l.split(':').nth(1))
510 .map(|s: &str| s.trim().to_string())
511 .unwrap()
512 } else {
513 panic!("Expected text content");
514 };
515
516 let mut verify_args = HashMap::new();
518 verify_args.insert("hash".to_string(), json!(hash));
519 verify_args.insert("content".to_string(), json!("Immutable content"));
520
521 let result = handler.handle_verify(&verify_args).await.unwrap();
522
523 if let ToolResultContent::Text { text } = &result.content[0] {
524 assert!(text.contains("VERIFIED"));
525 assert!(text.contains("Match: true"));
526 }
527 }
528
529 #[tokio::test]
530 async fn test_delta_verify_drift() {
531 let handler = DeltaToolHandler::in_memory().unwrap();
532
533 let mut anchor_args = HashMap::new();
535 anchor_args.insert("content".to_string(), json!("Original content"));
536 anchor_args.insert("url".to_string(), json!("https://example.com"));
537
538 let anchor_result = handler.handle_anchor(&anchor_args).await.unwrap();
539
540 let hash = if let ToolResultContent::Text { text } = &anchor_result.content[0] {
541 text.lines()
542 .find(|l: &&str| l.starts_with("Citation ID:"))
543 .and_then(|l: &str| l.split(':').nth(1))
544 .map(|s: &str| s.trim().to_string())
545 .unwrap()
546 } else {
547 panic!("Expected text content");
548 };
549
550 let mut verify_args = HashMap::new();
552 verify_args.insert("hash".to_string(), json!(hash));
553 verify_args.insert("content".to_string(), json!("Modified content"));
554
555 let result = handler.handle_verify(&verify_args).await.unwrap();
556
557 if let ToolResultContent::Text { text } = &result.content[0] {
558 assert!(text.contains("DRIFT_DETECTED"));
559 assert!(text.contains("WARNING"));
560 }
561 }
562
563 #[tokio::test]
564 async fn test_delta_stats() {
565 let handler = DeltaToolHandler::in_memory().unwrap();
566
567 let result = handler.handle_stats().await.unwrap();
568
569 if let ToolResultContent::Text { text } = &result.content[0] {
570 assert!(text.contains("PROTOCOL DELTA LEDGER STATUS"));
571 assert!(text.contains("Total Anchors:"));
572 assert!(text.contains("Delta V2"));
573 }
574 }
575
576 #[tokio::test]
577 async fn test_tool_definitions() {
578 let tools = DeltaToolHandler::tool_definitions();
579
580 assert_eq!(tools.len(), 5);
581
582 let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
583 assert!(names.contains(&"delta_anchor"));
584 assert!(names.contains(&"delta_verify"));
585 assert!(names.contains(&"delta_lookup"));
586 assert!(names.contains(&"delta_list_by_url"));
587 assert!(names.contains(&"delta_stats"));
588 }
589}