- 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.
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 |
|
|
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:
- El usuario navega en su browser real (Chrome / Edge / Brave / Vivaldi instalado en el sistema), con su layout, sus extensiones, sus marcadores.
- graph_explorer lanza una instancia controlable del browser cuando
hace falta, con
--remote-debugging-porty--user-data-dirapuntando a un profile gestionado por el app. - El app habla con esa instancia via CDP (Chrome DevTools Protocol) para los enrichers que necesiten JS/cookies/auth.
- 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-clilo 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-cliGo 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-portque ya tiene abierto el browser lanzado desde0038. 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
- Usuario abre panel Browsers → selecciona profile (
defaulto crea uno nuevo) → click Launch. browser_detectlocaliza el binario (busca registry de Windows /~/.local/share/applications/where chrome.exe).browser_launchhace 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- Puerto se asigna dinamicamente desde un pool (9222..9322) para soportar varios profiles en paralelo.
- App registra la instancia en una tabla
browser_sessionsdegraph_explorer.db:{profile, pid, port, browser_path, started_at}.
Flujo enricher con CDP
- Enricher (Python o Go) recibe el
profilecomo param. - Llama a
cdp-clicon 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" cdp-clilocaliza el puerto del profile (leebrowser_sessions), abre websocket CDP, navega, esperaPage.loadEventFired, devuelve.- Si el profile no esta vivo,
cdp-clilanza 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=linkedinheredan 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/#devicespara 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 enbrowser_sessionscon su puerto. cdp-cli get-html --profile default --url https://example.comdevuelve el DOM post-JS por stdout en menos de 5s.- Un enricher
fetch_webpage_browserllamado conprofile=linkedinlanza 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, eventosNetwork.*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).