browser-agent-server 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. browser_agent_server-1.0.0/.github/workflows/ci.yml +35 -0
  2. browser_agent_server-1.0.0/.github/workflows/release.yml +73 -0
  3. browser_agent_server-1.0.0/.gitignore +9 -0
  4. browser_agent_server-1.0.0/AGENTS.md +23 -0
  5. browser_agent_server-1.0.0/CHANGELOG.md +20 -0
  6. browser_agent_server-1.0.0/LICENSE +21 -0
  7. browser_agent_server-1.0.0/PKG-INFO +98 -0
  8. browser_agent_server-1.0.0/README.md +75 -0
  9. browser_agent_server-1.0.0/docs/api.md +76 -0
  10. browser_agent_server-1.0.0/docs/architecture.md +34 -0
  11. browser_agent_server-1.0.0/docs/deployment.md +73 -0
  12. browser_agent_server-1.0.0/docs/publishing.md +32 -0
  13. browser_agent_server-1.0.0/docs/security.md +11 -0
  14. browser_agent_server-1.0.0/docs/troubleshooting.md +19 -0
  15. browser_agent_server-1.0.0/pyproject.toml +48 -0
  16. browser_agent_server-1.0.0/src/browser_agent/__init__.py +128 -0
  17. browser_agent_server-1.0.0/src/browser_agent/chrome.py +591 -0
  18. browser_agent_server-1.0.0/src/browser_agent/cli.py +414 -0
  19. browser_agent_server-1.0.0/src/browser_agent/data/browser-agent.service +26 -0
  20. browser_agent_server-1.0.0/src/browser_agent/server.py +250 -0
  21. browser_agent_server-1.0.0/src/browser_agent/vision_agent.py +165 -0
  22. browser_agent_server-1.0.0/src/browser_agent/x11_input.py +273 -0
  23. browser_agent_server-1.0.0/tests/test_bezier.py +23 -0
  24. browser_agent_server-1.0.0/tests/test_cli.py +59 -0
  25. browser_agent_server-1.0.0/tests/test_client.py +78 -0
  26. browser_agent_server-1.0.0/tests/test_cloudflare.py +21 -0
  27. browser_agent_server-1.0.0/tests/test_http_api.py +84 -0
  28. browser_agent_server-1.0.0/tests/test_ws_frames.py +118 -0
  29. browser_agent_server-1.0.0/uv.lock +635 -0
@@ -0,0 +1,35 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ python:
10
+ strategy:
11
+ matrix:
12
+ python: ["3.12", "3.13"]
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - name: Install
21
+ run: |
22
+ uv venv
23
+ uv pip install -e ".[dev]" --no-cache
24
+ - name: Ruff
25
+ run: uv run ruff check src tests
26
+ - name: Tests
27
+ run: uv run pytest -q
28
+
29
+ gitleaks:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ fetch-depth: 0
35
+ - uses: gitleaks/gitleaks-action@v2
@@ -0,0 +1,73 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v5
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Verify
17
+ run: |
18
+ uv venv
19
+ uv pip install -e ".[dev]" --no-cache
20
+ uv run ruff check src tests
21
+ uv run pytest -q
22
+ - name: Build
23
+ run: uv build
24
+ - uses: actions/upload-artifact@v4
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+
29
+ pypi:
30
+ needs: build
31
+ runs-on: ubuntu-latest
32
+ environment:
33
+ name: pypi
34
+ url: https://pypi.org/p/browser-agent-server
35
+ permissions:
36
+ id-token: write # PyPI trusted publishing (OIDC)
37
+ steps:
38
+ - uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+ - name: Skip if this version is already on PyPI
43
+ id: exists
44
+ run: |
45
+ VER="${GITHUB_REF_NAME#v}"
46
+ code=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/browser-agent-server/${VER}/json")
47
+ echo "exists=$([ "$code" = 200 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
48
+ [ "$code" = 200 ] && echo "${VER} already published β€” skipping (manual/token publish?)" || true
49
+ - uses: pypa/gh-action-pypi-publish@release/v1
50
+ if: steps.exists.outputs.exists == 'false'
51
+
52
+ github-release:
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ contents: write
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+ - name: Release notes from CHANGELOG
64
+ id: notes
65
+ run: |
66
+ TAG="${GITHUB_REF_NAME}"
67
+ VER="${TAG#v}"
68
+ awk "/^## \[${VER}\]/{f=1;next} /^## \[/{f=0} f" CHANGELOG.md > notes.md || true
69
+ [ -s notes.md ] || echo "Release ${TAG}" > notes.md
70
+ - uses: softprops/action-gh-release@v2
71
+ with:
72
+ body_path: notes.md
73
+ files: dist/*
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
@@ -0,0 +1,23 @@
1
+ # AGENTS.md β€” guide for AI coding agents working in this repo
2
+
3
+ > πŸ”— **Companion project:** [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) (npm, TypeScript) β€” the MCP bridge that consumes this daemon's HTTP API. Change [`docs/api.md`](docs/api.md) ⇄ change the bridge β€” keep them in the same release conversation.
4
+
5
+ ## Repo facts
6
+
7
+ - PyPI dist: `browser-agent-server` Β· import: `browser_agent` Β· CLI: `browser-agent`
8
+ - Python β‰₯ 3.12, src-layout, hatchling, **one runtime dependency** (`trafilatura`). That is deliberate (1 GB RAM target hosts) β€” do not add deps without an issue discussion.
9
+ - Toolchain: `uv` (`uv pip install -e ".[dev]"`), ruff (E/F/I/UP/B, line 100), pytest.
10
+ - Tests: unit only in CI (`pytest`); real-Chrome tests are opt-in: `BROWSER_AGENT_IT=1 pytest -m integration`.
11
+
12
+ ## Invariants β€” do not break these
13
+
14
+ 1. **Never emit `Runtime.enable`, `Console.enable`, `Debugger.enable` over CDP** β€” `FORBIDDEN_CDP_METHODS` exists for bot-detection reasons; extend it, don't shrink it.
15
+ 2. **Loopback-only HTTP API** (no auth by design). Never default-bind to `0.0.0.0`.
16
+ 3. **One tab at a time** (`BrowserCoordinator.lock`); the idle reaper must keep working (RAM discipline).
17
+ 4. Human-like input stays human: Bézier paths, micro-jitter, real `mousedown`→hold→`mouseup`. No CDP `Input.*` shortcuts.
18
+ 5. `/status` must always report `api_version` (see docs/api.md) and `version`.
19
+ 6. X11 profile/state dirs resolve via `BROWSER_AGENT_PROFILE_DIR` / `BROWSER_AGENT_STATE_DIR` / XDG β€” never hardcode `/tmp/...` paths again.
20
+
21
+ ## Definition of done for a PR
22
+
23
+ `ruff check src tests` clean Β· `pytest` green Β· CHANGELOG.md entry under `[Unreleased]` Β· docs updated if behavior/config changed Β· cross-repo note if the HTTP contract moved.
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [1.0.0] - 2026-10-10
9
+
10
+ Initial public release. Extracted from a private production deployment where
11
+ the daemon served real research workloads.
12
+
13
+ ### Added
14
+ - `browser-agent` CLI: `serve`, `status`, `stop`, `fetch`, `click`, `move`, `type`, `agent`, `vnc`, plus new `setup` and `doctor`.
15
+ - Localhost HTTP API (`127.0.0.1:8765`): `GET /status`, `POST /fetch`, `POST /action`, `POST /agent`. `/status` now reports `api_version` and the package `version`.
16
+ - Real headful Chrome on an on-demand Xvfb display with a hardware-level X11 (XTEST) mouse and keyboard over randomized Bezier paths.
17
+ - Zero-leak CDP client (`Runtime.enable`/`Console.enable`/`Debugger.enable` are refused by construction).
18
+ - Cloudflare Turnstile auto-solve via real X11 clicks; automatic WARP SOCKS5 retry for blocked datacenter IPs.
19
+ - RAM discipline: renderer caps + auto-hibernation on idle; systemd template with memory limits.
20
+ - Python SDK: `from browser_agent import BrowserClient`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 vernikr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.5
2
+ Name: browser-agent-server
3
+ Version: 1.0.0
4
+ Summary: Real headful Chrome on Xvfb with a hardware-level X11 mouse, Cloudflare Turnstile auto-solve and zero-leak CDP. Localhost HTTP API + CLI + Python SDK.
5
+ Project-URL: Homepage, https://github.com/vernikr/browser-agent
6
+ Project-URL: Repository, https://github.com/vernikr/browser-agent
7
+ Project-URL: Issues, https://github.com/vernikr/browser-agent/issues
8
+ Author: vernikr
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: automation,browser,chrome,cloudflare,mcp,stealth,turnstile,x11
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: trafilatura<3,>=1.12
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.0; extra == 'dev'
21
+ Requires-Dist: ruff>=0.6; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # browser-agent
25
+
26
+ > πŸ”— **Companion project:** [`@vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) β€” the MCP server that exposes this daemon to LLM agents (Claude Code, Cursor, Freebuff…). The two projects are developed together and speak the HTTP contract in [`docs/api.md`](docs/api.md).
27
+
28
+ **Real headful Chrome on Xvfb with a hardware-level X11 mouse.** One localhost daemon gives any project, script, or AI agent on a Linux box a genuine desktop browser: steered over real XTEST input events on randomized BΓ©zier paths, with zero-leak CDP (no `Runtime.enable`), Cloudflare Turnstile auto-solve, and RAM discipline for tiny VPSes (auto-hibernates to ~12 MB idle).
29
+
30
+ Why not Playwright/CDP directly: mainstream automation stacks call `Runtime.enable` / `Debugger.enable` on connect β€” the primary headless-detection vector. This daemon's CDP client refuses those methods by construction and reads the DOM via `DOM.getOuterHTML`; all mouse/keyboard input goes through the X server, so clicks are `isTrusted: true` hardware events.
31
+
32
+ ## Install & provision (server, Ubuntu 24.04)
33
+
34
+ ```bash
35
+ curl -LsSf https://astral.sh/uv/install.sh | sh # if uv is missing
36
+ uv tool install browser-agent-server
37
+ sudo browser-agent setup --with-chrome # apt deps + google-chrome-stable + systemd unit
38
+ sudo browser-agent doctor # self-check
39
+ ```
40
+
41
+ Use it three ways:
42
+
43
+ ```bash
44
+ browser-agent fetch "https://example.com" --text # CLI
45
+ curl -s 127.0.0.1:8765/status # HTTP API (any language)
46
+ ```
47
+
48
+ ```python
49
+ from browser_agent import BrowserClient # Python SDK
50
+
51
+ client = BrowserClient()
52
+ page = client.fetch("https://example.com")
53
+ print(page["title"], len(page["text"]))
54
+ ```
55
+
56
+ ## Connect from your workstation (agents)
57
+
58
+ Point the MCP bridge at this daemon β€” details and ready-made agent configs live in the companion repo:
59
+
60
+ ```bash
61
+ pnpm dlx @vernikr/browser-agent-mcp init --client claude-code
62
+ ```
63
+
64
+ ## What you get
65
+
66
+ | Capability | Detail |
67
+ |---|---|
68
+ | Real Chrome, never headless | `google-chrome-stable` headed on on-demand `Xvfb` (1280Γ—720x24) + `openbox` |
69
+ | Stealth CDP | Hand-rolled RFC-6455 client; `Runtime.enable`/`Console.enable`/`Debugger.enable` refused by construction |
70
+ | Human-like input | X11 XTEST mouse on randomized cubic BΓ©zier curves with ease-in/out pacing and Gaussian micro-jitter; human hold delays 55–125 ms |
71
+ | Cloudflare Turnstile | Detects the interstitial, locates the checkbox via the DOM and clicks it with the real X11 mouse |
72
+ | RAM discipline | `--renderer-process-limit=1`, JS heap capped at 256 MB, tab reset after each fetch, auto-hibernation after idle (default 60 s) β‡’ ~12 MB resting |
73
+ | One at a time | Global mutex: 1 tab across all callers |
74
+ | Interfaces | CLI `browser-agent`, HTTP API `127.0.0.1:8765`, Python SDK `browser_agent.BrowserClient` |
75
+ | Live view | `browser-agent vnc start` + SSH-forwarded VNC |
76
+ | Optional WARP route | Auto-retry blocked pages through a local SOCKS5 proxy (`127.0.0.1:40000`) |
77
+
78
+ ## Docs
79
+
80
+ - [`docs/deployment.md`](docs/deployment.md) β€” provisioning, systemd, memory limits, WARP, VNC logins
81
+ - [`docs/api.md`](docs/api.md) β€” HTTP contract (v1)
82
+ - [`docs/architecture.md`](docs/architecture.md) β€” how and why it works
83
+ - [`docs/security.md`](docs/security.md) β€” trust boundaries (loopback-only, no auth)
84
+ - [`docs/troubleshooting.md`](docs/troubleshooting.md) β€” symptom β†’ cause β†’ action
85
+
86
+ ## For AI agents
87
+
88
+ Install one-liner: `uv tool install browser-agent-server && sudo browser-agent setup --with-chrome && browser-agent doctor`
89
+ Self-check: `browser-agent doctor --json` (exit 0 = host ready).
90
+ Companion MCP for agent hosts: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp).
91
+
92
+ ## Platform support
93
+
94
+ Linux/X11 only (Xvfb, XTEST). Works great on a 1 vCPU / 1 GB RAM VPS. macOS/Windows workstations should run the [MCP bridge](https://github.com/vernikr/browser-agent-mcp) and talk to a Linux host running this daemon.
95
+
96
+ ## License
97
+
98
+ MIT β€” see [LICENSE](LICENSE).
@@ -0,0 +1,75 @@
1
+ # browser-agent
2
+
3
+ > πŸ”— **Companion project:** [`@vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) β€” the MCP server that exposes this daemon to LLM agents (Claude Code, Cursor, Freebuff…). The two projects are developed together and speak the HTTP contract in [`docs/api.md`](docs/api.md).
4
+
5
+ **Real headful Chrome on Xvfb with a hardware-level X11 mouse.** One localhost daemon gives any project, script, or AI agent on a Linux box a genuine desktop browser: steered over real XTEST input events on randomized BΓ©zier paths, with zero-leak CDP (no `Runtime.enable`), Cloudflare Turnstile auto-solve, and RAM discipline for tiny VPSes (auto-hibernates to ~12 MB idle).
6
+
7
+ Why not Playwright/CDP directly: mainstream automation stacks call `Runtime.enable` / `Debugger.enable` on connect β€” the primary headless-detection vector. This daemon's CDP client refuses those methods by construction and reads the DOM via `DOM.getOuterHTML`; all mouse/keyboard input goes through the X server, so clicks are `isTrusted: true` hardware events.
8
+
9
+ ## Install & provision (server, Ubuntu 24.04)
10
+
11
+ ```bash
12
+ curl -LsSf https://astral.sh/uv/install.sh | sh # if uv is missing
13
+ uv tool install browser-agent-server
14
+ sudo browser-agent setup --with-chrome # apt deps + google-chrome-stable + systemd unit
15
+ sudo browser-agent doctor # self-check
16
+ ```
17
+
18
+ Use it three ways:
19
+
20
+ ```bash
21
+ browser-agent fetch "https://example.com" --text # CLI
22
+ curl -s 127.0.0.1:8765/status # HTTP API (any language)
23
+ ```
24
+
25
+ ```python
26
+ from browser_agent import BrowserClient # Python SDK
27
+
28
+ client = BrowserClient()
29
+ page = client.fetch("https://example.com")
30
+ print(page["title"], len(page["text"]))
31
+ ```
32
+
33
+ ## Connect from your workstation (agents)
34
+
35
+ Point the MCP bridge at this daemon β€” details and ready-made agent configs live in the companion repo:
36
+
37
+ ```bash
38
+ pnpm dlx @vernikr/browser-agent-mcp init --client claude-code
39
+ ```
40
+
41
+ ## What you get
42
+
43
+ | Capability | Detail |
44
+ |---|---|
45
+ | Real Chrome, never headless | `google-chrome-stable` headed on on-demand `Xvfb` (1280Γ—720x24) + `openbox` |
46
+ | Stealth CDP | Hand-rolled RFC-6455 client; `Runtime.enable`/`Console.enable`/`Debugger.enable` refused by construction |
47
+ | Human-like input | X11 XTEST mouse on randomized cubic BΓ©zier curves with ease-in/out pacing and Gaussian micro-jitter; human hold delays 55–125 ms |
48
+ | Cloudflare Turnstile | Detects the interstitial, locates the checkbox via the DOM and clicks it with the real X11 mouse |
49
+ | RAM discipline | `--renderer-process-limit=1`, JS heap capped at 256 MB, tab reset after each fetch, auto-hibernation after idle (default 60 s) β‡’ ~12 MB resting |
50
+ | One at a time | Global mutex: 1 tab across all callers |
51
+ | Interfaces | CLI `browser-agent`, HTTP API `127.0.0.1:8765`, Python SDK `browser_agent.BrowserClient` |
52
+ | Live view | `browser-agent vnc start` + SSH-forwarded VNC |
53
+ | Optional WARP route | Auto-retry blocked pages through a local SOCKS5 proxy (`127.0.0.1:40000`) |
54
+
55
+ ## Docs
56
+
57
+ - [`docs/deployment.md`](docs/deployment.md) β€” provisioning, systemd, memory limits, WARP, VNC logins
58
+ - [`docs/api.md`](docs/api.md) β€” HTTP contract (v1)
59
+ - [`docs/architecture.md`](docs/architecture.md) β€” how and why it works
60
+ - [`docs/security.md`](docs/security.md) β€” trust boundaries (loopback-only, no auth)
61
+ - [`docs/troubleshooting.md`](docs/troubleshooting.md) β€” symptom β†’ cause β†’ action
62
+
63
+ ## For AI agents
64
+
65
+ Install one-liner: `uv tool install browser-agent-server && sudo browser-agent setup --with-chrome && browser-agent doctor`
66
+ Self-check: `browser-agent doctor --json` (exit 0 = host ready).
67
+ Companion MCP for agent hosts: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp).
68
+
69
+ ## Platform support
70
+
71
+ Linux/X11 only (Xvfb, XTEST). Works great on a 1 vCPU / 1 GB RAM VPS. macOS/Windows workstations should run the [MCP bridge](https://github.com/vernikr/browser-agent-mcp) and talk to a Linux host running this daemon.
72
+
73
+ ## License
74
+
75
+ MIT β€” see [LICENSE](LICENSE).
@@ -0,0 +1,76 @@
1
+ # HTTP API β€” contract v1
2
+
3
+ > πŸ”— Companion project: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) implements this contract on the agent side. Any change here must be reflected there and in `api_version`.
4
+
5
+ The daemon listens on `127.0.0.1:8765` (loopback only, no authentication β€” see security.md).
6
+ `api_version: 1` is reported by `GET /status`; breaking changes bump the major version of both packages.
7
+
8
+ ## GET /status (aliases: /health, /)
9
+
10
+ ```json
11
+ {
12
+ "ok": true,
13
+ "api_version": 1,
14
+ "version": "1.0.0",
15
+ "chrome_running": false,
16
+ "x11_running": false,
17
+ "display": ":99",
18
+ "resolution": "1280x720",
19
+ "mouse_pos": [640, 360],
20
+ "warp_proxy_ready": true,
21
+ "idle_seconds": 512.3,
22
+ "idle_timeout_sec": 60
23
+ }
24
+ ```
25
+
26
+ `chrome_running: false` usually means *hibernating* β€” the first `/fetch` pays the cold start (seconds).
27
+
28
+ ## POST /fetch
29
+
30
+ ```json
31
+ { "url": "https://example.com", "wait_sec": 3.5, "solve_cloudflare": true, "use_proxy": null, "include_screenshot": false }
32
+ ```
33
+
34
+ `use_proxy`: `true` forces the WARP SOCKS5 route, `false` forbids it, `null` (default) lets the daemon auto-retry via WARP when the direct fetch looks blocked.
35
+
36
+ Response:
37
+
38
+ ```json
39
+ {
40
+ "ok": true, "blocked": false, "cloudflare_solved": true, "via_warp": false,
41
+ "url": "…", "title": "…", "html": "<full outer html>", "text": "<trafilatura-extracted>",
42
+ "cookies": [ { "name": "…" } ], "screenshot_b64": ""
43
+ }
44
+ ```
45
+
46
+ `text` falls back to a plain tag-stripped extraction if trafilatura finds <120 chars. `blocked: true` means a Cloudflare-style interstitial is still up after solving attempts.
47
+
48
+ ## POST /action
49
+
50
+ | action | payload | returns |
51
+ |---|---|---|
52
+ | `goto` | `url`, `wait_sec?`, `solve_cloudflare?` | `{ok, url, title}` |
53
+ | `move` | `x`, `y` | `{ok, mouse_pos}` |
54
+ | `click` | `x?` `y?` `button?` `double?` | `{ok, mouse_pos}` |
55
+ | `type` | `text`, `submit?` | `{ok}` |
56
+ | `key` | `key` (X11 name, e.g. `Return`, `Page_Down`) | `{ok}` |
57
+ | `scroll` | `clicks?`, `direction?` (`down`/`up`), `x?` `y?` | `{ok}` |
58
+ | `screenshot` | β€” | `{ok, screenshot_b64}` (1280Γ—720 PNG) |
59
+ | `html` | β€” | `{ok, url, title, html}` |
60
+ | `stop` | β€” | `{ok, stopped: true}` β€” hibernate now |
61
+ | `vnc_start` / `vnc_stop` | `port?` (default 5900) | `{ok, vnc_port?}` |
62
+
63
+ ## POST /agent
64
+
65
+ ```json
66
+ { "url": "https://example.com", "goal": "Find the pricing page and report the Pro price", "max_steps": 8, "use_proxy": null }
67
+ ```
68
+
69
+ Returns `{ "ok": true, "url", "title", "result", "steps": [ …decisions… ], "text" }`.
70
+ Requires a Gemini key (`GEMINI_API_KEY(S)` / `LLM_API_KEY`) in the daemon's environment. Slow and costly on small hosts β€” reserve for pages plain `/fetch` cannot handle.
71
+
72
+ ## Errors
73
+
74
+ - `500 {"ok": false, "error": "<message>"}` β€” validation errors, Chrome failures, upstream issues.
75
+ - `404 {"ok": false, "error": "…"}` β€” unknown endpoint.
76
+ - Every request holds the global session mutex; long calls serialize behind it.
@@ -0,0 +1,34 @@
1
+ # Architecture
2
+
3
+ > πŸ”— Companion project: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) β€” the agent-facing MCP server built on this daemon.
4
+
5
+ ```
6
+ caller ──HTTP──► BrowserCoordinator (mutex, idle reaper)
7
+ β”œβ”€ RealChromeSession ──► google-chrome-stable (headed, on Xvfb :99)
8
+ β”‚ └─ StealthCDPConnection (DOM.getDocument / Page.navigate / Network.getAllCookies)
9
+ β”œβ”€ X11Display ──► Xvfb + openbox + xdotool/scrot/x11vnc (XTEST input)
10
+ └─ vision_agent (Gemini Flash loop; screenshots + X11 actions)
11
+ ```
12
+
13
+ ## Why real Chrome + XTEST (bot-detection threat model)
14
+
15
+ 1. **Headless tells.** Headless Chrome leaks via `navigator.webdriver`, rendering quirks, missing GPU rasterization. We run official `google-chrome-stable` *headed* on a virtual display: to the site it is a desktop browser.
16
+ 2. **CDP tells.** Playwright/puppeteer call `Runtime.enable`, `Console.enable`, `Debugger.enable` at attach time β€” Cloudflare/DataDome/PerimeterX fingerprint exactly this. `StealthCDPConnection` is a minimal RFC-6455 WebSocket client that *refuses those methods by construction* (`FORBIDDEN_CDP_METHODS`) and only uses quiet domains: `Page.navigate`, `DOM.getDocument/getOuterHTML/querySelector/getBoxModel`, `Network.getAllCookies`, `Page.stopLoading`.
17
+ 3. **Synthetic input tells.** CDP `Input.dispatchMouseEvent` produces `isTrusted: false`-style anomalies and unnatural timing. We move the *real X11 cursor* with `xdotool` along randomized cubic BΓ©zier curves (smoothstep velocity profile + Gaussian micro-jitter) and fire physical `mousedown β†’ 55–125 ms hold β†’ mouseup` via XTEST. Clicks arrive as hardware events.
18
+ 4. **Turnstile.** Cloudflare's checkbox lives in a cross-origin iframe you cannot inspect deeply; we locate its screen box via `DOM.getBoxModel` on the iframe element (offset + known 28Γ—32 checkbox inset + chrome top-bar offset), warm the mouse up with a detour move, then XTEST-click. Falls back to the canonical interstitial coordinates on 1280Γ—720.
19
+
20
+ ## Lifecycle & RAM discipline
21
+
22
+ - Chrome and Xvfb are started **on demand**; after `idle_timeout_sec` (default 60) of no requests the reaper thread kills Chrome, openbox and Xvfb β‡’ daemon rests at ~12 MB (stdlib listener only).
23
+ First fetch after hibernation pays a cold start of a few seconds (design client timeouts accordingly: β‰₯60 s).
24
+ - Chrome flags: `--renderer-process-limit=1 --process-per-site --js-flags=--max-old-space-size=256`, no GPU, no background networking. Tab resets to `about:blank` after each `/fetch`.
25
+ - One global mutex β‡’ one tab at a time across all callers. Callers shall batch and order their fetch waves, not fan out.
26
+ - systemd unit ships `MemoryHigh`/`MemoryMax` + `OOMScoreAdjust=500` so a small VPS's other tenants never starve.
27
+
28
+ ## Text extraction
29
+
30
+ `trafilatura` (favor_recall, no comments/tables) β†’ fallback plain tag-strip when <120 chars. PDFs are *rendered* by Chrome (its built-in viewer) β€” text comes back empty; download such URLs via plain HTTP instead.
31
+
32
+ ## Why not raw CDP from the workstation (FAQ)
33
+
34
+ Pointing `chrome-devtools-mcp`/Playwright at a remote debugging port would (a) expose the CDP port beyond loopback, (b) re-introduce the `*.enable` calls this design exists to avoid, and (c) give a worse contract than the daemon's (clean text, cookies, titles, Cloudflare solving included). Use raw CDP only to *debug* the browser.
@@ -0,0 +1,73 @@
1
+ # Deployment
2
+
3
+ > πŸ”— Companion project: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) β€” run it on your workstation to give LLM agents access to this host.
4
+
5
+ ## Fresh host (Ubuntu 24.04, any Linux with systemd)
6
+
7
+ ```bash
8
+ curl -LsSf https://astral.sh/uv/install.sh | sh
9
+ uv tool install browser-agent-server
10
+ sudo browser-agent setup --with-chrome # apt deps + Chrome + systemd unit
11
+ sudo browser-agent doctor # verify
12
+ ```
13
+
14
+ `setup` flags: `--no-apt` (packages already installed), `--with-chrome` (Google apt repo), `--no-unit` / `--no-start`, `--user <u>`, `--env-file /etc/browser-agent.env`, `--memory-high 450M --memory-max 600M`, `--dry-run`.
15
+
16
+ After setup put keys into the env file (`chmod 600`):
17
+
18
+ ```bash
19
+ # /etc/browser-agent.env
20
+ GEMINI_API_KEYS=key1,key2 # only needed for `browser-agent agent` (vision loop)
21
+ ```
22
+
23
+ ## Configuration (environment variables)
24
+
25
+ | Var | Default | Meaning |
26
+ |---|---|---|
27
+ | `BROWSER_AGENT_PORT` | `8765` | HTTP listen port (always loopback) |
28
+ | `BROWSER_AGENT_IDLE_SEC` | `60` | hibernate Chrome+Xvfb after N idle seconds (min 15) |
29
+ | `BROWSER_AGENT_STATE_DIR` | `$XDG_STATE_HOME/browser-agent` | where `chrome.err` and defaults live |
30
+ | `BROWSER_AGENT_PROFILE_DIR` | `<state>/profile` | Chrome profile (logins persist here β€” see VNC below) |
31
+ | `CHROME_BIN` | autodetect | pin a specific Chrome/Chromium binary |
32
+ | `GEMINI_API_KEY(S)`, `GEMINI_API_KEY_1..10`, `LLM_API_KEY` | β€” | keys for the vision agent |
33
+
34
+ ## Upgrading from a legacy hand-rolled install
35
+
36
+ Older deployments kept the Chrome profile at `/tmp/browser-agent-profile` (with VNC-logged-in sessions). To keep those logins after switching to the packaged daemon, pin the old path once in the unit:
37
+
38
+ ```ini
39
+ # /etc/systemd/system/browser-agent.service.d/override.conf
40
+ [Service]
41
+ Environment=BROWSER_AGENT_PROFILE_DIR=/tmp/browser-agent-profile
42
+ ```
43
+
44
+ (`systemctl edit browser-agent`, then `systemctl restart browser-agent`.)
45
+
46
+ ## Service operations
47
+
48
+ ```bash
49
+ systemctl status browser-agent
50
+ journalctl -u browser-agent -f
51
+ browser-agent status # JSON: api_version, chrome/x11, idle, warp
52
+ browser-agent stop # hibernate now (free RAM)
53
+ uv tool upgrade browser-agent-server && sudo systemctl restart browser-agent
54
+ ```
55
+
56
+ ## Optional: Cloudflare WARP fallback route
57
+
58
+ Some sites distrust datacenter ASNs. Install `warp-svc` in local proxy mode (127.0.0.1:40000); the daemon detects it (`warp_proxy_ready`) and `/fetch` with `use_proxy: null` auto-retries blocked pages through it. Never let WARP become the system default route on a VPN host β€” local SOCKS5 mode only.
59
+
60
+ ## Logins once, over VNC
61
+
62
+ ```bash
63
+ browser-agent vnc start # on the server
64
+ ssh -L 5900:127.0.0.1:5900 user@<server> # on your machine
65
+ # connect any VNC viewer to localhost:5900, log in, then:
66
+ browser-agent vnc stop
67
+ ```
68
+
69
+ Sessions persist in `BROWSER_AGENT_PROFILE_DIR`. Protect that directory like a password store.
70
+
71
+ ## Resource expectations
72
+
73
+ Idle daemon ~12 MB. Active fetch: transient 300–600 MB (Chrome). Works on 1 vCPU/1 GB RAM; avoid parallel browser users and heavy co-tenants during fetch waves.
@@ -0,0 +1,32 @@
1
+ # Publishing `browser-agent-server` to PyPI
2
+
3
+ > πŸ”— Companion project: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) β€” same release cadence logic over there.
4
+
5
+ ## Automated (preferred): OIDC trusted publishing
6
+
7
+ `.github/workflows/release.yml` publishes on tags `v*` via
8
+ `pypa/gh-action-pypi-publish` with `id-token: write` (no stored secrets).
9
+ One-time setup on PyPI (only the account owner can do this):
10
+
11
+ 1. Log in at https://pypi.org β†’ **Account settings β†’ Publishing** (or: the project page β†’ **Manage β†’ Publishing**).
12
+ 2. Under **GitHub**, add a publisher: owner `vernikr`, repository `browser-agent`, workflow `release.yml`, environment `pypi`.
13
+ (For brand-new projects use β€œpending publishers” *before* the first release; for existing ones add it on the project's Publishing page.)
14
+ 3. Optionally on GitHub: repo **Settings β†’ Environments β†’ pypi** β€” add protection rules (required reviewers) for a human gate on releases.
15
+ 4. Release: `git tag v1.0.1 && git push origin v1.0.1` β€” CI builds, tests, publishes to PyPI and drafts the GitHub Release from CHANGELOG.md.
16
+
17
+ The workflow **skips the publish step when the version already exists on PyPI**, so re-runs and manual-then-tag flows stay green.
18
+
19
+ ## Manual (one-off, e.g. the very first release)
20
+
21
+ ```bash
22
+ uv build
23
+ uv publish --token "pypi-…" # or: UV_PUBLISH_TOKEN=pypi-… uv publish
24
+ ```
25
+
26
+ Then still tag, so the GitHub Release and CHANGELOG stay in sync: `git tag vX.Y.Z && git push origin vX.Y.Z`.
27
+
28
+ ## After publishing
29
+
30
+ - Verify: https://pypi.org/pypi/browser-agent-server/json (`info.version`)
31
+ - Bump CHANGELOG under `[Unreleased]` for the next cycle.
32
+ - Revoke the manual token once trusted publishing is proven (pypi.org β†’ Account settings β†’ API tokens).
@@ -0,0 +1,11 @@
1
+ # Security notes
2
+
3
+ > πŸ”— Companion project: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp).
4
+
5
+ - **No authentication on the HTTP API β€” loopback by design.** The daemon binds `127.0.0.1` only and must stay that way. Do not put it behind a public reverse proxy without adding your own auth layer.
6
+ - **Access pattern:** SSH local forward (`ssh -L 8766:127.0.0.1:8765 …`) or your private VPN/mesh. Client-side helper: `pnpm dlx @vernikr/browser-agent-mcp tunnel --ssh user@host`.
7
+ - **Secrets:** keys live in `/etc/browser-agent.env` (`chmod 600`, `EnvironmentFile=`). Never commit `.env`; CI runs gitleaks.
8
+ - **The Chrome profile is a secret store** (logged-in sessions, cookies). `BROWSER_AGENT_PROFILE_DIR` should be root-owned `0700`; exclude it from backups you sync elsewhere; wipe it to log out everywhere instantly.
9
+ - **CDP port 9222** binds `127.0.0.1` (`--remote-debugging-address=127.0.0.1`) β€” keep it that way; anyone with loopback can drive the browser.
10
+ - **Memory limits** (`MemoryHigh`/`MemoryMax`, `OOMScoreAdjust=500`) are part of the security posture on shared hosts: the OOM killer must pick this service before anything critical.
11
+ - The daemon runs **root or unprivileged** β€” unprivileged is fine as long as it can create the Xvfb display and write its state dir. `--no-sandbox` is added automatically only when euid is 0.
@@ -0,0 +1,19 @@
1
+ # Troubleshooting
2
+
3
+ > πŸ”— Companion project: [`vernikr/browser-agent-mcp`](https://github.com/vernikr/browser-agent-mcp) β€” client-side issues (tunnel, MCP handshake) are covered in *its* troubleshooting doc.
4
+
5
+ | Symptom | Likely cause | Action |
6
+ |---|---|---|
7
+ | `browser-agent status` β†’ daemon not running | unit down | `systemctl start browser-agent`; check `journalctl -u browser-agent -n 100` |
8
+ | First fetch of a wave times out | cold start (Xvfb+Chrome boot) | retry with larger `wait_sec`/client timeout; batch subsequent fetches while warm |
9
+ | Page returns interstitial/login form | challenge needs a human | log in once over VNC (see deployment.md); don't hammer in a loop |
10
+ | Repeated 403 on many sites | datacenter ASN distrusted | `fetch … --proxy` (WARP), else fetch from another route |
11
+ | `via_warp: false, warp_proxy_ready: false` | WARP not installed | optional; install `warp-svc` in local SOCKS5 mode |
12
+ | Chrome killed mid-fetch | memory pressure | avoid co-tenant load; keep `MemoryMax` tight; check `journalctl` for OOM |
13
+ | `doctor` says stale `/tmp/.X99-lock` | previous crash | remove the lock file or reboot; daemon also cleans it on start |
14
+ | `bin/xdotool` errors "Can't open display" | Xvfb not running | the daemon starts Xvfb on demand β€” call via API/CLI, don't drive xdotool manually |
15
+ | PDFs return empty `text` | Chrome renders PDFs in its viewer | download PDF URLs with plain HTTP (`curl`) instead |
16
+ | After upgrade: logins gone | profile dir moved to XDG default | pin `BROWSER_AGENT_PROFILE_DIR=/tmp/browser-agent-profile` (deployment.md β†’ Upgrading) |
17
+ | CDP port conflict | another Chrome debug instance | stop it or change nothing (daemon manages its own profile); check `ss -ltnp | grep 9222` |
18
+
19
+ Diagnostics shortcuts: `browser-agent doctor` (host) β†’ `browser-agent status` (daemon) β†’ `journalctl -u browser-agent -n 200` (logs).