# Les rétrogradations de capacités MCP affaiblissent-elles les contrôles des agents ?

La compatibilité des protocoles fait partie de la surface d’attaque. Lorsqu’un client MCP négocie une version plus ancienne ou omet une capacité, il devrait perdre des fonctionnalités pratiques, pas les contrôles qui empêchent un agent d’agir avec l’autorité de quelqu’un d’autre.

J’ai déjà vu cet échec arriver sous la forme d’un correctif de compatibilité apparemment anodin : un client n’annonce pas une fonctionnalité de notification, le code emprunte donc une ancienne branche ; cette branche avait été écrite avant l’existence de l’approbation par session ; une action passe parce que personne n’a considéré cette branche comme un chemin d’autorisation. Le code réussit toujours les tests du parcours nominal. Il reste pourtant incorrect.

Les rétrogradations de capacités MCP nécessitent des tests de sécurité, car les métadonnées d’initialisation modifient le chemin dans la passerelle avant même que l’agent n’envoie sa première requête sensible. Testez ce chemin comme le ferait un adversaire : déclarez une ancienne version du protocole, omettez des champs, envoyez un objet de capacités vide, redémarrez le processus, verrouillez le coffre, révoquez la session, puis examinez le contenu du journal d’audit. Si la réponse passe de « refuser ou demander » à « exécuter », la compatibilité est devenue un contournement d’identifiants.

## Une rétrogradation modifie le chemin, pas l’autorité

Un chemin de compatibilité peut modifier la forme des messages, les notifications disponibles, la gestion de la progression ou la quantité de contexte reçue par le client. Il ne doit jamais changer l’identité autorisée à utiliser un identifiant, ni déterminer si une personne doit approuver l’appel.

Cette distinction semble évidente jusqu’à ce que le code commence à créer des branches selon `protocolVersion` ou `capabilities`. Un développeur écrit souvent cette branche pour éviter d’envoyer un message serveur non pris en charge. Plus tard, quelqu’un place la configuration de session, la transmission de l’approbation ou l’initialisation de l’audit dans la même branche parce que ces éléments se trouvent à proximité. La rétrogradation modifie alors accidentellement une frontière de sécurité.

La spécification MCP définit l’initialisation comme un échange au cours duquel le client déclare une version du protocole et ses capacités, puis le serveur répond avec sa propre version et ses propres capacités. Traitez ces champs comme des déclarations provenant d’un pair non fiable. Ils peuvent guider l’interopérabilité. Ils ne peuvent pas accorder de pouvoir.

Sallyport rend cette séparation concrète : son verrou de coffre, l’autorisation de session et la configuration de la clé pour chaque appel se trouvent sous la conversation MCP. Un agent ne peut donc pas recevoir un identifiant simplement en se présentant comme un ancien client. C’est la bonne structure, mais elle doit tout de même être couverte par des tests de régression à la frontière du protocole.

Écrivez l’invariant de sécurité avant la matrice :

> Pour toute variante d’initialisation MCP acceptée, une action protégée est refusée lorsque le coffre est verrouillé ; un nouveau processus client nécessite une approbation de session ; un identifiant configuré pour une vérification à chaque appel doit être vérifié à chaque utilisation ; et l’action crée un événement d’audit qu’elle réussisse, échoue ou soit refusée.

Cette phrase donne aux réviseurs un critère plus précis que « les anciens clients fonctionnent ». Un ancien client ne fonctionne correctement que s’il conserve ces résultats.

## Les données d’initialisation méritent des tests hostiles

Un client bien conçu envoie une requête d’initialisation propre une seule fois, énumère les capacités qu’il prend en charge et continue après avoir reçu la réponse. Les tests de sécurité doivent commencer par ce cas, puis retirer chaque hypothèse une par une.

Utilisez un petit dispositif client capable d’envoyer des messages JSON-RPC bruts, plutôt qu’une bibliothèque MCP de haut niveau qui remplit elle-même les valeurs par défaut. Les tests de bibliothèque sont utiles, mais ils masquent les conditions sur le fil qui provoquent les erreurs de rétrogradation.

Une requête de référence peut ressembler à ceci :

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "CURRENT_TEST_VERSION",
    "capabilities": {
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": { "name": "compat-fixture", "version": "1.0" }
  }
}
```

Le dispositif doit ensuite produire des variantes qui ne diffèrent que sur un point significatif :

- la version du protocole prise en charge la plus ancienne
- la version actuelle avec `{}` comme capacités
- la version actuelle sans le champ `capabilities`, si votre analyseur l’accepte
- un objet de capacités dont certaines entrées facultatives connues sont absentes
- une ancienne version non prise en charge que le serveur doit rejeter

Ne combinez pas toutes les omissions dans le premier test. Lorsqu’une requête mal formée de plusieurs façons échoue, vous apprenez très peu de choses. Les cas à variable unique montrent quelle valeur par défaut ou quelle branche a modifié le résultat.

Pour chaque cas accepté, vérifiez que le résultat de l’action protégée reste identique avant l’approbation. La réponse exacte du transport dépendra de l’implémentation, mais sa forme doit être sans ambiguïté :

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32001,
    "message": "Session authorization required"
  }
}
```

Ne vérifiez pas seulement le texte. Vérifiez aussi une catégorie d’erreur ou un code de résultat interne stable, l’absence de toute requête HTTP sortante ou invocation SSH, ainsi qu’un enregistrement d’audit pour la tentative refusée. Une erreur aimable qui masque un véritable effet de bord est un test en échec.

Le cas délicat est celui d’une requête d’initialisation dont les champs sont syntaxiquement valides, mais incohérents entre eux. Le client affirme peut-être parler une version reconnue par votre serveur tout en ne déclarant aucune des capacités qu’un client normal de cette version utiliserait. Votre passerelle n’a pas besoin de faire la leçon au client. Elle doit choisir un chemin sûr : accepter la session limitée en conservant tous les contrôles locaux, ou rejeter l’initialisation. Elle ne doit jamais choisir silencieusement un chemin non protégé parce que l’affichage habituel de l’approbation n’est pas disponible.

## Les anciens formats sur le fil ne peuvent pas créer un ancien modèle de sécurité

Les équipes disent souvent prendre en charge une ancienne version du protocole lorsqu’elles veulent dire qu’elles savent encore analyser ses messages. Ce n’est que la moitié du travail. Vous devez aussi décider quels comportements de sécurité actuels survivent lorsque l’ancien format ne possède aucun champ correspondant.

La réponse devrait généralement être : tous. L’autorisation de session est un état local. La vérification à chaque appel est un état local. La capture d’audit est un état local. Aucun de ces éléments ne nécessite que le client transporte un champ équivalent dans une ancienne requête.

C’est ici que la négociation des capacités et l’autorisation se confondent. Un client peut ne pas disposer d’une fonctionnalité permettant de recevoir une mise à jour d’état structurée. Cela vous indique comment communiquer l’état de l’approbation, pas s’il faut la supprimer. Si votre produit ne peut pas présenter une vérification à l’utilisateur pour cette invocation, refusez l’appel avec une erreur claire. Demander au client de promettre qu’il a affiché une invite ne remplace pas une décision locale.

Évitez le repli courant qui consiste à « approuver toute la session si le client ne peut pas gérer les demandes à chaque appel ». Il est populaire parce qu’il réduit les frictions et fait fonctionner une démonstration. Il est incorrect, car il transforme sans son consentement la décision d’un détenteur d’identifiant, prise à chaque utilisation, en une décision unique. La configuration de l’identifiant doit primer.

Gardez l’adaptateur de version étroit. Il doit traduire la syntaxe des requêtes et des réponses, filtrer les messages que le pair ne peut pas comprendre et convertir les formes d’erreur compatibles. Il ne doit pas décider si une action s’exécute. Placez l’autorisation dans un chemin unique appelé par chaque adaptateur.

Une question utile en revue de code est la suivante : une branche de compatibilité peut-elle renvoyer un descripteur d’exécution, injecter un en-tête d’autorisation ou produire un résultat SSH sans passer d’abord par la même fonction d’autorisation que le client le plus récent ? Si oui, cette branche a besoin d’un test immédiatement et d’une refonte prochainement.

## L’approbation de session doit suivre le processus qui l’a demandée

L’approbation par session est facile à mal tester. Si le client de test conserve un seul processus actif, l’approuve et exécute dix appels, vous prouvez seulement qu’une session nominale reste approuvée. Vous n’avez pas prouvé que le processus d’agent suivant devra prendre sa propre décision.

Utilisez deux processus de dispositif lancés indépendamment. Donnez-leur le même nom de client affiché, la même version du protocole et la même déclaration de capacités. Le processus A s’initialise et demande une action protégée. Approuvez sa session. Le processus B s’initialise ensuite et demande la même action.

Le processus B doit recevoir une nouvelle demande d’approbation. Il ne doit pas hériter de l’autorisation de A parce qu’il partage un libellé client, un répertoire de travail, un point d’accès de transport ou un détail de connexion mis en cache. Si votre implémentation affiche l’autorité de signature du code sur la fiche d’approbation, vérifiez que la fiche identifie l’autorité du processus qui a réellement lancé B. Ne demandez pas au test de vérifier les éléments décoratifs de la fiche ; vérifiez le signal d’identité dont une personne se sert pour décider.

Testez ensuite les limites du cycle de vie rencontrées par les agents en production :

1. Approuvez le processus A et laissez-le se terminer normalement. Lancez B avec des métadonnées identiques. B doit nécessiter une approbation.
2. Approuvez A, puis arrêtez-le sans fermeture élégante. Lancez B. B doit nécessiter une approbation.
3. Approuvez A, révoquez A dans le journal de session alors qu’il est encore actif, puis effectuez un nouvel appel depuis A. Cet appel doit s’arrêter avant l’exécution.
4. Révoquez A alors que sa première action attend une réponse externe. L’action en attente ne doit pas créer une seconde action approuvée après la révocation.

Les troisième et quatrième cas révèlent une erreur courante : le système ne vérifie l’approbation qu’à l’ouverture d’une connexion ou à la création d’un objet d’action. Une session peut changer alors qu’un processus est toujours actif. Vérifiez l’autorité au moment où la passerelle s’engage à lancer l’action sortante.

N’utilisez pas un identifiant de session fourni par l’appelant comme clé d’approbation. Un test peut rendre ce défaut évident : faites rejouer à B le libellé de session de A et vérifiez que la passerelle demande tout de même une approbation. Le nom du client sert à l’affichage et au diagnostic, il ne prouve pas que le même exécutable effectue une nouvelle demande.

## La vérification à chaque appel appartient à l’utilisation de l’identifiant

Un réglage de clé avec vérification à chaque appel signifie que la passerelle demande un consentement à chaque utilisation de cet identifiant. Cela ne signifie pas « une fois par nom d’outil », « une fois par connexion ouverte » ou « une fois, sauf si le client est ancien ». Ce qui doit être vérifié est la véritable requête qui transportera le secret ou utilisera la clé SSH.

Construisez un test avec un identifiant configuré pour une vérification à chaque appel et un dispositif qui envoie deux actions équivalentes dans une session déjà approuvée. Pour HTTP, utilisez deux appels vers un point d’accès de test qui renvoie un marqueur inoffensif. Pour SSH, utilisez deux commandes inoffensives sur un hôte de test isolé. Le test doit observer deux événements de vérification distincts et deux enregistrements d’action distincts.

Une séquence minimale d’événements attendue ressemble à ceci :

```text
session_authorized process=fixture-A
call_review_requested action=41 credential=deploy-token
call_completed action=41 result=success
call_review_requested action=42 credential=deploy-token
call_completed action=42 result=success
```

L’assertion importante ne concerne pas la numérotation. Elle vérifie que l’action 42 ne peut pas utiliser l’approbation accordée à l’action 41. Faites arriver la deuxième requête après la fin de la première, puis ajoutez un autre cas où les deux arrivent presque simultanément. La concurrence révèle les implémentations qui stockent sur la session un unique indicateur temporaire « vérification réussie » au lieu de le lier à une seule action.

Répétez maintenant le test avec une ancienne déclaration de protocole et sans la capacité utilisée par votre parcours normal d’état de l’approbation. Le mécanisme de vérification peut être présenté différemment sur la machine locale, mais le résultat doit rester identique. Si l’utilisateur ferme, annule ou laisse expirer la deuxième vérification, le point d’accès de test doit voir une seule requête, pas deux.

Conservez une assertion stricte sur l’exposition des secrets. La réponse MCP doit contenir le résultat de l’action, une erreur ou l’état indiquant qu’une vérification est requise. Elle ne doit contenir ni clé d’API, ni clé privée SSH, ni espace réservé masqué tenant lieu de secret, ni paramètre d’outil permettant à l’agent de le reconstituer. Une rétrogradation est un endroit tentant pour un adaptateur de compatibilité qui voudrait sérialiser un contexte supplémentaire. Inspectez la transcription brute, pas seulement votre objet de test structuré.

## La capture d’audit doit se trouver sous la négociation du protocole

Un journal d’audit qui n’enregistre que les appels réussis de clients modernes donne une fausse impression de sécurité. Les cas dont vous aurez besoin plus tard sont les appels refusés, les vérifications annulées, les demandes d’initialisation rejetées et les chemins de compatibilité étranges qui ont fait dire à un ingénieur « cela ne devrait jamais arriver ».

Enregistrez les faits nécessaires pour reconstituer la décision : l’exécution ou la session de l’agent, l’identité de l’action, le canal, la cible demandée, la référence de l’identifiant ou son identifiant sûr, l’état de l’autorisation, le résultat de la vérification et le résultat de l’exécution. Évitez de stocker le secret lui-même. Pour une initialisation échouée, enregistrez suffisamment de contexte sur le pair et l’analyse pour expliquer le rejet, sans transformer le journal en copie des charges utiles non fiables.

Sallyport projette ses journaux Sessions et Activity à partir d’un journal d’audit chiffré et chaîné par hachage. Cette conception est importante pour les tests de rétrogradation, car les capacités du client ne doivent pas décider de l’existence d’un second journal. Le même chemin d’écriture doit voir un client actuel, un client partiel et un appel refusé.

Utilisez une vérification hors ligne de la chaîne comme assertion après chaque cas de bout en bout :

```text
$ sp audit verify
verified: 18 records
chain: intact
```

La formulation exacte peut varier, mais le test doit exiger un résultat de vérification réussi sans ouvrir le coffre. Ce test détecte une écriture chaînée rompue ou omise. Il ne prouve pas que le bon événement a été écrit. Associez-le donc à une requête des vues de session et d’activité qui vérifie la présence des enregistrements refusés, approuvés, annulés ou terminés attendus.

Testez également l’ordre des événements. Si une vérification est refusée, le journal doit afficher la demande et le refus, sans enregistrer une fin d’exécution fictive. Si l’action de transport échoue après l’approbation, enregistrez cet échec comme un échec d’exécution, pas comme un refus d’autorisation. Ces deux événements répondent à des questions opérationnelles différentes. Les mélanger complique l’analyse d’incident et permet à un adaptateur défaillant de se faire passer pour un choix de l’utilisateur.

## Construisez la matrice de compatibilité autour des résultats de sécurité

Le produit cartésien complet de chaque version, capacité, canal et réglage d’identifiant finira par devenir si grand que plus personne ne l’exécutera. Conservez une petite matrice obligatoire qui couvre chaque décision de sécurité, puis ajoutez un cas lorsqu’une nouvelle branche d’adaptateur apparaît.

Utilisez les lignes pour la forme de l’initialisation et les colonnes pour l’état de décision local. Par exemple, exécutez un client utilisant la version la plus ancienne prise en charge, un client aux capacités vides et un client actuel normal contre un coffre verrouillé, une session non approuvée, un identifiant associé à une session approuvée et un identifiant nécessitant une vérification à chaque appel. Faites passer chaque cellule significative par HTTP et SSH si les deux canaux partagent le même chemin d’autorisation, mais utilisent des assistants d’exécution différents.

Le résultat attendu doit être écrit en termes simples avant l’exécution du test :

| Condition du client | État local | Résultat attendu |
| --- | --- | --- |
| Version acceptée la plus ancienne | Coffre verrouillé | Action refusée et enregistrée |
| Capacités vides | Coffre déverrouillé, nouveau processus | Approbation de session demandée et enregistrée |
| Version ancienne | Processus approuvé, clé avec vérification à chaque appel | Vérification demandée à chaque appel |
| Version non prise en charge | Tout état | Initialisation rejetée, aucune action sortante |
| Version actuelle | Processus révoqué | Action refusée et enregistrée |

Cette table mérite sa place, car elle force une décision sur les versions non prises en charge. Ne laissez pas le serveur « faire au mieux » après l’échec de la négociation. Cette formule signifie souvent qu’il exécute une requête sur un chemin d’analyse moins testé que le chemin pris en charge. Rejetez la session, enregistrez le rejet de manière sûre et exigez un client pris en charge.

Pour chaque ligne, recueillez trois observations indépendantes : la réponse MCP brute, une observation au niveau du service HTTP simulé ou de l’hôte SSH de test, et le résultat d’audit. Une réponse d’erreur seule ne peut pas prouver qu’aucune action n’a atteint le monde extérieur. Un point d’accès de test seul ne peut pas prouver que la passerelle a enregistré le refus. Il vous faut les trois.

## Une transcription d’échec doit nommer la frontière rompue

Lorsqu’un test de compatibilité échoue, ne le classez pas comme un « problème de l’ancien client MCP ». Cette description garantit qu’un autre développeur corrigera l’affichage en oubliant l’autorisation.

Nommez l’invariant et montrez la chronologie. Voici un rapport d’échec qui permet d’agir :

```text
Client B initialized with oldest accepted version and no optional capabilities.
Client A had already received session approval and was still running.
Client B sent an HTTP action using the same displayed clientInfo name.
Gateway injected the credential and the test endpoint received the request.
No approval card appeared for B.
Activity journal recorded completion, but Sessions journal contained only A.
```

Cette séquence indique que le défaut concerne l’identité du processus ou la portée du cache d’autorisation, pas l’analyse de la version. Elle indique aussi si le journal d’audit contient un enregistrement susceptible d’induire un enquêteur en erreur. Ajoutez le test de régression à ce niveau, puis remontez vers l’implémentation jusqu’à obtenir une seule vérification d’autorisation que B ne peut pas contourner.

Un deuxième type d’échec est plus subtil :

```text
Client initialized without the capability used for approval status updates.
Credential required per-call review.
First action displayed local review and completed.
Second action completed immediately.
Audit log recorded two completed actions and one review event.
```

Cela indique qu’un jeton de vérification a dépassé la portée de son action. La solution n’est pas d’interdire l’ancien client. Il faut lier la vérification approuvée à un nonce d’action ou à un enregistrement d’action interne, puis la consommer exactement une fois.

Ces transcriptions aident également les équipes d’assistance à distinguer une décision de l’utilisateur d’une défaillance logicielle. Si le journal indique qu’un utilisateur a refusé une vérification, examinez la demande. S’il indique qu’aucune vérification n’a eu lieu pour un identifiant qui en exige une, cessez de considérer l’événement comme un comportement normal de l’agent.

## Le verrouillage et la révocation sont deux tests distincts

Le verrou du coffre, l’approbation de session et la vérification à chaque appel répondent à des questions différentes. Une suite de tests de rétrogradation doit les tester séparément, car un test d’approbation réussi peut masquer un échec du verrouillage du coffre, et inversement.

Commencez par verrouiller le coffre. Lancez chaque dispositif de compatibilité, y compris celui qui utilise la version la plus ancienne acceptée. Chaque action protégée doit échouer avant l’injection de l’identifiant et avant que le service externe ne voie quoi que ce soit. N’acceptez pas un résultat qui demande simplement à l’agent de fournir lui-même les identifiants. L’agent ne doit jamais les recevoir.

Déverrouillez ensuite le coffre, mais laissez la session non approuvée. Le dispositif doit obtenir un seul parcours d’autorisation de session pour son exécution. Approuvez-le, effectuez une action protégée, puis révoquez la session alors que le processus reste actif. L’action suivante doit échouer même si le coffre reste déverrouillé.

Enfin, utilisez un identifiant nécessitant une vérification à chaque appel dans cette session approuvée. Approuvez une action, refusez la suivante et examinez le journal. Cette séquence prouve que l’échelle de décision conserve son ordre : le coffre bloque toute action lorsqu’il est verrouillé ; l’approbation de session établit l’autorité de ce processus ; le réglage de l’identifiant peut exiger une nouvelle décision par-dessus cette autorité.

Ne regroupez pas ces tests dans un seul scénario interminable sans conserver aussi des tests ciblés pour chaque frontière. Les longs tests de bout en bout montrent bien qu’un chemin existe. Ils expliquent mal pourquoi ce chemin s’est cassé après une refonte.

## Les critères de mise en production doivent sanctionner les réussites silencieuses

Les erreurs de compatibilité doivent bloquer une mise en production lorsqu’elles permettent à une action de se produire discrètement. Une petite différence dans un message d’état peut être corrigée dans le correctif suivant. Un client rétrogradé qui hérite d’une approbation, ignore une vérification à chaque appel, agit alors que le coffre est verrouillé ou disparaît de la piste d’audit a franchi une frontière de sécurité.

Rendez ces tests obligatoires dans l’intégration continue pour toute modification touchant l’initialisation MCP, les adaptateurs de protocole, la transmission des approbations, le suivi des processus, l’injection d’identifiants ou la projection de l’audit. N’attendez pas une nouvelle version du protocole. Le risque vient des branches créées autour d’une capacité absente, et le travail courant sur les fonctionnalités en crée constamment.

Le test qui se rentabilise généralement le plus est aussi le plus disgracieux : une ancienne déclaration de client, un objet de capacités vide, un nom affiché réutilisé, un deuxième processus et une deuxième utilisation de l’identifiant après l’approbation de la première. Gardez-le. C’est ainsi que vous repérez le code de compatibilité bien intentionné qui a discrètement transformé une décision humaine en simple résultat de cache.
