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.
- browser_agent_server-1.0.0/.github/workflows/ci.yml +35 -0
- browser_agent_server-1.0.0/.github/workflows/release.yml +73 -0
- browser_agent_server-1.0.0/.gitignore +9 -0
- browser_agent_server-1.0.0/AGENTS.md +23 -0
- browser_agent_server-1.0.0/CHANGELOG.md +20 -0
- browser_agent_server-1.0.0/LICENSE +21 -0
- browser_agent_server-1.0.0/PKG-INFO +98 -0
- browser_agent_server-1.0.0/README.md +75 -0
- browser_agent_server-1.0.0/docs/api.md +76 -0
- browser_agent_server-1.0.0/docs/architecture.md +34 -0
- browser_agent_server-1.0.0/docs/deployment.md +73 -0
- browser_agent_server-1.0.0/docs/publishing.md +32 -0
- browser_agent_server-1.0.0/docs/security.md +11 -0
- browser_agent_server-1.0.0/docs/troubleshooting.md +19 -0
- browser_agent_server-1.0.0/pyproject.toml +48 -0
- browser_agent_server-1.0.0/src/browser_agent/__init__.py +128 -0
- browser_agent_server-1.0.0/src/browser_agent/chrome.py +591 -0
- browser_agent_server-1.0.0/src/browser_agent/cli.py +414 -0
- browser_agent_server-1.0.0/src/browser_agent/data/browser-agent.service +26 -0
- browser_agent_server-1.0.0/src/browser_agent/server.py +250 -0
- browser_agent_server-1.0.0/src/browser_agent/vision_agent.py +165 -0
- browser_agent_server-1.0.0/src/browser_agent/x11_input.py +273 -0
- browser_agent_server-1.0.0/tests/test_bezier.py +23 -0
- browser_agent_server-1.0.0/tests/test_cli.py +59 -0
- browser_agent_server-1.0.0/tests/test_client.py +78 -0
- browser_agent_server-1.0.0/tests/test_cloudflare.py +21 -0
- browser_agent_server-1.0.0/tests/test_http_api.py +84 -0
- browser_agent_server-1.0.0/tests/test_ws_frames.py +118 -0
- 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,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).
|