Skip to main content

agentzero_tools/
url_validation.rs

1use agentzero_core::common::url_policy::UrlAccessPolicy;
2use agentzero_core::common::util::parse_http_url_with_policy;
3use agentzero_core::{Tool, ToolContext, ToolResult};
4use anyhow::anyhow;
5use async_trait::async_trait;
6
7#[derive(Default)]
8pub struct UrlValidationTool {
9    url_policy: UrlAccessPolicy,
10}
11
12impl UrlValidationTool {
13    pub fn with_url_policy(mut self, policy: UrlAccessPolicy) -> Self {
14        self.url_policy = policy;
15        self
16    }
17}
18
19#[async_trait]
20impl Tool for UrlValidationTool {
21    fn name(&self) -> &'static str {
22        "url_validation"
23    }
24
25    fn description(&self) -> &'static str {
26        "Validate a URL against the access policy (private IPs, domain allowlist, etc.)."
27    }
28
29    fn input_schema(&self) -> Option<serde_json::Value> {
30        Some(serde_json::json!({
31            "type": "object",
32            "required": ["url"],
33            "properties": {
34                "url": {"type": "string", "description": "URL to validate"}
35            }
36        }))
37    }
38
39    async fn execute(&self, input: &str, _ctx: &ToolContext) -> anyhow::Result<ToolResult> {
40        let trimmed = input.trim();
41        if trimmed.is_empty() {
42            return Err(anyhow!("url input is required"));
43        }
44
45        let parsed = parse_http_url_with_policy(trimmed, &self.url_policy)?;
46
47        Ok(ToolResult {
48            output: parsed.to_string(),
49        })
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::UrlValidationTool;
56    use agentzero_core::{Tool, ToolContext};
57
58    #[tokio::test]
59    async fn url_validation_accepts_https_success_path() {
60        let tool = UrlValidationTool::default();
61        let result = tool
62            .execute(
63                "https://example.com/path?q=1",
64                &ToolContext::new(".".to_string()),
65            )
66            .await
67            .expect("validation should succeed");
68        assert!(result.output.starts_with("https://example.com/path"));
69    }
70
71    #[tokio::test]
72    async fn url_validation_rejects_unsupported_scheme_negative_path() {
73        let tool = UrlValidationTool::default();
74        let err = tool
75            .execute("file:///tmp/test.txt", &ToolContext::new(".".to_string()))
76            .await
77            .expect_err("unsupported scheme should fail");
78        assert!(err.to_string().contains("unsupported url scheme"));
79    }
80
81    #[tokio::test]
82    async fn url_validation_blocks_private_ip_negative_path() {
83        let tool = UrlValidationTool::default();
84        let err = tool
85            .execute(
86                "http://172.16.0.1/admin",
87                &ToolContext::new(".".to_string()),
88            )
89            .await
90            .expect_err("private IP should be blocked");
91        assert!(err.to_string().contains("URL access denied"));
92    }
93}