Merge quick/browser-cdp-issues — issues 0038, 0039, 0040 (browser externo + CDP + profiles)
This commit is contained in:
@@ -0,0 +1,241 @@
|
|||||||
|
---
|
||||||
|
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).
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
id: 0039
|
||||||
|
title: Gestor de cookies y sesiones por profile
|
||||||
|
status: pending
|
||||||
|
priority: high
|
||||||
|
created: 2026-05-04
|
||||||
|
depends_on: [0038]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto y objetivo
|
||||||
|
|
||||||
|
Una vez que `0038` deja al app lanzar browsers externos con
|
||||||
|
`--user-data-dir` por profile, las cookies y el `localStorage` quedan
|
||||||
|
persistidas automaticamente por Chrome/Edge en disco. Este issue cubre
|
||||||
|
la **gestion explicita** de esas sesiones desde graph_explorer:
|
||||||
|
visualizarlas, exportarlas, importarlas, limpiarlas y monitorizar cuando
|
||||||
|
caducan.
|
||||||
|
|
||||||
|
Sin esto, los profiles son cajas negras que solo se manipulan abriendo
|
||||||
|
el browser. Un OSINT serio necesita ver "que sesiones tengo activas, en
|
||||||
|
que dominios, cuando expiran, que hacer si alguna se rompe".
|
||||||
|
|
||||||
|
## Alcance
|
||||||
|
|
||||||
|
### 1. Inventario de sesiones (CDP read-only)
|
||||||
|
|
||||||
|
Panel **Sessions** dentro o junto al panel **Browsers**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ Sessions in profile: linkedin ─────────────────────────┐
|
||||||
|
│ Domain Cookies Auth? Expires │
|
||||||
|
│ linkedin.com 42 Yes 2026-08-12 │
|
||||||
|
│ www.linkedin.com 18 Yes session │
|
||||||
|
│ static.licdn.com 6 No 2027-01-01 │
|
||||||
|
│ google-analytics.com 3 No 2026-11-01 │
|
||||||
|
│ │
|
||||||
|
│ [Export profile] [Import .json] [Clear domain] │
|
||||||
|
│ [Clear all cookies] [Open browser] │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Datos via `Network.getAllCookies` (CDP). Detectar "Auth?" heuristico:
|
||||||
|
cookie con flag `httpOnly=true` o nombre matching
|
||||||
|
`/sess|auth|token|sid|li_at|c_user/i`.
|
||||||
|
|
||||||
|
### 2. Export / import de profiles
|
||||||
|
|
||||||
|
- **Export profile** → `<app>/local_files/exports/<profile>-<date>.json`
|
||||||
|
con TODAS las cookies + un manifest (`browser`, `version`,
|
||||||
|
`created_at`, `domains`).
|
||||||
|
- **Import .json** → escribe via `Network.setCookie` sobre un profile
|
||||||
|
vivo. Si el profile no esta vivo, lanzarlo headless solo para inyectar
|
||||||
|
y cerrarlo.
|
||||||
|
- Formato JSON compatible con la extension EditThisCookie (estandar de
|
||||||
|
facto), para que el usuario pueda importar/exportar fuera del app.
|
||||||
|
|
||||||
|
### 3. Limpieza selectiva
|
||||||
|
|
||||||
|
| Accion | Implementacion CDP |
|
||||||
|
|---|---|
|
||||||
|
| Clear all cookies | `Network.clearBrowserCookies` |
|
||||||
|
| Clear domain | iterar `getAllCookies` + `deleteCookies` filtrado |
|
||||||
|
| Clear cookie | `Network.deleteCookies` con name+domain |
|
||||||
|
| Clear localStorage de un origin | `Storage.clearDataForOrigin` |
|
||||||
|
|
||||||
|
Antes de cualquier "clear all" → confirmacion modal con el numero de
|
||||||
|
cookies que se van a perder.
|
||||||
|
|
||||||
|
### 4. Health check de sesiones autenticadas
|
||||||
|
|
||||||
|
Por profile, opcional, definir en `<profile>/.fn_session.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth_check:
|
||||||
|
- name: linkedin
|
||||||
|
url: https://www.linkedin.com/feed/
|
||||||
|
success_selector: "main[role='main']"
|
||||||
|
redirect_to_login_means_failed: true
|
||||||
|
- name: google
|
||||||
|
url: https://myaccount.google.com
|
||||||
|
success_selector: "[data-email]"
|
||||||
|
```
|
||||||
|
|
||||||
|
El app puede lanzar un check por sesion (CDP navigate + check selector)
|
||||||
|
y mostrar:
|
||||||
|
|
||||||
|
```
|
||||||
|
linkedin ✓ Authenticated (last check: 2 min ago)
|
||||||
|
google ✗ Login expired (redirected to /accounts/signin)
|
||||||
|
twitter ? Never checked [check now]
|
||||||
|
```
|
||||||
|
|
||||||
|
Health checks NO son automaticos por defecto — requieren click manual
|
||||||
|
para no quemar sesiones con polls innecesarios. Opcion de "check on
|
||||||
|
launch" por profile.
|
||||||
|
|
||||||
|
### 5. Lock / busy state
|
||||||
|
|
||||||
|
Cuando un enricher esta usando un profile (CDP busy), bloquear acciones
|
||||||
|
destructivas (clear, import) en el panel — solo lectura permitida.
|
||||||
|
Visual: candado al lado del profile + tooltip "in use by enricher
|
||||||
|
fetch_webpage_browser".
|
||||||
|
|
||||||
|
Lock implementado en `graph_explorer.db` tabla `browser_locks(profile,
|
||||||
|
holder, acquired_at)`. TTL 60s por si el holder muere sin liberar.
|
||||||
|
|
||||||
|
## Schema en `graph_explorer.db`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE browser_profiles (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
browser_path TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
notes TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE browser_locks (
|
||||||
|
profile TEXT PRIMARY KEY REFERENCES browser_profiles(name),
|
||||||
|
holder TEXT, -- enricher_id, panel, etc.
|
||||||
|
acquired_at TIMESTAMP,
|
||||||
|
expires_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE browser_session_checks (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
profile TEXT REFERENCES browser_profiles(name),
|
||||||
|
check_name TEXT, -- 'linkedin', 'google', ...
|
||||||
|
status TEXT, -- 'authenticated' | 'expired' | 'error'
|
||||||
|
detail TEXT,
|
||||||
|
checked_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan de implementacion
|
||||||
|
|
||||||
|
| Fase | Entregable |
|
||||||
|
|---|---|
|
||||||
|
| 0039a | `cdp-cli cookies list --profile X` — JSON con todas las cookies por dominio. Lectura via `Network.getAllCookies`. |
|
||||||
|
| 0039b | `cdp-cli cookies export --profile X --out file.json` — formato EditThisCookie. |
|
||||||
|
| 0039c | `cdp-cli cookies import --profile X --in file.json` — `Network.setCookie` masivo. |
|
||||||
|
| 0039d | `cdp-cli cookies clear --profile X [--domain Y]` — clear selectivo. |
|
||||||
|
| 0039e | Panel **Sessions** en C++: tabla agregada por dominio, botones de export/import/clear. |
|
||||||
|
| 0039f | `cdp-cli session-check --profile X --config <file>.yaml` — health check con selectores. |
|
||||||
|
| 0039g | UI de health check en panel Sessions: status por sesion, boton "check now". |
|
||||||
|
| 0039h | Locks: implementar `browser_locks` + UI para mostrar profile busy. |
|
||||||
|
| 0039i | Migrar profiles preexistentes (creados manualmente) — boton "Register existing folder". |
|
||||||
|
|
||||||
|
## Riesgos y mitigaciones
|
||||||
|
|
||||||
|
| Riesgo | Mitigacion |
|
||||||
|
|---|---|
|
||||||
|
| Importar cookies sobreescribe datos validos | Dialogo de import con preview + opcion "merge" vs "replace". |
|
||||||
|
| Export con cookies de auth = secret en disco | `local_files/exports/` con permisos 0600 en POSIX. Warning explicito al exportar. Documentar en panel. |
|
||||||
|
| Health check quema rate limit del sitio | Caching: si ultimo check < 5 min, no re-checkear sin force. |
|
||||||
|
| Cookies httpOnly no se ven via JS pero si via CDP | Documentar: el panel muestra MAS cookies que las visibles desde DevTools de la pagina. |
|
||||||
|
| Diferencias de schema entre Chrome / Edge / Brave | CDP es estandar — mismo subset funciona en todos. Tests por browser en CI manual. |
|
||||||
|
|
||||||
|
## Definicion de hecho
|
||||||
|
|
||||||
|
- En el panel **Sessions** del profile `linkedin`, veo 42 cookies
|
||||||
|
agrupadas por dominio, con flag de auth detectado y fecha de
|
||||||
|
expiracion.
|
||||||
|
- Click en **Export profile** genera un JSON valido que la extension
|
||||||
|
EditThisCookie puede importar en otro browser.
|
||||||
|
- Click en **Clear domain: google-analytics.com** elimina solo esas 3
|
||||||
|
cookies sin tocar las demas.
|
||||||
|
- Configurando un `auth_check` para LinkedIn y haciendo click en
|
||||||
|
**Check now**, el app lanza CDP, navega, evalua el selector y muestra
|
||||||
|
`✓ Authenticated` en menos de 4s.
|
||||||
|
- Un enricher corriendo sobre `linkedin` bloquea las acciones
|
||||||
|
destructivas del panel (boton "Clear all" deshabilitado con tooltip).
|
||||||
|
- Cerrar manualmente el browser y volver a lanzarlo desde el panel
|
||||||
|
conserva todas las cookies del profile (verificado por el inventario
|
||||||
|
pre/post).
|
||||||
|
|
||||||
|
## Fuera de alcance
|
||||||
|
|
||||||
|
- Cifrado at-rest del `user-data-dir` — Chrome ya cifra los secrets con
|
||||||
|
DPAPI (Windows) / Keychain (Mac). En Linux Chrome usa cifrado debil
|
||||||
|
por defecto, pero esta fuera de nuestro control.
|
||||||
|
- Sync de profiles entre PCs — los `local_files/browser_profiles/`
|
||||||
|
estan gitignorados a proposito (secrets). Si en el futuro hace falta,
|
||||||
|
un export manual + import en el otro PC es la via.
|
||||||
|
- Auto-renovacion de tokens (cuando un sitio refresca cookies via
|
||||||
|
refresh_token) — fuera de v1, demasiado especifico por sitio.
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
---
|
||||||
|
id: 0040
|
||||||
|
title: Gestion de multiples profiles de browser como concepto de primera clase
|
||||||
|
status: pending
|
||||||
|
priority: high
|
||||||
|
created: 2026-05-04
|
||||||
|
depends_on: [0038]
|
||||||
|
related: [0039]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto y objetivo
|
||||||
|
|
||||||
|
`0038` ya soporta tecnicamente N profiles en paralelo (cada uno con
|
||||||
|
`--user-data-dir` propio y puerto CDP propio). `0039` gestiona las
|
||||||
|
cookies dentro de cada profile. Falta la **capa de gestion humana**:
|
||||||
|
que cada profile sea un "usuario" identificable con metadata, que el
|
||||||
|
agente y el humano puedan elegir cual usar en cada momento, y que
|
||||||
|
existan templates para los casos tipicos del flujo OSINT.
|
||||||
|
|
||||||
|
Caso real: el usuario tiene en paralelo
|
||||||
|
- `personal` — su cuenta real de LinkedIn / Gmail / X.
|
||||||
|
- `osint_anon` — sin login, user-agent neutro, sin extensiones, para
|
||||||
|
scraping no atribuido.
|
||||||
|
- `corp_aurgi` — cuenta corporativa con SSO.
|
||||||
|
- `linkedin_alt` — cuenta secundaria para busquedas masivas.
|
||||||
|
- `investigation_<caso>` — profile temporal por investigacion concreta.
|
||||||
|
|
||||||
|
Hoy no hay forma de distinguir uno de otro mas alla del nombre de
|
||||||
|
carpeta. Este issue convierte el "profile" en una entidad propia con
|
||||||
|
metadata, ciclo de vida y selector unificado.
|
||||||
|
|
||||||
|
## Modelo de profile
|
||||||
|
|
||||||
|
Cada profile es una fila en `browser_profiles` (ya creada en `0039`)
|
||||||
|
ampliada con metadata operativa:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE browser_profiles (
|
||||||
|
name TEXT PRIMARY KEY, -- 'personal', 'osint_anon', ...
|
||||||
|
display_name TEXT, -- 'Personal — Lucas'
|
||||||
|
color TEXT, -- '#7B61FF' para badge en UI
|
||||||
|
icon TEXT, -- nombre Tabler: 'user', 'mask', 'building'
|
||||||
|
browser_preference TEXT, -- 'chrome' | 'edge' | 'brave' | 'auto'
|
||||||
|
default_user_agent TEXT, -- override; null = el del browser
|
||||||
|
default_headless INTEGER DEFAULT 0, -- bool
|
||||||
|
proxy TEXT, -- 'http://user:pass@host:port' o null
|
||||||
|
project_id TEXT, -- FK opcional a projects/*/project.md
|
||||||
|
template TEXT, -- 'anon' | 'authenticated' | 'work' | null
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_used_at TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
`name` sigue siendo el nombre de carpeta (`local_files/browser_profiles/<name>/`).
|
||||||
|
El resto es metadata pura — el directorio fisico no cambia.
|
||||||
|
|
||||||
|
## UX: panel **Profiles**
|
||||||
|
|
||||||
|
Pestaña dedicada (separada del panel **Browsers** del `0038`, que sigue
|
||||||
|
mostrando solo runtime de instancias vivas):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ Profiles ────────────────────────────────────────────────────────────┐
|
||||||
|
│ [+ New] [Clone] [Import .json] [Filter: all|active|by-project ▼] │
|
||||||
|
│ │
|
||||||
|
│ ● personal 👤 Chrome Personal — cuenta real [edit] │
|
||||||
|
│ ● osint_anon 🎭 Chrome Anon scraping (UA neutro) [edit] │
|
||||||
|
│ ○ corp_aurgi 🏢 Edge SSO Aurgi [edit] │
|
||||||
|
│ ○ linkedin_alt 👥 Chrome Cuenta secundaria LI [edit] │
|
||||||
|
│ ○ inv_caso_2026_03 🔍 Chrome Investigacion <proyecto X> [edit] │
|
||||||
|
│ │
|
||||||
|
│ Selected: personal │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Browser: Chrome 130 Display name: Personal — Lucas │ │
|
||||||
|
│ │ UA: (default) Proxy: - │ │
|
||||||
|
│ │ Project: (global) Template: authenticated │ │
|
||||||
|
│ │ Folder: local_files/browser_profiles/personal/ (124 MB) │ │
|
||||||
|
│ │ Last used: 2 hours ago Cookies: 312 in 47 domains │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Launch] [Launch headless] [Sessions] [Cookies] [Delete] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Color + icon dan distincion visual rapida en cualquier lugar del app
|
||||||
|
donde aparezca el profile (badge en Toolbar, tag en executions, ...).
|
||||||
|
- ● vivo / ○ parado (estado runtime, leido del panel `Browsers`).
|
||||||
|
- "Sessions" abre el panel del `0039` filtrado por este profile.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
Botones de **[+ New]** ofrecen 4 templates con defaults distintos:
|
||||||
|
|
||||||
|
| Template | UA default | Headless | Notas tipicas |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `anon` | `Mozilla/5.0 ... Chrome/130` (neutro, sin client hints) | true | scraping sin atribucion, sin login |
|
||||||
|
| `authenticated` | UA real del browser | false | tu cuenta real, requiere login interactivo |
|
||||||
|
| `work` | UA real | false | cuenta corporativa con SSO |
|
||||||
|
| `investigation` | UA neutro | false | profile efimero por caso, ligado a un project |
|
||||||
|
|
||||||
|
El template solo prerellena los campos al crear — despues se editan.
|
||||||
|
|
||||||
|
## Seleccion de profile al lanzar enricher
|
||||||
|
|
||||||
|
Cuando un enricher CDP se invoca (desde Echo, jobs queue o boton manual)
|
||||||
|
necesita decidir el profile. Reglas en orden:
|
||||||
|
|
||||||
|
1. **Param explicito** `browser_profile=<name>` → usa ese.
|
||||||
|
2. **Project default** — si la entidad/operacion pertenece a un project
|
||||||
|
y existe un profile con `project_id=<proj>` marcado como default →
|
||||||
|
usa ese.
|
||||||
|
3. **Template hint** — si el enricher tiene `prefers_template:
|
||||||
|
authenticated` (ej. `fetch_webpage_browser` para LinkedIn) → propon
|
||||||
|
los profiles con ese template ordenados por `last_used_at`.
|
||||||
|
4. **Fallback** → `default` (siempre existe, template `anon`).
|
||||||
|
|
||||||
|
Si la regla 1 no se cumple y hay ambiguedad, Echo pregunta:
|
||||||
|
> "Para enriquecer este perfil de LinkedIn necesito una sesion. Tengo
|
||||||
|
> `personal` (auth, last used 2h ago), `linkedin_alt` (auth, 3 dias),
|
||||||
|
> `osint_anon` (sin auth). ¿Cual uso?"
|
||||||
|
|
||||||
|
En la UI, cualquier dialogo que dispare un enricher CDP muestra un
|
||||||
|
**dropdown de profile** con el sugerido pre-seleccionado.
|
||||||
|
|
||||||
|
## Operaciones por profile
|
||||||
|
|
||||||
|
| Accion | Implementacion |
|
||||||
|
|---|---|
|
||||||
|
| **Create** | Crea fila + carpeta vacia. No lanza browser. |
|
||||||
|
| **Clone** | `cp -r` carpeta + nueva fila. Util para "voy a probar algo, hago clone de mi profile real". |
|
||||||
|
| **Rename** | Update fila + `mv` carpeta + actualizar `browser_locks` y referencias en jobs queue. |
|
||||||
|
| **Delete** | Confirm modal. Borra fila + carpeta. Falla si profile esta vivo. |
|
||||||
|
| **Export full** | Tar de la carpeta + manifest. Util para llevar profile a otro PC (con warning de secrets). |
|
||||||
|
| **Import full** | Untar + registrar fila. |
|
||||||
|
| **Set as project default** | Marca el profile como default para enrichers que operen sobre entidades del proyecto. |
|
||||||
|
|
||||||
|
Las operaciones de cookies/sesiones (`0039`) siguen siendo otro panel
|
||||||
|
y otro nivel.
|
||||||
|
|
||||||
|
## Profile como tag en `executions`
|
||||||
|
|
||||||
|
Cada `execution` de un enricher CDP guarda el `browser_profile` usado
|
||||||
|
en sus `metrics`. Permite consultas tipo:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Cuantas paginas LinkedIn enriquecio cada profile?
|
||||||
|
SELECT json_extract(metrics, '$.browser_profile') as profile, COUNT(*)
|
||||||
|
FROM executions
|
||||||
|
WHERE function_id LIKE 'fetch_webpage_browser%'
|
||||||
|
AND json_extract(metrics, '$.url') LIKE '%linkedin.com%'
|
||||||
|
GROUP BY profile;
|
||||||
|
|
||||||
|
-- Profiles que han fallado mas en la ultima semana
|
||||||
|
SELECT profile, SUM(status='failure') as failures
|
||||||
|
FROM executions
|
||||||
|
WHERE created_at > date('now', '-7 days')
|
||||||
|
GROUP BY profile
|
||||||
|
ORDER BY failures DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plan de implementacion
|
||||||
|
|
||||||
|
| Fase | Entregable |
|
||||||
|
|---|---|
|
||||||
|
| 0040a | Schema de `browser_profiles` ampliado + migracion de profiles preexistentes (los del `0038` quedan como filas con metadata vacia). |
|
||||||
|
| 0040b | Panel **Profiles** read-only: lista + detalle. Lee carpetas existentes, muestra tamaño en disco, last_used_at. |
|
||||||
|
| 0040c | CRUD basico: New (con templates), Edit, Delete, Rename, Clone. |
|
||||||
|
| 0040d | Selector de profile en cualquier dialogo de enricher CDP — componente reusable `ProfilePicker` que aplica las reglas de seleccion de §"Seleccion de profile al lanzar enricher". |
|
||||||
|
| 0040e | Project default — UI para marcar profile X como default del project Y. Rule engine en `cdp-cli` lee este default. |
|
||||||
|
| 0040f | Tag `browser_profile` en `executions.metrics` automatico desde `cdp-cli`. |
|
||||||
|
| 0040g | Export/import full de profile (tar + manifest). Al importar, dialog para mapear si ya existe el nombre. |
|
||||||
|
| 0040h | Echo gana herramienta MCP `browser_profile_pick` para preguntar al usuario cuando hay ambiguedad. |
|
||||||
|
|
||||||
|
## Riesgos y mitigaciones
|
||||||
|
|
||||||
|
| Riesgo | Mitigacion |
|
||||||
|
|---|---|
|
||||||
|
| Borrar profile destruye 1 GB de cache | Confirm modal con tamaño. Opcion "delete cookies only, keep cache" para casos intermedios. |
|
||||||
|
| Confundir `personal` con `linkedin_alt` y postear con la cuenta equivocada | Color + icon prominentes en cualquier UI. Echo SIEMPRE confirma profile antes de acciones de escritura (post, like, message — fuera de v1 igualmente). |
|
||||||
|
| Profile `default` se borra por accidente | Bloqueado: no se puede eliminar el profile literal `default`, solo renombrar (y se autocrea uno vacio). |
|
||||||
|
| Race condition al renombrar mientras enricher corre | El rename adquiere el `browser_lock` (de `0039`) primero — falla si esta tomado. |
|
||||||
|
| Schema drift entre profiles creados a mano y registrados | Boton **Scan folders** en panel: detecta carpetas en `browser_profiles/` sin fila en BD y propone registrarlas. |
|
||||||
|
|
||||||
|
## Definicion de hecho
|
||||||
|
|
||||||
|
- Tengo 5 profiles visibles en el panel **Profiles** con icon/color
|
||||||
|
distintos, cada uno con su browser_preference y notas.
|
||||||
|
- Lanzo un enricher `fetch_webpage_browser` desde el menu contextual de
|
||||||
|
un nodo `Url`, aparece un dropdown de profile con `default` pre-seleccionado
|
||||||
|
y puedo cambiar a `personal` antes de ejecutar.
|
||||||
|
- Marco `corp_aurgi` como default del project `<X>` → al enriquecer una
|
||||||
|
entidad de ese project, el dropdown viene pre-seleccionado en
|
||||||
|
`corp_aurgi`.
|
||||||
|
- Clono `personal` a `personal_test`, modifico cookies en el clon, y
|
||||||
|
`personal` original sigue intacto.
|
||||||
|
- Borro un profile vivo → falla con mensaje claro "profile in use,
|
||||||
|
stop the browser instance first".
|
||||||
|
- Echo me pregunta "uso `personal` o `linkedin_alt`?" cuando lanza un
|
||||||
|
scraping LinkedIn y no hay default de project.
|
||||||
|
- Una query SQL sobre `executions.metrics->browser_profile` agrupa
|
||||||
|
correctamente todos los fetches por profile usado.
|
||||||
|
|
||||||
|
## Fuera de alcance
|
||||||
|
|
||||||
|
- **Sync de profiles entre PCs** — los profiles tienen secrets (cookies
|
||||||
|
de auth). El export/import manual es la via. Sync automatico fuera
|
||||||
|
de v1.
|
||||||
|
- **Compartir profile entre apps del registry** — hoy `local_files/`
|
||||||
|
es por-app. Si en el futuro otra app necesita los mismos profiles,
|
||||||
|
se decide entonces si centralizar en `~/.fn/browser_profiles/` o
|
||||||
|
symlinks.
|
||||||
|
- **Browser non-chromium (Firefox)** — un profile Firefox tiene formato
|
||||||
|
distinto. Misma decision que `0038`: solo chromium-based.
|
||||||
|
- **Profile encryption at rest** — Chrome ya cifra secrets con DPAPI
|
||||||
|
(Win) / Keychain (Mac). Linux usa cifrado debil pero esta fuera de
|
||||||
|
nuestro control.
|
||||||
Reference in New Issue
Block a user