chore: auto-commit (8 archivos)

- .claude/rules/registry_calls.md
- apps/dag_engine/README.md
- apps/dag_engine/app.md
- docs/capabilities/INDEX.md
- docs/capabilities/systemd.md
- docs/execution_standard.md
- dev/proposals_e2e_checks_0121/
- docs/capabilities/backends.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 00:31:30 +02:00
parent f8efb7d177
commit eb30074792
12 changed files with 645 additions and 45 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ Cobertura por capa, no todas activas a la vez:
### Que NO se monitoriza
- Funcion Go/C++ llamada internamente por app ya compilada.
- Funcion ejecutada por systemd timer / cron / Dagu sin pasar por `fn run`.
- Funcion ejecutada por systemd timer / cron / dag_engine **step `command:`** (no `function:`) sin pasar por `fn run`. Nota: dag_engine steps con `function:` SI quedan trazados — el executor invoca `fn run <id>` y guarda `function_id` en `dag_step_results`.
- Sub-agente (`Agent` tool) — sus tools no propagan a hook del padre.
- Service de produccion recibiendo HTTP.
+10 -10
View File
@@ -1,6 +1,6 @@
# dag_engine — Guia de uso
Motor de DAGs propio (reemplazo de Dagu). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
Motor de DAGs propio del fn_registry. **Scheduler oficial** del ecosistema (issue 0007a-e + flow 0001). Backend Go + frontend web (Vite/React) + frontend C++ ImGui (`cpp/apps/dag_engine_ui`).
Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagnostico de fallos**.
@@ -11,8 +11,7 @@ Doc canonica para **anadir DAGs**, **formato YAML**, **comandos CLI**, y **diagn
| Path | Que |
|---|---|
| `apps/dag_engine/dags_migrated/` | DAGs activos servidos por `dag_engine.service` (systemd user unit). |
| `~/dagu/dags/` | Path por defecto del binario si no se pasa `--dags-dir`. Vacio tras la migracion del 2026-05-15 (ver tag `dagu_pre_removal`). |
| `~/backups/dagu_pre_removal_<fecha>.tar.gz` | Backup completo de la carpeta dagu antes de borrar. |
| `apps/dag_engine/dags_migrated/archive/` | DAGs deshabilitados (no se cargan por el scheduler). |
Por defecto el systemd unit apunta a `apps/dag_engine/dags_migrated/`. Para usar otro dir, edita `~/.config/systemd/user/dag_engine.service`:
@@ -68,7 +67,7 @@ Devuelve `{"dag":"<nombre>","run_id":"...","status":"accepted"}` y dispara el WS
## 3. Formato YAML
dag_engine es **compatible con el formato Dagu**. Los YAMLs heredados de `~/dagu/dags/` validan sin modificaciones.
Formato YAML propio de dag_engine. Schema: `name`, `description`, `schedule`, `env`, `tags`, `working_dir`, `steps[]`, `handlers` (alias `handler_on`).
### Ejemplo completo
@@ -193,7 +192,7 @@ Ventajas vs `command: ./fn run ...`:
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
Ejemplo completo: `~/.dagu/dags/example-fn-call.yaml`.
Ejemplo completo: `apps/dag_engine/dags_migrated/daily-registry-audit.yaml`.
### Cron schedule
@@ -226,7 +225,7 @@ Flags del `server`:
| Flag | Default | Que |
|---|---|---|
| `--port` | 8090 | Puerto HTTP. |
| `--dags-dir` | `~/dagu/dags` | Dir scaneado para YAMLs. |
| `--dags-dir` | `apps/dag_engine/dags_migrated` (via systemd unit) | Dir scaneado para YAMLs. |
| `--db` | `dag_engine.db` | SQLite con `dag_runs` + `dag_step_results`. |
| `--scheduler` | false | Si presente, arranca cron tickers automaticamente. |
@@ -323,11 +322,12 @@ SQL
### 5.7. Restaurar desde backup
Si rompes `dags_migrated/`:
Si rompes `dags_migrated/`, recupera desde el snapshot de `backup_all_bash_pipelines` (BACKUP_ROOT por defecto `~/backups/fn_registry`):
```bash
tar -xzf ~/backups/dagu_pre_removal_20260515.tar.gz -C /tmp/
cp /tmp/dagu/dags/*.yaml apps/dag_engine/dags_migrated/
cp ~/backups/fn_registry/registry/daily.0/dags_migrated/*.yaml \
apps/dag_engine/dags_migrated/ 2>/dev/null || \
git checkout HEAD -- apps/dag_engine/dags_migrated/
systemctl --user restart dag_engine.service
```
@@ -357,4 +357,4 @@ systemctl --user restart dag_engine.service
- Cron: `functions/core/parse_cron_expr.go` + `next_cron_time.go`.
- Frontend C++: `cpp/apps/dag_engine_ui/` (issue 0095).
- WS hub: `apps/dag_engine/events.go`.
- Migracion desde Dagu: 2026-05-15. Backup en `~/backups/dagu_pre_removal_20260515.tar.gz`.
- dag_engine es el scheduler oficial del ecosistema. Single-binary Go + SQLite, sin dependencias externas.
+6 -7
View File
@@ -3,7 +3,7 @@ name: dag_engine
lang: go
domain: infra
version: 0.1.0
description: "Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite."
description: "Motor de ejecucion de DAGs del fn_registry: CLI + servidor HTTP + scheduler cron. Schema YAML propio con `function:` para invocar funciones del registry (`fn run <id>`) y `command:` para shell. Historial en SQLite. Scheduler oficial del ecosistema."
tags: [service, dag, workflow, scheduler, web, cron]
uses_functions:
- dag_parse_go_core
@@ -84,18 +84,17 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
```bash
# CLI
./dag-engine run ~/dagu/dags/example.yaml
./dag-engine list ~/dagu/dags/
./dag-engine run apps/dag_engine/dags_migrated/fn_backup.yaml
./dag-engine list apps/dag_engine/dags_migrated/
# Servidor web
./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
# Servidor web (production: gestionado por dag_engine.service systemd user unit)
./dag-engine server --port 8090 --dags-dir apps/dag_engine/dags_migrated/ --scheduler
# Browser: http://localhost:8090
```
## Notas
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
Puerto por defecto 8090 (mismo que Dagu).
Schema YAML propio (ver `README.md` seccion 3 + ejemplos en `dags_migrated/`). Steps tipo `function:` invocan `fn run <id>` y propagan `function_id` a `dag_step_results` para el bucle reactivo. Puerto default 8090.
### 2026-05-16 — Fix function-not-found en steps `function:` + panel Logs en RunDetail `[done]`
@@ -0,0 +1,133 @@
# Propuesta e2e_checks para apps/auto_metabase
#
# Generado por fn-recopilador en modo design-e2e
# App: auto_metabase (lang: py, framework: httpx, entry_point: main.py)
# Detectado:
# - CLI con argparse, subcomandos (projects, login, pull, push, validate, ...)
# - NO service (no tag 'service', no puerto propio)
# - Deps: yaml, httpx en python/.venv (registry venv compartido)
# - Test: dashboard_split_test.py — pytest puro, imports relativos a apps/auto_metabase/
# - NO operations.db propia (no bucle reactivo propio)
# - Toca Metabase REAL en todos los comandos de red (login, pull, push, ...)
# -> todos los checks e2e deben ser OFF-LINE: import, CLI help, tests puros
#
# Justificacion por check:
#
# | check | razon |
# |--------------------|---------------------------------------------------------------------------|
# | import | Valida que el entry point y sus modulos cargan sin errores de import, |
# | | que sys.path al registry de funciones esta bien y que yaml/httpx estan |
# | | disponibles en el venv compartido. |
# | cli_help | Valida que argparse esta correctamente configurado y que el CLI no |
# | | crashea en el arranque (antes de que necesite project o red). Confirma |
# | | que todos los submodulos (sync_push, sync_pull, payload, ...) se cargan. |
# | mbql_validate_pure | Valida la funcion PURA metabase_mbql_validate del registry sin tocar |
# | | red. Es la unica funcion del modulo que puede correr offline sin |
# | | credenciales. Detecta regresiones en el validador MBQL local. |
# | tests_dashboard_split | Corre dashboard_split_test.py — tests puros del helper de split/merge |
# | | de dashboards por tab. No requieren red ni credenciales. |
# Los checks que SI requieren red (login, pull, push, validate --check-sql)
# no se incluyen porque tocan Metabase real o credenciales. Se dejan para
# un entorno de smoke con Metabase en Docker (ver nota abajo).
#
# NOTA — smoke con Docker (severity: warning, no incluido por default):
# Si se quiere gate de smoke real, levantar Metabase en Docker local y usar:
# docker run -d -p 3100:3000 --name mb_e2e metabase/metabase
# python/.venv/bin/python3 apps/auto_metabase/main.py init-project e2e_test --base-url http://localhost:3100 \
# --email test@test.com --password Test1234!
# python/.venv/bin/python3 apps/auto_metabase/main.py -p e2e_test login
# python/.venv/bin/python3 apps/auto_metabase/main.py -p e2e_test status
# Ese bloque queda fuera de e2e_checks hasta tener un entorno Docker reproducible.
app_id: auto_metabase
dir_path: apps/auto_metabase
lang: py
generated_by: fn-recopilador
generated_at: "2026-05-19"
e2e_checks:
- id: import
# Carga completa del entry point: sys.path al registry, yaml, httpx,
# metabase.client, payload, sync_pull, sync_push, etc.
# Si cualquier modulo tiene un import roto, falla aqui.
cmd: >-
python/.venv/bin/python3 -c
'import sys; sys.path.insert(0, "apps/auto_metabase");
import importlib.util, pathlib;
spec = importlib.util.spec_from_file_location("main", "apps/auto_metabase/main.py");
m = importlib.util.module_from_spec(spec);
spec.loader.exec_module(m);
print("import OK")'
expect_stdout_contains: "import OK"
timeout_s: 30
severity: critical
# por que: valida que el entry point carga sin ImportError (yaml, httpx,
# metabase.client, payload, sync_push, ...) usando el venv del registro.
- id: cli_help
# Valida que argparse esta bien formado y todos los submodulos cargan.
# 'projects --help' dispara solo el parser, no requiere red ni proyecto.
cmd: "python/.venv/bin/python3 apps/auto_metabase/main.py --help"
expect_stdout_contains: "usage:"
timeout_s: 15
severity: critical
# por que: el CLI tiene ~30 subcomandos; si cualquier modulo falla al
# importarse (payload.py, sync_push.py, etc.) esto lo captura.
- id: mbql_validate_pure
# Corre la funcion PURA metabase_mbql_validate del registry de forma offline.
# Prueba un dataset_query MBQL minimo valido (debe devolver lista vacia de errores)
# y uno con UUID duplicado (debe detectar el error).
cmd: >-
python/.venv/bin/python3 - <<'PYEOF'
import sys
sys.path.insert(0, "python/functions")
from metabase import metabase_mbql_validate
import uuid
# Caso valido: query nativo minimo sin errores
u1, u2 = str(uuid.uuid4()), str(uuid.uuid4())
valid_q = {
"lib/type": "mbql/query",
"database": 1,
"stages": [{"lib/type": "mbql.stage/native",
"native": "SELECT 1",
"lib/uuid": u1}],
"lib/uuid": u2,
}
errs = metabase_mbql_validate(valid_q)
assert errs == [], f"Esperaba 0 errores, got: {errs}"
# Caso invalido: UUID duplicado
dup = str(uuid.uuid4())
bad_q = {
"lib/type": "mbql/query",
"database": 1,
"stages": [{"lib/type": "mbql.stage/native",
"native": "SELECT 1",
"lib/uuid": dup}],
"lib/uuid": dup,
}
errs2 = metabase_mbql_validate(bad_q)
assert len(errs2) > 0, "Esperaba error de UUID duplicado"
print("mbql_validate_pure OK")
PYEOF
expect_stdout_contains: "mbql_validate_pure OK"
timeout_s: 30
severity: critical
# por que: metabase_mbql_validate es la unica funcion del modulo metabase
# ejecutable offline. Detecta regresiones en el validador MBQL puro.
- id: tests_dashboard_split
# Corre el unico test file puro de la app: dashboard_split_test.py.
# No requiere red, no toca Metabase. Pytest con confdir en la app dir.
cmd: >-
python/.venv/bin/python3 -m pytest -x -q
--rootdir=apps/auto_metabase
apps/auto_metabase/dashboard_split_test.py
timeout_s: 60
severity: critical
# por que: dashboard_split.py tiene logica de split/merge de dashboards
# multi-tab y el test file tiene casos exhaustivos. Es el unico gate
# automatico de esa logica que no requiere Metabase.
@@ -0,0 +1,43 @@
# app_id: deploy_server
# lang: go (CGO, sin fts5)
# stack: net/http + go-sqlite3 (CGO) + multi-strategy deployer (systemd/systemd-remote/docker-compose)
# deteccion automatica fn-recopilador 2026-05-19, issue 0121a
#
# Notas de deteccion:
# - go.mod usa github.com/mattn/go-sqlite3 (CGO=1) pero NO usa fts5 tags
# - sin *_test.go en el directorio -> check tests OMITIDO
# - server.go confirma: --port flag con default 9090, GET /api/health handler
# - puerto e2e: 9190 (+100 sobre puerto productivo 9090)
# - operations.db presente con tablas propias (deploy_targets, deploy_logs) -> ops_audit incluido
# - BD e2e en /tmp/ para idempotencia; binario en /tmp/ para no contaminar el directorio de la app
e2e_checks:
# build: compila el binario con CGO habilitado. Sin -tags fts5 (no lo usa el modulo).
# Valida que el modulo go-sqlite3 linkea correctamente en el entorno local.
- id: build
cmd: "cd /home/lucas/fn_registry/apps/deploy_server && CGO_ENABLED=1 go build -o /tmp/deploy_server_e2e_bin ."
timeout_s: 180
# cli_help: verifica que el binario arranca y responde sin crashear ni pedir args obligatorios.
# Captura el caso de init() con side effects o missing assets.
- id: cli_help
cmd: "/tmp/deploy_server_e2e_bin help"
expect_exit: 0
expect_stdout_contains: "deploy_server"
timeout_s: 10
# migrations: arranca el servidor con la BD de prueba y lo cierra inmediatamente.
# El servidor abre la BD al arrancar y aplica migraciones automaticamente (sqlite_apply_migrations).
# Usamos --port 0 no esta soportado (flag es int sin port-0), asi que usamos puerto alto efimero.
# Tras el health check, el proceso se mata automaticamente por fn-analizador.
- id: smoke
cmd: "/tmp/deploy_server_e2e_bin serve --port 9190 --db /tmp/deploy_server_e2e.db &"
health: "http://127.0.0.1:9190/api/health"
timeout_s: 15
# ops_audit: fn-recopilador audita operations.db de la app.
# Verifica integridad referencial, schema completo, ausencia de datos corruptos.
# La BD tiene tablas propias (deploy_targets, deploy_logs) + tablas del ciclo reactivo.
- id: ops_audit
ref: "fn-recopilador:apps/deploy_server"
severity: warning
@@ -0,0 +1,116 @@
# e2e_checks proposal — registry_api
#
# app_id: registry_api
# lang: go
# stack: net/http, CGO+FTS5, SQLite (mattn/go-sqlite3), replace fn-registry => ../../
# date: 2026-05-19
# issue: 0121 (design-e2e fn-recopilador)
#
# Diagnostico del stack:
# - entry_point: main.go (acepta --port N y --db PATH)
# - health endpoint: GET /api/status (sin auth, siempre 200 + JSON {"status":"ok",...})
# - auth layer: checkToken() en handleSync solamente; Traefik basicAuth en prod (no
# aplica en e2e local). /api/status, /api/search, /api/locations NO tienen auth.
# - no hay *_test.go => check "tests" omitido
# - operations.db no declarada para esta app => ops_audit omitido
# - puerto prod 8420 => e2e usa 8521 para no colisionar
#
# Instrucciones de adopcion:
# 1. Copiar el bloque "e2e_checks:" al frontmatter de apps/registry_api/app.md
# (justo antes del primer "##" de la seccion de prosa).
# 2. Revisar que el binario compilado quede en apps/registry_api/registry_api
# (el build check lo deja ahi por convencian).
# 3. El check auth_check requiere que REGISTRY_API_TOKEN este seteada en el
# entorno de test; si no lo esta, /api/sync devuelve 200 (token desactivado).
# Ajustar severity a "warning" si el CI no tiene la variable disponible.
#
# NOTA: NO escribir directo al app.md — propuesta para revision humana.
e2e_checks:
# --- build ---
# Compila el binario localmente con CGO+FTS5. El replace directive en go.mod
# apunta a ../../ (raiz del registry), por lo que el build debe lanzarse
# desde dentro del directorio de la app.
- id: build
cmd: "cd /home/lucas/fn_registry/apps/registry_api && CGO_ENABLED=1 go build -tags fts5 -o registry_api ."
timeout_s: 120
severity: critical
# por que: sin binario el resto de checks no tiene sentido; fallo de build
# indica cambio de API en fn-registry (replace) o dep rota.
# --- smoke ---
# Arranca el servidor con una BD efimera en /tmp y espera que responda en
# /api/status. Puerto 8521 (!=8420 prod, !=8420 dev) para no colisionar.
# El proceso en background se mata al terminar la suite por fn-analizador.
- id: smoke
cmd: "/home/lucas/fn_registry/apps/registry_api/registry_api -port 8521 -db /tmp/registry_api_e2e.db &"
health: "http://127.0.0.1:8521/api/status"
timeout_s: 10
severity: critical
# por que: tag 'service' => gate minimo es que el binario arranca y sirve
# el health endpoint. Sin esto no tiene sentido probar endpoints de negocio.
# --- status_json ---
# Verifica que /api/status devuelve JSON con campo "status":"ok".
# Distingue un servidor arrancado de uno en modo degradado.
- id: status_json
cmd: "curl -sf http://127.0.0.1:8521/api/status"
expect_stdout_contains: '"status":"ok"'
timeout_s: 5
severity: critical
# por que: el health check del smoke solo valida HTTP 200; este check
# valida ademas el contrato de respuesta JSON.
# --- search_open ---
# Verifica que /api/search acepta GET sin auth y devuelve JSON valido.
# Usa q=test para tener un resultado predecible (siempre devuelve arrays).
- id: search_open
cmd: "curl -sf 'http://127.0.0.1:8521/api/search?q=test'"
expect_stdout_contains: '"functions"'
timeout_s: 5
severity: critical
# por que: /api/search es el endpoint mas usado por fn sync y el MCP.
# Fallo aqui indica problema de FTS5 o schema de registry.db.
# --- search_missing_q ---
# Verifica que /api/search sin q= devuelve 400 (validacion de entrada).
- id: search_missing_q
cmd: "curl -s -o /dev/null -w '%{http_code}' 'http://127.0.0.1:8521/api/search'"
expect_stdout_contains: "400"
timeout_s: 5
severity: warning
# por que: regression guard para la validacion de parametros obligatorios.
# --- auth_check ---
# Verifica que POST /api/sync rechaza requests con token incorrecto cuando
# REGISTRY_API_TOKEN esta seteada. Usa credencial falsa conocida.
# IMPORTANTE: si REGISTRY_API_TOKEN no esta en el entorno de e2e, el servidor
# responde 200 (token desactivado por diseno). En ese caso este check pasa
# igualmente (exit 0 de curl) pero no valida auth real — ajustar a warning
# o setear la variable antes de correr la suite.
- id: auth_check
cmd: >
REGISTRY_API_TOKEN=real-secret
/home/lucas/fn_registry/apps/registry_api/registry_api -port 8522 -db /tmp/registry_api_e2e_auth.db &
sleep 1 &&
STATUS=$(curl -s -o /dev/null -w '%{http_code}'
-X POST http://127.0.0.1:8522/api/sync
-H 'Content-Type: application/json'
-H 'X-Registry-Token: wrong-token'
-d '{"pc_id":"e2e"}') &&
kill %1 2>/dev/null; [ "$STATUS" = "401" ]
timeout_s: 15
severity: warning
# por que: /api/sync es el unico endpoint protegido por token de aplicacion.
# Fallo indica regresion en checkToken(). Severity warning porque en CI sin
# REGISTRY_API_TOKEN el comportamiento es abierto por diseno.
# --- locations_open ---
# Verifica que /api/locations devuelve JSON sin requerir auth.
- id: locations_open
cmd: "curl -sf http://127.0.0.1:8521/api/locations"
expect_stdout_contains: "["
timeout_s: 5
severity: warning
# por que: /api/locations es consumido por fn sync locations; fallo indica
# problema en db.ListAllPcLocations() o schema de pc_locations.
@@ -0,0 +1,47 @@
# Propuesta e2e_checks para apps/shaders_lab
# Generado por fn-recopilador modo design-e2e
# Fecha: 2026-05-19
#
# Diagnostico:
# lang=cpp, framework=imgui (fn::run_app), domain=gfx
# toolchain: mingw-w64 (cross-compile Windows desde WSL)
# modulos: main.cpp + compiler.cpp + 19 .cpp del registry (gfx + core)
# deps externas: imgui_node_editor, SQLite::SQLite3
# persistencia propia: shaders_lab.db (junto al .exe, via shaderlab_db)
# sin tests/ ni tests_*.py detectados
# sin primitives_gallery integration (golden_image: OMITIDO)
# sin --self-test: fn::run_app no parsea argv (confirmado piloto 0120)
# sin tag 'service': no expone HTTP (smoke con health: OMITIDO)
# operations.db: NO usa (ops_audit: OMITIDO)
#
# Patron: C++ ImGui — checks estructurales sin lanzar GUI ni GPU
app_id: shaders_lab
e2e_checks:
# Build: compila el target completo para Windows via mingw-w64.
# Valida que todos los .cpp del registry enlazados (19 archivos gfx+core)
# compilan sin errores y el linkado con imgui_node_editor + SQLite tiene exito.
# Es el check mas valioso: detecta regresiones de API en cualquiera de las
# 19 funciones del registry que usa la app.
- id: build
cmd: "cmake --build /home/lucas/fn_registry/cpp/build/windows --target shaders_lab -j"
timeout_s: 300
severity: critical
# Verifica que el artefacto .exe existe tras el build.
# Detecta casos donde cmake reporta exit 0 pero el linker no produjo binario
# (muy raro con mingw-w64 pero ha ocurrido en builds parciales con -j).
- id: binary_exists
cmd: "test -f /home/lucas/fn_registry/cpp/build/windows/apps/shaders_lab/shaders_lab.exe"
timeout_s: 5
severity: critical
# Verifica que el .ico esta presente junto al .exe.
# add_imgui_app genera shaders_lab_appicon.rc que windres incluye como recurso;
# si el .ico falta el build pasa pero el .exe queda sin icono embebido
# (visible al deploy a /mnt/c/.../Desktop/apps/).
- id: icon_exists
cmd: "test -f /home/lucas/fn_registry/apps/shaders_lab/appicon.ico"
timeout_s: 5
severity: warning
+1
View File
@@ -39,6 +39,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu
| [navegator](navegator.md) | 4 | Automatización de browser via CDP + AX tree + LLM: obtener, limpiar, chunkear AX tree y llamar a Claude CLI |
| [cpp-dashboard-viz](cpp-dashboard-viz.md) | 10 | Primitivas C++ ImGui para dashboards: kpi_card, sparkline, line/bar/scatter/pie/heatmap/histogram, panel containers |
| [agents](agents.md) | 3 | Orquestar agentes Claude headless en git worktrees: launch, cleanup, DoD evidence schema audit |
| [backends](backends.md) | — | Stacks backend (Go net/http+SQLite default, MCP, mautrix, bubbletea, httpx, docker-compose): decision tree + esqueleto canonico + funciones del registry a componer |
## Como anadir grupo
+258
View File
@@ -0,0 +1,258 @@
# Capability: backends
Catalogo de stacks backend usados en `apps/`. Para que un agente que va a construir un backend nuevo sepa **que stack elegir**, **que funciones del registry componer** y **que esqueleto copiar** sin reinventar nada.
No es un grupo `tag:` del registry — es una guia transversal (los stacks atraviesan dominios). Complementa `cpp_apps.md` (frontend C++) y `deploy.md` (entrega).
## Stacks soportados
| Stack | Lang | Cuando elegir | Apps de referencia |
|---|---|---|---|
| **Go net/http stdlib + SQLite** | go | **Default.** API HTTP local o detras de Traefik. Persistencia SQLite con migraciones embed.FS. | `sqlite_api`, `services_api`, `registry_api`, `kanban`, `deploy_server`, `dag_engine`, `agent_runner_api`, `call_monitor` |
| **Go MCP stdio/http** | go | Tool server para Claude/agentes. Lectura/escritura registry. | `registry_mcp` |
| **Go bubbletea TUI** | go | CLI interactiva sin HTTP. Pipeline launcher, docker manager. | `pipeline_launcher`, `docker_tui`, `dev_console` |
| **Go mautrix bot** | go | Bot Matrix con E2EE, LLM tools, comandos. | `agents_and_robots` |
| **Python httpx + YAML** | py | Cliente declarativo de API REST externa (Metabase, Gitea, etc.). Pull/push contra disco. | `auto_metabase`, `metabase_registry` |
| **Bash docker-compose** | bash | Stack multi-container (Synapse + Element + LiveKit, PostGIS + Valhalla). | `element_matrix_chat`, `footprint_geo_stack` |
| **C++ ImGui frontend** | cpp | NO es backend — cliente HTTP/WS de los Go services. Ver `cpp_apps.md`. | `services_monitor`, `registry_dashboard`, ... |
## Decision tree
```
¿Necesitas HTTP API?
├─ si → ¿Es para Claude/agentes?
│ ├─ si → MCP server (Go) → copiar `registry_mcp`
│ └─ no → Go net/http stdlib + SQLite + embed.FS migrations → copiar `services_api`
└─ no → ¿Interactivo terminal?
├─ si → Go bubbletea → copiar `pipeline_launcher`
└─ no → ¿Cliente de API externa?
├─ si → Python httpx → copiar `auto_metabase`
└─ no → ¿Stack de containers?
├─ si → Bash + docker-compose.yml → copiar `element_matrix_chat`
└─ no → reevalua, probablemente sea funcion del registry no app
```
## Esqueleto canonico: Go net/http stdlib + SQLite (el default)
### Layout
```
apps/<name>/
app.md # frontmatter + service: block (issue 0105)
main.go # flags + listener + signal handling
server.go # http.ServeMux + routes + middleware chain
db.go # sql.Open con WAL + FK + apply migrations
handlers_*.go # 1 archivo por recurso
migrations/
001_init.sql # CREATE TABLE IF NOT EXISTS (idempotente)
002_*.sql # aditivo. NUNCA modificar 001 ya commiteado
operations.db # SQLite local. Gitignored.
```
### Frontmatter app.md
```yaml
---
name: my_service
lang: go
domain: infra # ver list_domains
version: 0.1.0
description: "1 linea: que hace + por que existe."
tags: [service, api, http, sqlite]
uses_functions:
- sqlite_apply_versioned_migrations_go_infra
- http_serve_go_infra
- http_json_response_go_infra
- http_parse_body_go_infra
- http_router_go_infra
- http_cors_middleware_go_infra
- http_logger_middleware_go_infra
- logger_go_infra
uses_types: []
framework: "net/http"
entry_point: "main.go"
dir_path: "apps/my_service"
service: # OBLIGATORIO si tag service. issue 0105
port: 8500
health_endpoint: /api/health
health_timeout_s: 3
systemd_unit: my_service.service
systemd_scope: user
restart_policy: always # NUNCA on-failure (ver gotcha cpp_apps.md)
runtime: systemd-user
pc_targets: [home-wsl]
is_local_only: false
---
```
### main.go (minimo viable)
```go
package main
import (
"context"
"flag"
"log"
"os/signal"
"syscall"
)
func main() {
bind := flag.String("bind", "127.0.0.1:8500", "addr to listen on")
dbPath := flag.String("db", "operations.db", "sqlite path")
flag.Parse()
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer cancel()
db, err := openDB(*dbPath) // db.go — usa sqlite_apply_versioned_migrations
if err != nil { log.Fatal(err) }
defer db.Close()
srv := newServer(db) // server.go — http.ServeMux + rutas
log.Printf("listening on %s", *bind)
if err := serve(ctx, *bind, srv); err != nil { log.Fatal(err) }
}
```
### Build + service unit
```bash
CGO_ENABLED=1 go build -tags fts5 -o my_service .
systemctl --user enable --now my_service.service # tras generar unit
```
## Esqueleto canonico: MCP server Go
Copia `registry_mcp`:
- `main.go` parsea flags (`--stdio` o `--http`).
- Cada tool = funcion Go con schema JSON y handler.
- Schema generation con `jsonschema` reflect.
- Read-only por default; `--enable-run` / `--enable-write` gating.
Funciones del registry relevantes: cualquier de `mcp__registry__fn_*` para entender shape. Patron de gating en `apps/registry_mcp/main.go`.
## Esqueleto canonico: Python httpx cliente declarativo
Copia `auto_metabase`:
- `python/.venv/bin/python3` como interprete.
- Importa wrappers del registry (`from infra import metabase_auth, metabase_get_dashboard, ...`).
- YAML manifest define estado deseado.
- Pull = volcar a YAML. Push = enviar a API.
- NUNCA `requests.post(...)` directo — siempre via wrapper que ya hace auth + retry + telemetria.
```python
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "python", "functions"))
from infra import metabase_auth, metabase_list_dashboards
session = metabase_auth(host, user, pwd)
for d in metabase_list_dashboards(session):
...
```
## Esqueleto canonico: bubbletea TUI
Copia `pipeline_launcher`:
- `tea.Model` con `Init/Update/View`.
- Sin HTTP. Estado local + acciones sobre disco/registry.
- Tag `launcher` si debe aparecer en el Pipeline Launcher (function_tags.md).
## Esqueleto canonico: Bash docker-compose stack
Copia `element_matrix_chat`:
- `docker-compose.yml` + `.env` + scripts `up.sh`/`down.sh`.
- Sin codigo Go/Py — solo composicion de imagenes.
- Tag `service` + `runtime: docker-compose` en `service:`.
- Deploy via `docker_compose_remote_deploy_bash_infra` (ver capability `deploy`).
## Funciones del registry — paleta backend Go
| ID | Para |
|---|---|
| `sqlite_apply_versioned_migrations_go_infra` | Aplicar `migrations/*.sql` con embed.FS al arrancar |
| `sqlite_open_*_go_infra` | Abrir SQLite con WAL + foreign keys + ping |
| `http_serve_go_infra` | Listener con graceful shutdown via ctx |
| `http_router_go_infra` | `*http.ServeMux` desde `[]Route` declarativo |
| `http_json_response_go_infra` | Escribir JSON + status code |
| `http_error_response_go_infra` | Errores con shape consistente |
| `http_parse_body_go_infra` | Decode JSON con maxBytes (anti-DoS) |
| `http_cors_middleware_go_infra` | CORS para frontend ImGui/web |
| `http_logger_middleware_go_infra` | Log estructurado por request |
| `http_middleware_chain_go_infra` | Componer middlewares |
| `http_session_cookie_middleware_go_infra` | Sesiones cookie con TTL |
| `http_session_cookie_set_go_infra` / `clear` / `extract` | CRUD cookie sesion |
| `jwt_middleware_go_infra` | Auth JWT (alternativa a cookies) |
| `crud_generate_handlers_go_infra` | 5 handlers REST a partir de `CRUDResource` |
| `crud_register_routes_go_infra` | Registrar `GET/POST/PUT/DELETE /resource[/id]` en mux |
| `file_serve_go_infra` | Static files con `Cache-Control: max-age` |
| `health_check_http_go_infra` | Polling de URL hasta 2xx (smoke en tests) |
| `logger_go_infra` | Logger estructurado (consumido por log_window apps C++) |
| `notify_telegram_go_infra` | Alertas a chat (opcional) |
| `ssh_exec_go_infra` | Ejecutar comandos en host remoto (services cross-PC) |
Buscar mas: `mcp__registry__fn_search query="http" lang="go" tag="service"`.
## Patron CRUD declarativo
Si el backend expone una entidad CRUD plana (cards, jobs, deploys), evita escribir 5 handlers a mano:
```go
res := infra.CRUDResource{
Table: "cards",
PKColumn: "id",
Columns: []string{"id","title","status","created_at"},
}
mux := http.NewServeMux()
infra.CRUDRegisterRoutes(mux, "/api/cards", res, db)
// GET /api/cards → list
// GET /api/cards/{id} → get
// POST /api/cards → create
// PUT /api/cards/{id} → update
// DELETE /api/cards/{id} → delete
```
## Service: block (issue 0105) — checklist obligatorio
Toda app con `tag: service` declara:
- `port` (null si stdio).
- `health_endpoint` (ruta GET 2xx → sano).
- `health_timeout_s` (default 3).
- `systemd_unit` si `runtime` empieza con `systemd-`.
- `systemd_scope`: `user` | `system`.
- `restart_policy`: **`always`** (no `on-failure` — ver gotcha en `function_tags.md`).
- `runtime`: `systemd-user` | `systemd-system` | `docker-compose` | `stdio` | `manual`.
- `pc_targets[]`: pc_ids de `pc_locations`.
Auditar: `fn doctor services-spec`.
## Gotchas comunes
| Gotcha | Causa | Solucion |
|---|---|---|
| Service muere en `SIGTERM` y systemd no reinicia | `Restart=on-failure` en unit | Cambiar a `Restart=always`. `sqlite_api` cayo 20h asi (2026-05-17) |
| `database is locked` al escribir desde 2 procesos | SQLite sin WAL | Abrir con `?_journal_mode=WAL&_foreign_keys=on` (usar `sqlite_open_*`) |
| Frontend ImGui no recibe CORS preflight | Falta `http_cors_middleware` | Anadir middleware antes del router |
| Body POST llega vacio | `json.NewDecoder(r.Body)` sin maxBytes — falla silenciosa | Usar `http_parse_body_go_infra` con `maxBytes=10MB` |
| Migracion 002 borra datos en otros PCs al hacer `fn sync` | Migracion destructiva | Solo aditivo. Ver `db_migrations.md` |
| MCP tool no aparece en Claude tras anadirla | Schema no regenerado | Rebuild + `claude mcp remove/add` o reset cache |
## Fronteras (que NO cubre esta capability)
- **Apps C++ frontend** → `cpp_apps.md` + capability `cpp-dashboard-viz`.
- **Deploy** del backend a VPS → capability `deploy` (Docker+Traefik, systemd, rsync).
- **Frontend web Vite/React/Mantine** consumido por el backend → capability `mantine`.
- **Migraciones de schema** → regla `db_migrations.md`.
- **Telemetria de calls** del agente al registry → `registry_calls.md` + `call_monitor` app.
## Cuando promover a pipeline / extraer al registry
Si tras construir el N-esimo Go service detectas patron repetido (>2 apps):
1. Buscar funcion existente en registry (`mcp__registry__fn_search`).
2. Si no existe → spawn `fn-constructor` con tag de grupo (`http`, `service`, etc.).
3. Migrar las apps existentes para consumirla.
4. Capability page se actualiza sola via `fn doctor capabilities`.
+1 -1
View File
@@ -62,7 +62,7 @@ unit=$(./fn run systemd_generate_unit \
## Fronteras
- **NO maneja timers ni paths units**. Solo service units. Para cron/timer usa Dagu o cron clasico.
- **NO maneja timers ni paths units**. Solo service units. Para cron/timer usa dag_engine (`apps/dag_engine/`, `schedule:` en el YAML del DAG) o cron clasico.
- **NO genera socket activation**. Asume que la app abre su propio puerto.
- **NO instala unit a nivel usuario (`--user`)** salvo que el caller pase la flag al systemctl. Default es `/etc/systemd/system/`.
- Apps en `apps/` que se ejecutan local pero NO son service de larga duracion: NO necesitan systemd. Solo apps con `tags: [service]` en su `app.md`.
+29 -26
View File
@@ -1,6 +1,6 @@
# Estandar de Ejecucion: YAML + Exit Codes + Hooks
Contrato comun para apps, pipelines y automatizaciones del fn-registry. Define como declarar flujos en YAML, que exit codes devolver, como reportar resultados y como integrarse con Dagu y operations.db.
Contrato comun para apps, pipelines y automatizaciones del fn-registry. Define como declarar flujos en YAML, que exit codes devolver, como reportar resultados y como integrarse con dag_engine y operations.db.
---
@@ -8,7 +8,7 @@ Contrato comun para apps, pipelines y automatizaciones del fn-registry. Define c
Tenemos dos patrones que ya funcionan:
- **Dagu**: orquesta DAGs con `command`/`script`, usa exit codes para decidir flujo, tiene handlers globales y scheduling cron.
- **dag_engine** (`apps/dag_engine/`): orquesta DAGs con `command`/`function`, usa exit codes para decidir flujo, soporta `handlers` (init/success/failure/exit) y scheduling cron via `schedule:`.
- **script_navegador**: ejecuta pasos tipados (CDP actions) desde YAML, registra todo en operations.db, sale con 0/1.
Ambos comparten la misma idea: **YAML declara el flujo, exit code señala el resultado, logs estructurados permiten analisis posterior**. Este documento formaliza ese contrato para que cualquier app nueva lo siga desde el inicio.
@@ -73,7 +73,8 @@ Cada app registra sus actions validos. Ejemplos:
| Dominio | Actions | App de referencia |
|---------|---------|-------------------|
| navegacion | `navigate`, `click`, `type`, `wait`, `screenshot`, `evaluate`, `get_html`, `wait_load`, `sleep` | script_navegador |
| shell | `command`, `script` | Dagu / apps genericas |
| shell | `command`, `script` | dag_engine / apps genericas |
| registry | `function`, `args` | dag_engine (invoca `fn run <id>` y captura `function_id` en `dag_step_results`) |
| database | `query`, `migrate`, `backup` | futuras apps |
| http | `request`, `assert_status`, `extract` | futuras apps |
@@ -96,16 +97,16 @@ Toda app/script que siga este estandar DEBE salir con uno de estos tres codigos:
- Si algun paso falla pero todos los fallos tenian `continue_on_error: true``2`
- Un timeout agotado cuenta como fallo del paso
### Compatibilidad con Dagu
### Compatibilidad con dag_engine
Dagu interpreta exit code != 0 como fallo. Para que `2` (partial) no dispare el handler de failure en Dagu, el DAG que llama a la app puede usar:
dag_engine interpreta exit code != 0 como fallo. Para que `2` (partial) no dispare el handler de failure, el DAG que llama a la app puede usar `continue_on.exit_code`:
```yaml
steps:
- name: run_app
command: ./mi_app --script flujo.yaml
continue_on:
failure: true # no abortar el DAG por parcial
exit_code: [2] # exit 2 no aborta el DAG (partial es tolerado)
- name: check_result
command: test $? -le 1 # falla solo si fue error fatal
depends: [run_app]
@@ -152,10 +153,10 @@ Toda app DEBE escribir un **resumen JSON en stdout** al finalizar (despues de su
### Separacion humano / maquina
- **stderr**: logs legibles para humanos (progreso, warnings)
- **stdout**: output JSON estructurado al final (para Dagu, hooks, pipes)
- **stdout**: output JSON estructurado al final (para dag_engine, hooks, pipes)
- Flag `--json` o `--quiet`: suprime output humano, solo JSON en stdout
Esto permite que Dagu capture el JSON:
Esto permite que dag_engine capture el JSON:
```yaml
steps:
@@ -197,23 +198,24 @@ hooks:
| `{{.StepName}}` | string | Nombre del paso (solo en on_step_error) |
| `{{.Error}}` | string | Mensaje de error (solo en on_step_error/on_failure) |
### En Dagu (handlers globales)
### En dag_engine (handlers globales)
Los hooks del YAML son locales al flujo. Para orquestacion, Dagu maneja sus propios handlers:
Los hooks del YAML son locales al flujo. Para orquestacion, dag_engine maneja sus propios handlers (`init/success/failure/exit`, alias `handler_on`):
```yaml
# dags/mi_automatizacion.yaml
# apps/dag_engine/dags_migrated/mi_automatizacion.yaml
name: mi_automatizacion
handlers:
failure:
- name: alerta
command: echo "Fallo en mi_automatizacion" >> ~/dagu/logs/failures.log
command: echo "Fallo en mi_automatizacion" >> /var/log/fn_registry/failures.log
success:
- name: registrar
command: echo "OK $(date)" >> ~/dagu/logs/success.log
command: echo "OK $(date)" >> /var/log/fn_registry/success.log
steps:
- name: ejecutar
command: ./fn run mi_pipeline --script flujo.yaml
function: mi_pipeline_bash_pipelines
args: ["flujo.yaml"]
```
---
@@ -249,15 +251,15 @@ Este patron ya esta implementado en `script_navegador/ops.go` y sirve como refer
---
## 6. Flujo completo: App → Dagu → Hooks
## 6. Flujo completo: App → dag_engine → Hooks
```
Dagu (scheduler)
dag_engine (scheduler)
cron / manual
cron / manual / API
┌────▼────┐
│ DAG │ dags/mi_dag.yaml
│ DAG │ apps/dag_engine/dags_migrated/mi_dag.yaml
│ step │ command: ./mi_app --script flujo.yaml --json
└────┬────┘
@@ -280,11 +282,11 @@ Este patron ya esta implementado en `script_navegador/ops.go` y sirve como refer
success failure partial
│ │ │
▼ ▼ ▼
Dagu OK Dagu FAIL Dagu FAIL*
handler handler (configurable)
engine OK engine FAIL engine FAIL*
handler handler (configurable)
```
*Con `continue_on: failure: true` en el step de Dagu, exit 2 no aborta el DAG.
*Con `continue_on: { exit_code: [2] }` en el step, exit 2 no aborta el DAG.
---
@@ -330,19 +332,20 @@ hooks:
on_failure: "echo 'Backup fallo' >> /tmp/backup_errors.log"
```
### Invocacion desde Dagu
### Invocacion desde dag_engine
```yaml
name: backup_diario
schedule: "0 2 * * *"
working_dir: /home/lucas/fn_registry/apps/mi_app
steps:
- name: backup
command: ./fn run backup_db --script flujo.yaml --json
working_dir: /home/lucas/fn_registry/apps/mi_app
function: backup_db_bash_pipelines
args: ["flujo.yaml", "--json"]
handlers:
failure:
- name: alerta
command: echo "Backup fallo $(date)" >> ~/dagu/logs/failures.log
command: echo "Backup fallo $(date)" >> /var/log/fn_registry/failures.log
```
### Output esperado (stdout)
@@ -380,5 +383,5 @@ Exit code: `2` (partial — push fallo pero tenia continue_on_error).
| Output humano | stderr (progreso, warnings) |
| Error handling | `continue_on_error` por paso, hooks por resultado |
| Trazabilidad | operations.db (executions + logs) |
| Orquestacion | Dagu como scheduler, apps como ejecutores |
| Orquestacion | dag_engine como scheduler (`apps/dag_engine/`), apps/funciones del registry como ejecutores |
| Funciones | Reutilizar del registry, actions por dominio |