1use crate::registry::{Tool, ToolContext};
8use aegis_security::{is_safe_url, sanitize_credentials};
9use anyhow::Result;
10use async_trait::async_trait;
11use serde_json::{json, Value};
12
13pub struct HttpRequestTool;
15
16impl HttpRequestTool {
17 pub fn new() -> Self {
19 HttpRequestTool
20 }
21}
22
23impl Default for HttpRequestTool {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29#[async_trait]
30impl Tool for HttpRequestTool {
31 fn name(&self) -> &str {
32 "http_request"
33 }
34
35 fn description(&self) -> &str {
36 "Make an HTTP request to any API/URL with a chosen method, headers and body. Returns status, headers and (size-capped) body. SSRF-protected; credentials in the output are redacted."
37 }
38
39 fn parameters(&self) -> Value {
40 json!({
41 "type": "object",
42 "properties": {
43 "url": { "type": "string", "description": "Full request URL (http/https)" },
44 "method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"], "description": "HTTP method (default GET)" },
45 "headers": { "type": "object", "description": "Request headers as a JSON object of string→string" },
46 "body": { "type": "string", "description": "Request body (for POST/PUT/PATCH)" },
47 "timeout_secs": { "type": "integer", "description": "Request timeout in seconds (default 30)" },
48 "max_bytes": { "type": "integer", "description": "Cap the response body at this many bytes (default 1000000)" }
49 },
50 "required": ["url"]
51 })
52 }
53
54 async fn execute(&self, args: Value, ctx: &ToolContext<'_>) -> Result<String> {
55 let url = args["url"].as_str().unwrap_or("").trim();
56 if url.is_empty() {
57 return Ok("Error: url is required".to_string());
58 }
59
60 if ctx.sandbox_enabled {
63 let identity = ctx.effective_identity();
64 let policy = aegis_security::derive_sandbox_policy(&identity, "http_request", &ctx.cwd);
65 if policy.deny_all {
66 return Ok(format!(
67 "http_request denied by sandbox policy: identity '{}' (trust '{}') may not make network requests.",
68 identity.display(),
69 identity.trust_level(),
70 ));
71 }
72 }
73
74 is_safe_url(url).map_err(|e| anyhow::anyhow!("SSRF check failed: {e}"))?;
75
76 let method_str = args["method"].as_str().unwrap_or("GET").to_uppercase();
77 let method = reqwest::Method::from_bytes(method_str.as_bytes())
78 .map_err(|_| anyhow::anyhow!("invalid HTTP method: {method_str}"))?;
79 let timeout = args["timeout_secs"].as_u64().unwrap_or(30);
80 let max_bytes = args["max_bytes"].as_u64().map(|v| v as usize).unwrap_or(1_000_000);
81
82 let client = reqwest::Client::builder()
83 .timeout(std::time::Duration::from_secs(timeout))
84 .user_agent("aegis-agent/2.0 (+https://github.com/Druuugbug/Aegis)")
85 .build()?;
86
87 let mut req = client.request(method, url);
88 if let Some(map) = args["headers"].as_object() {
89 for (k, v) in map {
90 if let Some(vs) = v.as_str() {
91 req = req.header(k.as_str(), vs);
92 }
93 }
94 }
95 if let Some(body) = args["body"].as_str() {
96 req = req.body(body.to_string());
97 }
98
99 let mut resp = req.send().await?;
100 let status = resp.status();
101
102 let mut header_lines = String::new();
104 for (name, value) in resp.headers().iter() {
105 if let Ok(v) = value.to_str() {
106 header_lines.push_str(&format!("{name}: {v}\n"));
107 }
108 }
109
110 let mut body: Vec<u8> = Vec::new();
112 while let Some(chunk) = resp.chunk().await? {
113 body.extend_from_slice(&chunk);
114 if body.len() >= max_bytes {
115 body.truncate(max_bytes);
116 break;
117 }
118 }
119 let body_str = String::from_utf8_lossy(&body);
120
121 let out = format!(
122 "HTTP {} {}\n\n{}\n{}",
123 status.as_u16(),
124 status.canonical_reason().unwrap_or(""),
125 header_lines.trim_end(),
126 body_str
127 );
128 Ok(sanitize_credentials(&out))
130 }
131}