1use std::collections::HashMap;
4use std::sync::Arc;
5
6use serde::Deserialize;
7use serde_json::json;
8use tokio::sync::RwLock;
9
10use webpuppet::{
11 BrowserDetector, InterventionHandler, InterventionState, Operation, PermissionGuard,
12 PromptRequest, Provider, ScreeningConfig, WebPuppet,
13};
14
15use crate::error::{Error, Result};
16use crate::protocol::{ContentItem, ToolCallResult, ToolDefinition};
17
18#[async_trait::async_trait]
20pub trait Tool: Send + Sync {
21 fn definition(&self) -> ToolDefinition;
23
24 async fn execute(
26 &self,
27 arguments: serde_json::Value,
28 context: &ToolContext,
29 ) -> Result<ToolCallResult>;
30}
31
32pub struct ToolContext {
34 pub puppet: Arc<RwLock<Option<WebPuppet>>>,
36 pub permissions: Arc<PermissionGuard>,
38 pub screening_config: ScreeningConfig,
40 pub intervention_handler: Arc<RwLock<InterventionHandler>>,
42 pub headless: bool,
44}
45
46impl ToolContext {
47 pub fn new(permissions: PermissionGuard) -> Self {
49 Self {
50 puppet: Arc::new(RwLock::new(None)),
51 permissions: Arc::new(permissions),
52 screening_config: ScreeningConfig::default(),
53 intervention_handler: Arc::new(RwLock::new(InterventionHandler::new())),
54 headless: true,
55 }
56 }
57
58 pub fn with_visible_browser(permissions: PermissionGuard) -> Self {
60 Self {
61 puppet: Arc::new(RwLock::new(None)),
62 permissions: Arc::new(permissions),
63 screening_config: ScreeningConfig::default(),
64 intervention_handler: Arc::new(RwLock::new(InterventionHandler::new())),
65 headless: false,
66 }
67 }
68
69 pub async fn get_puppet(&self) -> Result<WebPuppet> {
71 let guard = self.puppet.read().await;
72 if let Some(ref _puppet) = *guard {
73 drop(guard);
75 } else {
76 drop(guard);
77 }
78
79 let puppet = WebPuppet::builder()
81 .with_all_providers()
82 .headless(self.headless)
83 .with_screening_config(self.screening_config.clone())
84 .build()
85 .await?;
86
87 Ok(puppet)
88 }
89}
90
91pub struct ToolRegistry {
93 tools: HashMap<String, Arc<dyn Tool>>,
94 context: Arc<ToolContext>,
95}
96
97impl ToolRegistry {
98 pub fn new(permissions: PermissionGuard) -> Self {
100 Self::with_context(ToolContext::new(permissions))
101 }
102
103 pub fn with_visible_browser(permissions: PermissionGuard) -> Self {
105 Self::with_context(ToolContext::with_visible_browser(permissions))
106 }
107
108 fn with_context(context: ToolContext) -> Self {
110 let context = Arc::new(context);
111 let mut tools: HashMap<String, Arc<dyn Tool>> = HashMap::new();
112
113 let prompt_tool = Arc::new(PromptTool);
115 tools.insert(prompt_tool.definition().name.clone(), prompt_tool);
116
117 let list_providers_tool = Arc::new(ListProvidersTool);
118 tools.insert(
119 list_providers_tool.definition().name.clone(),
120 list_providers_tool,
121 );
122
123 let provider_caps_tool = Arc::new(ProviderCapabilitiesTool);
124 tools.insert(
125 provider_caps_tool.definition().name.clone(),
126 provider_caps_tool,
127 );
128
129 let detect_browsers_tool = Arc::new(DetectBrowsersTool);
130 tools.insert(
131 detect_browsers_tool.definition().name.clone(),
132 detect_browsers_tool,
133 );
134
135 let screenshot_tool = Arc::new(ScreenshotTool);
136 tools.insert(screenshot_tool.definition().name.clone(), screenshot_tool);
137
138 let check_permission_tool = Arc::new(CheckPermissionTool);
139 tools.insert(
140 check_permission_tool.definition().name.clone(),
141 check_permission_tool,
142 );
143
144 let intervention_status_tool = Arc::new(InterventionStatusTool);
146 tools.insert(
147 intervention_status_tool.definition().name.clone(),
148 intervention_status_tool,
149 );
150
151 let intervention_complete_tool = Arc::new(InterventionCompleteTool);
152 tools.insert(
153 intervention_complete_tool.definition().name.clone(),
154 intervention_complete_tool,
155 );
156
157 let intervention_pause_tool = Arc::new(InterventionPauseTool);
158 tools.insert(
159 intervention_pause_tool.definition().name.clone(),
160 intervention_pause_tool,
161 );
162
163 let intervention_resume_tool = Arc::new(InterventionResumeTool);
164 tools.insert(
165 intervention_resume_tool.definition().name.clone(),
166 intervention_resume_tool,
167 );
168
169 let navigate_tool = Arc::new(NavigateTool);
171 tools.insert(navigate_tool.definition().name.clone(), navigate_tool);
172
173 let browser_status_tool = Arc::new(BrowserStatusTool);
174 tools.insert(
175 browser_status_tool.definition().name.clone(),
176 browser_status_tool,
177 );
178
179 Self { tools, context }
180 }
181
182 pub fn list_tools(&self) -> Vec<ToolDefinition> {
184 self.tools.values().map(|t| t.definition()).collect()
185 }
186
187 pub async fn execute(
189 &self,
190 name: &str,
191 arguments: serde_json::Value,
192 ) -> Result<ToolCallResult> {
193 let tool = self
194 .tools
195 .get(name)
196 .ok_or_else(|| Error::ToolNotFound(name.to_string()))?;
197
198 tool.execute(arguments, &self.context).await
199 }
200
201 pub fn register(&mut self, tool: Arc<dyn Tool>) {
203 let name = tool.definition().name.clone();
204 self.tools.insert(name, tool);
205 }
206}
207
208pub struct PromptTool;
214
215#[derive(Debug, Deserialize)]
216struct PromptArgs {
217 provider: String,
219 message: String,
221 context: Option<String>,
223}
224
225#[async_trait::async_trait]
226impl Tool for PromptTool {
227 fn definition(&self) -> ToolDefinition {
228 ToolDefinition {
229 name: "webpuppet_prompt".into(),
230 description: "Send a prompt through browser automation (AI providers + select web tools). Uses existing authenticated sessions.".into(),
231 input_schema: json!({
232 "type": "object",
233 "properties": {
234 "provider": {
235 "type": "string",
236 "enum": ["claude", "grok", "gemini", "chatgpt", "perplexity", "notebooklm", "kaggle"],
237 "description": "Provider/tool to use"
238 },
239 "message": {
240 "type": "string",
241 "description": "The prompt message to send"
242 },
243 "context": {
244 "type": "string",
245 "description": "Optional context or system instructions"
246 }
247 },
248 "required": ["provider", "message"]
249 }),
250 }
251 }
252
253 async fn execute(
254 &self,
255 arguments: serde_json::Value,
256 context: &ToolContext,
257 ) -> Result<ToolCallResult> {
258 context
260 .permissions
261 .require(Operation::SendPrompt)
262 .map_err(|e| Error::PermissionDenied(e.to_string()))?;
263
264 let args: PromptArgs =
266 serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
267
268 let provider = match args.provider.to_lowercase().as_str() {
270 "claude" => Provider::Claude,
271 "grok" => Provider::Grok,
272 "gemini" => Provider::Gemini,
273 "chatgpt" | "openai" => Provider::ChatGpt,
274 "perplexity" => Provider::Perplexity,
275 "notebooklm" | "notebook" => Provider::NotebookLm,
276 "kaggle" => Provider::Kaggle,
277 _ => {
278 return Err(Error::InvalidParams(format!(
279 "unknown provider: {}",
280 args.provider
281 )))
282 }
283 };
284
285 let mut request = PromptRequest::new(args.message);
287 if let Some(ctx) = args.context {
288 request = request.with_context(ctx);
289 }
290
291 let puppet = context.get_puppet().await?;
293
294 puppet.authenticate(provider).await?;
296
297 let (response, screening) = puppet.prompt_screened(provider, request).await?;
299
300 puppet.close().await.ok();
302
303 let result_text = if screening.passed {
305 response.text
306 } else {
307 format!(
308 "[SECURITY WARNING: Response had risk score {:.2}]\n\n{}",
309 screening.risk_score, response.text
310 )
311 };
312
313 Ok(ToolCallResult {
314 content: vec![ContentItem::text(result_text)],
315 is_error: false,
316 })
317 }
318}
319
320pub struct ListProvidersTool;
322
323#[async_trait::async_trait]
324impl Tool for ListProvidersTool {
325 fn definition(&self) -> ToolDefinition {
326 ToolDefinition {
327 name: "webpuppet_list_providers".into(),
328 description: "List available AI providers and their status.".into(),
329 input_schema: json!({
330 "type": "object",
331 "properties": {},
332 "required": []
333 }),
334 }
335 }
336
337 async fn execute(
338 &self,
339 _arguments: serde_json::Value,
340 _context: &ToolContext,
341 ) -> Result<ToolCallResult> {
342 let providers = [
343 (
344 "claude",
345 "Claude (Anthropic)",
346 "https://claude.ai",
347 "Large context, artifacts, code",
348 ),
349 (
350 "grok",
351 "Grok (X/xAI)",
352 "https://x.com/i/grok",
353 "Real-time info, integrated with X",
354 ),
355 (
356 "gemini",
357 "Gemini (Google)",
358 "https://gemini.google.com",
359 "Google integration, large context",
360 ),
361 (
362 "chatgpt",
363 "ChatGPT (OpenAI)",
364 "https://chat.openai.com",
365 "GPT-4o, vision, code, web search",
366 ),
367 (
368 "perplexity",
369 "Perplexity AI",
370 "https://www.perplexity.ai",
371 "Search-focused, sources cited",
372 ),
373 (
374 "notebooklm",
375 "NotebookLM (Google)",
376 "https://notebooklm.google.com",
377 "Research assistant, 500k context",
378 ),
379 (
380 "kaggle",
381 "Kaggle (Datasets)",
382 "https://www.kaggle.com/datasets",
383 "Dataset search/catalog; returns dataset page links",
384 ),
385 ];
386
387 let text = providers
388 .iter()
389 .map(|(id, name, url, features)| {
390 format!(
391 "- **{}** (`{}`): [{}]({})\n _{}_",
392 name, id, url, url, features
393 )
394 })
395 .collect::<Vec<_>>()
396 .join("\n");
397
398 Ok(ToolCallResult {
399 content: vec![ContentItem::text(format!(
400 "# Available Providers\n\n{}\n\n*Note: Uses browser sessions; some providers require login.*",
401 text
402 ))],
403 is_error: false,
404 })
405 }
406}
407
408pub struct ProviderCapabilitiesTool;
410
411#[derive(Debug, Deserialize)]
412struct ProviderCapabilitiesArgs {
413 provider: String,
415}
416
417#[async_trait::async_trait]
418impl Tool for ProviderCapabilitiesTool {
419 fn definition(&self) -> ToolDefinition {
420 ToolDefinition {
421 name: "webpuppet_provider_capabilities".into(),
422 description: "Get declared capabilities for a provider/tool (conversation, vision, file upload, web search, etc).".into(),
423 input_schema: json!({
424 "type": "object",
425 "properties": {
426 "provider": {
427 "type": "string",
428 "enum": ["claude", "grok", "gemini", "chatgpt", "perplexity", "notebooklm", "kaggle"],
429 "description": "Provider/tool to inspect"
430 }
431 },
432 "required": ["provider"]
433 }),
434 }
435 }
436
437 async fn execute(
438 &self,
439 arguments: serde_json::Value,
440 context: &ToolContext,
441 ) -> Result<ToolCallResult> {
442 context
443 .permissions
444 .require(Operation::ReadContent)
445 .map_err(|e| Error::PermissionDenied(e.to_string()))?;
446
447 let args: ProviderCapabilitiesArgs =
448 serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
449
450 let provider = match args.provider.to_lowercase().as_str() {
451 "claude" => Provider::Claude,
452 "grok" => Provider::Grok,
453 "gemini" => Provider::Gemini,
454 "chatgpt" | "openai" => Provider::ChatGpt,
455 "perplexity" => Provider::Perplexity,
456 "notebooklm" | "notebook" => Provider::NotebookLm,
457 "kaggle" => Provider::Kaggle,
458 _ => {
459 return Err(Error::InvalidParams(format!(
460 "unknown provider: {}",
461 args.provider
462 )))
463 }
464 };
465
466 let puppet = context.get_puppet().await?;
468
469 let caps = puppet
470 .provider_capabilities(provider)
471 .ok_or_else(|| Error::InvalidParams(format!("provider not available: {}", provider)))?;
472
473 puppet.close().await.ok();
474
475 Ok(ToolCallResult {
476 content: vec![ContentItem::text(
477 serde_json::to_string_pretty(&json!({
478 "provider": provider.to_string(),
479 "capabilities": {
480 "conversation": caps.conversation,
481 "vision": caps.vision,
482 "file_upload": caps.file_upload,
483 "code_execution": caps.code_execution,
484 "web_search": caps.web_search,
485 "max_context": caps.max_context,
486 "models": caps.models,
487 "note": "Declared capabilities (not runtime UI detection)."
488 }
489 }))
490 .map_err(|e| Error::Internal(e.to_string()))?,
491 )],
492 is_error: false,
493 })
494 }
495}
496
497pub struct DetectBrowsersTool;
499
500#[async_trait::async_trait]
501impl Tool for DetectBrowsersTool {
502 fn definition(&self) -> ToolDefinition {
503 ToolDefinition {
504 name: "webpuppet_detect_browsers".into(),
505 description: "Detect installed browsers that can be used for automation.".into(),
506 input_schema: json!({
507 "type": "object",
508 "properties": {},
509 "required": []
510 }),
511 }
512 }
513
514 async fn execute(
515 &self,
516 _arguments: serde_json::Value,
517 _context: &ToolContext,
518 ) -> Result<ToolCallResult> {
519 let browsers = BrowserDetector::detect_all();
520
521 if browsers.is_empty() {
522 return Ok(ToolCallResult {
523 content: vec![ContentItem::text(
524 "No supported browsers detected. Please install Brave, Chrome, or Chromium.",
525 )],
526 is_error: true,
527 });
528 }
529
530 let text = browsers
531 .iter()
532 .map(|b| {
533 let version = b.version.as_deref().unwrap_or("unknown");
534 let profiles = b.list_profiles().unwrap_or_default();
535 format!(
536 "- **{}** ({})\n - Path: `{}`\n - Data: `{}`\n - Profiles: {}",
537 b.browser_type,
538 version,
539 b.executable_path.display(),
540 b.user_data_dir.display(),
541 if profiles.is_empty() {
542 "none".to_string()
543 } else {
544 profiles.join(", ")
545 }
546 )
547 })
548 .collect::<Vec<_>>()
549 .join("\n\n");
550
551 Ok(ToolCallResult {
552 content: vec![ContentItem::text(format!(
553 "# Detected Browsers\n\n{}",
554 text
555 ))],
556 is_error: false,
557 })
558 }
559}
560
561pub struct ScreenshotTool;
563
564#[derive(Debug, Deserialize)]
565struct ScreenshotArgs {
566 url: String,
568}
569
570#[async_trait::async_trait]
571impl Tool for ScreenshotTool {
572 fn definition(&self) -> ToolDefinition {
573 ToolDefinition {
574 name: "webpuppet_screenshot".into(),
575 description: "Take a screenshot of a web page. Only allowed domains can be accessed."
576 .into(),
577 input_schema: json!({
578 "type": "object",
579 "properties": {
580 "url": {
581 "type": "string",
582 "description": "URL to take a screenshot of"
583 }
584 },
585 "required": ["url"]
586 }),
587 }
588 }
589
590 async fn execute(
591 &self,
592 arguments: serde_json::Value,
593 context: &ToolContext,
594 ) -> Result<ToolCallResult> {
595 let args: ScreenshotArgs =
596 serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
597
598 context
600 .permissions
601 .require_with_url(Operation::Navigate, &args.url)
602 .map_err(|e| Error::PermissionDenied(e.to_string()))?;
603
604 context
605 .permissions
606 .require(Operation::Screenshot)
607 .map_err(|e| Error::PermissionDenied(e.to_string()))?;
608
609 Ok(ToolCallResult {
611 content: vec![ContentItem::text(format!(
612 "Screenshot of `{}` would be captured here.\n\n*Note: Full browser implementation required for actual screenshots.*",
613 args.url
614 ))],
615 is_error: false,
616 })
617 }
618}
619
620pub struct CheckPermissionTool;
622
623#[derive(Debug, Deserialize)]
624struct CheckPermissionArgs {
625 operation: String,
627 url: Option<String>,
629}
630
631#[async_trait::async_trait]
632impl Tool for CheckPermissionTool {
633 fn definition(&self) -> ToolDefinition {
634 ToolDefinition {
635 name: "webpuppet_check_permission".into(),
636 description: "Check if an operation is allowed by the security policy.".into(),
637 input_schema: json!({
638 "type": "object",
639 "properties": {
640 "operation": {
641 "type": "string",
642 "description": "Operation to check (e.g., Navigate, SendPrompt, DeleteAccount)"
643 },
644 "url": {
645 "type": "string",
646 "description": "Optional URL context for navigation checks"
647 }
648 },
649 "required": ["operation"]
650 }),
651 }
652 }
653
654 async fn execute(
655 &self,
656 arguments: serde_json::Value,
657 context: &ToolContext,
658 ) -> Result<ToolCallResult> {
659 let args: CheckPermissionArgs =
660 serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
661
662 let operation = match args.operation.to_lowercase().as_str() {
664 "navigate" => Operation::Navigate,
665 "sendprompt" | "send_prompt" => Operation::SendPrompt,
666 "readresponse" | "read_response" => Operation::ReadResponse,
667 "screenshot" => Operation::Screenshot,
668 "click" => Operation::Click,
669 "typetext" | "type_text" => Operation::TypeText,
670 "deleteaccount" | "delete_account" => Operation::DeleteAccount,
671 "changepassword" | "change_password" => Operation::ChangePassword,
672 _ => {
673 return Ok(ToolCallResult {
674 content: vec![ContentItem::text(format!(
675 "Unknown operation: `{}`\n\nValid operations: Navigate, SendPrompt, ReadResponse, Screenshot, Click, TypeText, DeleteAccount, ChangePassword",
676 args.operation
677 ))],
678 is_error: true,
679 });
680 }
681 };
682
683 let decision = if let Some(url) = args.url {
684 context.permissions.check_with_url(operation, &url)
685 } else {
686 context.permissions.check(operation)
687 };
688
689 let status = if decision.allowed {
690 "✅ ALLOWED"
691 } else {
692 "❌ DENIED"
693 };
694 let text = format!(
695 "# Permission Check\n\n**Operation**: `{}`\n**Status**: {}\n**Reason**: {}\n**Risk Level**: {}/10",
696 operation, status, decision.reason, decision.risk_level
697 );
698
699 Ok(ToolCallResult {
700 content: vec![ContentItem::text(text)],
701 is_error: false,
702 })
703 }
704}
705
706pub struct InterventionStatusTool;
712
713#[async_trait::async_trait]
714impl Tool for InterventionStatusTool {
715 fn definition(&self) -> ToolDefinition {
716 ToolDefinition {
717 name: "webpuppet_intervention_status".into(),
718 description: "Check if human intervention is needed (captcha, 2FA, etc.). Returns current automation state and any pending intervention reason.".into(),
719 input_schema: json!({
720 "type": "object",
721 "properties": {},
722 "required": []
723 }),
724 }
725 }
726
727 async fn execute(
728 &self,
729 _arguments: serde_json::Value,
730 context: &ToolContext,
731 ) -> Result<ToolCallResult> {
732 let handler = context.intervention_handler.read().await;
733 let state = handler.state();
734 let reason = handler.current_reason();
735
736 let state_str = match state {
737 InterventionState::Running => "🟢 Running",
738 InterventionState::WaitingForHuman => "🟡 Waiting for human",
739 InterventionState::Resuming => "🔵 Resuming",
740 InterventionState::TimedOut => "🔴 Timed out",
741 InterventionState::Cancelled => "⚫ Cancelled",
742 };
743
744 let text = if let Some(reason) = reason {
745 format!(
746 "# Intervention Status\n\n**State**: {}\n**Reason**: {}\n\n⚠️ **Action Required**: Please complete the intervention in the browser, then call `webpuppet_intervention_complete` with success=true.",
747 state_str, reason
748 )
749 } else {
750 format!(
751 "# Intervention Status\n\n**State**: {}\n\nNo intervention currently required. Automation is running normally.",
752 state_str
753 )
754 };
755
756 Ok(ToolCallResult {
757 content: vec![ContentItem::text(text)],
758 is_error: false,
759 })
760 }
761}
762
763pub struct InterventionCompleteTool;
765
766#[derive(Debug, Deserialize)]
767struct InterventionCompleteArgs {
768 success: bool,
770 message: Option<String>,
772}
773
774#[async_trait::async_trait]
775impl Tool for InterventionCompleteTool {
776 fn definition(&self) -> ToolDefinition {
777 ToolDefinition {
778 name: "webpuppet_intervention_complete".into(),
779 description: "Signal that a human intervention (captcha, 2FA, etc.) has been completed. Call this after manually handling the intervention in the browser.".into(),
780 input_schema: json!({
781 "type": "object",
782 "properties": {
783 "success": {
784 "type": "boolean",
785 "description": "Whether the intervention was completed successfully"
786 },
787 "message": {
788 "type": "string",
789 "description": "Optional message about what was done"
790 }
791 },
792 "required": ["success"]
793 }),
794 }
795 }
796
797 async fn execute(
798 &self,
799 arguments: serde_json::Value,
800 context: &ToolContext,
801 ) -> Result<ToolCallResult> {
802 let args: InterventionCompleteArgs =
803 serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
804
805 let handler = context.intervention_handler.read().await;
806 handler.complete(args.success, args.message.clone());
807
808 let status = if args.success {
809 "✅ SUCCESS"
810 } else {
811 "❌ FAILED"
812 };
813 let text = format!(
814 "# Intervention Complete\n\n**Status**: {}\n**Message**: {}\n\nAutomation will now resume.",
815 status,
816 args.message.unwrap_or_else(|| "None".into())
817 );
818
819 Ok(ToolCallResult {
820 content: vec![ContentItem::text(text)],
821 is_error: false,
822 })
823 }
824}
825
826pub struct InterventionPauseTool;
828
829#[async_trait::async_trait]
830impl Tool for InterventionPauseTool {
831 fn definition(&self) -> ToolDefinition {
832 ToolDefinition {
833 name: "webpuppet_pause".into(),
834 description: "Pause browser automation. Use this when you need to manually interact with the browser.".into(),
835 input_schema: json!({
836 "type": "object",
837 "properties": {},
838 "required": []
839 }),
840 }
841 }
842
843 async fn execute(
844 &self,
845 _arguments: serde_json::Value,
846 context: &ToolContext,
847 ) -> Result<ToolCallResult> {
848 let handler = context.intervention_handler.read().await;
849 handler.pause();
850
851 Ok(ToolCallResult {
852 content: vec![ContentItem::text(
853 "# Automation Paused\n\n⏸️ Automation is now paused. The browser is available for manual interaction.\n\nCall `webpuppet_resume` when ready to continue."
854 )],
855 is_error: false,
856 })
857 }
858}
859
860pub struct InterventionResumeTool;
862
863#[async_trait::async_trait]
864impl Tool for InterventionResumeTool {
865 fn definition(&self) -> ToolDefinition {
866 ToolDefinition {
867 name: "webpuppet_resume".into(),
868 description: "Resume browser automation after a pause or manual intervention.".into(),
869 input_schema: json!({
870 "type": "object",
871 "properties": {},
872 "required": []
873 }),
874 }
875 }
876
877 async fn execute(
878 &self,
879 _arguments: serde_json::Value,
880 context: &ToolContext,
881 ) -> Result<ToolCallResult> {
882 let handler = context.intervention_handler.read().await;
883 handler.resume();
884
885 Ok(ToolCallResult {
886 content: vec![ContentItem::text(
887 "# Automation Resumed\n\n▶️ Automation has been resumed. Browser operations will continue."
888 )],
889 is_error: false,
890 })
891 }
892}
893
894pub struct NavigateTool;
896
897#[derive(Debug, Deserialize)]
898struct NavigateArgs {
899 url: String,
901}
902
903#[async_trait::async_trait]
904impl Tool for NavigateTool {
905 fn definition(&self) -> ToolDefinition {
906 ToolDefinition {
907 name: "webpuppet_navigate".into(),
908 description: "Navigate browser to a URL. Opens a browser window if not already open."
909 .into(),
910 input_schema: json!({
911 "type": "object",
912 "properties": {
913 "url": {
914 "type": "string",
915 "description": "URL to navigate to"
916 }
917 },
918 "required": ["url"]
919 }),
920 }
921 }
922
923 async fn execute(
924 &self,
925 arguments: serde_json::Value,
926 context: &ToolContext,
927 ) -> Result<ToolCallResult> {
928 context
930 .permissions
931 .require(Operation::Navigate)
932 .map_err(|e| Error::PermissionDenied(e.to_string()))?;
933
934 let args: NavigateArgs =
936 serde_json::from_value(arguments).map_err(|e| Error::InvalidParams(e.to_string()))?;
937
938 let puppet = context.get_puppet().await?;
940
941 let session = puppet.get_session(Provider::Grok).await?;
943
944 session.navigate(&args.url).await?;
946
947 let current_url = session
949 .current_url()
950 .await
951 .unwrap_or_else(|_| args.url.clone());
952 let title = session
953 .get_title()
954 .await
955 .unwrap_or_else(|_| "Unknown".into());
956
957 Ok(ToolCallResult {
958 content: vec![ContentItem::text(format!(
959 "# Browser Navigated\n\n✅ Successfully navigated to URL.\n\n- **URL**: {}\n- **Title**: {}",
960 current_url, title
961 ))],
962 is_error: false,
963 })
964 }
965}
966
967pub struct BrowserStatusTool;
969
970#[async_trait::async_trait]
971impl Tool for BrowserStatusTool {
972 fn definition(&self) -> ToolDefinition {
973 ToolDefinition {
974 name: "webpuppet_browser_status".into(),
975 description: "Get current browser status including URL, title, and visibility.".into(),
976 input_schema: json!({
977 "type": "object",
978 "properties": {},
979 "required": []
980 }),
981 }
982 }
983
984 async fn execute(
985 &self,
986 _arguments: serde_json::Value,
987 context: &ToolContext,
988 ) -> Result<ToolCallResult> {
989 let guard = context.puppet.read().await;
990
991 if guard.is_none() {
992 return Ok(ToolCallResult {
993 content: vec![ContentItem::text(
994 "# Browser Status\n\n⚪ No browser session is currently active.\n\nA browser will be launched when you use `webpuppet_navigate` or `webpuppet_prompt`."
995 )],
996 is_error: false,
997 });
998 }
999
1000 let visibility = if context.headless {
1002 "Headless"
1003 } else {
1004 "Visible"
1005 };
1006
1007 Ok(ToolCallResult {
1008 content: vec![ContentItem::text(format!(
1009 "# Browser Status\n\n🟢 Browser session is active.\n\n- **Mode**: {}\n- **Providers**: Grok, Claude, Gemini",
1010 visibility
1011 ))],
1012 is_error: false,
1013 })
1014 }
1015}
1016
1017mod async_trait_impl {
1019 pub use async_trait::async_trait;
1020}
1021pub use async_trait_impl::async_trait;