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/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
+ ]