dsh-perm-gate 5.0.3 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.ja.md +0 -2
  2. package/CHANGELOG.ko.md +0 -2
  3. package/CHANGELOG.md +4 -2
  4. package/INSTALL.ja.md +0 -15
  5. package/INSTALL.ko.md +0 -15
  6. package/INSTALL.md +0 -15
  7. package/INSTALL.zh.md +0 -15
  8. package/README.ja.md +6 -18
  9. package/README.ko.md +6 -18
  10. package/README.md +40 -19
  11. package/README.zh.md +34 -18
  12. package/assets/appcontainer-launcher.cs +703 -0
  13. package/dsh.plugin.json +2 -2
  14. package/lib/appcontainer/compile.d.ts +17 -0
  15. package/lib/appcontainer/compile.js +74 -0
  16. package/lib/appcontainer/exempt.d.ts +18 -0
  17. package/lib/appcontainer/exempt.js +71 -0
  18. package/lib/appcontainer/index.d.ts +85 -0
  19. package/lib/appcontainer/index.js +286 -0
  20. package/lib/appcontainer/probe.d.ts +44 -0
  21. package/lib/appcontainer/probe.js +66 -0
  22. package/lib/appcontainer/sid.d.ts +23 -0
  23. package/lib/appcontainer/sid.js +33 -0
  24. package/lib/appcontainer/wrap.d.ts +22 -0
  25. package/lib/appcontainer/wrap.js +40 -0
  26. package/lib/cli.js +0 -0
  27. package/lib/client.js +1425 -1108
  28. package/lib/client.js.map +1 -1
  29. package/lib/config.d.ts +41 -1
  30. package/lib/config.js +6 -0
  31. package/lib/index.d.ts +1 -1
  32. package/lib/index.js +53 -0
  33. package/lib/runtime.d.ts +22 -0
  34. package/lib/runtime.js +24 -0
  35. package/locale/de.json +1 -0
  36. package/locale/en.json +1 -0
  37. package/locale/es.json +1 -0
  38. package/locale/fr.json +1 -0
  39. package/locale/it.json +1 -0
  40. package/locale/ja.json +1 -0
  41. package/locale/ko.json +1 -0
  42. package/locale/ru.json +1 -0
  43. package/locale/zh.json +1 -0
  44. package/package.json +28 -28
  45. package/scripts/patch-permission-glyph.mjs +0 -0
  46. package/README.de.md +0 -487
  47. package/README.es.md +0 -489
  48. package/README.fr.md +0 -498
  49. package/README.it.md +0 -490
  50. package/README.ru.md +0 -478
package/README.fr.md DELETED
@@ -1,498 +0,0 @@
1
- # dsh-perm-gate
2
-
3
- - [English README](./README.md)
4
- - [中文 README](./README.zh.md)
5
- - [日本語 README](./README.ja.md)
6
- - [한국어 README](./README.ko.md)
7
- - [Français README](./README.fr.md)
8
- - [Deutsch README](./README.de.md)
9
- - [Italiano README](./README.it.md)
10
- - [Русский README](./README.ru.md)
11
- - [Español README](./README.es.md)
12
- - [Installation guide](./INSTALL.md)
13
- - [中文安装指南](./INSTALL.zh.md)
14
- - [日本語インストールガイド](./INSTALL.ja.md)
15
- - [한국어 설치 안내](./INSTALL.ko.md)
16
- - [Guide d'installation](./INSTALL.fr.md)
17
- - [Installationsanleitung](./INSTALL.de.md)
18
- - [Guida all'installazione](./INSTALL.it.md)
19
- - [Руководство по установке](./INSTALL.ru.md)
20
- - [Guía de instalación](./INSTALL.es.md)
21
- - [Changelog](./CHANGELOG.md)
22
- - [日本語 changelog](./CHANGELOG.ja.md)
23
- - [한국어 changelog](./CHANGELOG.ko.md)
24
- - [Français changelog](./CHANGELOG.fr.md)
25
- - [Deutsch changelog](./CHANGELOG.de.md)
26
- - [Italiano changelog](./CHANGELOG.it.md)
27
- - [Русский changelog](./CHANGELOG.ru.md)
28
- - [Español changelog](./CHANGELOG.es.md)
29
-
30
- > **Note de compatibilité :** la v2.0.0 est livrée avec des dictionnaires `ja` / `ko`, mais le DSH
31
- > officiel n'expose que `zh` / `en` via `LocaleRuntime` (`LOCALE_IDS = ["zh", "en"]`). Sur un DSH
32
- > d'origine, la sélection de `ja` / `ko` échoue avec `locale "<id>" is not registered`. Utilisez un
33
- > fork de DSH qui met à jour `LOCALE_IDS` (locale-settings.ts) et les libellés `LOCALES`
34
- > (client/index.ts), puis recompilez.
35
-
36
- > **▼ Compatibilité des versions de DSH**
37
- >
38
- > Deux lignes DSH sont servies depuis deux branches de longue durée, chacune avec sa propre série
39
- > de versions, son `engines.dsh` et son dist-tag npm ([organisation des releases](./RELEASING.md)) :
40
- >
41
- > | Version de DSH | Branche | Version | Tag npm |
42
- > | --- | --- | --- | --- |
43
- > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
44
- > | 0.1.2-alpha.1+ (incl. 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x` est une plage) |
45
- > | 0.2.0-rc.1 (ligne 0.2.0) | `compat/0.2.0` | `5.x` | `@dsh-0.2.0` |
46
- >
47
- > Le numéro de série suit la **ligne DSH** (`1.x` = DSH ≤ 0.1.1, `2.x` = DSH 0.1.2+), et les
48
- > majeures se cloisonnent mutuellement : une installation `^1.x` ne résout jamais une version
49
- > `2.x`, et inversement. `engines.dsh` exprime la même séparation, mais DSH ne le lit jamais —
50
- > ce sont les plages et les dist-tags qui maintiennent un vieux DSH sur `1.x`.
51
- >
52
- > `@deepseek-ai/dsh-client-runtime` a été **supprimé** à partir de `0.1.2-alpha.1` — il n'a pas
53
- > simplement été déplacé. La ligne `legacy` atteint toujours `ctx.slots` à travers lui ; `main`
54
- > obtient la même déclaration depuis `@deepseek-ai/dsh-client-ui-renderer/client`. Deux coutures
55
- > sensibles à la version sont gérées par des sondes de capacité plutôt que par des vérifications
56
- > de version : (1) l'enregistrement des réglages utilise `register`, présent sur les deux lignes
57
- > (`installSection` est un ajout, pas un remplacement) ; (2) `effectivePolicy` est une méthode
58
- > **privée** du service d'approbation utilisateur sur les deux lignes ; elle est donc lue derrière
59
- > une sonde `typeof` et se dégrade en « politique inconnue » lorsqu'elle est absente ou lève une
60
- > erreur.
61
-
62
- Version **2.6.0** — voir le [changelog](./CHANGELOG.fr.md).
63
-
64
- Un gate de permissions unique, autosuffisant, déterministe d'abord et fail-closed, pour DeepSeek
65
- Harness.
66
-
67
- `dsh-perm-gate` tranche chaque appel d'outil selon une chaîne de priorité fixe :
68
-
69
- | Étape | Décision | De quoi il s'agit |
70
- | ---- | ---- | ---- |
71
- | **P0** | `deny` | refus dur déterministe : matériau d'identifiants, mutation de chemin protégé, shell dangereux |
72
- | **P1** | `allow` | un **octroi de session** précis et borné |
73
- | **P2** | `deny/allow/ask` | chaîne de règles statique : d'abord la blacklist, puis l'allow, puis l'ask |
74
- | **P3** | `allow/deny/ask` | classificateur sémantique LLM optionnel (**désactivé** par défaut) |
75
- | **P4** | `ask` | seam d'approbation officiel |
76
-
77
- Strictement fail-closed : une décision P0 n'est jamais annulée par un octroi, une règle, le
78
- classificateur ou un humain.
79
-
80
- ## Pourquoi
81
-
82
- L'écosystème de sécurité DSH éclate cela entre plusieurs plugins (`dsh-permission-rules`,
83
- `dsh-auto-mode`, `dsh-auto-review`, `dsh-movein-permissions`). `dsh-perm-gate` fusionne le gate +
84
- l'approbation + le classificateur (facultatif) en un seul paquet, avec une seule piste d'audit et
85
- aucun couplage de versions entre plugins.
86
-
87
- ## Fonctionnalités
88
-
89
- - **Liste blanche / liste noire de commandes** — filtrage sur une **décomposition argv** (pas une
90
- chaîne brute), avec descente récursive dans `sh -c`/`bash -c`, détection de pipelines,
91
- vérification des cibles de redirection et reconnaissance du récursif/forcé (`rm -rf`).
92
- - **Priorité au deny** — une règle deny correspondante l'emporte sur toute règle allow.
93
- - **Octrois de session** — des octrois précis `(outil, empreinte canonique)` avec `TTL` +
94
- `maxUses` ; relancer avec une cible différente ne réutilise jamais l'autorité. Les sous-agents
95
- héritent mais ne peuvent pas en frapper.
96
- - **Moteur de règles à fonctions pures** — compilation glob/regex avec une borne ReDoS, échec
97
- bruyant sur règle malformée, et cache de compilation par hash de la source.
98
- - **Audit** — chaque décision est journalisée comme un événement `{ignorable:true}` avec son
99
- `callId` ; la raison visible du modèle correspond au résultat enregistré.
100
- - **Palier 自动审查** (`permissive`, plus `permissive-full`) — un **mode d'approbation
101
- indépendant** (distinct des paliers lecture seule, accès complet et liste blanche) qui n'est ni
102
- « auto-approbation » ni confiance aveugle. Le front expose un **interrupteur unique**
103
- (`permissive`) ; les quatre stratégies backend sont **combinables** et pilotées par les réglages
104
- du plugin — toujours fail-closed face à P0. Le sélecteur de permissions et la carte de réglages
105
- l'affichent tous deux sous le libellé produit 自动审查, sans icône. La variante `permissive-full`
106
- conserve un comportement d'approbation identique mais supprime le bac à sable de fichiers
107
- intégré, qui sinon refuse les tubes nommés dont `git clone` / Cygwin / ConPTY ont besoin.
108
- - **Réponse automatique aux escalades de sandbox** (`trustEscalation`) — une escalade de sandbox
109
- est demandée depuis *l'intérieur* du corps de l'outil shell / pwsh / edit, après
110
- `tools/pre-execute`, si bien que le gate ne l'a jamais vue et qu'un appel qu'il avait
111
- auto-autorisé vous demandait quand même d'approuver l'élargissement. Avec cette stratégie
112
- activée, c'est ici qu'est répondue l'exacte requête que le gate avait dégagée (repérée par
113
- `callId`).
114
- - **`llmAssist` à risque gradué** — un LLM personnalisé compatible OpenAI grade chaque `ask` en
115
- `safe` / `risky:<catégorie>` ; les catégories dures (suppression, identifiants, distant,
116
- système, masse) **demandent toujours**, le neutre alimente l'apprentissage des verdicts, et tout
117
- échec reste fail-closed.
118
- - **Apprentissage des verdicts** — les `ask` à risque neutre que l'humain approuve et qui
119
- s'exécutent réellement s'accumulent ; passé le seuil, la *même opération exacte* (repérée par
120
- empreinte) s'auto-autorise.
121
- - **Flux d'événements de décision** — chaque décision est ajoutée à un flux JSONL et remontée par
122
- la moitié navigateur sous forme de bandeau d'avis au-dessus de la saisie de conversation, plus
123
- un onglet d'enregistrements d'approbation (du plus récent au plus ancien) dans la vue de
124
- conversation.
125
-
126
- Une **blacklist de mots-clés deny préconfigurée** (héritée des `DEFAULT_DENY_KEYWORDS` de
127
- dsh-approval-gate : `rm -rf`, `push --force`, `drop table`, `mkfs`, `git reset --hard`,
128
- `docker system prune`, …) oppose son veto à tout appel dont le texte contient un mot-clé —
129
- sous-chaîne insensible à la casse, appliquée avant la liste blanche, les octrois et le LLM. Elle
130
- est modifiable sous forme de liste dans la carte de réglages (les entrées préconfigurées sont
131
- étiquetées, et une restauration en un clic ramène le préréglage) ; non définie ou vide, elle
132
- applique le préréglage — la liste noire ne se désactive jamais silencieusement.
133
-
134
- ## Installation
135
-
136
- Nécessite une installation existante de
137
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
138
-
139
- ```sh
140
- dsh plugin --profile web add dsh-perm-gate
141
- ```
142
-
143
- Les étapes complètes d'installation, de mise à niveau, de migration et de dépannage se trouvent
144
- dans le [Guide d'installation](./INSTALL.fr.md) — également disponible en
145
- [English](./INSTALL.md) / [中文](./INSTALL.zh.md) / [日本語](./INSTALL.ja.md) /
146
- [한국어](./INSTALL.ko.md) / [Deutsch](./INSTALL.de.md) / [Italiano](./INSTALL.it.md) /
147
- [Русский](./INSTALL.ru.md) / [Español](./INSTALL.es.md).
148
-
149
- ## Configuration
150
-
151
- Ajoutez le plugin au `cordis.yml` :
152
-
153
- ```yaml
154
- - id: dsh-perm-gate
155
- name: dsh-perm-gate
156
- config:
157
- rulesFile: ./permissions.yaml # optional; defaults to $DSH_HOME/perm-gate/rules.yml
158
- dshHome: $DSH_HOME # root pinned for protected-target checks
159
- defaultAction: ask # allow | ask | deny
160
- gatePresets: [permissive, permissive-full] # tiers where the gate is active (default)
161
- sessionSweep: true # hourly cleanup of archived/dead sessions' gate data
162
- ```
163
-
164
- ### Nettoyage des sessions
165
-
166
- Au démarrage puis chaque heure, le gate lit le magasin d'espaces de travail de DSH
167
- (`$DSH_HOME/storages/workspace.json`, en lecture seule) et classe chaque session pour laquelle il
168
- détient des données de chaîne d'autorisation. Les sessions que DSH a archivées
169
- (`global.archivedSessionIds`) ou ne suit plus du tout voient leurs événements de décision retirés
170
- de `$DSH_HOME/perm-gate/events.jsonl` et leurs fichiers d'instantanés pré-modification supprimés
171
- de `$DSH_HOME/perm-gate/snapshots/` — de l'historique que la page de revue ne peut plus atteindre,
172
- pour des données que le harnais lui-même considère comme disparues. Les sessions vivantes ne sont
173
- pas touchées, les lignes inattribuables (identifiant de session vide) ne sont jamais supprimées,
174
- et tout échec est fail-open : la passe est ignorée et retentée une heure plus tard. Définissez
175
- `sessionSweep: false` pour désactiver ; `workspaceStoreFile` remplace le chemin du magasin.
176
- Restaurer une session archivée ne restaure pas son historique nettoyé.
177
-
178
- ### Fichier de règles
179
-
180
- ```yaml
181
- permissions:
182
- defaultAction: ask
183
- deny:
184
- - command: [rm#recursive]
185
- reason: no recursive rm
186
- - command: [ssh]
187
- reason: no direct ssh
188
- - paths: [.dsh/**]
189
- reason: protect harness metadata
190
- allow:
191
- - command: [pnpm, node]
192
- reason: dev tools
193
- - command: [curl, wget]
194
- args: ["https://*.example.com/*"]
195
- reason: allowed endpoint
196
- ask:
197
- - command: [bash, sh]
198
- reason: ask shells
199
- ```
200
-
201
- Une entrée de commande `word#flag` correspond au mot de commande (`word`) avec le modificateur
202
- `recursive` ou `force` — ainsi `rm#recursive` correspond à `rm -rf`, `env rm -rf` et
203
- `sh -c "rm -rf /"`.
204
-
205
- ### Politique réseau (opt-in)
206
-
207
- Un proxy HTTP/CONNECT local qui arbitre le trafic sortant des **sous-processus shell** selon le
208
- même fichier de règles, plus un chemin d'approbation pour les cibles qu'aucune règle ne couvre.
209
- **Désactivé par défaut** — l'activer lie un port loopback et réécrit l'environnement proxy des
210
- processus enfants ; il n'est donc jamais activé implicitement.
211
-
212
- ```yaml
213
- - id: dsh-perm-gate
214
- config:
215
- networkEnabled: false # master switch (default false)
216
- networkMode: whitelist # deny-all | whitelist | allow-all
217
- networkUnlisted: ask # ask | deny — unlisted target handling
218
- networkUnattributed: allow # allow | deny — traffic with no shell attribution
219
- networkInjectEnv: true # rewrite HTTP(S)_PROXY/ALL_PROXY for children
220
- networkAskTimeoutMs: 120000 # approval wait before failing closed
221
- networkGrantTtlMs: 1800000 # how long one approval covers its target
222
- ```
223
-
224
- **Comportement par paliers.** Rien n'atteint le réseau sans règle allow. Une cible non
225
- répertoriée est escaladée vers le seam d'approbation interactif, soulevée au nom de la commande
226
- shell qui a ouvert la connexion ; approuver élargit la portée pour cette cible pendant la session.
227
- Une règle `deny` n'est **jamais** escaladée — l'approbation peut élargir ce qu'une cible non
228
- répertoriée peut atteindre, mais elle ne peut jamais annuler une règle qui dit non.
229
-
230
- **La limite — à lire avant de vous y fier.** Le proxy est une couche de politique *coopérative*,
231
- pas une frontière d'application. Il ne voit que le trafic des clients qui lisent l'environnement
232
- proxy :
233
-
234
- | Client | Couvert ? |
235
- |--------|----------|
236
- | `curl`, `wget`, `git`, Go `net/http`, Python `requests` | oui |
237
- | **Node.js `http`/`https`/`fetch`** | **non — connexion directe** |
238
- | Java (sans options proxy `-D`), .NET `HttpClient` | non |
239
- | Sockets bruts, TCP personnalisé | non |
240
- | DNS, QUIC/HTTP3, protocoles non HTTP | non |
241
- | Connexions vers une IP littérale | non |
242
-
243
- Ainsi une commande shell comme `node -e "require('http').get('http://host/')"` n'est pas
244
- interceptée. Considérez cela comme un garde-fou contre les accidents et un endroit pour exprimer
245
- une intention, pas comme un bac à sable hermétique.
246
-
247
- Le trafic réseau **propre** de DSH — les outils réseau intégrés et le transport LLM — est
248
- délibérément laissé de côté. Ces connexions ne portent aucune attribution shell, et
249
- `networkUnattributed: allow` (le défaut) les laisse passer sans revue : les examiner permettrait à
250
- l'hôte de se bloquer *lui-même*, ce qui est un échec pire qu'un blocage manqué. Ne définissez
251
- `networkUnattributed: deny` que si vous savez que les clients de votre hôte ignorent
252
- l'environnement proxy.
253
-
254
- Interrogez l'état en direct via `GET /api/dsh-perm-gate/network` (mode, bind, port, vivacité du
255
- proxy, état d'injection d'env, compteurs de blocage, blocages récents).
256
-
257
- ## Le palier 自动审查 (valeur machine `permissive`)
258
-
259
- 自动审查 est un **palier d'approbation indépendant** du sélecteur de permissions DSH, parallèle à
260
- Lecture seule / Écriture espace de travail / Accès complet / Liste blanche. Ce n'est **pas** une
261
- « auto-approbation » générique et il ne frappe jamais d'autorité aveugle : il ne fait que
262
- rétrécir ou élargir le seam *avant* l'étape humaine/LLM, tandis que le refus dur P0 reste
263
- monotone et non négociable **dans le périmètre propre du gate**.
264
-
265
- > **P0 est délimité, pas global.** Le gate n'agit que lorsque le préréglage de permissions de la
266
- > session est l'un des `gatePresets` (défaut `permissive` / `permissive-full`). Sous tout autre
267
- > préréglage — Lecture seule, Écriture espace de travail ou Accès complet — **tout** le gate se
268
- > retire, refus dur P0 compris, parce que la politique propre au palier sélectionné régit cette
269
- > session. C'est délibéré (voir `gatePresets` dans le tableau de configuration), mais cela
270
- > signifie que « P0 est non négociable » vaut *à l'intérieur* des paliers du gate plutôt qu'à
271
- > travers chaque palier. Un retrait n'est pas silencieux : le gate enregistre un événement
272
- > `stand-down` par transition session/préréglage et le navigateur affiche un bandeau persistant
273
- > **GATE OFF** au-dessus de la saisie. Définissez `gatePresets: ['*']` pour rendre P0 global à
274
- > nouveau.
275
-
276
- **Deux variantes sont livrées**, car le `sandbox` et l'`approval` d'un préréglage sont des
277
- réglages indépendants et leur couplage imposait un mauvais compromis :
278
-
279
- | Libellé du sélecteur | Valeur machine | sandbox | approval |
280
- |--------------|---------------|---------|----------|
281
- | 自动审查 | `permissive` | `workspace-write` | `ask` |
282
- | 自动审查(高权限) | `permissive-full` | `danger-full-access` | `ask` |
283
-
284
- Le palier simple conserve le bac à sable de fichiers intégré. Ce bac à sable refuse aussi les
285
- tubes nommés dont un processus enfant a besoin pour démarrer, si bien que `git clone`, le
286
- `sh.exe` MSYS2/Cygwin et ConPTY échouent dessous avec `Win32 error 5` /
287
- `couldn't create signal pipe`. Le gate n'étant actif **que** dans les paliers listés dans
288
- `gatePresets`, vouloir le gate impliquait d'accepter cette restriction. 自动审查(高权限)
289
- supprime le couplage : comportement d'approbation identique, sans restriction de bac à sable de
290
- fichiers. La description propre du palier énonce le compromis sans détour — le flux de travail
291
- est plus fluide, les approbations s'appliquent toujours par appel, mais il ne reste **aucun bac à
292
- sable système en filet de secours**. Les deux sont dans les `gatePresets` par défaut ; l'un comme
293
- l'autre vous donne la chaîne complète P0–P4 — le gate ne lit que le **nom** du préréglage, jamais
294
- le mode de sandbox.
295
-
296
- Le libellé du sélecteur est une **chaîne produit fournie par l'hôte**, pas une entrée de
297
- dictionnaire par locale : DSH rend verbatim le `name:` du palier d'un plugin sur les deux
298
- surfaces de permission (la ligne par défaut des réglages généraux et le sélecteur du composer) et
299
- ne fournit ses propres libellés localisés que pour les trois valeurs intégrées ;
300
- `cordis.patch.yml` livre donc le libellé chinois pour chaque session.
301
-
302
- **L'icône** est une autre histoire. La table de glyphes du composer est fermée et son propre
303
- commentaire énonce la règle : *host-configured names outside the design set get none.*
304
- `permissive` est une valeur intégrée, donc 自动审查 a déjà un glyphe bouclier+œil ;
305
- `permissive-full` ne reçoit le même glyphe que parce que `npx dsh-perm-gate-patch-glyph` l'ajoute
306
- à cette table. Ce patch modifie un paquet **hôte**, il se perd donc à chaque mise à niveau de
307
- DSH — voir [Après une mise à niveau de DSH](./INSTALL.fr.md).
308
-
309
- Dans `cordis.yml` :
310
-
311
- ```yaml
312
- - id: dsh-perm-gate
313
- name: dsh-perm-gate
314
- config:
315
- rulesFile: ./permissions.yaml
316
- defaultAction: ask
317
- permissive: true # single front-facing switch (independent tier on)
318
- permissiveStrategies: # backend, combinable
319
- trustAutoAllow: true # in-scope safe ops auto-allow; dangerous/unknown ask
320
- alwaysConfirm: false # every crossing asks; allow-controls add repeat-allow / wl-migrate buttons
321
- llmAssist: false # LLM classify first, human fallback on ask/failure
322
- trustEscalation: true # a cleared call's own sandbox escalation needs no prompt
323
- ```
324
-
325
- `trustAutoAllow` est le palier intermédiaire de base (une règle allow passe automatiquement).
326
- `alwaysConfirm` affiche le panneau d'approbation à chaque franchissement ; ses « contrôles allow »
327
- ajoutent deux boutons étendus — **ré-autoriser ce type pour cette session** (un octroi de session
328
- borné) et **autoriser chaque occurrence** (qui persiste le mot de commande dans la liste blanche
329
- `allow` du `permissions.yaml` via `approveAllowEverywhere`). `llmAssist` consulte un vrai LLM
330
- configurable (`classifierEndpoint` / `classifierModel`, toute API compatible OpenAI) pour trancher
331
- automatiquement un `ask`, et retombe sur le seam humain en cas d'`ask`/erreur — toujours
332
- fail-closed. `trustEscalation` (actif par défaut quand le palier l'est) répond à une escalade
333
- `sandbox_permissions` soulevée depuis l'intérieur d'un appel que le gate a déjà autorisé ; voir
334
- ci-dessous. Quand `permissive` est désactivé, le gate se comporte exactement comme avant.
335
-
336
- ### Escalade de sandbox : pourquoi un verdict `safe` provoquait quand même une invite
337
-
338
- Un appel d'outil peut soulever **deux approbations indépendantes**. Le gate possède la première —
339
- son propre `ask`, sur la cascade `tools/pre-execute`. La seconde vient d'`approveEscalation`
340
- **dans le corps de l'outil**, au moment de `tools/execute`, chaque fois que le modèle a transmis
341
- `sandbox_permissions` + `justification` ; à ce stade, `tools/pre-execute` est déjà tranché, si
342
- bien que l'allow du gate ne l'atteint jamais. Un appel que le LLM avait gradé `safe` et que le
343
- gate avait auto-autorisé affichait donc quand même une invite demandant d'approuver
344
- l'élargissement de sandbox.
345
-
346
- `trustEscalation` comble ce fossé. Le gate se souvient de chaque appel qu'il a positivement
347
- autorisé (indexé par le `callId` de l'hôte, que la requête d'escalade répète) et répond lui-même
348
- à l'escalade `allowed-once`. Elle ne s'applique que si **tout** est réuni :
349
-
350
- - le palier 自动审查 est actif et `trustEscalation` est activé ;
351
- - la requête porte un `callId` que le gate a dégagé, avec un nom d'outil correspondant ;
352
- - la raison est une escalade reconnue nommant `workspace-write` ou `danger-full-access`.
353
-
354
- Tout le reste — une raison non reconnue, un autre appel, un appel que le gate a demandé ou
355
- refusé, le passe-plat `approval: never` — est délégué à l'humain à l'identique, si bien qu'un
356
- futur changement de formulation de DSH échoue fermé plutôt qu'ouvert. La réponse automatique est
357
- enregistrée sur le flux d'événements (`verdict: "escalation-auto"`, `mode: <cible>`). Désactivez
358
- l'interrupteur pour garder l'élargissement de sandbox sous contrôle humain pendant que les autres
359
- allows restent automatiques.
360
-
361
- ### llmAssist à risque gradué, apprentissage des verdicts et flux d'événements
362
-
363
- Quand `llmAssist` est actif, le LLM configuré grade un `ask` à la fois selon un protocole
364
- structuré. La notation se fait **dans la cascade `tools/pre-execute` du gate, avant que la
365
- décision ne soit renvoyée à l'hôte** : un verdict `safe` délègue l'appel directement, si bien que
366
- le panneau d'approbation n'apparaît jamais ; seul un verdict réellement incertain vous parvient.
367
- Deux sources de récepteur sont sélectionnables dans la carte de réglages : une **API
368
- personnalisée** (tout endpoint compatible OpenAI — `classifierEndpoint` / `classifierModel` /
369
- `classifierApiKey`, avec des préréglages dont Xiaomi MiMo `https://api.xiaomimimo.com/v1`), ou le
370
- **groupe de modèles hôte DSH** (le service `llm` configuré de la session, via
371
- `agentDefaultModel.currentSelection`, avec possibilité de le remplacer par `classifierProvider` /
372
- `classifierModel`). Un bouton de **test de santé** (`POST /api/dsh-perm-gate/health`) exécute une
373
- complétion minimale et rapporte la latence.
374
-
375
- - `safe` → l'appel est auto-autorisé (audité comme source `classifier`) ; aucun panneau n'est
376
- affiché.
377
- - `risky` + une **catégorie dure** (`deletion`, `credential`, `remote`, `system`, `bulk`) →
378
- l'appel **conserve l'ask humain**. Le classificateur **ne refuse jamais** : le chemin de deny
379
- appartient aux seules couches déterministes (refus dur P0, blacklist de mots-clés deny, règles
380
- `deny:` explicites), si bien qu'une catégorie mal gradée reste toujours négociable au lieu d'un
381
- blocage sans recours. Les catégories dures restent distinctes de `neutral` d'une manière qui
382
- compte : elles ne sont **jamais apprises**, si bien que des approbations humaines répétées ne
383
- peuvent jamais les sédimenter en auto-allow.
384
- (Refuser sur la parole du modèle a été mesuré en conditions réelles : un bénin
385
- `git commit -F …` gradé `remote` a produit un auto-deny sans panneau et sans octroi pour
386
- retenter.)
387
- - `risky:neutral` → avec `riskLearning` activé (carte Réglages, désactivé par défaut), chaque
388
- approbation humaine réellement exécutée (solde via l'événement `tools/result` de l'hôte) compte
389
- pour une clé `outil|catégorie` ; dès que le compte atteint `riskThreshold` (défaut 3) **et** que
390
- l'empreinte d'opération (mot de commande + basename de la cible) du nouvel appel correspond à un
391
- échantillon confirmé, la même opération exacte s'auto-autorise. Une cible différente ne réutilise
392
- jamais cette autorité. Avec la sédimentation (`riskSediment`, actif par défaut), les échantillons
393
- confirmés d'une clé au seuil deviennent des **règles allow déterministes** : une correspondance
394
- exacte autorise net, sans nouvel appel LLM — même avec `llmAssist` désactivé — et les règles
395
- sédimentées sont visibles et gérables (terminer / supprimer) dans la carte de réglages.
396
- - Les dépassements de délai (`riskTimeoutMs`, défaut 20 s, une seule nouvelle tentative), les
397
- échecs de transport et les sorties hors protocole laissent l'ask intact — le gate ne devine
398
- jamais.
399
-
400
- L'état d'apprentissage persiste dans un JSON possédé par le plugin
401
- (`$DSH_HOME/perm-gate/learning.json`, ou `learningFile`), jamais dans votre fichier de règles
402
- YAML. Chaque décision est ajoutée à `$DSH_HOME/perm-gate/events.jsonl` (ou `eventsFile`) et servie
403
- à `GET /api/dsh-perm-gate/events?sessionId=&since=` ; la moitié navigateur l'interroge et affiche
404
- la dernière décision en bandeau d'avis au-dessus de la saisie de conversation (les asks restent
405
- visibles jusqu'à l'événement suivant) et liste toutes les décisions de la session, de la plus
406
- récente à la plus ancienne, dans l'onglet **Approbations** de la vue de conversation.
407
-
408
- Les fichiers affectés de chaque décision sont copiés en instantanés avant que la modification
409
- n'atterrisse (≤ 5 fichiers, ≤ 256 Ko chacun) sous `$DSH_HOME/perm-gate/snapshots/` ; dans l'onglet
410
- **Approbations**, chaque pastille de fichier ouvre un diff de lignes
411
- (`GET /api/dsh-perm-gate/diff`) avec une action de **revert** qui délivre une instruction de
412
- restauration dans la conversation (`POST /api/dsh-perm-gate/revert`). Une barre d'inventaire des
413
- instantanés les efface par session ou entièrement (`GET /api/dsh-perm-gate/snapshots-stats` /
414
- `POST /api/dsh-perm-gate/snapshots-clear`).
415
-
416
- Un `ask` que le gate route vers un humain est suivi jusqu'à la réponse de l'humain : un
417
- observateur passif `approval/request` enregistre l'issue close (`allowed-once` → **approuvé**,
418
- `rejected` → **rejeté**, `cancelled` → **annulé**, `unavailable` → un refus, puisqu'aucun canal
419
- d'approbation n'existait), avec `tools/result` qui solde le même ask en secours lorsque
420
- l'observateur ne peut pas le corréler. Les approbations rapportent la progression d'apprentissage
421
- post-approbation (`n`/seuil), et le bandeau d'avis étiquette les trois états terminaux.
422
-
423
- 自动审查 et 自动审查(高权限) dessinent toutes deux le glyphe bouclier+œil dans le sélecteur —
424
- la première depuis la table intégrée de DSH, la seconde depuis le patch hôte que décrit le guide
425
- d'installation. Sans ce patch, le second palier est en texte seul sur toutes les surfaces ; son
426
- libellé et son filtrage ne sont pas affectés.
427
-
428
- ### Un palier de session sélectionnable
429
-
430
- `cordis.patch.yml` ajoute un préréglage `permissive` (`sandbox: workspace-write`,
431
- `approval: ask`, nom **自动审查**) entre Écriture espace de travail et Accès complet. Le patch de
432
- bundle DSH remplace toute la table `permission.config.presets` au lieu de fusionner par clé, si
433
- bien que le fichier réénonce aussi les trois intégrés (`read-only` / `workspace-write` /
434
- `danger-full-access`, issus de `@deepseek-ai/dsh-base/cordis.patch.yml`) ;
435
- `test/patch-presets.spec.ts` épingle ce jeu de clés. Ainsi, le sélecteur de permissions de session
436
- propose 自动审查 comme palier d'approbation indépendant sélectionnable, et non un mode générique
437
- d'« auto-approbation ».
438
-
439
- Le gate n'est actif **que dans les paliers listés dans `gatePresets`** (défaut
440
- `['permissive', 'permissive-full']`, les deux paliers qu'ajoute ce plugin). Dans tout autre
441
- palier — Lecture seule, Écriture espace de travail, Accès complet, `custom` — le flux de décision
442
- du gate ne s'exécute pas du tout : aucun allow, aucun ask, aucun deny, aucun refus dur P0, aucun
443
- veto de mot-clé deny, aucun événement d'audit. La politique propre au palier sélectionné régit
444
- l'appel, et c'est le propos : le `danger-full-access` intégré est défini comme « accès complet
445
- sans invites d'approbation », donc le contourner par un ask (auquel on ne peut répondre là-bas —
446
- le seam d'approbation refuse avant que le moindre répondant ne s'exécute, produisant
447
- `the user rejected tool "..."` sans panneau) ou par un refus dur contredirait silencieusement le
448
- palier choisi par l'utilisateur. `gatePresets: ['*']` rend le gate global à nouveau (refus dur
449
- compris) ; à l'intérieur d'un palier actif, un `ask` est toujours dégradé en passe-plat lorsque
450
- la politique d'approbation effective de la session est `never` — voilà pourquoi les deux paliers
451
- 自动审查 déclarent `approval: ask`.
452
-
453
- ### Configurable dans l'interface
454
-
455
- Le palier est également ajustable à l'exécution depuis **Réglages → Plugins → 自动审查** (une
456
- page `settings.plugins.tab` rendue par le client navigateur du plugin) : un interrupteur bascule
457
- `permissive`, et quatre bascules éditent les `permissiveStrategies` backend. L'hôte lit l'espace
458
- de noms en direct, si bien qu'un changement s'applique à l'appel d'outil suivant sans redémarrage.
459
- C'est une classe d'approbation indépendante, PAS un mode générique d'« auto-approbation ».
460
-
461
- ### Test de règle (dry-run)
462
-
463
- La même page embarque un panneau de **test de règle** : saisissez un nom d'outil et une commande,
464
- appuyez sur Tester, et le gate juge cet appel au regard du jeu de règles qu'il a actuellement
465
- chargé — sans rien exécuter et sans écrire aucune règle. Vous obtenez le verdict, la règle
466
- correspondante (index et action), les dimensions que cette règle contraint, et la raison.
467
-
468
- Il rapporte à la fois le verdict effectif (toute la chaîne P0 → P1 → P2 → P3 → P4) *et* la
469
- réponse propre de la couche de règles, qui ne sont pas la même chose : un refus dur P0 ou un
470
- mot-clé deny de préréglage se déclenche avant la chaîne de règles et ne laisse aucun index de
471
- règle derrière lui, si bien que le panneau dit « aucune règle correspondante » plutôt que de
472
- nommer une règle sans rapport. Une note « 0 règles chargées » signifie que le chemin `rulesFile`
473
- n'a résolu aucun fichier.
474
-
475
- Le panneau communique avec `POST /api/dsh-perm-gate/dry-run`, qui est **en lecture seule par
476
- construction** — il n'a aucune forme d'écriture, si bien que tester une règle ne peut jamais la
477
- modifier.
478
-
479
- ## CLI
480
-
481
- Dry-run d'un appel contre un fichier de règles (sans harnais) :
482
-
483
- ```sh
484
- dsh-perm-gate --rules permissions.yaml --tool bash --args '{"command":"pnpm install"}'
485
- dsh-perm-gate --rules permissions.yaml --list
486
- ```
487
-
488
- ## Développement
489
-
490
- ```sh
491
- npm run typecheck
492
- npm test
493
- npm run build
494
- ```
495
-
496
- ## Licence
497
-
498
- [MIT](./LICENSE)