1use async_trait::async_trait;
7use rustant_core::browser::{BrowserSecurityGuard, CdpClient, SnapshotMode};
8use rustant_core::error::ToolError;
9use rustant_core::types::{RiskLevel, ToolOutput};
10use std::sync::Arc;
11use std::time::Duration;
12
13use crate::registry::Tool;
14
15#[derive(Clone)]
17pub struct BrowserToolContext {
18 pub client: Arc<dyn CdpClient>,
19 pub security: Arc<BrowserSecurityGuard>,
20}
21
22impl BrowserToolContext {
23 pub fn new(client: Arc<dyn CdpClient>, security: Arc<BrowserSecurityGuard>) -> Self {
24 Self { client, security }
25 }
26}
27
28fn browser_err(name: &str, e: impl std::fmt::Display) -> ToolError {
32 ToolError::ExecutionFailed {
33 name: name.to_string(),
34 message: e.to_string(),
35 }
36}
37
38fn missing_arg(tool: &str, param: &str) -> ToolError {
39 ToolError::InvalidArguments {
40 name: tool.to_string(),
41 reason: format!("missing required '{}' parameter", param),
42 }
43}
44
45pub struct BrowserNavigateTool {
49 ctx: BrowserToolContext,
50}
51
52impl BrowserNavigateTool {
53 pub fn new(ctx: BrowserToolContext) -> Self {
54 Self { ctx }
55 }
56}
57
58#[async_trait]
59impl Tool for BrowserNavigateTool {
60 fn name(&self) -> &str {
61 "browser_navigate"
62 }
63 fn description(&self) -> &str {
64 "Navigate the browser to a URL."
65 }
66 fn parameters_schema(&self) -> serde_json::Value {
67 serde_json::json!({
68 "type": "object",
69 "properties": {
70 "url": { "type": "string", "description": "The URL to navigate to" }
71 },
72 "required": ["url"]
73 })
74 }
75 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
76 let url = args["url"]
77 .as_str()
78 .ok_or_else(|| missing_arg("browser_navigate", "url"))?;
79 self.ctx
80 .security
81 .check_url(url)
82 .map_err(|e| browser_err("browser_navigate", e))?;
83 self.ctx
84 .client
85 .navigate(url)
86 .await
87 .map_err(|e| browser_err("browser_navigate", e))?;
88 Ok(ToolOutput::text(format!("Navigated to {}", url)))
89 }
90 fn risk_level(&self) -> RiskLevel {
91 RiskLevel::Write
92 }
93 fn timeout(&self) -> Duration {
94 Duration::from_secs(30)
95 }
96}
97
98pub struct BrowserBackTool {
102 ctx: BrowserToolContext,
103}
104impl BrowserBackTool {
105 pub fn new(ctx: BrowserToolContext) -> Self {
106 Self { ctx }
107 }
108}
109#[async_trait]
110impl Tool for BrowserBackTool {
111 fn name(&self) -> &str {
112 "browser_back"
113 }
114 fn description(&self) -> &str {
115 "Go back in browser history."
116 }
117 fn parameters_schema(&self) -> serde_json::Value {
118 serde_json::json!({"type": "object", "properties": {}})
119 }
120 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
121 self.ctx
122 .client
123 .go_back()
124 .await
125 .map_err(|e| browser_err("browser_back", e))?;
126 Ok(ToolOutput::text("Navigated back"))
127 }
128 fn risk_level(&self) -> RiskLevel {
129 RiskLevel::Write
130 }
131}
132
133pub struct BrowserForwardTool {
137 ctx: BrowserToolContext,
138}
139impl BrowserForwardTool {
140 pub fn new(ctx: BrowserToolContext) -> Self {
141 Self { ctx }
142 }
143}
144#[async_trait]
145impl Tool for BrowserForwardTool {
146 fn name(&self) -> &str {
147 "browser_forward"
148 }
149 fn description(&self) -> &str {
150 "Go forward in browser history."
151 }
152 fn parameters_schema(&self) -> serde_json::Value {
153 serde_json::json!({"type": "object", "properties": {}})
154 }
155 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
156 self.ctx
157 .client
158 .go_forward()
159 .await
160 .map_err(|e| browser_err("browser_forward", e))?;
161 Ok(ToolOutput::text("Navigated forward"))
162 }
163 fn risk_level(&self) -> RiskLevel {
164 RiskLevel::Write
165 }
166}
167
168pub struct BrowserRefreshTool {
172 ctx: BrowserToolContext,
173}
174impl BrowserRefreshTool {
175 pub fn new(ctx: BrowserToolContext) -> Self {
176 Self { ctx }
177 }
178}
179#[async_trait]
180impl Tool for BrowserRefreshTool {
181 fn name(&self) -> &str {
182 "browser_refresh"
183 }
184 fn description(&self) -> &str {
185 "Refresh the current page."
186 }
187 fn parameters_schema(&self) -> serde_json::Value {
188 serde_json::json!({"type": "object", "properties": {}})
189 }
190 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
191 self.ctx
192 .client
193 .refresh()
194 .await
195 .map_err(|e| browser_err("browser_refresh", e))?;
196 Ok(ToolOutput::text("Page refreshed"))
197 }
198 fn risk_level(&self) -> RiskLevel {
199 RiskLevel::Write
200 }
201}
202
203pub struct BrowserClickTool {
207 ctx: BrowserToolContext,
208}
209impl BrowserClickTool {
210 pub fn new(ctx: BrowserToolContext) -> Self {
211 Self { ctx }
212 }
213}
214#[async_trait]
215impl Tool for BrowserClickTool {
216 fn name(&self) -> &str {
217 "browser_click"
218 }
219 fn description(&self) -> &str {
220 "Click an element matching a CSS selector."
221 }
222 fn parameters_schema(&self) -> serde_json::Value {
223 serde_json::json!({
224 "type": "object",
225 "properties": {
226 "selector": { "type": "string", "description": "CSS selector of the element to click" }
227 },
228 "required": ["selector"]
229 })
230 }
231 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
232 let selector = args["selector"]
233 .as_str()
234 .ok_or_else(|| missing_arg("browser_click", "selector"))?;
235 self.ctx
236 .client
237 .click(selector)
238 .await
239 .map_err(|e| browser_err("browser_click", e))?;
240 Ok(ToolOutput::text(format!("Clicked '{}'", selector)))
241 }
242 fn risk_level(&self) -> RiskLevel {
243 RiskLevel::Write
244 }
245}
246
247pub struct BrowserTypeTool {
251 ctx: BrowserToolContext,
252}
253impl BrowserTypeTool {
254 pub fn new(ctx: BrowserToolContext) -> Self {
255 Self { ctx }
256 }
257}
258#[async_trait]
259impl Tool for BrowserTypeTool {
260 fn name(&self) -> &str {
261 "browser_type"
262 }
263 fn description(&self) -> &str {
264 "Type text into an element matching a CSS selector."
265 }
266 fn parameters_schema(&self) -> serde_json::Value {
267 serde_json::json!({
268 "type": "object",
269 "properties": {
270 "selector": { "type": "string", "description": "CSS selector" },
271 "text": { "type": "string", "description": "Text to type" }
272 },
273 "required": ["selector", "text"]
274 })
275 }
276 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
277 let selector = args["selector"]
278 .as_str()
279 .ok_or_else(|| missing_arg("browser_type", "selector"))?;
280 let text = args["text"]
281 .as_str()
282 .ok_or_else(|| missing_arg("browser_type", "text"))?;
283 self.ctx
284 .client
285 .type_text(selector, text)
286 .await
287 .map_err(|e| browser_err("browser_type", e))?;
288 Ok(ToolOutput::text(format!(
289 "Typed '{}' into '{}'",
290 text, selector
291 )))
292 }
293 fn risk_level(&self) -> RiskLevel {
294 RiskLevel::Write
295 }
296}
297
298pub struct BrowserFillTool {
302 ctx: BrowserToolContext,
303}
304impl BrowserFillTool {
305 pub fn new(ctx: BrowserToolContext) -> Self {
306 Self { ctx }
307 }
308}
309#[async_trait]
310impl Tool for BrowserFillTool {
311 fn name(&self) -> &str {
312 "browser_fill"
313 }
314 fn description(&self) -> &str {
315 "Clear a form field and fill it with a new value."
316 }
317 fn parameters_schema(&self) -> serde_json::Value {
318 serde_json::json!({
319 "type": "object",
320 "properties": {
321 "selector": { "type": "string", "description": "CSS selector of the input" },
322 "value": { "type": "string", "description": "Value to fill in" }
323 },
324 "required": ["selector", "value"]
325 })
326 }
327 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
328 let selector = args["selector"]
329 .as_str()
330 .ok_or_else(|| missing_arg("browser_fill", "selector"))?;
331 let value = args["value"]
332 .as_str()
333 .ok_or_else(|| missing_arg("browser_fill", "value"))?;
334 self.ctx
335 .client
336 .fill(selector, value)
337 .await
338 .map_err(|e| browser_err("browser_fill", e))?;
339 Ok(ToolOutput::text(format!(
340 "Filled '{}' with '{}'",
341 selector, value
342 )))
343 }
344 fn risk_level(&self) -> RiskLevel {
345 RiskLevel::Write
346 }
347}
348
349pub struct BrowserSelectTool {
353 ctx: BrowserToolContext,
354}
355impl BrowserSelectTool {
356 pub fn new(ctx: BrowserToolContext) -> Self {
357 Self { ctx }
358 }
359}
360#[async_trait]
361impl Tool for BrowserSelectTool {
362 fn name(&self) -> &str {
363 "browser_select"
364 }
365 fn description(&self) -> &str {
366 "Select an option in a <select> element."
367 }
368 fn parameters_schema(&self) -> serde_json::Value {
369 serde_json::json!({
370 "type": "object",
371 "properties": {
372 "selector": { "type": "string", "description": "CSS selector of the select element" },
373 "value": { "type": "string", "description": "Option value to select" }
374 },
375 "required": ["selector", "value"]
376 })
377 }
378 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
379 let selector = args["selector"]
380 .as_str()
381 .ok_or_else(|| missing_arg("browser_select", "selector"))?;
382 let value = args["value"]
383 .as_str()
384 .ok_or_else(|| missing_arg("browser_select", "value"))?;
385 self.ctx
386 .client
387 .select_option(selector, value)
388 .await
389 .map_err(|e| browser_err("browser_select", e))?;
390 Ok(ToolOutput::text(format!(
391 "Selected '{}' in '{}'",
392 value, selector
393 )))
394 }
395 fn risk_level(&self) -> RiskLevel {
396 RiskLevel::Write
397 }
398}
399
400pub struct BrowserScrollTool {
404 ctx: BrowserToolContext,
405}
406impl BrowserScrollTool {
407 pub fn new(ctx: BrowserToolContext) -> Self {
408 Self { ctx }
409 }
410}
411#[async_trait]
412impl Tool for BrowserScrollTool {
413 fn name(&self) -> &str {
414 "browser_scroll"
415 }
416 fn description(&self) -> &str {
417 "Scroll the page by the given x and y pixel offsets."
418 }
419 fn parameters_schema(&self) -> serde_json::Value {
420 serde_json::json!({
421 "type": "object",
422 "properties": {
423 "x": { "type": "integer", "description": "Horizontal scroll offset", "default": 0 },
424 "y": { "type": "integer", "description": "Vertical scroll offset", "default": 0 }
425 }
426 })
427 }
428 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
429 let x = args["x"].as_i64().unwrap_or(0) as i32;
430 let y = args["y"].as_i64().unwrap_or(0) as i32;
431 self.ctx
432 .client
433 .scroll(x, y)
434 .await
435 .map_err(|e| browser_err("browser_scroll", e))?;
436 Ok(ToolOutput::text(format!("Scrolled by ({}, {})", x, y)))
437 }
438 fn risk_level(&self) -> RiskLevel {
439 RiskLevel::Write
440 }
441}
442
443pub struct BrowserHoverTool {
447 ctx: BrowserToolContext,
448}
449impl BrowserHoverTool {
450 pub fn new(ctx: BrowserToolContext) -> Self {
451 Self { ctx }
452 }
453}
454#[async_trait]
455impl Tool for BrowserHoverTool {
456 fn name(&self) -> &str {
457 "browser_hover"
458 }
459 fn description(&self) -> &str {
460 "Hover over an element matching a CSS selector."
461 }
462 fn parameters_schema(&self) -> serde_json::Value {
463 serde_json::json!({
464 "type": "object",
465 "properties": {
466 "selector": { "type": "string", "description": "CSS selector" }
467 },
468 "required": ["selector"]
469 })
470 }
471 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
472 let selector = args["selector"]
473 .as_str()
474 .ok_or_else(|| missing_arg("browser_hover", "selector"))?;
475 self.ctx
476 .client
477 .hover(selector)
478 .await
479 .map_err(|e| browser_err("browser_hover", e))?;
480 Ok(ToolOutput::text(format!("Hovered over '{}'", selector)))
481 }
482 fn risk_level(&self) -> RiskLevel {
483 RiskLevel::Write
484 }
485}
486
487pub struct BrowserPressKeyTool {
491 ctx: BrowserToolContext,
492}
493impl BrowserPressKeyTool {
494 pub fn new(ctx: BrowserToolContext) -> Self {
495 Self { ctx }
496 }
497}
498#[async_trait]
499impl Tool for BrowserPressKeyTool {
500 fn name(&self) -> &str {
501 "browser_press_key"
502 }
503 fn description(&self) -> &str {
504 "Press a keyboard key (e.g., 'Enter', 'Tab', 'Escape')."
505 }
506 fn parameters_schema(&self) -> serde_json::Value {
507 serde_json::json!({
508 "type": "object",
509 "properties": {
510 "key": { "type": "string", "description": "Key to press" }
511 },
512 "required": ["key"]
513 })
514 }
515 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
516 let key = args["key"]
517 .as_str()
518 .ok_or_else(|| missing_arg("browser_press_key", "key"))?;
519 self.ctx
520 .client
521 .press_key(key)
522 .await
523 .map_err(|e| browser_err("browser_press_key", e))?;
524 Ok(ToolOutput::text(format!("Pressed key '{}'", key)))
525 }
526 fn risk_level(&self) -> RiskLevel {
527 RiskLevel::Write
528 }
529}
530
531pub struct BrowserSnapshotTool {
535 ctx: BrowserToolContext,
536}
537impl BrowserSnapshotTool {
538 pub fn new(ctx: BrowserToolContext) -> Self {
539 Self { ctx }
540 }
541}
542#[async_trait]
543impl Tool for BrowserSnapshotTool {
544 fn name(&self) -> &str {
545 "browser_snapshot"
546 }
547 fn description(&self) -> &str {
548 "Take a snapshot of the page content in the specified mode (html, text, aria_tree)."
549 }
550 fn parameters_schema(&self) -> serde_json::Value {
551 serde_json::json!({
552 "type": "object",
553 "properties": {
554 "mode": {
555 "type": "string",
556 "description": "Snapshot mode: html, text, or aria_tree",
557 "default": "text",
558 "enum": ["html", "text", "aria_tree"]
559 }
560 }
561 })
562 }
563 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
564 let mode_str = args["mode"].as_str().unwrap_or("text");
565 let mode = match mode_str {
566 "html" => SnapshotMode::Html,
567 "aria_tree" => SnapshotMode::AriaTree,
568 _ => SnapshotMode::Text,
569 };
570 let content = match mode {
571 SnapshotMode::Html => self
572 .ctx
573 .client
574 .get_html()
575 .await
576 .map_err(|e| browser_err("browser_snapshot", e))?,
577 SnapshotMode::Text => self
578 .ctx
579 .client
580 .get_text()
581 .await
582 .map_err(|e| browser_err("browser_snapshot", e))?,
583 SnapshotMode::AriaTree => self
584 .ctx
585 .client
586 .get_aria_tree()
587 .await
588 .map_err(|e| browser_err("browser_snapshot", e))?,
589 SnapshotMode::Screenshot => {
590 return Err(ToolError::ExecutionFailed {
591 name: "browser_snapshot".into(),
592 message: "Screenshot mode is handled separately by the screenshot tool".into(),
593 });
594 }
595 };
596 let masked = BrowserSecurityGuard::mask_credentials(&content);
597 Ok(ToolOutput::text(masked))
598 }
599 fn risk_level(&self) -> RiskLevel {
600 RiskLevel::ReadOnly
601 }
602}
603
604pub struct BrowserUrlTool {
608 ctx: BrowserToolContext,
609}
610impl BrowserUrlTool {
611 pub fn new(ctx: BrowserToolContext) -> Self {
612 Self { ctx }
613 }
614}
615#[async_trait]
616impl Tool for BrowserUrlTool {
617 fn name(&self) -> &str {
618 "browser_url"
619 }
620 fn description(&self) -> &str {
621 "Get the current page URL."
622 }
623 fn parameters_schema(&self) -> serde_json::Value {
624 serde_json::json!({"type": "object", "properties": {}})
625 }
626 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
627 let url = self
628 .ctx
629 .client
630 .get_url()
631 .await
632 .map_err(|e| browser_err("browser_url", e))?;
633 Ok(ToolOutput::text(url))
634 }
635 fn risk_level(&self) -> RiskLevel {
636 RiskLevel::ReadOnly
637 }
638}
639
640pub struct BrowserTitleTool {
644 ctx: BrowserToolContext,
645}
646impl BrowserTitleTool {
647 pub fn new(ctx: BrowserToolContext) -> Self {
648 Self { ctx }
649 }
650}
651#[async_trait]
652impl Tool for BrowserTitleTool {
653 fn name(&self) -> &str {
654 "browser_title"
655 }
656 fn description(&self) -> &str {
657 "Get the current page title."
658 }
659 fn parameters_schema(&self) -> serde_json::Value {
660 serde_json::json!({"type": "object", "properties": {}})
661 }
662 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
663 let title = self
664 .ctx
665 .client
666 .get_title()
667 .await
668 .map_err(|e| browser_err("browser_title", e))?;
669 Ok(ToolOutput::text(title))
670 }
671 fn risk_level(&self) -> RiskLevel {
672 RiskLevel::ReadOnly
673 }
674}
675
676pub struct BrowserScreenshotTool {
680 ctx: BrowserToolContext,
681}
682impl BrowserScreenshotTool {
683 pub fn new(ctx: BrowserToolContext) -> Self {
684 Self { ctx }
685 }
686}
687#[async_trait]
688impl Tool for BrowserScreenshotTool {
689 fn name(&self) -> &str {
690 "browser_screenshot"
691 }
692 fn description(&self) -> &str {
693 "Take a screenshot of the current page and return it as base64-encoded PNG."
694 }
695 fn parameters_schema(&self) -> serde_json::Value {
696 serde_json::json!({"type": "object", "properties": {}})
697 }
698 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
699 let bytes = self
700 .ctx
701 .client
702 .screenshot()
703 .await
704 .map_err(|e| browser_err("browser_screenshot", e))?;
705 let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
706 Ok(ToolOutput::text(b64))
707 }
708 fn risk_level(&self) -> RiskLevel {
709 RiskLevel::ReadOnly
710 }
711}
712
713pub struct BrowserJsEvalTool {
717 ctx: BrowserToolContext,
718}
719impl BrowserJsEvalTool {
720 pub fn new(ctx: BrowserToolContext) -> Self {
721 Self { ctx }
722 }
723}
724#[async_trait]
725impl Tool for BrowserJsEvalTool {
726 fn name(&self) -> &str {
727 "browser_js_eval"
728 }
729 fn description(&self) -> &str {
730 "Evaluate a JavaScript expression in the page context and return the result."
731 }
732 fn parameters_schema(&self) -> serde_json::Value {
733 serde_json::json!({
734 "type": "object",
735 "properties": {
736 "script": { "type": "string", "description": "JavaScript code to evaluate" }
737 },
738 "required": ["script"]
739 })
740 }
741 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
742 let script = args["script"]
743 .as_str()
744 .ok_or_else(|| missing_arg("browser_js_eval", "script"))?;
745 let result = self
746 .ctx
747 .client
748 .evaluate_js(script)
749 .await
750 .map_err(|e| browser_err("browser_js_eval", e))?;
751 Ok(ToolOutput::text(
752 serde_json::to_string_pretty(&result).unwrap_or_default(),
753 ))
754 }
755 fn risk_level(&self) -> RiskLevel {
756 RiskLevel::Execute
757 }
758 fn timeout(&self) -> Duration {
759 Duration::from_secs(30)
760 }
761}
762
763pub struct BrowserWaitTool {
767 ctx: BrowserToolContext,
768}
769impl BrowserWaitTool {
770 pub fn new(ctx: BrowserToolContext) -> Self {
771 Self { ctx }
772 }
773}
774#[async_trait]
775impl Tool for BrowserWaitTool {
776 fn name(&self) -> &str {
777 "browser_wait"
778 }
779 fn description(&self) -> &str {
780 "Wait for an element matching a CSS selector to appear on the page."
781 }
782 fn parameters_schema(&self) -> serde_json::Value {
783 serde_json::json!({
784 "type": "object",
785 "properties": {
786 "selector": { "type": "string", "description": "CSS selector to wait for" },
787 "timeout_ms": { "type": "integer", "description": "Timeout in milliseconds", "default": 5000 }
788 },
789 "required": ["selector"]
790 })
791 }
792 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
793 let selector = args["selector"]
794 .as_str()
795 .ok_or_else(|| missing_arg("browser_wait", "selector"))?;
796 let timeout_ms = args["timeout_ms"].as_u64().unwrap_or(5000);
797 self.ctx
798 .client
799 .wait_for_selector(selector, timeout_ms)
800 .await
801 .map_err(|e| browser_err("browser_wait", e))?;
802 Ok(ToolOutput::text(format!("Element '{}' found", selector)))
803 }
804 fn risk_level(&self) -> RiskLevel {
805 RiskLevel::ReadOnly
806 }
807 fn timeout(&self) -> Duration {
808 Duration::from_secs(60)
809 }
810}
811
812pub struct BrowserFileUploadTool {
816 ctx: BrowserToolContext,
817}
818impl BrowserFileUploadTool {
819 pub fn new(ctx: BrowserToolContext) -> Self {
820 Self { ctx }
821 }
822}
823#[async_trait]
824impl Tool for BrowserFileUploadTool {
825 fn name(&self) -> &str {
826 "browser_file_upload"
827 }
828 fn description(&self) -> &str {
829 "Upload a file to a file input element."
830 }
831 fn parameters_schema(&self) -> serde_json::Value {
832 serde_json::json!({
833 "type": "object",
834 "properties": {
835 "selector": { "type": "string", "description": "CSS selector of the file input" },
836 "path": { "type": "string", "description": "Local file path to upload" }
837 },
838 "required": ["selector", "path"]
839 })
840 }
841 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
842 let selector = args["selector"]
843 .as_str()
844 .ok_or_else(|| missing_arg("browser_file_upload", "selector"))?;
845 let path = args["path"]
846 .as_str()
847 .ok_or_else(|| missing_arg("browser_file_upload", "path"))?;
848 self.ctx
849 .client
850 .upload_file(selector, path)
851 .await
852 .map_err(|e| browser_err("browser_file_upload", e))?;
853 Ok(ToolOutput::text(format!(
854 "Uploaded '{}' to '{}'",
855 path, selector
856 )))
857 }
858 fn risk_level(&self) -> RiskLevel {
859 RiskLevel::Network
860 }
861}
862
863pub struct BrowserDownloadTool {
867 ctx: BrowserToolContext,
868}
869impl BrowserDownloadTool {
870 pub fn new(ctx: BrowserToolContext) -> Self {
871 Self { ctx }
872 }
873}
874#[async_trait]
875impl Tool for BrowserDownloadTool {
876 fn name(&self) -> &str {
877 "browser_download"
878 }
879 fn description(&self) -> &str {
880 "Trigger a download by clicking a link element."
881 }
882 fn parameters_schema(&self) -> serde_json::Value {
883 serde_json::json!({
884 "type": "object",
885 "properties": {
886 "selector": { "type": "string", "description": "CSS selector of the download link/button" }
887 },
888 "required": ["selector"]
889 })
890 }
891 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
892 let selector = args["selector"]
893 .as_str()
894 .ok_or_else(|| missing_arg("browser_download", "selector"))?;
895 self.ctx
896 .client
897 .click(selector)
898 .await
899 .map_err(|e| browser_err("browser_download", e))?;
900 Ok(ToolOutput::text(format!(
901 "Download triggered via '{}'",
902 selector
903 )))
904 }
905 fn risk_level(&self) -> RiskLevel {
906 RiskLevel::Network
907 }
908}
909
910pub struct BrowserCloseTool {
914 ctx: BrowserToolContext,
915}
916impl BrowserCloseTool {
917 pub fn new(ctx: BrowserToolContext) -> Self {
918 Self { ctx }
919 }
920}
921#[async_trait]
922impl Tool for BrowserCloseTool {
923 fn name(&self) -> &str {
924 "browser_close"
925 }
926 fn description(&self) -> &str {
927 "Close the current browser page/tab."
928 }
929 fn parameters_schema(&self) -> serde_json::Value {
930 serde_json::json!({"type": "object", "properties": {}})
931 }
932 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
933 self.ctx
934 .client
935 .close()
936 .await
937 .map_err(|e| browser_err("browser_close", e))?;
938 Ok(ToolOutput::text("Browser page closed"))
939 }
940 fn risk_level(&self) -> RiskLevel {
941 RiskLevel::Write
942 }
943}
944
945pub struct BrowserNewTabTool {
949 ctx: BrowserToolContext,
950}
951impl BrowserNewTabTool {
952 pub fn new(ctx: BrowserToolContext) -> Self {
953 Self { ctx }
954 }
955}
956#[async_trait]
957impl Tool for BrowserNewTabTool {
958 fn name(&self) -> &str {
959 "browser_new_tab"
960 }
961 fn description(&self) -> &str {
962 "Open a new browser tab and navigate to a URL. Returns the tab ID."
963 }
964 fn parameters_schema(&self) -> serde_json::Value {
965 serde_json::json!({
966 "type": "object",
967 "properties": {
968 "url": { "type": "string", "description": "URL to open in the new tab (default: about:blank)" }
969 }
970 })
971 }
972 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
973 let url = args["url"].as_str().unwrap_or("about:blank");
974 if url != "about:blank" {
975 self.ctx
976 .security
977 .check_url(url)
978 .map_err(|e| browser_err("browser_new_tab", e))?;
979 }
980 let tab_id = self
981 .ctx
982 .client
983 .new_tab(url)
984 .await
985 .map_err(|e| browser_err("browser_new_tab", e))?;
986 Ok(ToolOutput::text(format!(
987 "Opened new tab (id: {}) at {}",
988 tab_id, url
989 )))
990 }
991 fn risk_level(&self) -> RiskLevel {
992 RiskLevel::Write
993 }
994}
995
996pub struct BrowserListTabsTool {
1000 ctx: BrowserToolContext,
1001}
1002impl BrowserListTabsTool {
1003 pub fn new(ctx: BrowserToolContext) -> Self {
1004 Self { ctx }
1005 }
1006}
1007#[async_trait]
1008impl Tool for BrowserListTabsTool {
1009 fn name(&self) -> &str {
1010 "browser_list_tabs"
1011 }
1012 fn description(&self) -> &str {
1013 "List all open browser tabs with their IDs, URLs, titles, and which is active."
1014 }
1015 fn parameters_schema(&self) -> serde_json::Value {
1016 serde_json::json!({"type": "object", "properties": {}})
1017 }
1018 async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
1019 let tabs = self
1020 .ctx
1021 .client
1022 .list_tabs()
1023 .await
1024 .map_err(|e| browser_err("browser_list_tabs", e))?;
1025 let mut output = format!("{} tab(s) open:\n", tabs.len());
1026 for tab in &tabs {
1027 let marker = if tab.active { " [ACTIVE]" } else { "" };
1028 output.push_str(&format!(
1029 " - {} | {} | {}{}\n",
1030 tab.id, tab.url, tab.title, marker
1031 ));
1032 }
1033 Ok(ToolOutput::text(output))
1034 }
1035 fn risk_level(&self) -> RiskLevel {
1036 RiskLevel::ReadOnly
1037 }
1038}
1039
1040pub struct BrowserSwitchTabTool {
1044 ctx: BrowserToolContext,
1045}
1046impl BrowserSwitchTabTool {
1047 pub fn new(ctx: BrowserToolContext) -> Self {
1048 Self { ctx }
1049 }
1050}
1051#[async_trait]
1052impl Tool for BrowserSwitchTabTool {
1053 fn name(&self) -> &str {
1054 "browser_switch_tab"
1055 }
1056 fn description(&self) -> &str {
1057 "Switch the active browser tab by tab ID. Use browser_list_tabs to see available IDs."
1058 }
1059 fn parameters_schema(&self) -> serde_json::Value {
1060 serde_json::json!({
1061 "type": "object",
1062 "properties": {
1063 "tab_id": { "type": "string", "description": "ID of the tab to switch to" }
1064 },
1065 "required": ["tab_id"]
1066 })
1067 }
1068 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
1069 let tab_id = args["tab_id"]
1070 .as_str()
1071 .ok_or_else(|| missing_arg("browser_switch_tab", "tab_id"))?;
1072 self.ctx
1073 .client
1074 .switch_tab(tab_id)
1075 .await
1076 .map_err(|e| browser_err("browser_switch_tab", e))?;
1077 Ok(ToolOutput::text(format!("Switched to tab {}", tab_id)))
1078 }
1079 fn risk_level(&self) -> RiskLevel {
1080 RiskLevel::ReadOnly
1081 }
1082}
1083
1084pub struct BrowserCloseTabTool {
1088 ctx: BrowserToolContext,
1089}
1090impl BrowserCloseTabTool {
1091 pub fn new(ctx: BrowserToolContext) -> Self {
1092 Self { ctx }
1093 }
1094}
1095#[async_trait]
1096impl Tool for BrowserCloseTabTool {
1097 fn name(&self) -> &str {
1098 "browser_close_tab"
1099 }
1100 fn description(&self) -> &str {
1101 "Close a specific browser tab by its ID."
1102 }
1103 fn parameters_schema(&self) -> serde_json::Value {
1104 serde_json::json!({
1105 "type": "object",
1106 "properties": {
1107 "tab_id": { "type": "string", "description": "ID of the tab to close" }
1108 },
1109 "required": ["tab_id"]
1110 })
1111 }
1112 async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
1113 let tab_id = args["tab_id"]
1114 .as_str()
1115 .ok_or_else(|| missing_arg("browser_close_tab", "tab_id"))?;
1116 self.ctx
1117 .client
1118 .close_tab(tab_id)
1119 .await
1120 .map_err(|e| browser_err("browser_close_tab", e))?;
1121 Ok(ToolOutput::text(format!("Closed tab {}", tab_id)))
1122 }
1123 fn risk_level(&self) -> RiskLevel {
1124 RiskLevel::Write
1125 }
1126}
1127
1128pub fn create_browser_tools(ctx: BrowserToolContext) -> Vec<Arc<dyn Tool>> {
1134 vec![
1135 Arc::new(BrowserNavigateTool::new(ctx.clone())),
1136 Arc::new(BrowserBackTool::new(ctx.clone())),
1137 Arc::new(BrowserForwardTool::new(ctx.clone())),
1138 Arc::new(BrowserRefreshTool::new(ctx.clone())),
1139 Arc::new(BrowserClickTool::new(ctx.clone())),
1140 Arc::new(BrowserTypeTool::new(ctx.clone())),
1141 Arc::new(BrowserFillTool::new(ctx.clone())),
1142 Arc::new(BrowserSelectTool::new(ctx.clone())),
1143 Arc::new(BrowserScrollTool::new(ctx.clone())),
1144 Arc::new(BrowserHoverTool::new(ctx.clone())),
1145 Arc::new(BrowserPressKeyTool::new(ctx.clone())),
1146 Arc::new(BrowserSnapshotTool::new(ctx.clone())),
1147 Arc::new(BrowserUrlTool::new(ctx.clone())),
1148 Arc::new(BrowserTitleTool::new(ctx.clone())),
1149 Arc::new(BrowserScreenshotTool::new(ctx.clone())),
1150 Arc::new(BrowserJsEvalTool::new(ctx.clone())),
1151 Arc::new(BrowserWaitTool::new(ctx.clone())),
1152 Arc::new(BrowserFileUploadTool::new(ctx.clone())),
1153 Arc::new(BrowserDownloadTool::new(ctx.clone())),
1154 Arc::new(BrowserCloseTool::new(ctx.clone())),
1155 Arc::new(BrowserNewTabTool::new(ctx.clone())),
1157 Arc::new(BrowserListTabsTool::new(ctx.clone())),
1158 Arc::new(BrowserSwitchTabTool::new(ctx.clone())),
1159 Arc::new(BrowserCloseTabTool::new(ctx)),
1160 ]
1161}
1162
1163pub fn register_browser_tools(
1165 registry: &mut crate::registry::ToolRegistry,
1166 ctx: BrowserToolContext,
1167) {
1168 let tools = create_browser_tools(ctx);
1169 for tool in tools {
1170 if let Err(e) = registry.register(tool) {
1171 tracing::warn!("Failed to register browser tool: {}", e);
1172 }
1173 }
1174}
1175
1176#[cfg(test)]
1180mod tests {
1181 use super::*;
1182 use crate::registry::ToolRegistry;
1183 use rustant_core::browser::MockCdpClient;
1184 use rustant_core::error::BrowserError;
1185
1186 fn make_ctx() -> (BrowserToolContext, Arc<MockCdpClient>) {
1187 let client = Arc::new(MockCdpClient::new());
1188 let security = Arc::new(BrowserSecurityGuard::default());
1189 let ctx = BrowserToolContext::new(client.clone() as Arc<dyn CdpClient>, security);
1190 (ctx, client)
1191 }
1192
1193 fn make_ctx_with_security(
1194 security: BrowserSecurityGuard,
1195 ) -> (BrowserToolContext, Arc<MockCdpClient>) {
1196 let client = Arc::new(MockCdpClient::new());
1197 let security = Arc::new(security);
1198 let ctx = BrowserToolContext::new(client.clone() as Arc<dyn CdpClient>, security);
1199 (ctx, client)
1200 }
1201
1202 #[tokio::test]
1203 async fn test_navigate_tool_calls_cdp_navigate() {
1204 let (ctx, client) = make_ctx();
1205 let tool = BrowserNavigateTool::new(ctx);
1206 let result = tool
1207 .execute(serde_json::json!({"url": "https://example.com"}))
1208 .await
1209 .unwrap();
1210 assert!(result.content.contains("Navigated to"));
1211 assert_eq!(*client.current_url.lock().unwrap(), "https://example.com");
1212 }
1213
1214 #[tokio::test]
1215 async fn test_navigate_tool_blocked_url() {
1216 let security = BrowserSecurityGuard::new(vec![], vec!["evil.com".to_string()]);
1217 let (ctx, _client) = make_ctx_with_security(security);
1218 let tool = BrowserNavigateTool::new(ctx);
1219 let result = tool
1220 .execute(serde_json::json!({"url": "https://evil.com"}))
1221 .await;
1222 assert!(result.is_err());
1223 }
1224
1225 #[tokio::test]
1226 async fn test_click_tool_calls_cdp_click() {
1227 let (ctx, client) = make_ctx();
1228 let tool = BrowserClickTool::new(ctx);
1229 tool.execute(serde_json::json!({"selector": "#submit"}))
1230 .await
1231 .unwrap();
1232 assert_eq!(client.call_count("click"), 1);
1233 }
1234
1235 #[tokio::test]
1236 async fn test_click_tool_element_not_found() {
1237 let (ctx, client) = make_ctx();
1238 client.set_click_error(BrowserError::ElementNotFound {
1239 selector: "#missing".to_string(),
1240 });
1241 let tool = BrowserClickTool::new(ctx);
1242 let result = tool
1243 .execute(serde_json::json!({"selector": "#missing"}))
1244 .await;
1245 assert!(result.is_err());
1246 }
1247
1248 #[tokio::test]
1249 async fn test_type_tool_calls_cdp_type() {
1250 let (ctx, client) = make_ctx();
1251 let tool = BrowserTypeTool::new(ctx);
1252 tool.execute(serde_json::json!({"selector": "#input", "text": "hello"}))
1253 .await
1254 .unwrap();
1255 assert_eq!(client.call_count("type_text"), 1);
1256 }
1257
1258 #[tokio::test]
1259 async fn test_fill_tool_clears_and_types() {
1260 let (ctx, client) = make_ctx();
1261 let tool = BrowserFillTool::new(ctx);
1262 let result = tool
1263 .execute(serde_json::json!({"selector": "#email", "value": "a@b.com"}))
1264 .await
1265 .unwrap();
1266 assert!(result.content.contains("Filled"));
1267 assert_eq!(client.call_count("fill"), 1);
1268 }
1269
1270 #[tokio::test]
1271 async fn test_screenshot_tool_returns_base64() {
1272 let (ctx, client) = make_ctx();
1273 client.set_screenshot(vec![0x89, 0x50, 0x4E, 0x47]);
1274 let tool = BrowserScreenshotTool::new(ctx);
1275 let result = tool.execute(serde_json::json!({})).await.unwrap();
1276 assert!(!result.content.is_empty());
1278 let decoded =
1280 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &result.content)
1281 .unwrap();
1282 assert_eq!(decoded, vec![0x89, 0x50, 0x4E, 0x47]);
1283 }
1284
1285 #[tokio::test]
1286 async fn test_snapshot_tool_html_mode() {
1287 let (ctx, client) = make_ctx();
1288 client.set_html("<html><body>Hello</body></html>");
1289 let tool = BrowserSnapshotTool::new(ctx);
1290 let result = tool
1291 .execute(serde_json::json!({"mode": "html"}))
1292 .await
1293 .unwrap();
1294 assert!(result.content.contains("Hello"));
1295 }
1296
1297 #[tokio::test]
1298 async fn test_snapshot_tool_aria_mode() {
1299 let (ctx, client) = make_ctx();
1300 client.set_aria_tree("document\n heading 'Welcome'");
1301 let tool = BrowserSnapshotTool::new(ctx);
1302 let result = tool
1303 .execute(serde_json::json!({"mode": "aria_tree"}))
1304 .await
1305 .unwrap();
1306 assert!(result.content.contains("heading"));
1307 }
1308
1309 #[tokio::test]
1310 async fn test_snapshot_tool_text_mode() {
1311 let (ctx, client) = make_ctx();
1312 client.set_text("Welcome to the page");
1313 let tool = BrowserSnapshotTool::new(ctx);
1314 let result = tool
1315 .execute(serde_json::json!({"mode": "text"}))
1316 .await
1317 .unwrap();
1318 assert_eq!(result.content, "Welcome to the page");
1319 }
1320
1321 #[tokio::test]
1322 async fn test_js_eval_tool_returns_result() {
1323 let (ctx, client) = make_ctx();
1324 client.add_js_result("document.title", serde_json::json!("My Page"));
1325 let tool = BrowserJsEvalTool::new(ctx);
1326 let result = tool
1327 .execute(serde_json::json!({"script": "document.title"}))
1328 .await
1329 .unwrap();
1330 assert!(result.content.contains("My Page"));
1331 }
1332
1333 #[tokio::test]
1334 async fn test_url_tool_returns_current_url() {
1335 let (ctx, client) = make_ctx();
1336 client.set_url("https://example.com/page");
1337 let tool = BrowserUrlTool::new(ctx);
1338 let result = tool.execute(serde_json::json!({})).await.unwrap();
1339 assert_eq!(result.content, "https://example.com/page");
1340 }
1341
1342 #[tokio::test]
1343 async fn test_wait_tool_times_out() {
1344 let (ctx, client) = make_ctx();
1345 client.set_wait_error(BrowserError::Timeout { timeout_secs: 5 });
1346 let tool = BrowserWaitTool::new(ctx);
1347 let result = tool
1348 .execute(serde_json::json!({"selector": "#never", "timeout_ms": 5000}))
1349 .await;
1350 assert!(result.is_err());
1351 }
1352
1353 #[tokio::test]
1354 async fn test_all_browser_tools_register() {
1355 let (ctx, _client) = make_ctx();
1356 let mut registry = ToolRegistry::new();
1357 register_browser_tools(&mut registry, ctx);
1358 assert_eq!(registry.len(), 24);
1359
1360 let names = registry.list_names();
1362 let unique: std::collections::HashSet<_> = names.iter().collect();
1363 assert_eq!(unique.len(), 24);
1364 }
1365
1366 #[tokio::test]
1367 async fn test_browser_tool_risk_levels() {
1368 let (ctx, _client) = make_ctx();
1369 let tools = create_browser_tools(ctx);
1370 let mut risk_map = std::collections::HashMap::new();
1371 for tool in &tools {
1372 risk_map.insert(tool.name().to_string(), tool.risk_level());
1373 }
1374 assert_eq!(risk_map["browser_snapshot"], RiskLevel::ReadOnly);
1376 assert_eq!(risk_map["browser_url"], RiskLevel::ReadOnly);
1377 assert_eq!(risk_map["browser_title"], RiskLevel::ReadOnly);
1378 assert_eq!(risk_map["browser_screenshot"], RiskLevel::ReadOnly);
1379 assert_eq!(risk_map["browser_wait"], RiskLevel::ReadOnly);
1380 assert_eq!(risk_map["browser_navigate"], RiskLevel::Write);
1382 assert_eq!(risk_map["browser_click"], RiskLevel::Write);
1383 assert_eq!(risk_map["browser_type"], RiskLevel::Write);
1384 assert_eq!(risk_map["browser_fill"], RiskLevel::Write);
1385 assert_eq!(risk_map["browser_close"], RiskLevel::Write);
1386 assert_eq!(risk_map["browser_js_eval"], RiskLevel::Execute);
1388 assert_eq!(risk_map["browser_file_upload"], RiskLevel::Network);
1390 assert_eq!(risk_map["browser_download"], RiskLevel::Network);
1391 assert_eq!(risk_map["browser_new_tab"], RiskLevel::Write);
1393 assert_eq!(risk_map["browser_list_tabs"], RiskLevel::ReadOnly);
1394 assert_eq!(risk_map["browser_switch_tab"], RiskLevel::ReadOnly);
1395 assert_eq!(risk_map["browser_close_tab"], RiskLevel::Write);
1396 }
1397
1398 #[tokio::test]
1399 async fn test_browser_back_forward_refresh() {
1400 let (ctx, client) = make_ctx();
1401 BrowserBackTool::new(ctx.clone())
1402 .execute(serde_json::json!({}))
1403 .await
1404 .unwrap();
1405 BrowserForwardTool::new(ctx.clone())
1406 .execute(serde_json::json!({}))
1407 .await
1408 .unwrap();
1409 BrowserRefreshTool::new(ctx)
1410 .execute(serde_json::json!({}))
1411 .await
1412 .unwrap();
1413 assert_eq!(client.call_count("go_back"), 1);
1414 assert_eq!(client.call_count("go_forward"), 1);
1415 assert_eq!(client.call_count("refresh"), 1);
1416 }
1417
1418 #[tokio::test]
1419 async fn test_scroll_tool() {
1420 let (ctx, client) = make_ctx();
1421 let tool = BrowserScrollTool::new(ctx);
1422 tool.execute(serde_json::json!({"x": 0, "y": 500}))
1423 .await
1424 .unwrap();
1425 let calls = client.calls();
1426 let scroll_call = calls.iter().find(|(m, _)| m == "scroll").unwrap();
1427 assert_eq!(scroll_call.1, vec!["0", "500"]);
1428 }
1429
1430 #[tokio::test]
1431 async fn test_hover_tool() {
1432 let (ctx, client) = make_ctx();
1433 let tool = BrowserHoverTool::new(ctx);
1434 tool.execute(serde_json::json!({"selector": "#menu"}))
1435 .await
1436 .unwrap();
1437 assert_eq!(client.call_count("hover"), 1);
1438 }
1439
1440 #[tokio::test]
1441 async fn test_press_key_tool() {
1442 let (ctx, client) = make_ctx();
1443 let tool = BrowserPressKeyTool::new(ctx);
1444 tool.execute(serde_json::json!({"key": "Enter"}))
1445 .await
1446 .unwrap();
1447 assert_eq!(client.call_count("press_key"), 1);
1448 }
1449
1450 #[tokio::test]
1451 async fn test_select_tool() {
1452 let (ctx, client) = make_ctx();
1453 let tool = BrowserSelectTool::new(ctx);
1454 tool.execute(serde_json::json!({"selector": "#country", "value": "US"}))
1455 .await
1456 .unwrap();
1457 assert_eq!(client.call_count("select_option"), 1);
1458 }
1459
1460 #[tokio::test]
1461 async fn test_title_tool() {
1462 let (ctx, client) = make_ctx();
1463 client.set_title("Test Page");
1464 let tool = BrowserTitleTool::new(ctx);
1465 let result = tool.execute(serde_json::json!({})).await.unwrap();
1466 assert_eq!(result.content, "Test Page");
1467 }
1468
1469 #[tokio::test]
1470 async fn test_file_upload_tool() {
1471 let (ctx, client) = make_ctx();
1472 let tool = BrowserFileUploadTool::new(ctx);
1473 tool.execute(serde_json::json!({"selector": "#file", "path": "/tmp/test.txt"}))
1474 .await
1475 .unwrap();
1476 assert_eq!(client.call_count("upload_file"), 1);
1477 }
1478
1479 #[tokio::test]
1480 async fn test_download_tool() {
1481 let (ctx, client) = make_ctx();
1482 let tool = BrowserDownloadTool::new(ctx);
1483 tool.execute(serde_json::json!({"selector": "#download-btn"}))
1484 .await
1485 .unwrap();
1486 assert_eq!(client.call_count("click"), 1);
1487 }
1488
1489 #[tokio::test]
1490 async fn test_close_tool() {
1491 let (ctx, client) = make_ctx();
1492 let tool = BrowserCloseTool::new(ctx);
1493 tool.execute(serde_json::json!({})).await.unwrap();
1494 assert!(*client.closed.lock().unwrap());
1495 }
1496}