pi n’a pas de système de permissions, et il ne s’en cache pas : son README le dit en toutes lettres (« Pi does not include a built-in permission system for restricting filesystem, process, network, or credential access »), et il vous renvoie directement vers une sandbox si vous en voulez une (« If you need stronger boundaries, containerize or sandbox Pi »). Je pars du principe que c’est exactement ce que vous cherchez si vous lisez cet article, donc allons-y.

Claude Code a son propre système de permissions, qui vous demande une confirmation avant d’écrire. opencode aussi. Chez eux, Greywall renforce une première ligne qui existe déjà. Chez pi, il n’y a rien à renforcer : c’est la seule ligne qui existe.

Qu’apporte Greywall à un harness ?

Greywall est une sandbox sans conteneur, en deny-by-default, bâtie sur bubblewrap, Landlock, seccomp et un proxy réseau transparent. Pour pi, elle apporte 3 choses : elle décide quels fichiers l’agent ouvre, au fichier près ; elle confine les connexions sortantes à une liste blanche journalisée (un agent qui atteint github.com exfiltre par un gist sans effort, pensez-y) ; et elle remplace les credentials de l’environnement par des placeholders, afin qu’une clé d’API ne traverse jamais en clair le processus sandboxé. J’ai détaillé ce montage, mesures à l’appui, dans Sécuriser pi, agent de code IA, avec Greywall.

Pourquoi le profil intégré ne fait pas le travail ?

Greywall embarque un profil pi, que vous appelez par --profile pi. Je ne le recommande pas, pour 3 raisons.

Côté réseau, il n’ouvre que api.openai.com et api.anthropic.com (donc si votre fournisseur est Z.ai, OpenRouter ou Ollama Cloud, votre agent tourne dans le vide). Côté chemins, 2 de ses 3 entrées n’ont jamais existé chez pi, car ~/.config/pi et ~/.cache/pi sont des chemins XDG qu’il n’a jamais utilisés, lui qui range tout sous ~/.pi/agent. Et côté sécurité, le vrai problème : son allowWrite: ["~/.pi"] laisse inscriptibles extensions/, git/, bin/, settings.json et auth.json, c’est-à-dire exactement les éléments par lesquels un agent se rend persistant d’une session à la suivante. Vous sandboxez pi et vous lui laissez la possibilité de réécrire son propre code de démarrage : ce n’est pas un détail.

Le correctif évident échoue, et sans le dire

Retirer ~/.pi de allowWrite est le premier réflexe. Mais il ne marche pas, car pi lit ses settings à travers proper-lockfile, qui crée le répertoire ~/.pi/agent/settings.json.lock avant chaque accès (oui, un répertoire, pas un fichier). Le parent étant en lecture seule, ce mkdir renvoie EROFS, donc pi n’ouvre pas ses settings, donc il ignore le champ packages, et aucune extension installée ne se charge.

Le seul signe est un avertissement d’apparence anodine au démarrage, qui ne dit rien du vrai problème : un agent amputé de ses packages, sans qu’aucun message ne relie l’un à l’autre. On ne le comprend qu’en comparant pi list dans la sandbox et hors de la sandbox.

Le profil que je propose

Je garde ~/.pi/agent inscriptible pour le verrou, et je gèle chaque cible par denyWrite. J’ai vérifié qu’un chemin denyWrite tient même avec un parent inscriptible, car c’est un point de montage : EROFS en écriture, EBUSY sur rm comme sur mv, et récursif sur un répertoire (autant dire qu’on ne le contourne pas en passant par la porte de derrière).

Le profil fait 3 entrées d’allowWrite et 13 de denyWrite. Il est ici :

https://gist.github.com/mwolff44/20a51174c89b13f30f85a814126a3cca

Il referme les vecteurs de persistance de pi, rend auth.json lisible mais non modifiable (pi doit bien s’authentifier quelque part), gèle les politiques de sécurité tout en laissant security/ inscriptible afin que le journal d’audit et sa rotation continuent de tourner, et maintient ~/.agents, ~/.skills et ~/AGENTS.md hors écriture, ce qui referme l’injection de prompt persistante.

Copiez-le dans ~/.config/greywall/learned/pi.json, remplacez YOUR_PROVIDER par le domaine de votre fournisseur, puis lancez l’agent :

greywall --profile pi -- pi

2 avertissements avant de vous y fier. Un fichier placé dans learned/ remplace intégralement le profil intégré, socle de protection compris, donc celui-ci doit rester autosuffisant : vous ne pouvez pas en retirer des lignes au hasard sous prétexte qu’elles semblent redondantes. Et il ne porte que les ouvertures, car les refus globaux (dont le masquage de votre trousseau système) se déclarent à part dans ~/.config/greywall/greywall.json, que l’article détaille.

Ce que la sandbox ne couvre pas

Une brèche que je n’avais pas anticipée. Le blocage de commandes de Greywall découpe la chaîne sur |, ||, && et ;, puis compare chaque segment par préfixe strict, mais il ne découpe jamais sur un retour à la ligne. Un git push --force placé en deuxième ligne d’un script, dans un heredoc ou dans un fichier .sh, traverse donc command.deny sans être vu. C’est vérifié, et face à un agent qui écrit des scripts multi-lignes toute la journée, ce n’est pas un cas d’école.

Greywall ne peut pas s’en sortir seul : il n’observe qu’une chaîne shell déjà découpée. Il faut que quelque chose inspecte l’appel d’outil avant qu’il ne devienne cette chaîne, et c’est le travail de pi-secured-setup, l’extension que je maintiens pour pi (guards, scanners et journal d’audit détaillés dans Sécuriser pi de l’intérieur). Elle referme aussi un autre trou : la substitution de credentials de Greywall ne couvre que les en-têtes et les paramètres d’URL, jamais le corps d’une requête POST, là où l’extension scanne et redacte les secrets qu’elle y trouve.

Encore faut-il regarder de près, car les 4 formes du contournement ne tombent pas sur le même filet. Mesuré sur la version 1.1.1 :

FormeSegment classéVerdict
git push --force en 2ᵉ ligne d’un scriptle script entierdangerous
bash <<< "git push --force"la chaîne entièredangerous
heredoc bash <<EOFl’ouverture seulecommande inconnue
bash script.shl’appel seulcommande inconnue

Les 2 premières tombent sur une vraie règle, une expression rationnelle non ancrée testée sur la totalité du segment, donc le retour à la ligne ne la gêne pas. Les 2 dernières tombent sur un filet plus lâche, celui des commandes inconnues, car l’extension absorbe le corps d’un heredoc à dessein (pour qu’une ligne comme rm -rf / n’y soit pas prise pour une commande autonome) et n’ouvre évidemment pas un fichier de script pour lire ce qu’il contient.

Les 4 finissent par demander une confirmation, y compris sans personne pour y répondre : lancé avec pi -p ou dans un pipeline, sans interface disponible, une demande de confirmation devient un refus, donc le comportement reste fail-closed. Mais les 2 dernières ne tiennent qu’à ce que bash et sh restent non classés ailleurs. Or les règles sont additives : un .pi/security/command-rules.json de projet peut les faire basculer en safe ou moderate, et la confirmation devient une approbation automatique sans que personne ne l’ait vraiment décidé. Voilà le maillon à surveiller.

Les 2 couches ne se recouvrent donc pas, elles se complètent : Greywall décide ce que le processus peut atteindre, l’extension décide ce que l’agent peut demander. Reste à les faire cohabiter sans qu’elles se marchent dessus, ce sera le sujet du prochain article.