roboticus_browser/
agent_browser_backend.rs1use async_trait::async_trait;
8use serde_json::json;
9use tracing::{debug, warn};
10
11use crate::actions::{ActionResult, BrowserAction};
12use crate::backend::BrowserBackend;
13
14#[derive(Debug, Clone)]
16pub struct AgentBrowserConfig {
17 pub binary_path: String,
19 pub timeout_seconds: u64,
21}
22
23impl Default for AgentBrowserConfig {
24 fn default() -> Self {
25 Self {
26 binary_path: "agent-browser".into(),
27 timeout_seconds: 30,
28 }
29 }
30}
31
32pub struct AgentBrowserBackend {
34 config: AgentBrowserConfig,
35 available: bool,
36}
37
38impl AgentBrowserBackend {
39 pub fn new(config: AgentBrowserConfig) -> Self {
41 let available = std::process::Command::new("which")
42 .arg(&config.binary_path)
43 .output()
44 .map(|o| o.status.success())
45 .unwrap_or(false);
46
47 if !available {
48 warn!(
49 binary = %config.binary_path,
50 "agent-browser binary not found — backend will be unavailable"
51 );
52 }
53
54 Self { config, available }
55 }
56
57 fn action_to_args(action: &BrowserAction) -> Vec<String> {
59 match action {
60 BrowserAction::Navigate { url } => vec!["navigate".into(), url.clone()],
61 BrowserAction::Click { selector } => vec!["click".into(), selector.clone()],
62 BrowserAction::Type { selector, text } => {
63 vec!["type".into(), selector.clone(), text.clone()]
64 }
65 BrowserAction::Screenshot => vec!["screenshot".into()],
66 BrowserAction::Pdf => vec!["pdf".into()],
67 BrowserAction::Evaluate { expression } => {
68 vec!["evaluate".into(), expression.clone()]
69 }
70 BrowserAction::GetCookies => vec!["get-cookies".into()],
71 BrowserAction::ClearCookies => vec!["clear-cookies".into()],
72 BrowserAction::ReadPage => vec!["read-page".into()],
73 BrowserAction::GoBack => vec!["go-back".into()],
74 BrowserAction::GoForward => vec!["go-forward".into()],
75 BrowserAction::Reload => vec!["reload".into()],
76 }
77 }
78}
79
80#[async_trait]
81impl BrowserBackend for AgentBrowserBackend {
82 async fn execute(&self, action: &BrowserAction) -> ActionResult {
83 if !self.available {
84 return ActionResult::err("agent-browser", "agent-browser binary not available".into());
85 }
86
87 let args = Self::action_to_args(action);
88 let action_name = args.first().cloned().unwrap_or_default();
89 debug!(backend = "agent-browser", action = %action_name, "executing");
90
91 let timeout = std::time::Duration::from_secs(self.config.timeout_seconds);
92 let result = tokio::time::timeout(timeout, async {
93 tokio::process::Command::new(&self.config.binary_path)
94 .args(&args)
95 .arg("--json")
96 .output()
97 .await
98 })
99 .await;
100
101 match result {
102 Ok(Ok(output)) => {
103 if output.status.success() {
104 let stdout = String::from_utf8_lossy(&output.stdout);
105 match serde_json::from_str::<serde_json::Value>(&stdout) {
106 Ok(data) => ActionResult::ok(&action_name, data),
107 Err(_) => ActionResult::ok(&action_name, json!({ "raw": stdout.trim() })),
108 }
109 } else {
110 let stderr = String::from_utf8_lossy(&output.stderr);
111 ActionResult::err(
112 &action_name,
113 format!("exit code {}: {}", output.status, stderr.trim()),
114 )
115 }
116 }
117 Ok(Err(e)) => ActionResult::err(&action_name, format!("spawn failed: {e}")),
118 Err(_) => ActionResult::err(
119 &action_name,
120 format!("timeout after {}s", self.config.timeout_seconds),
121 ),
122 }
123 }
124
125 fn name(&self) -> &str {
126 "agent-browser"
127 }
128
129 fn is_available(&self) -> bool {
130 self.available
131 }
132}