docs(issues): roadmap fase 2 navegador + ports Go + runtime embebido
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 <app>/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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
`<app_dir>/browser_profiles/<profile>.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 `<app_dir>/browser_profiles/<name>.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 | `<app_dir>/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.
|
||||||
@@ -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` → `<dir>/<exec>.py` (default `run.py`).
|
||||||
|
- `lang: go` → `<dir>/<exec>` (Linux) o `<dir>/<exec>.exe` (Windows).
|
||||||
|
- `lang: bash` → `<dir>/<exec>.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<std::string> 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 `<dir>/<exec_basename>.py`.
|
||||||
|
- `go`: busca primero `<dir>/<exec_basename>.exe` en Windows o
|
||||||
|
`<dir>/<exec_basename>` en Linux. Si no existe, log de warning y
|
||||||
|
el enricher queda **deshabilitado** (no aparece en el menu).
|
||||||
|
- `bash`: `<dir>/<exec_basename>.sh`.
|
||||||
|
|
||||||
|
### 3. Dispatcher en `jobs.cpp`
|
||||||
|
|
||||||
|
`run_subprocess()` elige el `argv` segun `spec.lang`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::vector<std::string> 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. `<exe_dir>/runtime/python/python` (Linux) o `python.exe` (Windows)
|
||||||
|
2. `$FN_PYTHON` env var
|
||||||
|
3. `<registry_root>/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
|
||||||
|
|
||||||
|
```
|
||||||
|
<app_dir>/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 <app_dir> <platform>
|
||||||
|
# <platform> = linux | windows
|
||||||
|
#
|
||||||
|
# Lee deps de <app_dir>/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.
|
||||||
@@ -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 `<registry_root>/python/functions/`
|
||||||
|
en runtime. Cada enricher copia las funciones del registry que
|
||||||
|
declara en `uses_functions` a `<enricher>/_vendored/` durante el
|
||||||
|
build, y `run.py` importa de ahi.
|
||||||
|
|
||||||
|
## Implementacion
|
||||||
|
|
||||||
|
### 1. Script `tools/vendor_enricher_python.sh`
|
||||||
|
|
||||||
|
Recibe `<enricher_dir>` y `<registry_root>`. Lee `uses_functions` del
|
||||||
|
manifest, filtra los IDs `*_py_*`, resuelve `file_path` desde
|
||||||
|
`registry.db`, copia el `.py` a `_vendored/<domain>/<name>.py` y
|
||||||
|
crea `__init__.py` por dominio. Genera `.vendor.lock` con
|
||||||
|
`<id> <sha256> <src_path>` 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/<app>/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.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
@@ -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 <app_dir> windows
|
||||||
|
b. para cada enricher con lang: python:
|
||||||
|
tools/vendor_enricher_python.sh <enricher_dir> $(pwd)
|
||||||
|
3. Para cada enricher con lang: go:
|
||||||
|
bash <enricher_dir>/build.sh (cross-compile linux+windows)
|
||||||
|
4. Copiar arbol final a /mnt/c/Users/lucas/Desktop/apps/<app>/
|
||||||
|
incluyendo: <app>.exe, runtime/, enrichers/ con run.exe + _vendored/.
|
||||||
|
5. Verificar layout: smoke check que <app>.exe + runtime/python/python.exe
|
||||||
|
+ enrichers/*/run.exe existen.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cambios
|
||||||
|
|
||||||
|
Editar el skill `~/.claude/plugins/<plugin>/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.
|
||||||
@@ -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/<id>/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/<id>/
|
||||||
|
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.
|
||||||
Reference in New Issue
Block a user