Browser CDP Supervisor — Design
Status: Shipped (PR 14540) Last updated: 2026-04-23 Author: @teknium1
Problem
Native JS dialogs (alert/confirm/prompt/beforeunload) and iframes are
the two biggest gaps in our browser tooling:
- Dialogs block the JS thread. Any operation on the page stalls until the dialog is handled. Before this work, the agent had no way to know a dialog was open — subsequent tool calls would hang or throw opaque errors.
- Iframes are invisible. The agent could see iframe nodes in the DOM snapshot but could not click, type, or eval inside them — especially cross-origin (OOPIF) iframes that live in separate Chromium processes.
PR #12550 proposed a
stateless browser_dialog wrapper. That doesn't solve detection — it's a
cleaner CDP call for when the agent already knows (via symptoms) that a dialog
is open. Closed as superseded.
Backend capability matrix (verified live 2026-04-23)
Using throwaway probe scripts against a data-URL page that fires alerts in the
main frame and in a same-origin srcdoc iframe, plus a cross-origin
https://example.com iframe:
| Backend | Dialog detect | Dialog respond | Frame tree | OOPIF Runtime.evaluate via browser_cdp(frame_id=...) |
||
lang: ru
|---|---|---|
| Локальный Chrome (--remote-debugging-port) / /browser connect | ✓ | ✓ полный рабочий процесс | ✓ | ✓ |
| База браузеров | ✓ (через мост) | ✓ полный рабочий процесс (через мост) | ✓ | ✓ (document.title = "Example Domain" проверено на реальном iframe с перекрестным происхождением) |
| Камофокс | ✗ нет 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() по-прежнему возвращает
строка, предоставленная агентом. С точки зрения агента, это то же самое.
browser_dialog(action=...) API в любом случае. Сквозное тестирование против
реальные сеансы с базой браузеров — 4/4 (предупреждение/запрос/подтверждение-принятие/подтверждение-отклонение)
pass, включая возврат значений обратно на страницу JS.
Camofox остается без поддержки этого PR; последующий выпуск восходящего потока запланирован на
jo-inc/camofox-browser запрашивает конечную точку опроса диалога.
Архитектура
CDPSupervisor
Один asyncio.Task работает в фоновом потоке демона согласно Hermes task_id.
Удерживает постоянный WebSocket для конечной точки CDP серверной части. Поддерживает:
- Очередь диалогов —
List[PendingDialog]с{id, type, message, default_prompt, session_id, opened_at} - Дерево фреймов —
Dict[frame_id, FrameInfo]с родительскими отношениями, URL-адресом, источником, дочерним сеансом с перекрестным происхождением. - Карта сеансов —
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, создание сеанса базы браузера,/browser connect. Идемпотент. - Стоп: разрыв сеанса или
/browser disconnect. Отменяет асинхронность задача, закрывает 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 с использованием дочернего объекта OOPIF sessionId. Агенты выбирают идентификаторы кадров из
browser_snapshot.frame_tree.children[] где is_oopif=true и передайте их
на browser_cdp. Для iframe того же происхождения (без выделенного сеанса CDP)
агент использует contentWindow/contentDocument с верхнего уровня
Вместо этого Runtime.evaluate — супервизор выдает ошибку, указывающую на это.
резервный вариант, когда frame_id принадлежит не-OOPIF.
В Browserbase это ЕДИНСТВЕННЫЙ надежный путь взаимодействия iframe —
Соединения CDP без сохранения состояния (открытые для вызова browser_cdp) достигают подписанного URL-адреса
истечения срока действия, в то время как долговременное соединение супервизора сохраняет действительный сеанс.
Камофокс (продолжение)
Выпуск запланирован против jo-inc/camofox-browser добавления:
- Драматург 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_dialogобработчик инструментаtests/tools/test_browser_supervisor.py— макет сервера CDP WebSocket + тесты жизненного цикла/состояния.website/docs/developer-guide/browser-supervisor.md— этот файл
Изменено
toolsets.py— зарегистрируйтеbrowser_dialogвbrowser,hermes-acp,hermes-api-server, базовых наборах инструментов (с учетом доступности CDP)tools/browser_tool.pybrowser_navigatestart-hook: если 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 (пробел в восходящем направлении; отслеживается отдельно)
- Потоковая передача событий диалога/кадра пользователю в реальном времени (потребуются перехватчики шлюза)
- Сохранение истории диалогов между сеансами (только в памяти)
- Политики диалога для каждого iframe (агент может выразить это через
dialog_id) - Замена
browser_cdp— он остается аварийным люком для длинного хвоста (файлы cookie, область просмотра, регулирование сети)
Тестирование
Модульные тесты используют асинхронный макет CDP-сервера, который достаточно хорошо знает протокол.
для выполнения всех переходов между состояниями: присоединения, включения, навигации, запуска диалога,
закрытие диалога, присоединение/отсоединение фрейма, присоединение дочерней цели, завершение сеанса.
Реальный бэкэнд E2E (база браузера + локальный Chrome) выполняется вручную; проверять сценарии из
расследование от 23 апреля 2026 г. хранится в репозитории под
scripts/browser_supervisor_e2e.py, чтобы каждый мог пройти повторную верификацию на новом сервере.
версии.