use crate::registry::{Tool, ToolContext};
use aegis_security::is_safe_url;
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{json, Value};
use std::process::Stdio;
pub struct WebFetchProTool {
pub binary: String,
pub mode: String,
pub timeout_secs: u64,
}
#[async_trait]
impl Tool for WebFetchProTool {
fn name(&self) -> &str {
"web_fetch_pro"
}
fn description(&self) -> &str {
"Fetch a URL through anti-bot defenses (TLS-fingerprint HTTP, or a stealth browser that bypasses Cloudflare) via the external Scrapling tool. Returns Markdown. Use when web_extract is blocked."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"url": { "type": "string", "description": "URL to fetch" },
"css_selector": { "type": "string", "description": "Optional CSS selector to extract only matching elements" },
"solve_cloudflare": { "type": "boolean", "description": "stealth mode only: attempt to solve Cloudflare Turnstile (default false)" }
},
"required": ["url"]
})
}
async fn execute(&self, args: Value, _ctx: &ToolContext<'_>) -> Result<String> {
let url = args["url"].as_str().unwrap_or("").trim();
if url.is_empty() {
return Ok("Error: url is required".to_string());
}
is_safe_url(url).map_err(|e| anyhow::anyhow!("SSRF check failed: {e}"))?;
let css = args["css_selector"].as_str();
let solve_cf = args["solve_cloudflare"].as_bool().unwrap_or(false);
let tmp = std::env::temp_dir().join(format!("aegis_wfp_{}.md", std::process::id()));
let subcmd = if self.mode == "stealth" {
"stealthy-fetch"
} else {
"get"
};
let mut cmd = tokio::process::Command::new(&self.binary);
cmd.arg("extract")
.arg(subcmd)
.arg(url)
.arg(&tmp);
if let Some(sel) = css {
if !sel.trim().is_empty() {
cmd.arg("--css-selector").arg(sel);
}
}
if self.mode == "stealth" && solve_cf {
cmd.arg("--solve-cloudflare");
}
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let output = tokio::time::timeout(
std::time::Duration::from_secs(self.timeout_secs),
cmd.output(),
)
.await
.map_err(|_| anyhow::anyhow!("web_fetch_pro timed out after {}s", self.timeout_secs))?
.map_err(|e| {
anyhow::anyhow!(
"failed to run '{}': {e}. Is Scrapling installed (pip install \"scrapling[shell]\") and on PATH?",
self.binary
)
})?;
let result = std::fs::read_to_string(&tmp).ok();
let _ = std::fs::remove_file(&tmp);
if let Some(md) = result {
if !md.trim().is_empty() {
return Ok(md);
}
}
let stderr = String::from_utf8_lossy(&output.stderr);
if !output.status.success() {
Ok(format!(
"[web_fetch_pro error] exit {}\n{stderr}",
output.status.code().unwrap_or(-1)
))
} else {
Ok(format!("web_fetch_pro returned no content. stderr: {stderr}"))
}
}
}