From 8733b7d1752321eee673071c7d2e5de63970cec0 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 4 May 2026 22:16:42 +0200 Subject: [PATCH] docs(issues): browser externo + CDP + multi-profile (0038, 0039, 0040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- issues/0038-browser-launch-cdp-control.md | 241 ++++++++++++++++++++++ issues/0039-cookie-session-manager.md | 185 +++++++++++++++++ issues/0040-multi-profile-management.md | 217 +++++++++++++++++++ 3 files changed, 643 insertions(+) create mode 100644 issues/0038-browser-launch-cdp-control.md create mode 100644 issues/0039-cookie-session-manager.md create mode 100644 issues/0040-multi-profile-management.md diff --git a/issues/0038-browser-launch-cdp-control.md b/issues/0038-browser-launch-cdp-control.md new file mode 100644 index 0000000..8725dc6 --- /dev/null +++ b/issues/0038-browser-launch-cdp-control.md @@ -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:`) | +| Persistencia | `--user-data-dir=/local_files/browser_profiles//` | +| **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=/local_files/browser_profiles/ \ + --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/.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). diff --git a/issues/0039-cookie-session-manager.md b/issues/0039-cookie-session-manager.md new file mode 100644 index 0000000..31d443f --- /dev/null +++ b/issues/0039-cookie-session-manager.md @@ -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** → `/local_files/exports/-.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 `/.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 .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. diff --git a/issues/0040-multi-profile-management.md b/issues/0040-multi-profile-management.md new file mode 100644 index 0000000..9968dc8 --- /dev/null +++ b/issues/0040-multi-profile-management.md @@ -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_` — 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//`). +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 [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=` → usa ese. +2. **Project default** — si la entidad/operacion pertenece a un project + y existe un profile con `project_id=` 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 `` → 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.