Browser CDP Supervisor — Дизайн
Статус: Релиз (PR 14540) Последнее обновление: 2026-04-23 Автор: @teknium1
Проблема
Нативные диалоговые окна JavaScript (alert/confirm/prompt/beforeunload) и iframe —
два самых больших пробела в нашем инструментарии для работы с браузером:
- Диалоги блокируют поток JS. Любая операция на странице приостанавливается, пока диалог не обработан. До этой работы агент не имел возможности узнать, что диалог открыт — последующие вызовы инструментов зависали или выдавали непонятные ошибки.
- Iframe невидимы. Агент мог видеть узлы iframe в DOM-снимке, но не мог кликать, вводить текст или выполнять eval внутри них — особенно для кросс-доменных (OOPIF) iframe, которые находятся в отдельных процессах Chromium.
PR #12550 предлагал
обёртку browser_dialog без состояния. Это не решает проблему обнаружения — это
более чистый вызов CDP для случаев, когда агент уже знает (по симптомам), что диалог
открыт. Закрыт как заменённый.
Матрица возможностей бэкенда (проверено live 2026-04-23)
С помощью одноразовых скриптов-пробников на странице data-URL, которая запускает
alert в главном фрейме и в same-origin srcdoc iframe, а также кросс-доменном
https://example.com iframe:
| Бэкенд | Обнаружение диалога | Ответ диалогу | Дерево фреймов | Runtime.evaluate в OOPIF через browser_cdp(frame_id=...) |
|---|---|---|---|---|
Локальный Chrome (--remote-debugging-port) / /browser connect |
✓ | ✓ полный рабочий процесс | ✓ | ✓ |
| Browserbase | ✓ (через мост) | ✓ полный рабочий процесс (через мост) | ✓ | ✓ (document.title = "Example Domain" проверено на реальном кросс-доменном iframe) |
| Camofox | ✗ нет CDP (только REST) | ✗ | частично через DOM-снимок | ✗ |
Как работает ответ Browserbase. Прокси CDP Browserbase использует Playwright
внутренне и автоматически закрывает нативные диалоги в течение ~10 мс, поэтому
Page.handleJavaScriptDialog не успевает сработать. Чтобы обойти это,
супервизор внедряет скрипт-мост через
Page.addScriptToEvaluateOnNewDocument, который переопределяет
window.alert/confirm/prompt синхронным XHR-запросом к магическому хосту
(hermes-dialog-bridge.invalid). Fetch.enable перехватывает эти XHR-запросы
до того, как они достигнут сети — диалог становится событием Fetch.requestPaused,
которое захватывает супервизор, а respond_to_dialog обрабатывает через
Fetch.fulfillRequest с JSON-телом, которое декодирует внедрённый скрипт.
Итоговый результат: с точки зрения страницы, prompt() по-прежнему возвращает
строку, предоставленную агентом. С точки зрения агента, это тот же
API browser_dialog(action=...) в любом случае. Протестировано полным циклом
на реальных сессиях Browserbase — 4/4 (alert/prompt/confirm-accept/confirm-dismiss)
пройдены, включая возврат значения обратно в JS страницы.
Camofox остаётся неподдерживаемым в этом PR; запланирован follow-up upstream issue
в jo-inc/camofox-browser с запросом конечной точки для опроса диалогов.
Архитектура
CDPSupervisor
Одна задача asyncio.Task, работающая в фоновом потоке-демоне на каждую task_id Hermes.
Поддерживает постоянное WebSocket-соединение с конечной точкой CDP бэкенда. Сохраняет:
- Очередь диалогов —
List[PendingDialog]с{id, type, message, default_prompt, session_id, opened_at} - Дерево фреймов —
Dict[frame_id, FrameInfo]с отношениями родитель-потомок, URL, origin, флагом кросс-доменного дочернего сеанса - Карта сеансов —
Dict[session_id, SessionInfo]для того, чтобы инструменты взаимодействия могли маршрутизировать к правильному присоединённому сеансу для операций OOPIF - Последние ошибки консоли — кольцевой буфер последних 50 (для диагностики PR 2)
Подписки при подключении:
- Page.enable — javascriptDialogOpening, frameAttached, frameNavigated, frameDetached
- Runtime.enable — executionContextCreated, consoleAPICalled, exceptionThrown
- Target.setAutoAttach {autoAttach: true, flatten: true} — выявляет дочерние цели OOPIF; супервизор включает Page+Runtime на каждой
Потокобезопасный доступ к состоянию через блокировку снимка; обработчики инструментов (синхронные) читают замороженный снимок без ожидания.
Жизненный цикл
- Запуск:
SupervisorRegistry.get_or_start(task_id, cdp_url)— вызываетсяbrowser_navigate, созданием сессии Browserbase,/browser connect. Идемпотентен. - Остановка: завершение сеанса или
/browser disconnect. Отменяет задачу asyncio, закрывает WebSocket, удаляет состояние. - Перепривязка: если URL CDP изменился (пользователь переподключился к новому Chrome), остановить старый супервизор и запустить новый — никогда не переиспользовать состояние между конечными точками.
Политика диалогов
Настраивается в config.yaml под ключом browser.dialog_policy:
must_respond(по умолчанию) — захватить, отобразить вbrowser_snapshot, ждать явного вызоваbrowser_dialog(action=...). После 300-секундного тайм-аута безопасности без ответа автоматически закрыть и записать в лог. Предотвращает зависание из-за багов агента.auto_dismiss— записать и сразу закрыть; агент видит это позже черезbrowser_stateвнутриbrowser_snapshot.auto_accept— записать и принять (полезно дляbeforeunload, когда пользователь хочет чисто уйти со страницы).
Политика применяется к задаче; в v1 нет переопределений для отдельных диалогов.
Поверхность агента (PR 1)
Один новый инструмент
browser_dialog(action, prompt_text=None, dialog_id=None)
action="accept"/"dismiss"→ отвечает на указанный или единственный ожидающий диалог (обязательно)prompt_text=...→ текст для передачи в диалогprompt()dialog_id=...→ для устранения затруднений, когда в очереди несколько диалогов (редко)
Инструмент только для ответа. Агент читает ожидающие диалоги из результатов
browser_snapshot перед вызовом.
Расширение browser_snapshot
Добавляет три опциональных поля в существующий вывод вывода, когда подключен супервизор:
{
"pending_dialogs": [
{"id": "d-1", "type": "alert", "message": "Hello", "opened_at": 1650000000.0}
],
"recent_dialogs": [
{"id": "d-1", "type": "alert", "message": "...", "opened_at": 1650000000.0,
"closed_at": 1650000000.1, "closed_by": "remote"}
],
"frame_tree": {
"top": {"frame_id": "FRAME_A", "url": "https://example.com/", "origin": "https://example.com"},
"children": [
{"frame_id": "FRAME_B", "url": "about:srcdoc", "is_oopif": false},
{"frame_id": "FRAME_C", "url": "https://ads.example.net/", "is_oopif": true, "session_id": "SID_C"}
],
"truncated": false
}
}
-
pending_dialogs: диалоги, в данный момент блокирующие поток JS страницы. Агент должен вызватьbrowser_dialog(action=...)для ответа. Пусто в Browserbase, потому что их прокси CDP автоматически закрывает диалоги в течение ~10 мс. -
recent_dialogs: кольцевой буфер до 20 недавно закрытых диалогов с меткойclosed_by—"agent"(мы ответили),"auto_policy"(локальный auto_dismiss/auto_accept),"watchdog"(тайм-аут must_respond), или"remote"(браузер/бэкенд закрыл его за нас, например Browserbase). Это даёт агентам на Browserbase возможность видеть, что произошло. -
frame_tree: структура фреймов, включая кросс-доменные (OOPIF) потомки. Ограничено 30 записями + глубина OOPIF 2, чтобы ограничить размер снимка на страницах с большим количеством рекламы.truncated: trueпоявляется, когда лимиты были превышены; агенты, которым нужно полное дерево, могут использоватьbrowser_cdpсPage.getFrameTree.
Ни один новый инструмент не добавляется для этих полей — агент читает снимок, который он уже запрашивает.
Ограничение доступности
Обе поверхности ограничены проверкой _browser_cdp_check (супервизор может
работать только тогда, когда конечная точка CDP достижима). В сессиях на Camofox /
без бэкенда инструмент диалогов скрыт, а снимок опускает новые поля — никакого
раздувания схемы.
Взаимодействие с кросс-доменными iframe
Расширяя работу по обнаружению диалогов, browser_cdp(frame_id=...) маршрутизирует
CDP-вызовы (особенно Runtime.evaluate) через уже подключённый WebSocket
супервизора, используя дочерний sessionId OOPIF. Агенты берут frame_id из
browser_snapshot.frame_tree.children[], где is_oopif=true, и передают их
в browser_cdp. Для same-origin iframe (без выделенного сеанса CDP)
агент использует contentWindow/contentDocument из верхнеуровневого
Runtime.evaluate — супервизор возвращает ошибку, указывающую на этот
запасной вариант, когда frame_id принадлежит не-OOPIF.
В Browserbase это ЕДИНСТВЕННЫЙ надёжный путь для взаимодействия с iframe —
соединения CDP без состояния (открываемые при каждом вызове browser_cdp)
сталкиваются с истечением подписанных URL, в то время как долгоживущее соединение
супервизора сохраняет действующий сеанс.
Camofox (последующее обновление)
Запланирован issue в jo-inc/camofox-browser с добавлением:
- Playwright page.on('dialog', handler) для каждого сеанса
- Конечная точка опроса GET /tabs/:tabId/dialogs
- POST /tabs/:tabId/dialogs/:id для принятия/закрытия
- Конечная точка интроспекции дерева фреймов
Затронутые файлы (PR 1)
Новые
tools/browser_supervisor.py—CDPSupervisor,SupervisorRegistry,PendingDialog,FrameInfotools/browser_dialog_tool.py— обработчик инструментаbrowser_dialogtests/tools/test_browser_supervisor.py— мок-сервер WebSocket CDP + тесты жизненного цикла/состоянияwebsite/docs/developer-guide/browser-supervisor.md— этот файл
Изменённые
toolsets.py— регистрацияbrowser_dialogвbrowser,hermes-acp,hermes-api-server, основных наборах инструментов (ограничено доступностью CDP)tools/browser_tool.py- хук запуска
browser_navigate: если URL CDP разрешим,SupervisorRegistry.get_or_start(task_id, cdp_url) browser_snapshot(строка ~1536): объединение состояния супервизора в возвращаемые данные- обработчик
/browser connect: перезапуск супервизора с новой конечной точкой - хуки завершения сеанса в
_cleanup_browser_session hermes_cli/config.py— добавлениеbrowser.dialog_policyиbrowser.dialog_timeout_sвDEFAULT_CONFIG- Документация:
website/docs/user-guide/features/browser.md,website/docs/reference/tools-reference.md,website/docs/reference/toolsets-reference.md
Не-цели
- Обнаружение/взаимодействие для Camofox (пробел на стороне поставщика; отслеживается отдельно)
- Потоковая передача событий диалогов/фреймов в реальном времени пользователю (потребовались бы хуки шлюза)
- Сохранение истории диалогов между сеансами (только в памяти)
- Политики диалогов для отдельных фреймов (агент может выразить это через
dialog_id) - Замена
browser_cdp— он остаётся запасным вариантом для долгого хвоста (cookies, viewport, сетевое троттлинг)
Тестирование
Модульные тесты используют асинхронный мок-сервер CDP, который поддерживает
достаточно протокола для отработки всех переходов состояний: подключение, включение,
навигация, запуск диалога, закрытие диалога, подключение/отключение фрейма,
подключение дочерней цели, завершение сеанса. Сквозное тестирование с реальным
бэкендом (Browserbase + локальный Chrome) выполняется вручную — проверка через
/browser connect к живому Chrome и запуск описанных тестовых случаев с диалогами и фреймами.