# 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, `<(…)`, `>(…)`) n'est **jamais** autorisée automatiquement par un `allow` : MAIA demande toujours. - Une commande qui contient une **redirection de sortie** (`>`, `>>`, `2>`, `&>`…) n'est autorisée que si le fichier visé l'est aussi par une règle `Redirect` (§ 8). Sans règle `Redirect`, 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 vers un fichier, voir § 8) | | `ls 2>&1` | autorisée (duplication de flux, aucun fichier) | | `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. Redirections (`>`, `>>`, `2>`, `&>`…) Une redirection de sortie écrit dans un fichier **sans passer par l'outil `Write`**. Elle est donc contrôlée à part, avec des règles **`Redirect(chemin)`** (ou `Redirect` seul pour « toutes les redirections »). Le chemin suit la même syntaxe que `Read`/`Write` : relatif au dossier de lancement, `*` et `**` acceptés, un dossier couvre son contenu. ### 8.1 Comportement par défaut : on demande Tant qu'aucune règle `Redirect` ne les autorise, les redirections vers un fichier **repassent en confirmation**, même si la commande elle-même est autorisée : ```json { "permissions": { "allow": ["Bash(ls *)"] } } ``` | Commande | Décision | | --- | --- | | `ls` | autorisée | | `ls > out.txt` · `ls >> out.txt` · `ls 2> err.txt` · `ls &> all.txt` | confirmation | | `ls 2>&1` · `ls >&2` | autorisées : ce sont des **duplications de flux**, elles n'écrivent dans aucun fichier | | `echo "a > b"` (avec `Bash(echo *)`) | autorisée : un `>` entre guillemets n'est pas une redirection | C'est la configuration la plus sûre : rien à faire pour **ne pas** autoriser les redirections. ### 8.2 Autoriser certaines redirections Ajoutez des règles `Redirect(...)` dans `allow`. Le fichier visé doit être couvert **et** la commande doit être autorisée par ailleurs (`Redirect` seul n'autorise aucune commande) : ```json { "permissions": { "allow": [ "Bash(ls *)", "Bash(npm run build *)", "Redirect(/dev/null)", "Redirect(logs/**)" ] } } ``` | Commande | Décision | | --- | --- | | `ls > /dev/null` · `ls 2>/dev/null` | autorisées | | `npm run build >> logs/build.log 2>&1` | autorisée | | `ls > src/main.ts` | confirmation (fichier non couvert) | | `ls > logs/a.log > src/x` | confirmation : **toutes** les cibles doivent être couvertes | | `cat f > logs/a.log` | confirmation : `cat` n'est pas autorisé | | `ls > logs/*.txt` · `ls > $HOME/x` · `ls > logs/../src/a` | confirmation : une cible qui dépend du shell (`*`, `?`, `[ ]`, `{ }`, `$`) n'est jamais considérée comme couverte | Pour autoriser **toutes** les redirections (à réserver à un environnement jetable ou combiné à des `deny`) : `"Redirect"`. ### 8.3 Interdire ou toujours demander `deny` et `ask` s'appliquent même si la commande est autorisée, et même avec `"Bash"` complet dans `allow` : ```json { "permissions": { "allow": ["Bash", "Redirect"], "deny": ["Redirect(.env)", "Redirect(.git/**)", "Redirect(~/**)"], "ask": ["Redirect(docs/**)"] } } ``` | Commande | Décision | | --- | --- | | `echo x > .env` | refusée | | `echo x >> .git/config` | refusée | | `echo x >> ~/.bashrc` | refusée (le `~` est comparé tel qu'il est écrit : le motif `~/**` le reconnaît) | | `echo y 2> .env` (placé dans une suite `a; b`) | refusée | | `echo x > docs/a.md` | confirmation | | `echo x > notes.txt` | autorisée | ### 8.4 À savoir - `Redirect` ne concerne que les redirections de **sortie**. Une redirection d'entrée (`< fichier`) lit un fichier : elle n'est pas contrôlée (utilisez `Bash(cat .env)` ou `Read(.env)` en `deny` pour protéger une lecture). - Une redirection sans commande (`> fichier`, qui vide un fichier), un guillemet non fermé ou une cible absente : confirmation. - Les substitutions (`> $(…)`, `>(…)`) sont toujours en confirmation. - D'autres commandes écrivent sans `>` : `tee`, `cp`, `mv`, `dd of=…`, `sed -i`. Autorisez-les (ou interdisez-les) explicitement avec des règles `Bash(...)`, par exemple `deny: ["Bash(tee *)", "Bash(* tee *)"]`. - Les règles `Redirect` vont dans les mêmes fichiers `settings.json` que les autres et s'additionnent comme elles. Un sous-agent peut en ajouter dans la propriété `permissions` de son `AGENT.md`. ## 9. 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)` ou une redirection `> .env` sont d'autres règles (`Bash(...)`, `Redirect(...)`), à 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. ## 10. 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]`. ## 11. Serveurs MCP : `Mcp(serveur[,outil])` Les serveurs MCP sont déclarés une fois pour toutes dans `~/.maia/mcp.json` (voir le README) ; **aucun n'est actif tant qu'une règle `allow` ne l'autorise**. Syntaxe : `Mcp(clé du serveur)` ou `Mcp(clé du serveur,outil)`. | Règle | Effet | | --- | --- | | `allow: ["Mcp(github)"]` | active `github` ; tous ses outils sont autorisés sans confirmation | | `deny: ["Mcp(github,delete_repository)"]` | l'outil n'est jamais proposé au modèle (`*` accepté : `Mcp(github,delete_*)`) | | `ask: ["Mcp(github,merge_pull_request)"]` | cet outil demande confirmation malgré le `allow` du serveur | | `allow: ["Mcp(github)"]` + `deny: ["Mcp(github,*)"]` | serveur **désactivé** : MAIA ne tente pas de s'y connecter | | `allow: ["Mcp(*)"]` (= `Mcp(*,*)`) | active **tous** les serveurs déclarés dans `mcp.json`, tous leurs outils autorisés | | `deny: ["Mcp(*,delete_repository)"]` | retire cet outil (motif accepté) sur tous les serveurs ; en `ask` : confirmation partout | | `allow: ["Mcp(github,list_issues)"]` | active `github` ; seul cet outil est autorisé, les autres demandent confirmation | Ces règles sont lues **avant** toute connexion ; deny > ask > allow, comme pour les autres outils. Mettre `allow` dans `~/.maia/settings.json` active le serveur pour tous les projets. ## 12. Aide-mémoire ```json { "permissions": { "allow": [ "Read", "Glob", "Grep", "Edit(src/**)", "Bash(ls *)", "Bash(git status)", "Bash(git diff *)", "Bash(npm run test:*)", "Redirect(/dev/null)", "Redirect(logs/**)", "WebFetch(domain:docs.python.org)" ], "ask": [ "Bash(git push *)" ], "deny": [ "Bash(rm *)", "Bash(* rm *)", "Bash(sudo *)", "Bash(* sudo *)", "Read(.env)", "Write(.git/**)", "Redirect(.env)", "Redirect(.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 *)"]` | | autoriser les redirections vers `/dev/null` et `logs/` | `allow: ["Bash(ls *)", "Redirect(/dev/null)", "Redirect(logs/**)"]` (§ 8.2) | | ne pas autoriser les redirections | ne rien ajouter : elles demandent confirmation (§ 8.1) | | interdire d'écrire dans un fichier par `>` | `deny: ["Redirect(.env)"]` (§ 8.3) | | protéger un fichier | `deny: ["Read(.env)", "Write(.env)", "Edit(.env)"]` | | limiter les sites web | `allow: ["WebFetch(domain:docs.python.org)"]` |