--- id: 0038 title: Browser externo lanzable desde el app + control via CDP status: pending priority: high created: 2026-05-04 depends_on: [0014, 0029] supersedes: [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:`) | | Persistencia | `--user-data-dir=/local_files/browser_profiles//` | | **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=/local_files/browser_profiles/ \ --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/.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).