Implementa un Agente Hermes Privado en Render de manera segura con Pulumi, Modal y Tailscale.
Compartir
Estás leyendo un resumen. El contenido completo está en pulumi.com.
Los agentes personales de IA tuvieron su gran explosión este año. OpenClaw superó los 100,000 estrellas en GitHub en meses tras su lanzamiento, y auto-hospedar tu propio asistente pasó de ser un truco de aficionado a algo que muchos desarrolladores realmente hacen. Escribí sobre cómo desplegar ese langostino en AWS o Hetzner en su momento. El que la gente está empezando a usar ahora es Hermes, el entorno de ejecución de código abierto de Nous Research, y se popularizó igual de rápido. La razón aparece en cada hilo de "cambié OpenClaw por Hermes": realmente aprende, construye memoria y escribe sus propias habilidades a medida que avanza en vez de correr basado en una lista estática escrita por humanos. Aquí está la parte que los videos de lanzamiento omiten. Hermes escribe y ejecuta su propio código, sin que un humano apruebe las órdenes. Un modelo que puede escribir código eventualmente escribirá uno defectuoso, y lo único que hay entre ese comando y tus credenciales es el entorno aislado en el que se ejecuta. Esa es la caja que no quieres tener en internet público. Los investigadores encontraron 175,000 servidores de Ollama expuestos y abiertos a principios de 2026, y los atacantes secuestran los que encuentran para obtener recursos computacionales. La solución no es tener una mejor cerradura en la puerta de entrada. Es no tener puerta de entrada en absoluto. Así que esta publicación despliega un agente privado de Hermes como un solo programa de Pulumi a través de Render, Modal y Tailscale. El agente y su interfaz de chat funcionan como servicios privados de Render sin URL pública, Tailscale coloca la UI en tu tailnet, y Modal ejecuta el código del agente en entornos desechables. Un pulumi up para levantarlo, un pulumi destroy para desmontarlo, alrededor de $50 al mes. Sin Makefile, sin salidas de CLI, y una mirada honesta a los pocos lugares donde "nada en internet público" todavía tiene fugas. ¿Qué es Hermes? Hermes es el entorno de ejecución de agentes de código abierto y licenciado bajo MIT de Nous Research. Funciona continuamente en un servidor, en lugar de vivir en una pestaña del navegador, y te comunicas con él a través de una interfaz de chat o una plataforma de mensajería como Telegram, Discord, Slack o Signal. Algunas cosas lo hacen más que un chatbot: es agnóstico del modelo. Lo señalas hacia cualquier proveedor. Este despliegue utiliza OpenRouter, una clave única que presenta más de 400 modelos a través de proveedores, con failover automático cuando uno está caído. Recuerda entre reinicios. La memoria persiste en disco, por lo que construye contexto a lo largo del tiempo en lugar de reiniciar cada sesión. Escribe sus propias habilidades. Cuando trabaja en una tarea, puede guardar ese enfoque como una habilidad reutilizable y acceder a ella la próxima vez. Se programa a sí mismo. Un cron integrado ejecuta automatizaciones en un horario, impulsado en lenguaje sencillo. Ejecuta código en un entorno aislado. Cuando el agente necesita ejecutar código, lo transfiere a un contenedor aislado y efímero en lugar de ejecutarlo junto a su propio proceso. La diferencia con un asistente alojado en la nube es el habitual intercambio de auto-hospedaje: funciona en tu infraestructura con tus claves, y tu historial de conversación y memoria se quedan en tu disco. "Hermes" aquí se refiere al entorno de ejecución de agentes de Nous Research, no a la familia de modelos lingüísticos que comparten el nombre. El agente es agnóstico del modelo, y el modelo con el que se comunica es una elección separada que haces a través de OpenRouter.
Requisitos antes de comenzar, asegúrate de tener: Pulumi CLI instalado y configurado una cuenta de Pulumi Cloud una cuenta de Render, una clave de API y tu ID de propietario de espacio de trabajo una cuenta de Modal y un par de tokens (ID de token y secreto) una clave API de OpenRouter con crédito en la cuenta un registro de contenedores al que puedas enviar; esta publicación utiliza GitHub Container Registry (un nombre de usuario de GHCR y token) Docker funcionando localmente (la construcción de imágenes se realiza en un motor buildx local), o un corredor CI habilitado para Docker una cuenta de Tailscale con HTTPS habilitado y una clave de autenticación reutilizable Node.js 18+ (para TypeScript) o Python 3.9+ (para Python), dependiendo del lenguaje que elijas. Esta guía enruta modelos a través de OpenRouter, pero Hermes también funciona con otros proveedores. El modelo es solo una cadena de enrutamiento que pasas (gpt-5.5 aquí), por lo que cualquier modelo que OpenRouter soporte funciona al cambiar ese solo valor.
Comprendiendo la arquitectura de Hermes, elimina los clics del panel y la arquitectura consta de cuatro piezas. Una regla rige todas ellas: lo que se permite tocar a internet público. flujo LR Usuario ([Tus dispositivos]) -->|"tailnet (WireGuard)"| TS["Tailscale<br/>malla privada"] TS -->|"open-webui.<tailnet>.ts.net"| WebUI["Interfaz web abierta<br/>(servicio privado de Render,<br/>unido a tu tailnet)"] WebUI -->|"red privada<br/><hermes-slug>:8642"| Hermes["Puerta de enlace Hermes<br/>(servicio privado de Render)"] Hermes -->|"Modal SDK en tiempo de ejecución"| Modal["Sandbox Modal<br/>ejecución de código aislada"] Hermes -->|llamadas de modelo| OR["OpenRouter"]
Descripción del componente del puerto Hermes puerta de enlace 8642 Mantiene secretos y memoria, habla con el modelo, crea sandboxes de Modal. La caja más peligrosa, por lo que no tiene URL pública. Interfaz web abierta 8080 La interfaz de chat. Se une a tu tailnet y accede a Hermes a través de la red privada de Render. Sandbox de Modal - Contenedor aislado y efímero para ejecutar el código del agente. Tailscale - Una malla privada de WireGuard que hace que la UI sea accesible solo para tus dispositivos. El agente que puede ejecutar código y leer secretos nunca es enrutable desde internet, y tampoco lo es la UI que está frente a él. No hay entrada en ninguna caja (las llamadas salientes al modelo, a Modal y al registro de imágenes aún cruzan internet, como deben). Expresar "qué caja puede alcanzar qué" en código es lo que hace que el límite sea auditable: la regla vive en el programa en lugar de en un panel. Las cuatro piezas se dividen limpiamente en dos: dos que declaras como recursos, y dos que le das una credencial y dejas funcionar.
Capa Cómo lo dispones Estado Servicios Render, discos, secretos proveedor oficial de terraform render-oss, conectado Declarativo, a través de una adición de un paquete de pulumi. Imágenes de contenedor @pulumi/docker-build Construidas y enviadas durante pulumi up. Sin docker CLI. Sandboxes de Modal nada No existe un proveedor de Terraform. Una credencial de tiempo de ejecución. Acceso a la tailnet (Tailscale) nada No se necesita proveedor. Una credencial de tiempo de ejecución, como Modal. Dos de estas cuatro capas son credenciales, no recursos. Esa distinción a menudo se pasa por alto, y vale la pena entenderla: un proveedor solo puede gestionar lo que puede provisionar, y levantar un sandbox de Modal es un acto de tiempo de ejecución, no un recurso provisionado. Volveremos a ello en cada capa.
Configurando ESC para la gestión de secretos Desplegar Hermes significa manejar una pila de credenciales: una clave de API de Render, credenciales de registro, la clave de OpenRouter, el par de tokens de Modal, y la clave de autenticación de Tailscale. No quieres que estén codificadas de forma rígida o dispersas en variables de entorno. Pulumi ESC (Entornos, Secretos y Configuración) las almacena de forma segura y las pasa directamente a tu programa Pulumi. Crea un nuevo entorno ESC: pulumi env init <tu-org>/hermes-secrets Agrega tus secretos al entorno: valores: renderApiKey: fn::secret: "rnd_xxxxx" renderOwnerId: "tea-xxxxx" ghcrUser: "tu-nombre-de-usuario-github" ghcrToken: fn::secret: "ghp_xxxxx" openrouterApiKey: fn::secret: "sk-or-xxxxx" modalTokenId: fn::secret: "ak-xxxxx" modalTokenSecret: fn::secret: "as-xxxxx" tailscaleAuthKey: fn::secret: "tskey-auth-xxxxx" tailnetDnsName: "tailxxxxx.ts.net" pulumiConfig: renderApiKey: ${renderApiKey} ownerId: ${renderOwnerId} ghcrUser: ${ghcrUser} ghcrToken: ${ghcrToken} openrouterApiKey: ${openrouterApiKey} modalTokenId: ${modalTokenId} modalTokenSecret: ${modalTokenSecret} tailscaleAuthKey: ${tailscaleAuthKey} tailnetDnsName: ${tailnetDnsName} Para encontrar el nombre DNS de tu tailnet, ve al consola de administración de Tailscale, busca en la sección de DNS, y encuentra el nombre de tu tailnet (por ejemplo, tailxxxxx.ts.net). Este es el sufijo de dominio utilizado para todas las máquinas en tu red de Tailscale. Luego crea un archivo Pulumi.dev.yaml en tu proyecto para hacer referencia al entorno: entorno: - <tu-org>/hermes-secrets Esto mantiene tus secretos fuera de tu base de código y los entrega al programa en el momento del despliegue. El par de tokens de Modal y la clave de autenticación de Tailscale nunca se convierten en recursos; pasan directamente como variables de entorno encriptadas en los servicios que las necesitan.
Generando el SDK de Render Render no tiene un proveedor de Pulumi de primera parte. Lo que tiene es un proveedor de Terraform oficial y mantenido, y Pulumi puede convertir cualquier proveedor de Terraform en un SDK local tipado con Any Terraform Provider: pulumi package add terraform-provider render-oss/render 1.8.0 Ese solo comando genera un SDK de render que importas como cualquier otro paquete, en el lenguaje que esté escrito tu pila. No hay un paquete @pulumi/render publicado y ningún proveedor que esperar. Aquí también es donde el mapa de proveedores muestra sus bordes. El puente envuelve un proveedor de Terraform que ya existe, por lo que funciona para Render. Pero Modal no tiene proveedor de Terraform en ninguna parte, así que pulumi package add terraform-provider modal-labs/modal no tiene nada que obtener. Eso no es un vacío que haya que sortear. Modal no es un recurso provisionado aquí; Hermes crea sandboxes en tiempo de ejecución a través del SDK de Modal, y el único trabajo de Pulumi es mantener el par de tokens y entregarlos. Tailscale cae en el mismo cubo: un proveedor de Tailscale existe, pero gestiona la configuración de tu tailnet, no el acto de poner un contenedor en la red, que es un trabajo de tiempo de ejecución realizado dentro de la imagen.
Asegurando con Tailscale La forma habitual de hacer que una UI auto-hospedada sea accesible es darle un nombre de host público y poner una puerta de identidad al frente: un proveedor de SSO, una política de acceso, un registro DNS público, y un certificado que mantener renovado. Tailscale colapsa todo eso en nada que declares. No hay una puerta que añadir frente a una URL pública, porque no hay URL pública. La interfaz web abierta se une a tu tailnet y responde solo a tus propios dispositivos. Esta es una posición más fuerte que una UI pública detrás de un inicio de sesión. Debido a que no hay URL pública, no hay inicio de sesión que endurecer y ningún certificado que renovar, y la interfaz web abierta responde solo a tus propios dispositivos. Tailscale vive en la imagen de la interfaz web abierta en lugar de en el programa de Pulumi. La imagen añade Tailscale y un punto de entrada que levanta tailscaled en modo de espacio de usuario (un contenedor no tiene /dev/net/tun), se une a tu tailnet y ejecuta tailscale serve para exponer la interfaz web abierta en ella. serve publica sobre HTTPS solo a miembros autenticados de tu tailnet, donde su hermano tailscale funnel pondría el mismo puerto en internet público, la única cosa que este diseño evita.
FROM ghcr.io/open-webui/open-webui:main-slim RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \ && curl -fsSL https://tailscale.com/install.sh | sh COPY entrypoint.sh /usr/local/bin/ts-entrypoint ENTRYPOINT ["/usr/local/bin/ts-entrypoint"] #!/usr/bin/env bash set -e PORT="${PORT:-8080}" mkdir -p /app/backend/data/tailscale # Networking de espacio de usuario: un contenedor no tiene /dev/net/tun. Mantener el estado del nodo en el disco # para que sobreviva reinicios, y accept-dns=false para que el DNS de Render siga resolviendo el # nombre de host privado de Hermes. tailscaled --tun=userspace-networking --statedir=/app/backend/data/tailscale & n=0 until tailscale up --authkey="${TS_AUTHKEY}" --hostname="${TS_HOSTNAME}" --accept-dns=false; do n=$((n + 1)); [ "${n}" -ge 30 ] && { echo "unión a tailnet fallida; comprueba TS_AUTHKEY"; exit 1; } sleep 2 done tailscale serve --bg "${PORT}" # sirve la interfaz web abierta en tailnet sobre HTTPS cd /app/backend && exec bash start.sh La variable de entorno TS_AUTHKEY en el servicio, extraída de ESC, es lo único que contribuye Pulumi; todo lo demás es la imagen haciendo su propia red. Hay un requisito previo, el mismo que necesita cualquier nodo de Tailscale para servir: habilitar HTTPS para tu tailnet una vez en la consola de administración para que pueda obtener un certificado. Usa una clave de autenticación reutilizable, ya que el servicio se re-autentica en cada redepliegue, y el bucle de unión tiene un límite para que una clave muerta falle el despliegue en un minuto en lugar de quedársele colgado. Para generar la clave de autenticación reutilizable: Ve a la consola de administración de Tailscale. Haz clic en Generar clave de autenticación. Habilita Reutilizable, ya que el servicio se re-autentica en cada redepliegue. Copia la clave en tu entorno ESC como tailscaleAuthKey.
Desplegando el agente Esta es la parte que normalmente maneja un Makefile. Dos servicios privados, dos discos, cada secreto una variable de entorno encriptada, y ambas imágenes construidas y enviadas durante el mismo pulumi up que crea los servicios. No se ejecuta nada de CLI, aunque la construcción aún depende de un motor Docker local a través de buildx: la llamada docker está ausente, pero el demonio al que se comunica no lo está, así que pulumi up necesita que Docker esté funcionando. Crea un nuevo proyecto de Pulumi (elige el lenguaje que prefieras; el repositorio compañero tiene los tres): mkdir hermes-agent-pulumi && cd hermes-agent-pulumi pulumi new typescript Instala las dependencias y genera el SDK de Render: npm install @pulumi/docker-build @pulumi/random pulumi package add terraform-provider render-oss/render 1.8.0 Ambos servicios necesitan el plan estándar de 2 GB. La puerta de enlace y la interfaz web abierta agotan la memoria en los 512 MB del nivel de inicio y OOM al arrancar. Dos hechos hacen que esto sea limpio. Los valores de las variables de entorno de Render están encriptados en reposo, por lo que un secreto es solo una variable de entorno, sin un recurso separado. Y @pulumi/docker-build construye y envía una imagen a través de un buildx embebido durante pulumi up, así que la canalización de imágenes también es declarativa. Render luego recupera la imagen enviada por su digest.
El programa completo Hermes viene primero: un servicio privado de Render sin URL pública, un disco para su memoria, y cada secreto como una variable de entorno encriptada. La clave compartida se genera en el estado con random.RandomBytes, el reemplazo limpio para openssl rand -base64 32; el par de tokens de Modal y la clave de OpenRouter provienen de la configuración o Pulumi ESC. Render no tiene campo de puerto, así que Hermes se enlaza a 0.0.0.0:8642 y la interfaz web abierta lo alcanza en el slug de solo lectura del servicio. La interfaz web abierta es casi la imagen espejo, también privada, excepto que su imagen se une a tu tailnet (el punto de entrada de tailscaled de la sección anterior) y lleva los valores de env operativos que la mantienen ligera: sin modelos de incrustación locales, y ENABLE_PERSISTENT_CONFIG=false para que tu configuración siempre venza a una copia obsoleta en su base de datos. Un render.Provider lleva tu renderApiKey y ownerId a cada recurso de Render. Aquí está todo el programa, listo para copiar y pegar como tu index.ts (o __main__.py, o Pulumi.yaml): import * as pulumi from "@pulumi/pulumi"; import * as dockerbuild from "@pulumi/docker-build"; import * as random from "@pulumi/random"; // SDK local generado por: pulumi package add terraform-provider render-oss/render 1.8.0 // (no se publica @pulumi/render; el SDK está puenteado del proveedor oficial de Render). import * as render from "@pulumi/render"; const cfg = new pulumi.Config(); // --------------------------------------------------------------------------- // Configuración. Valores no secretos a través de `pulumi config set`, secretos a través de // `pulumi config set --secret` o un entorno ESC de Pulumi (ver README). // El proveedor de Render lee renderApiKey/renderOwnerId (o RENDER_API_KEY/RENDER_OWNER_ID). // --------------------------------------------------------------------------- const region = cfg.get("renderRegion") ?? "oregon"; const modelDefault = cfg.get("modelDefault") ?? "gpt-5.5"; // Registro de contenedores al que se envían las imágenes y de donde Render tira (GHCR aquí). const ghcrUser = cfg.require("ghcrUser"); const ghcrToken = cfg.requireSecret("ghcrToken"); // PAT de GHCR con write:packages // Secretos a largo plazo proporcionados por ti (mintados por el panel). Mantener en la configuración o ESC. const modalTokenId = cfg.requireSecret("modalTokenId"); const modalTokenSecret = cfg.requireSecret("modalTokenSecret"); const openrouterApiKey = cfg.requireSecret("openrouterApiKey"); // Tailscale: la única "puerta de enlace". La interfaz web abierta se une a tu tailnet y es accesible // en https://<hostname>.<tailnet>.ts.net solo por tus dispositivos — nunca pública. // Habilitar HTTPS una vez en la consola de administración de Tailscale para que pueda obtener un certificado. const tailscaleAuthKey = cfg.requireSecret("tailscaleAuthKey"); const tailnetDnsName = cfg.require("tailnetDnsName"); // por ejemplo, tailc6fb4e.ts.net const webuiHostname = "open-webui"; const webuiTailnetUrl = `https://${webuiHostname}.${tailnetDnsName}`; // Autenticación del proveedor de Render (desde ESC: pulumiConfig.renderApiKey / pulumiConfig.renderOwnerId). const renderProvider = new render.Provider("render", { apiKey: cfg.requireSecret("renderApiKey"), ownerId: cfg.require("renderOwnerId"), }); // --------------------------------------------------------------------------- // Secretos generados en el estado (el reemplazo en IaC para `openssl rand -base64 32`). // El portador compartido autentica Interfaz web abierta -> Hermes; aterriza en ambos servicios. // --------------------------------------------------------------------------- const sharedKey = new random.RandomBytes("hermes-shared-key", { length: 32 }); const webuiSecretKey = new random.RandomBytes("webui-secret-key", { length: 32 }); // --------------------------------------------------------------------------- // Construir y enviar ambas imágenes durante `pulumi up` (sin docker CLI; el proveedor // embebe buildx). Render luego las toma por su referencia de digestión fijada. // --------------------------------------------------------------------------- const registryAuth = [{ address: "ghcr.io", username: ghcrUser, password: ghcrToken }]; const hermesImage = new dockerbuild.Image("hermes-image", { context: { location: "./hermes" }, dockerfile: { location: "./hermes/Dockerfile" }, platforms: ["linux/amd64"], tags: [pulumi.interpolate`ghcr.io/${ghcrUser}/hermes-agent:latest`], push: true, registries: registryAuth, }); const webuiImage = new dockerbuild.Image("webui-image", { context: { location: "./openwebui" }, dockerfile: { location: "./openwebui/Dockerfile" }, platforms: ["linux/amd64"], tags: [pulumi.interpolate`ghcr.io/${ghcrUser}/open-webui:latest`], push: true, registries: registryAuth, }); // Credencial que Render usa para tirar de las imágenes privadas de GHCR. const ghcrCredential = new render.RegistryCredential("ghcr", { name: "ghcr", registry: "GITHUB", username: ghcrUser, authToken: ghcrToken, }, { provider: renderProvider }); // --------------------------------------------------------------------------- // Hermes: un servicio PRIVADO de Render. Sin URL pública; accesible solo a través de la // red privada de Render. Un disco mantiene su memoria y espacio de trabajo. Los valores de env // están encriptados en reposo, que es cómo los secretos viajan en Render (sin bandera separada). // --------------------------------------------------------------------------- const hermes = new render.PrivateService("hermes-agent", { name: "hermes-agent", plan: "standard", // 2 GB; la puerta de enlace OOMs en el estándar de 512 MB región: region, runtimeSource: { // Render quiere el repositorio base en imageUrl; la etiqueta/digest va en su propio campo. image: { imageUrl: pulumi.interpolate`ghcr.io/${ghcrUser}/hermes-agent`, digest: hermesImage.digest, registryCredentialId: ghcrCredential.id, }, }, disk: { name: "hermes-data", mountPath: "/opt/data", sizeGb: 5 }, envVars: { PORT: { value: "8642" }, API_SERVER_ENABLED: { value: "true" }, API_SERVER_HOST: { value: "0.0.0.0" }, API_SERVER_PORT: { value: "8642" }, API_SERVER_MODEL_NAME: { value: modelDefault }, // Aprobar automáticamente las ganchos del shell/terminal del agente (sin TTY en un contenedor), // para que pueda ejecutar código en el sandbox de Modal sin un aviso interactivo. HERMES_ACCEPT_HOOKS: { value: "1" }, // Secretos (encriptados en reposo en Render): API_SERVER_KEY: { value: sharedKey.base64 }, OPENROUTER_API_KEY: { value: openrouterApiKey }, // Modal es una credencial de tiempo de ejecución, no un recurso provisionado: Hermes crea // sandboxes a tiempo de ejecución con este par de tokens. Sin proveedor de Modal, sin CLI de Modal. MODAL_TOKEN_ID: { value: modalTokenId }, MODAL_TOKEN_SECRET: { value: modalTokenSecret }, }, }, { provider: renderProvider }); // La interfaz web abierta alcanza a Hermes en su nombre interno asignado por Render (el slug de solo lectura) // no el nombre base del servicio. Misma región + mismo espacio de trabajo comparten la red. const hermesApiBaseUrl = pulumi.interpolate`http://${hermes.slug}:8642/v1`; // --------------------------------------------------------------------------- // Interfaz web abierta: también un servicio PRIVADO de Render, pero el contenedor se une a tu tailnet // (tailscaled en espacio de usuario + `tailscale serve`), así que lo alcanza en // https://open-webui.<tailnet>.ts.net desde tus dispositivos. Nunca público; la puerta // es la membresía en tailnet. Aún alcanza a Hermes a través de la red privada de Render. // --------------------------------------------------------------------------- const webui = new render.PrivateService("open-webui", { name: "open-webui", plan: "standard", // 2 GB; Open WebUI OOMs en el estándar de 512 MB región: region, // debe coincidir con Hermes para la red privada runtimeSource: { image: { imageUrl: pulumi.interpolate`ghcr.io/${ghcrUser}/open-webui`, digest: webuiImage.digest, registryCredentialId: ghcrCredential.id, }, }, disk: { name: "openwebui-data", mountPath: "/app/backend/data", sizeGb: 5 }, envVars: { PORT: { value: "8080" }, // tailscale serve apunta a esto; Open WebUI lo enlaza OPENAI_API_BASE_URL: { value: hermesApiBaseUrl }, ENABLE_OPENAI_API: { value: "true" }, ENABLE_OLLAMA_API: { value: "false" }, DEFAULT_MODELS: { value: modelDefault }, WEBUI_AUTH: { value: "true" }, WEBUI_URL: { value: webuiTailnetUrl }, // Alcanzado solo a través de la tailnet, por lo que el primer registro es el administrador. ENABLE_LOGIN_FORM: { value: "true" }, ENABLE_SIGNUP: { value: "true" }, DEFAULT_USER_ROLE: { value: "admin" }, // Siempre aplicar la configuración de env; de lo contrario, la interfaz web abierta persiste la configuración del primer uso // en su base de datos e ignora cambios posteriores. ENABLE_PERSISTENT_CONFIG: { value: "false" }, // No cargar modelos de incrustación locales/RAG (un gran ahorro de memoria; el agente es el cerebro). RAG_EMBEDDING_ENGINE: { value: "openai" }, BYPASS_EMBEDDING_AND_RETRIEVAL: { value: "true" }, OFFLINE_MODE: { value: "true" }, HF_HUB_OFFLINE: { value: "1" }, // Secretos (encriptados en reposo): OPENAI_API_KEY: { value: sharedKey.base64 }, // portador compartido para llamar a Hermes WEBUI_SECRET_KEY: { value: webuiSecretKey.base64 }, // Tailscale une este contenedor a tu tailnet y sirve la interfaz web abierta en él. TS_AUTHKEY: { value: tailscaleAuthKey }, TS_HOSTNAME: { value: webuiHostname }, }, }, { dependsOn: hermes, provider: renderProvider });
export const hermesInternalUrl = hermesApiBaseUrl; // Accede a esto desde cualquier dispositivo en tu tailnet: export const openWebUiTailnetUrl = webuiTailnetUrl; import pulumi import pulumi_docker_build as docker_build import pulumi_random as random # SDK local generado por: pulumi package add terraform-provider render-oss/render 1.8.0 # (no se publica pulumi_render; el SDK está puenteado del proveedor oficial de Render). import pulumi_render as render cfg = pulumi.Config() # --------------------------------------------------------------------------- # Configuración. Valores no secretos a través de `pulumi config set`, secretos a través de # `pulumi config set --secret` o un entorno ESC de Pulumi (ver README). # El proveedor de Render lee renderApiKey/renderOwnerId (o RENDER_API_KEY/RENDER_OWNER_ID). # --------------------------------------------------------------------------- region = cfg.get("renderRegion") or "oregon" model_default = cfg.get("modelDefault") or "gpt-5.5" # Registro de contenedores al que se envían las imágenes y de donde Render tira (GHCR aquí). ghcr_user = cfg.require("ghcrUser") ghcr_token = cfg.require_secret("ghcrToken") # PAT de GHCR con write:packages # Secretos a largo plazo proporcionados por ti (mintados por el panel). Mantener en la configuración o ESC. modal_token_id = cfg.require_secret("modalTokenId") modal_token_secret = cfg.require_secret("modalTokenSecret") openrouter_api_key = cfg.require_secret("openrouterApiKey") # Tailscale: la única "puerta de enlace". La interfaz web abierta se une a tu tailnet y es accesible # en https://<hostname>.<tailnet>.ts.net solo por tus dispositivos — nunca pública. # Habilitar HTTPS una vez en la consola de administración de Tailscale para que `tailscale serve` pueda obtener un cert. tailscale_auth_key = cfg.require_secret("tailscaleAuthKey") tailnet_dns_name = cfg.require("tailnetDnsName") # por ejemplo, tailc6fb4e.ts.net webui_hostname = "open-webui" webui_tailnet_url = f"https://{webui_hostname}.{tailnet_dns_name}" # Autenticación del proveedor de Render (desde ESC: pulumiConfig.renderApiKey / pulumiConfig.renderOwnerId). render_provider = render.Provider( "render", api_key=cfg.require_secret("renderApiKey"), owner_id=cfg.require("renderOwnerId"), ) # --------------------------------------------------------------------------- # Secretos generados en el estado (el reemplazo en IaC para `openssl rand -base64 32`). # El portador compartido autentica Interfaz web abierta -> Hermes; aterriza en ambos servicios. # --------------------------------------------------------------------------- shared_key = random.RandomBytes("hermes-shared-key", length=32) webui_secret_key = random.RandomBytes("webui-secret-key", length=32) # --------------------------------------------------------------------------- # Construir y enviar ambas imágenes durante `pulumi up` (sin docker CLI; el proveedor # embebe buildx). Render luego las toma por su referencia de digestión fijada. # --------------------------------------------------------------------------- registry_auth = [{"address": "ghcr.io", "username": ghcr_user, "password": ghcr_token}] hermes_image = docker_build.Image( "hermes-image", context={"location": "./hermes"}, dockerfile={"location": "./hermes/Dockerfile"}, platforms=["linux/amd64"], tags=[pulumi.Output.concat("ghcr.io/", ghcr_user, "/hermes-agent:latest")], push=True, registries=registry_auth, ) webui_image = docker_build.Image( "webui-image", context={"location": "./openwebui"}, dockerfile={"location": "./openwebui/Dockerfile"}, platforms=["linux/amd64"], tags=[pulumi.Output.concat("ghcr.io/", ghcr_user, "/open-webui:latest")], push=True, registries=registry_auth, ) # Credencial Render usa para tirar de las imágenes privadas de GHCR. ghcr_credential = render.RegistryCredential( "ghcr", name="ghcr", registry="GITHUB", username=ghcr_user, auth_token=ghcr_token, opts=pulumi.ResourceOptions(provider=render_provider), ) # --------------------------------------------------------------------------- # Hermes: un servicio PRIVADO de Render. Sin URL pública; accesible solo a través de la # red privada de Render. Un disco mantiene su memoria y espacio de trabajo. Los valores de env # están encriptados en reposo, que es cómo los secretos viajan en Render (sin bandera separada). # --------------------------------------------------------------------------- hermes = render.PrivateService( "hermes-agent", name="hermes-agent", plan="standard", # 2 GB; la puerta de enlace OOMs en el estándar de 512 MB region=region, runtime_source={ "image": { "image_url": pulumi.Output.concat("ghcr.io/", ghcr_user, "/hermes-agent"), "digest": hermes_image.digest, "registry_credential_id": ghcr_credential.id, }, }, disk={"name": "hermes-data", "mount_path": "/opt/data", "size_gb": 5}, env_vars={ "PORT": {"value": "8642"}, "API_SERVER_ENABLED": {"value": "true"}, "API_SERVER_HOST": {"value": "0.0.0.0"}, "API_SERVER_PORT": {"value": "8642"}, "API_SERVER_MODEL_NAME": {"value": model_default}, # Auto-approve the agent's shell/terminal hooks (no TTY in a container). "HERMES_ACCEPT_HOOKS": {"value": "1"}, # Secretos (encriptados en reposo en Render): "API_SERVER_KEY": {"value": shared_key.base64}, "OPENROUTER_API_KEY": {"value": openrouter_api_key}, # Modal es una credencial de tiempo de ejecución: Hermes crea sandboxes en tiempo de ejecución con este par. "MODAL_TOKEN_ID": {"value": modal_token_id}, "MODAL_TOKEN_SECRET": {"value": modal_token_secret}, }, opts=pulumi.ResourceOptions(provider=render_provider), ) # Interfaz web abierta alcanza a Hermes en su nombre interno asignado por Render (el slug de solo lectura). hermes_api_base_url = pulumi.Output.concat("http://", hermes.slug, ":8642/v1") # --------------------------------------------------------------------------- # Interfaz web abierta: también un servicio PRIVADO de Render, pero el contenedor se une a tu tailnet # (tailscaled en espacio de usuario + `tailscale serve`), así que lo alcanza en # https://open-web-ui.<tailnet>.ts.net desde tus dispositivos. Nunca público. # --------------------------------------------------------------------------- webui = render.PrivateService( "open-webui", name="open-webui", plan="standard", # 2 GB; Open WebUI OOMs en el estándar de 512 MB region=region, # debe coincidir con Hermes para la red privada runtime_source={ "image": { "image_url": pulumi.Output.concat("ghcr.io/", ghcr_user, "/open-webui"), "digest": webui_image.digest, "registry_credential_id": ghcr_credential.id, }, }, disk={"name": "openwebui-data", "mount_path": "/app/backend/data", "size_gb": 5}, env_vars={ "PORT": {"value": "8080"}, # tailscale serve apunta a esto; Open WebUI lo enlaza "OPENAI_API_BASE_URL": {"value": hermes_api_base_url}, "ENABLE_OPENAI_API": {"value": "true"}, "ENABLE_OLLAMA_API": {"value": "false"}, "DEFAULT_MODELS": {"value": model_default}, "WEBUI_AUTH": {"value": "true"}, "WEBUI_URL": {"value": webui_tailnet_url}, # Alcanzado solo a través de la tailnet, por lo que el primer registro es el administrador. "ENABLE_LOGIN_FORM": {"value": "true"}, "ENABLE_SIGNUP": {"value": "true"}, "DEFAULT_USER_ROLE": {"value": "admin"}, # Siempre aplicar la configuración de env; de lo contrario, la interfaz web abierta persiste la configuración del primer uso. "ENABLE_PERSISTENT_CONFIG": {"value": "false"}, # No cargar modelos de incrustación locales/RAG (un gran ahorro de memoria; el agente es el cerebro). "RAG_EMBEDDING_ENGINE": {"value": "openai"}, "BYPASS_EMBEDDING_AND_RETRIEVAL": {"value": "true"}, "OFFLINE_MODE": {"value": "true"}, "HF_HUB_OFFLINE": {"value": "1"}, # Secretos (encriptados en reposo): "OPENAI_API_KEY": {"value": shared_key.base64}, "WEBUI_SECRET_KEY": {"value": webui_secret_key.base64}, # Tailscale une este contenedor a tu tailnet y sirve la interfaz web abierta en él. "TS_AUTHKEY": {"value": tailscale_auth_key}, "TS_HOSTNAME": {"value": webui_hostname}, }, opts=pulumi.ResourceOptions(depends_on=[hermes], provider=render_provider), ) pulumi.export("hermesInternalUrl", hermes_api_base_url) # Abre esto desde cualquier dispositivo en tu tailnet: pulumi.export("openWebUiTailnetUrl", webui_tailnet_url) nombre: hermes-agent-pulumi runtime: yaml descripción: Desplegar un agente de Hermes (Render + Modal + Tailscale) como un solo programa declarativo de Pulumi. # El SDK de Render está puenteado del proveedor oficial de Terraform. Ejecuta una vez: # pulumi package add terraform-provider render-oss/render 1.8.0 # Luego `pulumi install` lo regenera en un nuevo checkout. paquetes: render: fuente: terraform-provider versión: 1.1.4 parámetros: - render-oss/render - 1.8.0 config: renderRegion: tipo: string valor predeterminado: oregón modelDefault: tipo: string valor predeterminado: gpt-5.5 ghcrUser: tipo: string ghcrToken: tipo: string secreto: true modalTokenId: tipo: string secreto: true modalTokenSecret: tipo: string secreto: true openrouterApiKey: tipo: string secreto: true tailscaleAuthKey: tipo: string secreto: true tailnetDnsName: tipo: string renderApiKey: tipo: string secreto: true renderOwnerId: tipo: string variables: registryAuth: - dirección: ghcr.io nombre de usuario: ${ghcrUser} contraseña: ${ghcrToken} # La interfaz web abierta alcanza a Hermes en su slug privado; tú alcanzas la interfaz web abierta en la tailnet. hermesApiBaseUrl: http://${hermes-agent.slug}:8642/v1 webuiTailnetUrl: https://open-webui.${tailnetDnsName} recursos: # Autenticación del proveedor de Render (desde la configuración o ESC). render: tipo: pulumi:providers:render propiedades: apiKey: ${renderApiKey} ownerId: ${renderOwnerId} # Secretos generados en el estado: el reemplazo para `openssl rand -base64 32`. hermes-shared-key: tipo: random:RandomBytes propiedades: length: 32 webui-secret-key: tipo: random:RandomBytes propiedades: length: 32 # Construir + enviar ambas imágenes durante `pulumi up` (sin docker CLI). hermes-image: tipo: docker-build:Image propiedades: context: location: ./hermes dockerfile: location: ./hermes/Dockerfile platforms: [linux/amd64] tags: - ghcr.io/${ghcrUser}/hermes-agent:latest push: true registries: ${registryAuth} webui-image: tipo: docker-build:Image propiedades: context: location: ./openwebui dockerfile: location: ./openwebui/Dockerfile platforms: [linux/amd64] tags: - ghcr.io/${ghcrUser}/open-webui:latest push: true registries: ${registryAuth} # Credencial que Render usa para tirar de las imágenes privadas de GHCR. ghcr: tipo: render:RegistryCredential propiedades: nombre: ghcr registro: GITHUB nombre de usuario: ${ghcrUser} authToken: ${ghcrToken} opciones: provider: ${render} # Hermes: un servicio PRIVADO de Render. Sin URL pública; las variables de entorno encriptadas son el mecanismo secreto. hermes-agent: tipo: render:PrivateService propiedades: nombre: hermes-agent plan: estándar # 2 GB; la puerta de enlace OOMs en el estándar de 512 MB región: ${renderRegion} runtimeSource: imagen: imageUrl: ghcr.io/${ghcrUser}/hermes-agent digest: ${hermes-image.digest} registryCredentialId: ${ghcr.id} disco: nombre: hermes-data mountPath: /opt/data sizeGb: 5 varsDeEnv: PORT: { value: "8642" } API_SERVER_ENABLED: { value: "true" } API_SERVER_HOST: { value: "0.0.0.0" } API_SERVER_PORT: { value: "8642" } API_SERVER_MODEL_NAME: { value: "${modelDefault}" } HERMES_ACCEPT_HOOKS: { value: "1" } API_SERVER_KEY: { value: "${hermes-shared-key.base64}" } OPENROUTER_API_KEY: { value: "${openrouterApiKey}" } MODAL_TOKEN_ID: { value: "${modalTokenId}" } MODAL_TOKEN_SECRET: { value: "${modalTokenSecret}" } opciones: provider: ${render} # Interfaz web abierta: también un servicio PRIVADO de Render, unida a tu tailnet por su imagen. open-webui: tipo: render:PrivateService propiedades: nombre: open-webui plan: estándar # 2 GB; Open WebUI OOMs en el estándar de 512 MB región: ${renderRegion} # debe coincidir con Hermes para la red privada runtimeSource: imagen: imageUrl: ghcr.io/${ghcrUser}/open-webui digest: ${webui-image.digest} registryCredentialId: ${ghcr.id} disco: nombre: openwebui-data mountPath: /app/backend/data sizeGb: 5 varsDeEnv: PORT: { value: "8080" } OPENAI_API_BASE_URL: { value: "${hermesApiBaseUrl}" } ENABLE_OPENAI_API: { value: "true" } ENABLE_OLLAMA_API: { value: "false" } DEFAULT_MODELS: { value: "${modelDefault}" } WEBUI_AUTH: { value: "true" } WEBUI_URL: { value: "${webuiTailnetUrl}" } ENABLE_LOGIN_FORM: { value: "true" } ENABLE_SIGNUP: { value: "true" } DEFAULT_USER_ROLE: { value: "admin" } ENABLE_PERSISTENT_CONFIG: { value: "false" } RAG_EMBEDDING_ENGINE: { value: "openai" } BYPASS_EMBEDDING_AND_RETRIEVAL: { value: "true" } OFFLINE_MODE: { value: "true" } HF_HUB_OFFLINE: { value: "1" } OPENAI_API_KEY: { value: "${hermes-shared-key.base64}" } WEBUI_SECRET_KEY: { value: "${webui-secret-key.base64}" } TS_AUTHKEY: { value: "${tailscaleAuthKey}" } TS_HOSTNAME: { value: "open-webui" } opciones: provider: ${render} dependsOn: - ${hermes-agent} salidas: hermesInternalUrl: ${hermesApiBaseUrl} # Abre esto desde cualquier dispositivo en tu tailnet: openWebUiTailnetUrl: ${webuiTailnetUrl} Ninguna de las cajas está publicada en internet. El mismo código, más los Dockerfiles y el punto de entrada de Tailscale, está en el repositorio compañero: github.com/dirien/hermes-agent-pulumi
Costo Dos detalles configuran la factura. El host comienza en el espacio de trabajo Hobby gratuito de Render, pero un servicio privado con un disco persistente utiliza computación de pago, así que necesitas una tarjeta registrada en cuanto los añadas. No hay una versión gratuita de esta arquitectura. Ambos servicios también necesitan el plan estándar de 2 GB, ya que la puerta de enlace y la interfaz web abierta agotan los 512 MB del nivel de inicio. Puerta de enlace de Hermes Interfaz web abierta Plan Standard (2 GB) Standard (2 GB) Computación ~$25/mes ~$25/mes Disco 5 GB 5 GB URL pública ninguna ninguna (solo tailnet) Eso coloca el costo del host alrededor de $50/mes, en línea con lo que un configuración administrada comparable cuesta. Modal y OpenRouter facturan por uso adicional, así que el total se ajusta a lo duro que trabaja el agente en lugar de un número fijo. Tailscale es gratuito para una tailnet personal, así que la puerta de enlace privada no añade nada a eso. El disco persistente fija cada servicio a una única instancia, así que no hay escalado automático, y un redepliegue causa unos segundos de conectividad mientras la instancia antigua deja el disco. Para un agente de un solo usuario, eso es un buen intercambio, y es por eso que "levantarlo en una segunda región" es un movimiento de copiar la configuración y no de failover en vivo.
El panel de actividad de OpenRouter: 56 solicitudes y 738K tokens de uso de gpt-5.5, todo enrutado a través de una clave. Ejecutando el despliegue Con tu entorno ESC referenciado en Pulumi.dev.yaml, despliega con: pulumi up Esto genera los recursos del SDK de Render, construye y envía ambas imágenes a través de buildx, y crea los dos servicios privados con sus discos y variables de entorno encriptadas, todo en una sola ejecución, sin CLI de Docker y sin importación secreta oculta en un script. Cuando termine, ambos servicios aparecerán en el panel de Render sin URL pública. Los servicios privados hermes-agent y open-webui en el panel de Render, ambos desplegados sin URL pública.
Accediendo al agente Dale unos minutos después de que pulumi up termine. Las imágenes aún tienen que ser recuperadas y los contenedores iniciados, y la interfaz web abierta tiene que unirse a tu tailnet antes de que la URL se resuelva. Actualiza hasta que se cargue. La interfaz web abierta es accesible solo desde un dispositivo en tu tailnet, en https://open-webui.<tu-tailnet>.ts.net, sin dominio que comprar, sin DNS que apuntar, y sin certificado que gestionar. Ábrelo, regístrate para que la primera cuenta se convierta en el administrador, y pídele al agente que ejecute un comando en Python. Debería iniciar un sandbox de Modal y devolver el resultado, sin que nada esté expuesto a internet público en ningún momento. La interfaz web abierta en la URL de tailnet: el agente gpt-5.5 ejecuta sum(i*i for i in range(1, 1001)) en un sandbox de Modal y retorna 333833500. Cuando el agente ejecuta ese código, Modal construye un sandbox desechable específicamente para ello. No hay recurso de Modal que declarar, no hay CLI de Modal que llamar, y no hay proveedor de Modal que puente. El servicio de Hermes lleva MODAL_TOKEN_ID y MODAL_TOKEN_SECRET como dos variables de entorno encriptadas, y esa es toda la huella. El panel de Modal: los sandboxes por tarea que el agente de Hermes levantó en tiempo de ejecución para ejecutar el código.
Verificando el despliegue (opcional) Si la UI de chat no se carga, dale a los servicios unos minutos para completar su primer arranque, luego comprueba dos cosas: los registros del panel de Render para los servicios hermes-agent y open-webui, para confirmar que ambos iniciaron y recuperaron sus imágenes. Consola de administración de Tailscale (Máquinas), para confirmar que el nodo de open-webui se unió a tu tailnet. Si no lo hizo, es probable que el TS_AUTHKEY haya expirado o no sea reutilizable. Consideraciones de seguridad Los servidores de IA auto-hospedados se encuentran rápido: escáneres como Shodan y Censys enumeran uno expuesto recientemente en horas a días, y los que pueden ejecutar código están ya bajo ataque activo. Ejecutar un agente siempre activo en una máquina que también usas para todo lo demás invita a inyecciones rápidas además de eso. La respuesta aquí es mantener el agente no enrutado desde internet y aislar el código que ejecuta. Eso derrota al escáner: nadie encuentra un puerto abierto para atacar. Hacerlo es menos efectivo contra el agente usando el sistema en tu contra desde adentro, lo que es el problema más difícil. Un detalle hace que ese aislamiento sea esencial. En un contenedor sin cabeza no hay nadie en el terminal para aprobar nada, y lo mismo es cierto para sus cron y ejecuciones de CI, así que el despliegue ejecuta Hermes con HERMES_ACCEPT_HOOKS=1 para aceptar automáticamente los ganchos de shell que de otro modo detendría. El resultado práctico es que el código del agente se ejecuta sin un humano en el circuito, que es exactamente por qué el sandbox de Modal tiene que llevar el peso: cuando el modelo escribe un comando erróneo, el contenedor desechable en el que se ejecuta es lo que se interpone entre ese comando y las credenciales de la puerta de enlace, no un aviso de confirmación.
Preocupación típico auto-hospedado Este despliegue Alcance de puerta pública puerto público, o detrás de un inicio de sesión Servicio privado, sin URL pública Alcance del UI de chat Nombre de host público + auth Solo tailnet (tailscale serve) Ejecución de código Se ejecuta junto a la puerta de enlace Sandbox aislado de Modal, destruido después Aprobación del comando Un humano aprueba comandos peligrosos Sin cabeza: el aislamiento reemplaza el aviso Secretos en tránsito Expuestos si se accede a través de HTTP Variables de entorno encriptadas; acceso encriptado en tailnet Esto es privado, lo que no es lo mismo que completamente confiable. Algunos lugares donde el límite aún tiene fugas: El portador compartido nunca rota. Hermes no es enrutable desde internet, pero está completamente abierto a la interfaz web abierta, y los dos comparten una clave portadora estática. Quien posea el frontend posee la puerta de enlace detrás de él. Rótala periódicamente. La política de tailnet es click-ops. Las ACL, las etiquetas del nodo, y el interruptor de HTTPS de una sola vez viven en la consola de administración de Tailscale, no en pulumi up, y la clave reutilizable no tiene etiqueta que limite su radio de acción. El proveedor de Tailscale puede gestionar esa política declarativamente si deseas que esté en código. El registro predeterminado es el administrador. Bien en una tailnet de un solo usuario, lo primero que hay que cerrar (ENABLE_SIGNUP, DEFAULT_USER_ROLE) una vez que más de una persona pueda alcanzar la URL. Cada cliente de mensajería es otro punto de entrada. Cada plataforma que conectes añade otra vía, por lo que la tailnet es la base para asegurar esto, no su extensión completa. Un agente comprometido aún puede salir. El núcleo privado detiene el descubrimiento externo, pero no contiene el agente en sí. La puerta de enlace mantiene los tokens de Modal y OpenRouter como variables de entorno y puede llamar libremente, por lo que un agente inyectado por un aviso podría leer su propio entorno y enviarles a algún lugar; el sandbox de Modal aisla el código que ejecuta el agente, no las credenciales que sostiene. Limita y rota la clave de OpenRouter, mantén el token de Modal fuera de la puerta de enlace, y permite tráfico saliente, lo que en Render significa un sidecar de proxy hacia adelante (Render te da IPs salientes dedicadas y reglas de entrada, pero ningún filtro de salida de destino). Mis recomendaciones: Mantén ambos servicios privados; nunca des a Hermes una URL pública Ejecuta código en sandboxes de Modal, ya que HERMES_ACCEPT_HOOKS=1 significa que ningún humano aprueba comandos Rótala la clave portadora compartida y la clave de autenticación de Tailscale periódicamente Limita y rota la clave de OpenRouter; una credencial filtrada es el modo de fallo más probable que un puerto abierto Usa Pulumi ESC para secretos en lugar de codificarlos Endurece el registro de Open WebUI una vez que más de una persona pueda alcanzarlo Gestiona las ACLs y etiquetas de Tailscale deliberadamente, en la consola de administración o con el proveedor de Tailscale.
¿Qué sigue? El despliegue es la base sobre la que se construye el agente. Un agente privado se vuelve útil cuando recuerda el contexto anterior, ejecuta código sin un humano en el terminal y comienza el contacto por su cuenta. El disco es lo que hace que esto persista: su memoria y su programación viven en ese disco, por lo que sobreviven a cada reinicio. Se programa a sí mismo. Hermes envía un cron de primera clase que manejas en lenguaje simple, ya sea preguntando al agente en la ventana de chat o a través de su CLI. La programación toma intervalos, cron de cinco campos, o uno solo: hermes cron create "cada 1h" \ "Raspar <listing-url> por el precio, compararlo con seen.json en el sandbox, mensaje solo si disminuyó, y de lo contrario responde exactamente [SILENCIO]." \ --deliver telegram La respuesta [SILENCIO] suprime la entrega cuando nada cambió, así que el vigilante solo se manifiesta cuando hay algo que decir. Las ejecuciones programadas omiten la memoria por defecto, así que el sondeo no contamina lo que el agente ha aprendido. Se conecta donde trabajas, con el mismo patrón secreto. Los clientes de mensajería se conectan a la misma puerta de enlace, y sus tokens viajan como variables de entorno encriptadas en el servicio de Hermes, la misma forma que usaste para el par de Modal: // junto a MODAL_TOKEN_ID en el servicio de hermes: TELEGRAM_BOT_TOKEN: { value: cfg.requireSecret("telegramBotToken") }, más un bloque de plataforma que añades a config.yaml (la imagen solo envía el modelo y los bloques de terminal; Hermes apoya las plataformas de mensajería en la parte superior, según su documentación): gateway: platforms: telegram: require_mention: true Con esas dos piezas, algunas cosas son genuinamente posibles hoy: Un vigilante de precios o tiempo de actividad que solo se manifieste cuando algo cambie, usando el patrón [SILENCIO] y la deduplicación de sandbox persistente anterior. Un informe diario que clasifica y deduplica tus feeds y entrega los elementos principales cada mañana, el bot de informe diario documentado. Si deseas que se adapte a tu gusto con el tiempo, ese es el trabajo del proveedor de memoria opcional de Honcho; lee tus preferencias de la conversación misma. La memoria incorporada no tiene ninguna versión de eso. Un compañero de revisión de PR que revisa solicitudes de extracción abiertas contra las convenciones de AGENTES.md de tu repositorio y guarda patrones recurrentes como una habilidad, por lo que la quinta revisión necesita menos mano que la primera. Algunos límites merecen ser declarados claramente, ya que las demostraciones tienden a exagerar las capacidades. Hermes no envía Gmail, Calendar o integraciones de mercado por defecto; esas se conectan como servidores MCP o habilidades, lo cual es más trabajo de lo que un video de lanzamiento implica. Curaduría su propia memoria y mejora sus propias habilidades, pero no vuelve a entrenar el modelo subyacente ni cambia autónomamente su estrategia de alto nivel, así que "auto-aprendizaje" aquí significa lo que sea que escriba en memoria y habilidades cada vez.
Conclusión Desplegar un agente de Hermes con infraestructura como código significa que puedes reproducir la configuración en cualquier momento, controlarla con versiones y desmontarla con un solo pulumi destroy. Mantener ambos servicios privados, sin URL pública y sin puertos expuestos, significa que el núcleo que ejecuta código nunca se encuentra en internet para que alguien lo encuentre. La parte que vale la pena llevar al siguiente servicio que despliegues: no cada capa tiene un proveedor, y está bien. La tarea es distinguir un recurso real de una dependencia de tiempo de ejecución que usa un disfraz. Render y las imágenes son recursos que declaras; Modal y Tailscale son credenciales que entregas. Traza ese mapa primero y el programa sigue a partir de ello. Para generar ese mapa en lugar de escribirlo a mano, Pulumi Neo puede tomar una arquitectura objetivo como esta y producir un primer borrador del programa. La parte del agente de este cambio se cubre en Cómo Ha Cambiado la Construcción de Agentes de IA en 2026 y la parte de la plataforma en La Dispersión de Agentes Está Aquí. Tu Plataforma IaC Es la Respuesta. Si te encuentras con problemas o tienes preguntas, visita el Slack de la comunidad de Pulumi o las discusiones de GitHub. ¿Nuevo en Pulumi? Empieza aquí.
Enlace externo a pulumi.com

