MagicCode/GUIDE_PERMISSIONS.md

11 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 — 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, <(…)) 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 :

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

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

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

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