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