Metadata-Version: 2.5
Name: scalebrowser
Version: 1.2.0
Summary: Official Python SDK for the Scalebrowser daemon: typed REST client plus a direct-CDP driver (nodriver-style).
Project-URL: Homepage, https://scalebrowser.net
Project-URL: Documentation, https://scalebrowser.net
Project-URL: Examples, https://github.com/blackdiamond-labs/scalebrowser-mcp/tree/main/examples/python
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'
Requires-Dist: tomli>=2.4.1; (python_version < '3.11') and extra == 'dev'
Description-Content-Type: text/markdown

# scalebrowser: the 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's humanized input

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`, `list_profile_ids`, `count_profiles`,
  `get_profile`, `create_profile`, `update_profile`, `delete_profile`,
  `start_profile`, `stop_profile`. `release_profile` drops an agent's lease
  without its handle (the operator's way out), and `takeover_profile` asks for a
  profile that is open on another machine of your account
- **Bulk**: `bulk_create_profiles`, `bulk_start`, `bulk_stop`, `bulk_delete`,
  `bulk_assign_proxy`, `bulk_assign_extensions`
- **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, so 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 (`list_library_extensions`, `upload_extension`,
  `get_library_extension`, `delete_library_extension`,
  `delete_library_extension_version`). 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`, `delete_credential`,
  `reveal_credential` (needs the vault password), `export_credentials`,
  `import_credentials`. The vault password itself: `vault_status`,
  `set_vault_password`
- **Secret layer**: `get_secret_layer`, `set_secret_layer`, `list_secrets`,
  `update_secret`, `delete_secret`. `scrub_text` filters a text you hold against
  a profile's values, and `run_with_secrets` runs a command with named values in
  its environment, so no value crosses a socket
- **Cookies**: `reveal_cookies`, the one route a cookie VALUE leaves through,
  behind the same vault password
- **Recorded requests**: `reveal_request`, the one route what a request carried (query,
  header values, body) 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`, which is 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)
- **Tasks and memory**: `list_tasks`, `create_tasks`, `cancel_task`,
  `delete_task`, plus a profile's standing instructions for agents, its
  `PROFILE.md`: `get_profile_memory`, `put_profile_memory`,
  `delete_profile_memory`
- **Videos**: `list_videos`, `get_video`, `rename_video`, `delete_video`,
  `cancel_video`, `rerender_video`, `video_access`, `download_video_file`,
  `videos_storage`; recordings with `list_recordings`, `get_recording`,
  `delete_recording_raw`, `render_recording`; public links with `share_video`,
  `share_status`, `revoke_share`
- **Video presets**: `list_video_presets`, `get_video_preset`,
  `create_video_preset`, `update_video_preset`, `duplicate_video_preset`,
  `delete_video_preset`, and a look at an unsaved config with
  `preview_video_preset`, `preview_video_preset_clip`,
  `download_video_preset_clip`. The files a preset points at: `list_media`,
  `upload_media`, `delete_media`, `media_file`. Narration keys:
  `list_tts_providers`, `put_tts_provider`, `verify_tts_provider`,
  `delete_tts_provider`. Encoders: `get_codecs`, `set_codec_enabled`
- **Fingerprint audit**: `start_audit` drives the profile's running browser
  through the external detector pages, `get_audit` reads the state and the last
  report
- **Exit addresses**: `get_exit_status` (which addresses a profile owns), and the
  machine's rule that one address belongs to one profile: `get_exit_rule`,
  `set_exit_rule`
- **Machine settings**: `get_remote_access`, `set_remote_access` (may a chat
  service reach this machine's browsers), `get_agent_tools`,
  `set_agent_tool_group` (which optional MCP tool groups agents see),
  `get_connection` (which installation answered: its data directory and its
  binary)
- **Input / Metrics / Account / Events**: `send_input`, `get_metrics`,
  `get_account`, `health`, `ready`, `events()`. Close the client with `close()`
  (sync) or `aclose()` (async), or use it as a context manager

```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–4013` (`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)`, which **never** call
  `Runtime.enable` (a detection leak); isolated worlds via
  `create_isolated_world()`. The wait for the load reads from an isolated world,
  so a page that watches its own `document.readyState` sees no foreign read
- `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 CDP endpoint is guarded by the OS user, not by that token.** The engine's
  DevTools port has no authentication of its own: it answers a bogus bearer with
  `200`, measured, so the engine drops any connection whose peer process runs as
  a different user. Nothing to configure and nothing to pass: your process is the
  one that started the profile, so it is on the allowed side. A helper running as
  another account will not get in, by design.
- 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.
