Problema
Los agentes de IA que ejecutan acciones externas (publicar en redes, enviar correos, crear contenido) pueden generar resultados inesperados o inseguros si actúan sin supervisión. En entornos productivos o en un homelab, la práctica recomendada es introducir una capa de aprobación humana: el agente propone la acción, un operador la revisa y la autoriza o rechaza, y solo entonces se lleva a cabo. El reto consiste en crear un punto de entrada (inbox) que sea:
- Self‑hosted: sin dependencias externas ni telemetría.
- Ligero: sin necesidad de bases de datos pesadas ni orquestadores complejos.
- Accesible: API REST para los agentes y UI web/PWA para los operadores.
- Persistente: almacenamiento fiable pero sencillo (por ejemplo SQLite).
Muchos proyectos intentan resolver esto con soluciones de workflow completas (n8n, Temporal) que añaden complejidad innecesaria cuando solo se necesita una puerta de aprobación.
Causa
Los fallos habituales al intentar montar este tipo de gateway provienen de tres áreas:
- Persistencia improvisada – usar archivos planos sin bloqueo o bases de datos embebidas sin volúmenes Docker lleva a pérdida de datos al reiniciar el contenedor.
- Exposición de la API – abrir puertos sin autenticación o depender de claves estáticas que se pierden al recrear el contenedor.
- Integración con el agente – los agentes suelen esperar respuestas síncronas; si el inbox sólo permite polling, la lógica del agente se vuelve frágil y se generan time‑outs.
En setups domésticos o VPS, la combinación de Docker Compose, un volumen nombrado y una clave de admin generada al iniciar suele ser la solución más estable, siempre que se gestione correctamente el ciclo de vida del contenedor.
Solución
Construir un inbox de aprobación humana con los siguientes componentes:
| Componente | Rol |
|---|---|
| Docker Compose | Orquesta el contenedor del servidor y garantiza la persistencia mediante un volumen. |
| SQLite | Base de datos ligera, almacenada en el volumen impri-data. |
| REST API | Punto de entrada para que los agentes envíen propuestas (POST /proposals). |
| Web UI / PWA | Interfaz para que el operador revise, edite y apruebe/rechace. |
| MCP (Message Control Protocol) | Wrapper opcional que transforma la API en llamadas de herramienta para agentes como Claude. |
| HMAC‑signed webhooks | Opcional, permite notificar al agente mediante callback seguro. |
Paso a paso
-
Clonar el repositorio
git clone https://gitlab.com/sekera.radim/impri.git cd impri -
Definir
docker-compose.yml(el repositorio ya lo incluye, pero se muestra la parte esencial):version: "3.9" services: server: image: ghcr.io/sekera/impri:latest ports: - "8484:8484" volumes: - impri-data:/app/data restart: unless-stopped volumes: impri-data: -
Levantar la pila
docker compose up -d -
Obtener la clave de administrador – al iniciar, el contenedor escribe en
stdoutuna cadena tipoADMIN_KEY=xxxx. Copia esa clave y úsala para iniciar sesión enhttp://<host>:8080. -
Configurar agentes – los agentes deben apuntar a
http://<host>:8484/api/v1/proposalsy firmar la carga con la misma clave (o con HMAC si se usa webhook). Un ejemplo en Python:import requests, hmac, hashlib, json admin_key = "xxxx" payload = {"action": "post_tweet", "content": "Hola mundo"} signature = hmac.new(admin_key.encode(), json.dumps(payload).encode(), hashlib.sha256).hexdigest() headers = {"X-Signature": signature} r = requests.post("http://localhost:8484/api/v1/proposals", json=payload, headers=headers) print(r.json()) -
Aprobación – el operador entra en la UI, revisa la propuesta, la edita si es necesario y pulsa Approve. El servidor devuelve la decisión al agente mediante la misma llamada de polling o mediante el webhook configurado.
Alternativas prácticas
- Persistencia externa – si la carga supera los límites de SQLite, montar un contenedor con PostgreSQL y cambiar la variable de entorno
DB_URLes trivial. - TLS – colocar un proxy Nginx delante del contenedor y habilitar HTTPS evita que la clave admin viaje en texto plano.
- Escalado – para alta disponibilidad, replicar el contenedor y usar un volumen compartido (por ejemplo, NFS) mantiene la coherencia de la base de datos.
Cuándo aplicar esta solución
Aplica cuando:
- Necesitas una puerta de aprobación para acciones críticas de agentes de IA (publicaciones, envíos de correo, despliegues).
- Tu infraestructura es pequeña‑mediana (homelab, VPS, servidor dedicado) y no justifica un motor de workflow completo.
- Prefieres mantener los datos en tu propio entorno, sin telemetría ni dependencias SaaS.
No aplica si:
- Requieres flujos de trabajo con ramificaciones, programación de tareas o ejecución automática de acciones (en ese caso, n8n, Temporal o Airflow son más adecuados).
- El número de propuestas supera varios miles por minuto; SQLite puede convertirse en cuello de botella.
Código
# 1. Clonar repositorio
git clone https://gitlab.com/sekera.radim/impri.git
cd impri
# 2. Levantar con Docker Compose
docker compose up -d
# 3. Ver logs para obtener la admin key
docker logs $(docker ps -qf "ancestor=ghcr.io/sekera/impri") | grep ADMIN_KEY
# 4. Enviar propuesta desde un agente (ejemplo con curl)
ADMIN_KEY="copied_key_here"
PAYLOAD='{"action":"send_email","to":"[email protected]","subject":"Test","body":"Hola"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$ADMIN_KEY" | cut -d' ' -f2)
curl -X POST http://localhost:8484/api/v1/proposals \
-H "Content-Type: application/json" \
-H "X-Signature: $SIGNATURE" \
-d "$PAYLOAD"
Verificación
- Comprobar que el contenedor está activo
docker ps -f "ancestor=ghcr.io/sekera/impri" - Acceder a la UI – abrir
http://<host>:8080y validar que la pantalla de login muestra el campo de clave. - Enviar una propuesta (paso 4 del bloque de código). La respuesta debe contener un
idy un estadopending. - Aprobar desde la UI – localizar la propuesta, pulsar Approve y observar que el estado cambia a
approved. - Consultar el endpoint de decisión (polling) desde el agente para confirmar que recibe
approved. - Persistencia – reiniciar el contenedor (
docker compose restart) y verificar que la propuesta sigue presente en la UI.
Notas adicionales
- Respaldo de SQLite – aunque el volumen protege contra reinicios, es buena práctica programar
sqlite3 /app/data/impri.db ".backup '/backup/impri_$(date +%F).db'"en un cron externo. - Rotación de claves – la clave admin se genera solo al primer arranque. Si la pierdes, elimina el volumen
impri-datay vuelve a crear el contenedor; perderás los datos, por lo que el backup previo es esencial. - Modo offline – si el agente no puede hacer polling, habilita los webhooks HMAC; el servidor enviará una petición POST a la URL configurada al cambiar el estado.
- Mobile‑friendly – la UI está diseñada como PWA, así que puedes “añadir a pantalla de inicio” en Android/iOS y recibir notificaciones de nuevas propuestas (requiere Service Worker habilitado).
- Limitaciones de SQLite – evita consultas complejas; la tabla de propuestas está indexada solo por
idystatus. Si necesitas filtros avanzados, migra a PostgreSQL y actualizaDB_URLendocker-compose.yml.
Con esta arquitectura, cualquier agente de IA puede operar bajo control humano sin depender de servicios externos, manteniendo la simplicidad de Docker y la fiabilidad de SQLite. La solución es lo suficientemente genérica para adaptarse a distintos tipos de agentes y suficientemente ligera para ejecutarse en un Raspberry Pi o en una VM de 512 MiB.