Browser CDP Supervisor
CDP supervisor закрывает два застарелых пробела в браузерном инструментарии Hermes:
- Нативные JS-диалоги (
alert/confirm/prompt/beforeunload) блокируют поток JS на странице. Без мониторинга агент не знает, что диалог открыт — последующие вызовы инструментов зависают или бросают непрозрачные ошибки. - Кросс-доменные iframe (OOPIFs) невидимы для
Runtime.evaluateверхнего уровня. Узлы iframe в снимке DOM агенту видны, но кликнуть, напечатать или выполнить eval внутри них он не может без CDP-сессии, прикреплённой к дочернему таргету.
Обе задачи supervisor решает так: держит постоянный WebSocket к CDP-эндпоинту бэкенда
на каждую браузерную задачу, отражает ожидающие диалоги и структуру фреймов в
browser_snapshot, а для явных ответов на диалоги даёт инструмент browser_dialog.
Поддержка бэкендов
| Бэкенд | Обнаружение диалогов | Ответ на диалог | Дерево фреймов | OOPIF Runtime.evaluate через browser_cdp(frame_id=...) |
|---|---|---|---|---|
Локальный Chrome (--remote-debugging-port) / /browser connect | ✓ | ✓ полный воркфлоу | ✓ | ✓ |
| Browserbase | ✓ (через мост) | ✓ полный воркфлоу (через мост) | ✓ | ✓ |
| Camofox | ✗ нет CDP (только REST) | ✗ | частично через снимок DOM | ✗ |
Особенность Browserbase. CDP-прокси Browserbase использует Playwright внутри
и автоматически закрывает нативные диалоги примерно за 10 мс, поэтому
Page.handleJavaScriptDialog не успевает среагировать. Supervisor инжектирует
мостовой скрипт через Page.addScriptToEvaluateOnNewDocument, который перекрывает
window.alert/confirm/prompt синхронным XHR к магическому хосту
(hermes-dialog-bridge.invalid). Fetch.enable перехватывает эти XHR до того,
как они уходят в сеть — диалог превращается в событие Fetch.requestPaused, которое
supervisor захватывает, а respond_to_dialog выполняет через Fetch.fulfillRequest
с JSON-телом, которое декодирует инжектированный скрипт.
Для страницы prompt() по-прежнему возвращает строку, заданную
агентом. Со стороны агента в обоих случаях это один и тот же API browser_dialog(action=...).
Camofox не поддерживается — нет CDP-поверхности, только REST.
Архитектура
CDPSupervisor
Один asyncio.Task в фоновом daemon-потоке на каждый 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 записей для диагностики
Подписки при подключении:
Page.enable—javascriptDialogOpening,frameAttached,frameNavigated,frameDetachedRuntime.enable—executionContextCreated,consoleAPICalled,exceptionThrownTarget.setAutoAttach {autoAttach: true, flatten: true}— обнаруживает дочерние OOPIF-таргеты; на каждом из них supervisor включаетPage+Runtime
Доступ к состоянию потокобезопасен через snapshot-блокировку; синхронные обработчики инструментов читают замороженный снимок без await.
Жизненный цикл
- Запуск:
SupervisorRegistry.get_or_start(task_id, cdp_url)— вызывается изbrowser_navigate, при создании сессии Browserbase, через/browser connect. Идемпотентен. - Остановка: при завершении сессии или через
/browser disconnect. Отменяет asyncio-задачу, закрывает WebSocket, сбрасывает состояние. - Переподключение: при смене CDP URL (пользователь подключается к новому Chrome) старый supervisor останавливается и запускается свежий — состояние между эндпоинтами не переносится никогда.
Политика диалогов
Настраивается в config.yaml в разделе browser.dialog_policy:
must_respond(по умолчанию) — захватить, отразить вbrowser_snapshot, ждать явного вызоваbrowser_dialog(action=...). После 300 секунд без ответа срабатывает защитный таймаут: диалог автоматически закрывается с записью в лог. Это страховка от зависания агента с багом.auto_dismiss— зафиксировать и сразу закрыть; диалог агент видит постфактум черезbrowser_stateвнутриbrowser_snapshot.auto_accept— зафиксировать и принять (удобно дляbeforeunload, когда воркфлоу нужно корректно уйти со страницы).
Политика задаётся на уровне задачи; переопределений для отдельных диалогов нет.
Поверхность для агента
Инструмент browser_dialog
browser_dialog(action, prompt_text=None, dialog_id=None)
action="accept"/"dismiss"→ отвечает на указанный или единственный ожидающий диалог (обязательный параметр)prompt_text=...→ текст для диалогаprompt()dialog_id=...→ уточняет выбор, если в очереди несколько диалогов (редкий случай)
Инструмент работает только на ответ. Ожидающие диалоги агент читает из вывода
browser_snapshot перед вызовом.
Расширение browser_snapshot
При подключённом supervisor к существующему выводу снимка добавляются три опциональных поля:
{
"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 (supervisor запускается только
при наличии доступного CDP-эндпоинта). На Camofox и в сессиях без бэкенда инструмент
диалога скрывается, а снимок не включает новые поля — схема не раздувается.
Взаимодействие с кросс-доменными iframe
browser_cdp(frame_id=...) направляет CDP-вызовы (прежде всего Runtime.evaluate)
через уже подключённый WebSocket supervisor, используя дочерний sessionId OOPIF.
Frame_id агенты берут из browser_snapshot.frame_tree.children[], где
is_oopif=true, и передают их в browser_cdp. Для однодоменных iframe (без
отдельной CDP-сессии) агент использует contentWindow/contentDocument из
Runtime.evaluate верхнего уровня — если же frame_id принадлежит не-OOPIF,
supervisor выводит ошибку с подсказкой про этот вариант.
На Browserbase это единственный надёжный путь для взаимодействия с iframe:
соединения CDP без состояния (открываемые на каждый вызов browser_cdp) упираются
в истечение срока подписанного URL, тогда как долгоживущее соединение supervisor
удерживает валидную сессию.
Расположение файлов
tools/browser_supervisor.py—CDPSupervisor,SupervisorRegistry,PendingDialog,FrameInfotools/browser_dialog_tool.py— обработчик инструментаbrowser_dialogtools/browser_tool.py— хук запуска вbrowser_navigate, слияние вbrowser_snapshot, переподключение через/browser connect, завершение в_cleanup_browser_sessiontoolsets.py— регистрируетbrowser_dialogв наборах инструментовbrowser,hermes-acp,hermes-api-serverи core (с проверкой доступности CDP)hermes_cli/config.py— значения по умолчанию дляbrowser.dialog_policyиbrowser.dialog_timeout_s
За рамками задачи
- Обнаружение и взаимодействие для Camofox (пробел на стороне upstream; отслеживается отдельно)
- Потоковая передача событий диалогов и фреймов пользователю в реальном времени (потребовала бы хуков шлюза)
- Сохранение истории диалогов между сессиями (только в памяти)
- Политики диалогов на уровне отдельного iframe (агент может выразить это через
dialog_id) - Замена
browser_cdp— он остаётся аварийным выходом для длинного хвоста случаев (куки, viewport, троттлинг сети)
Тестирование
Юнит-тесты (tests/tools/test_browser_supervisor.py) используют asyncio mock-сервер
CDP, реализующий ровно столько протокола, сколько нужно для проверки всех переходов
состояний: подключение, enable, навигация, открытие диалога, закрытие диалога,
прикрепление/отсоединение фрейма, подключение дочернего таргета, завершение сессии.
E2E с реальным бэкендом (Browserbase + локальный браузер на базе Chromium) —
ручное тестирование: подключитесь через /browser connect к живому браузеру на
базе Chromium и прогоните тест-кейсы для диалогов и фреймов, описанные выше.