vex_llm/tools/
hash.rs

1//! Hash tool for computing cryptographic hashes
2//!
3//! Computes SHA-256 and SHA-512 hashes of text input.
4//!
5//! # Security
6//!
7//! - Uses Rust's sha2 crate (no known vulnerabilities)
8//! - Pure computation, no I/O
9//! - Input length limited to prevent DoS
10
11use async_trait::async_trait;
12use serde_json::Value;
13use sha2::{Digest, Sha256, Sha512};
14
15use crate::tool::{Capability, Tool, ToolDefinition};
16use crate::tool_error::ToolError;
17
18/// Hash tool for computing SHA-256 and SHA-512 hashes.
19///
20/// # Example
21///
22/// ```ignore
23/// use vex_llm::HashTool;
24/// use vex_llm::Tool;
25///
26/// let hash = HashTool::new();
27/// let result = hash.execute(json!({"text": "hello", "algorithm": "sha256"})).await?;
28/// println!("{}", result["hash"]);
29/// ```
30pub struct HashTool {
31    definition: ToolDefinition,
32}
33
34impl HashTool {
35    /// Create a new hash tool
36    pub fn new() -> Self {
37        Self {
38            definition: ToolDefinition::new(
39                "hash",
40                "Compute cryptographic hash (SHA-256 or SHA-512) of text input.",
41                r#"{
42                    "type": "object",
43                    "properties": {
44                        "text": {
45                            "type": "string",
46                            "description": "Text to hash"
47                        },
48                        "algorithm": {
49                            "type": "string",
50                            "enum": ["sha256", "sha512"],
51                            "default": "sha256",
52                            "description": "Hash algorithm to use"
53                        }
54                    },
55                    "required": ["text"]
56                }"#,
57            ),
58        }
59    }
60}
61
62impl Default for HashTool {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68#[async_trait]
69impl Tool for HashTool {
70    fn definition(&self) -> &ToolDefinition {
71        &self.definition
72    }
73
74    fn capabilities(&self) -> Vec<Capability> {
75        vec![Capability::PureComputation, Capability::Cryptography]
76    }
77
78    fn validate(&self, args: &Value) -> Result<(), ToolError> {
79        let text = args
80            .get("text")
81            .and_then(|t| t.as_str())
82            .ok_or_else(|| ToolError::invalid_args("hash", "Missing required field 'text'"))?;
83
84        // Limit input size to prevent DoS (1MB max)
85        if text.len() > 1_000_000 {
86            return Err(ToolError::invalid_args(
87                "hash",
88                "Input text too large (max 1MB)",
89            ));
90        }
91
92        // Validate algorithm if provided
93        if let Some(algo) = args.get("algorithm").and_then(|a| a.as_str()) {
94            if algo != "sha256" && algo != "sha512" {
95                return Err(ToolError::invalid_args(
96                    "hash",
97                    format!("Invalid algorithm '{}'. Must be 'sha256' or 'sha512'", algo),
98                ));
99            }
100        }
101
102        Ok(())
103    }
104
105    async fn execute(&self, args: Value) -> Result<Value, ToolError> {
106        let text = args["text"]
107            .as_str()
108            .ok_or_else(|| ToolError::invalid_args("hash", "Missing 'text' field"))?;
109
110        let algorithm = args
111            .get("algorithm")
112            .and_then(|a| a.as_str())
113            .unwrap_or("sha256");
114
115        let hash_hex = match algorithm {
116            "sha512" => {
117                let mut hasher = Sha512::new();
118                hasher.update(text.as_bytes());
119                hex::encode(hasher.finalize())
120            }
121            _ => {
122                let mut hasher = Sha256::new();
123                hasher.update(text.as_bytes());
124                hex::encode(hasher.finalize())
125            }
126        };
127
128        Ok(serde_json::json!({
129            "hash": hash_hex,
130            "algorithm": algorithm,
131            "input_length": text.len()
132        }))
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[tokio::test]
141    async fn test_sha256_hash() {
142        let tool = HashTool::new();
143        let result = tool
144            .execute(serde_json::json!({"text": "hello", "algorithm": "sha256"}))
145            .await
146            .unwrap();
147
148        // Known SHA-256 of "hello"
149        assert_eq!(
150            result["hash"],
151            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
152        );
153        assert_eq!(result["algorithm"], "sha256");
154    }
155
156    #[tokio::test]
157    async fn test_sha512_hash() {
158        let tool = HashTool::new();
159        let result = tool
160            .execute(serde_json::json!({"text": "hello", "algorithm": "sha512"}))
161            .await
162            .unwrap();
163
164        assert_eq!(result["algorithm"], "sha512");
165        // SHA-512 produces 128 hex characters
166        assert_eq!(result["hash"].as_str().unwrap().len(), 128);
167    }
168
169    #[tokio::test]
170    async fn test_default_algorithm() {
171        let tool = HashTool::new();
172        let result = tool
173            .execute(serde_json::json!({"text": "test"}))
174            .await
175            .unwrap();
176
177        // Default is SHA-256
178        assert_eq!(result["algorithm"], "sha256");
179    }
180
181    #[tokio::test]
182    async fn test_invalid_algorithm() {
183        let tool = HashTool::new();
184        let result = tool.validate(&serde_json::json!({"text": "hello", "algorithm": "md5"}));
185
186        assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
187    }
188
189    #[tokio::test]
190    async fn test_missing_text() {
191        let tool = HashTool::new();
192        let result = tool.validate(&serde_json::json!({}));
193
194        assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
195    }
196
197    #[tokio::test]
198    async fn test_empty_string() {
199        let tool = HashTool::new();
200        let result = tool.execute(serde_json::json!({"text": ""})).await.unwrap();
201
202        // SHA-256 of empty string
203        assert_eq!(
204            result["hash"],
205            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
206        );
207    }
208}