1use async_trait::async_trait;
5use serde_json::json;
6use std::sync::Arc;
7
8use crate::security::SecurityPolicy;
9use crate::security::policy::ToolOperation;
10use crate::verifiable_intent::error::ViError;
11use crate::verifiable_intent::types::{Constraint, Fulfillment};
12use crate::verifiable_intent::verification::{
13 ConstraintCheckResult, StrictnessMode, check_constraints, verify_sd_hash_binding,
14 verify_timestamps,
15};
16use zeroclaw_api::tool::{Tool, ToolResult};
17
18pub struct VerifiableIntentTool {
21 security: Arc<SecurityPolicy>,
22 strictness: StrictnessMode,
23}
24
25impl VerifiableIntentTool {
26 pub fn new(security: Arc<SecurityPolicy>, strictness: StrictnessMode) -> Self {
27 Self {
28 security,
29 strictness,
30 }
31 }
32}
33
34#[async_trait]
35impl Tool for VerifiableIntentTool {
36 fn name(&self) -> &str {
37 "vi_verify"
38 }
39
40 fn description(&self) -> &str {
41 "Verify a Verifiable Intent credential chain. Supports two operations: \
42 'verify_binding' checks sd_hash binding between credential layers; \
43 'evaluate_constraints' validates constraints against fulfillment data."
44 }
45
46 fn parameters_schema(&self) -> serde_json::Value {
47 json!({
48 "type": "object",
49 "properties": {
50 "operation": {
51 "type": "string",
52 "enum": ["verify_binding", "evaluate_constraints", "verify_timestamps"],
53 "description": "The VI operation to perform."
54 },
55 "sd_hash": {
56 "type": "string",
57 "description": "Expected sd_hash value (for verify_binding)."
58 },
59 "serialized_parent": {
60 "type": "string",
61 "description": "Serialized parent SD-JWT (for verify_binding)."
62 },
63 "iat": {
64 "type": "integer",
65 "description": "Issued-at timestamp (for verify_timestamps)."
66 },
67 "exp": {
68 "type": "integer",
69 "description": "Expiration timestamp (for verify_timestamps)."
70 },
71 "constraints": {
72 "type": "array",
73 "description": "Constraint array (for evaluate_constraints)."
74 },
75 "fulfillment": {
76 "type": "object",
77 "description": "Fulfillment data to evaluate against (for evaluate_constraints)."
78 }
79 },
80 "required": ["operation"]
81 })
82 }
83
84 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
85 if let Err(error) = self
86 .security
87 .enforce_tool_operation(ToolOperation::Read, "vi_verify")
88 {
89 return Ok(ToolResult {
90 success: false,
91 output: String::new(),
92 error: Some(error),
93 });
94 }
95
96 let operation = args.get("operation").and_then(|v| v.as_str()).unwrap_or("");
97
98 match operation {
99 "verify_binding" => execute_verify_binding(&args),
100 "evaluate_constraints" => execute_evaluate_constraints(&args, self.strictness),
101 "verify_timestamps" => execute_verify_timestamps(&args),
102 _ => Ok(ToolResult {
103 success: false,
104 output: String::new(),
105 error: Some(format!("unknown operation: {operation}")),
106 }),
107 }
108 }
109}
110
111fn execute_verify_binding(args: &serde_json::Value) -> anyhow::Result<ToolResult> {
112 let sd_hash = args
113 .get("sd_hash")
114 .and_then(|v| v.as_str())
115 .ok_or_else(|| {
116 ::zeroclaw_log::record!(
117 WARN,
118 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
119 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
120 .with_attrs(::serde_json::json!({"param": "sd_hash"})),
121 "tool argument validation failed"
122 );
123
124 anyhow::Error::msg("missing 'sd_hash' parameter")
125 })?;
126 let serialized_parent = args
127 .get("serialized_parent")
128 .and_then(|v| v.as_str())
129 .ok_or_else(|| {
130 ::zeroclaw_log::record!(
131 WARN,
132 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
133 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
134 .with_attrs(::serde_json::json!({"param": "serialized_parent"})),
135 "tool argument validation failed"
136 );
137
138 anyhow::Error::msg("missing 'serialized_parent' parameter")
139 })?;
140
141 match verify_sd_hash_binding(sd_hash, serialized_parent) {
142 Ok(()) => Ok(ToolResult {
143 success: true,
144 output: "sd_hash binding verified".into(),
145 error: None,
146 }),
147 Err(e) => Ok(vi_error_result(&e)),
148 }
149}
150
151fn execute_evaluate_constraints(
152 args: &serde_json::Value,
153 strictness: StrictnessMode,
154) -> anyhow::Result<ToolResult> {
155 let constraints_value = args.get("constraints").ok_or_else(|| {
156 ::zeroclaw_log::record!(
157 WARN,
158 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
159 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
160 .with_attrs(::serde_json::json!({"param": "constraints"})),
161 "tool argument validation failed"
162 );
163
164 anyhow::Error::msg("missing 'constraints' parameter")
165 })?;
166 let fulfillment_value = args.get("fulfillment").ok_or_else(|| {
167 ::zeroclaw_log::record!(
168 WARN,
169 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
170 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
171 .with_attrs(::serde_json::json!({"param": "fulfillment"})),
172 "tool argument validation failed"
173 );
174
175 anyhow::Error::msg("missing 'fulfillment' parameter")
176 })?;
177
178 let constraints: Vec<Constraint> = serde_json::from_value(constraints_value.clone())?;
179 let fulfillment: Fulfillment = serde_json::from_value(fulfillment_value.clone())?;
180
181 let results = check_constraints(&constraints, &fulfillment, strictness);
182 let all_satisfied = results.iter().all(|r| r.satisfied);
183
184 let summary: Vec<serde_json::Value> = results.iter().map(constraint_result_json).collect();
185
186 Ok(ToolResult {
187 success: all_satisfied,
188 output: serde_json::to_string_pretty(&json!({
189 "all_satisfied": all_satisfied,
190 "results": summary,
191 }))?,
192 error: if all_satisfied {
193 None
194 } else {
195 Some("one or more constraints violated".into())
196 },
197 })
198}
199
200fn execute_verify_timestamps(args: &serde_json::Value) -> anyhow::Result<ToolResult> {
201 let iat = args.get("iat").and_then(|v| v.as_i64()).ok_or_else(|| {
202 ::zeroclaw_log::record!(
203 WARN,
204 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
205 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
206 .with_attrs(::serde_json::json!({"param": "iat"})),
207 "tool argument validation failed"
208 );
209
210 anyhow::Error::msg("missing 'iat' parameter")
211 })?;
212 let exp = args.get("exp").and_then(|v| v.as_i64()).ok_or_else(|| {
213 ::zeroclaw_log::record!(
214 WARN,
215 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
216 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
217 .with_attrs(::serde_json::json!({"param": "exp"})),
218 "tool argument validation failed"
219 );
220
221 anyhow::Error::msg("missing 'exp' parameter")
222 })?;
223
224 match verify_timestamps(iat, exp) {
225 Ok(()) => Ok(ToolResult {
226 success: true,
227 output: "timestamps valid".into(),
228 error: None,
229 }),
230 Err(e) => Ok(vi_error_result(&e)),
231 }
232}
233
234fn vi_error_result(e: &ViError) -> ToolResult {
235 ToolResult {
236 success: false,
237 output: String::new(),
238 error: Some(format!("{}", e)),
239 }
240}
241
242fn constraint_result_json(r: &ConstraintCheckResult) -> serde_json::Value {
243 json!({
244 "constraint_type": r.constraint_type,
245 "satisfied": r.satisfied,
246 "violations": r.violations.iter().map(|v: &ViError| v.to_string()).collect::<Vec<_>>(),
247 })
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::security::SecurityPolicy;
254
255 fn test_tool() -> VerifiableIntentTool {
256 let policy = Arc::new(SecurityPolicy::default());
257 VerifiableIntentTool::new(policy, StrictnessMode::Strict)
258 }
259
260 #[tokio::test]
261 async fn verify_timestamps_valid() {
262 let tool = test_tool();
263 let now = chrono::Utc::now().timestamp();
264 let args = json!({
265 "operation": "verify_timestamps",
266 "iat": now - 60,
267 "exp": now + 3600,
268 });
269 let result = tool.execute(args).await.unwrap();
270 assert!(result.success);
271 }
272
273 #[tokio::test]
274 async fn verify_timestamps_expired() {
275 let tool = test_tool();
276 let args = json!({
277 "operation": "verify_timestamps",
278 "iat": 1_000_000,
279 "exp": 1_000_001,
280 });
281 let result = tool.execute(args).await.unwrap();
282 assert!(!result.success);
283 }
284
285 #[tokio::test]
286 async fn evaluate_constraints_empty() {
287 let tool = test_tool();
288 let args = json!({
289 "operation": "evaluate_constraints",
290 "constraints": [],
291 "fulfillment": {},
292 });
293 let result = tool.execute(args).await.unwrap();
294 assert!(result.success);
295 }
296
297 #[tokio::test]
298 async fn unknown_operation_fails() {
299 let tool = test_tool();
300 let args = json!({ "operation": "bad_op" });
301 let result = tool.execute(args).await.unwrap();
302 assert!(!result.success);
303 }
304}