Skip to content

Commit e2a3cfc

Browse files
committed
refactor: split rexos tools defs dispatch and ops
1 parent fa8630e commit e2a3cfc

23 files changed

+4359
-3809
lines changed
Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
use anyhow::{bail, Context};
2+
use std::net::IpAddr;
3+
4+
use crate::is_forbidden_ip;
5+
6+
use rexos_llm::openai_compat::{ToolDefinition, ToolFunctionDefinition};
7+
#[derive(Debug, serde::Deserialize)]
8+
pub(crate) struct BrowserNavigateArgs {
9+
pub(crate) url: String,
10+
#[serde(default)]
11+
pub(crate) timeout_ms: Option<u64>,
12+
#[serde(default)]
13+
pub(crate) allow_private: bool,
14+
#[serde(default)]
15+
pub(crate) headless: Option<bool>,
16+
}
17+
18+
#[derive(Debug, serde::Deserialize)]
19+
pub(crate) struct BrowserRunJsArgs {
20+
pub(crate) expression: String,
21+
}
22+
23+
#[derive(Debug, serde::Deserialize)]
24+
pub(crate) struct BrowserClickArgs {
25+
pub(crate) selector: String,
26+
}
27+
28+
#[derive(Debug, serde::Deserialize)]
29+
pub(crate) struct BrowserTypeArgs {
30+
pub(crate) selector: String,
31+
pub(crate) text: String,
32+
}
33+
34+
#[derive(Debug, serde::Deserialize)]
35+
pub(crate) struct BrowserPressKeyArgs {
36+
pub(crate) key: String,
37+
#[serde(default)]
38+
pub(crate) selector: Option<String>,
39+
}
40+
41+
#[derive(Debug, serde::Deserialize)]
42+
pub(crate) struct BrowserScrollArgs {
43+
#[serde(default)]
44+
pub(crate) direction: Option<String>,
45+
#[serde(default)]
46+
pub(crate) amount: Option<i64>,
47+
}
48+
49+
#[derive(Debug, serde::Deserialize)]
50+
pub(crate) struct BrowserWaitArgs {
51+
pub(crate) selector: String,
52+
#[serde(default)]
53+
pub(crate) timeout_ms: Option<u64>,
54+
}
55+
56+
#[derive(Debug, serde::Deserialize)]
57+
pub(crate) struct BrowserWaitForArgs {
58+
#[serde(default)]
59+
pub(crate) selector: Option<String>,
60+
#[serde(default)]
61+
pub(crate) text: Option<String>,
62+
#[serde(default)]
63+
pub(crate) timeout_ms: Option<u64>,
64+
}
65+
66+
#[derive(Debug, serde::Deserialize)]
67+
pub(crate) struct BrowserScreenshotArgs {
68+
#[serde(default)]
69+
pub(crate) path: Option<String>,
70+
}
71+
72+
pub(crate) fn core_tool_defs() -> Vec<ToolDefinition> {
73+
vec![
74+
browser_navigate_def(),
75+
browser_back_def(),
76+
browser_scroll_def(),
77+
browser_click_def(),
78+
browser_type_def(),
79+
browser_press_key_def(),
80+
browser_wait_def(),
81+
browser_wait_for_def(),
82+
browser_read_page_def(),
83+
browser_run_js_def(),
84+
browser_screenshot_def(),
85+
browser_close_def(),
86+
]
87+
}
88+
89+
pub(crate) fn compat_tool_defs() -> Vec<ToolDefinition> {
90+
Vec::new()
91+
}
92+
93+
fn browser_navigate_def() -> ToolDefinition {
94+
ToolDefinition {
95+
kind: "function".to_string(),
96+
function: ToolFunctionDefinition {
97+
name: "browser_navigate".to_string(),
98+
description: "Navigate the browser to a URL (SSRF-protected by default).".to_string(),
99+
parameters: serde_json::json!({
100+
"type": "object",
101+
"properties": {
102+
"url": { "type": "string", "description": "HTTP(S) URL to open." },
103+
"timeout_ms": { "type": "integer", "description": "Timeout in milliseconds (default 30000).", "minimum": 1 },
104+
"allow_private": { "type": "boolean", "description": "Allow loopback/private IPs (default false)." },
105+
"headless": { "type": "boolean", "description": "Run the browser in headless mode (default true). Set false to show a GUI window." }
106+
},
107+
"required": ["url"],
108+
"additionalProperties": false
109+
}),
110+
},
111+
}
112+
}
113+
114+
fn browser_back_def() -> ToolDefinition {
115+
ToolDefinition {
116+
kind: "function".to_string(),
117+
function: ToolFunctionDefinition {
118+
name: "browser_back".to_string(),
119+
description: "Go back in browser history.".to_string(),
120+
parameters: serde_json::json!({
121+
"type": "object",
122+
"properties": {},
123+
"required": [],
124+
"additionalProperties": false
125+
}),
126+
},
127+
}
128+
}
129+
130+
fn browser_scroll_def() -> ToolDefinition {
131+
ToolDefinition {
132+
kind: "function".to_string(),
133+
function: ToolFunctionDefinition {
134+
name: "browser_scroll".to_string(),
135+
description: "Scroll the current page.".to_string(),
136+
parameters: serde_json::json!({
137+
"type": "object",
138+
"properties": {
139+
"direction": { "type": "string", "description": "Scroll direction: down/up/left/right (default down).", "enum": ["down", "up", "left", "right"] },
140+
"amount": { "type": "integer", "description": "Scroll amount in pixels (default 600).", "minimum": 0 }
141+
},
142+
"required": [],
143+
"additionalProperties": false
144+
}),
145+
},
146+
}
147+
}
148+
149+
fn browser_click_def() -> ToolDefinition {
150+
ToolDefinition {
151+
kind: "function".to_string(),
152+
function: ToolFunctionDefinition {
153+
name: "browser_click".to_string(),
154+
description:
155+
"Click an element in the browser by CSS selector (or best-effort text fallback)."
156+
.to_string(),
157+
parameters: serde_json::json!({
158+
"type": "object",
159+
"properties": {
160+
"selector": { "type": "string", "description": "CSS selector (or text fallback) to click." }
161+
},
162+
"required": ["selector"],
163+
"additionalProperties": false
164+
}),
165+
},
166+
}
167+
}
168+
169+
fn browser_type_def() -> ToolDefinition {
170+
ToolDefinition {
171+
kind: "function".to_string(),
172+
function: ToolFunctionDefinition {
173+
name: "browser_type".to_string(),
174+
description: "Type into an input element in the browser (fills the field).".to_string(),
175+
parameters: serde_json::json!({
176+
"type": "object",
177+
"properties": {
178+
"selector": { "type": "string", "description": "CSS selector for the input element." },
179+
"text": { "type": "string", "description": "Text to input." }
180+
},
181+
"required": ["selector", "text"],
182+
"additionalProperties": false
183+
}),
184+
},
185+
}
186+
}
187+
188+
fn browser_press_key_def() -> ToolDefinition {
189+
ToolDefinition {
190+
kind: "function".to_string(),
191+
function: ToolFunctionDefinition {
192+
name: "browser_press_key".to_string(),
193+
description: "Press a key in the browser (optionally on a target element).".to_string(),
194+
parameters: serde_json::json!({
195+
"type": "object",
196+
"properties": {
197+
"key": { "type": "string", "description": "Key to press (example: Enter, Escape, ArrowDown, Control+A)." },
198+
"selector": { "type": "string", "description": "Optional CSS selector to target before pressing the key." }
199+
},
200+
"required": ["key"],
201+
"additionalProperties": false
202+
}),
203+
},
204+
}
205+
}
206+
207+
fn browser_wait_def() -> ToolDefinition {
208+
ToolDefinition {
209+
kind: "function".to_string(),
210+
function: ToolFunctionDefinition {
211+
name: "browser_wait".to_string(),
212+
description: "Wait for a CSS selector to appear on the page.".to_string(),
213+
parameters: serde_json::json!({
214+
"type": "object",
215+
"properties": {
216+
"selector": { "type": "string", "description": "CSS selector to wait for." },
217+
"timeout_ms": { "type": "integer", "description": "Optional timeout in milliseconds.", "minimum": 1 }
218+
},
219+
"required": ["selector"],
220+
"additionalProperties": false
221+
}),
222+
},
223+
}
224+
}
225+
226+
fn browser_wait_for_def() -> ToolDefinition {
227+
ToolDefinition {
228+
kind: "function".to_string(),
229+
function: ToolFunctionDefinition {
230+
name: "browser_wait_for".to_string(),
231+
description: "Wait for a selector or text to appear on the page.".to_string(),
232+
parameters: serde_json::json!({
233+
"type": "object",
234+
"properties": {
235+
"selector": { "type": "string", "description": "Optional CSS selector to wait for." },
236+
"text": { "type": "string", "description": "Optional visible text to wait for." },
237+
"timeout_ms": { "type": "integer", "description": "Optional timeout in milliseconds.", "minimum": 1 }
238+
},
239+
"additionalProperties": false
240+
}),
241+
},
242+
}
243+
}
244+
245+
fn browser_read_page_def() -> ToolDefinition {
246+
ToolDefinition {
247+
kind: "function".to_string(),
248+
function: ToolFunctionDefinition {
249+
name: "browser_read_page".to_string(),
250+
description: "Read the current page content (title/url/text).".to_string(),
251+
parameters: serde_json::json!({
252+
"type": "object",
253+
"properties": {},
254+
"required": [],
255+
"additionalProperties": false
256+
}),
257+
},
258+
}
259+
}
260+
261+
fn browser_run_js_def() -> ToolDefinition {
262+
ToolDefinition {
263+
kind: "function".to_string(),
264+
function: ToolFunctionDefinition {
265+
name: "browser_run_js".to_string(),
266+
description: "Run a JavaScript expression on the current page and return the result."
267+
.to_string(),
268+
parameters: serde_json::json!({
269+
"type": "object",
270+
"properties": {
271+
"expression": { "type": "string", "description": "JavaScript expression to evaluate." }
272+
},
273+
"required": ["expression"],
274+
"additionalProperties": false
275+
}),
276+
},
277+
}
278+
}
279+
280+
fn browser_screenshot_def() -> ToolDefinition {
281+
ToolDefinition {
282+
kind: "function".to_string(),
283+
function: ToolFunctionDefinition {
284+
name: "browser_screenshot".to_string(),
285+
description: "Take a screenshot and write it to a workspace path.".to_string(),
286+
parameters: serde_json::json!({
287+
"type": "object",
288+
"properties": {
289+
"path": { "type": "string", "description": "Relative output path (default .loopforge/browser/screenshot.png)." }
290+
},
291+
"required": [],
292+
"additionalProperties": false
293+
}),
294+
},
295+
}
296+
}
297+
298+
fn browser_close_def() -> ToolDefinition {
299+
ToolDefinition {
300+
kind: "function".to_string(),
301+
function: ToolFunctionDefinition {
302+
name: "browser_close".to_string(),
303+
description: "Close the browser session (idempotent).".to_string(),
304+
parameters: serde_json::json!({
305+
"type": "object",
306+
"properties": {},
307+
"required": [],
308+
"additionalProperties": false
309+
}),
310+
},
311+
}
312+
}
313+
314+
pub(crate) async fn resolve_host_ips(host: &str, port: u16) -> anyhow::Result<Vec<IpAddr>> {
315+
if let Ok(ip) = host.parse::<IpAddr>() {
316+
return Ok(vec![ip]);
317+
}
318+
319+
let addrs = tokio::net::lookup_host((host, port))
320+
.await
321+
.context("dns lookup")?;
322+
323+
let mut ips = Vec::new();
324+
for sa in addrs {
325+
ips.push(sa.ip());
326+
}
327+
328+
if ips.is_empty() {
329+
bail!("no addresses found");
330+
}
331+
332+
ips.sort();
333+
ips.dedup();
334+
Ok(ips)
335+
}
336+
337+
pub(crate) async fn ensure_browser_url_allowed(
338+
url: &str,
339+
allow_private: bool,
340+
) -> anyhow::Result<()> {
341+
let url = reqwest::Url::parse(url).context("parse url")?;
342+
343+
match url.scheme() {
344+
"http" | "https" => {}
345+
// Safe, non-network internal pages we still want to allow for screenshots/debugging.
346+
"about" if url.as_str() == "about:blank" => return Ok(()),
347+
"chrome-error" if matches!(url.host_str(), Some("chromewebdata")) => return Ok(()),
348+
_ => bail!("only http/https urls are allowed"),
349+
}
350+
351+
if allow_private {
352+
return Ok(());
353+
}
354+
355+
let host = url.host_str().context("url missing host")?;
356+
let port = url.port_or_known_default().context("url missing port")?;
357+
358+
let ips = resolve_host_ips(host, port)
359+
.await
360+
.with_context(|| format!("resolve {host}:{port}"))?;
361+
for ip in ips {
362+
if is_forbidden_ip(ip) {
363+
bail!("url resolves to loopback/private address: {ip}");
364+
}
365+
}
366+
Ok(())
367+
}

0 commit comments

Comments
 (0)