MCP stdio ou sockets Unix dépend de la limite de confiance
Comparaison de MCP stdio et des sockets Unix pour un courtier de secrets local : durée de vie, droits, identité, nettoyage et déploiement.

Un courtier de secrets de bureau devrait utiliser stdio quand l'autorité appartient à un seul processus agent, et un socket de domaine Unix quand elle appartient à un service local partagé. Cela ressemble à un choix de transport, mais déplacer des octets est la partie facile. La difficulté consiste à décider qui peut déclencher une action, combien de temps cette autorisation dure et quelles preuves subsistent après la panne de l'une ou l'autre partie.
J'ai vu des équipes commencer par un socket parce qu'il ressemblait à de l'infrastructure, puis passer des semaines à reconstruire l'attribution de l'appelant et les contrôles de durée de vie que l'arbre des processus fournissait déjà. J'ai aussi vu stdio transformé en service résident par un empilement de lanceurs, de multiplexeurs et d'état caché. Les deux transports peuvent être sûrs. Chacun peut invalider discrètement le modèle de menace si ses hypothèses de durée et d'identité ne correspondent pas à l'autorité accordée.
Cette comparaison suppose un courtier de bureau macOS qui conserve des identifiants API ou SSH et exécute des actions pour des agents IA locaux. L'agent ne doit jamais recevoir le secret. Le courtier protège donc bien plus qu'un cache local : il décide quel processus peut transformer une autorité stockée en effet externe.
Choisissez la limite de confiance avant le transport
Le bon transport découle de l'unité d'autorisation. Si une personne approuve une exécution de l'agent, un serveur stdio enfant lui donne une limite naturelle : le tube existe pour cette relation entre processus et se ferme lorsqu'une extrémité quitte. Si plusieurs outils doivent partager un courtier déverrouillé, un socket Unix offre un point de rendez-vous stable, mais le courtier doit créer sa propre limite de session au-dessus du socket.
Décrivez le modèle de menace avec des acteurs et des actions, pas comme le simple souhait d'avoir un IPC sûr. Nommez les processus locaux autorisés à se connecter, les identifiants détenus par le courtier, les opérations qu'ils permettent et les événements qui doivent couper l'accès. Incluez un logiciel malveillant exécuté sous le même compte. Les droits de fichier arrêtent souvent les autres utilisateurs, mais font peu contre un autre processus du même compte. Incluez aussi une copie du binaire de l'agent, une extension modifiée, un shell lancé par l'agent et un ancien processus encore actif après la fin du travail visible.
Définissez ensuite ce que signifie une approbation. Elle peut autoriser un exécutable signé, un processus du système, un arbre de processus, une tâche de terminal ou tous les clients de l'utilisateur connecté. Ces promesses diffèrent. Un transport ne peut pas choisir à votre place. Il facilite seulement l'application de certaines d'entre elles.
La révocation est un bon test de conception. Demandez ce que le courtier peut révoquer immédiatement sans verrouiller tout le coffre. Avec stdio, fermer un tube ou tuer l'enfant met fin à ce canal, même si des descendants peuvent avoir hérité de descripteurs mal gérés par le lanceur. Avec un socket, fermer une connexion acceptée arrête ce client tandis que le socket d'écoute reste disponible. Si l'enregistrement d'autorisation survit à la connexion, aucune de ces fermetures ne suffit.
Ne placez pas les attaquants réseau au centre de cette décision locale, sauf si le courtier expose aussi un listener réseau. Les risques les plus précis sont une identité confuse, l'autorité ambiante de la session, l'héritage des descripteurs, le remplacement d'un socket dans le système de fichiers et une autorisation plus longue que le travail approuvé.
Stdio lie le canal à la durée du processus
MCP sur stdio est le plus pertinent lorsque le shim du courtier doit naître et mourir avec le processus agent. Le client lance une commande serveur, écrit des messages JSON-RPC sur son entrée standard et lit les réponses sur sa sortie standard. EOF est un signal de durée de vie fourni par le système, pas une convention cachée dans un heartbeat applicatif.
La spécification de transport Model Context Protocol impose aussi une règle opérationnelle utile : un serveur stdio ne doit écrire aucune donnée hors protocole sur stdout. Les journaux vont sur stderr. On pourrait n'y voir qu'une règle de cadrage, mais elle évite qu'une ligne de diagnostic devienne un message invalide au sein d'une limite de sécurité. Traitez stdout comme la mémoire du protocole. Configurez les bibliothèques et les rapports de panne avant la première réponse pour éviter toute pollution.
Stdio ne demande ni nom de fichier, ni répertoire de sockets, ni mode de droits, ni fichier de découverte, ni listener résident. La configuration de l'agent pointe vers un exécutable et ses arguments. C'est un vrai avantage de déploiement pour un produit de bureau : aucun endpoint à découvrir, aucun chemin obsolète à réparer. Une mise à jour a aussi un point d'activation clair : le prochain shim lancé utilise le nouvel exécutable. Une session en cours peut conserver l'ancienne version, donc consignez l'identité et la version de l'exécutable avec la session au lieu de supposer que tous les clients actifs ont changé lors de l'installation.
La relation de processus constitue une preuve utile, mais pas une identité complète. Le courtier peut inspecter le shim relié à son backend privé, et le shim peut inspecter son parent. Un identifiant de processus peut être réutilisé après une sortie, et un lanceur peut se trouver entre l'agent visible et le shim. Capturez le jeton d'audit ou les informations de signature pendant que le processus vit. Ne conservez pas seulement un PID à résoudre plus tard.
Les tubes présentent aussi des pièges d'héritage. Si un lanceur marque des descripteurs comme héritables, un petit-enfant peut garder l'extrémité d'écriture ouverte après la fin de l'agent. Le courtier attend alors un EOF qui ne viendra jamais. Activez close on exec lorsque la plateforme ne le fait pas, fermez les extrémités inutiles juste après la création et surveillez à la fois le tube et le processus client attendu. EOF doit révoquer l'accès, et la sortie du processus doit le révoquer indépendamment.
La concurrence est volontairement étroite. Une instance de serveur stdio sert normalement un processus client. L'isolation est nette, au prix d'un processus supplémentaire par agent. Si le vrai coffre réside dans une application de bureau, l'exécutable stdio est souvent un petit shim qui transmet des requêtes typées à l'application. Ce saut interne exige encore une authentification et une liaison de session. Stdio à la frontière MCP ne sécurise pas un backend partagé sans authentification.
Un socket Unix crée la durée d'un service
Un socket de domaine Unix convient à un courtier déjà résident qui attend des clients indépendants au fil du temps. L'endpoint d'écoute survit à leur départ, donc une application de barre de menus accepte les appels sans que chaque agent possède le processus courtier. Plusieurs clients, la contre-pression et les mises à jour centrales se gèrent facilement. En contrepartie, le service doit définir chaque limite qu'un processus client unique fournissait implicitement.
Le chemin du socket sert à la découverte, pas à l'authentification. Un client qui le trouve a encore besoin du droit de se connecter, et un client connecté a encore besoin d'une décision d'autorisation. Placez le socket dans un répertoire contrôlé par l'utilisateur ou l'application, créez ce répertoire en mode 0700 et le socket en 0600. Évitez un chemin prévisible dans un répertoire partagé accessible en écriture. Le sticky bit d'un répertoire temporaire bloque certaines suppressions, mais n'en fait pas un espace de noms digne de confiance.
Les droits ne dépendent pas seulement du mode final. L'umask du processus influence la création. Les répertoires parents déterminent si un autre compte peut parcourir ou remplacer des noms. Un lien symbolique ou un noeud obsolète peut déjà occuper le chemin. Le service doit ouvrir un parent fiable, examiner l'entrée sans suivre les liens et ne la supprimer qu'après avoir prouvé qu'il s'agit du socket d'une instance morte ou d'un chemin appartenant à l'installation actuelle. Faire unlink sans contrôle sur un nom connu crée une course au remplacement.
Sur macOS, un socket local accepté fournit les identifiants du pair avec des appels comme getpeereid, et des API plus basses exposent un jeton d'audit. Les identifiants d'utilisateur et de groupe indiquent quel compte possède le processus pair. Ils n'indiquent pas quelle application l'utilisateur voulait autoriser. Pour cela, résolvez le jeton vers le processus vivant et évaluez la signature, le chemin de l'exécutable et le contexte de lancement selon la politique annoncée du produit.
Un socket facilite le multiplexage, mais celui-ci peut brouiller les sessions. Ne considérez jamais une connexion réussie comme l'approbation de toute requête munie d'un identifiant de session arbitraire. Le serveur attribue l'identité de connexion, y lie l'autorisation et rejette toute tentative du client de changer d'identité. Si un helper se reconnecte après un redémarrage, exigez une nouvelle décision, sauf si le modèle autorise explicitement l'approbation à survivre.
Les performances décident rarement. Les tubes locaux et les sockets Unix transportent de petites requêtes JSON-RPC bien plus vite qu'une opération API HTTP ou SSH ne s'achève. Mesurez si le courtier transfère de grandes réponses, mais n'échangez pas un modèle d'autorité lisible contre une baisse spéculative du coût IPC local.
Les droits du socket ne prouvent pas l'intention
Le mode 0600 signifie que des processus sous d'autres identifiants d'utilisateur ne peuvent pas ouvrir le socket avec les contrôles discrétionnaires ordinaires. Il ne signifie pas que chaque processus du même utilisateur mérite les identifiants stockés. Un malware de bureau, un script de paquet douteux, une extension d'éditeur et l'agent approuvé partagent souvent cet identifiant.
Cette distinction se brouille parce que les droits Unix sont concrets et faciles à inspecter. Une équipe voit un socket privé et déclare l'endpoint authentifié. Il ne l'est qu'à la frontière du compte. Cela peut suffire si le modèle s'arrête aux autres utilisateurs. Un courtier pour outils autonomes prétend généralement contrôler ce qui se passe au sein d'une session, et a donc besoin d'une identité plus étroite.
Sur macOS, la signature de code permet de réduire cette identité. Validez le pair vivant, pas un chemin transmis dans la requête. Le chemin peut désigner un fichier remplacé, et un processus peut continuer d'exécuter une ancienne image déjà chargée après une mise à jour. Consignez l'autorité de signature et l'exigence désignée rapportées par le système. Décidez du sort des builds de développement signés ad hoc ; les traiter en silence comme l'application de production transforme une commodité en contournement.
L'identité et l'autorité de l'appelant diffèrent aussi. L'identité répond à la question du processus qui a ouvert la connexion. L'autorité dit si ce processus peut utiliser cet identifiant pour cette action, maintenant. Un agent correctement signé peut travailler dans un dépôt non vérifié ou subir un contenu hostile dans son prompt. Accorder toute action parce que l'exécutable est familier confond provenance et consentement.
Avec stdio, le lanceur peut présenter l'identité du parent immédiat au démarrage du shim, et le courtier peut la lier à un nonce de canal neuf. Avec un socket, le courtier dérive l'identité du pair lors de l'acceptation et la lie au descripteur. Dans les deux cas, le courtier est la source de l'identité. Un champ comme client_name n'est qu'une donnée d'affichage, jamais une preuve.
Une carte d'approbation honnête montre ce que le système peut établir et ce que l'utilisateur approuve. Si un script enveloppeur empêche l'attribution fiable à l'agent supérieur, dites-le ou refusez l'appel. Remonter l'arbre jusqu'à un nom familier crée un chemin de recherche contrôlé par l'attaquant.
Le nettoyage appartient au modèle de sécurité
Le nettoyage de stdio porte sur les descripteurs et les enfants. Le cas normal est simple : le client ferme stdin, le serveur voit EOF, termine ou annule le travail, puis quitte. Les cas anormaux comptent davantage. Le client peut tomber tandis qu'un descendant garde le tube. Le serveur peut se bloquer alors que le client le croit mort. Une action privilégiée peut finir après révocation si l'annulation n'atteint pas le worker qui l'exécute.
Donnez à chaque requête un identifiant d'opération attribué par le courtier et un état comme queued, executing, completed, denied ou indeterminate. À la fermeture du canal, révoquez aussitôt les requêtes futures. Pour une action déjà envoyée à une API distante, ne prétendez pas l'annuler si le distant ne le permet pas. Inscrivez indeterminate si le processus local meurt avant de connaître le résultat. Cette entrée évite qu'une boucle de reprise exécute l'action deux fois.
Le nettoyage d'un socket concerne deux objets : les connexions acceptées et le chemin d'écoute. Fermez une connexion après erreur de protocole, refus d'autorisation, expiration d'inactivité ou arrêt du service. Supprimez le chemin uniquement si le service possède encore le même objet. Au redémarrage, EADDRINUSE invite à enquêter, pas à faire unlink de ce qui existe.
Cette vérification shell aide pendant le développement, car elle montre type, propriétaire, mode et processus sans rien modifier :
sock="$TMPDIR/com.example.broker.sock"
stat -f 'type=%HT owner=%Su mode=%Sp inode=%i' "$sock"
lsof -n -U "$sock"
Un résultat macOS normal a cette forme :
type=Socket owner=alice mode=srw------- inode=123456
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
Broker 48102 alice 9u unix 0x0123456789abcdef 0t0 /.../com.example.broker.sock
N'analysez pas les valeurs d'exemple. Vérifiez le type Socket, le propriétaire attendu, l'absence d'accès groupe et autres, et l'identité de l'instance installée. Refaites le contrôle après un crash forcé, une mise à jour, une déconnexion et un second lancement. Un nettoyage qui ne marche qu'après une fermeture normale n'a pas été testé.
Si launchd démarre le service, laissez-lui cette responsabilité de manière cohérente. Mélanger un démarrage géré par l'application et un launch agent peut produire deux instances en concurrence. Un seul composant possède la création, l'état prêt et la suppression ; les clients ont besoin d'un état unavailable explicite au lieu de relancer sans cesse des courtiers.
L'effort de déploiement change d'endroit
Stdio coûte moins à l'installation et davantage quand de nombreux clients partagent un état résident. Chaque client MCP requiert une commande. Le shim doit trouver l'application signée ou son endpoint privé, négocier une version compatible et signaler clairement une application absente ou verrouillée. L'empaquetage conserve les droits d'exécution et les signatures. Ne dépendez pas des fichiers de démarrage du shell : une application graphique est souvent lancée sans l'environnement du terminal.
Un socket Unix demande installation du service, propriété du démarrage, découverte, droits du répertoire, reprise après panne et compatibilité entre mises à jour indépendantes. En échange, chaque client utilise un endpoint stable et le service maintient état du coffre et ordre d'audit dans un processus. C'est intéressant lorsque l'application tourne déjà en permanence.
La découverte mérite un contrat. Sur macOS, $TMPDIR est propre à chaque utilisateur, mais l'environnement hérité varie entre terminal, éditeur et lanceur graphique. Un chemin fixe dans un répertoire protégé de support est plus simple à comprendre, même si le sandbox et l'installation peuvent le contraindre. Si une commande d'amorçage donne le chemin, authentifiez son résultat au lieu d'accepter un chemin arbitraire de la configuration du projet.
Le décalage de versions touche les deux modèles. Avec stdio, le client choisit l'exécutable shim. Avec un socket résident, un ancien client peut joindre un nouveau service, ou l'inverse, pendant une mise à jour progressive. Négociez une petite version lors du premier échange. Refusez les combinaisons incompatibles avant l'approbation, car un utilisateur ne peut pas approuver utilement une requête interprétée différemment par les deux parties.
Les équipes d'exploitation aiment parfois les sockets que leurs outils savent lister. Les développeurs aiment parfois stdio, dont la commande se lance dans le terminal. Aucune commodité ne doit devenir une porte de débogage. Un client de diagnostic capable d'envoyer des requêtes privilégiées suit le même chemin d'attribution et d'approbation. Un flag qui saute l'autorisation finira par quitter une machine de développement.
La comparaison change si le courtier n'est pas résident. Démarrer une application graphique complète à chaque connexion stdio entraîne lenteur et invites maladroites. Garder un service socket uniquement pour éviter un petit shim ajoute mises à jour et nettoyage. Choisissez d'abord la durée de l'application, puis adaptez le transport externe.
Reliez le dessin à des menaces explicites
L'équipe plateforme doit noter quelle propriété répond à chaque menace. Le tableau suivant est une trace de décision, pas un score. Un transport ne gagne une ligne que si son implémentation apporte le contrôle indiqué.
| Menace ou besoin | Conception stdio | Conception socket Unix |
|---|---|---|
| Un autre utilisateur tente l'accès | Descripteurs privés et héritage correct | Répertoire parent protégé et mode 0600 |
| Un autre processus du même utilisateur se connecte | Plus difficile si seul le lanceur détient le tube, mais l'héritage reste un risque | Prévu par défaut, donc identifiants du pair et identité de l'application requis |
| L'approbation finit avec une exécution | EOF et sortie surveillée donnent des signaux naturels | Le serveur crée une session liée à la connexion et au processus |
| Plusieurs agents partagent un coffre | Chaque shim exige un saut privé authentifié vers le coffre | Le service résident accepte des connexions authentifiées séparées |
| Le courtier tombe et redémarre | Le client voit EOF et relance ou signale l'échec | Le service gère le chemin obsolète et impose reconnexion et réautorisation |
| L'exécutable change pendant l'exécution | L'identité capturée reste liée à la session | L'identité reste liée à la connexion ; une reconnexion relance l'évaluation |
| Première installation simple | Commande et exécutable signé, sans endpoint | Enregistrement au démarrage, chemin protégé, découverte et nettoyage |
| Ordre d'audit central | Le noyau partagé ordonne les événements des shims | Naturel dans un service résident, mais le journal durable reste à concevoir |
Deux conclusions subsistent souvent. Un attaquant sous le même utilisateur efface une grande partie du réconfort des bits de mode. La durée de l'approbation compte autant que l'accès à l'endpoint. Un socket privé approuvé toute la journée peut accorder plus d'autorité qu'un canal stdio soigneusement borné.
N'attribuez pas de poids numériques sans pouvoir les défendre. Pour un identifiant sensible, un seul contrôle d'identité manquant peut dominer tous les avantages de déploiement. Écrivez un test d'acceptation pour chaque ligne. Lancez par exemple un processus non approuvé sous le même utilisateur et prouvez le refus ; approuvez un agent, tuez-le, gardez un descendant vivant et prouvez que l'ancienne autorisation ne se réutilise pas.
Les choix peuvent aussi se superposer. MCP utilise stdio entre l'agent et un petit shim, lequel parle au courtier résident par socket privé ou autre IPC. C'est souvent une bonne forme de bureau, mais elle crée deux limites. Authentifiez les deux. Le shim prouve quelle exécution il représente, et l'application prouve qu'il a joint le bon courtier plutôt qu'un endpoint substitué par le projet.
Placez l'identité dans une session vérifiée
Une petite enveloppe de protocole rend la décision de sécurité vérifiable. Elle ne remplace pas l'identité du système. Elle lie des faits déjà vérifiés par le courtier à la requête qu'il va exécuter.
Après inspection de l'appelant vivant, le courtier crée identifiant de session et nonce, puis les renvoie sur le canal authentifié. Chaque requête suivante porte cet identifiant, un numéro croissant, la capacité et les paramètres. Le courtier refuse une session inconnue, une séquence répétée ou sautée lorsque l'ordre est strict, une capacité non approuvée ou une requête reçue par un autre canal.
{"session_id":"s_7M4K","sequence":12,"capability":"http:billing.read","action":{"method":"GET","path":"/v1/invoices"}}
La réponse répète l'identité et l'état de l'opération, sans secret ni en-tête injecté :
{"operation_id":"op_01J8","sequence":12,"state":"completed","result":{"status":200,"body_ref":"activity:8841"}}
Ce sont des formes de protocole, pas des noms universels. L'intérêt est que le serveur attribue l'identité, impose l'ordre et renvoie une référence durable. Ne signez pas l'enveloppe avec une clé remise à l'agent, car il détiendrait alors un identifiant secret. Liez-la au canal local authentifié et gardez les secrets dans le courtier.
Pour stdio, la liaison peut inclure une valeur aléatoire transmise seulement dans le nouveau tube et l'identité du lanceur. Pour un socket, elle inclut la connexion acceptée et le jeton d'audit du pair. Si des requêtes passent entre connexions, définissez une reprise volontaire avec durée courte et jetons à usage unique. Un identifiant copié d'un journal ne doit pas restaurer l'autorité.
Séparez les données d'approbation des données d'affichage. Chemin du dépôt, étiquette fournie par l'agent, nom de l'outil et motif demandé aident à décider, mais l'attaquant peut les choisir. Le journal distingue faits de processus vérifiés, déclarations, capacités approuvées et contenu. Lors d'un incident, une belle étiquette ne passera pas pour une preuve.
Un courtier de bureau peut employer les deux sans confusion
Pour une application macOS signée et toujours active, la conception la plus claire utilise souvent stdio à la frontière MCP et un saut local authentifié jusqu'à l'application. L'agent obtient le modèle de lancement attendu et une durée liée au processus. L'application conserve un coffre, une interface d'approbation et un historique ordonné. Cela ne marche que si le saut intérieur préserve l'identité extérieure au lieu de réduire tous les shims à un client de confiance.
Sallyport adopte cette forme : les agents compatibles MCP lancent le shim sp mcp, tandis que l'application exécute les actions HTTP et SSH afin que les secrets n'atteignent pas l'agent. Son approbation de session commence par l'autorité de signature du processus, le point faible auquel les droits de socket ne répondent pas.
Ce choix ne rend pas les sockets mauvais ni stdio suffisant. Une équipe qui construit plusieurs clients natifs peut exposer un socket protégé comme API principale. Elle assume alors inspection du pair, construction des sessions, reprise des endpoints obsolètes et compatibilité. Si elle ne peut expliquer chacun de ces points, le socket n'est pas prêt à transporter des identifiants.
Choisissez la conception dont l'échec se refuse le plus facilement. Si l'attribution échoue, refusez l'action. Si le courtier ne prouve pas que l'endpoint appartient à son instance, ne connectez pas et ne faites pas unlink. Après tout redémarrage, créez une nouvelle session. Ces règles coûtent un peu de confort et suppriment la continuité silencieuse qui rend les courtiers locaux dangereux.
La revue finale tient sur une page : unité d'autorisation, preuve vérifiée, début et fin de session, propriété de l'endpoint, panne, mise à jour et audit. Si une réponse dépend de « même utilisateur », dites si le malware de ce compte est hors modèle. Cette phrase révèle si stdio et sockets sont comparés comme transports ou utilisés à la place d'une décision de sécurité.
FAQ
MCP stdio est-il plus sûr qu'un socket Unix ?
Pas en soi. Stdio limite naturellement le canal à un processus lancé, tandis qu'un socket favorise un service résident ; la sécurité dépend de l'accord entre cette durée et l'unité d'autorisation.
Le mode 0600 authentifie-t-il l'application appelante ?
Non. Il limite généralement l'accès au propriétaire, mais chaque processus de cet utilisateur peut essayer de se connecter. Inspectez le pair vivant et prenez une décision d'autorisation distincte.
Un courtier peut-il servir plusieurs agents via stdio ?
Oui. Lancez un shim par agent et donnez à chacun un saut privé authentifié vers le noyau partagé. Séparez les identités pour qu'un shim ne réclame pas l'approbation d'un autre.
Que faire si un client stdio tombe ?
Le courtier révoque les appels futurs à EOF ou à la sortie et consigne le résultat du travail actif. Si une action distante a pu finir, notez indeterminate plutôt que de réessayer aveuglément.
Comment supprimer un socket Unix obsolète ?
Inspectez l'objet sans suivre les liens et vérifiez type, propriétaire et relation avec une instance morte. Ne faites jamais unlink uniquement parce que bind renvoie EADDRINUSE.
Les sockets Unix sont-ils assez rapides pour MCP ?
Oui pour les requêtes ordinaires. Le transport local pèse peu face à HTTP ou SSH, donc identité et durée doivent décider avant le débit.
L'autorisation doit-elle survivre à un redémarrage ?
En général non. Le redémarrage rompt le canal et les preuves du processus ; exigez reconnexion et nouvelle session, sauf promesse explicite d'autorisation durable et protégée.
Le client peut-il envoyer son nom de processus ?
Il peut fournir une étiquette d'affichage, mais le courtier ne doit pas lui faire confiance. Dérivez l'identité du processus vivant, du jeton d'audit, de la signature ou de la relation de lancement.
Où macOS doit-il placer un socket Unix ?
Utilisez un parent contrôlé par l'utilisateur ou l'application et refusez groupe et autres. Définissez aussi comment terminal, éditeur et interface trouvent ce chemin sans faire confiance au projet.
Quand combiner stdio et socket ?
Cela convient à un coffre résident servant des agents MCP lancés séparément. Le bord stdio fournit des sessions par processus ; le socket interne exige encore authentification et contexte vérifié.