From 53a3cdbda9c01aac5e260948e8feb5fac15b38eb Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Tue, 19 May 2026 00:31:30 +0200 Subject: [PATCH] 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) --- .claude/rules/registry_calls.md | 2 +- dev/proposals_e2e_checks_0121/.gitkeep | 0 .../auto_metabase.yaml | 133 +++++++++ .../deploy_server.yaml | 43 +++ .../registry_api.yaml | 116 ++++++++ .../shaders_lab.yaml | 47 ++++ docs/capabilities/INDEX.md | 1 + docs/capabilities/backends.md | 258 ++++++++++++++++++ docs/capabilities/systemd.md | 2 +- docs/execution_standard.md | 55 ++-- 10 files changed, 629 insertions(+), 28 deletions(-) create mode 100644 dev/proposals_e2e_checks_0121/.gitkeep create mode 100644 dev/proposals_e2e_checks_0121/auto_metabase.yaml create mode 100644 dev/proposals_e2e_checks_0121/deploy_server.yaml create mode 100644 dev/proposals_e2e_checks_0121/registry_api.yaml create mode 100644 dev/proposals_e2e_checks_0121/shaders_lab.yaml create mode 100644 docs/capabilities/backends.md diff --git a/.claude/rules/registry_calls.md b/.claude/rules/registry_calls.md index 85e1339c..acb1958c 100644 --- a/.claude/rules/registry_calls.md +++ b/.claude/rules/registry_calls.md @@ -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 ` 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. diff --git a/dev/proposals_e2e_checks_0121/.gitkeep b/dev/proposals_e2e_checks_0121/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/dev/proposals_e2e_checks_0121/auto_metabase.yaml b/dev/proposals_e2e_checks_0121/auto_metabase.yaml new file mode 100644 index 00000000..d8d814bf --- /dev/null +++ b/dev/proposals_e2e_checks_0121/auto_metabase.yaml @@ -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. diff --git a/dev/proposals_e2e_checks_0121/deploy_server.yaml b/dev/proposals_e2e_checks_0121/deploy_server.yaml new file mode 100644 index 00000000..e2250e42 --- /dev/null +++ b/dev/proposals_e2e_checks_0121/deploy_server.yaml @@ -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 diff --git a/dev/proposals_e2e_checks_0121/registry_api.yaml b/dev/proposals_e2e_checks_0121/registry_api.yaml new file mode 100644 index 00000000..870d1a6f --- /dev/null +++ b/dev/proposals_e2e_checks_0121/registry_api.yaml @@ -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. diff --git a/dev/proposals_e2e_checks_0121/shaders_lab.yaml b/dev/proposals_e2e_checks_0121/shaders_lab.yaml new file mode 100644 index 00000000..f0a4a679 --- /dev/null +++ b/dev/proposals_e2e_checks_0121/shaders_lab.yaml @@ -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 diff --git a/docs/capabilities/INDEX.md b/docs/capabilities/INDEX.md index 14274418..17675375 100644 --- a/docs/capabilities/INDEX.md +++ b/docs/capabilities/INDEX.md @@ -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 diff --git a/docs/capabilities/backends.md b/docs/capabilities/backends.md new file mode 100644 index 00000000..5a3e46f4 --- /dev/null +++ b/docs/capabilities/backends.md @@ -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// + 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`. diff --git a/docs/capabilities/systemd.md b/docs/capabilities/systemd.md index 7a0b42d3..eff6d721 100644 --- a/docs/capabilities/systemd.md +++ b/docs/capabilities/systemd.md @@ -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`. diff --git a/docs/execution_standard.md b/docs/execution_standard.md index 48895eeb..5bd082d0 100644 --- a/docs/execution_standard.md +++ b/docs/execution_standard.md @@ -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 ` 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 |