scalebrowser 0.2.0__py3-none-any.whl
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.
- scalebrowser/__init__.py +234 -0
- scalebrowser/_http.py +180 -0
- scalebrowser/_sync.py +604 -0
- scalebrowser/_version.py +3 -0
- scalebrowser/cdp.py +429 -0
- scalebrowser/client.py +825 -0
- scalebrowser/errors.py +97 -0
- scalebrowser/events.py +52 -0
- scalebrowser/models.py +861 -0
- scalebrowser/models_control.py +128 -0
- scalebrowser/models_identity.py +130 -0
- scalebrowser/models_runs.py +56 -0
- scalebrowser-0.2.0.dist-info/METADATA +170 -0
- scalebrowser-0.2.0.dist-info/RECORD +16 -0
- scalebrowser-0.2.0.dist-info/WHEEL +4 -0
- scalebrowser-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""What the browser asks (interruptions), the bytes that go in and out
|
|
2
|
+
(artifacts), and what the licence says (account).
|
|
3
|
+
|
|
4
|
+
Interruption shapes mirror ``web-ui/src/api/types/credentials.ts``, the shared
|
|
5
|
+
source of contract truth.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal, Optional
|
|
11
|
+
|
|
12
|
+
from .models import _ReadModel, _WriteModel
|
|
13
|
+
|
|
14
|
+
#: What an operator has taken out of an agent's hands.
|
|
15
|
+
InterruptionLock = Literal["always_block", "always_allow", "agent_decides"]
|
|
16
|
+
|
|
17
|
+
#: One answer, in the browser's OWN vocabulary — hence ``allowOnce`` in
|
|
18
|
+
#: camelCase while everything else in this API is snake_case. The daemon's enum
|
|
19
|
+
#: is ``#[serde(rename_all = "camelCase")]``; sending ``allow_once`` is a 400.
|
|
20
|
+
InterruptionChoice = Literal["allow", "allowOnce", "block", "dismiss"]
|
|
21
|
+
|
|
22
|
+
#: Who wrote a standing rule. An agent must not be able to widen what the
|
|
23
|
+
#: operator allowed, so the two are told apart at rest.
|
|
24
|
+
InterruptionAuthor = Literal["operator", "agent"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InterruptionLockRow(_ReadModel):
|
|
28
|
+
kind: str
|
|
29
|
+
#: ``None`` for the account-wide row.
|
|
30
|
+
profile_id: Optional[str] = None
|
|
31
|
+
decision: InterruptionLock
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EffectiveInterruptionLock(_ReadModel):
|
|
35
|
+
"""What applies right now for one kind, account-wide."""
|
|
36
|
+
|
|
37
|
+
kind: str
|
|
38
|
+
#: ``None`` means the agent decides.
|
|
39
|
+
decision: Optional[InterruptionLock] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InterruptionLockView(_ReadModel):
|
|
43
|
+
"""Everything about locks in one call, ``effective`` included.
|
|
44
|
+
|
|
45
|
+
The daemon computes ``effective`` with the same function the launch path
|
|
46
|
+
uses. Recomputing it yourself would be a second opinion, and code that
|
|
47
|
+
disagrees with the browser is worse than code that shows nothing.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
kinds: list[str] = []
|
|
51
|
+
locked_by_default: list[str] = []
|
|
52
|
+
rows: list[InterruptionLockRow] = []
|
|
53
|
+
effective: list[EffectiveInterruptionLock] = []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SetInterruptionLockBody(_WriteModel):
|
|
57
|
+
kind: str
|
|
58
|
+
profile_id: Optional[str] = None
|
|
59
|
+
decision: InterruptionLock
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InterruptionRuleRow(_ReadModel):
|
|
63
|
+
origin: str
|
|
64
|
+
kind: str
|
|
65
|
+
choice: InterruptionChoice
|
|
66
|
+
author: InterruptionAuthor
|
|
67
|
+
profile_id: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SetInterruptionRuleBody(_WriteModel):
|
|
71
|
+
origin: str
|
|
72
|
+
kind: str
|
|
73
|
+
choice: InterruptionChoice
|
|
74
|
+
profile_id: Optional[str] = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ArtifactPutResult(_ReadModel):
|
|
78
|
+
"""The reply to a file handed in for a later upload."""
|
|
79
|
+
|
|
80
|
+
artifact_id: str
|
|
81
|
+
size_bytes: int
|
|
82
|
+
name: Optional[str] = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ArtifactBytes(_ReadModel):
|
|
86
|
+
"""Bytes plus what the daemon said they are."""
|
|
87
|
+
|
|
88
|
+
data: bytes
|
|
89
|
+
content_type: Optional[str] = None
|
|
90
|
+
#: Parsed out of ``Content-Disposition``, when the daemon sent one.
|
|
91
|
+
filename: Optional[str] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Account(_ReadModel):
|
|
95
|
+
"""Licence state, for display.
|
|
96
|
+
|
|
97
|
+
The daemon decides on every launch whether a start is allowed; this never
|
|
98
|
+
gets a say in it. Never gate your own code on it — a green light on a
|
|
99
|
+
profile the launch gate refuses is worse than no light.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
#: ``False`` on a self-hosted build with no control plane.
|
|
103
|
+
licensed: bool = False
|
|
104
|
+
plan: Optional[str] = None
|
|
105
|
+
status: Optional[str] = None
|
|
106
|
+
#: Browsers the subscription may run AT ONCE. Replaced the profile allowance
|
|
107
|
+
#: on 2026-08-18: profiles are unlimited on every plan now.
|
|
108
|
+
concurrency: Optional[int] = None
|
|
109
|
+
#: Browsers running on THIS machine, not the abo-wide count.
|
|
110
|
+
running_local: int = 0
|
|
111
|
+
#: Profiles stored on THIS machine.
|
|
112
|
+
profiles_local: int = 0
|
|
113
|
+
features: list[str] = []
|
|
114
|
+
#: Unix seconds; when the cached entitlement stops being valid.
|
|
115
|
+
expires_at: Optional[int] = None
|
|
116
|
+
permits_launch: bool = True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class HealthStatus(_ReadModel):
|
|
120
|
+
status: str
|
|
121
|
+
service: Optional[str] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ReadyStatus(_ReadModel):
|
|
125
|
+
status: str
|
|
126
|
+
#: Where the daemon looks for engines. Whether one is INSTALLED is a
|
|
127
|
+
#: launch-time question the preflight answers properly.
|
|
128
|
+
engines_dir: str = ""
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""The account layer: mailboxes, passkeys and the one door a cookie value leaves
|
|
2
|
+
through.
|
|
3
|
+
|
|
4
|
+
Mailbox and passkey shapes mirror ``web-ui/src/api/types/{inboxes,credentials}.ts``,
|
|
5
|
+
the shared source of contract truth. The cookie reveal has no UI counterpart: it
|
|
6
|
+
is a REST-only route deliberately kept off every screen.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal, Optional
|
|
12
|
+
|
|
13
|
+
from .models import _ReadModel, _WriteModel
|
|
14
|
+
|
|
15
|
+
#: Which channel a confirmation code arrives on.
|
|
16
|
+
InboxChannel = Literal["email", "sms"]
|
|
17
|
+
|
|
18
|
+
#: Why a credential disappeared. ``unknown`` is a reason this build cannot name,
|
|
19
|
+
#: and it deliberately claims nothing — reading it as ``operator`` would tell you
|
|
20
|
+
#: that you had deleted something you never touched.
|
|
21
|
+
PasskeyRetiredReason = Literal["operator", "agent", "site_revoked", "unknown"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Inbox(_ReadModel):
|
|
25
|
+
"""One configured mailbox. The password is never sent back."""
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
name: str
|
|
29
|
+
host: str
|
|
30
|
+
port: int
|
|
31
|
+
#: The mailbox login. Not a secret, and it is how two rows are told apart.
|
|
32
|
+
user: str
|
|
33
|
+
#: TLS from the first byte. ``False`` is only accepted towards this machine.
|
|
34
|
+
tls: bool = True
|
|
35
|
+
folder: str = "INBOX"
|
|
36
|
+
#: The domain new addresses are minted under. ``None`` for a mailbox with
|
|
37
|
+
#: exactly one address.
|
|
38
|
+
address_domain: Optional[str] = None
|
|
39
|
+
created_at: int
|
|
40
|
+
#: The stored password could not be decrypted — a restored database without
|
|
41
|
+
#: its key. The row stays visible and deletable; every use fails closed.
|
|
42
|
+
credentials_unreadable: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PutInboxBody(_WriteModel):
|
|
46
|
+
"""Create or update a mailbox.
|
|
47
|
+
|
|
48
|
+
An omitted ``password`` means "leave the stored one unchanged", never
|
|
49
|
+
"delete it" — which is why it is optional on an edit and required on create.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
host: str
|
|
54
|
+
port: int
|
|
55
|
+
user: str
|
|
56
|
+
password: Optional[str] = None
|
|
57
|
+
tls: bool = True
|
|
58
|
+
folder: str = "INBOX"
|
|
59
|
+
address_domain: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InboxBinding(_ReadModel):
|
|
63
|
+
"""What one profile is bound to, per channel."""
|
|
64
|
+
|
|
65
|
+
profile_id: str
|
|
66
|
+
channel: InboxChannel
|
|
67
|
+
#: The address or number this profile is known by.
|
|
68
|
+
address: str
|
|
69
|
+
#: The mailbox that answers. ``None`` means the operator's configured
|
|
70
|
+
#: command does, which is the only shape an SMS binding can have.
|
|
71
|
+
inbox_id: Optional[str] = None
|
|
72
|
+
created_at: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BindInboxBody(_WriteModel):
|
|
76
|
+
channel: InboxChannel
|
|
77
|
+
inbox_id: Optional[str] = None
|
|
78
|
+
#: Absent mints one under the mailbox's catch-all domain.
|
|
79
|
+
address: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InboxBindings(_ReadModel):
|
|
83
|
+
bindings: list[InboxBinding] = []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class PasskeyRow(_ReadModel):
|
|
87
|
+
"""One passkey of a profile — a login that needs no password.
|
|
88
|
+
|
|
89
|
+
``credential_id`` names the row for deletion and nothing else. The private
|
|
90
|
+
key has no field here and no endpoint: it IS the account, and it could not
|
|
91
|
+
be typed anywhere useful, so returning it would be risk without a purpose.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
credential_id: str
|
|
95
|
+
#: The site it belongs to, e.g. ``github.com``.
|
|
96
|
+
rp_id: str
|
|
97
|
+
user_name: Optional[str] = None
|
|
98
|
+
user_display: Optional[str] = None
|
|
99
|
+
created_at: int
|
|
100
|
+
last_used_at: Optional[int] = None
|
|
101
|
+
#: True when a stored secret cannot be decrypted — the sign-in WILL fail.
|
|
102
|
+
secrets_unreadable: bool = False
|
|
103
|
+
#: When the key stopped being usable, if it has. A retired key is never
|
|
104
|
+
#: loaded into the browser and never offered to an agent, but it stays
|
|
105
|
+
#: visible until it is cleared: an account that quietly stops working is the
|
|
106
|
+
#: first thing anyone asks about.
|
|
107
|
+
retired_at: Optional[int] = None
|
|
108
|
+
retired_reason: Optional[PasskeyRetiredReason] = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RevealedCookie(_ReadModel):
|
|
112
|
+
"""A cookie VALUE skips both the password and the second factor beside it."""
|
|
113
|
+
|
|
114
|
+
name: str
|
|
115
|
+
domain: str
|
|
116
|
+
path: str
|
|
117
|
+
value: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RevealCookiesBody(_WriteModel):
|
|
121
|
+
#: The same gate a far weaker password sits behind.
|
|
122
|
+
vault_password: str
|
|
123
|
+
domain: str
|
|
124
|
+
#: One cookie by name; omit for every cookie on the domain.
|
|
125
|
+
name: Optional[str] = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class RevealCookiesResult(_ReadModel):
|
|
129
|
+
domain: str
|
|
130
|
+
cookies: list[RevealedCookie] = []
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Agent runs and live activity (``/v1/runs``, ``/v1/activity``).
|
|
2
|
+
|
|
3
|
+
All of it is display: nothing here starts, stops or changes anything.
|
|
4
|
+
|
|
5
|
+
``RunActor``, ``RunOutcome``, ``StepKind``, ``StepTarget``, ``RunStep`` and
|
|
6
|
+
``ProfileActivity`` are NOT redeclared here — they live in :mod:`.models`
|
|
7
|
+
because the live event stream carries them, and the run list describes the same
|
|
8
|
+
rows. A second copy would let the two drift apart while both looked right.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Literal, Optional
|
|
14
|
+
|
|
15
|
+
from .models import ProfileActivity, RunActor, RunOutcome, _ReadModel
|
|
16
|
+
|
|
17
|
+
#: Who made the outcome claim. ``derived`` means nobody did.
|
|
18
|
+
OutcomeSource = Literal["agent", "check", "human", "derived"]
|
|
19
|
+
|
|
20
|
+
#: Whether the goal text was stated or merely describes the session.
|
|
21
|
+
GoalSource = Literal["agent", "derived"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentRun(_ReadModel):
|
|
25
|
+
"""One agent run, as the activity view reads it back."""
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
#: Groups runs that shared one profile reservation.
|
|
29
|
+
session_id: str
|
|
30
|
+
profile_id: str
|
|
31
|
+
profile_name: str
|
|
32
|
+
actor: RunActor
|
|
33
|
+
#: Never empty — described when it was not declared (see :attr:`goal_source`).
|
|
34
|
+
goal: str
|
|
35
|
+
goal_source: GoalSource
|
|
36
|
+
#: ``reached`` and ``partial`` are CLAIMS: the daemon can tell that a run
|
|
37
|
+
#: ended, never that it worked, so those two only ever come from an agent, a
|
|
38
|
+
#: check or a person.
|
|
39
|
+
outcome: RunOutcome
|
|
40
|
+
outcome_source: Optional[OutcomeSource] = None
|
|
41
|
+
note: Optional[str] = None
|
|
42
|
+
started_at: int
|
|
43
|
+
ended_at: Optional[int] = None
|
|
44
|
+
step_count: int = 0
|
|
45
|
+
#: Steps that never reached the trail because the writer queue was full.
|
|
46
|
+
#: Zero on a healthy run, and worth reading: a trail missing entries looks
|
|
47
|
+
#: exactly like an agent that did less work.
|
|
48
|
+
steps_lost: int = 0
|
|
49
|
+
first_domain: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ActivitySnapshot(_ReadModel):
|
|
53
|
+
"""What every running profile currently shows."""
|
|
54
|
+
|
|
55
|
+
sampled_at: Optional[int] = None
|
|
56
|
+
profiles: list[ProfileActivity] = []
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scalebrowser
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for the Scalebrowser daemon — typed REST client + direct-CDP driver (nodriver-style).
|
|
5
|
+
Project-URL: Homepage, https://scalebrowser.net
|
|
6
|
+
Project-URL: Documentation, https://scalebrowser.net
|
|
7
|
+
Author: Scalebrowser
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent-browser,ai-agents,browser,browser-automation,cdp,mcp,scalebrowser
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: pydantic>=2.7
|
|
23
|
+
Requires-Dist: websockets>=13
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# scalebrowser — Python SDK
|
|
31
|
+
|
|
32
|
+
Official Python SDK for the [Scalebrowser](https://scalebrowser.net) daemon: a
|
|
33
|
+
typed REST client **plus a direct-CDP driver** (nodriver-style) for the
|
|
34
|
+
self-hosted browser infrastructure that gives each AI agent its own browser.
|
|
35
|
+
|
|
36
|
+
The driver plane is **direct-CDP, not** Playwright/Puppeteer: anti-bot stacks
|
|
37
|
+
block the Playwright control plane regardless of how good the browser patches
|
|
38
|
+
are. `start_profile` returns a `cdp_ws` endpoint and this SDK speaks the Chrome
|
|
39
|
+
DevTools Protocol over it directly. Credentials never leave the daemon and are
|
|
40
|
+
never logged by the SDK.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install scalebrowser
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python ≥ 3.10 and depends on `httpx`, `websockets`, `pydantic` v2. The
|
|
49
|
+
SDK is MIT-licensed; the daemon it talks to is a separate, licensed product.
|
|
50
|
+
|
|
51
|
+
## Quickstart (sync)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from scalebrowser import ScalebrowserClient, CreateProfileBody
|
|
55
|
+
|
|
56
|
+
sb = ScalebrowserClient(base_url="http://127.0.0.1:8787", token="…")
|
|
57
|
+
|
|
58
|
+
profile = sb.create_profile(CreateProfileBody(name="acct-01"))
|
|
59
|
+
|
|
60
|
+
# start → direct-CDP connect → navigate → humanized click → stop
|
|
61
|
+
with sb.launch(profile.id, headless=True) as page:
|
|
62
|
+
page.navigate("https://example.com")
|
|
63
|
+
print(page.evaluate("document.title"))
|
|
64
|
+
page.humanize_click(120, 240) # routed through the daemon trusted-input (G8)
|
|
65
|
+
|
|
66
|
+
sb.close()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quickstart (async)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import asyncio
|
|
73
|
+
from scalebrowser import AsyncScalebrowserClient
|
|
74
|
+
|
|
75
|
+
async def main():
|
|
76
|
+
async with AsyncScalebrowserClient(token="…") as sb:
|
|
77
|
+
started = await sb.start_profile(profile_id, headless=True) # StartProfileResult
|
|
78
|
+
async with await sb.connect_cdp(started, profile_id) as page:
|
|
79
|
+
await page.navigate("https://example.com")
|
|
80
|
+
title = await page.evaluate("document.title")
|
|
81
|
+
await page.humanize_click(120, 240)
|
|
82
|
+
await sb.stop_profile(profile_id)
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## REST surface
|
|
88
|
+
|
|
89
|
+
Every `/v1` endpoint is a typed method on the client, under the same name in
|
|
90
|
+
both the sync and the async client:
|
|
91
|
+
|
|
92
|
+
- **Profiles** — `list_profiles`, `get_profile`, `create_profile`,
|
|
93
|
+
`update_profile`, `delete_profile`, `start_profile`, `stop_profile`
|
|
94
|
+
- **Bulk** — `bulk_create_profiles`, `bulk_start`, `bulk_stop`, `bulk_delete`,
|
|
95
|
+
`bulk_assign_proxy`
|
|
96
|
+
- **Groups / Presets** — `list_groups`/`create_group`/`get_group`/`update_group`/`delete_group`,
|
|
97
|
+
`list_presets`/`create_preset`/`get_preset`/`update_preset`/`delete_preset`,
|
|
98
|
+
`get_persona_constraints`. A preset is `config` (what the profiles do:
|
|
99
|
+
`geo_mode`, `proxy_id`, …) plus `constraints` (what they are: `country`, which
|
|
100
|
+
pins the persona's language, timezone and voices). Both are typed
|
|
101
|
+
(`PresetConfig` / `PresetConstraints`) and the daemon refuses an unknown key
|
|
102
|
+
with a 400 — read the valid regions from `get_persona_constraints()` rather than
|
|
103
|
+
hardcoding them.
|
|
104
|
+
- **Proxies** — `list_proxies`/`create_proxy`/`get_proxy`/`update_proxy`/`delete_proxy`/`check_proxy`,
|
|
105
|
+
plus `check_proxy_config` (probe a config before saving it; pass `id` to reuse
|
|
106
|
+
an existing proxy's stored credentials)
|
|
107
|
+
- **Extensions** — `list_extensions`, `attach_extension`, `detach_extension`,
|
|
108
|
+
plus the daemon-wide library (`upload_extension`, `get_library_extension`,
|
|
109
|
+
`delete_library_extension`). An attached package IS loaded into the browser at
|
|
110
|
+
launch, under the canonical Web-Store id its own key derives
|
|
111
|
+
- **Credentials** — `list_credentials`, `put_credential`, `reveal_credential`
|
|
112
|
+
(needs the vault password), `export_credentials`, `import_credentials`
|
|
113
|
+
- **Cookies** — `reveal_cookies`, the one route a cookie VALUE leaves through,
|
|
114
|
+
behind the same vault password
|
|
115
|
+
- **Sessions** — `export_session`, `import_session`
|
|
116
|
+
- **Mailboxes** — `list_inboxes`, `create_inbox`, `update_inbox`, `delete_inbox`,
|
|
117
|
+
`get_inbox_bindings`, `bind_inbox`, `unbind_inbox` — where a profile's
|
|
118
|
+
confirmation codes arrive
|
|
119
|
+
- **Passkeys** — `list_passkeys`, `delete_passkey`. Metadata only: the private
|
|
120
|
+
key has no field and no endpoint
|
|
121
|
+
- **Agent runs** — `list_runs`, `get_run`, `list_run_steps`, `get_run_shot`,
|
|
122
|
+
`get_activity`. Read-only, all of it
|
|
123
|
+
- **Interruptions** — `list_interruption_locks`, `set_interruption_lock`,
|
|
124
|
+
`list_interruption_rules`, `set_interruption_rule`,
|
|
125
|
+
`delete_interruption_rule` — who may answer when the browser asks something
|
|
126
|
+
- **Artifacts** — `put_artifact` (hand the daemon a file to upload later),
|
|
127
|
+
`get_artifact` (fetch a screenshot, download or saved PDF as bytes)
|
|
128
|
+
- **Input / Metrics / Account / Events** — `send_input`, `get_metrics`,
|
|
129
|
+
`get_account`, `health`, `ready`, `events()`
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
async for event in sb_async.events(): # SSE lifecycle stream (Bearer-authenticated)
|
|
133
|
+
print(event.type) # typed: profile_started / profile_crashed / …
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Errors map the daemon contract: `ApiError(status, code, message)` with codes
|
|
137
|
+
`4001–4010` (`ApiError.is_auth_error` for 401 / 4010); `NetworkError` when the
|
|
138
|
+
daemon is unreachable; `CdpError` for protocol-level failures.
|
|
139
|
+
|
|
140
|
+
## Direct-CDP driver
|
|
141
|
+
|
|
142
|
+
`CdpSession` (async) / `SyncCdpSession` give you:
|
|
143
|
+
|
|
144
|
+
- `send(method, params)` — any CDP command, awaited by `id`
|
|
145
|
+
- `navigate(url)`, `evaluate(expr, isolated=False)` — **never** calls
|
|
146
|
+
`Runtime.enable` (a detection leak); isolated worlds via
|
|
147
|
+
`create_isolated_world()`
|
|
148
|
+
- `on(method, cb)` / `events()` — subscribe to CDP events
|
|
149
|
+
- `humanize_move/click/type/scroll` — humanized OS-level input via the daemon
|
|
150
|
+
|
|
151
|
+
## Tests
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
pip install -e ".[dev]"
|
|
155
|
+
pytest # unit tests (mock REST + a real fake-CDP ws server)
|
|
156
|
+
SCALEBROWSER_E2E=1 pytest tests/test_e2e.py # against a real daemon
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Contract assumptions
|
|
160
|
+
|
|
161
|
+
- Default base URL `http://127.0.0.1:8787`; Bearer token always.
|
|
162
|
+
- The trusted-input body beyond `{action, humanize}` (coordinates, `button`,
|
|
163
|
+
`delta_x/y`, `text`) is an SDK convention — see `cdp.py`.
|
|
164
|
+
- Two endpoints are optional and answer 404 on a daemon without them, which the
|
|
165
|
+
SDK treats as information rather than as an error: `get_metrics()` then derives
|
|
166
|
+
running counts from profile state, and `get_account()` returns
|
|
167
|
+
`licensed=False`, which is what "self-hosted, no control plane" means.
|
|
168
|
+
- Every method is present on BOTH clients under the same name. The sync client
|
|
169
|
+
is a hand-written mirror over one background event loop; there is no duplicated
|
|
170
|
+
endpoint logic.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
scalebrowser/__init__.py,sha256=LO-KXc0vC5Nwxxv8iRg4Y3u0FHk77gweFUwSSBoRlhQ,5232
|
|
2
|
+
scalebrowser/_http.py,sha256=Jy8s8uVVNf1gXbu0KyL0IxFRcHPsPkGHvGHXWgWXZBc,7302
|
|
3
|
+
scalebrowser/_sync.py,sha256=aH5Wvo-kwmOWxi0k0a588YgTXT28TjmFt1FwscWF3bA,24565
|
|
4
|
+
scalebrowser/_version.py,sha256=jo6gYau27_G4AJyjriElyTAix57i6fx3UJk35fJuhF8,67
|
|
5
|
+
scalebrowser/cdp.py,sha256=YpRfSK2SsUZS6lud-IUO72K6VKPODBUgQTzePxGYdZs,16506
|
|
6
|
+
scalebrowser/client.py,sha256=lQaDPYuFqXX-risuY4pY2xLTNvyk7IZUaGWCoK7-Pug,36616
|
|
7
|
+
scalebrowser/errors.py,sha256=Du98Ow_Z2wdtLZyvptXSQhcPXvgnwWSIwPAWVg-UsPo,3337
|
|
8
|
+
scalebrowser/events.py,sha256=a65ZypdsFJxWAe06nAErPRKdGNbj8_jU8yUm-HQ2F20,1686
|
|
9
|
+
scalebrowser/models.py,sha256=sAbn_bZjP48C3kQuhZNrC9Hp5kv8SOYH7OyYnfLtZNU,24974
|
|
10
|
+
scalebrowser/models_control.py,sha256=GMZYF7tO7jYiODSYfz7wCXYIID3lplD1NzLl9KMt4Fc,3950
|
|
11
|
+
scalebrowser/models_identity.py,sha256=y1xtclyf2lWGKCXx4TGw1OcD1siLK69uVkhxnx-UrTY,4206
|
|
12
|
+
scalebrowser/models_runs.py,sha256=ul45KLZa2RctK6QQ54LzjLFtwPkfRT04bE6bUJNLlZA,2007
|
|
13
|
+
scalebrowser-0.2.0.dist-info/METADATA,sha256=6QD2SpinSZIwpjcbiemW_qiCmRDe0ZLzrfgxb0m1D8U,7567
|
|
14
|
+
scalebrowser-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
15
|
+
scalebrowser-0.2.0.dist-info/licenses/LICENSE,sha256=_ev9UGCBOfQuc_ESy8jXDlrXrTeUb-t3wg3we1JDTbg,1069
|
|
16
|
+
scalebrowser-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scalebrowser
|
|
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.
|