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)
Outilseul (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
denyetask: 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 dansecho $(rm -rf x). - Une commande qui contient une substitution (
$(…), backticks,<(…),>(…)) n'est jamais autorisée automatiquement par unallow: 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ègleRedirect(§ 8). Sans règleRedirect, 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 :
podman run --rm alpine sh -c "echo 'hello world'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 *)autorisegreppartout, 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 parrm.Bash(* rm *):rmapparaî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
Redirectne concerne que les redirections de sortie. Une redirection d'entrée (< fichier) lit un fichier : elle n'est pas contrôlée (utilisezBash(cat .env)ouRead(.env)endenypour 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èglesBash(...), par exempledeny: ["Bash(tee *)", "Bash(* tee *)"]. - Les règles
Redirectvont dans les mêmes fichierssettings.jsonque les autres et s'additionnent comme elles. Un sous-agent peut en ajouter dans la propriétépermissionsde sonAGENT.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')"ouperl -e …suppriment des fichiers sans écrirerm. 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 (listeallowcourte) 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
denysur les outils de fichiers (Read(.env),Write(.git/**)) ne concernent que ces outils :Bash(cat .env)ou une redirection> .envsont 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)"] |