Metadata-Version: 2.5
Name: scalebrowser
Version: 0.3.0
Summary: Official Python SDK for the Scalebrowser daemon — typed REST client + direct-CDP driver (nodriver-style).
Project-URL: Homepage, https://scalebrowser.net
Project-URL: Documentation, https://scalebrowser.net
Author: Scalebrowser
License-Expression: MIT
License-File: LICENSE
Keywords: agent-browser,ai-agents,browser,browser-automation,cdp,mcp,scalebrowser
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Requires-Dist: websockets>=13
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# scalebrowser — Python SDK

Official Python SDK for the [Scalebrowser](https://scalebrowser.net) daemon: a
typed REST client **plus a direct-CDP driver** (nodriver-style) for the
self-hosted browser infrastructure that gives each AI agent its own browser.

The driver plane is **direct-CDP, not** Playwright/Puppeteer: anti-bot stacks
block the Playwright control plane regardless of how good the browser patches
are. `start_profile` returns a `cdp_ws` endpoint and this SDK speaks the Chrome
DevTools Protocol over it directly. Credentials never leave the daemon and are
never logged by the SDK.

## Install

```bash
pip install scalebrowser
```

Requires Python ≥ 3.10 and depends on `httpx`, `websockets`, `pydantic` v2. The
SDK is MIT-licensed; the daemon it talks to is a separate, licensed product.

## Quickstart (sync)

```python
from scalebrowser import ScalebrowserClient, CreateProfileBody

sb = ScalebrowserClient(base_url="http://127.0.0.1:8787", token="…")

profile = sb.create_profile(CreateProfileBody(name="acct-01"))

# start → direct-CDP connect → navigate → humanized click → stop
with sb.launch(profile.id, headless=True) as page:
    page.navigate("https://example.com")
    print(page.evaluate("document.title"))
    page.humanize_click(120, 240)        # routed through the daemon trusted-input (G8)

sb.close()
```

## Quickstart (async)

```python
import asyncio
from scalebrowser import AsyncScalebrowserClient

async def main():
    async with AsyncScalebrowserClient(token="…") as sb:
        started = await sb.start_profile(profile_id, headless=True)   # StartProfileResult
        async with await sb.connect_cdp(started, profile_id) as page:
            await page.navigate("https://example.com")
            title = await page.evaluate("document.title")
            await page.humanize_click(120, 240)
        await sb.stop_profile(profile_id)

asyncio.run(main())
```

## REST surface

Every `/v1` endpoint is a typed method on the client, under the same name in
both the sync and the async client:

- **Profiles** — `list_profiles`, `get_profile`, `create_profile`,
  `update_profile`, `delete_profile`, `start_profile`, `stop_profile`
- **Bulk** — `bulk_create_profiles`, `bulk_start`, `bulk_stop`, `bulk_delete`,
  `bulk_assign_proxy`
- **Groups / Presets** — `list_groups`/`create_group`/`get_group`/`update_group`/`delete_group`,
  `list_presets`/`create_preset`/`get_preset`/`update_preset`/`delete_preset`,
  `get_persona_constraints`. A preset is `config` (what the profiles do:
  `geo_mode`, `proxy_id`, …) plus `constraints` (what they are: `country`, which
  pins the persona's language, timezone and voices). Both are typed
  (`PresetConfig` / `PresetConstraints`) and the daemon refuses an unknown key
  with a 400 — read the valid regions from `get_persona_constraints()` rather than
  hardcoding them.
- **Proxies** — `list_proxies`/`create_proxy`/`get_proxy`/`update_proxy`/`delete_proxy`/`check_proxy`,
  plus `check_proxy_config` (probe a config before saving it; pass `id` to reuse
  an existing proxy's stored credentials)
- **Extensions** — `list_extensions`, `attach_extension`, `detach_extension`,
  plus the daemon-wide library (`upload_extension`, `get_library_extension`,
  `delete_library_extension`). An attached package IS loaded into the browser at
  launch, under the canonical Web-Store id its own key derives
- **Credentials** — `list_credentials`, `put_credential`, `reveal_credential`
  (needs the vault password), `export_credentials`, `import_credentials`
- **Cookies** — `reveal_cookies`, the one route a cookie VALUE leaves through,
  behind the same vault password
- **Sessions** — `export_session`, `import_session`
- **Mailboxes** — `list_inboxes`, `create_inbox`, `update_inbox`, `delete_inbox`,
  `get_inbox_bindings`, `bind_inbox`, `unbind_inbox` — where a profile's
  confirmation codes arrive
- **Passkeys** — `list_passkeys`, `delete_passkey`. Metadata only: the private
  key has no field and no endpoint
- **Agent runs** — `list_runs`, `get_run`, `list_run_steps`, `get_run_shot`,
  `get_activity`. Read-only, all of it
- **Interruptions** — `list_interruption_locks`, `set_interruption_lock`,
  `list_interruption_rules`, `set_interruption_rule`,
  `delete_interruption_rule` — who may answer when the browser asks something
- **Artifacts** — `put_artifact` (hand the daemon a file to upload later),
  `get_artifact` (fetch a screenshot, download or saved PDF as bytes)
- **Input / Metrics / Account / Events** — `send_input`, `get_metrics`,
  `get_account`, `health`, `ready`, `events()`

```python
async for event in sb_async.events():        # SSE lifecycle stream (Bearer-authenticated)
    print(event.type)                          # typed: profile_started / profile_crashed / …
```

Errors map the daemon contract: `ApiError(status, code, message)` with codes
`4001–4010` (`ApiError.is_auth_error` for 401 / 4010); `NetworkError` when the
daemon is unreachable; `CdpError` for protocol-level failures.

## Direct-CDP driver

`CdpSession` (async) / `SyncCdpSession` give you:

- `send(method, params)` — any CDP command, awaited by `id`
- `navigate(url)`, `evaluate(expr, isolated=False)` — **never** calls
  `Runtime.enable` (a detection leak); isolated worlds via
  `create_isolated_world()`
- `on(method, cb)` / `events()` — subscribe to CDP events
- `humanize_move/click/type/scroll` — humanized OS-level input via the daemon

## Tests

```bash
pip install -e ".[dev]"
pytest                       # unit tests (mock REST + a real fake-CDP ws server)
SCALEBROWSER_E2E=1 pytest tests/test_e2e.py   # against a real daemon
```

## Contract assumptions

- Default base URL `http://127.0.0.1:8787`; Bearer token always.
- The trusted-input body beyond `{action, humanize}` (coordinates, `button`,
  `delta_x/y`, `text`) is an SDK convention — see `cdp.py`.
- Two endpoints are optional and answer 404 on a daemon without them, which the
  SDK treats as information rather than as an error: `get_metrics()` then derives
  running counts from profile state, and `get_account()` returns
  `licensed=False`, which is what "self-hosted, no control plane" means.
- Every method is present on BOTH clients under the same name. The sync client
  is a hand-written mirror over one background event loop; there is no duplicated
  endpoint logic.
