vex_llm/
tool_result.rs

1//! Tool execution result with cryptographic verification
2//!
3//! This module provides the `ToolResult` type which wraps tool output
4//! with a SHA-256 hash for Merkle tree integration.
5//!
6//! # VEX Innovation
7//!
8//! Every tool execution result is automatically hashed, enabling:
9//! - Cryptographic proof of tool execution
10//! - Merkle tree integration for audit trails
11//! - Tamper-evident logging
12//!
13//! # Security Considerations
14//!
15//! - Hash includes timestamp to prevent replay attacks
16//! - Deterministic serialization ensures consistent hashing
17//! - Output is NOT sanitized here (responsibility of the tool)
18
19use serde::{Deserialize, Serialize};
20use std::time::Duration;
21use vex_core::Hash;
22
23/// Result of a tool execution with cryptographic verification data.
24///
25/// # Example
26///
27/// ```
28/// use vex_llm::ToolResult;
29/// use serde_json::json;
30/// use std::time::Duration;
31///
32/// let result = ToolResult::new(
33///     "calculator",
34///     &json!({"expression": "2+2"}),
35///     json!({"result": 4}),
36///     Duration::from_millis(5),
37/// );
38///
39/// // Hash is automatically computed
40/// assert!(!result.hash.to_string().is_empty());
41/// assert_eq!(result.output["result"], 4);
42/// ```
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ToolResult {
45    /// The output value from the tool
46    pub output: serde_json::Value,
47
48    /// SHA-256 hash of (tool_name + args + output + timestamp)
49    /// Used for Merkle tree integration and verification
50    pub hash: Hash,
51
52    /// How long the tool took to execute
53    #[serde(with = "duration_serde")]
54    pub execution_time: Duration,
55
56    /// Optional token count (for LLM-based tools)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub tokens_used: Option<u32>,
59
60    /// ISO 8601 timestamp of execution
61    pub timestamp: String,
62
63    /// Name of the tool that produced this result
64    pub tool_name: String,
65}
66
67impl ToolResult {
68    /// Create a new tool result with automatic hash computation.
69    ///
70    /// The hash is computed from a deterministic JSON representation of:
71    /// - Tool name
72    /// - Input arguments
73    /// - Output value
74    /// - Timestamp
75    ///
76    /// # Arguments
77    ///
78    /// * `tool_name` - Name of the tool
79    /// * `args` - Input arguments that were passed to the tool
80    /// * `output` - Result value from the tool
81    /// * `execution_time` - How long execution took
82    ///
83    /// # Security
84    ///
85    /// The timestamp is captured at creation time and included in the hash
86    /// to prevent replay attacks where an old result could be substituted.
87    pub fn new(
88        tool_name: &str,
89        args: &serde_json::Value,
90        output: serde_json::Value,
91        execution_time: Duration,
92    ) -> Self {
93        let timestamp = chrono::Utc::now().to_rfc3339();
94
95        // Create deterministic hash input
96        // Using sorted object keys for consistency
97        let hash_input = serde_json::json!({
98            "args": args,
99            "output": &output,
100            "timestamp": &timestamp,
101            "tool": tool_name,
102        });
103
104        // Compute SHA-256 hash
105        let hash = Hash::digest(&serde_json::to_vec(&hash_input).unwrap_or_default());
106
107        Self {
108            output,
109            hash,
110            execution_time,
111            tokens_used: None,
112            timestamp,
113            tool_name: tool_name.to_string(),
114        }
115    }
116
117    /// Add token usage information
118    pub fn with_tokens(mut self, tokens: u32) -> Self {
119        self.tokens_used = Some(tokens);
120        self
121    }
122
123    /// Verify that the hash matches the content
124    ///
125    /// # Returns
126    ///
127    /// `true` if the hash is valid for the current content, `false` otherwise.
128    /// A `false` result indicates potential tampering.
129    pub fn verify(&self, args: &serde_json::Value) -> bool {
130        let hash_input = serde_json::json!({
131            "args": args,
132            "output": &self.output,
133            "timestamp": &self.timestamp,
134            "tool": &self.tool_name,
135        });
136
137        let expected = Hash::digest(&serde_json::to_vec(&hash_input).unwrap_or_default());
138
139        self.hash == expected
140    }
141
142    /// Get execution time in milliseconds
143    pub fn execution_ms(&self) -> u128 {
144        self.execution_time.as_millis()
145    }
146}
147
148/// Custom serialization for Duration
149mod duration_serde {
150    use serde::{Deserialize, Deserializer, Serialize, Serializer};
151    use std::time::Duration;
152
153    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
154    where
155        S: Serializer,
156    {
157        // Serialize as milliseconds for portability
158        duration.as_millis().serialize(serializer)
159    }
160
161    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
162    where
163        D: Deserializer<'de>,
164    {
165        let millis = u64::deserialize(deserializer)?;
166        Ok(Duration::from_millis(millis))
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use serde_json::json;
174
175    #[test]
176    fn test_result_creation() {
177        let args = json!({"expression": "1+1"});
178        let result = ToolResult::new(
179            "calculator",
180            &args,
181            json!({"result": 2}),
182            Duration::from_millis(10),
183        );
184
185        assert_eq!(result.tool_name, "calculator");
186        assert_eq!(result.output["result"], 2);
187        assert!(!result.hash.to_string().is_empty());
188        assert!(result.tokens_used.is_none());
189    }
190
191    #[test]
192    fn test_hash_verification() {
193        let args = json!({"expression": "2+2"});
194        let result = ToolResult::new(
195            "calculator",
196            &args,
197            json!({"result": 4}),
198            Duration::from_millis(5),
199        );
200
201        // Valid verification
202        assert!(result.verify(&args));
203
204        // Invalid verification (different args)
205        let different_args = json!({"expression": "3+3"});
206        assert!(!result.verify(&different_args));
207    }
208
209    #[test]
210    fn test_with_tokens() {
211        let args = json!({});
212        let result = ToolResult::new(
213            "llm_tool",
214            &args,
215            json!({"text": "hello"}),
216            Duration::from_millis(100),
217        )
218        .with_tokens(150);
219
220        assert_eq!(result.tokens_used, Some(150));
221    }
222
223    #[test]
224    fn test_serialization() {
225        let args = json!({"x": 1});
226        let result = ToolResult::new("test", &args, json!({"y": 2}), Duration::from_millis(50));
227
228        let json = serde_json::to_string(&result).unwrap();
229        let deserialized: ToolResult = serde_json::from_str(&json).unwrap();
230
231        assert_eq!(deserialized.tool_name, result.tool_name);
232        assert_eq!(deserialized.output, result.output);
233        assert_eq!(deserialized.hash, result.hash);
234    }
235
236    #[test]
237    fn test_execution_ms() {
238        let args = json!({});
239        let result = ToolResult::new("test", &args, json!({}), Duration::from_millis(123));
240        assert_eq!(result.execution_ms(), 123);
241    }
242}