Un plugin ahora puede pedirle a Hermes que controle su propio servidor MCP


Instalas un plugin de la comunidad desde el catálogo. Registra un servidor MCP sin hacer ruido, y ese servidor puede hacer pedidos, mover dinero o enviar mensajes en tu nombre. Hermes no te pregunta ni una sola vez. Lo único que se interpone entre un modelo con prompt injection y la acción es una frase en el texto de la skill del plugin que le pide al modelo que consulte contigo primero — y que el modelo la obedezca es cuestión de humor, no de código.

Eso no es culpa del plugin. Es un agujero en el formato: Agent Plugins v1 no define ningún campo de confianza, así que todos los servidores MCP cargados desde un paquete portable arrancaban con el trust: full por defecto. El PR #134524, fusionado el 7 de octubre de 2026, lo cierra: un paquete puede pedirle a Hermes que controle su propio servidor, y no puede pedir nada más amplio.

De dónde venía el agujero

Hermes admite dos formas de plugin. Los plugins nativos son un plugin.yaml más una función register(ctx), integrados en el núcleo. Los paquetes portables siguen el formato Agent Plugins v1.0.0 y tienen este aspecto:

my-portable-plugin/
├── plugin.json
├── skills/
│   └── summarize/
│       ├── SKILL.md
│       └── references/
└── mcp.json

plugin.json es el manifiesto, mcp.json declara los servidores MCP a los que se conecta el paquete, y skills/ contiene las skills que trae. Un paquete recién instalado permanece deshabilitado hasta que ejecutas hermes plugins enable <name> — buena higiene, pero solo responde a “¿debería cargarse o no?”, no a “¿quién aprieta el freno mientras se ejecuta?”.

El problema real era el lector. El mcp.json portable de Hermes solo acepta type, url y headers (mira hermes_cli/agent_plugins.py), mientras que el nivel de confianza vive en la config nativa, en mcp_servers.<name>.trust dentro de config.yaml. Como la especificación no tenía ese campo, el lector nunca traducía ninguno, y todos los servidores de un paquete salían con el full por defecto.

Lo que puso esto en la hoja de ruta fue un caso concreto. Un barrido del catálogo destapó un plugin de trading cuyo servidor alojado pausa cada operación en la cuenta de Hyperliquid del usuario hasta que se llama a resolve_approval. Como dice el PR, lo único que se interponía entre un modelo con prompt injection y una operación aprobada era el texto de la skill. Hermes, por sí mismo, nunca preguntaba.

Cómo pide un paquete que lo controlen

plugin.json ya lleva un bloque de extensión específico de Hermes — el mismo que contiene app, requires y liveness. Ahora acepta trust:

{
  "extensions": {
    "com.nousresearch.hermes": {
      "servers": {
        "trade": { "trust": "untrusted" }
      }
    }
  }
}

Ese untrusted se escribe en la entrada de servidor traducida de mcp.json, y a partir de ahí se aplica sin cambios el gate que ya existe en tools/mcp_tool_handlers.py:

  • Toda llamada a herramienta sin la anotación readOnlyHint: true te pregunta primero, a través de la superficie de aprobación estándar;
  • donde no hay nadie que pueda responder — cron jobs, ejecuciones puntuales de hermes chat -q, una sesión de gateway sin supervisión — la llamada falla en cerrado en lugar de colarse;
  • una entrada que declara trust y nada más no crea ningún gate de disponibilidad; solo pone un candado sobre el servidor.

Solo restringir, por construcción

La regla de diseño importa más que la funcionalidad. Un paquete puede declarar exactamente dos valores:

Lo que escribe el paquete Qué ocurre
untrusted Se aplica — toda llamada con capacidad de escritura en ese servidor necesita aprobación
full Se acepta, pero ya es el valor por defecto, así que no otorga nada
Cualquier otra cosa El paquete se rechaza por completo, la misma postura de fallar en cerrado que ante un valor desconocido en la config nativa

Hay una regla de precedencia más que lo respalda: un servidor de config.yaml con el mismo nombre reemplaza la entrada del paquete por completo, trust incluido. Un paquete nunca puede imponerse al usuario. Si tú mismo pones un servidor en full, ningún untrusted en un manifiesto lo va a cambiar.

Merece la pena quedarse con el enfoque del propio PR: esto existe para que un paquete pueda restringir, nunca ampliar. El campo de extensión no es una forma de que los plugins ganen autoridad, sino una forma de que devuelvan la autoridad.

Compruébalo antes de habilitarlo

hermes plugins validate ahora lista los servidores con gate, para que quienes revisan el catálogo — y tú — puedan ver la declaración:

server trust: trade — untrusted (Hermes asks before every write-capable tool call)

Cuando aparece un paquete portable desconocido, este es el orden que te cuesta menos:

hermes plugins validate /path/to/package   # qué declara, incluidos los servidores con gate
hermes plugins install owner/repo          # se instala deshabilitado
hermes plugins capabilities <name>         # capacidades declaradas frente a concedidas
hermes plugins enable <name>               # actívalo cuando estés de acuerdo

Si publicas plugins

Tres cosas que conviene llevarse:

  1. Ponle gate a tus servidores de escritura. Cualquier servidor MCP que gaste dinero, opere, envíe mensajes o cambie cuentas debería declarar trust: "untrusted" en plugin.json. No confíes en que el texto de la skill le diga al modelo que pregunte — eso es una sugerencia, no un gate.
  2. Nunca pongas credenciales en mcp.json. Los valores env declarados ahí son datos visibles del paquete, no un almacén de secretos.
  3. “Se instala deshabilitado” no es un gate. Una vez que el usuario habilita el paquete, el bloque de extensión es la única garantía de seguridad que controlas.

Para ver el modelo de confianza más amplio en servidores MCP remotos — por qué readOnlyHint es una pista que aporta el servidor, y por qué las herramientas sin anotación devuelven outcome_uncertain cuando una sesión de transporte caduca a mitad de llamada — consulta el modelo de seguridad del catálogo de MCP remoto. Para saber hasta dónde aísla Hermes un plugin del host, lee aislamiento de plugins y la frontera con el host, y para entender cómo se comportan las aprobaciones cuando no hay nadie delante, la guía de aprobaciones sin supervisión entra más en detalle. Referencia de comandos: hermes plugins.

La parte que conviene recordar

readOnlyHint es una pista que el servidor da sobre sí mismo — un servidor que miente puede, como mucho, saltarse una pregunta para una herramienta que dice ser de solo lectura, pero nunca ganar acceso extra. trust: untrusted elimina la dependencia de cualquier autodescripción: cualquier cosa que no esté marcada como solo lectura pregunta primero.

Esa diferencia es el sentido entero del cambio. Una frontera de seguridad no debería trazarla la parte a la que se está revisando, y hasta este PR un paquete portable no podía trazar ninguna. Ahora sí puede, al precio de una línea extra en su manifiesto.