Хорошо структурировано, особенно разбор по уровням (user/kernel/server). Но интересно сравнить с похожей гонкой вооружений в смежной области - bot detection для веба (Cloudflare, Akamai и т.п.).
Там архитектурная динамика противоположная - и именно поэтому “защита всегда проигрывает” там не работает так же однозначно. В античите атакующий физически владеет машиной: DMA-плата, kernel-driver - он всегда может пойти глубже, чем сам античит. Это и есть структурная причина, почему защита проигрывает.
В bot detection атакующий владеет только браузером, а детект происходит на стороне сервера (или CDN). Но вот интересная параллель с DMA: если патчить сам движок Chromium на уровне C++ до компиляции - detection-скрипты Cloudflare крутятся уже внутри пропатченного бинарника и просто не видят automation-сигналов. Это структурно похоже на DMA - обходишь уровень, на котором работает детект. Только в отличие от DMA, это не “железо ниже ОС”, а “бинарник ниже JS”.
Так что ответ на “кто выигрывает” сильно зависит от того, кто контролирует среду исполнения.
koleso_O, проблема почти наверняка в стеке, не в вас.
headless chrome (и chromedp, и selenium-stealth из статьи) работают поверх stock Chromium. JS-патчи применяются уже после старта движка - между запуском процесса и моментом, когда патч перехватывает управление, есть окно. Cloudflare Bot Management делает fingerprint-замер именно в этом окне: Canvas hash, WebGL RENDERER/VENDOR, navigator.webdriver - всё это успевает “засветиться” до того как ваш патч загрузился.
Автору статьи повезло: Дом.ру, судя по описанию, использует менее агрессивную защиту. CF уровня “challenge при любом headless” - это уже L2-детект, который JS-патчи принципиально не закрывают.
Попробуйте CloakBrowser - Chromium с патчами на уровне C++ до компиляции. Движок просто никогда не отдаёт automation-сигналы, нет окна, нет детекта.
Vision-based проверки это правильное направление, но есть один нюанс: если браузер детектируется до рендеринга (Cloudflare Bot Management делает fingerprint ещё на уровне TLS-рукопожатия и первых JS-сигналов), то агент с vision уже смотрит на страницу с капчей. Проверка фактического состояния правильная, но "поздняя" - нужно устранять причину, а не фиксировать симптом.
Мы у себя пошли другим путём: вместо того чтобы обнаруживать и реагировать на детект, убрали саму возможность детекта. Используем CloakBrowser - это Chromium с патчами на уровне C++ (до компиляции), который просто никогда не отдаёт automation-сигналы. Тогда вся логика проверки среды становится чище: если страница отдаёт капчу - это реальное гео-ограничение или бан IP, а не артефакт автоматизации. Причины становятся диагностируемыми.
Ваш подход с synthetic/real environment в паре с таким браузером, кстати, закрывал бы большинство кейсов без гонки с антиботом.
Хорошая сборка, особенно часть про reward hacking - проблема куда шире, чем кажется на первый взгляд.
Хочу добавить похожий паттерн, который встречается, когда агент работает с браузерной автоматизацией. Там появляется "экологический" reward hacking: тест зелёный, код формально правильный, страница возвращает 200 - но реальный пользователь в это время видит Cloudflare-челлендж или капчу. TDD это не ловит, потому что юнит-тесты не знают про антибот-системы на целевом сайте.
Это как раз иллюстрация тезиса про "протекающие абстракции" - только здесь протекает не сам стек, а среда выполнения. Агент пишет правильный Playwright-код, все проверки проходят, а на проде автоматизация молча ломается при первом же деплое на реальный сайт.
Получается, что к вашему списку (TDD, SDD, риск-менеджмент) для агентов, работающих с внешними сервисами, напрашивается ещё один уровень: проверка среды, а не только кода. Как вы это решаете в Kodik, если сталкивались с подобным?
Хорошая демонстрация того, почему модель "один поток - одна LLM" не работает в real-time. Параллель с рефлекторной дугой понятна и точная. Любопытный нюанс для тех, кто применяет похожую архитектуру не в геймдеве, а в браузерных агентах (browser-use, Playwright-based сценарии). Там появляется третий источник задержки, который не покрывает ни System 1, ни System 2: сам браузер. Антибот-системы могут добавить 2-7 секунд на JS-челлендж до того, как страница вообще отдаст DOM. Рефлекторный слой в этот момент просто ждет данных, которых ещё нет. По сути, для веб-агентов нужен ещё один уровень - что-то вроде "вегетативной нервной системы": асинхронное управление сессиями и предварительная прогрузка контекста, которая не блокирует ни System 1, ни System 2. Думали про применение архитектуры вне геймдева - или пока фокус именно на NPC/робототехнике?
Точно, код-ревью sub-agent + человек - надёжный подход на текущем этапе. Аномалии в поведении тестов часто вылезают именно при переходе на реальные сценарии. Например, агент кликает идеально по центру элемента 10 раз подряд - логически тест зелёный, но для реального UI (особенно web-based) это флаг для антибота. В browser automation такое ловим отдельной проверкой на human-like паттерны.
Для этого как раз и используем CloakBrowser - он не только патчит fingerprint, но и добавляет behavioral noise (рандом в движении мыши, задержки) на уровне движка. Тогда агентский тест не только проходит, но и не палится как автоматика.
Классный кейс. Интересно, думали ли вы про следующий уровень проверки - когда агент не только чинит тест до зелёного статуса, но и валидируется с точки зрения поведения (например, что он не кликает 10 раз подряд по одному и тому же элементу, не открывает экран по 3 раза и т.п.)? В браузерной автоматизации это часто отдельный слой поверх обычных интеграционных тестов, чтобы ловить «странное» поведение ещё до продакшена.
Согласен, что поддерживать собственные JS-патчи для мониторинг-сервиса - это действительно гонка вооружений. Но суть в том, откуда берётся патч.
JS-патчи (puppeteer-extra-stealth и аналоги) работают поверх уже запущенного движка. Между стартом процесса Chromium и моментом, когда патч перехватывает управление, есть окно - антифрод замеряет fingerprint именно там, до загрузки вашего JS. Это и есть та самая гонка.
Если патч зашит в сам бинарник Chromium на уровне C++ до компиляции - движок просто никогда не отдаёт "нечеловеческие" значения. Нет окна, нет гонки. Видел такой подход в CloakBrowser - они именно так и сделали. Устойчиво между обновлениями, потому что детект-сигнатуры привязаны к поведению stock Chromium, а не к вашей сборке.
Вариант с "настроить Cloudflare чтоб не вредничал" рабочий, но только если сайт ваш. Для мониторинга сторонних сервисов это не вариант.
Хорошо разложено по слоям, особенно часть про L4 - её часто опускают. Один момент, который хотелось бы дополнить: автор пишет, что stealth-плагины «латают симптомы, а не причину» - но не объясняет, почему это принципиальная проблема на архитектурном уровне.
Дело в том, что JS-патчи (тот же puppeteer-extra-plugin-stealth) работают поверх уже запущенного движка. Это значит: между стартом процесса Chromium и моментом, когда патч успевает перехватить управление, есть окно. Агрессивные антифрод-системы (DataDome, некоторые имплементации Akamai) делают fingerprint-замер именно в этом окне - до того как ваш JS вообще загружается.
Единственный способ закрыть это окно - патчить сам движок на уровне C++, до компиляции. Тогда «нечеловеческие» значения просто не попадают в runtime никогда, а не перезаписываются постфактум.
BaaS-решения закрывают эту проблему по-разному: одни через runtime-патчи (быстро, но детектируемо при правильном замере), другие через модифицированный бинарник. Снаружи выглядит одинаково, а уровень защиты принципиально разный.
Хороший обзор. По уровню 5-6 хочу добавить момент, который обычно всплывает позже: когда агент начинает реально ходить по продакшн-сайтам, headless-браузер довольно быстро детектится. Сайт отдаёт другой контент, показывает капчу или тихо блокирует сессию. Агент при этом формально "завершает задачу", но работает не с теми данными. Для внутренних инструментов это не проблема. Но как только агент взаимодействует с внешними сервисами - это реально больная точка, которую в уровнях не упоминают.
Transaction-мониторинг интересная идея, но на практике есть один нюанс, который легко упустить: headless Playwright из коробки имеет характерный fingerprint, и многие сайты его обнаруживают. В результате сценарий «проходит», страница открывается, форма видна, но сайт отдаёт другой контент, показывает капчу или просто блокирует сессию. Монитор видит 200 OK, а реальный пользователь в это время видит стену с капчей. Особенно актуально для e-commerce и SaaS, где Cloudflare и аналоги стоят почти везде. Для таких случаев стоит запускать Playwright через браузер с патченным fingerprint - иначе результаты transaction-проверок будут ненадёжными именно на тех сайтах, где мониторинг важнее всего.
Хочу добавить кое-что про L2, что часто упускают: navigator.hardwareConcurrency и deviceMemory это не просто статические числа, которые нужно подменить. Проблема в том, что эти значения коррелируют с другими сигналами. Если вы говорите "16 ядер, 32GB RAM", но при этом WebGL renderer показывает llvmpipe или Mesa, или AudioContext показывает нулевые задержки без джиттера, вся легенда рассыпается. Современные системы детекции, в частности Akamai и DataDome, строят граф корреляций. Один параметр сам по себе не срабатывает, но несовпадение двух-трёх уже достаточно. Про deterministic noise в Canvas - хорошо, что упомянули. Именно этот момент часто ломает дешёвые stealth-решения: рандом на каждый вызов toDataURL() легко детектируется повторным рендером. Детерминизм привязанный к pid, это правильный подход.
По поводу локальных агентов - причина, почему агент «теряется», часто не в локаторах. Headless Chromium в автоматическом режиме ведёт себя иначе, чем реальный браузер, и сайты это замечают: отдают другой контент, блокируют сессию, показывают капчу. Агент видит совсем не то, что видит человек. Для exploratory testing на реальных сайтах это первое, что стоит проверить при настройке окружения.
Хорошо структурировано, особенно разбор по уровням (user/kernel/server). Но интересно сравнить с похожей гонкой вооружений в смежной области - bot detection для веба (Cloudflare, Akamai и т.п.).
Там архитектурная динамика противоположная - и именно поэтому “защита всегда проигрывает” там не работает так же однозначно. В античите атакующий физически владеет машиной: DMA-плата, kernel-driver - он всегда может пойти глубже, чем сам античит. Это и есть структурная причина, почему защита проигрывает.
В bot detection атакующий владеет только браузером, а детект происходит на стороне сервера (или CDN). Но вот интересная параллель с DMA: если патчить сам движок Chromium на уровне C++ до компиляции - detection-скрипты Cloudflare крутятся уже внутри пропатченного бинарника и просто не видят automation-сигналов. Это структурно похоже на DMA - обходишь уровень, на котором работает детект. Только в отличие от DMA, это не “железо ниже ОС”, а “бинарник ниже JS”.
Так что ответ на “кто выигрывает” сильно зависит от того, кто контролирует среду исполнения.
koleso_O, проблема почти наверняка в стеке, не в вас.
headless chrome (и chromedp, и selenium-stealth из статьи) работают поверх stock Chromium. JS-патчи применяются уже после старта движка - между запуском процесса и моментом, когда патч перехватывает управление, есть окно. Cloudflare Bot Management делает fingerprint-замер именно в этом окне: Canvas hash, WebGL RENDERER/VENDOR, navigator.webdriver - всё это успевает “засветиться” до того как ваш патч загрузился.
Автору статьи повезло: Дом.ру, судя по описанию, использует менее агрессивную защиту. CF уровня “challenge при любом headless” - это уже L2-детект, который JS-патчи принципиально не закрывают.
Попробуйте CloakBrowser - Chromium с патчами на уровне C++ до компиляции. Движок просто никогда не отдаёт automation-сигналы, нет окна, нет детекта.
Мы разрабатываем CloakBrowser, так что как раз наша тема :)
Загляните к нам на github.
Vision-based проверки это правильное направление, но есть один нюанс: если браузер детектируется до рендеринга (Cloudflare Bot Management делает fingerprint ещё на уровне TLS-рукопожатия и первых JS-сигналов), то агент с vision уже смотрит на страницу с капчей. Проверка фактического состояния правильная, но "поздняя" - нужно устранять причину, а не фиксировать симптом.
Мы у себя пошли другим путём: вместо того чтобы обнаруживать и реагировать на детект, убрали саму возможность детекта. Используем CloakBrowser - это Chromium с патчами на уровне C++ (до компиляции), который просто никогда не отдаёт automation-сигналы. Тогда вся логика проверки среды становится чище: если страница отдаёт капчу - это реальное гео-ограничение или бан IP, а не артефакт автоматизации. Причины становятся диагностируемыми.
Ваш подход с synthetic/real environment в паре с таким браузером, кстати, закрывал бы большинство кейсов без гонки с антиботом.
Хорошая сборка, особенно часть про reward hacking - проблема куда шире, чем кажется на первый взгляд.
Хочу добавить похожий паттерн, который встречается, когда агент работает с браузерной автоматизацией. Там появляется "экологический" reward hacking: тест зелёный, код формально правильный, страница возвращает 200 - но реальный пользователь в это время видит Cloudflare-челлендж или капчу. TDD это не ловит, потому что юнит-тесты не знают про антибот-системы на целевом сайте.
Это как раз иллюстрация тезиса про "протекающие абстракции" - только здесь протекает не сам стек, а среда выполнения. Агент пишет правильный Playwright-код, все проверки проходят, а на проде автоматизация молча ломается при первом же деплое на реальный сайт.
Получается, что к вашему списку (TDD, SDD, риск-менеджмент) для агентов, работающих с внешними сервисами, напрашивается ещё один уровень: проверка среды, а не только кода. Как вы это решаете в Kodik, если сталкивались с подобным?
Хорошая демонстрация того, почему модель "один поток - одна LLM" не работает в real-time. Параллель с рефлекторной дугой понятна и точная.
Любопытный нюанс для тех, кто применяет похожую архитектуру не в геймдеве, а в браузерных агентах (browser-use, Playwright-based сценарии). Там появляется третий источник задержки, который не покрывает ни System 1, ни System 2: сам браузер. Антибот-системы могут добавить 2-7 секунд на JS-челлендж до того, как страница вообще отдаст DOM. Рефлекторный слой в этот момент просто ждет данных, которых ещё нет.
По сути, для веб-агентов нужен ещё один уровень - что-то вроде "вегетативной нервной системы": асинхронное управление сессиями и предварительная прогрузка контекста, которая не блокирует ни System 1, ни System 2.
Думали про применение архитектуры вне геймдева - или пока фокус именно на NPC/робототехнике?
Точно, код-ревью sub-agent + человек - надёжный подход на текущем этапе.
Аномалии в поведении тестов часто вылезают именно при переходе на реальные сценарии. Например, агент кликает идеально по центру элемента 10 раз подряд - логически тест зелёный, но для реального UI (особенно web-based) это флаг для антибота. В browser automation такое ловим отдельной проверкой на human-like паттерны.
Для этого как раз и используем CloakBrowser - он не только патчит fingerprint, но и добавляет behavioral noise (рандом в движении мыши, задержки) на уровне движка. Тогда агентский тест не только проходит, но и не палится как автоматика.
Классный кейс.
Интересно, думали ли вы про следующий уровень проверки - когда агент не только чинит тест до зелёного статуса, но и валидируется с точки зрения поведения (например, что он не кликает 10 раз подряд по одному и тому же элементу, не открывает экран по 3 раза и т.п.)? В браузерной автоматизации это часто отдельный слой поверх обычных интеграционных тестов, чтобы ловить «странное» поведение ещё до продакшена.
Согласен, что поддерживать собственные JS-патчи для мониторинг-сервиса - это действительно гонка вооружений. Но суть в том, откуда берётся патч.
JS-патчи (puppeteer-extra-stealth и аналоги) работают поверх уже запущенного движка. Между стартом процесса Chromium и моментом, когда патч перехватывает управление, есть окно - антифрод замеряет fingerprint именно там, до загрузки вашего JS. Это и есть та самая гонка.
Если патч зашит в сам бинарник Chromium на уровне C++ до компиляции - движок просто никогда не отдаёт "нечеловеческие" значения. Нет окна, нет гонки. Видел такой подход в CloakBrowser - они именно так и сделали. Устойчиво между обновлениями, потому что детект-сигнатуры привязаны к поведению stock Chromium, а не к вашей сборке.
Вариант с "настроить Cloudflare чтоб не вредничал" рабочий, но только если сайт ваш. Для мониторинга сторонних сервисов это не вариант.
Хорошо разложено по слоям, особенно часть про L4 - её часто опускают.
Один момент, который хотелось бы дополнить: автор пишет, что stealth-плагины «латают симптомы, а не причину» - но не объясняет, почему это принципиальная проблема на архитектурном уровне.
Дело в том, что JS-патчи (тот же puppeteer-extra-plugin-stealth) работают поверх уже запущенного движка. Это значит: между стартом процесса Chromium и моментом, когда патч успевает перехватить управление, есть окно. Агрессивные антифрод-системы (DataDome, некоторые имплементации Akamai) делают fingerprint-замер именно в этом окне - до того как ваш JS вообще загружается.
Единственный способ закрыть это окно - патчить сам движок на уровне C++, до компиляции. Тогда «нечеловеческие» значения просто не попадают в runtime никогда, а не перезаписываются постфактум.
BaaS-решения закрывают эту проблему по-разному: одни через runtime-патчи (быстро, но детектируемо при правильном замере), другие через модифицированный бинарник. Снаружи выглядит одинаково, а уровень защиты принципиально разный.
Хороший обзор. По уровню 5-6 хочу добавить момент, который обычно всплывает позже: когда агент начинает реально ходить по продакшн-сайтам, headless-браузер довольно быстро детектится. Сайт отдаёт другой контент, показывает капчу или тихо блокирует сессию. Агент при этом формально "завершает задачу", но работает не с теми данными.
Для внутренних инструментов это не проблема. Но как только агент взаимодействует с внешними сервисами - это реально больная точка, которую в уровнях не упоминают.
Transaction-мониторинг интересная идея, но на практике есть один нюанс, который легко упустить: headless Playwright из коробки имеет характерный fingerprint, и многие сайты его обнаруживают. В результате сценарий «проходит», страница открывается, форма видна, но сайт отдаёт другой контент, показывает капчу или просто блокирует сессию. Монитор видит 200 OK, а реальный пользователь в это время видит стену с капчей.
Особенно актуально для e-commerce и SaaS, где Cloudflare и аналоги стоят почти везде. Для таких случаев стоит запускать Playwright через браузер с патченным fingerprint - иначе результаты transaction-проверок будут ненадёжными именно на тех сайтах, где мониторинг важнее всего.
Хочу добавить кое-что про L2, что часто упускают: navigator.hardwareConcurrency и deviceMemory это не просто статические числа, которые нужно подменить. Проблема в том, что эти значения коррелируют с другими сигналами. Если вы говорите "16 ядер, 32GB RAM", но при этом WebGL renderer показывает llvmpipe или Mesa, или AudioContext показывает нулевые задержки без джиттера, вся легенда рассыпается. Современные системы детекции, в частности Akamai и DataDome, строят граф корреляций. Один параметр сам по себе не срабатывает, но несовпадение двух-трёх уже достаточно.
Про deterministic noise в Canvas - хорошо, что упомянули. Именно этот момент часто ломает дешёвые stealth-решения: рандом на каждый вызов toDataURL() легко детектируется повторным рендером. Детерминизм привязанный к pid, это правильный подход.
По поводу локальных агентов - причина, почему агент «теряется», часто не в локаторах. Headless Chromium в автоматическом режиме ведёт себя иначе, чем реальный браузер, и сайты это замечают: отдают другой контент, блокируют сессию, показывают капчу. Агент видит совсем не то, что видит человек.
Для exploratory testing на реальных сайтах это первое, что стоит проверить при настройке окружения.