Un plugin peut désormais demander à Hermes de filtrer son propre serveur MCP


Vous installez un plugin communautaire depuis le catalogue. Il enregistre discrètement un serveur MCP, et ce serveur peut passer des commandes, déplacer de l’argent ou envoyer des messages en votre nom. Hermes ne vous demande jamais rien. La seule chose qui sépare un modèle victime d’une injection de prompt de l’action, c’est une phrase dans le texte de skill du plugin qui demande au modèle de vous consulter d’abord — et le fait que le modèle obéisse relève de son humeur, pas du code.

Ce n’est pas la faute du plugin. C’est un trou dans le format : Agent Plugins v1 ne définit aucun champ de confiance, donc chaque serveur MCP chargé depuis un paquet portable tournait avec le trust: full par défaut. La PR #134524, fusionnée le 7 octobre 2026, comble ce vide : un paquet peut demander à Hermes de filtrer son propre serveur, et ne peut rien demander de plus large.

D’où vient le trou

Hermes prend en charge deux formes de plugins. Les plugins natifs sont un plugin.yaml plus une fonction register(ctx), câblés dans le cœur. Les paquets portables visent le format Agent Plugins v1.0.0 et ressemblent à ceci :

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

plugin.json est le manifeste, mcp.json déclare les serveurs MCP auxquels le paquet se connecte, et skills/ contient les skills qu’il embarque. Un paquet fraîchement installé reste désactivé jusqu’à ce que vous lanciez hermes plugins enable <name> — bonne hygiène, mais cela ne répond qu’à la question « faut-il le charger du tout », pas à « qui appuie sur le frein pendant qu’il tourne ».

Le vrai problème venait du lecteur. Le mcp.json portable de Hermes n’accepte que type, url et headers (voir hermes_cli/agent_plugins.py), tandis que le niveau de confiance vit dans la config native à mcp_servers.<name>.trust dans config.yaml. Comme la spec ne prévoyait aucun champ de ce type, le lecteur n’en traduisait jamais, et chaque serveur de paquet arrivait avec le full par défaut.

Ce qui a mis le sujet à l’ordre du jour, c’est un cas concret. Un balayage du catalogue a fait remonter un plugin de trading dont le serveur hébergé met en pause chaque transaction sur le compte Hyperliquid de l’utilisateur jusqu’à ce que resolve_approval soit appelé. Comme le dit la PR, la seule chose qui séparait un modèle soumis à une injection de prompt d’une transaction approuvée était le texte du skill. Hermes lui-même ne demandait jamais rien.

Comment un paquet demande un garde-fou

plugin.json porte déjà un bloc d’extension spécifique à Hermes — le même qui contient app, requires et liveness. Il accepte désormais trust :

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

Cet untrusted est écrit sur l’entrée de serveur mcp.json traduite, et à partir de là, la barrière existante dans tools/mcp_tool_handlers.py s’applique sans changement :

  • Chaque appel d’outil sans annotation readOnlyHint: true vous demande d’abord votre accord, via la surface d’approbation standard ;
  • là où personne ne peut répondre — jobs cron, one-shots hermes chat -q, une session gateway non surveillée — l’appel échoue en mode fermé (fail-closed) au lieu de passer en douce ;
  • une entrée qui déclare trust et rien d’autre ne crée aucune barrière de disponibilité ; elle pose simplement un verrou sur le serveur.

Restreindre seulement, par construction

La règle de conception compte plus que la fonctionnalité. Un paquet peut déclarer exactement deux valeurs :

Ce que le paquet écrit Ce qui se passe
untrusted Appliqué — chaque appel capable d’écrire sur ce serveur nécessite une approbation
full Accepté, mais déjà la valeur par défaut, donc n’accorde rien
Tout le reste Le paquet est entièrement refusé, la même posture fail-closed qu’une valeur non reconnue dans la config native

Une règle de priorité supplémentaire sert de filet : un serveur config.yaml portant le même nom remplace entièrement l’entrée du paquet, trust compris. Un paquet ne peut jamais supplanter l’utilisateur. Réglez vous-même un serveur sur full et aucun untrusted dans un manifeste n’y changera quoi que ce soit.

La formulation de la PR mérite d’être retenue : tout cela existe pour qu’un paquet puisse restreindre, jamais élargir. Le champ d’extension n’est pas un moyen pour les plugins de gagner de l’autorité, c’est un moyen pour eux de rendre l’autorité.

Vérifiez avant d’activer

hermes plugins validate liste désormais les serveurs filtrés, ce qui permet aux relecteurs du catalogue — et à vous — de voir la revendication :

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

Quand un paquet portable inconnu se présente, l’ordre qui vous coûte le moins :

hermes plugins validate /path/to/package   # ce qu'il déclare, y compris les serveurs filtrés
hermes plugins install owner/repo          # installe désactivé
hermes plugins capabilities <name>         # capacités déclarées vs accordées
hermes plugins enable <name>               # active-le une fois que vous êtes d'accord

Si vous publiez des plugins

Trois choses à retenir :

  1. Filtrez vos serveurs d’écriture. Tout serveur MCP qui dépense de l’argent, trade, envoie des messages ou modifie des comptes devrait déclarer trust: "untrusted" dans plugin.json. Ne comptez pas sur le texte d’un skill qui demande au modèle de vous consulter — c’est un indice, pas une barrière.
  2. Ne mettez jamais d’identifiants dans mcp.json. Les valeurs env déclarées là sont des données de paquet visibles, pas un coffre de secrets.
  3. « Un paquet désactivé à l’installation » n’est pas une barrière. Une fois qu’un utilisateur active le paquet, le bloc d’extension est la seule revendication de sécurité que vous contrôlez.

Pour le modèle de confiance plus large des serveurs MCP distants — pourquoi readOnlyHint est un indice fourni par le serveur, et pourquoi les outils non annotés renvoient outcome_uncertain quand une session de transport expire en plein appel — voir le modèle de sécurité du catalogue MCP distant. Pour savoir jusqu’où Hermes isole un plugin de l’hôte, lisez l’isolation des plugins et la frontière avec l’hôte, et pour comprendre comment les approbations se comportent quand aucun humain n’est à proximité, le guide des approbations sans surveillance va plus loin. Référence des commandes : hermes plugins.

Ce qu’il faut retenir

readOnlyHint est un indice que le serveur donne sur lui-même — un serveur menteur peut au mieux éviter une invite pour un outil qu’il prétend en lecture seule, jamais gagner un accès supplémentaire. trust: untrusted supprime la dépendance à toute auto-description : tout ce qui n’est pas marqué en lecture seule demande d’abord.

Cette différence est tout l’intérêt du changement. Une frontière de sécurité ne devrait pas être tracée par la partie que l’on contrôle, et jusqu’à cette PR un paquet portable ne pouvait pas en tracer du tout. Désormais il peut, pour le prix d’une ligne supplémentaire dans son manifeste.