MagicCode/GUIDE_PERMISSIONS.md
2026-10-26 23:17:20 +01:00

17 KiB

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 (à la question, s = « stop » : l'action n'est pas exécutée, le travail s'interrompt sans rien annuler pour vous laisser donner d'autres informations) — 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.

{
  "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 :

"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 :

{
  "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 :

"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 :

{
  "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 :

{
  "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 :

"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 :

{ "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) :

{
  "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 :

{
  "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 :

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

{
  "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)"]