--- id: 0032 title: Fase 2 — Navegador controlable con sesion persistente (cookies, login, JS) status: pending priority: high created: 2026-05-02 depends_on: [0028, 0029] --- ## Contexto y objetivo La fase 1 (enrichers `fetch_webpage`, `web_search`, `extract_links`, `extract_text_entities`, `extract_domain`) cubre el caso "sitio publico, HTML estatico, sin JS". Limitaciones reales que estamos viendo: - DDG HTML cambia el markup y hay que reparar el parser. Ademas en busquedas masivas devuelve captcha tras N requests. - Sitios SPA (LinkedIn, X, Telegram) no se pueden enriquecer porque `requests` no ejecuta JS — el HTML viene vacio. - Para investigaciones serias hacen falta sesiones autenticadas (LinkedIn, foros, Telegram web). Sin cookies persistentes, cada enricher empieza de cero y se rompe el flujo. Fase 2 introduce un **navegador controlable** que comparten todos los enrichers que necesiten JS, cookies o login. ## Decision de stack | Opcion | Pros | Contras | Veredicto | |---|---|---|---| | Playwright (Python) | Bateria incluida, captura cookies/storage, easy install via pip, multinavegador | 200 MB de Chromium descargado | **Elegida** — mejor DX para enrichers Python | | pychrome + Chrome instalado | Mas ligero | API basica, reinventar capa de helpers | descartada | | CDP en Go (issue 0029) | Reusa funciones del registry, binario unico | Mantener wrapper aparte por enricher | aplazado a v3 | | Selenium | Estandar viejo | Mas lento que Playwright, peor API moderna | descartada | **Stack final:** Playwright Python en `python/.venv`, persistencia de estado con `BrowserContext.storage_state` (cookies + localStorage) en `/browser_profiles/.json`. ## Arquitectura ### Componente compartido: `browser_session_py_browser` Funcion del registry nueva en `python/functions/browser/`. NO es un enricher — es la primitiva que usan los enrichers. ```python def open_session(profile: str, *, headless: bool = True, user_agent: str | None = None) -> BrowserSession: ... ``` `BrowserSession` expone: - `goto(url, wait="load") -> Page` - `html() -> str` - `screenshot(path, full_page=True)` - `cookies() -> list[dict]` / `set_cookies(list)` - `evaluate(js) -> any` - `close()` — persiste storage_state al disco antes de cerrar. El profile vive en `/browser_profiles/.json`. Si no existe se crea vacio. Echo y los enrichers comparten profile via param `browser_profile` (default `"default"`). ### Enrichers nuevos | id | applies_to | reemplaza/complementa | |---|---|---| | `fetch_webpage_browser` | Url, Webpage | superset de `fetch_webpage` para JS/auth | | `web_search_browser` | text, Concept | superset de `web_search` (Google/DDG con JS y sin captcha facil) | | `fetch_screenshot` | Url, Webpage | nuevo — solo evidencia visual | | `browser_login` | Account, Credential | nuevo — abre login interactivo, guarda cookies | | `extract_dom_data` | Webpage | nuevo — extrae datos via selector CSS o XPath (JSON-LD, microdata, etc.) | Todos comparten params: ```yaml - { name: browser_profile, type: string, default: "default" } - { name: headless, type: bool, default: true } - { name: timeout_s, type: int, default: 30 } ``` ### Login interactivo (`browser_login`) Modo especial: lanza Chromium **no-headless** y deja que el humano haga el login a mano (CAPTCHA, 2FA, etc.). Cuando el usuario cierra la ventana, las cookies quedan guardadas en el profile. Los enrichers posteriores que usen ese profile heredan la sesion. UX en Echo: > Usuario: "Echo, prepara una sesion para LinkedIn" > Echo: ejecuta `browser_login` con `profile=linkedin`, target=linkedin.com. > Aparece la ventana del browser. Usuario hace login. Cierra ventana. > Echo confirma: "sesion linkedin guardada, 15 cookies". ### `web_search_browser` con multiples motores ```yaml params: - { name: engine, type: string, default: "google" } # google|ddg|bing|brave - { name: limit, type: int, default: 10 } - { name: browser_profile, type: string, default: "default" } ``` Con browser real, Google deja de bloquear. Cada engine tiene su selector CSS para resultados (mantenidos en `engines.yaml` dentro del enricher). El enricher cae en orden engine → engine si uno falla: google → ddg → bing. ### Extraccion CSS/XPath (`extract_dom_data`) Para nodos `Webpage` enriquecidos con un browser, permite definir selectores en el `Type` del nodo: ```yaml # en types.yaml de un proyecto - name: LinkedInProfile selectors: full_name: "h1.profile-name" headline: "div.profile-headline" company: "[data-section=experience] li:first-child .company" ``` `extract_dom_data` lee los selectores del Type, los aplica al DOM post-JS y guarda los valores en `metadata`. Esto **es lo que conecta** el grafo con scrapers tipados sin escribir un enricher por sitio. ## Integracion con Echo Echo gana 3 tools nuevas en el MCP server (`gx-cli`): - `browser_login(profile, url)` — pide al usuario hacer login. - `browser_session_status(profile)` — lista profiles, valid/expired, cookie count, ultima url visitada. - `browser_close(profile)` — cierra la sesion y persiste. System prompt amplia el bloque WORKFLOW: > Cuando una URL devuelva HTML vacio o redirija a login, propon usar > `fetch_webpage_browser` con un profile autenticado. Si no existe > profile, propon `browser_login` antes. ## Plan de implementacion (fases) 1. **0032a** — `python/functions/browser/browser_session.py` con Playwright. Test que abre about:blank, persiste storage_state, recarga y verifica cookies. 2. **0032b** — `fetch_webpage_browser` enricher. Test contra `httpbin.org/cookies/set` para verificar persistencia. 3. **0032c** — `fetch_screenshot` (la mas simple, valida la pipeline visual end-to-end). 4. **0032d** — `web_search_browser` con google + ddg + fallback. Tests con paginas guardadas en `tests/fixtures/`. 5. **0032e** — `browser_login` con UI no-headless. Test manual. 6. **0032f** — `extract_dom_data` + extension del schema de Types con `selectors`. Test con HTML local complejo. 7. **0032g** — Tools MCP en `gx-cli` y prompt update en chat.cpp. ## Riesgos y mitigaciones | Riesgo | Mitigacion | |---|---| | Playwright pesa 200 MB | Lazy install: `pip install playwright && playwright install chromium` solo cuando se ejecuta el primer enricher browser. Documentar en app.md. | | Profiles con secretos en disco | `/browser_profiles/` en .gitignore. Documentar advertencia en `browser_login`. | | Sites detectan headless | Default user-agent realista. Bloque opcional `stealth: true` en params (usa playwright-stealth). | | Concurrencia: 2 jobs leyendo el mismo profile | Lock por profile en sqlite (`browser_locks` tabla en operations.db). Si esta tomado, esperar 30s antes de fallar. | | Tests con red real | NO hay tests con red real. Fixtures HTML guardados o servidor mock con `pytest-httpserver`. | ## Definicion de hecho - Echo puede pedir al usuario "abre LinkedIn y haz login" y a partir de ahi enriquecer perfiles. - `web_search_browser` engine=google funciona masivamente (50+ busquedas seguidas) sin captcha. - Un Webpage enriquecido con `extract_dom_data` usando un Type con `selectors` queda con todos los campos en `metadata`. - Tests pasan en CI sin red — todos los enrichers browser tienen tests con fixtures locales. ## Fuera de alcance (v3) - Reescribir los enrichers a Go con CDP (issue 0029 sigue vivo como alternativa de bajo nivel si Playwright no escala). - Captcha solving — manual via `browser_login`, nunca automatico. - Anti-bot bypass agresivo (residential proxies, fingerprint randomizacion). Pendiente hasta que se demuestre necesidad real.