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
Metadata
Release files for s-browserkit 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| s_browserkit-0.1.2.tar.gz | 567.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| s_browserkit-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 876.6 kB
Release files / s_browserkit-0.1.2.tar.gz
| Download URL | s_browserkit-0.1.2.tar.gz |
|---|---|
| Size | 567.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d243c615d9f51ce9811390fcbabb1b745fa58cc5592e491bdde887869ef2485d
|
|
BLAKE2b-256 checksum How to use checksums |
e95e6c8bb9996d1f2ef3d4869cd4d5a00599f5d89f9f593a96d078ab674a2f43
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / s_browserkit-0.1.2-py3-none-any.whl
| Download URL | s_browserkit-0.1.2-py3-none-any.whl |
|---|---|
| Size | 309.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
140199aee6a33a5126b693b07493a3e6055b00597751e3ecf1e6d98ed7494250
|
|
BLAKE2b-256 checksum How to use checksums |
434ae0ec107456ade9386f1ad0a9359ed4abed9e921f11bc813c6776fd0e100d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|