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
scalebrowser/models.py
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
"""Wire models — a 1:1 mirror of the daemon's serialized domain model and the
|
|
2
|
+
endpoint contract in ``docs/features/api.md`` (kept in lockstep with
|
|
3
|
+
``web-ui/src/api/types.ts``).
|
|
4
|
+
|
|
5
|
+
All Rust enums serialize ``snake_case``. Timestamps are unix seconds (``int``).
|
|
6
|
+
Read models tolerate unknown fields (forward-compat); request models are plain
|
|
7
|
+
optional-field carriers. This module is the single source of contract truth for
|
|
8
|
+
the SDK: backend drift is a change here + ``client.py``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Annotated, Literal, Optional, Union
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
|
16
|
+
|
|
17
|
+
# ── Enums (string literals; Rust serializes snake_case) ───────────────────────
|
|
18
|
+
|
|
19
|
+
RuntimeState = Literal["stopped", "starting", "running", "crashed"]
|
|
20
|
+
RUNTIME_STATES: tuple[RuntimeState, ...] = ("stopped", "starting", "running", "crashed")
|
|
21
|
+
|
|
22
|
+
HostMode = Literal["linux_engine_spoof", "local_native_windows"]
|
|
23
|
+
HOST_MODES: tuple[HostMode, ...] = ("linux_engine_spoof", "local_native_windows")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
GeoMode = Literal["strict_expected", "follow_exit", "off"]
|
|
27
|
+
GEO_MODES: tuple[GeoMode, ...] = ("strict_expected", "follow_exit", "off")
|
|
28
|
+
|
|
29
|
+
ProxyKind = Literal["http", "socks5", "gateway"]
|
|
30
|
+
PROXY_KINDS: tuple[ProxyKind, ...] = ("http", "socks5", "gateway")
|
|
31
|
+
|
|
32
|
+
Rotation = Literal["sticky", "rotating"]
|
|
33
|
+
ROTATIONS: tuple[Rotation, ...] = ("sticky", "rotating")
|
|
34
|
+
|
|
35
|
+
SessionKind = Literal["cookies", "local_storage", "indexed_db", "service_worker"]
|
|
36
|
+
|
|
37
|
+
OsFamily = Literal["windows", "macos", "linux"]
|
|
38
|
+
|
|
39
|
+
#: Who is driving: an agent holding a reservation, or a person at a window.
|
|
40
|
+
RunActor = Literal["agent", "human"]
|
|
41
|
+
|
|
42
|
+
#: How a run ended. ``reached``/``partial`` appear only when a source claimed it —
|
|
43
|
+
#: the daemon can tell that a run ended, never that it worked.
|
|
44
|
+
RunOutcome = Literal[
|
|
45
|
+
"running", "reached", "partial", "blocked", "interrupted", "unknown"
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
#: What kind of thing a trail entry is.
|
|
49
|
+
StepKind = Literal["page", "action", "interruption"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _ReadModel(BaseModel):
|
|
53
|
+
"""Base for models read from the daemon — tolerant of unknown fields."""
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _WriteModel(BaseModel):
|
|
59
|
+
"""Base for request payloads the SDK sends to the daemon."""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── Persona (read-only; derived deterministically from `seed`) ────────────────
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Screen(_ReadModel):
|
|
68
|
+
width: int
|
|
69
|
+
height: int
|
|
70
|
+
avail_width: int
|
|
71
|
+
avail_height: int
|
|
72
|
+
color_depth: int
|
|
73
|
+
pixel_depth: int
|
|
74
|
+
device_pixel_ratio: float
|
|
75
|
+
orientation: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Locale(_ReadModel):
|
|
79
|
+
language: str
|
|
80
|
+
languages: list[str]
|
|
81
|
+
accept_language: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class WebGl(_ReadModel):
|
|
85
|
+
vendor: str
|
|
86
|
+
renderer: str
|
|
87
|
+
unmasked_vendor: str
|
|
88
|
+
unmasked_renderer: str
|
|
89
|
+
version: str
|
|
90
|
+
shading_language_version: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class WebGpu(_ReadModel):
|
|
94
|
+
present: bool
|
|
95
|
+
adapter: str
|
|
96
|
+
vendor: str
|
|
97
|
+
architecture: str
|
|
98
|
+
device: str
|
|
99
|
+
description: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AudioProps(_ReadModel):
|
|
103
|
+
sample_rate: int
|
|
104
|
+
base_latency: float
|
|
105
|
+
max_channel_count: int
|
|
106
|
+
output_channel_count: int
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SpeechVoice(_ReadModel):
|
|
110
|
+
name: str
|
|
111
|
+
lang: str
|
|
112
|
+
local_service: bool
|
|
113
|
+
default: bool
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class GpuPersona(_ReadModel):
|
|
117
|
+
backend: str
|
|
118
|
+
vendor: str
|
|
119
|
+
model: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class Brand(_ReadModel):
|
|
123
|
+
brand: str
|
|
124
|
+
version: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ClientHints(_ReadModel):
|
|
128
|
+
brands: list[Brand]
|
|
129
|
+
full_version_list: list[Brand]
|
|
130
|
+
platform: str
|
|
131
|
+
platform_version: str
|
|
132
|
+
architecture: str
|
|
133
|
+
bitness: str
|
|
134
|
+
model: str
|
|
135
|
+
mobile: bool
|
|
136
|
+
ua_full_version: str
|
|
137
|
+
wow64: bool
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class PermissionDefaults(_ReadModel):
|
|
141
|
+
notifications: str
|
|
142
|
+
geolocation: str
|
|
143
|
+
camera: str
|
|
144
|
+
microphone: str
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class MediaDevice(_ReadModel):
|
|
148
|
+
kind: str
|
|
149
|
+
label: str
|
|
150
|
+
device_id: str
|
|
151
|
+
group_id: str
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class PersonaMisc(_ReadModel):
|
|
155
|
+
webdriver: bool
|
|
156
|
+
get_installed_related_apps: bool
|
|
157
|
+
max_touch_points: int
|
|
158
|
+
pdf_viewer_enabled: bool
|
|
159
|
+
headless_guards: bool
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Persona(_ReadModel):
|
|
163
|
+
os: OsFamily
|
|
164
|
+
user_agent: str
|
|
165
|
+
platform: str
|
|
166
|
+
browser_version: str
|
|
167
|
+
screen: Screen
|
|
168
|
+
hardware_concurrency: int
|
|
169
|
+
device_memory: int
|
|
170
|
+
timezone: str
|
|
171
|
+
locale: Locale
|
|
172
|
+
webgl: WebGl
|
|
173
|
+
webgpu: WebGpu
|
|
174
|
+
fonts: list[str]
|
|
175
|
+
noise_seed: int
|
|
176
|
+
audio: AudioProps
|
|
177
|
+
speech_voices: list[SpeechVoice]
|
|
178
|
+
gpu_persona: GpuPersona
|
|
179
|
+
client_hints: ClientHints
|
|
180
|
+
permissions: PermissionDefaults
|
|
181
|
+
media_devices: list[MediaDevice]
|
|
182
|
+
misc: PersonaMisc
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── Resources ─────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
#: How protected a profile is right now, decided by the daemon WITHOUT starting
|
|
189
|
+
#: a browser — it runs the same gates the launch runs. ``blocked`` means the
|
|
190
|
+
#: launch will refuse this profile.
|
|
191
|
+
ProtectionLevel = Literal["ok", "warn", "blocked"]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ProtectionStatus(_ReadModel):
|
|
195
|
+
level: ProtectionLevel
|
|
196
|
+
#: Plain-language findings, worst first. Empty exactly when ``level`` is ``ok``.
|
|
197
|
+
reasons: list[str] = []
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Profile(_ReadModel):
|
|
201
|
+
"""A browser identity profile (``profiles`` table)."""
|
|
202
|
+
|
|
203
|
+
id: str
|
|
204
|
+
name: str
|
|
205
|
+
runtime_state: RuntimeState
|
|
206
|
+
enabled: bool
|
|
207
|
+
crash_reason: Optional[str] = None
|
|
208
|
+
pid: Optional[int] = None
|
|
209
|
+
run_lock: Optional[str] = None
|
|
210
|
+
seed: str
|
|
211
|
+
engine_version: str
|
|
212
|
+
persona: Persona
|
|
213
|
+
host_mode: HostMode
|
|
214
|
+
gpu_persona: Optional[str] = None
|
|
215
|
+
group_id: Optional[str] = None
|
|
216
|
+
proxy_id: Optional[str] = None
|
|
217
|
+
geo_mode: GeoMode
|
|
218
|
+
expected_country: Optional[str] = None
|
|
219
|
+
data_dir: str
|
|
220
|
+
created_at: int
|
|
221
|
+
last_open_at: Optional[int] = None
|
|
222
|
+
#: Additive: present on list/get (``ProfileView``), absent on create/delete/
|
|
223
|
+
#: bulk, which answer the bare profile.
|
|
224
|
+
protection: Optional[ProtectionStatus] = None
|
|
225
|
+
#: Platform slugs with a stored login, sorted by the daemon. Additive — a
|
|
226
|
+
#: daemon predating the credential vault simply omits it.
|
|
227
|
+
platforms: list[str] = []
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class Group(_ReadModel):
|
|
231
|
+
id: str
|
|
232
|
+
name: str
|
|
233
|
+
created_at: int
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class PresetConfig(_ReadModel):
|
|
237
|
+
"""Operating config: what the created profiles DO.
|
|
238
|
+
|
|
239
|
+
Every field mirrors a ``POST /v1/profiles`` body field. The daemon refuses an
|
|
240
|
+
unknown key with a 400 instead of dropping it — a preset makes up to 1000
|
|
241
|
+
profiles, so a swallowed key is a thousand wrong ones.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
# Pin the batch to one engine build. Omit for per-profile version diversity.
|
|
245
|
+
engine_version: Optional[str] = None
|
|
246
|
+
host_mode: Optional[HostMode] = None
|
|
247
|
+
geo_mode: Optional[GeoMode] = None
|
|
248
|
+
# The geo GATE's expectation — not the persona's drawn region.
|
|
249
|
+
expected_country: Optional[str] = None
|
|
250
|
+
group_id: Optional[str] = None
|
|
251
|
+
proxy_id: Optional[str] = None
|
|
252
|
+
#: Library ids every profile of the batch is created with. ``None`` leaves the
|
|
253
|
+
#: key out of the request entirely, which the daemon reads as "none" — the same
|
|
254
|
+
#: shape every other key of this half has.
|
|
255
|
+
extensions: Optional[list[str]] = None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class PresetConstraints(_ReadModel):
|
|
259
|
+
"""Generator constraints: what the created profiles ARE.
|
|
260
|
+
|
|
261
|
+
Exactly one axis, because an axis is only honest here if the daemon does not
|
|
262
|
+
overwrite it at launch — the GPU, screen, DPR and Chrome version all are.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
# ISO-3166 alpha-2 from ``GET /v1/persona/constraints``. Pins language,
|
|
266
|
+
# timezone and speech voices together.
|
|
267
|
+
country: Optional[str] = None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class Preset(_ReadModel):
|
|
271
|
+
id: str
|
|
272
|
+
name: str
|
|
273
|
+
constraints: PresetConstraints = PresetConstraints()
|
|
274
|
+
config: PresetConfig = PresetConfig()
|
|
275
|
+
created_at: int
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class PersonaConstraintOptions(_ReadModel):
|
|
279
|
+
"""``GET /v1/persona/constraints`` — the values a preset may constrain to."""
|
|
280
|
+
|
|
281
|
+
countries: list[str] = []
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class Proxy(_ReadModel):
|
|
285
|
+
"""Proxy — credentials are write-only on the daemon; never returned."""
|
|
286
|
+
|
|
287
|
+
id: str
|
|
288
|
+
kind: ProxyKind
|
|
289
|
+
host: str
|
|
290
|
+
port: int
|
|
291
|
+
rotation: Rotation
|
|
292
|
+
last_exit_ip: Optional[str] = None
|
|
293
|
+
last_country: Optional[str] = None
|
|
294
|
+
last_check_at: Optional[int] = None
|
|
295
|
+
healthy: Optional[bool] = None
|
|
296
|
+
created_at: int
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ── Request payloads ──────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class CreateProfileBody(_WriteModel):
|
|
303
|
+
name: str
|
|
304
|
+
seed: Optional[str] = None
|
|
305
|
+
host_mode: Optional[HostMode] = None
|
|
306
|
+
geo_mode: Optional[GeoMode] = None
|
|
307
|
+
expected_country: Optional[str] = None
|
|
308
|
+
group_id: Optional[str] = None
|
|
309
|
+
proxy_id: Optional[str] = None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class UpdateProfileBody(_WriteModel):
|
|
313
|
+
# host_mode and seed are fixed at create time (C4/C5): a DIFFERING host_mode is
|
|
314
|
+
# refused with 400, an unchanged echo is accepted. A tier change needs recreate.
|
|
315
|
+
name: Optional[str] = None
|
|
316
|
+
geo_mode: Optional[GeoMode] = None
|
|
317
|
+
expected_country: Optional[str] = None
|
|
318
|
+
group_id: Optional[str] = None
|
|
319
|
+
proxy_id: Optional[str] = None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class StartProfileBody(_WriteModel):
|
|
323
|
+
headless: Optional[bool] = None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class StartProfileResult(_ReadModel):
|
|
327
|
+
"""`POST /v1/profiles/:id/start` response (server `StartResp`)."""
|
|
328
|
+
|
|
329
|
+
profile_id: str
|
|
330
|
+
cdp_ws: str
|
|
331
|
+
# Parsed by the daemon out of `cdp_ws`; null when the URL carries no port.
|
|
332
|
+
debug_port: Optional[int] = None
|
|
333
|
+
headless: bool
|
|
334
|
+
pid: Optional[int] = None
|
|
335
|
+
started_at: int
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class StopProfileResult(_ReadModel):
|
|
339
|
+
stopped: bool
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class BulkCreateBody(_WriteModel):
|
|
343
|
+
preset_id: str
|
|
344
|
+
count: int
|
|
345
|
+
#: Base name for the batch; the daemon appends an index per profile.
|
|
346
|
+
name_prefix: Optional[str] = None
|
|
347
|
+
#: Drop the whole batch into this group at create time.
|
|
348
|
+
group_id: Optional[str] = None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class BulkIdsBody(_WriteModel):
|
|
352
|
+
ids: list[str]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class BulkAssignProxyBody(_WriteModel):
|
|
356
|
+
ids: list[str]
|
|
357
|
+
proxy_id: str
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class CreateProxyBody(_WriteModel):
|
|
361
|
+
kind: ProxyKind
|
|
362
|
+
host: str
|
|
363
|
+
port: int
|
|
364
|
+
username: Optional[str] = None
|
|
365
|
+
password: Optional[str] = None
|
|
366
|
+
rotation: Optional[Rotation] = None
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class UpdateProxyBody(_WriteModel):
|
|
370
|
+
kind: Optional[ProxyKind] = None
|
|
371
|
+
host: Optional[str] = None
|
|
372
|
+
port: Optional[int] = None
|
|
373
|
+
username: Optional[str] = None
|
|
374
|
+
password: Optional[str] = None
|
|
375
|
+
rotation: Optional[Rotation] = None
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class ProxyCheckResult(_ReadModel):
|
|
379
|
+
"""Result of ``POST /v1/proxies/:id/check`` (health + geo + JA4↔JA4T verdict)."""
|
|
380
|
+
|
|
381
|
+
healthy: bool
|
|
382
|
+
exit_ip: Optional[str] = None
|
|
383
|
+
country: Optional[str] = None
|
|
384
|
+
#: Carrier/mobile-exit hint (a mobile exit is the best case for anti-detect).
|
|
385
|
+
is_mobile: Optional[bool] = None
|
|
386
|
+
ja4: Optional[str] = None
|
|
387
|
+
ja4t: Optional[str] = None
|
|
388
|
+
ja4t_mismatch: Optional[bool] = None
|
|
389
|
+
warnings: Optional[list[str]] = None
|
|
390
|
+
checked_at: Optional[int] = None
|
|
391
|
+
#: Round-trip RTT — ``None`` on an unreachable exit (a timeout is not a latency).
|
|
392
|
+
latency_ms: Optional[int] = None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class CheckProxyConfigBody(_WriteModel):
|
|
396
|
+
"""Body of ``POST /v1/proxies/check`` — probe a config that is NOT persisted.
|
|
397
|
+
|
|
398
|
+
Edit-mode: pass ``id`` of an existing proxy and omit ``username``/``password``
|
|
399
|
+
to probe with its stored (write-only) credentials. A proxy whose credentials
|
|
400
|
+
cannot be decrypted fails closed instead of probing unauthenticated.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
kind: ProxyKind
|
|
404
|
+
host: str
|
|
405
|
+
port: int
|
|
406
|
+
username: Optional[str] = None
|
|
407
|
+
password: Optional[str] = None
|
|
408
|
+
rotation: Optional[Rotation] = None
|
|
409
|
+
#: Existing proxy id — enables the stored-credential fallback.
|
|
410
|
+
id: Optional[str] = None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class Extension(_ReadModel):
|
|
414
|
+
"""One package in the library, keyed by id AND version.
|
|
415
|
+
|
|
416
|
+
The library stocks several versions of the same extension; each profile draws
|
|
417
|
+
one from its own seed, because a fleet in lockstep on one version is itself
|
|
418
|
+
unusual (real auto-update rolls out gradually).
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
#: Canonical Web-Store id, derived from the package's own key. Repeats across versions.
|
|
422
|
+
id: str
|
|
423
|
+
name: str
|
|
424
|
+
version: str
|
|
425
|
+
#: Base64 DER public key lifted from the CRX header.
|
|
426
|
+
public_key: str
|
|
427
|
+
#: Unpacked library directory (a profile launches from its own copy). Derived
|
|
428
|
+
#: by the daemon from its data dir rather than stored, so a moved data dir does
|
|
429
|
+
#: not leave every row pointing at nothing.
|
|
430
|
+
dir: str
|
|
431
|
+
#: SHA-256 of the unpacked tree — names the CONTENT of this version.
|
|
432
|
+
content_hash: str = ""
|
|
433
|
+
added_at: int
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class ExtensionPolicy(_ReadModel):
|
|
437
|
+
"""How a profile's extension set reaches the browser.
|
|
438
|
+
|
|
439
|
+
``engine_switches`` is what the launch actually emits. ``id_model`` is
|
|
440
|
+
``"canonical"``: the id comes from the package's key and is the same for every
|
|
441
|
+
profile — deliberately, since that is the id millions of real users report.
|
|
442
|
+
Only the FILES are per-profile (``delivery = "per_profile_copy"``).
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
id_model: str
|
|
446
|
+
delivery: str
|
|
447
|
+
engine_switches: list[str]
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class ExtensionAdvice(_ReadModel):
|
|
451
|
+
"""One finding about a profile's extension set."""
|
|
452
|
+
|
|
453
|
+
#: ``warn`` — a launch is never blocked by an extension.
|
|
454
|
+
level: str
|
|
455
|
+
message: str
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class ExtensionsResult(_ReadModel):
|
|
459
|
+
"""Response of every ``/v1/profiles/:id/extensions`` call: the set + the policy."""
|
|
460
|
+
|
|
461
|
+
profile_id: str
|
|
462
|
+
#: Assigned library ids, as an idempotent set.
|
|
463
|
+
extensions: list[str]
|
|
464
|
+
#: The set this profile actually loads — ONE version per extension, drawn from the
|
|
465
|
+
#: profile's seed. Shorter than ``extensions`` when an id no longer resolves.
|
|
466
|
+
packages: list[Extension]
|
|
467
|
+
policy: ExtensionPolicy
|
|
468
|
+
#: What is worth knowing about THIS set, judged by the daemon with the same
|
|
469
|
+
#: function the launch gate uses. Empty for an unremarkable set — including an
|
|
470
|
+
#: empty one. Never recompute it: a second opinion could contradict the gate.
|
|
471
|
+
advice: list[ExtensionAdvice] = []
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class CredentialMeta(_ReadModel):
|
|
475
|
+
"""One stored platform login — everything about it EXCEPT the secrets.
|
|
476
|
+
|
|
477
|
+
The username is returned because it is the account's display identity, not a
|
|
478
|
+
secret on any platform; without it two stored accounts are indistinguishable.
|
|
479
|
+
The password and the two-factor key never travel here — only through
|
|
480
|
+
:meth:`Client.reveal_credential`, which needs the vault password.
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
platform: str
|
|
484
|
+
username: str | None = None
|
|
485
|
+
login_url: str | None = None
|
|
486
|
+
has_password: bool
|
|
487
|
+
has_totp: bool
|
|
488
|
+
#: Typed into a sign-up form but never confirmed — the account may not exist.
|
|
489
|
+
pending: bool = False
|
|
490
|
+
created_at: int
|
|
491
|
+
updated_at: int
|
|
492
|
+
last_used_at: int | None = None
|
|
493
|
+
#: The stored blobs no longer open with the current master key. The row stays
|
|
494
|
+
#: listable and deletable; every USE of it fails closed.
|
|
495
|
+
secrets_unreadable: bool = False
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class RevealedCredential(_ReadModel):
|
|
499
|
+
"""The plaintext of one login. The only shape in this SDK that carries one."""
|
|
500
|
+
|
|
501
|
+
platform: str
|
|
502
|
+
username: str | None = None
|
|
503
|
+
password: str | None = None
|
|
504
|
+
totp_secret: str | None = None
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
class CredentialBundle(_ReadModel):
|
|
508
|
+
"""A passphrase-sealed backup of one profile's logins."""
|
|
509
|
+
|
|
510
|
+
bundle: str
|
|
511
|
+
count: int
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class CredentialImportResult(_ReadModel):
|
|
515
|
+
"""What a restore put back."""
|
|
516
|
+
|
|
517
|
+
#: Boolean, like the session import — the number is in ``count``.
|
|
518
|
+
imported: bool
|
|
519
|
+
count: int
|
|
520
|
+
platforms: list[str]
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class VaultStatus(_ReadModel):
|
|
524
|
+
"""Whether a vault password has been set on this daemon."""
|
|
525
|
+
|
|
526
|
+
configured: bool
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class AuditDetectorReport(_ReadModel):
|
|
530
|
+
"""One detector's outcome. ``skip`` means the page could not be evaluated —
|
|
531
|
+
NOT that it passed."""
|
|
532
|
+
|
|
533
|
+
id: str
|
|
534
|
+
verdict: str
|
|
535
|
+
detail: str
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class AuditReport(_ReadModel):
|
|
539
|
+
"""What the detector sites saw on one launch."""
|
|
540
|
+
|
|
541
|
+
schema_version: int
|
|
542
|
+
timestamp: str
|
|
543
|
+
overall: str
|
|
544
|
+
detectors: list[AuditDetectorReport]
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class AuditStatus(_ReadModel):
|
|
548
|
+
"""State of a profile's detector audit, plus the last stored report."""
|
|
549
|
+
|
|
550
|
+
profile_id: str
|
|
551
|
+
#: ``idle`` (never run since the daemon started), ``running`` or ``finished``.
|
|
552
|
+
state: str
|
|
553
|
+
started_at: Optional[int] = None
|
|
554
|
+
finished_at: Optional[int] = None
|
|
555
|
+
#: Why the RUN failed — distinct from a detector's own ``skip``.
|
|
556
|
+
error: Optional[str] = None
|
|
557
|
+
report: Optional[AuditReport] = None
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class CreateGroupBody(_WriteModel):
|
|
561
|
+
name: str
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class UpdateGroupBody(_WriteModel):
|
|
565
|
+
name: Optional[str] = None
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class CreatePresetBody(_WriteModel):
|
|
569
|
+
name: str
|
|
570
|
+
constraints: Optional[PresetConstraints] = None
|
|
571
|
+
config: Optional[PresetConfig] = None
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class UpdatePresetBody(_WriteModel):
|
|
575
|
+
"""Partial update; an omitted half is left as stored."""
|
|
576
|
+
|
|
577
|
+
name: Optional[str] = None
|
|
578
|
+
constraints: Optional[PresetConstraints] = None
|
|
579
|
+
config: Optional[PresetConfig] = None
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class SessionExportBody(_WriteModel):
|
|
583
|
+
password: str
|
|
584
|
+
kinds: Optional[list[SessionKind]] = None
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class SessionExportResult(_ReadModel):
|
|
588
|
+
bundle: str
|
|
589
|
+
harvested: Optional[bool] = None
|
|
590
|
+
"""Whether the cookies came out of the RUNNING browser for this export.
|
|
591
|
+
|
|
592
|
+
``False`` means the bundle carries only what was stored earlier, which for a
|
|
593
|
+
stopped profile is all there can be: a browser holds its jar in its own
|
|
594
|
+
process. Export while the profile runs for a current session.
|
|
595
|
+
"""
|
|
596
|
+
note: Optional[str] = None
|
|
597
|
+
"""Why the bundle is thinner than it could be, when it is."""
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
class SessionImportBody(_WriteModel):
|
|
601
|
+
password: str
|
|
602
|
+
bundle: str
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
ProbeState = Literal["measured", "unsupported", "unavailable"]
|
|
606
|
+
"""Availability of one probed figure: really measured, not supported on this
|
|
607
|
+
host/HW, or temporarily unavailable (probe error / not yet sampled)."""
|
|
608
|
+
|
|
609
|
+
MetricsSource = Literal["live", "derived"]
|
|
610
|
+
"""`live` = served by the daemon's sampler; `derived` = client fallback (an old
|
|
611
|
+
daemon without the endpoint)."""
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class MetricsAvailability(_ReadModel):
|
|
615
|
+
ram: ProbeState
|
|
616
|
+
cpu: ProbeState
|
|
617
|
+
gpu: ProbeState
|
|
618
|
+
vram: ProbeState
|
|
619
|
+
per_profile: ProbeState
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class ProfileResource(_ReadModel):
|
|
623
|
+
profile_id: str
|
|
624
|
+
name: str
|
|
625
|
+
pid: Optional[int] = None
|
|
626
|
+
ram_mb: Optional[int] = None
|
|
627
|
+
cpu_pct: Optional[float] = None
|
|
628
|
+
vram_mb: Optional[int] = None
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class Metrics(_ReadModel):
|
|
632
|
+
running: int
|
|
633
|
+
capacity_max_concurrent: int
|
|
634
|
+
ram_used_mb: Optional[int] = None
|
|
635
|
+
# Never null on the wire: Rust serializes a bare `u64` (`metrics/mod.rs`).
|
|
636
|
+
# The Optional both mirrors used to carry was recalled, not read.
|
|
637
|
+
ram_budget_mb: int
|
|
638
|
+
vram_used_mb: Optional[int] = None
|
|
639
|
+
vram_budget_mb: Optional[int] = None
|
|
640
|
+
cpu_pct: Optional[float] = None
|
|
641
|
+
gpu_pct: Optional[float] = None
|
|
642
|
+
# Additive: distinguish a real value from "not on this host" vs "measuring".
|
|
643
|
+
source: Optional[MetricsSource] = None
|
|
644
|
+
sampled_at: Optional[int] = None
|
|
645
|
+
availability: Optional[MetricsAvailability] = None
|
|
646
|
+
profiles: list[ProfileResource] = Field(default_factory=list)
|
|
647
|
+
#: Launchable host memory (MiB) — the capacity gate's own reading. "How many
|
|
648
|
+
#: more browsers fit" derives from THIS; ``ram_budget_mb − ram_used_mb`` does
|
|
649
|
+
#: not subtract (used is host-wide, the budget is the daemon's own ceiling).
|
|
650
|
+
ram_available_mb: Optional[int] = None
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# ── Trusted input (G8, AC-API-003) ───────────────────────────────────────────
|
|
654
|
+
|
|
655
|
+
InputAction = Literal["move", "click", "type", "scroll"]
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# ── Events (SSE `/v1/events`, WS `/v1/ws`) — tagged union on `type` ───────────
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class ProfileStateChanged(_ReadModel):
|
|
662
|
+
type: Literal["profile_state_changed"]
|
|
663
|
+
profile_id: str
|
|
664
|
+
from_: RuntimeState = Field(alias="from")
|
|
665
|
+
to: RuntimeState
|
|
666
|
+
at: int
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
class ProfileStarted(_ReadModel):
|
|
670
|
+
type: Literal["profile_started"]
|
|
671
|
+
profile_id: str
|
|
672
|
+
cdp_ws: str
|
|
673
|
+
headless: bool
|
|
674
|
+
at: int
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class ProfileStopped(_ReadModel):
|
|
678
|
+
type: Literal["profile_stopped"]
|
|
679
|
+
profile_id: str
|
|
680
|
+
at: int
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class ProfileCrashed(_ReadModel):
|
|
684
|
+
type: Literal["profile_crashed"]
|
|
685
|
+
profile_id: str
|
|
686
|
+
reason: str
|
|
687
|
+
at: int
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class PreflightFailed(_ReadModel):
|
|
691
|
+
type: Literal["preflight_failed"]
|
|
692
|
+
profile_id: str
|
|
693
|
+
detail: str
|
|
694
|
+
at: int
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
class ProxyChecked(_ReadModel):
|
|
698
|
+
type: Literal["proxy_checked"]
|
|
699
|
+
proxy_id: str
|
|
700
|
+
healthy: bool
|
|
701
|
+
country: Optional[str] = None
|
|
702
|
+
at: int
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
class CapacityRejected(_ReadModel):
|
|
706
|
+
type: Literal["capacity_rejected"]
|
|
707
|
+
profile_id: str
|
|
708
|
+
at: int
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
# ── Agent runs + activity (payloads the live events carry) ───────────────────
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class StepTarget(_ReadModel):
|
|
715
|
+
"""The element a step acted on, as it can still be recognised later."""
|
|
716
|
+
|
|
717
|
+
role: str
|
|
718
|
+
name: str
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class RunStep(_ReadModel):
|
|
722
|
+
"""One tool call, as the operator gets to read it back.
|
|
723
|
+
|
|
724
|
+
Carries every field the daemon stored. The image is the one exception:
|
|
725
|
+
:attr:`shot_path` is a reference and the picture is fetched separately.
|
|
726
|
+
"""
|
|
727
|
+
|
|
728
|
+
seq: int
|
|
729
|
+
at: int
|
|
730
|
+
duration_ms: Optional[int] = None
|
|
731
|
+
kind: StepKind
|
|
732
|
+
tool: Optional[str] = None
|
|
733
|
+
target: Optional[StepTarget] = None
|
|
734
|
+
summary: Optional[str] = None
|
|
735
|
+
redacted: bool = False
|
|
736
|
+
url: Optional[str] = None
|
|
737
|
+
shot_path: Optional[str] = None
|
|
738
|
+
#: What the call produced, in one line — "3 blocks, 122 characters",
|
|
739
|
+
#: "no visible change · 14 elements". ``summary`` says what the agent asked
|
|
740
|
+
#: for, this says what came back. ``None`` on a step recorded before this
|
|
741
|
+
#: field existed.
|
|
742
|
+
result: Optional[str] = None
|
|
743
|
+
error: Optional[str] = None
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
class ProfileActivity(_ReadModel):
|
|
747
|
+
"""What one running profile is showing."""
|
|
748
|
+
|
|
749
|
+
profile_id: str
|
|
750
|
+
profile_name: str
|
|
751
|
+
url: str
|
|
752
|
+
title: str
|
|
753
|
+
actor: RunActor
|
|
754
|
+
since: int
|
|
755
|
+
#: The browser did not answer the last sweep; this is the last known value.
|
|
756
|
+
stale: bool = False
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class RunStarted(_ReadModel):
|
|
760
|
+
type: Literal["run_started"]
|
|
761
|
+
run_id: str
|
|
762
|
+
profile_id: str
|
|
763
|
+
actor: RunActor
|
|
764
|
+
goal: str
|
|
765
|
+
at: int
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class RunStepEvent(_ReadModel):
|
|
769
|
+
"""One tool call was appended to a run's trail — the whole stored step."""
|
|
770
|
+
|
|
771
|
+
type: Literal["run_step"]
|
|
772
|
+
run_id: str
|
|
773
|
+
profile_id: str
|
|
774
|
+
step: RunStep
|
|
775
|
+
at: int
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
class RunFinished(_ReadModel):
|
|
779
|
+
type: Literal["run_finished"]
|
|
780
|
+
run_id: str
|
|
781
|
+
profile_id: str
|
|
782
|
+
outcome: RunOutcome
|
|
783
|
+
at: int
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
class ProfileActivityChanged(_ReadModel):
|
|
787
|
+
"""A running profile's row appeared, or something about it is different."""
|
|
788
|
+
|
|
789
|
+
type: Literal["profile_activity_changed"]
|
|
790
|
+
activity: ProfileActivity
|
|
791
|
+
at: int
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
class ProfileActivityGone(_ReadModel):
|
|
795
|
+
type: Literal["profile_activity_gone"]
|
|
796
|
+
profile_id: str
|
|
797
|
+
at: int
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
class Resync(_ReadModel):
|
|
801
|
+
"""Frames were lost; re-read rather than carry on.
|
|
802
|
+
|
|
803
|
+
Raised when the daemon notices this connection fell behind the bus;
|
|
804
|
+
``dropped`` counts what it skipped.
|
|
805
|
+
"""
|
|
806
|
+
|
|
807
|
+
type: Literal["resync"]
|
|
808
|
+
dropped: int
|
|
809
|
+
at: int
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
Event = Annotated[
|
|
813
|
+
Union[
|
|
814
|
+
ProfileStateChanged,
|
|
815
|
+
ProfileStarted,
|
|
816
|
+
ProfileStopped,
|
|
817
|
+
ProfileCrashed,
|
|
818
|
+
PreflightFailed,
|
|
819
|
+
ProxyChecked,
|
|
820
|
+
CapacityRejected,
|
|
821
|
+
RunStarted,
|
|
822
|
+
RunStepEvent,
|
|
823
|
+
RunFinished,
|
|
824
|
+
ProfileActivityChanged,
|
|
825
|
+
ProfileActivityGone,
|
|
826
|
+
Resync,
|
|
827
|
+
],
|
|
828
|
+
Field(discriminator="type"),
|
|
829
|
+
]
|
|
830
|
+
|
|
831
|
+
EVENT_ADAPTER: TypeAdapter[Event] = TypeAdapter(Event)
|
|
832
|
+
"""Validates a wire payload into the typed :data:`Event` union."""
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
__all__ = [
|
|
836
|
+
# enums
|
|
837
|
+
"RuntimeState", "RUNTIME_STATES", "HostMode", "HOST_MODES",
|
|
838
|
+
"GeoMode", "GEO_MODES", "ProxyKind", "PROXY_KINDS",
|
|
839
|
+
"Rotation", "ROTATIONS", "SessionKind", "OsFamily", "InputAction",
|
|
840
|
+
"RunActor", "RunOutcome", "StepKind",
|
|
841
|
+
# persona
|
|
842
|
+
"Screen", "Locale", "WebGl", "WebGpu", "AudioProps", "SpeechVoice",
|
|
843
|
+
"GpuPersona", "Brand", "ClientHints", "PermissionDefaults", "MediaDevice",
|
|
844
|
+
"PersonaMisc", "Persona",
|
|
845
|
+
# resources
|
|
846
|
+
"Profile", "Group", "Preset", "Proxy",
|
|
847
|
+
# payloads
|
|
848
|
+
"CreateProfileBody", "UpdateProfileBody", "StartProfileBody",
|
|
849
|
+
"StartProfileResult", "StopProfileResult", "BulkCreateBody", "BulkIdsBody",
|
|
850
|
+
"BulkAssignProxyBody", "CreateProxyBody", "UpdateProxyBody",
|
|
851
|
+
"ProxyCheckResult", "CreateGroupBody", "UpdateGroupBody", "SessionExportBody",
|
|
852
|
+
"SessionExportResult", "SessionImportBody", "Metrics",
|
|
853
|
+
# runs + activity
|
|
854
|
+
"StepTarget", "RunStep", "ProfileActivity",
|
|
855
|
+
# events
|
|
856
|
+
"ProfileStateChanged", "ProfileStarted", "ProfileStopped", "ProfileCrashed",
|
|
857
|
+
"PreflightFailed", "ProxyChecked", "CapacityRejected",
|
|
858
|
+
"RunStarted", "RunStepEvent", "RunFinished",
|
|
859
|
+
"ProfileActivityChanged", "ProfileActivityGone", "Resync", "Event",
|
|
860
|
+
"EVENT_ADAPTER",
|
|
861
|
+
]
|