Перейти к основному содержимому

Browser CDP Supervisor

CDP-супервизор закрывает два давних пробела в браузерном инструментарии VibeOS:

  1. Нативные JS-диалоги (alert/confirm/prompt/beforeunload) блокируют JS-поток страницы. Без надзора агент не может узнать, что диалог открыт — последующие вызовы инструментов зависают или выдают непонятные ошибки.
  2. Кросс-доменные iframe (OOPIF) невидимы для Runtime.evaluate верхнего уровня. Агент видит узлы iframe в DOM-снимке, но не может кликать, вводить текст или выполнять eval внутри них без CDP-сессии, прикреплённой к дочернему целевому объекту.

Супервизор решает обе проблемы, удерживая постоянный WebSocket к CDP-точке бэкенда для каждой задачи браузера, выводя ожидающие диалоги и структуру фреймов в browser_snapshot, а также предоставляя инструмент browser_dialog для явных ответов.

Поддержка бэкендов​

БэкендОбнаружение диалоговОтвет на диалогиДерево фреймовOOPIF Runtime.evaluate через browser_cdp(frame_id=...)
Локальный Chrome (--remote-debugging-port) / /browser connect✓✓ полный workflow✓✓
Browserbase✓ (через мост)✓ полный workflow (через мост)✓✓
Camofox✗ нет CDP (только REST)✗частично через DOM-снимок✗

Особенность Browserbase. Прокси CDP Browserbase использует Playwright внутри и автоматически закрывает нативные диалоги примерно за 10 мс, поэтому Page.handleJavaScriptDialog не успевает сработать. Супервизор внедряет скрипт-мост через Page.addScriptToEvaluateOnNewDocument, который переопределяет window.alert/confirm/prompt синхронным XHR-запросом к магическому хосту (vibeos-dialog-bridge.invalid). Fetch.enable перехватывает эти XHR-запросы до того, как они достигают сети — диалог становится событием Fetch.requestPaused, которое супервизор захватывает, а respond_to_dialog обрабатывает через Fetch.fulfillRequest с JSON-телом, которое декодирует внедрённый скрипт.

С точки зрения страницы, prompt() всё ещё возвращает строку, предоставленную агентом. С точки зрения агента, это тот же API browser_dialog(action=...) в любом случае.

Camofox не поддерживается — нет CDP-поверхности, только REST.

Архитектура​

CDPSupervisor​

Одна задача asyncio.Task, работающая в фоновом потоке-демоне для каждого task_id VibeOS. Удерживает постоянный WebSocket к CDP-точке бэкенда. Поддерживает:

  • Очередь диалогов — List[PendingDialog] с {id, type, message, default_prompt, session_id, opened_at}
  • Дерево фреймов — Dict[frame_id, FrameInfo] с родительскими связями, URL, источником, признаком кросс-доменной дочерней сессии
  • Карта сессий — Dict[session_id, SessionInfo], чтобы инструменты взаимодействия могли направлять запросы к нужной прикреплённой сессии для операций с OOPIF
  • Недавние ошибки консоли — кольцевой буфер последних 50 для диагностики

Подписывается при подключении:

  • 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, когда workflow хочет чисто уйти со страницы).

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

Поверхность для агента​

Инструмент browser_dialog​

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. Для iframe одного источника (без выделенной CDP-сессии) агент использует contentWindow/contentDocument из Runtime.evaluate верхнего уровня — супервизор выводит ошибку, указывающую на этот запасной вариант, когда frame_id принадлежит не-OOPIF.

На Browserbase это единственный надёжный путь для взаимодействия с iframe — stateless CDP-соединения (открываемые при каждом вызове browser_cdp) сталкиваются с истечением подписанных URL, в то время как долгоживущее соединение супервизора сохраняет действительную сессию.

Структура файлов​

  • tools/browser_supervisor.py — CDPSupervisor, SupervisorRegistry, PendingDialog, FrameInfo
  • tools/browser_dialog_tool.py — обработчик инструмента browser_dialog
  • tools/browser_tool.py — хук запуска browser_navigate, слияние browser_snapshot, переподключение /browser connect, завершение _cleanup_browser_session
  • toolsets.py — регистрирует browser_dialog в browser, vibeos-acp, vibeos-api-server и основных наборах инструментов (с проверкой доступности CDP)
  • vibeos_cli/config.py — значения по умолчанию для browser.dialog_policy и browser.dialog_timeout_s

Нецелевые задачи​

  • Обнаружение/взаимодействие для Camofox (пробел на стороне вышестоящего проекта; отслеживается отдельно)
  • Потоковая передача событий диалогов/фреймов в реальном времени пользователю (требовало бы хуков шлюза)
  • Сохранение истории диалогов между сессиями (только в памяти)
  • Политики диалогов для отдельных iframe (агент может выразить это через dialog_id)
  • Замена browser_cdp — он остаётся запасным выходом для редких случаев (cookies, viewport, сетевое регулирование)

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

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