Votre clé API est partie sur le mauvais hôte — Hermes corrige le routage de base_url


Vous finissez par faire approuver le relais interne. Une ligne dans ~/.hermes/config.yaml — model.base_url — pointe Hermes vers le gateway de votre entreprise, la revue de sécurité valide, et pendant des semaines tout paraît normal : les réponses arrivent, les outils tournent, la facture atterrit sur la bonne entité. Puis quelqu’un du réseau pose une petite question. Pourquoi le credential de votre relais est-il attaché à du trafic qui part vers api.vendor.example ? Vous relisez la configuration. Elle indique toujours votre relais. Et c’est exactement le problème : sur quatre chemins de code, cette ligne n’a jamais été lue.

Ce que model.base_url promet

Chaque provider dans Hermes possède un hôte canonique — https://api.x.ai/v1, l’endpoint OpenRouter, chatgpt.com/backend-api/codex. Un model.base_url configuré est la surcharge : « ne parle pas à l’hôte canonique, parle à celui-ci à la place ». On le définit pour des raisons qui relèvent toutes du contrôle :

  • un relais d’entreprise ou auto-hébergé qui journalise, limite le débit et facture en interne ;
  • un endpoint régional, quand les données doivent rester dans une juridiction ;
  • un proxy devant un fournisseur, ou un gateway compatible OpenAI qui répartit vers plusieurs labos.

Quand la surcharge est ignorée, la requête n’échoue pas bruyamment. Elle réussit — contre le mauvais hôte, en portant le credential que vous aviez émis pour l’autre. C’est ce que réparent les deux correctifs fusionnés le 2026-09-25.

Bug un : quatre chemins qui n’ont jamais consulté la configuration

PR #121614 regroupe quatre bugs signalés séparément, qui partagent tous la même forme — un base_url configuré est ignoré et le code atteint l’hôte canonique du fournisseur, envoyant la requête et la clé API là où l’utilisateur n’a jamais voulu aller.

  • Résolution au runtime (xAI). Le résolveur au runtime possède une table de raccourcis pour les providers « simples ». L’entrée xAI de cette table portait une URL par défaut vide, donc la règle qui s’applique normalement — si la ligne du pool porte encore la valeur par défaut du registre, utiliser model.base_url — ne s’exécutait jamais pour xAI, et la ligne initialisée depuis l’environnement avec https://api.x.ai/v1 l’emportait à chaque fois.
  • Entrées de fallback. Quand un provider principal échoue et qu’Hermes active un fallback, le base_url de l’entrée est transmis. Deux providers ayant leurs propres branches de résolveur (OpenRouter et la branche Anthropic) le jetaient ensuite, alors que tous les autres providers l’honoraient.
  • Liste du sélecteur de modèle. La liste affichée par /model allait directement vers les récupérateurs canoniques par provider et ne demandait jamais si vous aviez pointé ce provider vers un relais ; le catalogue de votre relais restait donc invisible.
  • Validation de /model --provider. Le chemin de validation avait le même angle mort.

Trois des quatre avaient déjà des correctifs communautaires, qui ont été cherry-pickés dans la consolidation en conservant l’attribution. La conséquence pratique pour quiconque fait tourner un relais : la découverte et la validation des modèles interrogent désormais l’endpoint que l’inférence utilise réellement, au lieu d’un hôte dont vous vous étiez délibérément éloigné.

Bug deux : un gateway Codex personnalisé appelait encore chatgpt.com

Le second correctif, PR #121751, est celui qui se lit comme un incident de sécurité. Si vous faites tourner Codex derrière un gateway personnalisé — HERMES_CODEX_BASE_URL, ou un gateway déclaré uniquement via model.base_url — plusieurs chemins annexes codaient en dur chatgpt.com/backend-api/codex puis attachaient le credential résolu (celui du gateway) à la requête.

La liste est plus longue que prévu, parce que l’essentiel est invisible en usage normal :

  • la sonde de longueur de contexte qui tourne à chaque tour de chat ;
  • la liste Codex du sélecteur /model ;
  • le flux Codex de hermes model ;
  • le client Codex du plugin image ;
  • le client Codex auxiliaire ;
  • la sonde de quota du credential mutualisé ;
  • /usage.

Trois rapports sont arrivés le même matin (2026-09-24) — un sur les chemins de chat, un sur le chemin image, un faisant construire les URLs de catalogue de la sonde et du sélecteur à partir de la base résolue — et un relecteur a ensuite pointé la fuite restante : les chemins du sélecteur et de l’image reconstruisaient encore la destination à partir de variables d’environnement ambiantes, donc un gateway déclaré uniquement par model.base_url fuyait toujours. Les utilisateurs directs de ChatGPT OAuth n’ont jamais été touchés ; c’était un problème de gateway auto-hébergé.

Le correctif, en une phrase

Credential et destination sont désormais résolus ensemble. Le résolveur indique l’hôte vers lequel un credential mutualisé route réellement, avec une seule règle de priorité partout — surcharge d’environnement, puis model.base_url, puis l’URL propre de la ligne du pool — et le garde-fou « refuser une clé non-JWT » ne s’applique plus qu’aux sondes visant chatgpt.com. Une clé de gateway a le droit d’interroger son propre gateway ; elle n’a plus le droit de voyager vers celui qui est public.

Comment contrôler votre propre configuration

Vous n’avez pas besoin de lire le code source pour savoir si vous étiez concerné. Vous devez savoir quel hôte votre trafic a réellement atteint.

# que dit réellement votre configuration ?
hermes config get model.base_url

# si vous faites tourner Codex derrière un gateway, la surcharge est-elle définie dans ce shell ?
echo "$HERMES_CODEX_BASE_URL"

# et que contenait l'environnement de ce shell quand l'agent a démarré ?
grep -n "base_url" ~/.hermes/config.yaml

Ensuite, passez côté logs, parce que la configuration n’est pas l’egress :

  1. Le log d’accès de votre relais est la vérité terrain. Si votre gateway a journalisé le trafic, la réponse d’audit est un grep. Si l’hôte du fournisseur apparaît dans les logs d’un outil alors que votre relais ne montre rien, vous étiez sur le mauvais chemin.
  2. Regardez le trafic de sonde, pas seulement le trafic de chat. La sonde de longueur de contexte se déclenche à chaque tour, ce qui en fait la preuve la plus visible d’une fuite — et la plus facile à manquer, parce qu’elle n’apparaît jamais dans la transcription.
  3. Refaites le contrôle après une mise à jour. Les deux PR sont sur main uniquement pour l’instant — elles ont été fusionnées le 2026-09-25, un jour après la création du tag v0.21.5, donc elles n’y sont pas (la page de release de v0.21.5 indique ce qu’il contient). Elles arrivent dans les installations via la prochaine release ou un hermes update qui suit main.
  4. Considérez « ça a répondu » comme une absence totale de preuve. Les deux bugs produisaient des réponses normales et bien formées. Le seul contrôle fiable se fait côté destination.

Si vous utilisez model.base_url pour des raisons de résidence des données ou de conformité, cela mérite une confirmation explicite de votre côté plutôt qu’une supposition : pointez vers un relais qui journalise, envoyez un message, et confirmez où les octets sont allés. Ceux qui font tourner un proxy devant un labo — la configuration couverte par le guide du provider avec abonnement Claude — sont exactement le groupe que ces deux correctifs protègent. Si vous routez aussi la découverte de modèles ou les hooks via un endpoint personnalisé, le tutoriel sur les hooks de plugin provider montre jusqu’où cette surcharge est censée porter, et les notes sur le cache de configuration expliquent pourquoi une valeur modifiée peut attendre un lancement avant d’être prise en compte.

Ce qu’il faut retenir

Un endpoint personnalisé n’est pas un réglage cosmétique ; c’est une affirmation sur l’endroit où vos données et vos credentials ont le droit d’aller. Le schéma derrière les deux correctifs est celui qui vaut la peine d’être repris dans votre propre automatisation : une requête qui réussit ne prouve pas qu’elle est allée là où vous l’aviez demandé. Dès qu’un outil, un provider ou un plugin accepte une surcharge de type base_url, la question d’audit n’est pas « est-ce que ça a marché ? » mais « quel hôte a répondu, et quel credential a-t-il vu ? » Sur ce projet, la réponse est maintenant résolue une fois, en un seul endroit, avec un ordre de priorité documenté — et le garde-fou qui refuse d’envoyer une clé de gateway vers un hôte public est restreint à l’hôte pour lequel il a été écrit.