30f6f3758f
Permite distribuir graph_explorer.exe Windows sin dependencia de WSL
ni del .venv del registry. Tambien funciona en Linux como bundle
autocontenido portable.
Cambios:
1. tools/freeze_python_runtime.sh
- Linux: copia python-build-standalone (uv) ~87 MB,
elimina marker EXTERNALLY-MANAGED, instala wheels.
- Windows: descarga python-3.12.7-embed-amd64.zip oficial
(~12 MB), habilita site-packages, instala wheels via
pip install --target --platform win_amd64.
- Idempotente via runtime/.lock con SHA256 del estado.
- Lee python_runtime_deps del frontmatter de app.md.
2. jobs.cpp::cached_python_runtime() — resolver con cadena:
1. <exe_dir>/runtime/python/{python.exe|bin/python3} (embedded)
2. $FN_PYTHON (env)
3. <registry_root>/python/.venv/bin/python3 (registry_venv)
4. python3 del PATH (system)
Loggea procedencia al iniciar jobs_init.
3. POSIX run_subprocess: usa el runtime resuelto en lugar del
path hardcodeado.
4. Windows run_subprocess: ramifica por needs_wsl. Si embedded
o env, lanza Python Windows nativo via CreateProcessW
directamente (run_path tambien Windows nativo). Solo el
legacy registry_venv sigue por wsl.exe.
5. app.md: nuevos campos python_runtime: true y
python_runtime_deps: [requests, certifi, urllib3].
6. .gitignore extendido con runtime/, projects/, _vendored/,
.vendor.lock, binarios Go de enrichers.
Tests: 26/26 verde — 16 originales + 6 dispatcher fase A + 4
nuevos del resolver fase B (con/sin embed, FN_PYTHON, idempotencia
del freeze script).
Smoke E2E manual: runtime/python/bin/python3 ejecuta web_search
con cwd /tmp y registry_root pasado en ctx, sin tocar el .venv del
registry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
288 lines
10 KiB
Markdown
288 lines
10 KiB
Markdown
---
|
|
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 (COMPLETADA)
|
|
|
|
1. ✅ Extender `EnricherSpec` con `lang`, `exec_basename`,
|
|
`disabled`, `disabled_reason`.
|
|
2. ✅ `parse_manifest` lee `lang` (default `"python"`) y `exec`
|
|
(default `"run"`).
|
|
3. ✅ `enrichers_load` resuelve `run_path` segun lang + plataforma
|
|
via `resolve_run_path`. Enrichers Go sin binario quedan
|
|
`disabled` con razon visible y se ocultan del menu.
|
|
4. ✅ `run_subprocess` (POSIX y Windows) ramifica `argv` por lang.
|
|
5. ✅ Tests pytest del dispatcher: 6 tests nuevos en
|
|
`tests/test_dispatcher_lang.py` (default lang, bash wire
|
|
protocol, regresion de enricher real, etc.). 22/22 verde
|
|
incluyendo los 16 originales.
|
|
6. ✅ Los 5 enrichers existentes funcionan sin cambios — heredan
|
|
`lang: python` por default.
|
|
|
|
Implementado en commit `fce3f97` (rama `issue/0033a-multilang-dispatcher`).
|
|
|
|
### Fase B — runtime Python embebido (COMPLETADA)
|
|
|
|
1. ✅ `tools/freeze_python_runtime.sh` con dos backends:
|
|
- Linux: copia `python-build-standalone` (uv) ~87 MB, autocontenido.
|
|
- Windows: descarga `python-3.X.Y-embed-amd64.zip` oficial,
|
|
habilita site-packages, instala wheels via `pip install
|
|
--target` cross-platform.
|
|
Idempotente via `runtime/.lock` con SHA256 de (PY_VERSION, deps,
|
|
platform). Lee `python_runtime_deps` del frontmatter de `app.md`
|
|
(override: env `PY_DEPS=...`).
|
|
2. ⏳ Hook al `/compile` skill — pendiente, parte del issue 0033e.
|
|
3. ✅ `cached_python_runtime()` en `jobs.cpp` con cadena de
|
|
fallback: `<exe_dir>/runtime/python/{python.exe|bin/python3}` →
|
|
`$FN_PYTHON` → `<registry_root>/python/.venv/bin/python3` →
|
|
`python3` del PATH. Loggea procedencia (`kind=embedded|env|
|
|
registry_venv|system`) al iniciar jobs_init.
|
|
4. ✅ Tests:
|
|
- `tests/test_python_runtime_resolver.py` — 4 tests verifican
|
|
fallback con/sin embed, override via FN_PYTHON e idempotencia
|
|
del freeze script.
|
|
- Smoke E2E manual: `runtime/python/bin/python3` ejecuta
|
|
`web_search` en cwd arbitrario, sin pasar por el venv del
|
|
registry.
|
|
5. ✅ `app.md` actualizado con `python_runtime: true` y
|
|
`python_runtime_deps: [requests, certifi, urllib3]`.
|
|
6. ✅ `.gitignore` creado: ignora `runtime/`, `_vendored/`,
|
|
`.vendor.lock`, binarios Go de enrichers.
|
|
|
|
Implementado en commits `<hash-fase-B>`. Para Windows nativo, el
|
|
spawn ahora bifurca: si el resolver es `embedded`/`env`/`system`,
|
|
lanza Python Windows nativo via CreateProcessW; si es `registry_venv`
|
|
(legacy), sigue el camino `wsl.exe` previo.
|
|
|
|
### 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.
|