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:

  1. 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.
  2. Exposición de la API – abrir puertos sin autenticación o depender de claves estáticas que se pierden al recrear el contenedor.
  3. 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

  1. Clonar el repositorio

    git clone https://gitlab.com/sekera.radim/impri.git
    cd impri
    
  2. 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:
    
  3. Levantar la pila

    docker compose up -d
    
  4. Obtener la clave de administrador – al iniciar, el contenedor escribe en stdout una cadena tipo ADMIN_KEY=xxxx. Copia esa clave y úsala para iniciar sesión en http://<host>:8080.

  5. Configurar agentes – los agentes deben apuntar a http://<host>:8484/api/v1/proposals y 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())
    
  6. 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_URL es 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

  1. Comprobar que el contenedor está activo
    docker ps -f "ancestor=ghcr.io/sekera/impri"
    
  2. Acceder a la UI – abrir http://<host>:8080 y validar que la pantalla de login muestra el campo de clave.
  3. Enviar una propuesta (paso 4 del bloque de código). La respuesta debe contener un id y un estado pending.
  4. Aprobar desde la UI – localizar la propuesta, pulsar Approve y observar que el estado cambia a approved.
  5. Consultar el endpoint de decisión (polling) desde el agente para confirmar que recibe approved.
  6. 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-data y 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 id y status. Si necesitas filtros avanzados, migra a PostgreSQL y actualiza DB_URL en docker-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.