# Arbres de processus MCP : repérer les processus enfants inattendus

Les serveurs MCP locaux donnent facilement l’illusion qu’une limite de protocole propre garantit aussi une limite d’exécution propre. L’agent envoie un appel d’outil, le serveur renvoie un résultat et la transcription semble contenue. Pendant ce temps, le serveur peut démarrer un shell, un exécuteur de paquets, un environnement d’exécution, un compilateur, un client SSH ou un auxiliaire téléchargé dans un répertoire de projet. L’arbre des processus montre où le travail a réellement été effectué.

Pour examiner un serveur MCP, je ne le considère pas comme un processus unique. Je considère le serveur et chacun de ses descendants comme une même famille d’exécution jusqu’à la fin de ces descendants. Cette règle permet de repérer les problèmes que les transcriptions d’outils ne montrent presque jamais : un argument qui a amené un shell à lancer autre chose, un auxiliaire choisi via `PATH`, un enfant qui a conservé un jeton API ou un programme persistant qui a ouvert un socket alors que la requête initiale semblait terminée.

## Un appel d’outil peut devenir plusieurs programmes locaux

Un serveur MCP lance souvent des processus enfants pour des raisons ordinaires. Un outil de dépôt peut appeler `git`, un outil de code peut invoquer un formateur, un outil d’infrastructure peut appeler `ssh`, et un serveur qui gère les paquets peut démarrer un environnement d’exécution puis un exécutable de paquet. La présence d’un enfant ne prouve pas une compromission.

La distinction utile oppose l’**auxiliaire attendu** au **chemin d’exécution non comptabilisé**. Un auxiliaire attendu a une fonction documentée, un emplacement d’exécutable connu, des arguments adaptés à la demande et une durée de vie cohérente avec la tâche. Un chemin non comptabilisé rompt l’une de ces hypothèses. Il est peut-être inoffensif, mais il faut l’expliquer avant de lui donner accès à un identifiant, au système de fichiers ou au réseau.

Un PID parent ne suffit pas. La création d’un processus Unix établit une filiation, pas un contrat de sécurité. Un descendant hérite généralement de l’identifiant utilisateur du parent, de son répertoire courant, de ses variables d’environnement, de ses limites de ressources et parfois de ses descripteurs de fichiers ouverts. L’héritage exact dépend de la manière dont le parent le démarre, mais l’enfant a généralement reçu assez de contexte pour agir comme le parent là où cela compte.

C’est pourquoi « le serveur MCP n’appelle qu’une seule commande » n’est pas une garantie utile. `tool-server` peut démarrer `/bin/sh -c`, qui démarre `node`, qui démarre un script de paquet, qui démarre `curl`. L’examinateur qui n’enregistre que `tool-server` documente la partie la moins intéressante de la chaîne.

Le modèle `exec` de POSIX explique le risque. Un processus peut remplacer son image par un autre programme sans changer de PID. Une simple liste de processus prise avant et après une requête peut donc manquer un intermédiaire de courte durée. Lorsque la tâche est assez sensible, il faut à la fois une vue en arbre et des preuves d’événements.

## Établissez une référence avant de laisser un agent travailler

Une référence répertorie les enfants que votre serveur démarre lors d’une requête approuvée et sans particularité. Elle fournit un point de comparaison pour les observations ultérieures. Sans elle, chaque interpréteur semble suspect et chaque avertissement déclenche une discussion sur ce qui pourrait être normal.

Démarrez manuellement le serveur dans une session Terminal dédiée. Notez immédiatement son PID, puis capturez la table des processus avec les lignes de commande complètes :

```sh
ps -axo pid,ppid,user,etime,stat,command > mcp-process-baseline.txt
```

Les colonnes sont importantes. `PID` identifie le processus, `PPID` son parent direct, `USER` le compte qui en est propriétaire, `ETIME` la durée d’exécution écoulée et `STAT` peut révéler un processus arrêté ou zombie. `COMMAND` est aussi complet que le permet le système d’exploitation, mais c’est déjà le premier endroit où chercher un wrapper shell, un chemin temporaire ou un indicateur inattendu.

Sur macOS, `ps` ne fournit pas d’affichage en arbre disponible partout. Vous pouvez énumérer les enfants directs avec `pgrep` :

```sh
pgrep -P 48192 -alf
```

Remplacez `48192` par le PID du serveur. La sortie ressemble généralement à ceci :

```text
48207 /usr/bin/python3 /Users/me/tools/format_request.py
48211 /usr/bin/ssh -o BatchMode=yes build.example
```

Répétez ensuite la commande pour chaque PID enfant jusqu’à ce qu’aucun nouveau descendant n’apparaisse. Cela semble fastidieux, et ça l’est pour un examen ponctuel. Mais cela vous oblige aussi à voir la chaîne réelle au lieu de faire confiance à un schéma de documentation. Si vous installez `pstree` par votre procédure habituelle de gestion des paquets, `pstree -p 48192` fournit un instantané plus lisible. Ne faites pas de l’outil d’affichage un élément de votre garantie de sécurité. Il vous évite simplement de taper davantage.

Exécutez un petit ensemble de requêtes attendues : une interrogation en lecture seule du dépôt, l’action du formateur et une action SSH si le serveur en prend une en charge. Enregistrez un instantané pour chacune. Notez les chemins des exécutables, les arguments habituels, les répertoires de travail normaux et le fait que les auxiliaires se terminent ou non. Un compilateur qui apparaît pour une requête de compilation peut être attendu. Le même compilateur pendant une demande de résumé d’un fichier texte constitue un écart.

N’établissez pas votre référence à partir d’une copie de développement remplie de wrappers modifiables et de scripts de paquets, puis ne la déclarez pas fiable. Cette configuration vous indique ce que votre machine actuelle exécute par hasard. Elle ne vous indique pas ce que le serveur devrait avoir le droit d’exécuter. Écrivez séparément la liste des chemins d’exécutables attendus.

## Les wrappers shell masquent le processus que vous vouliez examiner

L’appel à un shell est courant parce qu’il facilite l’assemblage de commandes dynamiques. Il transforme aussi une limite entre arguments en texte, et le texte peut acquérir un sens par les guillemets, l’expansion, les motifs glob, les redirections, la substitution de commandes et les fonctions shell. L’auteur du serveur peut vouloir exécuter un formateur alors que le véritable processus enfant lance un shell qui décide ensuite du sens de la commande du formateur.

C’est une source fréquente d’examens trompeurs. Quelqu’un voit dans le code source le nom d’un binaire autorisé et suppose que c’est lui qui sera exécuté. Les preuves d’exécution montrent `/bin/sh -c ...`, puis le shell résout le reste. Ce sont deux affirmations différentes.

Comparez ces deux modèles dans une implémentation de serveur :

```js
spawn("/usr/bin/git", ["status", "--short"], {
  cwd: repositoryPath,
  shell: false
});
```

```js
exec(`git -C ${repositoryPath} status --short`);
```

Le premier modèle sépare l’exécutable et les arguments. Il faut tout de même valider `repositoryPath`, mais il évite l’analyse par un shell. Dans le second modèle, une erreur de gestion des guillemets peut permettre à `repositoryPath` de modifier la grammaire de la commande. Il ajoute aussi un processus shell à l’arbre, lequel peut lancer d’autres processus avant le démarrage de la commande finale.

N’acceptez pas « nous nettoyons les entrées » comme substitut à un vecteur d’arguments. Les filtres se dégradent à mesure que les options s’accumulent. Un développeur ajoute une fonction, autorise les espaces, ajoute un indicateur conditionnel, et l’ancien filtre ne décrit plus la grammaire de la commande. Un exécutable explicite accompagné d’un tableau d’arguments rend la limite visible dans le code et dans les sorties d’audit.

Si un serveur a réellement besoin de la syntaxe d’un shell, réduisez-la. Utilisez un script fixe conservé hors de l’espace de travail modifiable par l’agent, transmettez les données par des paramètres positionnels et inscrivez le chemin et l’empreinte du script dans vos notes de déploiement. Ne composez jamais une ligne shell à partir des entrées de l’outil en considérant ensuite l’enfant comme attendu simplement parce qu’il provient de votre arborescence source.

Le cas délicat concerne les outils de gestion des paquets. Des commandes comme les exécuteurs de paquets lancent souvent les scripts de cycle de vie fournis par le projet. Un serveur qui exécute une commande de paquet dans une copie de travail modifiée par l’agent peut donc lancer des commandes que l’agent a placées dans la configuration du projet. Ce n’est pas un défaut du gestionnaire de paquets. C’est une décision d’exécution que vous avez prise en traitant un dépôt modifiable comme une source d’instructions fiables.

## PATH et répertoires de travail changent le sens d’une commande

`git` n’est pas une identité d’exécutable. C’est une demande de recherche. Le processus le résout en parcourant `PATH`, et un dépôt contrôlé par l’agent peut influencer cette recherche si le serveur inclut des répertoires locaux au projet ou la configuration shell héritée. Un fichier nommé `git` dans un tel répertoire peut être exécuté avant `/usr/bin/git`.

Examinez l’environnement reçu par un processus, pas seulement la commande affichée dans la transcription de l’outil. Pour un processus dont vous êtes propriétaire, macOS permet sur de nombreuses versions d’interroger les informations d’environnement via `ps`, même si leur disponibilité varie. Une approche pratique consiste à faire journaliser par le serveur, au démarrage, un environnement volontairement court et expurgé : `PATH`, `HOME`, `TMPDIR`, le répertoire courant et les chemins des exécutables fixes. Ne journalisez pas les jetons d’accès, les cookies de session ni des vidages complets de l’environnement dans un journal de projet partagé.

Résolvez les commandes sensibles avec des chemins absolus. Ce n’est pas une formalité lorsqu’un agent peut modifier l’arbre de travail. Par exemple :

```sh
/usr/bin/git -C /Users/me/work/repo status --short
/usr/bin/ssh -o BatchMode=yes -o IdentitiesOnly=yes host.example
```

Les chemins absolus éliminent un problème de recherche. Ils ne rendent pas la cible sûre pour autant. Selon sa configuration, `git` peut appeler des hooks ou des programmes externes de comparaison. SSH peut charger une configuration et invoquer des auxiliaires. Le répertoire de travail contrôle également les lectures de fichiers relatifs, la configuration du projet et les sorties temporaires. Notez-le lorsque vous examinez un enfant.

Utilisez `lsof` pour examiner le répertoire courant et les fichiers ouverts d’un processus :

```sh
lsof -nP -p 48207 | sed -n '1,35p'
```

Cherchez `cwd` dans la colonne des descripteurs de fichiers, le texte de l’exécutable sous `txt`, ainsi que les fichiers situés dans le projet, les répertoires temporaires ou les emplacements d’identifiants. `lsof` fournit un instantané. Un enfant rapide peut apparaître puis disparaître avant son exécution, mais la sortie permet tout de même de repérer un serveur qui maintient discrètement un auxiliaire en vie.

Un enfant lancé depuis `/private/var/folders/...` mérite davantage d’attention qu’un enfant lancé depuis un répertoire d’application géré, surtout si son nom ressemble à celui d’un utilitaire normal. Les emplacements temporaires sont légitimes pour les produits de compilation. Ils sont aussi pratiques pour dissimuler un processus parmi des fichiers jetables. Demandez quel composant l’a créé et pourquoi ce composant avait besoin d’un exécutable à cet endroit.

## L’activité réseau doit correspondre à l’action demandée

Un enfant local peut dépasser la limite de la requête MCP même s’il n’écrit jamais de fichier suspect. Un formateur ne devrait généralement pas ouvrir de connexion sortante. Un auxiliaire SSH devrait se connecter à l’hôte demandé puis se terminer. Une installation de paquet peut contacter des registres, mais cette action doit faire l’objet d’un examen explicite, car elle peut récupérer et exécuter du nouveau code.

Inspectez les sockets du serveur et de chaque enfant qui reste en vie assez longtemps :

```sh
lsof -nP -i -p 48211
```

La sortie contient généralement le protocole, l’adresse locale, l’adresse distante et l’état de la connexion. Les indicateurs `-nP` empêchent les recherches de noms et de services, ce qui conserve une sortie littérale et évite d’ajouter du trafic de résolution pendant l’enquête. Une entrée `LISTEN` signifie que le processus accepte des connexions locales ou réseau. Une entrée `ESTABLISHED` signifie qu’il a un pair actif. Faites correspondre chaque entrée à l’action qui l’a provoquée.

Ne réagissez pas excessivement à tout processus utilisant une bibliothèque réseau. Certains outils de développement vérifient les services de mise à jour, l’état des certificats ou les métadonnées de dépendances. Je considère toutefois ce comportement comme une raison de renforcer le serveur. Un appel d’outil censé lire du code local ne devrait pas obtenir un canal sortant non déclaré simplement parce qu’un auxiliaire le trouve pratique.

Séparez l’injection d’identifiants de l’observation des processus. L’inspection peut vous dire que `curl` a été exécuté. Elle ne peut pas garantir que le jeton transmis dans une variable d’environnement n’a pas été copié, journalisé ou hérité par un petit-enfant. On recommande souvent de « placer simplement la clé API dans l’environnement enfant pour une seule commande ». Cette solution est populaire parce qu’elle est facile et fonctionne dans une démonstration. Elle ne convient pas aux actions exécutées par un agent, car un enfant peut afficher son environnement, le transmettre ou rester en vie après le retour de la commande visible.

Sallyport établit une limite différente pour ses canaux HTTP et SSH : l’agent ne reçoit pas l’identifiant API ou SSH enregistré, et l’application exécute elle-même l’action. Cela ne supprime pas la nécessité d’inspecter les enfants MCP locaux, mais évite de transformer chaque processus auxiliaire en détenteur potentiel du secret.

## Un problème commence souvent par un wrapper pratique et raisonnable

Prenons un serveur MCP local de dépôt qui propose un outil `run_test`. L’auteur veut une commande flexible. Le gestionnaire se place donc dans le dépôt et exécute un script de test défini par le projet au moyen d’une commande de paquet. L’agent a la permission de modifier la copie de travail, puisque modifier le code fait partie de sa tâche.

L’agent modifie le script du projet dans le cadre d’une correction proposée. Le serveur exécute l’outil de test. La commande de paquet démarre un shell, qui démarre l’environnement d’exécution, qui exécute le script modifié. Ce script démarre un auxiliaire en arrière-plan et redirige sa sortie vers un fichier temporaire. L’outil renvoie « tests réussis » parce que la commande au premier plan se termine correctement.

Rien dans cette séquence ne nécessite un exploit exotique. Le problème est que le serveur a traité des métadonnées de projet modifiables par l’agent comme une configuration exécutable approuvée. Un examinateur peut ne voir que la requête MCP initiale et le résultat positif. L’arbre des processus raconte l’histoire importante :

```text
mcp-repo-server(48192)
  package-runner(48230)
    sh(48233)
      runtime(48234)
        test-script(48240)
          helper(48247)
```

L’auxiliaire en arrière-plan est le point d’enquête. Vérifiez sa ligne de commande complète, le chemin de l’exécutable, la chaîne des parents, le répertoire de travail, les fichiers ouverts, les sockets et l’heure de démarrage. Vérifiez s’il survit après la fermeture de la session du serveur. Examinez ensuite la modification du projet qui a fourni le script. Ne présentez pas cela comme un simple échec de l’agent. Le serveur a proposé l’exécution d’instructions modifiables et a appelé le résultat un test.

La correction dépend du comportement attendu du produit. Un serveur prudent peut exécuter un exécutable de test fixe avec des arguments fixes. Si les scripts définis par le projet sont nécessaires, traitez le fichier de script et les métadonnées du paquet comme des entrées exécutables : affichez-les pour approbation, exécutez-les dans un environnement contraint et interdisez autant que possible la mise en arrière-plan. Au minimum, le serveur doit signaler chaque descendant qu’il a démarré, y compris celui qui survit à l’appel de l’outil.

La distinction importante oppose l’agent qui demande au serveur d’exécuter une commande de test connue et l’agent qui modifie le sens de cette commande avant son exécution par le serveur. Les deux peuvent apparaître comme `run_test` dans une transcription MCP. Leur niveau de risque est très différent.

## Observez la création des processus, pas seulement les instantanés

Les instantanés répondent à la question « qu’est-ce qui est encore en vie ? ». Ils ne répondent pas à « qu’est-ce qui a été exécuté pendant 200 millisecondes avant de se terminer ? ». Pour les appels d’outils suspects ou sensibles, observez la création des processus pendant l’exécution de la requête.

macOS inclut Endpoint Security pour les produits de sécurité disposant de l’habilitation nécessaire, mais les outils locaux ordinaires ne peuvent pas partir du principe qu’ils y ont accès. Ne concevez pas un système dépendant d’une collecte d’événements privilégiée si vous ne fournissez et n’exploitez pas réellement cette capacité. Pour une enquête de développement, utilisez un wrapper contrôlé autour de l’exécutable et recueillez les informations de démarrage et de fin, ou exécutez le serveur sous un moniteur de processus disponible dans votre environnement.

Un wrapper simple peut rendre l’exécution visible lorsque vous contrôlez le chemin de commande du serveur :

```sh
#!/bin/sh
printf '%s pid=%s ppid=%s cwd=%s argv=%s\n' \
  "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$$" "$PPID" "$(pwd)" "$*" \
  >> "$HOME/.local/state/mcp-exec.log"
exec /usr/bin/git "$@"
```

Cet artefact enregistre l’identité du wrapper, puis utilise `exec` pour le remplacer par `git`. Le wrapper ne reste donc pas comme parent sans intérêt. Il ne capture pas tous les descendants que `git` pourrait créer et ne doit pas recevoir de secrets dans ses arguments. Utilisez-le pour tester une hypothèse contrôlée, pas comme système d’audit complet.

Pour un processus existant, `dtruss` de macOS peut révéler les appels système, mais exige souvent des privilèges élevés et produit beaucoup de sortie. C’est un outil d’enquête, pas un mécanisme de surveillance courant. Commencez par l’arbre des processus, `lsof` et les journaux de l’application. Utilisez la trace des appels système lorsque vous devez répondre à une question précise, par exemple pour savoir si un enfant a exécuté un autre chemin ou s’est connecté à un socket.

Conservez les horodatages en UTC et enregistrez le PID parent avec chaque événement. Une ligne de commande sans parent ni heure constitue une preuve faible. Les valeurs de PID sont réutilisées après la fin des processus. Un instantané tardif peut donc associer par erreur un nouveau processus sans rapport avec un ancien incident. L’heure de démarrage fournie par `ps` ou votre propre journal d’événements aide à éviter cette erreur.

## L’approbation doit nommer la limite de l’exécutable

Une confirmation disant « Autoriser run_test ? » fournit trop peu d’informations à une personne. Elle doit savoir quel processus signé a initié l’action, quel exécutable sera lancé, quel est le répertoire de travail, si l’action peut accéder au réseau et si elle peut démarrer d’autres programmes. Une carte d’approbation vague apprend aux utilisateurs à approuver des actions vagues.

N’essayez pas de résoudre ce problème en demandant une approbation pour chaque appel à `fork`. Vous créeriez une fatigue décisionnelle, puis les utilisateurs approuveraient mécaniquement ou désactiveraient les demandes. Examinez les transitions importantes : un nouveau chemin d’exécutable, le passage d’un travail local à un accès réseau, une commande provenant d’un fichier de projet modifiable ou un enfant qui reste en vie après la requête.

Les informations de signature de code fournissent un signal d’identité utile sur macOS, mais ne leur accordez pas une portée excessive. Inspectez un exécutable avec :

```sh
codesign -dv --verbose=4 /path/to/executable 2>&1 | sed -n '1,20p'
```

La commande indique l’autorité de signature lorsqu’elle existe et signale une erreur pour du code non signé. Elle vous dit qui a signé le fichier inspecté par macOS. Elle ne vous dit pas si les arguments sont sûrs, si sa configuration est fiable ou s’il exécutera ensuite un script non signé. Examinez séparément le chemin de l’enfant et son contexte d’exécution.

L’autorisation de session de Sallyport commence par l’autorité de signature du processus connecté. Cela aide à décider si un nouveau processus agent peut agir. Le réglage de clé par appel convient au cas plus précis où l’utilisation d’un identifiant particulier doit être approuvée par une personne à chaque fois. Aucun de ces contrôles ne doit servir à prétendre que l’approbation d’un parent explique automatiquement chacun de ses descendants.

Formulez les demandes d’approbation autour de leurs conséquences. « Cette requête exécutera `/usr/bin/ssh` avec votre compte et se connectera à `host.example` » peut être examiné. « Cet outil a besoin d’un accès » ne le peut pas. Si l’outil peut invoquer des scripts du projet, dites-le clairement. Les personnes peuvent prendre une décision éclairée lorsque la demande a une forme concrète.

## Limitez le serveur avant d’examiner l’enfant

Lorsque vous trouvez un enfant inattendu, préservez assez de preuves pour l’expliquer, puis arrêtez la famille d’exécution. Ne commencez pas par supprimer les fichiers temporaires ou redémarrer la machine. Ces actions peuvent supprimer le seul chemin, la seule ligne de commande et les seules informations temporelles dont vous disposez.

Suivez cet ordre si l’enfant est encore actif :

1. Capturez la sortie de `ps` pour le serveur, son parent et les descendants connus. Notez les PID, les PPID, la durée écoulée et les commandes complètes.
2. Exécutez `lsof -nP -p <pid>` et `lsof -nP -i -p <pid>` pour le processus suspect. Enregistrez la sortie hors de l’espace de travail de l’agent.
3. Arrêtez le descendant suspect avec `kill <pid>` si une terminaison normale est sûre. N’utilisez `kill -KILL <pid>` que s’il ne se termine pas et que la poursuite de son exécution crée un risque inacceptable.
4. Arrêtez le serveur MCP et révoquez ou terminez la session de l’agent à l’origine de l’action. Vérifiez les enfants qui ont été réattribués après la fin du serveur.
5. Examinez l’exécutable, la source de son lancement et les modifications du dépôt ou de la configuration qui ont produit la commande.

`kill` ne termine pas automatiquement un groupe de processus ni tous les enfants. Un descendant mis en arrière-plan peut continuer après la fin de son parent direct. C’est pourquoi il faut obtenir l’arbre avant le nettoyage. Sur un serveur contrôlé, placez les auxiliaires dans un groupe de processus ou un périmètre de supervision dédié afin que le serveur puisse arrêter le groupe à la fin de la session. Testez ce comportement avec un auxiliaire volontairement placé en arrière-plan avant de vous y fier.

Vérifiez également la persistance. Sur macOS, `launchctl print` peut examiner les services de lancement chargés lorsque vous disposez d’un label ou d’un domaine précis. Ne déchargez pas aveuglément des tâches sans rapport parce que leur nom vous semble inhabituel. Faites d’abord correspondre un enfant suspect à son chemin d’exécutable, à sa configuration de lancement et à ses horodatages. Un processus qui revient après un redémarrage propre ou un nouveau démarrage du serveur nécessite une enquête différente de celle d’un processus présent uniquement pendant une session de test.

## Rendez chaque action explicable après coup

Un journal d’audit utile relie un processus agent approuvé par l’utilisateur à une requête, une action exécutée et son résultat observé. Les données de processus doivent ajouter le chemin de l’exécutable, la chaîne des parents, le répertoire de travail, les heures de démarrage et de fin ainsi que la destination de l’activité réseau. Sans l’arbre des processus, le journal ne peut pas indiquer si un auxiliaire faisait partie de la requête ou s’il s’agissait d’un processus indépendant de la machine.

Ne placez pas de secrets dans les journaux d’audit pour les rendre complets. Conservez les noms des identifiants ou des identifiants opaques, les métadonnées nécessaires aux examinateurs et le résultat de l’action après expurgation. Un journal qui résout la provenance en copiant des jetons porteurs crée un second coffre d’identifiants doté de contrôles plus faibles.

Les preuves d’altération comptent après un incident, car les journaux locaux peuvent être modifiés par le même compte que celui qui a exécuté la commande. Sallyport projette ses journaux Sessions et Activity à partir d’un journal d’audit chiffré et chaîné par hachage, et `sp audit verify` vérifie cette chaîne hors ligne sans clé de coffre. Cela constitue une preuve utile pour une action d’agent, mais les enregistrements de processus doivent toujours fournir assez de contexte pour expliquer ce que le serveur local a démarré.

Ajoutez une question à la procédure d’exploitation de votre serveur : « Quel exécutable cette requête a-t-elle fait tourner, et pourquoi cet exécutable avait-il le droit d’exister dans ce répertoire de travail ? » Si personne ne peut y répondre à partir de l’enregistrement de la requête et d’une capture des processus, réduisez la portée de l’outil. Un serveur MCP local incapable de rendre compte de ses descendants possède plus d’autorité que ses opérateurs ne peuvent en examiner en toute sécurité.
