Metadata-Version: 2.4
Name: botmask
Version: 0.1.8
Summary: Containerized stealth browser automation toolkit (Brave + Patchright)
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: patchright
Requires-Dist: python-dotenv
Requires-Dist: browserforge
Requires-Dist: PyAutoGUI
Provides-Extra: dev

# botmask

> Containerized stealth browser automation toolkit. Installable as a plugin in other projects that need browser automation while evading anti-bot systems.

## What is this?

A reusable, containerized browser-automation kit that simulates realistic user behavior
(Bezier-curve mouse movement, inertia scrolling, natural delays, typo typing, warm-up
sessions, reading simulation, auth-barrier detection) on top of **Brave Browser + Patchright**.

Extracted into an isolated, installable package.
package so it can be dropped into any project that needs stealthy browser interaction.

## Stack

| Layer | Tool |
| --- | --- |
| Runtime | Docker / Docker Compose |
| Browser | **Brave** (Chromium; ad-blocking, anti-fingerprinting) |
| Automation | **Patchright** (undetectable Playwright fork, CDP) |
| Behavior simulation | `behavior` module (this project) |
| Config | `config` module, TOML + defaults |
| Hardening | BrowserForge headers (planned), OS-level input fallback (planned) |

**Decision (Phase 0, `docs/decision.md`): Brave only.** Firefox engines (camoufox,
invisible_playwright) were evaluated and discarded. Hard targets are handled with
OS-level input (Xvfb + xdotool/PyAutoGUI) + graceful challenge detection.

## How the AI knows where to click

See **`docs/interaction-model.md`**. Short version: the AI picks elements from a numbered
accessibility tree (never pixel coordinates); the script resolves element → bounding box →
natural mouse path. OS-level input maps viewport → screen coords via a one-time window
offset calibration.

## Container usage

### Local (real display) — `develop` target

Shares the host display into the container (Wayland socket + X11). `compose.yaml`
builds the `develop` stage (no virtual display stack).

```bash
cp .env.example .env   # adjust DISPLAY, WAYLAND_DISPLAY for your host
docker compose up -d --build
```

Volumes: `.:/app`, `/tmp/.X11-unix`, `/run/user/<UID>/wayland-0`. CDP exposed at `:9222`.

### Server (no monitor) — Xvfb + noVNC — `deploy` target

The server override lives in **`.gitlab/compose.yaml`**; a future GitLab CI step copies it
to `compose.override.yaml` (which Docker Compose auto-loads), then:

```bash
docker compose up -d --build   # target=deploy via the override
```

Runs Brave headful on an Xvfb virtual display; watch it at `http://<host>:6080` (noVNC) or
VNC at `:5900`. Same element→box→input logic — only the display source differs.

`start.sh` auto-detects the mode from the environment: with `DISPLAY`/`WAYLAND_DISPLAY`
present it runs the browser directly; with none (or `VIRTUAL_DISPLAY=true`) it boots the
Xvfb stack first. On every start it also provisions Python packages + browser binaries at
runtime (`pip install .` + `patchright install chromium`), cached in the `cache` volume
(`/root/.cache` — pip wheels + browsers) so the image itself stays small.

## Status

- **Phase 0 — Evaluation: COMPLETE** → `docs/decision.md`
- **Phase 1 — Package extraction & container scaffold: IN PROGRESS**
  - `config.py` migrated ✅ · Dockerfile (develop/deploy) + compose + start.sh scaffolded ✅
  - Fitts + overshoot mouse port ⏳ · `behavior.py` migration ⏳ (see `TODO.md`)

## Docker targets (multistage)

| Target | For | Display | Build |
| --- | --- | --- | --- |
| `develop` (default via compose.yaml) | local dev on host Wayland/X11 | host display | `docker build --target develop -t botmask:develop .` |
| `deploy` (default target of plain `docker build`) | headless machine / CI | Xvfb + noVNC | `docker build --target deploy -t botmask:deploy .` |

Base image is Debian `python:slim` — Brave requires glibc and cannot run on Alpine/musl.

## CDP access — the relay (cross-container/host consumers)

Brave **ignores** `--remote-debugging-address` and binds CDP to `127.0.0.1` only — a
published `9222:9222` port does **not** expose it to other containers or the host.
On boot, `_boot.py` therefore opens a relay:

```
0.0.0.0:<CDP_PORT+1>  ->  127.0.0.1:<CDP_PORT>     # default 9223 -> 9222
```

Verified live: from another container, `curl http://<browser-ip>:9223/json/version`
responds, while `:9222` directly is refused.

Two rules for consumers:

- **Connect via the browser container's IP**, not its compose DNS name. Brave's
  DevTools endpoint rejects requests whose `Host` header is not an IP or
  `localhost` (DNS-rebinding protection) — `http://browser:9223` returns
  `403 Host header is specified...`, `http://<ip>:9223` works.
- Same-container consumers (agent and browser in one container) just use
  `127.0.0.1:9223` — or `127.0.0.1:9222` directly, no relay needed.

TOML keys (in `botmask.toml`):
- `cdp_port = 9222` — Brave's debug port (loopback-only, always).
- `cdp_relay_port` — `"auto"` (default): relay only inside containers, where the
  loopback-only bind blocks cross-container access. On bare metal the consumer
  reaches `127.0.0.1:9222` directly and socat may not even be installed, so it
  is skipped. A number forces the relay on that port; `"off"` never relays.
- `args = [...]` — Brave launch arguments (falls back to a built-in
  anti-detection set plus ozone auto-detect when absent).

`DISPLAY` / `WAYLAND_DISPLAY` remain environment-driven: they are host-injected
values, not project config.

## Migration guide

See `docs/migration.md` for historical migration notes.
