Metadata-Version: 2.4
Name: X5Browser
Version: 1.3.0
Summary: Browser library for AI agents: real Chrome over CDP (every action returns result + stable map + screenshot), ad-defense shield, live view.
Author: X5Coder
License: MIT
Keywords: browser,chrome,cdp,ai-agent,automation,live-view,vnc,adblock
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9
Requires-Dist: websockets>=15
Provides-Extra: annotate
Requires-Dist: Pillow>=10; extra == "annotate"
Dynamic: license-file

# X5Browser

Real Chrome for AI agents: drive a headed Chromium over raw CDP (no Playwright/Puppeteer/Selenium), read pages as numbered text maps, survive ad-infested sites with a built-in shield, and hand the **same browser** to a human and back.

```bash
pip install X5Browser
```

```python
import asyncio
from x5browser import X5Browser, BrowserConfig

async def main():
    async with X5Browser(BrowserConfig.default()) as browser:
        await browser.navigate("https://example.com")
        print(await browser.page_snapshot())          # numbered element map
        refs = await browser.find_elements("Learn more")
        # ... pick a ref, e.g. 3
        result = await browser.click(ref=3)           # real mouse click
        print(result.message)
        print(result.observation)                     # fresh stable map, attached

asyncio.run(main())
```

Every **acting** tool waits for full page stability, then replies with
`message` + fresh stable element map + one screenshot — the agent always sees
the outcome at once, no extra snapshot call needed.

---

## 1. Two facades

| Class | Use it when | Import |
|---|---|---|
| `X5Browser` | You wire your own agent loop / UI | `from x5browser import X5Browser, BrowserConfig` |
| `LiveBrowser` | You want hidden Chrome + live link + timeline in one object | `from x5browser import LiveBrowser` |

```python
from x5browser import LiveBrowser

live = LiveBrowser()              # library profile data, headed Chrome
await live.start()
print(live.links())               # {"control": ..., "preview": ..., "ready": true}
print(live.live_url)              # raw noVNC link (open in any browser / <iframe>)
await live.navigate("https://example.com")
for step in live.steps():         # timeline rows with screenshots
    ...
await live.take_control()         # user drives the SAME browser
await live.agent_control()        # agent resumes with a fresh map
print(live.links())               # same links until close/restart
await live.stop_browser()         # quiet close, data kept
```
### Live view — one gated link

`live.links()` (or the `human_control` / `live_preview` tool replies) gives the
two session links: a **control** link (interactive) and a **preview** link
(watch-only). They are random, distinct, bound to the same session, and stay
identical until the browser closes or restarts; a restart returns a new pair.

The link points at the token gate:

- valid token + live browser -> `200` viewer HTML;
- anything else -> `410` `{"status": "Not Connect"}`;
- both answer `Access-Control-Allow-Origin: *`, so one plain `fetch` tells
  "live" (HTML) from "closed" (JSON) — the consumer's single check.

The viewer's mode is forced from the token (never the query string), so a
preview link can never send input. Session state is pushed over a single SSE
stream (`GET /api/events`), so a UI needs no polling; the pusher only runs
while a client is connected.

Run the gate (brings up the hidden stack; the browser stays off until a tool
needs it):

```bash
python scripts/run_ui.py            # gate: http://127.0.0.1:8790/view
```

On Windows the hidden stack runs in WSL: the library keeps the distro alive
(no idle shutdown) while it is in use, and the systemd units self-heal
(`Restart=always`), so a dropped broadcast returns on the same link.



## 2. Tool catalog (38 tools, 9 groups)

`X5Browser.tools()` returns the default 20-tool surface; `tools(full=True)`
returns all 38. Model descriptions are concise professional English.
All interaction is genuine input events (`Input.*`); there is no JS-drive tool.

### Perception (read-only)

| Tool | What it does | Example |
|---|---|---|
| `page_snapshot` | Numbered map of interactive elements (modals, iframes). Start here | `page_snapshot(scope="viewport")` |
| `read_text` | Visible page/element text | `read_text(ref=5)` |
| `find_elements` | Find by name/role, returns refs directly | `find_elements(query="checkout")` |
| `inspect_element` | Deep diagnosis: attributes, state, box, covering element, HTML | `inspect_element(ref=12)` |
| `screenshot` | Viewport/element/full-page capture; `annotate` draws ref numbers | `screenshot(annotate=true)` |
| `extract_data` | Structured JSON: table, list, links, forms, images, meta | `extract_data(kind="table")` |
| `get_page_info` | Tab state: URL, title, control, viewport, scroll, frames, focus | `get_page_info()` |

### Navigation

| Tool | What it does | Example |
|---|---|---|
| `navigate` | Open URL / back / forward / reload; replies with fresh map | `navigate(to="https://example.com")` |
| `wait_for` | Wait for text, element, URL, network idle, download, seconds | `wait_for(condition="text", value="Done")` |

### Interaction (real input + fresh stable map in every reply)

| Tool | What it does | Example |
|---|---|---|
| `click` | Real click; follows real hrefs directly; absorbs ad hijacks (auto-clean + one re-click) | `click(ref=7)` |
| `click_verify` | Click plus checks in one call | `click_verify(ref=7, checks=[...])` |
| `fill` | Type into a field; optional Enter | `fill(ref=8, text="hi", submit=true)` |
| `fill_form` | Fill typed fields + optional submit (needs approval) | `fill_form(fields=[{ref:8,value:"hi"}])` |
| `press_keys` | Key/shortcut, optional focus + repeat | `press_keys(keys="Ctrl+A")` |
| `scroll` | Wheel / to element / in container / to edge | `scroll(direction="down", amount=600)` |
| `hover` | Gradual hover; replies with appeared elements | `hover(ref=6)` |
| `hover_click` | Hover menu open + click item, one call | `hover_click(menu_ref=6, target_ref=9)` |
| `choose_option` | Dropdown value (native/custom), verified | `choose_option(ref=13, label="Egypt")` |
| `drag_drop` | Gradual drag between refs/coordinates | `drag_drop(from_ref=3, to_ref=9)` |
| `upload_files` | Attach workspace files (needs approval) | `upload_files(ref=20, paths=["id.png"])` |

### Tabs / events / downloads

| Tool | What it does | Example |
|---|---|---|
| `manage_tabs` | List, open, switch, close tabs | `manage_tabs(action="list")` |
| `get_events` | New tabs, dialogs, permissions, downloads, crashes | `get_events()` |
| `answer_popup` | Answer dialog/permission/file-chooser (do first) | `answer_popup(kind="dialog", accept=true)` |
| `manage_downloads` | List / wait / cancel downloads | `manage_downloads(action="list")` |

### DevTools / task

| Tool | What it does | Example |
|---|---|---|
| `read_console` | Last 500 console messages, filterable | `read_console(level="error")` |
| `console_command` | One JS expression, read value (diagnosis only; needs `allow_unsafe_js=True`) | `console_command(script="document.title")` |
| `list_requests` | Always-on network log; `id` gives full detail | `list_requests(url_contains="api")` |
| `export_page` | Save PDF/MHTML/HTML (needs approval) | `export_page(format="pdf", path="p.pdf")` |
| `verify_state` | Pass/fail checks, cheaper than a snapshot | `verify_state(checks=[...])` |
| `batch_actions` | Several tools in one call, refs remap | `batch_actions(actions=[...])` |

### Lifecycle / handoff

| Tool | What it does | Example |
|---|---|---|
| `browser_status` | Report running/closed + uptime + links; never starts the browser | `browser_status()` |
| `open_browser` | Start explicitly; **refuses** if it already runs | `open_browser()` |
| `close_browser` | Quiet close, data kept; **refuses** if already closed | `close_browser()` |
| `restart_browser` | Restart on the same profile; new link pair, old links die | `restart_browser()` |
| `human_control` | Hand the live browser to the user (non-blocking) | `human_control(reason="pay")` |
| `take_control` / `agent_control` | Hand control over / resume agent | `take_control()` |
| `live_preview` | Watch-only link + JSON | `live_preview()` |

**Lifecycle contract.** The browser is never closed automatically by age:
only `close_browser` (or the library's own `stop`/`shutdown`) closes it. Any
tool auto-opens the browser when it is closed, and every session mints one
control link and one preview link that stay identical until the browser
closes or restarts. Nothing starts the browser on its own at boot.

**Ref rules:** refs come from `page_snapshot`/`find_elements` and die on page
change (`ref_not_found` → re-snapshot). Optional `pre_wait`/`post_wait`
(0–30s) on every tool, and an optional `reason` (≤500 chars) where you can note
why you called the tool; it is echoed back in the reply.

## 3. Ad-defense shield (automatic, no page JS)

A layer between Chrome and the agent. Ads are received normally on the wire
and cancelled at network/tab/map/display layers, so sites see ordinary HTTP.

| Line | What it does | Env |
|---|---|---|
| `NET` | Block/stub ad requests via `Fetch.*` (incl. empty-VAST for pre-roll) | `DEFENSE_NET=1` |
| `TABS` | Close ad popups, restore hijacked tabs (≤5), same-tab href bypass + sacrifice re-click in `click` | `DEFENSE_TABS=1` |
| `TREE` | Hide ad iframes/labels from the agent map | on with `NET` |
| `HEURISTIC` | Ad paths, affiliate redirects, rotating CDNs, brand creatives, tracking-param strip | `DEFENSE_HEURISTIC=1` |
| `SELFHEAL` | One calm reload with the harshest layer off per broken site | `DEFENSE_SELFHEAL=1` |
| `SHADOW` | Inspector stylesheet (CDP CSS, no script): ads render invisibly, clicks pass through | `DEFENSE_SHADOW=1` |

`COSMETIC/STUBS/OVERLAY/MEDIA` stay **off by design** (they need injected page
script). `DNS/EXT` are deployment-time (see `x5browser/defense/policy.json`).

```python
print(browser.defense_report())   # per-line counters: the impact meter
```

**Allowlist** (`x5browser/defense/allowlist.yaml`, editable): OAuth/SSO,
payments + bank 3DS, captchas, embeds, fonts, CDNs are never touched — with
built-in fallbacks even if the file is corrupt. Extra hosts:
```yaml
extra_hosts:
  - my-sso.example.com
```

**Blocklists:** built-in core + `defense/lists/custom.txt` (hand-kept) +
optional full lists (231k hosts):
```bash
python scripts/update_adlists.py   # refresh lists/*.txt at build time
python scripts/measure_defense.py  # blocked counts per line
```

## 4. Configuration

```python
BrowserConfig(
    profile_dir="...",      # default: inside the package (persistent)
    downloads_dir="...",     # default: x5browser/downloads/ (in-package)
    debug_port=9222,         # loopback only, never public
    window_width=1280, window_height=800,
    headless=False,          # headed so live shows everything
    confirm_dangerous=True,  # approval gates for pay/delete/send/…
    allow_unsafe_js=False,   # enable console_command explicitly
    extra_chrome_args=(),    # appended verbatim
)
```

## 5. Same-browser handoff cycle

`agent → human_control → user → agent_control → agent`, same Chrome, same
profile, same tabs and cookies. No new session, no Done button. While the user
holds control, agent tools refuse with `control_not_agent`.

## 6. Security notes

- CDP binds `127.0.0.1` only; uploads/downloads confined to the workspace.
- Passwords and card numbers are masked (`••••`) in maps, logs, screenshots.
- No Playwright/Puppeteer/Selenium; no JS-driven interaction (`Input.*` only).
- Secrets: copy `.env.example` → `.env` (never committed).

## License

MIT — see `LICENSE`.
