Skip to main content

s-browserkit — браузерный слой китов

Живой браузер, антибот и стелс — одним китом, который ставится отдельно. Чистой API-интеграции (белый REST/GraphQL) он не нужен и больше не навязывается: librarykit без browserkit полностью работоспособен.

        clikit          adapterkit          ветки-оболочки
             \             /
              \           /
               librarykit                   ОРКЕСТРАТОР (сессии, лимиты, фабрики)
                /        \
        browserkit        …                 СЛОЙ-КИТ (браузер и антибот)
                \        /
                 corekit                    ОСНОВАНИЕ (значения и чистые правила)

Главное: просят ВОЗМОЖНОСТЬ, а не движок

import browserkit

backend = browserkit.open_browser(
    browserkit.Capability.STEALTH_SESSION,   # ЦЕЛЬ, а не «camoufox»
    profile_dir=profile,
)
http = browserkit.make_requester(browserkit.Capability.STEALTH_REQUEST)

Соответствие «цель → движок» живёт ровно в одной таблице внутри кита (browserkit/capabilities.py). Anti-detect движок начал палиться — правится одна строка, ни один навык не меняется, потому что ни один навык движок не назвал.

Возможности:

Capability Что нужно потребителю
STEALTH_SESSION поднять браузер там, где обычный палится antibot'ом
VISIBLE_LOGIN видимое окно: пароль/2FA человек вводит руками
PERSISTENT_WORK обычная автоматизация в персистентном профиле
STEALTH_REQUEST один HTTP-запрос с браузерным TLS-отпечатком, БЕЗ браузера
IN_PAGE_REQUEST запрос изнутри живой страницы (её cookies/TLS/заголовки)
TRAFFIC_CAPTURE нормализованный сетевой трафик страницы (реверс скрытого API)
SYSTEM_CHROME CDP-attach к УЖЕ УСТАНОВЛЕННОМУ системному Chrome — не anti-detect, не персистентный профиль кита; деградации нет, кандидат один — cdp

Диагностика — «чем закрыта каждая цель прямо сейчас»:

Сразу после pip install s-browserkit (без единого extra) все цели пусты — это и есть нормальный ответ, а не поломка:

>>> import browserkit
>>> browserkit.describe_capabilities()
{'stealth_session': None, 'visible_login': None, 'persistent_work': None,
 'stealth_request': None, 'in_page_request': None, 'traffic_capture': None}

None означает «не установлен ни один исполнитель». Попытка воспользоваться такой целью даёт внятную ошибку с рецептом установки, а не голый ModuleNotFoundError:

>>> browserkit.make_requester(browserkit.Capability.STEALTH_REQUEST)
Traceback (most recent call last):
  ...
browserkit.CapabilityUnavailable: Возможность 'stealth_request' недоступна:
ни один исполнитель не установлен. Поставь один из: `pip install 's-browserkit[antibot]'`.

После pip install 's-browserkit[antibot]' та же строка вернёт готовый исполнитель (CurlCffiTransport), а describe_capabilities()['stealth_request'] станет 'tls_impersonate' — имя ИСПОЛНИТЕЛЯ цели, не пакета. Какие исполнители стоят в очереди за каждой целью, видно заранее:

>>> browserkit.capability_plan(browserkit.Capability.STEALTH_SESSION)
('camoufox', 'nodriver', 'cdp')

Побеждает первый установленный; порядок переопределяется set_capability_plan().

Поднять браузер под цель

open_browser() сам по себе отдаёт голый бэкенд — ни аренды процессов профиля, ни закрытия под сроком: за пределами кита их подключал руками КАЖДЫЙ потребитель. leased_browser() — тот же путь ОДНИМ вызовом: аренда профиля (lifecycle. browser_lease) → open_browser() под цель → закрытие под сроком (lifecycle. close_engine) в finally, даже если тело блока упало:

import browserkit
import netkit

async with browserkit.leased_browser(
    browserkit.Capability.VISIBLE_LOGIN,
    profile_dir=профиль,
    extra_http_headers=netkit.browser_context_kwargs(persona=p)["extra_http_headers"],
) as b:
    page = await b.ensure_page()
# тут браузер уже закрыт, а осиротевшие процессы профиля — добиты

netkit в примере — дело ВЫЗЫВАЮЩЕГО (формула HTTP-заголовков по персоне); сам browserkit netkit не импортирует и не должен. profile_dir обязателен: аренда без профиля не отличает один процесс от другого, поэтому его отсутствие — ValueError ДО того, как что-либо арендовано или поднято.

Состав

  • browserkit.browser — лаунчеры (системный Chromium по CDP, персистентный контекст), anti-detect / undetected бэкенды, capture_tap (нормализованный event-tap над любым движком), state (storage-state, тёплый вход, детекторы протухания), egress (выход сессии и пробы «чем нас видят снаружи»), persona (часы, язык, экран и UA сессии — одним заявлением на все движки), webauthn (passkey без человека: виртуальный аутентификатор CDP, а ключ живёт СНАРУЖИ профиля браузера — см. ниже), геометрия окна, брони портов;
  • browserkit.antibot — исполнители HTTP-запроса, ходящие «как браузер», минтер токенов тёплой страницей и token_cache — кэш чеканных токенов: срок жизни и одноразовость учитываются ПОРОЗНЬ, а одновременные чеканки схлопываются в одну (вместо N подъёмов браузера); bundle_daemon — постоянный процесс, исполняющий вендорский JS-бандл БЕЗ окна браузера (Tier 3: подъём один раз, вызов — десятки миллисекунд). Node — опциональная зависимость, нет его → внятный отказ с рецептом; mint_ladder + mint_gateлестница чеканки: один вызов «дай токен» со спуском от дешёвой ступени к дорогой (см. ниже);
  • browserkit.profile_health — предстартовая проверка профиля и карантин;
  • browserkit.proc — запуск подпроцессов без всплывающих консолей;
  • browserkit.ports — Protocol'ы, которых киту не хватает снаружи, плюс типы Page/BrowserContext/APIRequestContext (реэкспорт из playwright.async_api под TYPE_CHECKING, Any в рантайме — аннотировать можно без установленного вендора);
  • browserkit.errors — ошибки вендора без прямого импорта: timeout_errors(), engine_errors(), is_timeout(exc) (ловят ВСЕ установленные вендоры разом, включая откат patchright→playwright);
  • browserkit.browser.playwright_api(vendor="auto", *, sync=False, strict=None) — дверь к самой вендорской фабрике (async_playwright/sync_playwright) по имени вендора, без собственного import playwright/patchright у потребителя;
  • browserkit.browser.camoufox.camoufox_version() — версия установленной сборки CamouFox ("", если вендора нет); CamoufoxBackend(exclude_addons=["ubo"]) — аддоны из умолчания CamouFox по ИМЕНИ, без регистра (см. ADDON_UBO), без импорта вендорского camoufox.addons.DefaultAddons;
  • NodriverBackend.send(command) / .cdp — публичная дверь к CDP-команде текущей вкладки (.cdp — ленивый nodriver.cdp, для сборки команд самим потребителем: backend.send(backend.cdp.page.navigate(url)));
  • browserkit.browser_guard + browserkit.testingзапрет браузера в рабочем пути (см. ниже).

Персона: чем сессия выглядит

Детектор 2026 года ищет не «плохой отпечаток», а ПРОТИВОРЕЧИЕ МЕЖДУ СЛОЯМИ. Самое дешёвое для него — часы против адреса: страну называет прокси, а часовой пояс отдаёт сам браузер. Поэтому персона (corekit.persona.ClientPersona) едет в запуск ОДНИМ заявлением и доезжает до всех четырёх путей:

backend = browserkit.browser.build_capture_backend(
    "nodriver", profile_dir=profile, proxy="socks5://fi.example:1080",
    persona=persona,          # часы, язык, экран, окно, UA — одним значением
)

Три правила, которые из этого следуют:

  • несогласованная персона не стартует. Часы Москвы при финском выходе — это PersonaInconsistent в конструкторе бэкенда, ДО спавна процесса. Мы отказываем, а не чиним: «починить» значит решить за вызывающего, какой из слоёв врёт;
  • умолчание названо. Персону не передали — кит строит её из того, что знает о машине, и печатает перед стартом ([browserkit.persona] …). Часы уезжают явным timezone_id даже когда совпадают с системными: явное можно сверить с адресом, унаследованное молча — нельзя;
  • заявление проверяется наблюдением. confirm_persona_egress(backend, plan) спрашивает у живой сессии её настоящую страну и бьёт SessionEgressMismatch, если она разошлась с заявленной.

Passkey без человека — и ключ НЕ в профиле браузера

Для защиты входа WebAuthn + passkey сильнее любой CAPTCHA, и сервисы уводят туда чувствительные аккаунты. До сих пор наш ответ на passkey был один — «дальше только вручную». Теперь есть виртуальный аутентификатор через CDP-домен WebAuthn — тот самый механизм, которым разработчики сервисов тестируют свои же passkey-формы:

from browserkit.browser import SecretStorePasskeys, passkey_session

async with passkey_session(page, account="google:user@example.com",
                           store=SecretStorePasskeys(secret_store)) as auth:
    await page.goto("https://example.com/login")
    ...                        # форма passkey отрабатывает сама, без касания
    assert auth.asserted       # сервис ДЕЙСТВИТЕЛЬНО проверил ключ подписью
  • ключ хранится СНАРУЖИ профиля. Профиль — кэш: мы его чистим (карантин profile_health) и пересоздаём. Ключ passkey — единственный предъявитель аккаунта, и класть его в то, что мы сами стираем, значит однажды стереть доступ. Кит ОТДАЁТ ключ значением (PasskeyCredential), а хранит его тот, у кого стор: контракт — структурный PasskeyStorePort, переходник SecretStorePasskeys подходит к любому стору формы authkit_client.SecretStore утино, без единого импорта и без новой зависимости у кита;
  • аутентификатор привязан к аккаунту, а не к браузеру. Чужой ключ в него не заедет (PasskeyAccountMismatch), а персона-умолчание по машине ключом привязки быть не может — она одинакова у всех профилей;
  • честная граница. Это не обход WebAuthn, а его штатное исполнение: работает там, где сервис принимает platform-аутентификатор, и НЕ работает там, где требуется аттестация настоящего устройства (enterprise, TPM/Apple/Yubico, device-bound). Первичный вход тоже не отменяется: чтобы passkey зарегистрировать, надо уже быть внутри аккаунта.

Лестница чеканки: браузер — последняя ступень, а не первая

Токен, который сервис выдаёт только браузеру, можно добыть четырьмя способами разной цены. Лестница делает выбор между ними СВОЙСТВОМ РАНТАЙМА, а не решением, принятым однажды при написании навыка:

прямой минт → прямой с имитацией браузера → настоящий браузер БЕЗ окна → настоящий браузер С ВИДИМЫМ ОКНОМ.

from browserkit.antibot import MintLadder, MintRung, MintSpec, MintedToken, TokenCache

async def by_bundle(rung):            # дёшево: постоянный процесс, окна нет
    return MintedToken(value=await daemon.call("mint"), minter_ttl_s=120)

async def by_warm_page(rung):         # дорого: настоящий браузер без окна
    return MintedToken(value=await minter.mint(...), minter_ttl_s=120)

ladder = MintLadder(
    MintSpec(service="google-flow", kind="recaptcha-action", account="dmitry",
             minters={MintRung.DIRECT: by_bundle,
                      MintRung.HEADLESS_BROWSER: by_warm_page}),
    cache=TokenCache(),
)
token = await ladder.mint()           # спустится, ТОЛЬКО если дёшево не вышло
ladder.note_rejected("403 unusual activity")   # сервис отверг наш токен — улика

Четыре правила, которые из этого следуют:

  • спуск только по ПРЯМОЙ улике. Бандл отработал и отказал, сервис не пропустил, сервис отверг токен — двигают ступень. Умер Node, моргнула сеть, кончилась память, протухла сессия — НЕТ: это объясняет симптом дешевле, чем усиление защиты. Граница лежит данными — mint_gate.DENY_REASONS;
  • причина спуска записана. Кто, откуда, куда, почему и когда — значением (MintDescent) в журнале лестницы, в памяти ступени и предупреждением в логе;
  • возврат наверх обязателен. По sticky_ttl_s запись протухает и дешёвая ступень пробуется заново, а probe_up_after раз в N успехов отправляет один вызов ступенью выше. Иначе одна неудача сделала бы браузер постоянным;
  • видимое окно — не деградация, а прерывание работы человека. Лестница туда не спускается сама: нужно явное mint(allow_visible=True) И разрешение стража (см. следующий раздел). Браузерные ступени вообще спрашивают browser_guard, а не пролезают мимо него.

Кэш токенов стоит ПОВЕРХ лестницы: годный токен отдаётся до первого шага вниз, а восемь одновременных вызовов дают один подъём браузера, а не восемь.

Запрет браузера в рабочем пути

Канон транспорта: браузер поднимается только чтобы сминтить сессию, работа идёт прямыми запросами. Кит делает это конструкцией — перехватом на собственных дверях запуска. Подключение к прогону навыка — одна строка:

# tests/conftest.py навыка
from browserkit.testing import forbid_real_browser  # noqa: F401

Страж живёт здесь, а не в оркестраторе, ровно потому, что двери — здесь: страж и двери едут в одном дистрибутиве, поэтому «импорт прошёл» = «двери на месте». Он обязан честно сказать, если стеречь не может: пропажа СВОЕЙ двери валит установку (BrowserGuardBlind), а «пакета нет» отделено от «пакет есть, двери нет» (guard.absent / guard.blind / guard.describe()).

Подробности, боевая установка и границы применимости — docs/BROWSER_GUARD.md.

Откат со стелс-движка на обычный ГОВОРИТСЯ ВСЛУХ

Стелс-путь идёт patchright-форком (снятые палевные CDP-следы). Форка нет — кит берёт обычный playwright: падать вместо работы хуже, и есть законный случай, когда обычный движок поставлен НАМЕРЕННО. Но подмена больше не молчит — молчала она до задачи #2061, и стоило это так: браузер поднимался, тесты зеленели, рецепт установки формально был выполнен, а стелса не было ни в одной сессии (замер 19.08.2026: при установленном s-browserkit[browser] было playwright True, patchright False).

import browserkit

browserkit.patchright_available()          # стоит ли ИМЕННО стелс-форк
browserkit.resolve_playwright_vendor(True) # чем поедет ФАКТИЧЕСКИ: "patchright" | "playwright"

backend = browserkit.open_browser(browserkit.Capability.TRAFFIC_CAPTURE, profile_dir=p)
backend.stealth_driver                     # ★ машинный ответ: стелс ли движок ПРЯМО СЕЙЧАС
backend.active_driver                      # он же строкой — для лога и выбора ступени
  • вслух — одна строка в stderr на процесс (не на вызов), в манере announce_persona: [browserkit.engine] стелс-движок ... не установлен — работаем обычным ... с рецептом. reset_engine_announcements() — сказать заново;
  • строгоBROWSERKIT_STEALTH_STRICT=1 (или strict=True у загрузчика) превращает подмену в отказ StealthEngineUnavailable (наследник BrowserUnavailable). Для боевого контура и antibot-прогонов, где «тихо доехали на обычном» — негодный результат. По умолчанию строгости НЕТ.

Соседние движки (CamouFox, nodriver) подменой не занимаются и раньше: их отсутствие ОТКАЗЫВАЕТ с рецептом. Правило закреплено на весь браузерный пакет тестом tests/test_stealth_fallback.py.

Установка

pip install s-browserkit                 # без движков: фасад, антибот-контракты, здоровье профиля
pip install 's-browserkit[browser]'      # + Playwright (CDP-attach, персистентный контекст)
pip install 's-browserkit[patchright]'   # + стелс-форк Playwright (снятые CDP-следы)
pip install 's-browserkit[camoufox]'     # + anti-detect Firefox   (затем `camoufox fetch`)
pip install 's-browserkit[nodriver]'     # + undetected Chromium
pip install 's-browserkit[antibot]'      # + TLS/JA3-импersonate без браузера

import browserkit не поднимает браузер и не импортирует ни одного движка: подпакеты резолвятся лениво (PEP 562), вендоры — ещё позже, внутри бэкендов.

Совместимость

Прежние пути (librarykit.browser.*, librarykit.antibot.*, librarykit.profile_health, librarykit.browser_guard, librarykit.testing) продолжают работать: там оставлены алиасы на те же модули (sys.modules), а не копии. Поэтому monkeypatch, isinstance и is-сравнения ведут себя ровно как до переноса, а второй реализации стража, которая через месяц разъедется с первой, не существует.

Тесты

uv run --no-sync pytest -q          # либо .venv\Scripts\python.exe -m pytest -q

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

s_browserkit-0.1.0.tar.gz (553.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

s_browserkit-0.1.0-py3-none-any.whl (302.5 kB view details)

Uploaded Python 3

File details

Details for the file s_browserkit-0.1.0.tar.gz.

File metadata

  • Download URL: s_browserkit-0.1.0.tar.gz
  • Upload date:
  • Size: 553.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for s_browserkit-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b2263b61aec0c3bfa9bbebcf4d478063965c9250e5fadf171eff089a809d1c05
MD5 c08b807360b71cf2e40e9034ad4db5ac
BLAKE2b-256 afe5d692f2093b5e12efd55790dc93475a7f7f77b2d34705a0c5743789e1e972

See more details on using hashes here.

File details

Details for the file s_browserkit-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: s_browserkit-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 302.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for s_browserkit-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 223c1420cbffe3e076120f6b6b52a4bb7334ff05a67be9dddb1ea77262e25eb5
MD5 3dfb3e61897d50f11cab40ab525af6b3
BLAKE2b-256 5d5c0eb8c8df244e66f53f325b2b0d9d2c531c01381577054e82c968e9104d50

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

0.0.22

2 files

0.0.21

2 files

0.0.20

2 files

0.0.19

2 files

0.0.18

2 files

0.0.17

2 files

0.0.16

2 files

0.0.15

2 files

0.0.14

2 files

0.0.13

2 files

0.0.12

2 files

0.0.11

2 files

0.0.8

2 files

0.0.7

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page