From 35ace544d9ea700cc613021147caa726138d2c4b Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 2 May 2026 16:10:35 +0200 Subject: [PATCH] docs(issues): roadmap fase 2 navegador + ports Go + runtime embebido MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anade siete issues que definen el camino para hacer graph_explorer distribuible como binario Windows autocontenido (sin WSL): - 0032 — browser_session enrichers via Playwright (login interactivo, cookies persistentes, fetch_webpage_browser, web_search_browser). - 0033 — dispatcher multi-lenguaje (lang: go|python|bash en manifest) + runtime Python embebido en /runtime/. 3 fases (A=dispatcher, B=runtime, C=UI badges). - 0033b — vendoring de funciones Python por enricher (_vendored/ + .vendor.lock) para que los enrichers no dependan de registry_root en runtime. - 0033c — fn check vendored: drift detection con --fix. - 0033d — fn index lee python_runtime / python_runtime_deps de app.md. - 0033e — /compile orquesta freeze + vendor + go builds. - 0034 — port de los 5 enrichers de sistema a Go. Reusa funciones Go del registry directamente (no copias). Tests pytest existentes pasan sin cambios. Co-Authored-By: Claude Opus 4.7 (1M context) --- issues/0032-browser-session-enrichers.md | 182 ++++++++++++ ...33-multilang-dispatcher-embedded-python.md | 261 ++++++++++++++++++ issues/0033b-vendor-python-functions.md | 69 +++++ issues/0033c-fn-check-vendored.md | 65 +++++ issues/0033d-indexer-python-runtime-fields.md | 63 +++++ issues/0033e-compile-skill-orchestration.md | 61 ++++ issues/0034-port-system-enrichers-to-go.md | 236 ++++++++++++++++ 7 files changed, 937 insertions(+) create mode 100644 issues/0032-browser-session-enrichers.md create mode 100644 issues/0033-multilang-dispatcher-embedded-python.md create mode 100644 issues/0033b-vendor-python-functions.md create mode 100644 issues/0033c-fn-check-vendored.md create mode 100644 issues/0033d-indexer-python-runtime-fields.md create mode 100644 issues/0033e-compile-skill-orchestration.md create mode 100644 issues/0034-port-system-enrichers-to-go.md diff --git a/issues/0032-browser-session-enrichers.md b/issues/0032-browser-session-enrichers.md new file mode 100644 index 0000000..5fc5a4c --- /dev/null +++ b/issues/0032-browser-session-enrichers.md @@ -0,0 +1,182 @@ +--- +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. diff --git a/issues/0033-multilang-dispatcher-embedded-python.md b/issues/0033-multilang-dispatcher-embedded-python.md new file mode 100644 index 0000000..bed63a9 --- /dev/null +++ b/issues/0033-multilang-dispatcher-embedded-python.md @@ -0,0 +1,261 @@ +--- +id: 0033 +title: Dispatcher multi-lenguaje + runtime Python embebido +status: pending +priority: high +created: 2026-05-02 +depends_on: [0026] +blocks: [0034] +--- + +## Contexto + +`graph_explorer` hoy depende de WSL: el `.exe` Windows invoca +`python/.venv/bin/python3` que vive dentro de WSL. Consecuencias: + +- Cada job paga arranque de WSL (~100-300 ms warm, varios segundos + cold) + cruce de paths Linux/Windows. +- Bugs recurrentes de path mixto (`projects\default\operations.db` → + `/home/.../projects\default\...`). +- La app no es portable: requiere WSL configurado, distro presente y + el `.venv` del registry montado. +- El test del path absoluto (issue 0029-fix de hoy) es un parche; el + problema de fondo es el cruce de OS. + +Objetivo: que `graph_explorer.exe` se distribuya **autocontenido** y +soporte enrichers escritos en cualquiera de los lenguajes del +registry, sin pedirle nada al sistema huésped. + +## Diseño + +### 1. Manifest extendido + +```yaml +id: fetch_webpage +name: "Fetch web page" +applies_to: [Url, Webpage] +emits: [Domain] +relations: [BELONGS_TO] +lang: python # NUEVO — go|python|bash (default: python para retrocompat) +exec: run # NUEVO — basename del binario/script (sin extension) +params: + - { name: timeout_s, type: int, default: 15 } +``` + +Reglas: +- `lang: python` → `/.py` (default `run.py`). +- `lang: go` → `/` (Linux) o `/.exe` (Windows). +- `lang: bash` → `/.sh`. +- Default si falta `lang`: `python` (retrocompat con los 5 enrichers existentes). + +### 2. EnricherSpec (C++) extendido + +```cpp +// enrichers.h +struct EnricherSpec { + std::string id; + std::string name; + std::string description; + std::vector applies_to; + std::string run_path; // path absoluto al ejecutable/script + std::string lang; // "python", "go", "bash" (NUEVO) + std::string exec_basename; // "run" por defecto (NUEVO) +}; +``` + +`enrichers_load()` resuelve `run_path` segun `lang`: +- `python`: busca `/.py`. +- `go`: busca primero `/.exe` en Windows o + `/` en Linux. Si no existe, log de warning y + el enricher queda **deshabilitado** (no aparece en el menu). +- `bash`: `/.sh`. + +### 3. Dispatcher en `jobs.cpp` + +`run_subprocess()` elige el `argv` segun `spec.lang`: + +```cpp +std::vector argv; +if (spec.lang == "go") { + argv = { spec.run_path }; // ejecutable directo +} else if (spec.lang == "bash") { + argv = { "/bin/bash", spec.run_path }; +} else { + // python (default) + argv = { python_runtime_path(), spec.run_path }; +} +``` + +`python_runtime_path()` busca en este orden: +1. `/runtime/python/python` (Linux) o `python.exe` (Windows) +2. `$FN_PYTHON` env var +3. `/python/.venv/bin/python3` (legacy) +4. `python3` del PATH (fallback) + +Retorna `""` si no encuentra ninguno; los enrichers Python quedan +deshabilitados con un warning visible en el panel. + +### 4. Runtime Python embebido + +#### 4.1 Layout + +``` +/runtime/ # ignorado por git, generado por freeze + python/ + python.exe # Windows + python3 # Linux + pythonXY.zip # stdlib comprimida + DLLs/ Lib/ ... + site-packages/ + requests/ certifi/ charset_normalizer/ ... +``` + +En Windows usamos el **embedded zip** oficial de python.org +(`python-3.12.X-embed-amd64.zip`, ~12 MB). En Linux empaquetamos +`python3` del sistema build con un venv limpio (`python -m venv`) +copiado a `runtime/python/`. + +#### 4.2 Script de freeze + +`tools/freeze_python_runtime.sh`: + +```bash +#!/usr/bin/env bash +# Genera runtime/ embebido para una app. +# +# Uso: freeze_python_runtime.sh +# = linux | windows +# +# Lee deps de /app.md frontmatter: +# python_runtime_deps: [requests, beautifulsoup4, ...] + +set -euo pipefail +APP_DIR="${1:?app_dir requerido}" +PLATFORM="${2:?platform requerido (linux|windows)}" +PY_VERSION="3.12.7" +RUNTIME_DIR="$APP_DIR/runtime/python" + +deps=$(yq '.python_runtime_deps[]' "$APP_DIR/app.md" 2>/dev/null || echo "") +mkdir -p "$RUNTIME_DIR" + +if [[ "$PLATFORM" == "windows" ]]; then + zip="python-${PY_VERSION}-embed-amd64.zip" + [[ -f "/tmp/$zip" ]] || curl -sSL -o "/tmp/$zip" \ + "https://www.python.org/ftp/python/$PY_VERSION/$zip" + unzip -oq "/tmp/$zip" -d "$RUNTIME_DIR" + # Habilitar site-packages en el embedded (viene deshabilitado). + pth="$RUNTIME_DIR/python$(echo $PY_VERSION | tr -d . | cut -c1-3)._pth" + sed -i 's|^#import site|import site|' "$pth" + # Instalar deps con pip de un Python "normal". + [[ -n "$deps" ]] && python -m pip install --target "$RUNTIME_DIR/Lib/site-packages" \ + --platform win_amd64 --only-binary=:all: --python-version "$PY_VERSION" $deps +else + python3 -m venv "$RUNTIME_DIR" --copies --without-pip + "$RUNTIME_DIR/bin/python3" -m ensurepip --upgrade + [[ -n "$deps" ]] && "$RUNTIME_DIR/bin/python3" -m pip install $deps +fi + +echo "Frozen runtime: $RUNTIME_DIR ($PLATFORM)" +``` + +#### 4.3 Hook en `app.md` + +Frontmatter de la app declara las dependencias: + +```yaml +--- +name: graph_explorer +... +python_runtime: true +python_runtime_deps: + - requests + - certifi + - urllib3 +--- +``` + +`fn index` recoge estos campos en `apps.python_runtime` y +`apps.python_runtime_deps` (JSON). El build de Windows +(`compile.sh`/`/compile`) llama al freeze script si +`python_runtime: true`. + +### 5. UI: badge de lenguaje en el panel + +En el menu contextual de enrichers y en el panel Jobs, mostrar al +lado del nombre un badge `[Go]` / `[Py]` / `[Sh]` para que el +usuario sepa cual va a ejecutar. Coste: 3-4 lineas en `views.cpp` +y `jobs.cpp`. + +## Plan de implementacion + +### Fase A — dispatcher multi-lang sin runtime embebido (1-2 sesiones) + +1. Extender `EnricherSpec` con `lang`, `exec_basename`. +2. `parse_manifest` lee `lang` (default `"python"`) y `exec` + (default `"run"`). +3. `enrichers_load` resuelve `run_path` segun lang + plataforma. Si + un enricher `lang: go` no tiene su binario compilado, queda + deshabilitado con warning. +4. `run_subprocess` ramifica el `argv` por lang. +5. Tests pytest del dispatcher: un enricher dummy en bash y otro + en python con manifests distintos, verificar que ambos lanzan. +6. **No cambiamos los 5 enrichers existentes.** Siguen `lang: python` + y siguen funcionando. Solo el dispatcher se vuelve poliglota. + +### Fase B — runtime Python embebido (1-2 sesiones) + +1. Escribir `tools/freeze_python_runtime.sh` (Linux + Windows). +2. Anadir hook al `/compile` skill: tras compilar el `.exe`, ejecutar + el freeze si `python_runtime: true`. +3. `python_runtime_path()` en `jobs.cpp`: busca `runtime/python/` + junto al exe; fallback a env var → registry venv → PATH. +4. Test: arranque limpio de `graph_explorer.exe` con runtime/ junto al + exe — invocar enricher Python sin WSL ni venv del registry. +5. Documentar en `app.md` y en `cpp_apps.md` la nueva regla: + apps con enrichers Python necesitan `python_runtime: true`. + +### Fase C — UI hints (0.5 sesion) + +1. Badge `[Go]/[Py]/[Sh]` en el menu de enrichers. +2. Panel Jobs muestra `lang` en la columna detalles. +3. Si el dispatcher detecta runtime Python ausente y hay enrichers + Python registrados, banner persistente en el panel Echo: + "runtime Python no encontrado — los enrichers Py estan + deshabilitados". + +## Tests + +- `tests/test_dispatcher.py` — manifest dummy de cada lang lanzando + un script trivial que devuelve `{"ok": 1}`. Verifica wire protocol. +- `tests/test_python_runtime_path.py` — fakea estructuras de carpeta + y verifica que la cadena de fallback resuelve en el orden correcto. +- Los 16 tests existentes deben seguir pasando (regresion zero). + +## Definicion de hecho + +- `graph_explorer.exe` lanza enrichers Python sin WSL, usando + `runtime/python/python.exe` empaquetado junto al exe. +- Los manifests aceptan `lang: go` y un binario en el dir del + enricher se ejecuta directamente. +- `freeze_python_runtime.sh` es idempotente y se llama desde + `/compile`. +- 0 fallos por path mixto Linux/Windows en jobs (test que arranca el + exe desde un cwd arbitrario en cada SO). + +## Riesgos y mitigaciones + +| Riesgo | Mitigacion | +|---|---| +| Runtime Python suma 50-100 MB | Lazy/condicional via `python_runtime: true`. Apps 100% Go no pagan. | +| Embedded Python en Windows no soporta C extensions complejas | Limitar deps a wheels puras o con binarios precompilados (requests, urllib3, beautifulsoup4 OK; lxml/numpy quedan fuera del embedded — usarlas requiere venv full). Documentar lista soportada. | +| Mantener freeze script roto | Test en CI que reconstruye el runtime y lanza un enricher dummy. | +| Versionar runtime entre PCs | `runtime/` en .gitignore; cada PC lo regenera con `freeze_python_runtime.sh`. Hash SHA256 del zip embed se cachea en `runtime/.lock`. | +| Devs sin paciencia para esperar el freeze | Cachear `/tmp/python-embed-*.zip`. Build incremental: skip freeze si `runtime/.lock` matches deps de `app.md`. | + +## Fuera de alcance + +- Ports de los 5 enrichers a Go (eso es el issue 0034). +- Runtime Node.js / Bun para enrichers TypeScript (futuro, baja prioridad). +- Runtime Go para enrichers JIT (no aplica, Go se compila AOT). +- Distribucion del `.exe` por instalador (msi/setup) — fuera. La app + se sigue distribuyendo como carpeta zippeada al Desktop. diff --git a/issues/0033b-vendor-python-functions.md b/issues/0033b-vendor-python-functions.md new file mode 100644 index 0000000..9e0ce07 --- /dev/null +++ b/issues/0033b-vendor-python-functions.md @@ -0,0 +1,69 @@ +--- +id: 0033b +title: Vendoring de funciones Python por enricher +status: pending +priority: high +created: 2026-05-02 +depends_on: [0033] +--- + +## Objetivo + +Para que el `.exe` Windows sea portable a cualquier PC, los enrichers +con `lang: python` no pueden importar de `/python/functions/` +en runtime. Cada enricher copia las funciones del registry que +declara en `uses_functions` a `/_vendored/` durante el +build, y `run.py` importa de ahi. + +## Implementacion + +### 1. Script `tools/vendor_enricher_python.sh` + +Recibe `` y ``. Lee `uses_functions` del +manifest, filtra los IDs `*_py_*`, resuelve `file_path` desde +`registry.db`, copia el `.py` a `_vendored//.py` y +crea `__init__.py` por dominio. Genera `.vendor.lock` con +` ` por linea. Idempotente: si el sha256 de +origen coincide con el del lock, no copia. + +### 2. Adaptacion de `run.py` + +Cada enricher con `lang: python` que use funciones del registry +cambia el import: + +```python +# antes +sys.path.insert(0, os.path.join(registry_root, "python", "functions")) +from cybersecurity.cybersecurity import normalize_url + +# despues +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_vendored")) +from cybersecurity.normalize_url import normalize_url +``` + +### 3. .gitignore + +Anadir `_vendored/` y `.vendor.lock` al `.gitignore` del repo (o de +cada app). Son artefactos de build, regenerables. + +### 4. Hook al build + +`/compile` (o el script de freeze del 0033) recorre +`apps//enrichers/*/manifest.yaml`. Para cada manifest con +`lang: python`, ejecuta `vendor_enricher_python.sh`. + +## Tests + +- Test unit del script bash: dado un manifest con dos `uses_functions` + Python conocidas, verificar que `_vendored/` queda con la estructura + correcta y `.vendor.lock` lista los hashes. +- Test e2e: `fetch_webpage` (lang: python) ejecuta correctamente con + registry_root vacio en el ctx tras vendoring. + +## Definicion de hecho + +- `tools/vendor_enricher_python.sh` existe y es idempotente. +- Cada enricher Python del proyecto tiene `_vendored/` poblado y + funciona sin `registry_root` en runtime. +- El binario zippeado a Desktop arranca enrichers Python sin acceso + al fn_registry. diff --git a/issues/0033c-fn-check-vendored.md b/issues/0033c-fn-check-vendored.md new file mode 100644 index 0000000..a1118e1 --- /dev/null +++ b/issues/0033c-fn-check-vendored.md @@ -0,0 +1,65 @@ +--- +id: 0033c +title: Comando `fn check vendored` — drift detection +status: pending +priority: medium +created: 2026-05-02 +depends_on: [0033b] +--- + +## Objetivo + +Subcomando del CLI `fn` para detectar drift entre los `_vendored/` +de los enrichers y la fuente del registry. Permite auditoria en CI y +diagnostico rapido cuando algo deja de funcionar tras editar una +funcion. + +## CLI + +```bash +fn check vendored # recorre todas las apps con vendoring +fn check vendored apps/graph_explorer # solo esta app +fn check vendored --fix # re-vendoriza si detecta drift +``` + +Salida esperada: + +``` +[OK] apps/graph_explorer/enrichers/fetch_webpage 3 funcs, lock OK +[DRIFT] apps/graph_explorer/enrichers/extract_links 1 func cambio + - extract_urls_py_cybersecurity (lock=abc123, current=def456) +[MISS] apps/graph_explorer/enrichers/web_search sin .vendor.lock +``` + +Exit codes: +- 0: todo OK +- 1: drift detectado +- 2: error operativo (manifest invalido, registry.db inaccesible) + +## Implementacion + +`cmd/fn/check_vendored.go`: +1. Glob `apps/*/enrichers/*/manifest.yaml` y + `projects/*/apps/*/enrichers/*/manifest.yaml`. +2. Filtrar los con `lang: python` y `uses_functions` no vacio. +3. Por cada uno: + - Si no existe `.vendor.lock` → MISS. + - Por cada linea del lock: leer `file_path` actual de registry.db, + calcular sha256, comparar. +4. Con `--fix`: invocar `vendor_enricher_python.sh` cuando se detecta + drift o miss. + +## Tests + +- Caso OK: manifest + lock + ficheros consistentes. +- Caso DRIFT: editamos un .py vendorizado, el comando lo detecta. +- Caso MISS: enricher con `lang: python` y `uses_functions` no vacio + pero sin lock. +- Caso `--fix`: tras drift, ejecutar con `--fix` deja todo OK. + +## Definicion de hecho + +- `fn check vendored` reporta el estado correctamente en los 3 + casos. +- `--fix` re-vendoriza y deja exit code 0. +- Tests Go en `cmd/fn/check_vendored_test.go`. diff --git a/issues/0033d-indexer-python-runtime-fields.md b/issues/0033d-indexer-python-runtime-fields.md new file mode 100644 index 0000000..d42aecb --- /dev/null +++ b/issues/0033d-indexer-python-runtime-fields.md @@ -0,0 +1,63 @@ +--- +id: 0033d +title: Indexer lee `python_runtime` y `python_runtime_deps` de app.md +status: pending +priority: low +created: 2026-05-02 +depends_on: [] +blocks: [0033] +--- + +## Objetivo + +Que `fn index` recoja dos campos nuevos del frontmatter `app.md` y +los almacene en `apps` (registry.db). Permite consultas +"que apps necesitan runtime Python" y enable hooks de build +condicionales en `/compile`. + +## Cambios + +### Schema (`registry/migrations` o `registry/schema.go`) + +```sql +ALTER TABLE apps ADD COLUMN python_runtime INTEGER NOT NULL DEFAULT 0; +ALTER TABLE apps ADD COLUMN python_runtime_deps TEXT NOT NULL DEFAULT '[]'; +``` + +### Parser de `app.md` + +Extender el parser de frontmatter de apps en `registry/parser.go` +para leer: + +```yaml +python_runtime: true # bool, default false +python_runtime_deps: # array, default [] + - requests + - certifi +``` + +### Frontmatter del template + +Anadir los campos al template `docs/templates/app.md`: + +```yaml +python_runtime: false +python_runtime_deps: [] +``` + +Comentar que solo aplica a apps que ejecutan enrichers Python. + +## Tests + +- Parser test: `app.md` con los dos campos → struct correcto. +- Indexer test: tras `fn index`, query `SELECT python_runtime, + python_runtime_deps FROM apps WHERE id='graph_explorer'` devuelve + los valores esperados. + +## Definicion de hecho + +- `fn index` no rompe en apps sin los campos (default 0/`[]`). +- `graph_explorer/app.md` actualizado con `python_runtime: true` y + deps reales (`requests` minimo). +- Query `SELECT id FROM apps WHERE python_runtime = 1` lista las + apps que necesitan runtime embebido. diff --git a/issues/0033e-compile-skill-orchestration.md b/issues/0033e-compile-skill-orchestration.md new file mode 100644 index 0000000..bc379dd --- /dev/null +++ b/issues/0033e-compile-skill-orchestration.md @@ -0,0 +1,61 @@ +--- +id: 0033e +title: Skill `/compile` orquesta freeze + vendor + go builds +status: pending +priority: medium +created: 2026-05-02 +depends_on: [0033, 0033b, 0033d] +--- + +## Objetivo + +Tras compilar el `.exe` Windows de un app, el skill `/compile` debe +preparar todo lo necesario para que el zip distribuible sea +self-contained. + +## Pasos del skill (post-build) + +``` +1. Compilar .exe Windows (ya lo hace). +2. Si app.python_runtime == 1: + a. tools/freeze_python_runtime.sh windows + b. para cada enricher con lang: python: + tools/vendor_enricher_python.sh $(pwd) +3. Para cada enricher con lang: go: + bash /build.sh (cross-compile linux+windows) +4. Copiar arbol final a /mnt/c/Users/lucas/Desktop/apps// + incluyendo: .exe, runtime/, enrichers/ con run.exe + _vendored/. +5. Verificar layout: smoke check que .exe + runtime/python/python.exe + + enrichers/*/run.exe existen. +``` + +## Cambios + +Editar el skill `~/.claude/plugins//compile/skill.md` (o +donde viva el `/compile`) para anadir los pasos 2-5. Si el skill ya +copia archivos, extender la lista; si no, anadir el bloque entero. + +## Test manual + +Tras un `/compile` limpio: + +```bash +ls /mnt/c/Users/lucas/Desktop/apps/graph_explorer/ +# Esperado: +# graph_explorer.exe +# runtime/python/python.exe +# enrichers/extract_domain/run.exe (Go) +# enrichers/web_search/run.py + _vendored/ (Python custom, ej.) +# ... +``` + +Y desde Windows (no WSL): doble click en `graph_explorer.exe`, +arrancar Echo, ejecutar `web_search` sobre un nodo text. Debe +funcionar sin que WSL este corriendo. + +## Definicion de hecho + +- `/compile` (o equivalente) ejecuta los 5 pasos sin intervencion + manual. +- Un PC Windows limpio (sin WSL) corre la app desde el zip + copiado a Desktop. diff --git a/issues/0034-port-system-enrichers-to-go.md b/issues/0034-port-system-enrichers-to-go.md new file mode 100644 index 0000000..a680f6b --- /dev/null +++ b/issues/0034-port-system-enrichers-to-go.md @@ -0,0 +1,236 @@ +--- +id: 0034 +title: Port de los 5 enrichers de sistema a Go +status: pending +priority: medium +created: 2026-05-02 +depends_on: [0033] +--- + +## Contexto + +Tras 0033 el dispatcher acepta `lang: go` y los binarios se +distribuyen junto al `.exe` Windows. Los 5 enrichers de sistema +(`extract_domain`, `fetch_webpage`, `extract_links`, +`extract_text_entities`, `web_search`) son hoy Python — funcionan, +estan testeados y son el flujo OSINT canonico de la app. Portarlos +a Go nos da: + +- Cero dependencia de Python para el flujo base. El runtime + embebido pasa a ser **opcional** (solo apps con enrichers + custom Python lo necesitan). +- ~10x menos arranque por job (Python cold ~120 ms, Go ~10 ms). +- Reuso directo de funciones Go testeadas del registry (extract_iocs, + extract_urls, normalize_url, html_to_markdown). Esto cierra el + bucle del registry: lo que se testea en Go se ejecuta en Go. +- Distribucion: el `.exe` + binarios `enrichers//run.exe` son + ~25 MB totales; sin runtime Python si no hace falta. + +Los tests pytest existentes **no cambian** — testean el wire +protocol (stdin JSON / stdout JSON / exit code), no la +implementacion. Cada port intercambia el binario pero deja el +contrato intacto. + +## Funciones del registry necesarias + +Los 5 enrichers comparten una decena de funciones del dominio +`cybersecurity` y `core`. Verificar que existen en Go con `fn search` +antes de portar; si falta alguna, anadirla al registry primero. + +| Funcion Python actual | Equivalente Go que hace falta | Estado | +|---|---|---| +| `cybersecurity.normalize_url` | `normalize_url_go_cybersecurity` | revisar | +| `core.html_to_markdown` | `html_to_markdown_go_core` | crear si no esta | +| `cybersecurity.extract_urls` | `extract_urls_go_cybersecurity` | revisar | +| `cybersecurity.extract_iocs` | `extract_iocs_go_cybersecurity` | crear si no esta | + +`fn check params` + `fn list -d cybersecurity --lang go` muestra que +hay y que falta. Las que falten se crean primero (issue separada +por funcion si la complejidad lo justifica, ej. `html_to_markdown` +necesita un parser HTML — `golang.org/x/net/html` resuelve). + +## Layout por enricher + +``` +enrichers// + manifest.yaml # actualizado: lang: go + main.go # entry point con main() + go.mod # dependencias propias + run # binario Linux (gitignored, generado) + run.exe # binario Windows (gitignored, generado) + build.sh # cross-compile recipe + run.py # ELIMINADO tras port + tests verde +``` + +Ejemplo de `build.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o run . +GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o run.exe . +``` + +Hook al build de la app: `cpp/CMakeLists.txt` (o `compile.sh`) corre +`build.sh` de cada enricher Go antes de copiar la app a Desktop. Idem +en linux build. + +## Estructura del `main.go` (template) + +```go +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + + // Funciones del registry — paths relativos al repo root. + "fn_registry/cybersecurity/extractiocs" + "fn_registry/cybersecurity/extracturls" +) + +type ctxIn struct { + NodeID string `json:"node_id"` + NodeName string `json:"node_name"` + NodeType string `json:"node_type"` + Metadata map[string]any `json:"metadata"` + OpsDBPath string `json:"ops_db_path"` + AppDir string `json:"app_dir"` + CacheDir string `json:"cache_dir"` + RegistryRoot string `json:"registry_root"` + Params map[string]any `json:"params"` +} + +type ctxOut struct { + EntitiesAdded int `json:"entities_added"` + RelationsAdded int `json:"relations_added"` + Error string `json:"error,omitempty"` + // ... campos especificos del enricher +} + +func progress(p float64, stage string) { + fmt.Fprintf(os.Stderr, "PROGRESS:%.2f %s\n", p, stage) +} + +func main() { + raw, _ := io.ReadAll(os.Stdin) + var in ctxIn + if err := json.Unmarshal(raw, &in); err != nil { + fmt.Fprintln(os.Stderr, "stdin not valid JSON:", err) + os.Exit(2) + } + out, code := run(in) + enc, _ := json.Marshal(out) + fmt.Println(string(enc)) + os.Exit(code) +} + +func run(in ctxIn) (ctxOut, int) { + // Logica especifica del enricher, devolviendo (resumen, exit code). +} +``` + +## Plan de port (orden y por que) + +### Fase 1 — `extract_domain` (1 sesion) + +El mas simple: regex puro, sin red, sin parser HTML. Sirve de +**referencia canonica** para los demas. Confirma toda la cadena de +build + dispatcher + tests pytest sin tocar dependencias externas. + +### Fase 2 — `web_search` (1-2 sesiones) + +Recien escrito en Python, todavia caliente. Portar ahora minimiza el +trabajo de mantenerlo en dos sitios. Dependencias: `net/http` (stdlib), +parser HTML con `golang.org/x/net/html`. + +### Fase 3 — `extract_links` (1 sesion) + +Lee markdown del cache, extract_urls. Si `extract_urls_go_cybersecurity` +existe, port directo; si no, escribir la funcion antes (regex de URLs). + +### Fase 4 — `extract_text_entities` (1-2 sesiones) + +Si `extract_iocs_go_cybersecurity` no existe, crear primero como +funcion del registry — es el plato fuerte del port. + +### Fase 5 — `fetch_webpage` (2 sesiones) + +El mas complejo: HTTP con redirects, decode de encoding, parse HTML +a markdown, escritura de cache. Lo dejamos para el final cuando ya +tenemos las primitivas Go probadas. + +## Tests + +**Cero cambios necesarios** en los tests pytest si el dispatcher +detecta correctamente el binario Go. El stub `tests/_stubs/requests.py` +deja de aplicar para los enrichers Go — en su lugar se inyecta un +servidor HTTP local con `httptest.Server` desde un test Go nativo +**adicional** (no sustituto): + +```go +// enrichers/web_search/main_test.go +func TestWebSearchParsesDDGFixture(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w, r) { + body, _ := os.ReadFile("../../tests/fixtures/ddg_results.html") + w.Write(body) + })) + defer srv.Close() + // ... ctx con DDGEndpoint sobreescrito a srv.URL ... +} +``` + +Los tests pytest se mantienen como **integration tests del wire +protocol** y los tests Go cubren la logica unitaria. Total post-port: + +- 16 tests pytest (sin cambios) — wire protocol. +- ~20 tests Go nuevos — logica de cada enricher. + +## Limpieza tras port + +Por enricher, una vez los 16+N tests estan verde: +1. Borrar `run.py`. +2. Actualizar `manifest.yaml`: `lang: go`. +3. Actualizar `app.md` `python_runtime_deps` quitando lo que ya no se + use (si extract_text_entities ya no necesita `requests`, fuera). + +Cuando los 5 esten portados, decidir: +- Si `python_runtime: true` sigue siendo util → mantener (custom + enrichers, prototipado). +- Si nadie escribe Python custom → marcar `python_runtime: false` y + el `.exe` deja de embeber Python por completo. Re-habilitable con + un solo flag. + +## Definicion de hecho + +- 5 enrichers con `lang: go` y binarios precompilados para + Linux + Windows en el build pipeline. +- 16 tests pytest pasan contra los binarios Go (mismo wire + protocol). +- Tests Go nativos cubren parsing/regex/IO de cada enricher. +- `graph_explorer.exe` distribuido a Desktop sin runtime Python + ejecuta el flujo OSINT completo (search → fetch → extract). +- `python_runtime: true` queda como flag opcional, no obligatorio. + +## Riesgos y mitigaciones + +| Riesgo | Mitigacion | +|---|---| +| Falta `extract_iocs` o `html_to_markdown` en Go | Issue dependiente que las crea primero. Marcar este 0034 como bloqueado hasta que existan. | +| Diferencia de comportamiento Python vs Go (regex, normalizacion HTML) | Tests pytest comparten fixtures de input — si Go produce salida distinta a Python, falla. Iteramos hasta paridad. | +| Cross-compile de Go con cgo | Los enrichers no necesitan cgo. Build estatico simple `CGO_ENABLED=0`. | +| Mantener dos implementaciones durante el port | NO se mantienen dos. Cada enricher se porta en una rama corta, tests verde, merge, eliminacion del `run.py`. Nada de toggles. | +| Echo escribiendo enrichers nuevos durante el port | Echo escribe Python custom — eso vive feliz junto a los Go portados (gracias al dispatcher de 0033). Sin conflicto. | + +## Fuera de alcance + +- Port de enrichers de fase 2 (browser session, screenshot, login). + Esos viven en `lang: python` con Playwright porque la libreria Go + equivalente (`chromedp`) duplica esfuerzo sin ganancia clara. + Justamente el caso de uso ideal del runtime embebido. +- Reescribir el sistema de jobs en Go (issue futura si el panel C++ + se queda corto). +- Ports en otros lenguajes (Rust, Zig) — no aporta ahora.