MagicCode/GUIDE_PERMISSIONS.md

256 lines
11 KiB
Markdown

# 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)"]` |