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

242 lines
12 KiB
Markdown

---
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:<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).