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:
2026-05-04 22:16:42 +02:00
parent 2a49c2b3fa
commit 8733b7d175
3 changed files with 643 additions and 0 deletions
+241
View File
@@ -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).
+185
View File
@@ -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.
+217
View File
@@ -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.