# Extra HTTP headers
-agent-browser set credentials # HTTP basic auth
agent-browser set media [dark|light] # Emulate color scheme
```
@@ -339,7 +295,6 @@ agent-browser console --clear # Clear console
agent-browser errors # View page errors (uncaught JavaScript exceptions)
agent-browser errors --clear # Clear errors
agent-browser highlight # Highlight element
-agent-browser inspect # Open Chrome DevTools for the active page
agent-browser state save # Save auth state
agent-browser state load # Load auth state
agent-browser state list # List saved state files
@@ -376,36 +331,7 @@ agent-browser provides multiple ways to persist login sessions so you don't re-a
|----------|----------|------------|
| **Persistent profile** | Full browser state (cookies, IndexedDB, service workers, cache) across restarts | `--profile ` / `AGENT_BROWSER_PROFILE` |
| **Session persistence** | Auto-save/restore cookies + localStorage by name | `--session-name ` / `AGENT_BROWSER_SESSION_NAME` |
-| **Import from your browser** | Grab auth from a Chrome session you already logged into | `--auto-connect` + `state save` |
| **State file** | Load a previously saved state JSON on launch | `--state ` / `AGENT_BROWSER_STATE` |
-| **Auth vault** | Store credentials locally (encrypted), login by name | `auth save` / `auth login` |
-
-### Import auth from your browser
-
-If you are already logged in to a site in Chrome, you can grab that auth state and reuse it:
-
-```bash
-# 1. Launch Chrome with remote debugging enabled
-# macOS:
-"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222
-# Or use --auto-connect to discover an already-running Chrome
-
-# 2. Connect and save the authenticated state
-agent-browser --auto-connect state save ./my-auth.json
-
-# 3. Use the saved auth in future sessions
-agent-browser --state ./my-auth.json open https://app.example.com/dashboard
-
-# 4. Or use --session-name for automatic persistence
-agent-browser --session-name myapp state load ./my-auth.json
-# From now on, --session-name myapp auto-saves/restores this state
-```
-
-> **Security notes:**
-> - `--remote-debugging-port` exposes full browser control on localhost. Any local process can connect. Only use on trusted machines and close Chrome when done.
-> - State files contain session tokens in plaintext. Add them to `.gitignore` and delete when no longer needed. For encryption at rest, set `AGENT_BROWSER_ENCRYPTION_KEY` (see [State Encryption](#state-encryption)).
-
-For full details on login flows, OAuth, 2FA, cookie-based auth, and the auth vault, see the [Authentication](docs/src/app/sessions/page.mdx) docs.
## Sessions
@@ -500,24 +426,19 @@ agent-browser --session-name secure open example.com
agent-browser includes security features for safe AI agent deployments. All features are opt-in -- existing workflows are unaffected until you explicitly enable a feature:
-- **Authentication Vault** -- Store credentials locally (always encrypted), reference by name. The LLM never sees passwords. `auth login` navigates with `load` and then waits for login form selectors to appear (SPA-friendly, timeout follows the default action timeout). A key is auto-generated at `~/.agent-browser/.encryption-key` if `AGENT_BROWSER_ENCRYPTION_KEY` is not set: `echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin` then `agent-browser auth login github`
- **Content Boundary Markers** -- Wrap page output in delimiters so LLMs can distinguish tool output from untrusted content: `--content-boundaries`
-- **Domain Allowlist** -- Restrict navigation to trusted domains (wildcards like `*.example.com` also match the bare domain): `--allowed-domains "example.com,*.example.com"`. Sub-resource requests (scripts, images, fetch) and WebSocket/EventSource connections to non-allowed domains are also blocked. Include any CDN domains your target pages depend on (e.g., `*.cdn.example.com`).
-- **Action Policy** -- Gate destructive actions with a static policy file: `--action-policy ./policy.json`
-- **Action Confirmation** -- Require explicit approval for sensitive action categories: `--confirm-actions eval,download`
+- **Domain Allowlist** -- Restrict navigation to trusted domains (wildcards like `*.example.com` also match the bare domain). Set via `allowedDomains` in `agent-browser.json` (config-file-only, cannot be overridden by CLI flags or env vars). Sub-resource requests (scripts, images, fetch) and WebSocket/EventSource connections to non-allowed domains are also blocked.
+- **Action Policy** -- Gate destructive actions with a static policy file. Set via `actionPolicy` in `agent-browser.json` (config-file-only, cannot be overridden by CLI flags or env vars).
+- **Action Confirmation** -- Require explicit approval for sensitive action categories: `--confirm-actions download`
- **Output Length Limits** -- Prevent context flooding: `--max-output 50000`
| Variable | Description |
| ----------------------------------- | ---------------------------------------- |
| `AGENT_BROWSER_CONTENT_BOUNDARIES` | Wrap page output in boundary markers |
| `AGENT_BROWSER_MAX_OUTPUT` | Max characters for page output |
-| `AGENT_BROWSER_ALLOWED_DOMAINS` | Comma-separated allowed domain patterns |
-| `AGENT_BROWSER_ACTION_POLICY` | Path to action policy JSON file |
| `AGENT_BROWSER_CONFIRM_ACTIONS` | Action categories requiring confirmation |
| `AGENT_BROWSER_CONFIRM_INTERACTIVE` | Enable interactive confirmation prompts |
-See [Security documentation](https://agent-browser.dev/security) for details.
-
## Snapshot Options
The `snapshot` command supports filtering to reduce output size:
@@ -570,15 +491,11 @@ This is useful for multimodal AI models that can reason about visual layout, unl
| `--profile ` | Persistent browser profile directory (or `AGENT_BROWSER_PROFILE` env) |
| `--state ` | Load storage state from JSON file (or `AGENT_BROWSER_STATE` env) |
| `--headers ` | Set HTTP headers scoped to the URL's origin |
-| `--executable-path ` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
-| `--extension ` | Load browser extension (repeatable; or `AGENT_BROWSER_EXTENSIONS` env) |
| `--args ` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) |
| `--user-agent ` | Custom User-Agent string (or `AGENT_BROWSER_USER_AGENT` env) |
| `--proxy ` | Proxy server URL with optional auth (or `AGENT_BROWSER_PROXY` env) |
| `--proxy-bypass ` | Hosts to bypass proxy (or `AGENT_BROWSER_PROXY_BYPASS` env) |
| `--ignore-https-errors` | Ignore HTTPS certificate errors (useful for self-signed certs) |
-| `--allow-file-access` | Allow file:// URLs to access local files (Chromium only) |
-| `-p, --provider ` | Cloud browser provider (or `AGENT_BROWSER_PROVIDER` env) |
| `--device ` | iOS device name, e.g. "iPhone 15 Pro" (or `AGENT_BROWSER_IOS_DEVICE` env) |
| `--json` | JSON output (for agents) |
| `--annotate` | Annotated screenshot with numbered element labels (or `AGENT_BROWSER_ANNOTATE` env) |
@@ -586,14 +503,10 @@ This is useful for multimodal AI models that can reason about visual layout, unl
| `--screenshot-quality ` | JPEG quality 0-100 (or `AGENT_BROWSER_SCREENSHOT_QUALITY` env) |
| `--screenshot-format ` | Screenshot format: `png`, `jpeg` (or `AGENT_BROWSER_SCREENSHOT_FORMAT` env) |
| `--headed` | Show browser window (not headless) (or `AGENT_BROWSER_HEADED` env) |
-| `--cdp ` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |
-| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |
| `--color-scheme ` | Color scheme: `dark`, `light`, `no-preference` (or `AGENT_BROWSER_COLOR_SCHEME` env) |
| `--download-path ` | Default download directory (or `AGENT_BROWSER_DOWNLOAD_PATH` env) |
| `--content-boundaries` | Wrap page output in boundary markers for LLM safety (or `AGENT_BROWSER_CONTENT_BOUNDARIES` env) |
| `--max-output ` | Truncate page output to N characters (or `AGENT_BROWSER_MAX_OUTPUT` env) |
-| `--allowed-domains ` | Comma-separated allowed domain patterns (or `AGENT_BROWSER_ALLOWED_DOMAINS` env) |
-| `--action-policy ` | Path to action policy JSON file (or `AGENT_BROWSER_ACTION_POLICY` env) |
| `--confirm-actions ` | Action categories requiring confirmation (or `AGENT_BROWSER_CONFIRM_ACTIONS` env) |
| `--confirm-interactive` | Interactive confirmation prompts; auto-denies if stdin is not a TTY (or `AGENT_BROWSER_CONFIRM_INTERACTIVE` env) |
| `--engine ` | Browser engine: `chrome` (default), `lightpanda` (or `AGENT_BROWSER_ENGINE` env) |
@@ -601,33 +514,6 @@ This is useful for multimodal AI models that can reason about visual layout, unl
| `--config ` | Use a custom config file (or `AGENT_BROWSER_CONFIG` env) |
| `--debug` | Debug output |
-## Observability Dashboard
-
-Monitor agent-browser sessions in real time with a local web dashboard showing a live viewport and command activity feed.
-
-```bash
-# Install the dashboard (one time)
-agent-browser dashboard install
-
-# Start the dashboard server (runs in background on port 4848)
-agent-browser dashboard start
-agent-browser dashboard start --port 8080 # Custom port
-
-# All sessions are automatically visible in the dashboard
-agent-browser open example.com
-
-# Stop the dashboard
-agent-browser dashboard stop
-```
-
-The dashboard runs as a standalone background process on port 4848, independent of browser sessions. It stays available even when no sessions are running. All sessions automatically stream to the dashboard.
-
-The dashboard displays:
-- **Live viewport** -- real-time JPEG frames from the browser
-- **Activity feed** -- chronological command/result stream with timing and expandable details
-- **Console output** -- browser console messages (log, warn, error)
-- **Session creation** -- create new sessions from the UI with local engines (Chrome, Lightpanda) or cloud providers (Browserbase, Browserless, Browser Use, Kernel)
-
## Configuration
Create an `agent-browser.json` file to set persistent defaults instead of repeating flags on every command.
@@ -658,7 +544,7 @@ agent-browser --config ./ci-config.json open example.com
AGENT_BROWSER_CONFIG=./ci-config.json agent-browser open example.com
```
-All options from the table above can be set in the config file using camelCase keys (e.g., `--executable-path` becomes `"executablePath"`, `--proxy-bypass` becomes `"proxyBypass"`). Unknown keys are ignored for forward compatibility.
+All options from the table above can be set in the config file using camelCase keys (e.g., `--proxy-bypass` becomes `"proxyBypass"`). Unknown keys are ignored for forward compatibility. Note: `allowedDomains` and `actionPolicy` can only be set via config file (not CLI flags or env vars).
Boolean flags accept an optional `true`/`false` value to override config settings. For example, `--headed false` disables `"headed": true` from config. A bare `--headed` is equivalent to `--headed true`.
@@ -826,216 +712,6 @@ For global headers (all domains), use `set headers`:
agent-browser set headers '{"X-Custom-Header": "value"}'
```
-## Custom Browser Executable
-
-Use a custom browser executable instead of the bundled Chromium. This is useful for:
-
-- **Serverless deployment**: Use lightweight Chromium builds like `@sparticuz/chromium` (~50MB vs ~684MB)
-- **System browsers**: Use an existing Chrome/Chromium installation
-- **Custom builds**: Use modified browser builds
-
-### CLI Usage
-
-```bash
-# Via flag
-agent-browser --executable-path /path/to/chromium open example.com
-
-# Via environment variable
-AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com
-```
-
-### Serverless (Vercel)
-
-Run agent-browser + Chrome in an ephemeral Vercel Sandbox microVM. No external server needed:
-
-```typescript
-import { Sandbox } from "@vercel/sandbox";
-
-const sandbox = await Sandbox.create({ runtime: "node24" });
-await sandbox.runCommand("agent-browser", ["open", "https://example.com"]);
-const result = await sandbox.runCommand("agent-browser", ["screenshot", "--json"]);
-await sandbox.stop();
-```
-
-See the [environments example](examples/environments/) for a working demo with a UI and deploy-to-Vercel button.
-
-### Serverless (AWS Lambda)
-
-```typescript
-import chromium from '@sparticuz/chromium';
-import { execSync } from 'child_process';
-
-export async function handler() {
- const executablePath = await chromium.executablePath();
- const result = execSync(
- `AGENT_BROWSER_EXECUTABLE_PATH=${executablePath} agent-browser open https://example.com && agent-browser snapshot -i --json`,
- { encoding: 'utf-8' }
- );
- return JSON.parse(result);
-}
-```
-
-## Local Files
-
-Open and interact with local files (PDFs, HTML, etc.) using `file://` URLs:
-
-```bash
-# Enable file access (required for JavaScript to access local files)
-agent-browser --allow-file-access open file:///path/to/document.pdf
-agent-browser --allow-file-access open file:///path/to/page.html
-
-# Take screenshot of a local PDF
-agent-browser --allow-file-access open file:///Users/me/report.pdf
-agent-browser screenshot report.png
-```
-
-The `--allow-file-access` flag adds Chromium flags (`--allow-file-access-from-files`, `--allow-file-access`) that allow `file://` URLs to:
-
-- Load and render local files
-- Access other local files via JavaScript (XHR, fetch)
-- Load local resources (images, scripts, stylesheets)
-
-**Note:** This flag only works with Chromium. For security, it's disabled by default.
-
-## CDP Mode
-
-Connect to an existing browser via Chrome DevTools Protocol:
-
-```bash
-# Start Chrome with: google-chrome --remote-debugging-port=9222
-
-# Connect once, then run commands without --cdp
-agent-browser connect 9222
-agent-browser snapshot
-agent-browser tab
-agent-browser close
-
-# Or pass --cdp on each command
-agent-browser --cdp 9222 snapshot
-
-# Connect to remote browser via WebSocket URL
-agent-browser --cdp "wss://your-browser-service.com/cdp?token=..." snapshot
-```
-
-The `--cdp` flag accepts either:
-
-- A port number (e.g., `9222`) for local connections via `http://localhost:{port}`
-- A full WebSocket URL (e.g., `wss://...` or `ws://...`) for remote browser services
-
-This enables control of:
-
-- Electron apps
-- Chrome/Chromium instances with remote debugging
-- WebView2 applications
-- Any browser exposing a CDP endpoint
-
-### Auto-Connect
-
-Use `--auto-connect` to automatically discover and connect to a running Chrome instance without specifying a port:
-
-```bash
-# Auto-discover running Chrome with remote debugging
-agent-browser --auto-connect open example.com
-agent-browser --auto-connect snapshot
-
-# Or via environment variable
-AGENT_BROWSER_AUTO_CONNECT=1 agent-browser snapshot
-```
-
-Auto-connect discovers Chrome by:
-
-1. Reading Chrome's `DevToolsActivePort` file from the default user data directory
-2. Falling back to probing common debugging ports (9222, 9229)
-3. If HTTP-based discovery (`/json/version`, `/json/list`) fails, falling back to a direct WebSocket connection
-
-This is useful when:
-
-- Chrome 144+ has remote debugging enabled via `chrome://inspect/#remote-debugging` (which uses a dynamic port)
-- You want a zero-configuration connection to your existing browser
-- You don't want to track which port Chrome is using
-
-## Streaming (Browser Preview)
-
-Stream the browser viewport via WebSocket for live preview or "pair browsing" where a human can watch and interact alongside an AI agent.
-
-### Streaming
-
-Every session automatically starts a WebSocket stream server on an OS-assigned port. Use `stream status` to see the bound port and connection state:
-
-```bash
-agent-browser stream status
-```
-
-To bind to a specific port, set `AGENT_BROWSER_STREAM_PORT`:
-
-```bash
-AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com
-```
-
-You can also manage streaming at runtime with `stream enable`, `stream disable`, and `stream status`:
-
-```bash
-agent-browser stream enable --port 9223 # Re-enable on a specific port
-agent-browser stream disable # Stop streaming for the session
-```
-
-The WebSocket server streams the browser viewport and accepts input events.
-
-### WebSocket Protocol
-
-Connect to `ws://localhost:9223` to receive frames and send input:
-
-**Receive frames:**
-
-```json
-{
- "type": "frame",
- "data": "",
- "metadata": {
- "deviceWidth": 1280,
- "deviceHeight": 720,
- "pageScaleFactor": 1,
- "offsetTop": 0,
- "scrollOffsetX": 0,
- "scrollOffsetY": 0
- }
-}
-```
-
-**Send mouse events:**
-
-```json
-{
- "type": "input_mouse",
- "eventType": "mousePressed",
- "x": 100,
- "y": 200,
- "button": "left",
- "clickCount": 1
-}
-```
-
-**Send keyboard events:**
-
-```json
-{
- "type": "input_keyboard",
- "eventType": "keyDown",
- "key": "Enter",
- "code": "Enter"
-}
-```
-
-**Send touch events:**
-
-```json
-{
- "type": "input_touch",
- "eventType": "touchStart",
- "touchPoints": [{ "x": 100, "y": 200 }]
-}
-```
-
## Architecture
agent-browser uses a client-daemon architecture:
@@ -1074,7 +750,7 @@ The `--help` output is comprehensive and most agents can figure it out from ther
Add the skill to your AI coding assistant for richer context:
```bash
-npx skills add vercel-labs/agent-browser
+npx skills add Leonardo-Interactive/agent-browser
```
This works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, OpenCode, and Windsurf. The skill is fetched from the repository, so it stays up to date automatically -- do not copy `SKILL.md` from `node_modules` as it will become stale.
@@ -1084,7 +760,7 @@ This works with Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Goose, O
Install as a Claude Code skill:
```bash
-npx skills add vercel-labs/agent-browser
+npx skills add Leonardo-Interactive/agent-browser
```
This adds the skill to `.claude/skills/agent-browser/SKILL.md` in your project. The skill teaches Claude Code the full agent-browser workflow, including the snapshot-ref interaction pattern, session management, and timeout handling.
diff --git a/cli/Cargo.lock b/cli/Cargo.lock
index 60efae21d..3da604b26 100644
--- a/cli/Cargo.lock
+++ b/cli/Cargo.lock
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser"
-version = "0.23.4"
+version = "0.23.4-leonardo.1"
dependencies = [
"aes-gcm",
"async-trait",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index e239b8cce..add963f75 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,11 +1,11 @@
[package]
name = "agent-browser"
-version = "0.23.4"
+version = "0.23.4-leonardo.1"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
-repository = "https://github.com/vercel-labs/agent-browser"
-homepage = "https://agent-browser.dev"
+repository = "https://github.com/Leonardo-Interactive/agent-browser"
+homepage = "https://github.com/Leonardo-Interactive/agent-browser"
readme = "../README.md"
keywords = ["browser", "automation", "ai", "cdp", "chrome"]
categories = ["command-line-utilities", "web-programming"]
diff --git a/cli/src/commands.rs b/cli/src/commands.rs
index 661595c1c..11b67cbd8 100644
--- a/cli/src/commands.rs
+++ b/cli/src/commands.rs
@@ -1,6 +1,4 @@
-use base64::{engine::general_purpose::STANDARD, Engine};
use serde_json::{json, Value};
-use std::io::{self, BufRead};
use crate::color;
use crate::flags::Flags;
@@ -91,6 +89,13 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result {
+ Err(ParseError::UnknownCommand {
+ command: cmd.to_string(),
+ })
+ }
+
// === Navigation ===
// Maps to "navigate" action in protocol; reflected in ACTION_CATEGORIES in action-policy.ts
"open" | "goto" | "navigate" => {
@@ -112,9 +117,6 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result(headers_json).map_err(|_| {
@@ -125,12 +127,6 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "back" })),
@@ -559,183 +555,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result {
- // Check for flags: -b/--base64 or --stdin
- let (is_base64, is_stdin, script_parts): (bool, bool, &[&str]) =
- if rest.first() == Some(&"-b") || rest.first() == Some(&"--base64") {
- (true, false, &rest[1..])
- } else if rest.first() == Some(&"--stdin") {
- (false, true, &rest[1..])
- } else {
- (false, false, rest.as_slice())
- };
-
- let script = if is_stdin {
- // Read script from stdin
- let stdin = io::stdin();
- let lines: Vec = stdin
- .lock()
- .lines()
- .map(|l| l.unwrap_or_default())
- .collect();
- lines.join("\n")
- } else {
- let raw_script = script_parts.join(" ");
- if is_base64 {
- let decoded =
- STANDARD
- .decode(&raw_script)
- .map_err(|_| ParseError::InvalidValue {
- message: "Invalid base64 encoding".to_string(),
- usage: "eval -b ",
- })?;
- String::from_utf8(decoded).map_err(|_| ParseError::InvalidValue {
- message: "Base64 decoded to invalid UTF-8".to_string(),
- usage: "eval -b ",
- })?
- } else {
- raw_script
- }
- };
- Ok(json!({ "id": id, "action": "evaluate", "script": script }))
- }
-
// === Close ===
"close" | "quit" | "exit" => Ok(json!({ "id": id, "action": "close" })),
- // === Inspect ===
- "inspect" => Ok(json!({ "id": id, "action": "inspect" })),
-
- // === Authentication Vault ===
- "auth" => {
- let sub = rest.first().map(|s| s.as_ref());
- match sub {
- Some("save") => {
- let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
- context: "auth save".to_string(),
- usage: "agent-browser auth save --url --username --password ",
- })?;
-
- let mut url = None;
- let mut username = None;
- let mut password = None;
- let mut password_stdin = false;
- let mut username_selector = None;
- let mut password_selector = None;
- let mut submit_selector = None;
-
- let mut j = 2;
- while j < rest.len() {
- match rest[j] {
- "--url" => {
- url = rest.get(j + 1).cloned();
- j += 1;
- }
- "--username" => {
- username = rest.get(j + 1).cloned();
- j += 1;
- }
- "--password" => {
- password = rest.get(j + 1).cloned();
- j += 1;
- }
- "--password-stdin" => {
- password_stdin = true;
- }
- "--username-selector" => {
- username_selector = rest.get(j + 1).cloned();
- j += 1;
- }
- "--password-selector" => {
- password_selector = rest.get(j + 1).cloned();
- j += 1;
- }
- "--submit-selector" => {
- submit_selector = rest.get(j + 1).cloned();
- j += 1;
- }
- other => {
- if other.starts_with("--") {
- return Err(ParseError::InvalidValue {
- message: format!("unknown flag '{}' for auth save", other),
- usage: "agent-browser auth save --url --username --password ",
- });
- }
- }
- }
- j += 1;
- }
-
- let url_val = url.ok_or_else(|| ParseError::MissingArguments {
- context: "auth save".to_string(),
- usage: "agent-browser auth save --url --username --password [--password-stdin]",
- })?;
- let user_val = username.ok_or_else(|| ParseError::MissingArguments {
- context: "auth save".to_string(),
- usage: "agent-browser auth save --url --username --password [--password-stdin]",
- })?;
-
- if !password_stdin && password.is_none() {
- return Err(ParseError::MissingArguments {
- context: "auth save".to_string(),
- usage: "agent-browser auth save --url --username --password [--password-stdin]",
- });
- }
-
- let mut cmd = json!({
- "id": id,
- "action": "auth_save",
- "name": name,
- "url": url_val,
- "username": user_val,
- });
- if password_stdin {
- cmd["passwordStdin"] = json!(true);
- }
- if let Some(pass_val) = password {
- cmd["password"] = json!(pass_val);
- }
- if let Some(us) = username_selector {
- cmd["usernameSelector"] = json!(us);
- }
- if let Some(ps) = password_selector {
- cmd["passwordSelector"] = json!(ps);
- }
- if let Some(ss) = submit_selector {
- cmd["submitSelector"] = json!(ss);
- }
- Ok(cmd)
- }
- Some("login") => {
- let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
- context: "auth login".to_string(),
- usage: "agent-browser auth login ",
- })?;
- Ok(json!({ "id": id, "action": "auth_login", "name": name }))
- }
- Some("list") => Ok(json!({ "id": id, "action": "auth_list" })),
- Some("delete") | Some("remove") => {
- let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
- context: "auth delete".to_string(),
- usage: "agent-browser auth delete ",
- })?;
- Ok(json!({ "id": id, "action": "auth_delete", "name": name }))
- }
- Some("show") => {
- let name = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
- context: "auth show".to_string(),
- usage: "agent-browser auth show ",
- })?;
- Ok(json!({ "id": id, "action": "auth_show", "name": name }))
- }
- _ => Err(ParseError::UnknownSubcommand {
- subcommand: sub.unwrap_or("(none)").to_string(),
- valid_options: &["save", "login", "list", "delete", "show"],
- }),
- }
- }
-
// === Action Confirmation ===
"confirm" => {
let cid = rest.first().ok_or_else(|| ParseError::MissingArguments {
@@ -752,108 +574,6 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result {
- let endpoint = rest.first().ok_or_else(|| ParseError::MissingArguments {
- context: "connect".to_string(),
- usage: "connect ",
- })?;
- // Check if it's a URL (ws://, wss://, http://, https://)
- if endpoint.starts_with("ws://")
- || endpoint.starts_with("wss://")
- || endpoint.starts_with("http://")
- || endpoint.starts_with("https://")
- {
- Ok(json!({ "id": id, "action": "launch", "cdpUrl": endpoint }))
- } else {
- // It's a port number - validate and use cdpPort field
- let port: u16 = match endpoint.parse::() {
- Ok(0) => {
- return Err(ParseError::InvalidValue {
- message: "Invalid port: port must be greater than 0".to_string(),
- usage: "connect ",
- });
- }
- Ok(p) if p > 65535 => {
- return Err(ParseError::InvalidValue {
- message: format!(
- "Invalid port: {} is out of range (valid range: 1-65535)",
- p
- ),
- usage: "connect ",
- });
- }
- Ok(p) => p as u16,
- Err(_) => {
- return Err(ParseError::InvalidValue {
- message: format!(
- "Invalid value: '{}' is not a valid port number or URL",
- endpoint
- ),
- usage: "connect ",
- });
- }
- };
- Ok(json!({ "id": id, "action": "launch", "cdpPort": port }))
- }
- }
-
- // === Runtime stream control ===
- "stream" => match rest.first().copied() {
- Some("enable") => {
- let mut cmd = json!({ "id": id, "action": "stream_enable" });
- let mut i = 1;
- while i < rest.len() {
- match rest[i] {
- "--port" => {
- let value =
- rest.get(i + 1)
- .ok_or_else(|| ParseError::MissingArguments {
- context: "stream enable --port".to_string(),
- usage: "stream enable [--port ]",
- })?;
- let port =
- value.parse::().map_err(|_| ParseError::InvalidValue {
- message: format!(
- "Invalid port: '{}' is not a valid integer",
- value
- ),
- usage: "stream enable [--port ]",
- })?;
- if port > u16::MAX as u32 {
- return Err(ParseError::InvalidValue {
- message: format!(
- "Invalid port: {} is out of range (valid range: 0-65535)",
- port
- ),
- usage: "stream enable [--port ]",
- });
- }
- cmd["port"] = json!(port);
- i += 2;
- }
- flag => {
- return Err(ParseError::InvalidValue {
- message: format!("Unknown flag for stream enable: {}", flag),
- usage: "stream enable [--port ]",
- });
- }
- }
- }
- Ok(cmd)
- }
- Some("disable") => Ok(json!({ "id": id, "action": "stream_disable" })),
- Some("status") => Ok(json!({ "id": id, "action": "stream_status" })),
- Some(sub) => Err(ParseError::UnknownSubcommand {
- subcommand: sub.to_string(),
- valid_options: &["enable", "disable", "status"],
- }),
- None => Err(ParseError::MissingArguments {
- context: "stream".to_string(),
- usage: "stream ",
- }),
- },
-
// === Get ===
"get" => parse_get(&rest, &id),
@@ -1202,27 +922,6 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result match rest.first().copied() {
- Some("read") | None => {
- Ok(json!({ "id": id, "action": "clipboard", "operation": "read" }))
- }
- Some("write") => {
- rest.get(1).ok_or_else(|| ParseError::MissingArguments {
- context: "clipboard write".to_string(),
- usage: "clipboard write ",
- })?;
- let text = rest[1..].join(" ");
- Ok(json!({ "id": id, "action": "clipboard", "operation": "write", "text": text }))
- }
- Some("copy") => Ok(json!({ "id": id, "action": "clipboard", "operation": "copy" })),
- Some("paste") => Ok(json!({ "id": id, "action": "clipboard", "operation": "paste" })),
- Some(sub) => Err(ParseError::UnknownSubcommand {
- subcommand: sub.to_string(),
- valid_options: &["read", "write", "copy", "paste"],
- }),
- },
-
// === State ===
"state" => {
const VALID: &[&str] = &["save", "load", "list", "clear", "show", "clean", "rename"];
@@ -2007,8 +1706,6 @@ fn parse_set(rest: &[&str], id: &str) -> Result {
"geolocation",
"offline",
"headers",
- "credentials",
- "auth",
"media",
];
@@ -2096,17 +1793,6 @@ fn parse_set(rest: &[&str], id: &str) -> Result {
})?;
Ok(json!({ "id": id, "action": "headers", "headers": headers }))
}
- Some("credentials") | Some("auth") => {
- let user = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
- context: "set credentials".to_string(),
- usage: "set credentials ",
- })?;
- let pass = rest.get(2).ok_or_else(|| ParseError::MissingArguments {
- context: "set credentials".to_string(),
- usage: "set credentials ",
- })?;
- Ok(json!({ "id": id, "action": "credentials", "username": user, "password": pass }))
- }
Some("media") => {
let color = if rest.contains(&"dark") {
"dark"
@@ -2283,30 +1969,21 @@ mod tests {
headed: false,
debug: false,
headers: None,
- executable_path: None,
- extensions: Vec::new(),
- cdp: None,
profile: None,
state: None,
proxy: None,
proxy_bypass: None,
args: None,
user_agent: None,
- provider: None,
ignore_https_errors: false,
- allow_file_access: false,
device: None,
- auto_connect: false,
session_name: None,
- cli_executable_path: false,
- cli_extensions: false,
cli_profile: false,
cli_state: false,
cli_args: false,
cli_user_agent: false,
cli_proxy: false,
cli_proxy_bypass: false,
- cli_allow_file_access: false,
cli_annotate: false,
cli_download_path: false,
cli_headed: false,
@@ -3056,50 +2733,6 @@ mod tests {
// === Clipboard Tests ===
- #[test]
- fn test_clipboard_read_default() {
- let cmd = parse_command(&args("clipboard"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "clipboard");
- assert_eq!(cmd["operation"], "read");
- }
-
- #[test]
- fn test_clipboard_read_explicit() {
- let cmd = parse_command(&args("clipboard read"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "clipboard");
- assert_eq!(cmd["operation"], "read");
- }
-
- #[test]
- fn test_clipboard_write() {
- let cmd = parse_command(&args("clipboard write hello"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "clipboard");
- assert_eq!(cmd["operation"], "write");
- assert_eq!(cmd["text"], "hello");
- }
-
- #[test]
- fn test_clipboard_write_multi_word() {
- let cmd = parse_command(&args("clipboard write hello world"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "clipboard");
- assert_eq!(cmd["operation"], "write");
- assert_eq!(cmd["text"], "hello world");
- }
-
- #[test]
- fn test_clipboard_copy() {
- let cmd = parse_command(&args("clipboard copy"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "clipboard");
- assert_eq!(cmd["operation"], "copy");
- }
-
- #[test]
- fn test_clipboard_paste() {
- let cmd = parse_command(&args("clipboard paste"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "clipboard");
- assert_eq!(cmd["operation"], "paste");
- }
-
#[test]
fn test_clipboard_write_missing_text() {
let result = parse_command(&args("clipboard write"), &default_flags());
@@ -3295,54 +2928,6 @@ mod tests {
// === Eval Tests ===
- #[test]
- fn test_eval_basic() {
- let cmd = parse_command(&args("eval document.title"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "evaluate");
- assert_eq!(cmd["script"], "document.title");
- }
-
- #[test]
- fn test_eval_base64_short_flag() {
- // "document.title" in base64
- let cmd = parse_command(&args("eval -b ZG9jdW1lbnQudGl0bGU="), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "evaluate");
- assert_eq!(cmd["script"], "document.title");
- }
-
- #[test]
- fn test_eval_base64_long_flag() {
- // "document.title" in base64
- let cmd = parse_command(
- &args("eval --base64 ZG9jdW1lbnQudGl0bGU="),
- &default_flags(),
- )
- .unwrap();
- assert_eq!(cmd["action"], "evaluate");
- assert_eq!(cmd["script"], "document.title");
- }
-
- #[test]
- fn test_eval_base64_with_special_chars() {
- // "document.querySelector('[src*=\"_next\"]')" in base64
- let cmd = parse_command(
- &args("eval -b ZG9jdW1lbnQucXVlcnlTZWxlY3RvcignW3NyYyo9Il9uZXh0Il0nKQ=="),
- &default_flags(),
- )
- .unwrap();
- assert_eq!(cmd["action"], "evaluate");
- assert_eq!(cmd["script"], "document.querySelector('[src*=\"_next\"]')");
- }
-
- #[test]
- fn test_eval_base64_invalid() {
- let result = parse_command(&args("eval -b !!!invalid!!!"), &default_flags());
- assert!(result.is_err());
- let err = result.unwrap_err();
- assert!(matches!(err, ParseError::InvalidValue { .. }));
- assert!(err.format().contains("Invalid base64"));
- }
-
#[test]
fn test_unknown_command() {
let result = parse_command(&args("unknowncommand"), &default_flags());
@@ -3580,142 +3165,8 @@ mod tests {
// === Connect (CDP) tests ===
- #[test]
- fn test_connect_with_port() {
- let cmd = parse_command(&args("connect 9222"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "launch");
- assert_eq!(cmd["cdpPort"], 9222);
- assert!(cmd.get("cdpUrl").is_none());
- }
-
- #[test]
- fn test_connect_with_ws_url() {
- let input: Vec = vec![
- "connect".to_string(),
- "ws://localhost:9222/devtools/browser/abc123".to_string(),
- ];
- let cmd = parse_command(&input, &default_flags()).unwrap();
- assert_eq!(cmd["action"], "launch");
- assert_eq!(cmd["cdpUrl"], "ws://localhost:9222/devtools/browser/abc123");
- assert!(cmd.get("cdpPort").is_none());
- }
-
- #[test]
- fn test_connect_with_wss_url() {
- let input: Vec = vec![
- "connect".to_string(),
- "wss://remote-browser.example.com/cdp?token=xyz".to_string(),
- ];
- let cmd = parse_command(&input, &default_flags()).unwrap();
- assert_eq!(cmd["action"], "launch");
- assert_eq!(
- cmd["cdpUrl"],
- "wss://remote-browser.example.com/cdp?token=xyz"
- );
- assert!(cmd.get("cdpPort").is_none());
- }
-
- #[test]
- fn test_connect_with_http_url() {
- let input: Vec = vec!["connect".to_string(), "http://localhost:9222".to_string()];
- let cmd = parse_command(&input, &default_flags()).unwrap();
- assert_eq!(cmd["action"], "launch");
- assert_eq!(cmd["cdpUrl"], "http://localhost:9222");
- assert!(cmd.get("cdpPort").is_none());
- }
-
- #[test]
- fn test_connect_missing_argument() {
- let result = parse_command(&args("connect"), &default_flags());
- assert!(result.is_err());
- assert!(matches!(
- result.unwrap_err(),
- ParseError::MissingArguments { .. }
- ));
- }
-
- #[test]
- fn test_connect_invalid_port() {
- let result = parse_command(&args("connect notanumber"), &default_flags());
- assert!(result.is_err());
- let err = result.unwrap_err();
- assert!(matches!(err, ParseError::InvalidValue { .. }));
- assert!(err.format().contains("not a valid port number or URL"));
- }
-
- #[test]
- fn test_connect_port_zero() {
- let result = parse_command(&args("connect 0"), &default_flags());
- assert!(result.is_err());
- let err = result.unwrap_err();
- assert!(matches!(err, ParseError::InvalidValue { .. }));
- assert!(err.format().contains("port must be greater than 0"));
- }
-
- #[test]
- fn test_connect_port_out_of_range() {
- let result = parse_command(&args("connect 65536"), &default_flags());
- assert!(result.is_err());
- let err = result.unwrap_err();
- assert!(matches!(err, ParseError::InvalidValue { .. }));
- assert!(err.format().contains("out of range"));
- assert!(err.format().contains("1-65535"));
- }
-
- #[test]
- fn test_connect_port_max_valid() {
- let cmd = parse_command(&args("connect 65535"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "launch");
- assert_eq!(cmd["cdpPort"], 65535);
- }
-
- #[test]
- fn test_connect_port_min_valid() {
- let cmd = parse_command(&args("connect 1"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "launch");
- assert_eq!(cmd["cdpPort"], 1);
- }
-
// === Runtime stream control tests ===
- #[test]
- fn test_stream_enable_auto_port() {
- let cmd = parse_command(&args("stream enable"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "stream_enable");
- assert!(cmd.get("port").is_none());
- }
-
- #[test]
- fn test_stream_enable_with_port() {
- let cmd = parse_command(&args("stream enable --port 9223"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "stream_enable");
- assert_eq!(cmd["port"], 9223);
- }
-
- #[test]
- fn test_stream_status() {
- let cmd = parse_command(&args("stream status"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "stream_status");
- }
-
- #[test]
- fn test_stream_disable() {
- let cmd = parse_command(&args("stream disable"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "stream_disable");
- }
-
- #[test]
- fn test_stream_enable_invalid_port() {
- let result = parse_command(&args("stream enable --port abc"), &default_flags());
- assert!(matches!(result, Err(ParseError::InvalidValue { .. })));
- }
-
- #[test]
- fn test_stream_missing_subcommand() {
- let result = parse_command(&args("stream"), &default_flags());
- assert!(matches!(result, Err(ParseError::MissingArguments { .. })));
- }
-
// === Trace Tests ===
#[test]
@@ -4244,12 +3695,6 @@ mod tests {
// === Inspect / CDP URL ===
- #[test]
- fn test_inspect() {
- let cmd = parse_command(&args("inspect"), &default_flags()).unwrap();
- assert_eq!(cmd["action"], "inspect");
- }
-
#[test]
fn test_get_cdp_url() {
let cmd = parse_command(&args("get cdp-url"), &default_flags()).unwrap();
diff --git a/cli/src/connection.rs b/cli/src/connection.rs
index d532943cc..dc4284b96 100644
--- a/cli/src/connection.rs
+++ b/cli/src/connection.rs
@@ -196,8 +196,6 @@ pub struct DaemonResult {
pub struct DaemonOptions<'a> {
pub headed: bool,
pub debug: bool,
- pub executable_path: Option<&'a str>,
- pub extensions: &'a [String],
pub args: Option<&'a str>,
pub user_agent: Option<&'a str>,
pub proxy: Option<&'a str>,
@@ -205,10 +203,8 @@ pub struct DaemonOptions<'a> {
pub proxy_username: Option<&'a str>,
pub proxy_password: Option<&'a str>,
pub ignore_https_errors: bool,
- pub allow_file_access: bool,
pub profile: Option<&'a str>,
pub state: Option<&'a str>,
- pub provider: Option<&'a str>,
pub device: Option<&'a str>,
pub session_name: Option<&'a str>,
pub download_path: Option<&'a str>,
@@ -216,9 +212,7 @@ pub struct DaemonOptions<'a> {
pub action_policy: Option<&'a str>,
pub confirm_actions: Option<&'a str>,
pub engine: Option<&'a str>,
- pub auto_connect: bool,
pub idle_timeout: Option<&'a str>,
- pub cdp: Option<&'a str>,
pub no_auto_dialog: bool,
}
@@ -232,12 +226,6 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) {
if opts.debug {
cmd.env("AGENT_BROWSER_DEBUG", "1");
}
- if let Some(path) = opts.executable_path {
- cmd.env("AGENT_BROWSER_EXECUTABLE_PATH", path);
- }
- if !opts.extensions.is_empty() {
- cmd.env("AGENT_BROWSER_EXTENSIONS", opts.extensions.join(","));
- }
if let Some(a) = opts.args {
cmd.env("AGENT_BROWSER_ARGS", a);
}
@@ -259,18 +247,12 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) {
if opts.ignore_https_errors {
cmd.env("AGENT_BROWSER_IGNORE_HTTPS_ERRORS", "1");
}
- if opts.allow_file_access {
- cmd.env("AGENT_BROWSER_ALLOW_FILE_ACCESS", "1");
- }
if let Some(prof) = opts.profile {
cmd.env("AGENT_BROWSER_PROFILE", prof);
}
if let Some(st) = opts.state {
cmd.env("AGENT_BROWSER_STATE", st);
}
- if let Some(p) = opts.provider {
- cmd.env("AGENT_BROWSER_PROVIDER", p);
- }
if let Some(d) = opts.device {
cmd.env("AGENT_BROWSER_IOS_DEVICE", d);
}
@@ -292,15 +274,9 @@ fn apply_daemon_env(cmd: &mut Command, session: &str, opts: &DaemonOptions) {
if let Some(engine) = opts.engine {
cmd.env("AGENT_BROWSER_ENGINE", engine);
}
- if opts.auto_connect {
- cmd.env("AGENT_BROWSER_AUTO_CONNECT", "1");
- }
if let Some(idle) = opts.idle_timeout {
cmd.env("AGENT_BROWSER_IDLE_TIMEOUT_MS", idle);
}
- if let Some(cdp) = opts.cdp {
- cmd.env("AGENT_BROWSER_CDP", cdp);
- }
if opts.no_auto_dialog {
cmd.env("AGENT_BROWSER_NO_AUTO_DIALOG", "1");
}
diff --git a/cli/src/flags.rs b/cli/src/flags.rs
index 806d4e88a..1ef4a4dda 100644
--- a/cli/src/flags.rs
+++ b/cli/src/flags.rs
@@ -271,20 +271,14 @@ pub struct Flags {
pub debug: bool,
pub session: String,
pub headers: Option,
- pub executable_path: Option,
- pub cdp: Option,
- pub extensions: Vec,
pub profile: Option,
pub state: Option,
pub proxy: Option,
pub proxy_bypass: Option,
pub args: Option,
pub user_agent: Option,
- pub provider: Option,
pub ignore_https_errors: bool,
- pub allow_file_access: bool,
pub device: Option,
- pub auto_connect: bool,
pub session_name: Option,
pub annotate: bool,
pub color_scheme: Option,
@@ -304,15 +298,12 @@ pub struct Flags {
// Track which launch-time options were explicitly passed via CLI
// (as opposed to being set only via environment variables)
- pub cli_executable_path: bool,
- pub cli_extensions: bool,
pub cli_profile: bool,
pub cli_state: bool,
pub cli_args: bool,
pub cli_user_agent: bool,
pub cli_proxy: bool,
pub cli_proxy_bypass: bool,
- pub cli_allow_file_access: bool,
pub cli_annotate: bool,
pub cli_download_path: bool,
pub cli_headed: bool,
@@ -324,22 +315,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
std::process::exit(1);
});
- let extensions_env = env::var("AGENT_BROWSER_EXTENSIONS")
- .ok()
- .map(|s| {
- s.split(',')
- .map(|p| p.trim().to_string())
- .filter(|p| !p.is_empty())
- .collect::>()
- })
- .unwrap_or_default();
-
- let extensions = if !extensions_env.is_empty() {
- extensions_env
- } else {
- config.extensions.unwrap_or_default()
- };
-
let mut flags = Flags {
json: env_var_is_truthy("AGENT_BROWSER_JSON") || config.json.unwrap_or(false),
headed: env_var_is_truthy("AGENT_BROWSER_HEADED") || config.headed.unwrap_or(false),
@@ -349,11 +324,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
.or(config.session)
.unwrap_or_else(|| "default".to_string()),
headers: config.headers,
- executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH")
- .ok()
- .or(config.executable_path),
- cdp: config.cdp,
- extensions,
profile: env::var("AGENT_BROWSER_PROFILE").ok().or(config.profile),
state: env::var("AGENT_BROWSER_STATE").ok().or(config.state),
proxy: env::var("AGENT_BROWSER_PROXY")
@@ -374,14 +344,9 @@ pub fn parse_flags(args: &[String]) -> Flags {
user_agent: env::var("AGENT_BROWSER_USER_AGENT")
.ok()
.or(config.user_agent),
- provider: env::var("AGENT_BROWSER_PROVIDER").ok().or(config.provider),
ignore_https_errors: env_var_is_truthy("AGENT_BROWSER_IGNORE_HTTPS_ERRORS")
|| config.ignore_https_errors.unwrap_or(false),
- allow_file_access: env_var_is_truthy("AGENT_BROWSER_ALLOW_FILE_ACCESS")
- || config.allow_file_access.unwrap_or(false),
device: env::var("AGENT_BROWSER_IOS_DEVICE").ok().or(config.device),
- auto_connect: env_var_is_truthy("AGENT_BROWSER_AUTO_CONNECT")
- || config.auto_connect.unwrap_or(false),
session_name: env::var("AGENT_BROWSER_SESSION_NAME")
.ok()
.or(config.session_name),
@@ -398,18 +363,8 @@ pub fn parse_flags(args: &[String]) -> Flags {
.ok()
.and_then(|s| s.parse().ok())
.or(config.max_output),
- allowed_domains: env::var("AGENT_BROWSER_ALLOWED_DOMAINS")
- .ok()
- .map(|s| {
- s.split(',')
- .map(|d| d.trim().to_lowercase())
- .filter(|d| !d.is_empty())
- .collect()
- })
- .or(config.allowed_domains),
- action_policy: env::var("AGENT_BROWSER_ACTION_POLICY")
- .ok()
- .or(config.action_policy),
+ allowed_domains: config.allowed_domains,
+ action_policy: config.action_policy,
confirm_actions: env::var("AGENT_BROWSER_CONFIRM_ACTIONS")
.ok()
.or(config.confirm_actions),
@@ -434,15 +389,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
.or(config.idle_timeout),
no_auto_dialog: env_var_is_truthy("AGENT_BROWSER_NO_AUTO_DIALOG")
|| config.no_auto_dialog.unwrap_or(false),
- cli_executable_path: false,
- cli_extensions: false,
cli_profile: false,
cli_state: false,
cli_args: false,
cli_user_agent: false,
cli_proxy: false,
cli_proxy_bypass: false,
- cli_allow_file_access: false,
cli_annotate: false,
cli_download_path: false,
cli_headed: false,
@@ -498,26 +450,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
- "--executable-path" => {
- if let Some(s) = args.get(i + 1) {
- flags.executable_path = Some(s.clone());
- flags.cli_executable_path = true;
- i += 1;
- }
- }
- "--extension" => {
- if let Some(s) = args.get(i + 1) {
- flags.extensions.push(s.clone());
- flags.cli_extensions = true;
- i += 1;
- }
- }
- "--cdp" => {
- if let Some(s) = args.get(i + 1) {
- flags.cdp = Some(s.clone());
- i += 1;
- }
- }
"--profile" => {
if let Some(s) = args.get(i + 1) {
flags.profile = Some(s.clone());
@@ -560,12 +492,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
- "-p" | "--provider" => {
- if let Some(p) = args.get(i + 1) {
- flags.provider = Some(p.clone());
- i += 1;
- }
- }
"--ignore-https-errors" => {
let (val, consumed) = parse_bool_arg(args, i);
flags.ignore_https_errors = val;
@@ -573,27 +499,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
- "--allow-file-access" => {
- let (val, consumed) = parse_bool_arg(args, i);
- flags.allow_file_access = val;
- flags.cli_allow_file_access = true;
- if consumed {
- i += 1;
- }
- }
"--device" => {
if let Some(d) = args.get(i + 1) {
flags.device = Some(d.clone());
i += 1;
}
}
- "--auto-connect" => {
- let (val, consumed) = parse_bool_arg(args, i);
- flags.auto_connect = val;
- if consumed {
- i += 1;
- }
- }
"--session-name" => {
if let Some(s) = args.get(i + 1) {
flags.session_name = Some(s.clone());
@@ -636,23 +547,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
- "--allowed-domains" => {
- if let Some(s) = args.get(i + 1) {
- flags.allowed_domains = Some(
- s.split(',')
- .map(|d| d.trim().to_lowercase())
- .filter(|d| !d.is_empty())
- .collect(),
- );
- i += 1;
- }
- }
- "--action-policy" => {
- if let Some(s) = args.get(i + 1) {
- flags.action_policy = Some(s.clone());
- i += 1;
- }
- }
"--confirm-actions" => {
if let Some(s) = args.get(i + 1) {
flags.confirm_actions = Some(s.clone());
@@ -911,20 +805,6 @@ mod tests {
assert_eq!(clean, vec!["open", "example.com"]);
}
- #[test]
- fn test_parse_executable_path_flag() {
- let flags = parse_flags(&args(
- "--executable-path /path/to/chromium open example.com",
- ));
- assert_eq!(flags.executable_path, Some("/path/to/chromium".to_string()));
- }
-
- #[test]
- fn test_parse_executable_path_flag_no_value() {
- let flags = parse_flags(&args("--executable-path"));
- assert_eq!(flags.executable_path, None);
- }
-
#[test]
fn test_clean_args_removes_executable_path() {
let cleaned = clean_args(&args(
@@ -953,37 +833,6 @@ mod tests {
assert_eq!(flags.idle_timeout.as_deref(), Some("10000"));
}
- #[test]
- fn test_parse_flags_with_session_and_executable_path() {
- let flags = parse_flags(&args(
- "--session test --executable-path /custom/chrome open example.com",
- ));
- assert_eq!(flags.session, "test");
- assert_eq!(flags.executable_path, Some("/custom/chrome".to_string()));
- }
-
- #[test]
- fn test_cli_executable_path_tracking() {
- // When --executable-path is passed via CLI, cli_executable_path should be true
- let flags = parse_flags(&args("--executable-path /path/to/chrome snapshot"));
- assert!(flags.cli_executable_path);
- assert_eq!(flags.executable_path, Some("/path/to/chrome".to_string()));
- }
-
- #[test]
- fn test_cli_executable_path_not_set_without_flag() {
- // When no --executable-path is passed, cli_executable_path should be false
- // (even if env var sets executable_path to Some value, which we can't test here)
- let flags = parse_flags(&args("snapshot"));
- assert!(!flags.cli_executable_path);
- }
-
- #[test]
- fn test_cli_extension_tracking() {
- let flags = parse_flags(&args("--extension /path/to/ext snapshot"));
- assert!(flags.cli_extensions);
- }
-
#[test]
fn test_cli_profile_tracking() {
let flags = parse_flags(&args("--profile /path/to/profile snapshot"));
@@ -1018,13 +867,9 @@ mod tests {
#[test]
fn test_cli_multiple_flags_tracking() {
- let flags = parse_flags(&args(
- "--executable-path /chrome --profile /profile --proxy http://proxy snapshot",
- ));
- assert!(flags.cli_executable_path);
+ let flags = parse_flags(&args("--profile /profile --proxy http://proxy snapshot"));
assert!(flags.cli_profile);
assert!(flags.cli_proxy);
- assert!(!flags.cli_extensions);
assert!(!flags.cli_state);
}
@@ -1319,19 +1164,6 @@ mod tests {
assert!(!flags.ignore_https_errors);
}
- #[test]
- fn test_allow_file_access_false() {
- let flags = parse_flags(&args("--allow-file-access false open"));
- assert!(!flags.allow_file_access);
- assert!(flags.cli_allow_file_access);
- }
-
- #[test]
- fn test_auto_connect_false() {
- let flags = parse_flags(&args("--auto-connect false open"));
- assert!(!flags.auto_connect);
- }
-
#[test]
fn test_clean_args_removes_bool_flag_with_value() {
let cleaned = clean_args(&args("--headed false --debug true open example.com"));
diff --git a/cli/src/install.rs b/cli/src/install.rs
index 7ad365143..9c04f358c 100644
--- a/cli/src/install.rs
+++ b/cli/src/install.rs
@@ -713,113 +713,3 @@ pub fn get_dashboard_dir() -> PathBuf {
.join(".agent-browser")
.join("dashboard")
}
-
-const DASHBOARD_VERSION: &str = env!("CARGO_PKG_VERSION");
-
-fn dashboard_download_url() -> String {
- format!(
- "https://github.com/vercel-labs/agent-browser/releases/download/v{}/dashboard.zip",
- DASHBOARD_VERSION
- )
-}
-
-pub fn run_dashboard_install() {
- println!("{}", color::cyan("Installing dashboard..."));
-
- let dest = get_dashboard_dir();
-
- if dest.join("index.html").exists() {
- println!(
- "{} Dashboard is already installed at {}",
- color::success_indicator(),
- dest.display()
- );
- return;
- }
-
- let url = dashboard_download_url();
- println!(" Downloading dashboard v{}", DASHBOARD_VERSION);
- println!(" {}", url);
-
- let rt = tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()
- .unwrap_or_else(|e| {
- eprintln!(
- "{} Failed to create runtime: {}",
- color::error_indicator(),
- e
- );
- exit(1);
- });
-
- let bytes = match rt.block_on(download_bytes(&url)) {
- Ok(b) => b,
- Err(e) => {
- eprintln!("{} {}", color::error_indicator(), e);
- eprintln!(" The dashboard may not be available for this version yet.");
- eprintln!(" You can build it locally: cd packages/dashboard && pnpm build");
- exit(1);
- }
- };
-
- match extract_dashboard_zip(bytes, &dest) {
- Ok(()) => {
- println!(
- "{} Dashboard v{} installed successfully",
- color::success_indicator(),
- DASHBOARD_VERSION
- );
- println!(" Location: {}", dest.display());
- }
- Err(e) => {
- let _ = fs::remove_dir_all(&dest);
- eprintln!("{} {}", color::error_indicator(), e);
- exit(1);
- }
- }
-}
-
-fn extract_dashboard_zip(bytes: Vec, dest: &Path) -> Result<(), String> {
- fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
-
- let cursor = io::Cursor::new(bytes);
- let mut archive =
- zip::ZipArchive::new(cursor).map_err(|e| format!("Failed to read zip archive: {}", e))?;
-
- for i in 0..archive.len() {
- let mut file = archive
- .by_index(i)
- .map_err(|e| format!("Failed to read zip entry: {}", e))?;
-
- let enclosed = match file.enclosed_name() {
- Some(name) => name.to_owned(),
- None => continue,
- };
- let rel_path = enclosed.to_string_lossy().to_string();
-
- if rel_path.is_empty() || file.is_dir() {
- if file.is_dir() {
- let out_dir = dest.join(&rel_path);
- let _ = fs::create_dir_all(&out_dir);
- }
- continue;
- }
-
- let out_path = dest.join(&rel_path);
- if !out_path.starts_with(dest) {
- continue;
- }
-
- if let Some(parent) = out_path.parent() {
- fs::create_dir_all(parent)
- .map_err(|e| format!("Failed to create parent dir {}: {}", parent.display(), e))?;
- }
- let mut out_file = fs::File::create(&out_path)
- .map_err(|e| format!("Failed to create file {}: {}", out_path.display(), e))?;
- io::copy(&mut file, &mut out_file)
- .map_err(|e| format!("Failed to write {}: {}", out_path.display(), e))?;
- }
-
- Ok(())
-}
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 979f592c5..c29420ca2 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -198,186 +198,6 @@ fn run_session(args: &[String], session: &str, json_mode: bool) {
}
}
-fn get_dashboard_pid_path() -> std::path::PathBuf {
- get_socket_dir().join("dashboard.pid")
-}
-
-fn is_pid_alive(pid: u32) -> bool {
- #[cfg(unix)]
- {
- unsafe { libc::kill(pid as i32, 0) == 0 }
- }
- #[cfg(windows)]
- {
- unsafe {
- let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
- if handle != 0 {
- CloseHandle(handle);
- true
- } else {
- false
- }
- }
- }
-}
-
-fn run_dashboard_start(port: u16, json_mode: bool) {
- let pid_path = get_dashboard_pid_path();
-
- // Check if already running
- if let Ok(pid_str) = fs::read_to_string(&pid_path) {
- if let Ok(pid) = pid_str.trim().parse::() {
- if is_pid_alive(pid) {
- if json_mode {
- print_json_value(json!({
- "success": true,
- "data": { "port": port, "pid": pid, "already_running": true },
- }));
- } else {
- println!("Dashboard already running at http://localhost:{}", port);
- }
- return;
- }
- }
- let _ = fs::remove_file(&pid_path);
- }
-
- let socket_dir = get_socket_dir();
- if !socket_dir.exists() {
- let _ = fs::create_dir_all(&socket_dir);
- }
-
- let exe_path = match env::current_exe() {
- Ok(p) => p.canonicalize().unwrap_or(p),
- Err(e) => {
- if json_mode {
- print_json_error(format!("Failed to get executable path: {}", e));
- } else {
- eprintln!(
- "{} Failed to get executable path: {}",
- color::error_indicator(),
- e
- );
- }
- exit(1);
- }
- };
-
- let mut cmd = std::process::Command::new(&exe_path);
- cmd.env("AGENT_BROWSER_DASHBOARD", "1")
- .env("AGENT_BROWSER_DASHBOARD_PORT", port.to_string());
-
- #[cfg(unix)]
- {
- use std::os::unix::process::CommandExt;
- unsafe {
- cmd.pre_exec(|| {
- libc::setsid();
- Ok(())
- });
- }
- }
-
- #[cfg(windows)]
- {
- use std::os::windows::process::CommandExt;
- const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
- const DETACHED_PROCESS: u32 = 0x00000008;
- cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS);
- }
-
- match cmd
- .stdin(std::process::Stdio::null())
- .stdout(std::process::Stdio::null())
- .stderr(std::process::Stdio::null())
- .spawn()
- {
- Ok(child) => {
- let pid = child.id();
- let _ = fs::write(&pid_path, pid.to_string());
-
- if json_mode {
- print_json_value(json!({
- "success": true,
- "data": { "port": port, "pid": pid },
- }));
- } else {
- println!("Dashboard started at http://localhost:{}", port);
- }
- }
- Err(e) => {
- if json_mode {
- print_json_error(format!("Failed to start dashboard: {}", e));
- } else {
- eprintln!(
- "{} Failed to start dashboard: {}",
- color::error_indicator(),
- e
- );
- }
- exit(1);
- }
- }
-}
-
-fn run_dashboard_stop(json_mode: bool) {
- let pid_path = get_dashboard_pid_path();
-
- let pid_str = match fs::read_to_string(&pid_path) {
- Ok(s) => s,
- Err(_) => {
- if json_mode {
- print_json_value(
- json!({ "success": true, "data": { "stopped": false, "reason": "not running" } }),
- );
- } else {
- println!("Dashboard is not running");
- }
- return;
- }
- };
-
- let pid: u32 = match pid_str.trim().parse() {
- Ok(p) => p,
- Err(_) => {
- let _ = fs::remove_file(&pid_path);
- if json_mode {
- print_json_value(
- json!({ "success": true, "data": { "stopped": false, "reason": "invalid pid" } }),
- );
- } else {
- println!("Dashboard is not running");
- }
- return;
- }
- };
-
- #[cfg(unix)]
- {
- unsafe {
- libc::kill(pid as i32, libc::SIGTERM);
- }
- }
- #[cfg(windows)]
- {
- unsafe {
- let handle = OpenProcess(1, 0, pid); // PROCESS_TERMINATE = 1
- if handle != 0 {
- windows_sys::Win32::System::Threading::TerminateProcess(handle, 0);
- CloseHandle(handle);
- }
- }
- }
-
- let _ = fs::remove_file(&pid_path);
-
- if json_mode {
- print_json_value(json!({ "success": true, "data": { "stopped": true } }));
- } else {
- println!("{} Dashboard stopped", color::green("✓"));
- }
-}
-
fn run_close_all(flags: &Flags) {
let socket_dir = get_socket_dir();
let mut sessions: Vec = Vec::new();
@@ -499,17 +319,6 @@ fn main() {
return;
}
- // Standalone dashboard server mode
- if env::var("AGENT_BROWSER_DASHBOARD").is_ok() {
- let port: u16 = env::var("AGENT_BROWSER_DASHBOARD_PORT")
- .ok()
- .and_then(|s| s.parse().ok())
- .unwrap_or(4848);
- let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
- rt.block_on(native::stream::run_dashboard_server(port));
- return;
- }
-
let args: Vec = env::args().skip(1).collect();
let flags = parse_flags(&args);
let clean = clean_args(&args);
@@ -550,38 +359,6 @@ fn main() {
return;
}
- // Handle dashboard subcommand
- if clean.first().map(|s| s.as_str()) == Some("dashboard") {
- match clean.get(1).map(|s| s.as_str()) {
- Some("install") => {
- install::run_dashboard_install();
- return;
- }
- Some("start") | None => {
- let port = clean
- .iter()
- .position(|a| a == "--port")
- .and_then(|i| clean.get(i + 1))
- .and_then(|s| s.parse::().ok())
- .unwrap_or(4848);
- run_dashboard_start(port, flags.json);
- return;
- }
- Some("stop") => {
- run_dashboard_stop(flags.json);
- return;
- }
- Some(unknown) => {
- eprintln!(
- "{} Unknown dashboard subcommand: {}",
- color::error_indicator(),
- unknown
- );
- exit(1);
- }
- }
- }
-
// Handle session separately (doesn't need daemon)
if clean.first().map(|s| s.as_str()) == Some("session") {
run_session(&clean, &flags.session, flags.json);
@@ -698,8 +475,6 @@ fn main() {
let daemon_opts = DaemonOptions {
headed: flags.headed,
debug: flags.debug,
- executable_path: flags.executable_path.as_deref(),
- extensions: &flags.extensions,
args: flags.args.as_deref(),
user_agent: flags.user_agent.as_deref(),
proxy: proxy_server.as_deref(),
@@ -707,10 +482,8 @@ fn main() {
proxy_username: proxy_username.as_deref(),
proxy_password: proxy_password.as_deref(),
ignore_https_errors: flags.ignore_https_errors,
- allow_file_access: flags.allow_file_access,
profile: flags.profile.as_deref(),
state: flags.state.as_deref(),
- provider: flags.provider.as_deref(),
device: flags.device.as_deref(),
session_name: flags.session_name.as_deref(),
download_path: flags.download_path.as_deref(),
@@ -718,9 +491,7 @@ fn main() {
action_policy: flags.action_policy.as_deref(),
confirm_actions: flags.confirm_actions.as_deref(),
engine: flags.engine.as_deref(),
- auto_connect: flags.auto_connect,
idle_timeout: flags.idle_timeout.as_deref(),
- cdp: flags.cdp.as_deref(),
no_auto_dialog: flags.no_auto_dialog,
};
@@ -741,16 +512,6 @@ fn main() {
// variables (since the daemon already uses the env vars when it starts).
if daemon_result.already_running {
let ignored_flags: Vec<&str> = [
- if flags.cli_executable_path {
- Some("--executable-path")
- } else {
- None
- },
- if flags.cli_extensions {
- Some("--extension")
- } else {
- None
- },
if flags.cli_profile {
Some("--profile")
} else {
@@ -778,7 +539,6 @@ fn main() {
None
},
flags.ignore_https_errors.then_some("--ignore-https-errors"),
- flags.cli_allow_file_access.then_some("--allow-file-access"),
flags.cli_download_path.then_some("--download-path"),
flags.cli_headed.then_some("--headed"),
]
@@ -795,247 +555,17 @@ fn main() {
}
}
- // Validate mutually exclusive options
- if flags.cdp.is_some() && flags.provider.is_some() {
- let msg = "Cannot use --cdp and -p/--provider together";
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
-
- if flags.auto_connect && flags.cdp.is_some() {
- let msg = "Cannot use --auto-connect and --cdp together";
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
-
- if flags.auto_connect && flags.provider.is_some() {
- let msg = "Cannot use --auto-connect and -p/--provider together";
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
-
- if flags.provider.is_some() && !flags.extensions.is_empty() {
- let msg = "Cannot use --extension with -p/--provider (extensions require local browser)";
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
-
- if flags.cdp.is_some() && !flags.extensions.is_empty() {
- let msg = "Cannot use --extension with --cdp (extensions require local browser)";
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
-
- // Auto-connect to existing browser.
- // Skip when the daemon was already running — it already holds the connection
- // from a previous auto-connect launch, so re-sending the launch command would
- // redundantly probe Chrome and may trigger repeated permission prompts (#962).
- if flags.auto_connect && !daemon_result.already_running {
- let mut launch_cmd = json!({
- "id": gen_id(),
- "action": "launch",
- "autoConnect": true
- });
-
- if flags.ignore_https_errors {
- launch_cmd["ignoreHTTPSErrors"] = json!(true);
- }
-
- if let Some(ref cs) = flags.color_scheme {
- launch_cmd["colorScheme"] = json!(cs);
- }
-
- if let Some(ref dp) = flags.download_path {
- launch_cmd["downloadPath"] = json!(dp);
- }
-
- let err = match send_command(launch_cmd, &flags.session) {
- Ok(resp) if resp.success => None,
- Ok(resp) => Some(
- resp.error
- .unwrap_or_else(|| "Auto-connect failed".to_string()),
- ),
- Err(e) => Some(e.to_string()),
- };
-
- if let Some(msg) = err {
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
- }
-
- // Connect via CDP if --cdp flag is set
- // Accepts either a port number (e.g., "9222") or a full URL (e.g., "ws://..." or "wss://...")
- // Skip when daemon already running — it already holds the CDP connection.
- if let Some(ref cdp_value) = flags.cdp {
- // Validate CDP value eagerly (even when daemon is already running) so
- // the user gets an immediate error for bad input instead of a silent no-op.
- let launch_cmd = if cdp_value.starts_with("ws://")
- || cdp_value.starts_with("wss://")
- || cdp_value.starts_with("http://")
- || cdp_value.starts_with("https://")
- {
- // It's a URL - use cdpUrl field
- json!({
- "id": gen_id(),
- "action": "launch",
- "cdpUrl": cdp_value
- })
- } else {
- // It's a port number - validate and use cdpPort field
- let cdp_port: u16 = match cdp_value.parse::() {
- Ok(0) => {
- let msg = "Invalid CDP port: port must be greater than 0".to_string();
- if flags.json {
- print_json_error(&msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
- Ok(p) if p > 65535 => {
- let msg = format!(
- "Invalid CDP port: {} is out of range (valid range: 1-65535)",
- p
- );
- if flags.json {
- print_json_error(&msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
- Ok(p) => p as u16,
- Err(_) => {
- let msg = format!(
- "Invalid CDP value: '{}' is not a valid port number or URL",
- cdp_value
- );
- if flags.json {
- print_json_error(&msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
- };
- json!({
- "id": gen_id(),
- "action": "launch",
- "cdpPort": cdp_port
- })
- };
-
- if !daemon_result.already_running {
- let mut launch_cmd = launch_cmd;
-
- if flags.ignore_https_errors {
- launch_cmd["ignoreHTTPSErrors"] = json!(true);
- }
-
- if let Some(ref cs) = flags.color_scheme {
- launch_cmd["colorScheme"] = json!(cs);
- }
-
- if let Some(ref dp) = flags.download_path {
- launch_cmd["downloadPath"] = json!(dp);
- }
-
- let err = match send_command(launch_cmd, &flags.session) {
- Ok(resp) if resp.success => None,
- Ok(resp) => Some(
- resp.error
- .unwrap_or_else(|| "CDP connection failed".to_string()),
- ),
- Err(e) => Some(e.to_string()),
- };
-
- if let Some(msg) = err {
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
- }
- }
-
- // Launch with cloud provider if -p flag is set
- // Skip when daemon already running — it already holds the provider connection.
- if let Some(ref provider) = flags.provider {
- if !daemon_result.already_running {
- let mut launch_cmd = json!({
- "id": gen_id(),
- "action": "launch",
- "provider": provider
- });
-
- if let Some(ref cs) = flags.color_scheme {
- launch_cmd["colorScheme"] = json!(cs);
- }
-
- let err = match send_command(launch_cmd, &flags.session) {
- Ok(resp) if resp.success => None,
- Ok(resp) => Some(
- resp.error
- .unwrap_or_else(|| "Provider connection failed".to_string()),
- ),
- Err(e) => Some(e.to_string()),
- };
-
- if let Some(msg) = err {
- if flags.json {
- print_json_error(msg);
- } else {
- eprintln!("{} {}", color::error_indicator(), msg);
- }
- exit(1);
- }
- }
- }
-
- // Launch headed browser or configure browser options (without CDP or provider)
- if (flags.headed
+ // Launch headed browser or configure browser options
+ if flags.headed
|| flags.cli_headed // User explicitly set --headed (even if false)
- || flags.executable_path.is_some()
|| flags.profile.is_some()
|| flags.state.is_some()
|| flags.proxy.is_some()
|| flags.args.is_some()
|| flags.user_agent.is_some()
- || flags.allow_file_access
|| flags.color_scheme.is_some()
|| flags.download_path.is_some()
|| flags.engine.is_some()
- || !flags.extensions.is_empty())
- && flags.cdp.is_none()
- && flags.provider.is_none()
- && !flags.auto_connect
{
let mut launch_cmd = json!({
"id": gen_id(),
@@ -1047,11 +577,6 @@ fn main() {
.as_object_mut()
.expect("json! macro guarantees object type");
- // Add executable path if specified
- if let Some(ref exec_path) = flags.executable_path {
- cmd_obj.insert("executablePath".to_string(), json!(exec_path));
- }
-
// Add profile path if specified
if let Some(ref profile_path) = flags.profile {
cmd_obj.insert("profile".to_string(), json!(profile_path));
@@ -1091,18 +616,10 @@ fn main() {
cmd_obj.insert("args".to_string(), json!(args_vec));
}
- if !flags.extensions.is_empty() {
- cmd_obj.insert("extensions".to_string(), json!(&flags.extensions));
- }
-
if flags.ignore_https_errors {
launch_cmd["ignoreHTTPSErrors"] = json!(true);
}
- if flags.allow_file_access {
- launch_cmd["allowFileAccess"] = json!(true);
- }
-
if let Some(ref cs) = flags.color_scheme {
launch_cmd["colorScheme"] = json!(cs);
}
diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs
index 7f3f7434b..aeb4162a3 100644
--- a/cli/src/native/actions.rs
+++ b/cli/src/native/actions.rs
@@ -11,7 +11,6 @@ use tokio::sync::{broadcast, oneshot, RwLock};
use crate::connection::get_socket_dir;
-use super::auth;
use super::browser::{should_track_target, BrowserManager, WaitUntil};
use super::cdp::chrome::LaunchOptions;
use super::cdp::client::CdpClient;
@@ -23,7 +22,6 @@ use super::cdp::types::{
use super::cookies;
use super::diff;
use super::element::RefMap;
-use super::inspect_server::InspectServer;
use super::interaction;
use super::network::{self, DomainFilter, EventTracker};
use super::policy::{ActionPolicy, ConfirmActions, PolicyResult};
@@ -50,13 +48,6 @@ use super::webdriver::safari;
/// to appear before filling/clicking.
pub const AUTH_LOGIN_WAIT_UNTIL: WaitUntil = WaitUntil::Load;
-/// Poll interval used while waiting for auth form selectors to appear.
-const AUTH_LOGIN_SELECTOR_POLL_INTERVAL_MS: u64 = 100;
-
-/// Time spent trying targeted username selectors before broad text-input
-/// fallback selectors are allowed.
-const AUTH_LOGIN_PREFERRED_SELECTOR_WINDOW_MS: u64 = 5_000;
-
pub struct PendingConfirmation {
pub action: String,
pub cmd: Value,
@@ -186,7 +177,6 @@ pub struct DaemonState {
pub har_recording: bool,
pub har_entries: Vec,
pub confirm_actions: Option,
- pub inspect_server: Option,
pub routes: Arc>>,
pub tracked_requests: Vec,
pub request_tracking: bool,
@@ -249,7 +239,6 @@ impl DaemonState {
har_recording: false,
har_entries: Vec::new(),
confirm_actions: ConfirmActions::from_env(),
- inspect_server: None,
routes: Arc::new(RwLock::new(Vec::new())),
tracked_requests: Vec::new(),
request_tracking: false,
@@ -1108,23 +1097,12 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
"" | "launch"
| "close"
| "har_stop"
- | "credentials_set"
- | "credentials_get"
- | "credentials_delete"
- | "credentials_list"
- | "auth_save"
- | "auth_show"
- | "auth_delete"
- | "auth_list"
| "state_list"
| "state_show"
| "state_clear"
| "state_clean"
| "state_rename"
| "device_list"
- | "stream_enable"
- | "stream_disable"
- | "stream_status"
);
if !skip_launch {
// Check if existing connection is stale and needs re-launch.
@@ -1175,11 +1153,8 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
"launch" => handle_launch(cmd, state).await,
"navigate" => handle_navigate(cmd, state).await,
"url" => handle_url(state).await,
- "cdp_url" => handle_cdp_url(state),
- "inspect" => handle_inspect(state).await,
"title" => handle_title(state).await,
"content" => handle_content(state).await,
- "evaluate" => handle_evaluate(cmd, state).await,
"close" => handle_close(state).await,
"snapshot" => handle_snapshot(cmd, state).await,
"screenshot" => handle_screenshot(cmd, state).await,
@@ -1237,10 +1212,6 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
"download" => handle_download(cmd, state).await,
"diff_snapshot" => handle_diff_snapshot(cmd, state).await,
"diff_url" => handle_diff_url(cmd, state).await,
- "credentials_set" => handle_credentials_set(cmd).await,
- "credentials_get" => handle_credentials_get(cmd).await,
- "credentials_delete" => handle_credentials_delete(cmd).await,
- "credentials_list" => handle_credentials_list().await,
"mouse" => handle_mouse(cmd, state).await,
"keyboard" => handle_keyboard(cmd, state).await,
"focus" => handle_focus(cmd, state).await,
@@ -1267,14 +1238,10 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
"addscript" => handle_addscript(cmd, state).await,
"addinitscript" => handle_addinitscript(cmd, state).await,
"addstyle" => handle_addstyle(cmd, state).await,
- "clipboard" => handle_clipboard(cmd, state).await,
"wheel" => handle_wheel(cmd, state).await,
"device" => handle_device(cmd, state).await,
"screencast_start" => handle_screencast_start(cmd, state).await,
"screencast_stop" => handle_screencast_stop(state).await,
- "stream_enable" => handle_stream_enable(cmd, state).await,
- "stream_disable" => handle_stream_disable(state).await,
- "stream_status" => handle_stream_status(state).await,
"waitforurl" => handle_waitforurl(cmd, state).await,
"waitforloadstate" => handle_waitforloadstate(cmd, state).await,
"waitforfunction" => handle_waitforfunction(cmd, state).await,
@@ -1306,13 +1273,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value {
"unroute" => handle_unroute(cmd, state).await,
"requests" => handle_requests(cmd, state).await,
"request_detail" => handle_request_detail(cmd, state).await,
- "credentials" => handle_http_credentials(cmd, state).await,
"emulatemedia" => handle_set_media(cmd, state).await,
- "auth_save" => handle_auth_save(cmd).await,
- "auth_login" => handle_auth_login(cmd, state).await,
- "auth_list" => handle_credentials_list().await,
- "auth_delete" => handle_credentials_delete(cmd).await,
- "auth_show" => handle_auth_show(cmd).await,
"confirm" => handle_confirm(cmd, state).await,
"deny" => handle_deny(cmd, state).await,
"swipe" => handle_swipe(cmd, state).await,
@@ -1517,6 +1478,11 @@ async fn try_auto_restore_state(state: &mut DaemonState) {
// ---------------------------------------------------------------------------
async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result {
+ // Hardened build: reject cloud browser providers
+ if cmd.get("provider").is_some() {
+ return Err("Cloud browser providers are disabled in this build".to_string());
+ }
+
let headless = cmd
.get("headless")
.and_then(|v| v.as_bool())
@@ -1968,31 +1934,6 @@ async fn handle_url(state: &DaemonState) -> Result {
Ok(json!({ "url": url }))
}
-fn handle_cdp_url(state: &DaemonState) -> Result {
- let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
- Ok(json!({ "cdpUrl": mgr.get_cdp_url() }))
-}
-
-async fn handle_inspect(state: &mut DaemonState) -> Result {
- let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
-
- // Shut down any existing inspect server so we always target the current page
- if let Some(server) = state.inspect_server.take() {
- server.shutdown();
- }
-
- let target_id = mgr.active_target_id()?.to_string();
- let chrome_hp = mgr.chrome_host_port().to_string();
- let proxy_handle = mgr.client.inspect_handle();
-
- let server = InspectServer::start(proxy_handle, target_id, chrome_hp).await?;
- let url = format!("http://127.0.0.1:{}", server.port());
- open_url_in_browser(&url);
-
- state.inspect_server = Some(server);
- Ok(json!({ "opened": true, "url": url }))
-}
-
fn open_url_in_browser(url: &str) {
#[cfg(target_os = "macos")]
let result = std::process::Command::new("open").arg(url).spawn();
@@ -2038,29 +1979,6 @@ async fn handle_content(state: &DaemonState) -> Result {
Ok(json!({ "html": html, "origin": url }))
}
-async fn handle_evaluate(cmd: &Value, state: &DaemonState) -> Result {
- if let Some(ref wb) = state.webdriver_backend {
- if state.browser.is_none() {
- let script = cmd
- .get("script")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'script' parameter")?;
- let result = wb.evaluate(script).await?;
- let url = wb.get_url().await.unwrap_or_default();
- return Ok(json!({ "result": result, "origin": url }));
- }
- }
- let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
- let script = cmd
- .get("script")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'script' parameter")?;
-
- let result = mgr.evaluate(script, None).await?;
- let url = mgr.get_url().await.unwrap_or_default();
- Ok(json!({ "result": result, "origin": url }))
-}
-
async fn handle_close(state: &mut DaemonState) -> Result {
if let Some(ref mgr) = state.browser {
if let Some(ref session_name) = state.session_name {
@@ -2109,10 +2027,6 @@ async fn handle_close(state: &mut DaemonState) -> Result {
state.safari_driver = None;
state.backend_type = BackendType::Cdp;
- if let Some(server) = state.inspect_server.take() {
- server.shutdown();
- }
-
state.ref_map.clear();
Ok(json!({ "closed": true }))
}
@@ -3239,51 +3153,6 @@ async fn handle_diff_url(cmd: &Value, state: &mut DaemonState) -> Result Result {
- let name = cmd
- .get("name")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'name'")?;
- let username = cmd
- .get("username")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'username'")?;
- let password = cmd
- .get("password")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'password'")?;
- let url = cmd.get("url").and_then(|v| v.as_str());
- auth::credentials_set(name, username, password, url)
-}
-
-async fn handle_credentials_get(cmd: &Value) -> Result {
- let name = cmd
- .get("name")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'name'")?;
- auth::credentials_get(name)
-}
-
-async fn handle_credentials_delete(cmd: &Value) -> Result {
- let name = cmd
- .get("name")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'name'")?;
- auth::credentials_delete(name)
-}
-
-async fn handle_credentials_list() -> Result {
- auth::credentials_list()
-}
-
-async fn handle_auth_show(cmd: &Value) -> Result {
- let name = cmd
- .get("name")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'name'")?;
- auth::auth_show(name)
-}
-
async fn handle_mouse(cmd: &Value, state: &DaemonState) -> Result {
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
let session_id = mgr.active_session_id()?.to_string();
@@ -4407,50 +4276,6 @@ async fn handle_addstyle(cmd: &Value, state: &DaemonState) -> Result Result {
- let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
- let action = cmd
- .get("subAction")
- .or_else(|| cmd.get("operation"))
- .and_then(|v| v.as_str())
- .unwrap_or("read");
-
- let session_id = mgr.active_session_id()?.to_string();
-
- // cfg! is compile-time; assumes the browser runs on the same OS as the CLI binary.
- let modifier: i32 = if cfg!(target_os = "macos") { 4 } else { 2 };
-
- match action {
- "write" => {
- let text = cmd
- .get("text")
- .or_else(|| cmd.get("value"))
- .and_then(|v| v.as_str())
- .ok_or("Missing 'text' parameter")?;
- let js = format!(
- "navigator.clipboard.writeText({})",
- serde_json::to_string(text).unwrap_or_default()
- );
- mgr.evaluate(&js, None).await?;
- Ok(json!({ "written": text }))
- }
- "copy" => {
- interaction::press_key_with_modifiers(&mgr.client, &session_id, "c", Some(modifier))
- .await?;
- Ok(json!({ "copied": true }))
- }
- "paste" => {
- interaction::press_key_with_modifiers(&mgr.client, &session_id, "v", Some(modifier))
- .await?;
- Ok(json!({ "pasted": true }))
- }
- _ => {
- let result = mgr.evaluate("navigator.clipboard.readText()", None).await?;
- Ok(json!({ "text": result }))
- }
- }
-}
-
async fn handle_wheel(cmd: &Value, state: &DaemonState) -> Result {
let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
let session_id = mgr.active_session_id()?.to_string();
@@ -4622,57 +4447,6 @@ async fn current_stream_status(state: &DaemonState) -> Value {
})
}
-async fn handle_stream_enable(cmd: &Value, state: &mut DaemonState) -> Result {
- if state.stream_server.is_some() {
- return Err("Streaming is already enabled for this session".to_string());
- }
-
- let requested_port = match cmd.get("port").and_then(|value| value.as_u64()) {
- Some(raw) => u16::try_from(raw)
- .map_err(|_| format!("Invalid stream port '{}': expected 0-65535", raw))?,
- None => 0,
- };
-
- let (server, client_slot) =
- StreamServer::start_without_client(requested_port, state.session_id.clone(), false).await?;
- let port = server.port();
- if let Err(err) = write_stream_file(&state.session_id, port) {
- server.shutdown().await;
- return Err(err);
- }
-
- state.stream_client = Some(client_slot);
- state.stream_server = Some(Arc::new(server));
- state.request_tracking = true;
- if state.screencasting {
- if let Some(ref server) = state.stream_server {
- server.set_screencasting(true).await;
- }
- }
- state.update_stream_client().await;
-
- Ok(current_stream_status(state).await)
-}
-
-async fn handle_stream_disable(state: &mut DaemonState) -> Result {
- let Some(server) = state.stream_server.clone() else {
- return Err("Streaming is not enabled for this session".to_string());
- };
-
- server.shutdown().await;
- state.stream_server = None;
- state.stream_client = None;
- remove_stream_file(&state.session_id)?;
- remove_engine_file(&state.session_id);
- remove_provider_file(&state.session_id);
-
- Ok(json!({ "disabled": true }))
-}
-
-async fn handle_stream_status(state: &DaemonState) -> Result {
- Ok(current_stream_status(state).await)
-}
-
// ---------------------------------------------------------------------------
// Screencast handlers
// ---------------------------------------------------------------------------
@@ -6614,340 +6388,6 @@ async fn handle_request_detail(cmd: &Value, state: &mut DaemonState) -> Result Result {
- let mgr = state.browser.as_ref().ok_or("Browser not launched")?;
- let session_id = mgr.active_session_id()?.to_string();
- let username = cmd
- .get("username")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'username' parameter")?;
- let password = cmd
- .get("password")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'password' parameter")?;
-
- let encoded = base64::Engine::encode(
- &base64::engine::general_purpose::STANDARD,
- format!("{}:{}", username, password),
- );
-
- let mut headers = HashMap::new();
- headers.insert("Authorization".to_string(), format!("Basic {}", encoded));
- network::set_extra_headers(&mgr.client, &session_id, &headers).await?;
-
- Ok(json!({ "set": true }))
-}
-
-// ---------------------------------------------------------------------------
-// Auth handlers
-// ---------------------------------------------------------------------------
-
-/// Wait for any selector in `selectors` to appear and return the first match.
-///
-/// This is used by `auth_login` auto-detection so SPA login forms can render
-/// after initial navigation without requiring global network-idle.
-async fn wait_for_any_selector(
- client: &super::cdp::client::CdpClient,
- session_id: &str,
- selectors: &[&str],
- timeout_ms: u64,
-) -> Result {
- let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
-
- loop {
- for selector in selectors {
- let expression = format!(
- r#"(() => {{
- const el = document.querySelector({sel});
- if (!el) return false;
-
- const r = el.getBoundingClientRect();
- const s = window.getComputedStyle(el);
- const opacity = parseFloat(s.opacity || '1');
- const isVisible =
- r.width > 0 &&
- r.height > 0 &&
- s.visibility !== 'hidden' &&
- s.display !== 'none' &&
- (!Number.isFinite(opacity) || opacity > 0);
-
- if (!isVisible) return false;
- if (el.matches(':disabled')) return false;
-
- if (el instanceof HTMLInputElement && el.type === 'hidden') return false;
- if ((el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && el.readOnly) return false;
-
- return true;
- }})()"#,
- sel = serde_json::to_string(selector).unwrap_or_default()
- );
-
- let result: super::cdp::types::EvaluateResult = client
- .send_command_typed(
- "Runtime.evaluate",
- &super::cdp::types::EvaluateParams {
- expression,
- return_by_value: Some(true),
- await_promise: Some(true),
- },
- Some(session_id),
- )
- .await?;
-
- if result
- .result
- .value
- .as_ref()
- .and_then(|v| v.as_bool())
- .unwrap_or(false)
- {
- return Ok((*selector).to_string());
- }
- }
-
- if tokio::time::Instant::now() >= deadline {
- return Err(format!("Wait timed out after {}ms", timeout_ms));
- }
-
- tokio::time::sleep(tokio::time::Duration::from_millis(
- AUTH_LOGIN_SELECTOR_POLL_INTERVAL_MS,
- ))
- .await;
- }
-}
-
-async fn handle_auth_save(cmd: &Value) -> Result {
- let name = cmd
- .get("name")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'name'")?;
- let url = cmd
- .get("url")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'url'")?;
- let username = cmd
- .get("username")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'username'")?;
- let password = cmd
- .get("password")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'password'")?;
- let username_selector = cmd.get("usernameSelector").and_then(|v| v.as_str());
- let password_selector = cmd.get("passwordSelector").and_then(|v| v.as_str());
- let submit_selector = cmd.get("submitSelector").and_then(|v| v.as_str());
- auth::auth_save(
- name,
- url,
- username,
- password,
- username_selector,
- password_selector,
- submit_selector,
- )
-}
-
-async fn handle_auth_login(cmd: &Value, state: &mut DaemonState) -> Result {
- let name = cmd
- .get("name")
- .and_then(|v| v.as_str())
- .ok_or("Missing 'name'")?;
- let cred = auth::credentials_get_full(name)?;
- if cred.url.is_empty() {
- return Err("Credential has no URL".to_string());
- }
- let url = cred.url;
- let username = cred.username;
- let password = cred.password;
-
- let mgr = state.browser.as_mut().ok_or("Browser not launched")?;
- mgr.navigate(&url, AUTH_LOGIN_WAIT_UNTIL).await?;
-
- let session_id = mgr.active_session_id()?.to_string();
- let auth_timeout_ms = mgr.default_timeout_ms();
-
- let preferred_user_selectors = [
- "input[type=email]",
- "input[name=email]",
- "input[id=email]",
- "input[autocomplete=email]",
- "input[autocomplete=username]",
- "input[name=username]",
- "input[name*=email i]",
- "input[name*=user i]",
- "input[id*=email i]",
- "input[id*=user i]",
- "input[type=text][name*=email i]",
- "input[type=text][name*=user i]",
- "input[type=text][id*=email i]",
- "input[type=text][id*=user i]",
- "input[type=text][autocomplete=email]",
- "input[type=text][autocomplete=username]",
- ];
- let fallback_user_selectors = ["input[type=text]", "input:not([type])"];
- let auto_submit_selectors = [
- "button[type=submit]",
- "input[type=submit]",
- "button:not([type])",
- ];
-
- let username_sel = cmd
- .get("usernameSelector")
- .and_then(|v| v.as_str())
- .map(String::from)
- .or(cred.username_selector);
- let password_sel = cmd
- .get("passwordSelector")
- .and_then(|v| v.as_str())
- .map(String::from)
- .or(cred.password_selector);
- let submit_sel = cmd
- .get("submitSelector")
- .and_then(|v| v.as_str())
- .map(String::from)
- .or(cred.submit_selector);
-
- // Find and fill username
- let user_sel = if let Some(s) = username_sel {
- wait_for_selector(&mgr.client, &session_id, &s, "visible", auth_timeout_ms)
- .await
- .map_err(|_| format!("Timed out waiting for username selector '{}'", s))?;
- s
- } else {
- let preferred_window_ms = auth_timeout_ms.min(AUTH_LOGIN_PREFERRED_SELECTOR_WINDOW_MS);
- let fallback_window_ms = auth_timeout_ms.saturating_sub(preferred_window_ms);
-
- match wait_for_any_selector(
- &mgr.client,
- &session_id,
- &preferred_user_selectors,
- preferred_window_ms,
- )
- .await
- {
- Ok(selector) => selector,
- Err(_) => {
- if fallback_window_ms == 0 {
- return Err(format!(
- "Timed out waiting for username field (preferred selectors for {}ms: {})",
- preferred_window_ms,
- preferred_user_selectors.join(", ")
- ));
- }
-
- wait_for_any_selector(
- &mgr.client,
- &session_id,
- &fallback_user_selectors,
- fallback_window_ms,
- )
- .await
- .map_err(|_| {
- format!(
- "Timed out waiting for username field (preferred selectors for {}ms: {}; fallback selectors for {}ms: {})",
- preferred_window_ms,
- preferred_user_selectors.join(", "),
- fallback_window_ms,
- fallback_user_selectors.join(", ")
- )
- })?
- }
- }
- };
- interaction::fill(
- &mgr.client,
- &session_id,
- &state.ref_map,
- &user_sel,
- &username,
- &state.iframe_sessions,
- )
- .await?;
-
- // Find and fill password
- let pass_sel = password_sel.unwrap_or_else(|| "input[type=password]".to_string());
- wait_for_selector(
- &mgr.client,
- &session_id,
- &pass_sel,
- "visible",
- auth_timeout_ms,
- )
- .await
- .map_err(|_| format!("Timed out waiting for password selector '{}'", pass_sel))?;
- interaction::fill(
- &mgr.client,
- &session_id,
- &state.ref_map,
- &pass_sel,
- &password,
- &state.iframe_sessions,
- )
- .await?;
-
- // Find and click submit
- let sub_sel = if let Some(s) = submit_sel {
- wait_for_selector(&mgr.client, &session_id, &s, "visible", auth_timeout_ms)
- .await
- .map_err(|_| format!("Timed out waiting for submit selector '{}'", s))?;
- s
- } else {
- wait_for_any_selector(
- &mgr.client,
- &session_id,
- &auto_submit_selectors,
- auth_timeout_ms,
- )
- .await
- .map_err(|_| {
- format!(
- "Timed out waiting for submit button (tried selectors: {})",
- auto_submit_selectors.join(", ")
- )
- })?
- };
- interaction::click(
- &mgr.client,
- &session_id,
- &state.ref_map,
- &sub_sel,
- "left",
- 1,
- &state.iframe_sessions,
- )
- .await?;
-
- // Wait for navigation after submit (with fallback timeout)
- let mut rx = mgr.client.subscribe();
- let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10);
- let mut navigated = false;
-
- loop {
- let result = tokio::time::timeout_at(deadline, rx.recv()).await;
- match result {
- Ok(Ok(event)) => {
- if event.session_id.as_deref() == Some(&session_id) {
- match event.method.as_str() {
- "Page.frameNavigated" | "Page.loadEventFired" => {
- navigated = true;
- break;
- }
- _ => {}
- }
- }
- }
- Ok(Err(_)) => break,
- Err(_) => break,
- }
- }
-
- if !navigated {
- tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
- }
-
- Ok(json!({ "loggedIn": true, "name": name }))
-}
-
// ---------------------------------------------------------------------------
// Confirmation handlers (stub)
// ---------------------------------------------------------------------------
@@ -7438,183 +6878,6 @@ mod tests {
))
}
- #[tokio::test]
- async fn test_stream_enable_disable_and_status_without_browser() {
- let guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "AGENT_BROWSER_SESSION"]);
- let socket_dir = unique_socket_dir("stream-runtime");
- fs::create_dir_all(&socket_dir).expect("socket dir should be created");
- guard.set(
- "AGENT_BROWSER_SOCKET_DIR",
- socket_dir.to_str().expect("socket dir should be utf-8"),
- );
- guard.set("AGENT_BROWSER_SESSION", "stream-runtime-session");
-
- let mut state = DaemonState::new();
-
- let disabled_status = handle_stream_status(&state)
- .await
- .expect("status should work before enable");
- assert_eq!(disabled_status["enabled"], false);
- assert_eq!(disabled_status["port"], Value::Null);
- assert_eq!(disabled_status["connected"], false);
- assert_eq!(disabled_status["screencasting"], false);
-
- let enabled_status = handle_stream_enable(&json!({ "port": 0 }), &mut state)
- .await
- .expect("stream enable should succeed");
- let port = enabled_status["port"]
- .as_u64()
- .expect("runtime stream should report a bound port");
- assert!(port > 0, "runtime stream should bind a non-zero port");
- assert_eq!(enabled_status["enabled"], true);
- assert_eq!(enabled_status["connected"], false);
- assert_eq!(enabled_status["screencasting"], false);
-
- let stream_path = socket_dir.join("stream-runtime-session.stream");
- let port_file =
- fs::read_to_string(&stream_path).expect("stream metadata file should exist");
- assert_eq!(port_file.trim(), port.to_string());
-
- let duplicate_err = handle_stream_enable(&json!({}), &mut state)
- .await
- .expect_err("duplicate enable should fail");
- assert!(duplicate_err.contains("already enabled"));
-
- let status = handle_stream_status(&state)
- .await
- .expect("status should work after enable");
- assert_eq!(status["enabled"], true);
- assert_eq!(status["port"], port);
-
- let disabled = handle_stream_disable(&mut state)
- .await
- .expect("stream disable should succeed");
- assert_eq!(disabled["disabled"], true);
- assert!(
- !stream_path.exists(),
- "disabling runtime stream should remove the metadata file"
- );
- assert!(state.stream_server.is_none());
- assert!(state.stream_client.is_none());
-
- let final_status = handle_stream_status(&state)
- .await
- .expect("status should work after disable");
- assert_eq!(final_status["enabled"], false);
- assert_eq!(final_status["port"], Value::Null);
-
- let disable_err = handle_stream_disable(&mut state)
- .await
- .expect_err("duplicate disable should fail");
- assert!(disable_err.contains("not enabled"));
-
- let _ = fs::remove_dir_all(&socket_dir);
- }
-
- #[tokio::test]
- async fn test_stream_disable_preserves_existing_screencast_state() {
- let guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "AGENT_BROWSER_SESSION"]);
- let socket_dir = unique_socket_dir("stream-preserve-screencast");
- fs::create_dir_all(&socket_dir).expect("socket dir should be created");
- guard.set(
- "AGENT_BROWSER_SOCKET_DIR",
- socket_dir.to_str().expect("socket dir should be utf-8"),
- );
- guard.set(
- "AGENT_BROWSER_SESSION",
- "stream-preserve-screencast-session",
- );
-
- let mut state = DaemonState::new();
- handle_stream_enable(&json!({ "port": 0 }), &mut state)
- .await
- .expect("stream enable should succeed");
- state.screencasting = true;
-
- let disabled = handle_stream_disable(&mut state)
- .await
- .expect("stream disable should succeed");
- assert_eq!(disabled["disabled"], true);
- assert!(
- state.screencasting,
- "stream disable should not clear an independently managed screencast state"
- );
-
- let _ = fs::remove_dir_all(&socket_dir);
- }
-
- #[tokio::test]
- async fn test_stream_disable_clears_state_when_stream_file_removal_fails() {
- let guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "AGENT_BROWSER_SESSION"]);
- let socket_dir = unique_socket_dir("stream-disable-cleanup");
- fs::create_dir_all(&socket_dir).expect("socket dir should be created");
- guard.set(
- "AGENT_BROWSER_SOCKET_DIR",
- socket_dir.to_str().expect("socket dir should be utf-8"),
- );
- guard.set("AGENT_BROWSER_SESSION", "stream-disable-cleanup-session");
-
- let mut state = DaemonState::new();
- handle_stream_enable(&json!({ "port": 0 }), &mut state)
- .await
- .expect("stream enable should succeed");
-
- let stream_path = socket_dir.join("stream-disable-cleanup-session.stream");
- fs::remove_file(&stream_path).expect("stream metadata file should exist");
- fs::create_dir(&stream_path).expect("directory should force remove_stream_file failure");
-
- let err = handle_stream_disable(&mut state)
- .await
- .expect_err("stream disable should surface file removal failure");
- assert!(err.contains("Failed to remove stream metadata"));
- assert!(
- state.stream_server.is_none(),
- "stream disable should clear stream_server even when metadata cleanup fails"
- );
- assert!(
- state.stream_client.is_none(),
- "stream disable should clear stream_client even when metadata cleanup fails"
- );
-
- let _ = fs::remove_dir_all(&socket_dir);
- }
-
- #[tokio::test]
- async fn test_stream_enable_port_conflict_returns_error() {
- let guard = EnvGuard::new(&["AGENT_BROWSER_SOCKET_DIR", "AGENT_BROWSER_SESSION"]);
- let socket_dir = unique_socket_dir("stream-port-conflict");
- fs::create_dir_all(&socket_dir).expect("socket dir should be created");
- guard.set(
- "AGENT_BROWSER_SOCKET_DIR",
- socket_dir.to_str().expect("socket dir should be utf-8"),
- );
- guard.set("AGENT_BROWSER_SESSION", "stream-port-conflict-session");
-
- let listener = std::net::TcpListener::bind("127.0.0.1:0")
- .expect("test should reserve an ephemeral port");
- let port = listener
- .local_addr()
- .expect("listener should have local addr")
- .port();
-
- let mut state = DaemonState::new();
- let err = handle_stream_enable(&json!({ "port": port }), &mut state)
- .await
- .expect_err("conflicting port should fail");
- assert!(err.contains("Failed to bind stream server"));
- assert!(state.stream_server.is_none());
- assert!(state.stream_client.is_none());
- assert!(
- !socket_dir
- .join("stream-port-conflict-session.stream")
- .exists(),
- "failed enable should not leave stale metadata behind"
- );
-
- drop(listener);
- let _ = fs::remove_dir_all(&socket_dir);
- }
-
#[test]
fn test_success_response_structure() {
let resp = success_response("cmd-1", json!({"url": "https://example.com"}));
@@ -8058,55 +7321,6 @@ mod tests {
assert_eq!(result["success"], false);
}
- #[tokio::test]
- #[allow(clippy::await_holding_lock)]
- async fn test_credentials_roundtrip_via_actions() {
- let _lock = crate::native::auth::AUTH_TEST_MUTEX.lock().unwrap();
- let key_var = "AGENT_BROWSER_ENCRYPTION_KEY";
- let original = std::env::var(key_var).ok();
- // SAFETY: AUTH_TEST_MUTEX serializes all test access so no concurrent mutation.
- unsafe { std::env::set_var(key_var, "a".repeat(64)) };
-
- let mut state = DaemonState::new();
-
- let set_cmd = json!({
- "action": "credentials_set",
- "name": "test-cred-action",
- "username": "user",
- "password": "pass",
- "id": "c1"
- });
- let result = execute_command(&set_cmd, &mut state).await;
- assert_eq!(result["success"], true);
-
- let get_cmd = json!({
- "action": "credentials_get",
- "name": "test-cred-action",
- "id": "c2"
- });
- let result = execute_command(&get_cmd, &mut state).await;
- assert_eq!(result["success"], true);
- assert_eq!(result["data"]["username"], "user");
-
- let list_cmd = json!({ "action": "credentials_list", "id": "c3" });
- let result = execute_command(&list_cmd, &mut state).await;
- assert_eq!(result["success"], true);
-
- let del_cmd = json!({
- "action": "credentials_delete",
- "name": "test-cred-action",
- "id": "c4"
- });
- let result = execute_command(&del_cmd, &mut state).await;
- assert_eq!(result["success"], true);
-
- // SAFETY: AUTH_TEST_MUTEX serializes all test access so no concurrent mutation.
- match original {
- Some(val) => unsafe { std::env::set_var(key_var, val) },
- None => unsafe { std::env::remove_var(key_var) },
- }
- }
-
#[tokio::test]
async fn test_state_list_via_actions() {
let mut state = DaemonState::new();
diff --git a/cli/src/native/mod.rs b/cli/src/native/mod.rs
index f86c816e8..e24a98cf0 100644
--- a/cli/src/native/mod.rs
+++ b/cli/src/native/mod.rs
@@ -15,8 +15,6 @@ pub mod diff;
#[allow(dead_code)]
pub mod element;
#[allow(dead_code)]
-pub mod inspect_server;
-#[allow(dead_code)]
pub mod interaction;
#[allow(dead_code)]
pub mod network;
diff --git a/cli/src/native/parity_tests.rs b/cli/src/native/parity_tests.rs
index 2c0684048..56dbe607a 100644
--- a/cli/src/native/parity_tests.rs
+++ b/cli/src/native/parity_tests.rs
@@ -48,7 +48,6 @@ const DOCUMENTED_ACTIONS: &[&str] = &[
"url",
"title",
"content",
- "evaluate",
"close",
"snapshot",
"screenshot",
@@ -107,10 +106,6 @@ const DOCUMENTED_ACTIONS: &[&str] = &[
"download",
"diff_snapshot",
"diff_url",
- "credentials_set",
- "credentials_get",
- "credentials_delete",
- "credentials_list",
"mouse",
"keyboard",
"focus",
@@ -137,7 +132,6 @@ const DOCUMENTED_ACTIONS: &[&str] = &[
"addscript",
"addinitscript",
"addstyle",
- "clipboard",
"wheel",
"device",
"screencast_start",
@@ -173,12 +167,6 @@ const DOCUMENTED_ACTIONS: &[&str] = &[
"unroute",
"requests",
"request_detail",
- "credentials",
- "auth_save",
- "auth_login",
- "auth_list",
- "auth_delete",
- "auth_show",
"confirm",
"deny",
"swipe",
@@ -202,7 +190,7 @@ fn minimal_command(action: &str, id: &str) -> Value {
"navigate" | "diff_url" | "waitforurl" => {
obj.insert("url".to_string(), json!("https://example.com"));
}
- "evaluate" | "expose" => {
+ "expose" => {
obj.insert("script".to_string(), json!("1"));
}
"click" | "dblclick" | "fill" | "type" | "press" | "hover" | "scroll" | "select"
@@ -440,65 +428,10 @@ async fn test_state_list_without_browser() {
assert!(result["data"]["files"].is_array());
}
-#[tokio::test]
-async fn test_credentials_list_without_browser() {
- let mut state = DaemonState::new();
- let cmd = json!({ "action": "credentials_list", "id": "nb-2" });
- let result = execute_command(&cmd, &mut state).await;
-
- assert_eq!(result["success"], true);
- assert!(result["data"]["credentials"].is_array() || result["data"]["profiles"].is_array());
-}
-
// ---------------------------------------------------------------------------
// 4. New feature parity tests
// ---------------------------------------------------------------------------
-#[tokio::test]
-async fn test_auth_profile_name_validation() {
- use super::auth;
- let _key_guard = TestKeyGuard::new();
- let valid = auth::credentials_set("valid-name_123", "u", "p", None);
- assert!(valid.is_ok());
- let invalid = auth::credentials_set("invalid/name", "u", "p", None);
- assert!(invalid.is_err());
- let invalid2 = auth::credentials_set("", "u", "p", None);
- assert!(invalid2.is_err());
- let invalid3 = auth::credentials_set("has space", "u", "p", None);
- assert!(invalid3.is_err());
- // Cleanup
- let _ = auth::credentials_delete("valid-name_123");
-}
-
-#[tokio::test]
-async fn test_auth_save_and_show() {
- use super::auth;
- let _key_guard = TestKeyGuard::new();
- let result = auth::auth_save(
- "parity-roundtrip",
- "https://example.com",
- "user",
- "pass",
- Some("input#user"),
- None,
- None,
- );
- assert!(result.is_ok());
-
- let show = auth::auth_show("parity-roundtrip");
- assert!(show.is_ok());
- let data = show.unwrap();
- assert_eq!(data["profile"]["username"], "user");
- assert_eq!(data["profile"]["usernameSelector"], "input#user");
-
- let full = auth::credentials_get_full("parity-roundtrip");
- assert!(full.is_ok());
- assert_eq!(full.unwrap().password, "pass");
-
- // Cleanup
- let _ = auth::credentials_delete("parity-roundtrip");
-}
-
#[tokio::test]
async fn test_har_start_stop_without_browser() {
let mut state = DaemonState::new();
diff --git a/cli/src/output.rs b/cli/src/output.rs
index d49aa9723..1aef5edce 100644
--- a/cli/src/output.rs
+++ b/cli/src/output.rs
@@ -1560,37 +1560,6 @@ Examples:
"##
}
- // === Eval ===
- "eval" => {
- r##"
-agent-browser eval - Execute JavaScript
-
-Usage: agent-browser eval [options]