Browser CDP Supervisor — Дизайн

Статус: Релиз (PR 14540) Последнее обновление: 2026-04-23 Автор: @teknium1

Проблема

Нативные диалоговые окна JavaScript (alert/confirm/prompt/beforeunload) и iframe — два самых больших пробела в нашем инструментарии для работы с браузером:

  1. Диалоги блокируют поток JS. Любая операция на странице приостанавливается, пока диалог не обработан. До этой работы агент не имел возможности узнать, что диалог открыт — последующие вызовы инструментов зависали или выдавали непонятные ошибки.
  2. 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 бэкенда. Сохраняет:

Подписки при подключении: - Page.enablejavascriptDialogOpening, frameAttached, frameNavigated, frameDetached - Runtime.enableexecutionContextCreated, consoleAPICalled, exceptionThrown - Target.setAutoAttach {autoAttach: true, flatten: true} — выявляет дочерние цели OOPIF; супервизор включает Page+Runtime на каждой

Потокобезопасный доступ к состоянию через блокировку снимка; обработчики инструментов (синхронные) читают замороженный снимок без ожидания.

Жизненный цикл

Политика диалогов

Настраивается в config.yaml под ключом browser.dialog_policy:

Политика применяется к задаче; в v1 нет переопределений для отдельных диалогов.

Поверхность агента (PR 1)

Один новый инструмент

browser_dialog(action, prompt_text=None, dialog_id=None)

Инструмент только для ответа. Агент читает ожидающие диалоги из результатов 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
  }
}

Ни один новый инструмент не добавляется для этих полей — агент читает снимок, который он уже запрашивает.

Ограничение доступности

Обе поверхности ограничены проверкой _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)

Новые

Изменённые

Не-цели

Тестирование

Модульные тесты используют асинхронный мок-сервер CDP, который поддерживает достаточно протокола для отработки всех переходов состояний: подключение, включение, навигация, запуск диалога, закрытие диалога, подключение/отключение фрейма, подключение дочерней цели, завершение сеанса. Сквозное тестирование с реальным бэкендом (Browserbase + локальный Chrome) выполняется вручную — проверка через /browser connect к живому Chrome и запуск описанных тестовых случаев с диалогами и фреймами.