# Guide des permissions de MAIA MAIA demande une confirmation avant d'utiliser un outil sensible (Bash, Write, Edit, WebFetch, Agent…). Les **permissions** permettent d'autoriser, de demander ou d'interdire à l'avance des usages précis, pour ne plus répondre « o/n » à chaque `ls` — et pour bloquer définitivement ce qui ne doit jamais s'exécuter. Les exemples de ce guide ont été vérifiés avec le moteur de permissions de MAIA (`packages/agent/src/permissions.ts`). ## 1. Où les définir | Fichier | Portée | | --- | --- | | `~/.maia/settings.json` | toutes vos sessions (dossier de l'utilisateur) | | `./.maia/settings.json` | le projet (dossier de lancement de MAIA) | Les deux fichiers s'**additionnent** (les listes sont fusionnées). Le fichier du dossier parent n'est pas lu. `~/.maia/settings.json` contient déjà la configuration LiteLLM : ajoutez simplement la propriété `permissions`. ```json { "LiteLLM_URL": "http://localhost:4000", "OpenAI_EndPoint": "/v1/chat/completions", "LiteLLM_Models": "/v1/models", "permissions": { "allow": ["Bash(ls *)", "Read"], "ask": ["Bash(git push *)"], "deny": ["Bash(rm *)"] } } ``` Les trois listes sont facultatives. La commande `/permissions` affiche les règles chargées, par fichier (badges `ALLOW`, `ASK`, `DENY`). Une règle invalide est ignorée avec un avertissement au démarrage. Les règles sont lues au démarrage, à `/clear` et à `/new` : relancez ou tapez `/clear` après une modification. ## 2. Les trois familles et leur priorité | Liste | Effet | | --- | --- | | `deny` | l'appel est **refusé** sans rien demander ; le modèle est prévenu de ne pas réessayer tel quel | | `ask` | l'appel demande **toujours** une confirmation | | `allow` | l'appel s'exécute **sans confirmation** | Ordre d'évaluation : **`deny` > `ask` > `allow`**. La première famille qui correspond décide, quelle que soit la précision des règles : un `allow` très précis ne peut jamais lever un `deny` ou un `ask` plus large. Si **aucune** règle ne s'applique, MAIA demande une confirmation (c'est le comportement par défaut, pour tous les outils). ## 3. Syntaxe d'une règle ``` Outil Outil(motif) ``` - `Outil` seul (ex. `"Read"`) : **tous** les usages de l'outil. - `Outil(motif)` : seulement les usages qui correspondent au motif. Le nom de l'outil respecte la casse (`Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebFetch`, `Skill`, `Agent`, outils personnalisés…). - Chaque règle est une chaîne JSON : les guillemets doubles du motif doivent être échappés (`\"`). Le contenu du motif dépend de l'outil : | Outil | Le motif est… | Exemples | | --- | --- | --- | | `Bash` | une commande (voir § 4) | `Bash(ls *)`, `Bash(npm run test:*)` | | `Read`, `Write`, `Edit`, `Glob`, `Grep` | un motif de **chemin** relatif au dossier de lancement (glob `*`, `**`) ; un dossier couvre son contenu | `Read(src/**)`, `Edit(docs)`, `Write(*.md)`, `Read(.env)` | | `WebFetch` | `domain:nom.de.domaine` (le `*` est accepté) | `WebFetch(domain:docs.python.org)`, `WebFetch(domain:*.github.com)` | | `Skill` | le nom du skill | `Skill(api-docs)` | | `Agent` | le nom du sous-agent | `Agent(code_reviewer)` | | outils personnalisés | un motif (`*`) comparé aux valeurs des paramètres | `addition(*)` | Exemples hors Bash : ```json "permissions": { "allow": ["Read", "Glob", "Grep", "Edit(src/**)", "WebFetch(domain:docs.python.org)", "Agent(code_reviewer)"], "deny": ["Read(.env)", "Write(.git/**)"] } ``` ## 4. Les règles `Bash` ### 4.1 Comment une commande est analysée MAIA découpe la commande aux séparateurs `&&`, `||`, `;`, `|` et aux retours à la ligne, **sans tenir compte des guillemets**. Chaque morceau est une « sous-commande ». - Pour **`allow`** : **tous** les morceaux doivent être couverts par une règle. Un seul morceau non couvert, et MAIA redemande confirmation. - Pour **`deny` et `ask`** : **un seul** morceau qui correspond suffit. Le découpage est même plus fin : il coupe aussi aux `$(…)`, aux backticks, aux parenthèses et aux redirections, pour retrouver une commande cachée dans `echo $(rm -rf x)`. - Une commande qui contient une substitution (`$(…)`, backticks, `<(…)`) ou une redirection (`>`, `>>`) n'est **jamais** autorisée automatiquement par un `allow` : MAIA demande toujours. ### 4.2 Écrire un motif | Motif | Correspond à | Ne correspond pas à | | --- | --- | --- | | `ls` | `ls` exactement | `ls -la` | | `ls *` | `ls`, `ls -la`, `ls src` | `lsof` | | `npm run test:*` | tout ce qui **commence** par `npm run test:` (ancienne syntaxe « préfixe ») | `npm run build` | | `git * --help` | `git commit --help`, `git log --help` | `git commit` | | `git status` | `git status` exactement | `git status -s` | Règle pratique : `commande *` autorise la commande avec ou sans arguments ; la commande seule la limite à elle-même. Les espaces comptent. Un `*` au milieu remplace n'importe quelle suite de caractères. `"Bash"` seul autorise **toute** commande : à éviter sauf dans un environnement jetable. ## 5. Exemple simple : autoriser `ls` Pour ne plus confirmer `ls`, avec ou sans options : ```json { "permissions": { "allow": ["Bash(ls *)"] } } ``` Résultats : | Commande demandée par le modèle | Décision | | --- | --- | | `ls` | autorisée | | `ls -la src` | autorisée | | `lsof` | confirmation (autre commande) | | `ls && rm -rf x` | confirmation (le morceau `rm -rf x` n'est pas couvert) | | `ls > sortie.txt` | confirmation (redirection) | | `ls $(pwd)` | confirmation (substitution) | Pour autoriser plusieurs commandes de lecture : ```json "allow": ["Bash(ls *)", "Bash(cat *)", "Bash(grep *)", "Bash(git status)", "Bash(git diff *)", "Bash(git log *)"] ``` Un tube entre commandes toutes autorisées passe : `cat a.txt | grep foo` est autorisé avec `Bash(cat *)` et `Bash(grep *)`. ## 6. Exemple complexe : `podman run … sh -c "… | grep …"` Commande à autoriser : ``` podman run --rm alpine sh -c "echo 'hello world' | grep 'll'" ``` **Piège** : le `|` à l'intérieur des guillemets coupe quand même la commande en deux morceaux : 1. `podman run --rm alpine sh -c "echo 'hello world'` 2. `grep 'll'"` Une règle qui reproduit la commande entière n'est donc **jamais** reconnue, et une règle `podman … sh -c *` seule ne couvre que le premier morceau : MAIA redemande confirmation. (Les deux cas ont été vérifiés.) Chaque morceau doit avoir sa règle : ```json { "permissions": { "allow": [ "Bash(podman run --rm alpine sh -c *)", "Bash(grep *)" ] } } ``` Avec ces deux règles, la commande ci-dessus est autorisée sans confirmation. Remarques : - `Bash(podman run --rm alpine sh -c *)` limite l'image (`alpine`) et l'option `--rm` : `podman run --privileged …` ou une autre image redemandent confirmation. - `Bash(grep *)` autorise `grep` partout, pas seulement dans ce conteneur. C'est le prix du découpage : si c'est trop large, ne l'autorisez pas et acceptez la confirmation pour cette commande. - Dans un fichier JSON, écrivez la règle telle quelle ; il n'y a pas de guillemets à échapper tant que le motif n'en contient pas. Si le motif en contient : `"Bash(echo \"hello\")"`. Les morceaux non couverts restent bloqués : avec les mêmes règles, `podman run --rm alpine sh -c "echo x | rm -rf /tmp/y"` demande confirmation (le morceau `rm -rf /tmp/y"` n'est pas autorisé). ## 7. Interdire une commande, même cachée dans un tube Pour interdire `rm` quoi qu'il arrive : ```json { "permissions": { "deny": ["Bash(rm *)", "Bash(* rm *)"] } } ``` Les deux règles sont complémentaires : - `Bash(rm *)` : un morceau qui **commence** par `rm`. - `Bash(* rm *)` : `rm` apparaît comme un mot **après** autre chose (`xargs rm`, `sudo rm`, `time rm`…). Vérifications (avec ces deux règles) : | Commande | Décision | | --- | --- | | `rm -rf build` | refusée | | `ls && rm -rf build` | refusée | | `echo ok > f; rm f` | refusée | | `cat f \| (rm -f g)` | refusée | | `echo $(rm -rf x)` | refusée | | `ls \| xargs rm` | refusée (grâce à `* rm *`) | | `podman run --rm alpine sh -c "echo x \| rm -rf /tmp/y"` | refusée : le découpage de `deny` entre aussi dans les guillemets | Avec `Bash(rm *)` **seul**, `ls | xargs rm` n'est **pas** refusé (le morceau commence par `xargs`) : ajoutez `Bash(* rm *)`. Un `deny` l'emporte toujours sur un `allow` : même avec `Bash(podman run --rm alpine sh -c *)` et `Bash(grep *)` autorisés, la commande contenant `| rm -rf …` est refusée si `Bash(rm *)` est dans `deny`. Pour **demander confirmation** plutôt qu'interdire (ex. `git push`), utilisez `ask` : ```json "ask": ["Bash(git push *)", "Bash(git reset --hard *)"] ``` Même avec `"Bash(git *)"` en `allow`, ces commandes redemandent confirmation. ## 8. Limites : `deny` n'est pas un bac à sable Les règles analysent le **texte** de la commande. Elles ne peuvent pas tout voir : - `find . -delete`, `git clean -fd`, `truncate`, `mv x /dev/null`, `python -c "import os; os.remove('f')"` ou `perl -e …` suppriment des fichiers **sans** écrire `rm`. Ajoutez les règles correspondantes si elles vous importent (`Bash(find * -delete*)`, `Bash(git clean *)`), ou n'autorisez que les commandes dont vous avez besoin (liste `allow` courte) et laissez le reste en confirmation. - Une commande lancée depuis un script (`bash script.sh`) n'est pas analysée : le contenu du script est invisible. - Les `deny` sur les outils de fichiers (`Read(.env)`, `Write(.git/**)`) ne concernent que ces outils : `Bash(cat .env)` est une autre règle, à ajouter si besoin. Approche conseillée : peu de `allow` (lecture seule, tests, build), quelques `ask` (publication, écriture hors projet), des `deny` pour ce qui ne doit jamais arriver (`rm`, `sudo`, `curl … | sh`), et **laisser le défaut** (confirmation) pour tout le reste. ## 9. Sous-agents Un sous-agent hérite des permissions de l'agent principal. Sa définition (`AGENT.md`) peut en **ajouter** : ```yaml permissions: allow: [Read, Grep] deny: [Bash] ``` Les règles s'additionnent : un `deny` ou un `ask` hérité n'est jamais levé par un sous-agent. Quand un sous-agent a besoin d'une confirmation, la question s'affiche dans l'interface principale, préfixée `[sous-agent nom]`. ## 10. Aide-mémoire ```json { "permissions": { "allow": [ "Read", "Glob", "Grep", "Edit(src/**)", "Bash(ls *)", "Bash(git status)", "Bash(git diff *)", "Bash(npm run test:*)", "WebFetch(domain:docs.python.org)" ], "ask": [ "Bash(git push *)" ], "deny": [ "Bash(rm *)", "Bash(* rm *)", "Bash(sudo *)", "Bash(* sudo *)", "Read(.env)", "Write(.git/**)" ] } } ``` | Je veux… | Règle | | --- | --- | | autoriser `ls` et ses options | `allow: ["Bash(ls *)"]` | | autoriser une commande exacte, sans options | `allow: ["Bash(git status)"]` | | autoriser un préfixe de scripts npm | `allow: ["Bash(npm run test:*)"]` | | autoriser une commande avec tube | une règle **par morceau** (§ 6) | | toujours confirmer une commande | `ask: ["Bash(git push *)"]` | | interdire une commande partout, tubes compris | `deny: ["Bash(rm *)", "Bash(* rm *)"]` | | protéger un fichier | `deny: ["Read(.env)", "Write(.env)", "Edit(.env)"]` | | limiter les sites web | `allow: ["WebFetch(domain:docs.python.org)"]` |