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 2b5c30a022
commit 53a3cdbda9
10 changed files with 629 additions and 28 deletions
+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 |