Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

Supervisor CDP do Navegador

O supervisor CDP fecha duas lacunas antigas do ferramental de navegador do Hermes:

  1. Diálogos nativos de JS (alert/confirm/prompt/beforeunload) bloqueiam a thread de JS da página. Sem supervisão, o agente não tem como saber que um diálogo está aberto — as chamadas de ferramenta seguintes travam ou devolvem erros opacos.
  2. Iframes de origem cruzada (OOPIFs) são invisíveis para o Runtime.evaluate de nível superior. O agente enxerga os nós de iframe no snapshot do DOM, mas não consegue clicar, digitar nem avaliar código dentro deles sem uma sessão CDP anexada ao alvo filho.

O supervisor resolve os dois casos mantendo um WebSocket persistente com o endpoint CDP do backend por tarefa de navegador, expondo diálogos pendentes e a estrutura de frames no browser_snapshot, e disponibilizando a ferramenta browser_dialog para respostas explícitas.

Suporte por backend​

BackendDetecta diálogoResponde diálogoÁrvore de framesRuntime.evaluate em OOPIF via browser_cdp(frame_id=...)
Chrome local (--remote-debugging-port) / /browser connect✓✓ fluxo completo✓✓
Browserbase✓ (via ponte)✓ fluxo completo (via ponte)✓✓
Camofox✗ sem CDP (só REST)✗parcial, via snapshot do DOM✗

Peculiaridade do Browserbase. O proxy CDP do Browserbase usa Playwright internamente e descarta diálogos nativos automaticamente em cerca de 10 ms, então o Page.handleJavaScriptDialog não consegue acompanhar. O supervisor injeta um script de ponte via Page.addScriptToEvaluateOnNewDocument que substitui window.alert/confirm/prompt por um XHR síncrono para um host mágico (hermes-dialog-bridge.invalid). O Fetch.enable intercepta esses XHRs antes que eles cheguem à rede — o diálogo vira um evento Fetch.requestPaused que o supervisor captura, e o respond_to_dialog responde via Fetch.fulfillRequest com um corpo JSON que o script injetado decodifica.

Do ponto de vista da página, o prompt() continua devolvendo a string fornecida pelo agente. Do ponto de vista do agente, é a mesma API browser_dialog(action=...) nos dois casos.

O Camofox não tem suporte — não expõe superfície CDP, só REST.

Arquitetura​

CDPSupervisor​

Uma asyncio.Task rodando em uma thread daemon de segundo plano por task_id do Hermes. Mantém um WebSocket persistente com o endpoint CDP do backend. Guarda:

  • Fila de diálogos — List[PendingDialog] com {id, type, message, default_prompt, session_id, opened_at}
  • Árvore de frames — Dict[frame_id, FrameInfo] com relações de pai, URL, origem e se é sessão filha de origem cruzada
  • Mapa de sessões — Dict[session_id, SessionInfo] para que as ferramentas de interação encaminhem à sessão anexada correta nas operações com OOPIF
  • Erros recentes de console — buffer circular com os últimos 50, para diagnóstico

Assina, ao anexar:

  • Page.enable — javascriptDialogOpening, frameAttached, frameNavigated, frameDetached
  • Runtime.enable — executionContextCreated, consoleAPICalled, exceptionThrown
  • Target.setAutoAttach {autoAttach: true, flatten: true} — expõe alvos filhos de OOPIF; o supervisor habilita Page+Runtime em cada um

O acesso ao estado é seguro entre threads por meio de um lock de snapshot; os handlers de ferramenta (síncronos) leem o snapshot congelado sem precisar de await.

Ciclo de vida​

  • Início: SupervisorRegistry.get_or_start(task_id, cdp_url) — chamado por browser_navigate, pela criação de sessão do Browserbase e pelo /browser connect. É idempotente.
  • Parada: encerramento da sessão ou /browser disconnect. Cancela a task do asyncio, fecha o WebSocket e descarta o estado.
  • Religamento: se a URL do CDP muda (o usuário reconecta a um novo Chrome), o supervisor antigo é parado e um novo é iniciado — o estado nunca é reaproveitado entre endpoints.

Política de diálogos​

Configurável no config.yaml, em browser.dialog_policy:

  • must_respond (padrão) — captura, expõe no browser_snapshot e espera uma chamada explícita de browser_dialog(action=...). Depois de um timeout de segurança de 300 s sem resposta, descarta automaticamente e registra em log. Evita que um agente com defeito trave para sempre.
  • auto_dismiss — registra e descarta na hora; o agente vê o ocorrido depois do fato, pelo browser_state dentro do browser_snapshot.
  • auto_accept — registra e aceita (útil para beforeunload, quando o fluxo quer sair da página sem atrito).

A política vale por tarefa; não há sobrescrita por diálogo.

Superfície para o agente​

Ferramenta browser_dialog​

browser_dialog(action, prompt_text=None, dialog_id=None)
  • action="accept" / "dismiss" → responde ao diálogo pendente indicado ou ao único que houver (obrigatório)
  • prompt_text=... → texto a fornecer a um diálogo de prompt()
  • dialog_id=... → desambigua quando há vários diálogos na fila (raro)

A ferramenta só responde. O agente lê os diálogos pendentes na saída do browser_snapshot antes de chamá-la.

Extensão do browser_snapshot​

Acrescenta três campos opcionais à saída de snapshot já existente quando há um supervisor anexado:

{
"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 — diálogos que estão bloqueando a thread de JS da página no momento. O agente precisa chamar browser_dialog(action=...) para responder. Fica vazio no Browserbase, porque o proxy CDP deles descarta tudo em cerca de 10 ms.

  • recent_dialogs — buffer circular com até 20 diálogos fechados recentemente, com uma etiqueta closed_by: "agent" (nós respondemos), "auto_policy" (auto_dismiss/auto_accept local), "watchdog" (estourou o timeout do must_respond) ou "remote" (o navegador/backend fechou por conta própria, como no Browserbase). É assim que agentes no Browserbase ainda enxergam o que aconteceu.

  • frame_tree — estrutura de frames, incluindo os filhos de origem cruzada (OOPIF). Limitada a 30 entradas + profundidade 2 de OOPIF, para conter o tamanho do snapshot em páginas cheias de anúncio. O campo truncated: true aparece quando os limites foram atingidos; agentes que precisam da árvore completa podem usar browser_cdp com Page.getFrameTree.

Nada disso acrescenta esquema novo de ferramenta — o agente lê o snapshot que já pede.

Condição de disponibilidade​

As duas superfícies dependem do _browser_cdp_check (o supervisor só roda quando há um endpoint CDP acessível). Em sessões com Camofox ou sem backend, a ferramenta de diálogo fica oculta e o snapshot omite os campos novos — sem inchar o esquema.

Interação com iframes de origem cruzada​

O browser_cdp(frame_id=...) encaminha chamadas CDP (em especial o Runtime.evaluate) pelo WebSocket já conectado do supervisor, usando o sessionId filho do OOPIF. Os agentes escolhem os frame_ids em browser_snapshot.frame_tree.children[] onde is_oopif=true e os passam para o browser_cdp. Para iframes de mesma origem (sem sessão CDP dedicada), o agente usa contentWindow/contentDocument a partir de um Runtime.evaluate de nível superior — o supervisor devolve um erro apontando para esse fallback quando o frame_id pertence a um frame que não é OOPIF.

No Browserbase, esse é o único caminho confiável para interagir com iframes — conexões CDP sem estado (abertas a cada chamada de browser_cdp) esbarram na expiração da URL assinada, enquanto a conexão de longa duração do supervisor mantém uma sessão válida.

Organização dos arquivos​

  • tools/browser_supervisor.py — CDPSupervisor, SupervisorRegistry, PendingDialog, FrameInfo
  • tools/browser_dialog_tool.py — handler da ferramenta browser_dialog
  • tools/browser_tool.py — hook de início do browser_navigate, merge do browser_snapshot, reanexação do /browser connect, encerramento do _cleanup_browser_session
  • toolsets.py — registra o browser_dialog nos conjuntos browser, hermes-acp, hermes-api-server e nos conjuntos centrais (condicionado ao CDP estar acessível)
  • hermes_cli/config.py — padrões de browser.dialog_policy e browser.dialog_timeout_s

Fora de escopo​

  • Detecção e interação para o Camofox (lacuna do upstream; acompanhada em separado)
  • Transmitir eventos de diálogo/frame ao vivo para o usuário (exigiria hooks de gateway)
  • Persistir o histórico de diálogos entre sessões (só em memória)
  • Políticas de diálogo por iframe (o agente consegue expressar isso pelo dialog_id)
  • Substituir o browser_cdp — ele continua sendo a válvula de escape para a cauda longa (cookies, viewport, limitação de rede)

Testes​

Os testes de unidade (tests/tools/test_browser_supervisor.py) usam um servidor CDP falso em asyncio que fala o suficiente do protocolo para exercitar todas as transições de estado: anexar, habilitar, navegar, disparar diálogo, descartar diálogo, anexar/desanexar frame, anexar alvo filho, encerrar sessão. O E2E com backend real (Browserbase + navegador da família Chromium local) é manual — exercite pelo /browser connect em um navegador da família Chromium ao vivo e rode os casos de teste de diálogo/frame descritos acima.