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