Skip to main content

zeroclaw_runtime/tools/
verifiable_intent.rs

1//! Verifiable Intent tool — exposes VI verification and constraint evaluation
2//! to the agent orchestration loop.
3
4use 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
18/// Tool for verifying Verifiable Intent credential chains and evaluating
19/// constraints against fulfillment data.
20pub 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}