Files
graph_explorer/issues/0038-browser-launch-cdp-control.md
T
egutierrez 8733b7d175 docs(issues): browser externo + CDP + multi-profile (0038, 0039, 0040)
- 0038: lanzar Chrome/Edge/Brave externo con --remote-debugging-port +
  --user-data-dir por profile, control via CDP desde cdp-cli Go.
  Decision Go vs C++ in-process documentada; deja la puerta abierta a
  un cliente C++ minimo solo para streaming en el futuro. Supersedes 0032.
- 0039: gestor de cookies/sesiones por profile via CDP — list, export
  EditThisCookie, import, clear selectivo, health checks con selectores,
  locks cuando un enricher esta usando el profile.
- 0040: profiles como concepto de primera clase — metadata (color, icon,
  browser_preference, UA, project, template), templates anon/auth/work/
  investigation, ProfilePicker reusable, project default, tag en
  executions.metrics. Actualiza 0038 para apuntar a 0040 como duenio
  del UX de profiles.
2026-05-04 22:16:42 +02:00

12 KiB

id, title, status, priority, created, depends_on, supersedes
id title status priority created depends_on supersedes
0038 Browser externo lanzable desde el app + control via CDP pending high 2026-05-04
0014
0029
0032

Contexto y objetivo

En lugar de embeber un browser dentro del app (CEF/WebView2) o automatizar Chromium oculto via Playwright (issue 0032), el modelo elegido es:

  1. El usuario navega en su browser real (Chrome / Edge / Brave / Vivaldi instalado en el sistema), con su layout, sus extensiones, sus marcadores.
  2. graph_explorer lanza una instancia controlable del browser cuando hace falta, con --remote-debugging-port y --user-data-dir apuntando a un profile gestionado por el app.
  3. El app habla con esa instancia via CDP (Chrome DevTools Protocol) para los enrichers que necesiten JS/cookies/auth.
  4. La extension del issue 0014 cierra el loop por el lado humano: lo que el usuario lee en cualquier pestaña se manda al grafo con un click.

Beneficio: cero bytes de bundle (browser ya instalado), UX nativa, control programatico cuando se necesita.

Decision de stack

Aspecto Decision
Browser objetivo Chromium-based detectado en runtime: Chrome > Edge > Brave > Vivaldi
Protocolo control CDP (websocket a localhost:<port>)
Persistencia --user-data-dir=<app>/local_files/browser_profiles/<name>/
Cliente CDP Go — binario cdp-cli que envuelve las funciones del registry (issue 0029). El app C++ lo invoca via subprocess.
Descarga binaria Ninguna — se exige browser instalado, error claro si no se detecta

Por que Go y no C++ in-process

Las funciones CDP (cdp_connect, cdp_navigate, cdp_get_html, cdp_evaluate, cdp_screenshot, ...) ya estan planificadas en Go en 0029. Implementar un cliente CDP nativo en C++ duplicaria ~1500 LoC (websocket + state machine + JSON dispatch) sin ganancia real:

  • Las operaciones reales (get-html, screenshot, evaluate) tardan 500-3000 ms. El overhead de spawn de subprocess (~30-50 ms) es ruido.
  • Aislamiento de fallos: un cuelgue del websocket muere con el subprocess en vez de bloquear el render loop de ImGui.
  • Reuso: el mismo cdp-cli lo invocan los enrichers Python, scripts manuales y otras apps del registry. Una sola implementacion mantenida.
  • Bundle: +8 MB de binario Go vs +deps C++ (websocket lib + JSON) que hay que integrar en CMake.

Playwright (0032) queda descartado como dependencia del app: el binario de 200 MB y la complejidad de gestionar profiles via Python no compensan cuando el SO ya tiene browser. Se mantiene 0032 como nota historica.

Cliente C++ in-process: reservado para streaming (futuro)

La unica operacion donde un cliente CDP nativo en C++ ganaria es streaming en vivo del browser dentro del app (ej. Page.startScreencast para mostrar la pestaña actual a 30fps en un panel ImGui, o capturar eventos Network.responseReceived / Page.frameNavigated en tiempo real). Para eso si hace falta un websocket persistente en el render thread, sin overhead de spawn.

Cuando aparezca ese caso (no esta en 0038 ni 0039), se abre un issue separado para añadir un cliente C++ minimo orientado solo a streaming. El flujo sera:

  • Mantener cdp-cli Go para todo lo one-shot (sigue siendo la via default).
  • Añadir cpp/functions/browser/cdp_stream_cpp_browser.{h,cpp} con websocket persistente, suscrito a un subset de eventos CDP.
  • El stream se conecta al MISMO --remote-debugging-port que ya tiene abierto el browser lanzado desde 0038. Coexisten sin conflicto: CDP permite multiples clientes contra la misma instancia.

Es decir: la arquitectura de 0038 no cierra la puerta — la deja abierta. Empezamos en Go porque es la decision pragmatica hoy, y añadimos C++ in-process solo si aparece un caso de streaming concreto que lo justifique.

Arquitectura

Componentes

graph_explorer (C++)
  ├── browser_panel.{h,cpp}        ← UI: detectar/lanzar/listar instancias
  ├── browser_control.{h,cpp}      ← Wrapper sobre cdp-cli (subprocess Go)
  └── local_files/browser_profiles/
        ├── default/                ← user-data-dir Chrome
        ├── linkedin/
        └── osint_anon/

cpp/functions/browser/             ← funciones del registry (lang: cpp)
  ├── browser_detect.cpp           ← localiza chrome.exe / brave.exe / ...
  └── browser_launch.cpp           ← spawn con flags --remote-debugging-port + --user-data-dir

functions/browser/  (Go, ya en 0029)
  ├── chrome_launch.go             ← reusable
  ├── cdp_connect.go
  ├── cdp_navigate.go
  ├── cdp_get_html.go
  ├── cdp_evaluate.go
  └── cdp_screenshot.go

apps/graph_explorer/cdp-cli/       ← binario Go que envuelve las funciones CDP
                                     y expone subcomandos invocables desde C++

Flujo lanzar browser

  1. Usuario abre panel Browsers → selecciona profile (default o crea uno nuevo) → click Launch.
  2. browser_detect localiza el binario (busca registry de Windows / ~/.local/share/applications / where chrome.exe).
  3. browser_launch hace spawn:
    chrome.exe \
      --remote-debugging-port=9222 \
      --user-data-dir=<app>/local_files/browser_profiles/<profile> \
      --no-first-run \
      --no-default-browser-check \
      about:blank
    
  4. Puerto se asigna dinamicamente desde un pool (9222..9322) para soportar varios profiles en paralelo.
  5. App registra la instancia en una tabla browser_sessions de graph_explorer.db: {profile, pid, port, browser_path, started_at}.

Flujo enricher con CDP

  1. Enricher (Python o Go) recibe el profile como param.
  2. Llama a cdp-cli con el subcomando que necesite:
    cdp-cli get-html --profile linkedin --url https://...
    cdp-cli screenshot --profile default --url https://... --out cache/x.png
    cdp-cli evaluate --profile osint --js "document.title"
    
  3. cdp-cli localiza el puerto del profile (lee browser_sessions), abre websocket CDP, navega, espera Page.loadEventFired, devuelve.
  4. Si el profile no esta vivo, cdp-cli lanza el browser primero (mismo path que el panel, pero headless: opcional con --headless=new).

Coordinacion con la extension (issue 0014)

La extension del usuario (Firefox/Chrome) NO usa CDP — usa el endpoint HTTP POST /ingest/text|/ingest/url (issue 0012). Es el camino humano: "estoy leyendo esto, mandalo al grafo".

CDP es el camino programatico: "el app necesita el HTML post-JS de esta URL para extraer entidades". Los dos son complementarios y comparten el mismo profile (cookies/login compartidos):

  • Usuario hace login en LinkedIn manualmente (extension activa).
  • Enrichers posteriores con profile=linkedin heredan la sesion.

Panel UI: Browsers

Este panel muestra runtime (instancias vivas, puertos, PIDs). La gestion logica de profiles (metadata, templates, seleccion, project defaults, clone/rename/export) vive en el panel Profiles del issue 0040. Aqui solo aparece lo que esta corriendo ahora.

Layout en un panel ImGui (toggle via cfg.panels):

┌─ Browsers ─────────────────────────────────────────┐
│ Detected: Chrome 130, Edge 132, Brave (not found)  │
│                                                     │
│ Profiles                                            │
│ ┌─────────────────────────────────────────────────┐ │
│ │ default     Chrome  port 9222  ●  [open] [stop]│ │
│ │ linkedin    Chrome  port 9223  ●  [open] [stop]│ │
│ │ osint_anon  Edge    -      ○  [launch]         │ │
│ └─────────────────────────────────────────────────┘ │
│                                                     │
│ [+ New profile]   [Open profiles folder]            │
└─────────────────────────────────────────────────────┘
  • ● = vivo, ○ = parado.
  • Click en filas = expande con stats (pages abiertas, cookies count, ultima URL — todo via CDP Target.getTargets + Network.getCookies).
  • Doble click en profile abre el browser con ese profile (sin headless).
  • Boton "Console" abre chrome://inspect/#devices para debug.

Plan de implementacion

Fase Entregable
0038a cpp/functions/browser/browser_detect.cpp — localiza binarios chromium-based en el sistema. Tests con which/registry de Windows.
0038b cpp/functions/browser/browser_launch.cpp — spawn con flags. Stub de kill_pid para parar.
0038c apps/graph_explorer/cdp-cli/ — binario Go que envuelve chrome_launch_go_browser, cdp_* (issue 0029). Subcomandos: get-html, screenshot, evaluate, cookies.
0038d browser_panel.{h,cpp} — panel ImGui con detect/launch/list. Tabla browser_sessions en graph_explorer.db.
0038e enrichers/fetch_webpage_browser/ — port del enricher para invocar cdp-cli get-html.
0038f enrichers/fetch_screenshot/ — invoca cdp-cli screenshot, guarda en cache/<sha>.png, escribe screenshot_path en metadata.
0038g Auto-start del browser cuando un enricher pide un profile que no esta vivo.
0038h Tests E2E con un Chrome local y httpbin.org (gateado por env var, no en CI por defecto).

Riesgos y mitigaciones

Riesgo Mitigacion
No hay browser instalado Mensaje claro en el panel + link a chrome.com/edge. App sigue funcional sin browser para enrichers no-CDP.
Varias instancias del mismo profile Chrome bloquea automaticamente el user-data-dir. Capturar el error de spawn y mostrar "profile already in use, port=X".
Puerto CDP ocupado Pool dinamico 9222..9322 con probe tcp.Dial antes de spawnear.
El usuario cierra el browser Heartbeat cada 5s desde el panel (Target.getTargets). Si falla N veces, marcar como dead.
Updates de Chrome rompen flags Ninguno de los flags usados es experimental. --remote-debugging-port lleva 10+ anos.
CDP version skew Usar el subset estable: Page.*, Runtime.evaluate, Network.getCookies. Evitar APIs experimentales.

Definicion de hecho

  • Desde el panel Browsers, click en Launch default abre Chrome con local_files/browser_profiles/default/ como user-data-dir y queda registrado en browser_sessions con su puerto.
  • cdp-cli get-html --profile default --url https://example.com devuelve el DOM post-JS por stdout en menos de 5s.
  • Un enricher fetch_webpage_browser llamado con profile=linkedin lanza el browser si no esta vivo y devuelve el HTML autenticado.
  • La extension de issue 0014 sigue funcionando en paralelo sin interferir (usan el mismo profile pero por mecanismos distintos).
  • Cerrar el app NO mata los browsers lanzados — son procesos independientes, persisten hasta que el usuario los cierra. (Decision consciente: el usuario quiere seguir navegando aunque el app se reinicie.)

Fuera de alcance

  • Firefox / Safari / no-chromium — descartados (Firefox tiene su propio protocolo, demasiado divergente). Solo navegadores chromium-based.
  • Browser headless en CI — los tests E2E que requieran browser quedan fuera del CI por defecto.
  • Browser as a service (lanzar en VPS y conectar remoto) — fuera de v1.
  • Cliente CDP nativo en C++ para streaming (Page.startScreencast, eventos Network.* en vivo, mirror de pestaña en panel ImGui). Reservado para un issue futuro cuando aparezca el caso de uso. La arquitectura Go elegida aqui no lo bloquea — coexistirian contra el mismo --remote-debugging-port (CDP soporta multiples clientes).