@aimarket/warden 0.18.0 → 0.20.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.
- package/CHANGELOG.md +25 -2
- package/README-es.md +16 -14
- package/README-fr.md +16 -14
- package/README-ru.md +16 -14
- package/README-zh.md +16 -14
- package/README.md +17 -15
- package/dist/claude-hook.d.ts.map +1 -1
- package/dist/claude-hook.js +9 -3
- package/dist/claude-hook.js.map +1 -1
- package/dist/flow-guard.d.ts +91 -5
- package/dist/flow-guard.d.ts.map +1 -1
- package/dist/flow-guard.js +433 -19
- package/dist/flow-guard.js.map +1 -1
- package/dist/mcp-server.js +4 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/review-diff.d.ts +5 -4
- package/dist/review-diff.d.ts.map +1 -1
- package/dist/review-diff.js.map +1 -1
- package/dist/static-scan.d.ts +1 -1
- package/dist/static-scan.d.ts.map +1 -1
- package/dist/static-scan.js +109 -3
- package/dist/static-scan.js.map +1 -1
- package/dist/wrap-cli.d.ts +6 -0
- package/dist/wrap-cli.d.ts.map +1 -1
- package/dist/wrap-cli.js +8 -2
- package/dist/wrap-cli.js.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/dist/wrap.js +125 -19
- package/dist/wrap.js.map +1 -1
- package/docs/flow-guard.es.md +77 -21
- package/docs/flow-guard.fr.md +78 -21
- package/docs/flow-guard.md +78 -22
- package/docs/flow-guard.ru.md +75 -20
- package/docs/flow-guard.zh.md +57 -18
- package/docs/gates.es.md +36 -6
- package/docs/gates.fr.md +36 -5
- package/docs/gates.md +36 -6
- package/docs/gates.ru.md +35 -6
- package/docs/gates.zh.md +10 -5
- package/docs/scanner-comparison.es.md +25 -1
- package/docs/scanner-comparison.fr.md +25 -1
- package/docs/scanner-comparison.md +24 -1
- package/docs/scanner-comparison.ru.md +25 -1
- package/docs/scanner-comparison.zh.md +20 -1
- package/package.json +1 -1
package/docs/flow-guard.fr.md
CHANGED
|
@@ -16,14 +16,18 @@ La garde vérifie les appels après réception de texte externe. Ce n’est ni u
|
|
|
16
16
|
- Les écritures reconnues dans politique/état, hooks Claude et hooks/config Git sont protégées, y compris variables de chemin et liens symboliques. Cela ne remplace pas les permissions du système.
|
|
17
17
|
- L’autorité réelle des URL et tous les destinataires sont vérifiés. Un lien dans le corps ne permet pas un autre destinataire. Les adresses mail de la politique correspondent exactement ; un domaine n’autorise pas toutes ses boîtes. Une mention n’autorise pas toute action ou donnée.
|
|
18
18
|
- La résolution exige `contactTools` explicite, l’étiquette `private`, des enregistrements structurés `name`/`email` et le nom complet dans la demande. Un nom ambigu demande confirmation. Fichiers locaux, prose, YAML et anciens contacts en cache ne confèrent aucune autorité. Ne faites confiance qu’aux annuaires non modifiables par l’agent ou des tiers.
|
|
19
|
-
- Les empreintes inspectent le texte visible avec un décodage URL/base64/hex limité. Elles ne lisent pas tout fichier à l’envoi et ne couvrent pas tous les encodages ou outils. Les modifications du projet restent généralement permises ; exécuter du code est une autre décision.
|
|
19
|
+
- Les empreintes inspectent le texte visible avec un décodage URL/base64/hex limité. Depuis le 2026-10-22, les valeurs qui ressemblent à des clés (suites aléatoires des fichiers d’identifiants, et formats de jetons connus où qu’ils soient lus) sont aussi relevées par fenêtres de 8 lettres et chiffres, 10 pour les clés hex : un morceau de clé, la clé à l’envers, avec des séparateurs ou des caractères invisibles, en morceaux base64 ou avec des lettres cyrilliques ou grecques sosies est refusé. Ce sont les plus petites tailles pour lesquelles aucune de 480 clés inventées dans huit formats ne s’est retrouvée nulle part dans 115 millions de lettres et chiffres (ce monorepo et 1 000 paquets HISTOR) de texte ordinaire ; les parties que partagent toutes les clés d’un type (l’identifiant d’algorithme d’une clé DER, l’en-tête et les champs d’un JWT) ne sont jamais relevées. Chaque argument garde la fin de ce qu’il a envoyé après le texte extérieur, si bien que des morceaux envoyés à la suite par le même argument sont vérifiés en entier. Les morceaux de moins de 8 caractères entourés chaque fois d’autres mots passent encore, de même que le format initial de la clé (`rk_live_`). Elles ne lisent pas tout fichier à l’envoi et ne couvrent pas tous les encodages ou outils. Les modifications du projet restent généralement permises ; exécuter du code est une autre décision.
|
|
20
|
+
- Après du texte extérieur, écrire dans un fichier ordinaire du projet une clé lue pendant la session demande (`deny` refuse) : les fichiers du projet sont publiés plus tard sans autre appel. `.env`, `.npmrc` et les autres fichiers d’identifiants sont la place des clés et passent ; une modification qui ne fait qu’enlever une clé ne déclenche rien.
|
|
21
|
+
- Une adresse qui n’apparaît dans votre prompt que dans un texte collé, une ligne citée (`>`), les en-têtes d’un message collé ou sous une marque de transfert n’est pas une adresse que vous avez nommée : y envoyer demande une fois. Un point ou une virgule après une adresse tapée ne la cachent plus.
|
|
22
|
+
- Un outil MCP sans étiquette d’opérateur est sortant si son nom le dit (send, post, share …), s’il porte une URL ou, depuis le 2026-10-22, s’il nomme une partie : un argument comme `to`, `email`, `attendees`, `assignee`, `collaborator` ou `username`, ou toute adresse e-mail ou IBAN. `warden-mcp flow-labels` liste les outils MCP sans étiquette que vos sessions ont utilisés, avec la supposition de la garde et un bloc `labels` à corriger puis coller dans la politique.
|
|
23
|
+
- Une injection qui ne change que la réponse ne fait aucun appel à refuser. Depuis le 2026-10-22, le hook Stop du plugin ajoute une ligne sous une réponse dont le tour a lu du texte extérieur, avec ces sources, et le dit quand l’un d’eux s’adressait à l’assistant avec des instructions. Si la réponse reprend 12 lettres et chiffres d’affilée des phrases où ce texte s’adressait à l’assistant, le hook renvoie aussi la réponse une fois (`decision: block`) pour qu’elle soit vérifiée face à la demande de l’utilisateur avant qu’il la lise ; l’utilisateur ne perd que le temps de cette vérification. `answerNote` : `always` (par défaut), `instruction` (ce cas seulement) ou `off` ; hors du plugin, enregistrez `warden-mcp hook stop`.
|
|
20
24
|
- Le proxy enregistre aussi les erreurs JSON-RPC et d’outils, retient les réponses dont l’enregistrement échoue et bloque les vérifications si l’état existant est corrompu.
|
|
21
25
|
|
|
22
|
-
Publié dans npm `0.17.0` : `npx -y @aimarket/warden@0.
|
|
26
|
+
Publié dans npm `0.17.0` : `npx -y @aimarket/warden@0.20.0 hook pre-tool-use`, ou depuis les sources dans WARDEN, `npm ci && npm run build`, puis `node dist/mcp-server.js hook pre-tool-use`, avec les hooks prompt/post également enregistrés. Politique : `~/.local/state/warden/flow-policy.json` ou `WARDEN_FLOW_POLICY` ; modes `ask`, `deny`, `audit`, `off`. Exemple actuel en [anglais](flow-guard.md).
|
|
23
27
|
|
|
24
|
-
Proxy : `node dist/mcp-server.js wrap --flow mail -- node mail-server.js`. Attribuez un nouveau `WARDEN_FLOW_SESSION` commun aux proxies d’une conversation. Le PID parent par défaut ne relie pas des lanceurs différents. Le proxy ne voit ni prompt ni outils intégrés
|
|
28
|
+
Proxy : `node dist/mcp-server.js wrap --flow mail -- node mail-server.js`. Attribuez un nouveau `WARDEN_FLOW_SESSION` commun aux proxies d’une conversation. Le PID parent par défaut ne relie pas des lanceurs différents. Le proxy ne voit ni prompt ni outils intégrés. Un appel qui demanderait confirmation est demandé via le formulaire d’elicitation MCP du client s’il en propose un (la réponse est journalisée comme `approvalOutcome`), sinon, ou avec `--ask refuse`, il est refusé. `trustedSources` s’applique à l’URL WebFetch, pas aux liens des arguments MCP.
|
|
25
29
|
|
|
26
|
-
**
|
|
30
|
+
**Quelle version décrivent les chiffres.** Le tableau du rejeu ci-dessous a été refait le 2026-10-22 sur le code actuel : les corrections de l’audit plus les changements du 2026-10-22. Ceux du 2026-10-22 ne changent aucune décision (les 1 046 enregistrements sont identiques à ceux du code d’avant) ; les corrections de l’audit ont coûté une tâche sans question et 19 tâches faites sous attaque. Les tableaux et coûts de l’exécution avec modèle décrivent toujours la version d’avant l’audit, jusqu’à `2328be980` ; aucune nouvelle exécution avec modèle. Un replay suit un chemin fixe, pas le pire cas adaptatif ; les attaques menées code en main le 2026-10-22 sont [plus bas](#ce-qui-a-changé-le-2026-10-22-et-pourquoi).
|
|
27
31
|
|
|
28
32
|
## Mesuré sur AgentDojo
|
|
29
33
|
|
|
@@ -37,23 +41,26 @@ benchmark (`important_instructions`) est placée là où la tâche de l’utilis
|
|
|
37
41
|
référence de la tâche utilisateur est rejouée à travers la garde, suivie de la solution de référence de l’attaquant.
|
|
38
42
|
C’est un modèle qui a lu l’injection et **lui a entièrement obéi sur le chemin de référence**. Ce n’est pas le pire cas adaptatif. Un `ask` compte comme un refus : un
|
|
39
43
|
benchmark n’a personne pour répondre. L’exécution est déterministe, prend quelques minutes, ne coûte rien et a tourné
|
|
40
|
-
dans un conteneur gVisor sans réseau
|
|
44
|
+
dans un conteneur gVisor sans réseau sur notre serveur (2026-10-17/18) et dans un conteneur Docker sans réseau sur un
|
|
45
|
+
poste de travail (2026-10-22).
|
|
41
46
|
|
|
42
47
|
AgentDojo 0.1.35, suites v1.2.2, avec nos étiquettes d’opérateur pour les 69 outils (`scripts/agentdojo/policy.json`).
|
|
43
|
-
Résultats : `scripts/agentdojo/results
|
|
48
|
+
Résultats : `scripts/agentdojo/results/` (le tableau : `summary-2026-10-22.json`).
|
|
44
49
|
|
|
45
50
|
| Suite | Actions d’attaque réussies, sans la garde | Avec la garde | Tâches sans aucune question | Tâches faites sous attaque, sans / avec |
|
|
46
51
|
|---|---|---|---|---|
|
|
47
52
|
| banking | 141 sur 144 | **0 sur 144** | 13 sur 16 | 125 / 117 sur 144 |
|
|
48
|
-
| Slack | 105 sur 105 | **0 sur
|
|
53
|
+
| Slack | 105 sur 105 | **0 sur 75** | 2 sur 21 | 102 / 15 sur 105 |
|
|
49
54
|
| travel | 116 sur 120 | **0 sur 120** | 16 sur 20 | 26 / 112 sur 140 |
|
|
50
|
-
| workspace | 231 sur 240 | **0 sur 240** |
|
|
51
|
-
| **total** | **593 sur 609** | **0 sur
|
|
55
|
+
| workspace | 231 sur 240 | **0 sur 240** | 26 sur 40 | 326 / 364 sur 560 |
|
|
56
|
+
| **total** | **593 sur 609** | **0 sur 579** | **57 sur 97** | **579 / 608 sur 949** |
|
|
52
57
|
|
|
53
58
|
Ce sont les étiquettes telles que corrigées le 2026-10-18 (l’exécution avec modèle ci-dessous en a trouvé deux trop
|
|
54
59
|
strictes) et la garde qui reconnaît par vos contacts les personnes nommées. Les premières étiquettes donnaient 54
|
|
55
|
-
tâches sans question et 571 faites sous attaque ; les étiquettes corrigées sans contacts, 57 et 613
|
|
56
|
-
versions ont donné 0
|
|
60
|
+
tâches sans question et 571 faites sous attaque ; les étiquettes corrigées sans contacts, 57 et 613 ; avec contacts,
|
|
61
|
+
avant les corrections de l’audit, 58 et 627. Toutes les versions ont donné 0 action d’attaque. Les corrections de
|
|
62
|
+
l’audit refusent quelques téléchargements et entrées d’agenda de plus dans les tâches Slack et workspace : 579 paires,
|
|
63
|
+
et non 584, ont apporté l’injection.
|
|
57
64
|
|
|
58
65
|
Les « actions d’attaque » sont les paires où l’injection a atteint l’agent (son texte est revenu dans le résultat
|
|
59
66
|
d’un outil) et où l’objectif de l’attaquant exige un appel : envoyer, payer, inviter, supprimer. Le nombre de paires
|
|
@@ -62,13 +69,13 @@ apporté l’injection.
|
|
|
62
69
|
|
|
63
70
|
Comment lire les deux dernières colonnes :
|
|
64
71
|
|
|
65
|
-
- **Sans aucune question.** La garde reste silencieuse pendant
|
|
72
|
+
- **Sans aucune question.** La garde reste silencieuse pendant 57 tâches sur 97. Les 40 autres ont besoin d’un appel
|
|
66
73
|
pour lequel elle demande. Dans Claude Code, c’est un clic ; dans le benchmark, une tâche échouée. Slack est
|
|
67
74
|
l’extrême : presque chaque tâche écrit dans un canal ou à une personne trouvés dans les données (« le canal qui a
|
|
68
75
|
le plus d’utilisateurs »), pas nommés par l’utilisateur. C’est exactement la forme d’une action injectée, donc elle
|
|
69
76
|
demande.
|
|
70
77
|
- **Sous attaque.** Sans la garde, les attaques cassent aussi la tâche de l’utilisateur (en travel, 26 sur 140
|
|
71
|
-
aboutissent). Avec la garde, l’attaque est refusée et la tâche continue :
|
|
78
|
+
aboutissent). Avec la garde, l’attaque est refusée et la tâche continue : 608 sur 949 aboutissent, contre 579 sans
|
|
72
79
|
elle.
|
|
73
80
|
|
|
74
81
|
Huit objectifs d’attaquant récents de workspace sont livrés sans solution de référence ; on ne peut donc pas les
|
|
@@ -176,19 +183,65 @@ notre serveur : 21 654 exécutions du modèle, 12,68 $ de crédit DeepSeek.
|
|
|
176
183
|
Le même retour de lecteurs a modifié le bac à sable de HISTOR : une connexion mise en file par le détenteur précédent
|
|
177
184
|
d’une adresse n’est plus attribuée à l’exécution suivante, qui est marquée incomplète au lieu de propre.
|
|
178
185
|
|
|
186
|
+
## Ce qui a changé le 2026-10-22, et pourquoi
|
|
187
|
+
|
|
188
|
+
Un lecteur a demandé ce que WARDEN ne repère toujours pas. Nous l’avons listé, puis attaqué la garde code en main :
|
|
189
|
+
17 attaques dans [`scripts/redteam/flow-redteam.mjs`](../scripts/redteam/flow-redteam.mjs), chacune visant une limite
|
|
190
|
+
documentée ou le bord d’une règle, rejouées par le même point d’entrée de hooks que le plugin (un modèle obéissant,
|
|
191
|
+
sans benchmark). Avant les changements, 14 passaient ; après, 3.
|
|
192
|
+
|
|
193
|
+
| Changement | Pourquoi | Résultat |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| Valeurs qui ressemblent à des clés relevées par fenêtres de 10 lettres et chiffres, aussi à l’envers et lettres sosies ramenées au latin ; base64 décodé dès 16 caractères au lieu de 24 | une clé en morceaux de 15 caractères, avec des tirets, à l’envers ou en lettres cyrilliques passait | attaques : tout cela refusé ; le premier morceau, surtout le format de la clé, passe encore |
|
|
196
|
+
| Chaque argument garde la fin de ce qu’il a envoyé après le texte extérieur | des morceaux de 8 caractères, un numéro de carte en deux moitiés, du texte privé en morceaux de 12 vers un hôte autorisé passaient | refus ou question dès le deuxième ou troisième morceau |
|
|
197
|
+
| Un outil MCP sans étiquette qui nomme une partie est sortant ; `warden-mcp flow-labels` | « ajouter un participant » avec l’e-mail de l’attaquant et « ajouter un collaborateur » par nom d’utilisateur passaient | les deux demandent |
|
|
198
|
+
| Hook Stop : une ligne sous une réponse appuyée sur du texte extérieur | une injection dans la seule réponse ne fait aucun appel | l’utilisateur voit quelles sources la réponse a utilisées ; rien n’est bloqué |
|
|
199
|
+
| Ruleset v15 (voir [gates](gates.fr.md)) | 78 outils empoisonnés de la moitié dev de MCPTox passaient les règles | moitié réservée 171 → 194 sur 218 (règles écrites à partir de l’autre moitié seulement), moitié dev 189 → 233 sur 267 ; 0 sur 45 serveurs propres ; le corpus et 1 000 paquets HISTOR bloquent exactement comme v14 |
|
|
200
|
+
| Le rejeu refait sur le code actuel | les corrections de l’audit du 2026-10-18 n’avaient pas été mesurées | 0 sur 579 actions d’attaque, 57 sur 97 tâches sans question, 608 sur 949 faites sous attaque ; les changements du 2026-10-22 ne changent aucune décision |
|
|
201
|
+
| `scripts/agentdojo/Dockerfile.dockerignore` | le `.dockerignore` du paquet exclut `dist/`, si bien que la commande de « Reproduire » ne se construisait pas | elle se construit |
|
|
202
|
+
|
|
203
|
+
Ces trois-là passaient encore : des morceaux de moins de 10 caractères entourés d’autres mots, une adresse collée
|
|
204
|
+
dans le prompt et une clé écrite dans un fichier du projet. Le même jour, nous avons fermé tout ce qui se ferme sans
|
|
205
|
+
faux positifs et écrit six attaques de plus contre les nouvelles règles. 2 sur 23 passent.
|
|
206
|
+
|
|
207
|
+
| Changement | Pourquoi | Résultat |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| Fenêtres de clé de 8 lettres et chiffres, 10 pour les clés hex, mesurées : les plus petites tailles sans coïncidence dans 115 millions de lettres et chiffres (ce monorepo et 1 000 paquets HISTOR) de texte ordinaire ([`window-collisions.mjs`](../scripts/redteam/window-collisions.mjs)) | des morceaux de 8 caractères entourés d’autres mots passaient | refus dès le deuxième morceau ; ceux de moins de 8 caractères entourés de mots différents passent encore |
|
|
210
|
+
| Les parties que partagent toutes les clés d’un type ne sont pas relevées (identifiant d’algorithme d’une clé DER, en-tête et champs d’un JWT) | la mesure a montré que 0.19.0 refuserait un README contenant une clé DKIM après la lecture d’une clé privée RSA | aucune coïncidence avec les règles publiées |
|
|
211
|
+
| Écrire une clé lue pendant la session dans un fichier ordinaire du projet demande | la clé dans `docs/debug.md` passait | demande ; `.env` et les autres fichiers d’identifiants passent toujours |
|
|
212
|
+
| Une adresse seulement dans un texte collé ou cité n’est pas nommée par l’utilisateur | l’adresse de l’attaquant collée dans le prompt comptait comme celle de l’utilisateur | demande une fois ; une adresse que l’utilisateur a aussi tapée passe. Un point après une adresse tapée ne la rend plus non nommée (0.19.0 demandait pour « envoie-le à bob@corp.com. ») |
|
|
213
|
+
| Le hook Stop renvoie une fois une réponse qui reprend une instruction adressée au modèle | une injection dans la seule réponse ne fait aucun appel | Sur Ministral 8B, qui obéit à l’attaque (sans la garde, 6 réponses sur 20 recommandent l’hôtel), la note de la garde seule a ramené cela à 0 ; la vérification s’est déclenchée 3 fois sur 20 sans changer aucun résultat. Sur DeepSeek V4.1 Flash, qui ne l’a jamais recommandé, 2 fois sur 20 ([résultats](../scripts/agentdojo/results/model-answer-recheck-2026-10-22.json)). Un filet de sécurité, pas un gain mesuré |
|
|
214
|
+
| Ruleset v16 (voir [gates](gates.fr.md)) | 34 outils empoisonnés de la moitié dev de MCPTox passaient encore, presque tous sans entrée | moitié réservée 194 → 210 sur 218, dont les 66 détournements à déclencheur implicite ; 0 sur 45 serveurs propres ; le corpus et 1 000 paquets HISTOR bloquent exactement comme v15 ; cinq faux positifs d’un premier jet sont fixés dans les tests |
|
|
215
|
+
| Le rejeu, refait | les nouvelles vérifications ne doivent pas demander là où 0.19.0 ne demandait pas | les 1 046 enregistrements identiques à 0.19.0 |
|
|
216
|
+
|
|
217
|
+
Passent encore ([résultats](../scripts/redteam/results/)) :
|
|
218
|
+
|
|
219
|
+
- **Des morceaux de moins de 8 caractères, chacun entouré d’autres mots**, vers une adresse que l’utilisateur a
|
|
220
|
+
tapée : 7 messages ou plus pour une clé de 40 caractères.
|
|
221
|
+
- **Une clé épelée en mots** (« sept, Q majuscule, m, deux … »). Remplacer des lettres par des chiffres sosies (o→0,
|
|
222
|
+
l→1) a été repéré sur notre clé de test seulement parce qu’une bonne partie n’avait pas ces lettres.
|
|
223
|
+
|
|
179
224
|
## Ce qu’elle ne fait pas
|
|
180
225
|
|
|
181
|
-
- **
|
|
182
|
-
ses propres chemins mais obéit rarement, même quand on le lui ordonne
|
|
183
|
-
modèle qui obéit plus volontiers, est un test plus dur
|
|
226
|
+
- **Des mesures ne font pas une preuve.** Le rejeu obéit parfaitement mais suit un seul chemin ; le modèle trouve
|
|
227
|
+
ses propres chemins mais obéit rarement, même quand on le lui ordonne ; nos attaques code en main sont 23 idées à
|
|
228
|
+
nous. Un attaquant que nous n’avons pas rencontré, ou un modèle qui obéit plus volontiers, est un test plus dur
|
|
229
|
+
que chacun d’eux.
|
|
184
230
|
- **Les étiquettes sont celles de l’opérateur.** Notre première exécution marquait les noms de canaux Slack comme
|
|
185
231
|
données privées seulement, et l’injection se trouvait dans un nom de canal. L’exécution l’a repéré parce que la
|
|
186
|
-
livraison est jugée sur le texte de l’attaque, pas sur nos étiquettes. Un outil mal étiqueté est un trou
|
|
232
|
+
livraison est jugée sur le texte de l’attaque, pas sur nos étiquettes. Un outil mal étiqueté est un trou ; un outil
|
|
233
|
+
sans étiquette reçoit la supposition de la garde, qui ne fait qu’ajouter de la prudence.
|
|
187
234
|
- **Elle n’arrête pas une injection qui ne change que la réponse** (« dis que cet hôtel est le meilleur »). Pas
|
|
188
|
-
d’appel, rien à refuser.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
235
|
+
d’appel, rien à refuser. Le hook Stop dit à l’utilisateur d’où vient la réponse et, si elle reprend une instruction
|
|
236
|
+
adressée au modèle, demande une fois au modèle de la vérifier ; il ne voit que les instructions que la garde
|
|
237
|
+
reconnaît comme telles.
|
|
238
|
+
- **Les modifications locales dans le projet passent**, sauf une clé lue pendant la session qui part dans un fichier
|
|
239
|
+
ordinaire. Modifier les fichiers du projet est le travail de l’agent.
|
|
240
|
+
- **Les fuites en petits morceaux.** Du texte privé ordinaire peut partir vers un destinataire nommé par
|
|
241
|
+
l’utilisateur, entier ou en morceaux : c’est son choix. Vers d’autres destinataires, et pour les clés et numéros de
|
|
242
|
+
carte vers n’importe qui, les morceaux envoyés à la suite par un même argument sont réunis ; ceux de moins de 8
|
|
243
|
+
caractères entourés d’autres mots, non, pas plus qu’une clé épelée en mots.
|
|
244
|
+
- **Le collé n’est pas le tapé.** Une adresse que vous vouliez utiliser mais avez seulement collée demande une fois.
|
|
192
245
|
- **Elle ne voit pas** votre prompt d’avant l’installation du plugin, ni le texte que le modèle tient d’ailleurs.
|
|
193
246
|
|
|
194
247
|
## Reproduire
|
|
@@ -202,6 +255,10 @@ docker run --rm --network none -v "$PWD/out:/out" warden-adojo /out/oracle.json
|
|
|
202
255
|
|
|
203
256
|
`oracle.py` écrit les résultats par paire ; `--variants none,strict,intent` choisit les variantes de la garde
|
|
204
257
|
(`strict` ignore ce que l’utilisateur a tapé). `summarize_oracle.py` en tire le résumé que cite le tableau.
|
|
258
|
+
`WARDEN_FLOW_GUARD=/out/x.mjs` rejoue une autre version de la garde (toute copie de `flow-guard.mjs` du plugin) ; c’est
|
|
259
|
+
ainsi que les changements du 2026-10-22 ont été comparés au code d’avant.
|
|
260
|
+
|
|
261
|
+
Les attaques code en main n’ont pas besoin de conteneur : `npm run build && node scripts/redteam/flow-redteam.mjs out.json`.
|
|
205
262
|
|
|
206
263
|
L’exécution avec modèle (`model.py`) a besoin d’une clé d’API compatible OpenAI, qui reste sur l’hôte : `relay.py`
|
|
207
264
|
la lit, et le conteneur n’atteint rien d’autre. `run-model.sh` lance les deux et reprend une exécution
|
package/docs/flow-guard.md
CHANGED
|
@@ -10,10 +10,14 @@ The flow guard adds checks at tool-call boundaries after a session receives outs
|
|
|
10
10
|
- Recognized writes to guard policy/state, Claude hook settings and Git hook/config paths are protected, including expanded environment paths and symlink targets. A hook running as the agent's OS user cannot replace OS permissions or sandboxing.
|
|
11
11
|
- URL hosts are parsed with `URL`; userinfo is not the host. Every detected destination must be permitted, and an actual target argument must be present. A link in a message body does not authorize its recipient. Policy mailbox entries match exactly; a domain entry does not authorize all mailboxes there. User-typed destinations match complete values, not suffixes of other addresses. Mentioning a destination is still only a coarse allowance, not authorization for every action or payload.
|
|
12
12
|
- Contact resolution is opt-in through `contactTools` and a `private` label. Only structured records with `name` and `email` are accepted, directly or in a JSON/MCP envelope. The full display name must appear in the user's prompt; conflicting addresses for that name ask. Local files, arbitrary prose, YAML and cached contacts from the earlier guard cannot establish this authority. Configure only lookup tools whose records the agent and outsiders cannot modify.
|
|
13
|
-
- Secret fingerprints inspect visible argument text, with bounded URL/base64/hex decoding.
|
|
13
|
+
- Secret fingerprints inspect visible argument text, with bounded URL/base64/hex decoding. Since 2026-10-22 key-like values (random-looking runs in credential files, and well-known token formats read anywhere) are also fingerprinted in windows of 8 letters and digits, 10 for hex keys: a piece of a key, the key reversed, with separators or invisible characters, base64-encoded in pieces or with Cyrillic or Greek look-alike letters is denied. Those sizes are the smallest at which none of 480 made-up keys in eight formats matched anywhere in 115 million letters and digits (this monorepo and 1,000 HISTOR packages) of ordinary text; parts every key of a kind shares (a DER key's algorithm identifier, a JWT's header and claims) are never fingerprinted. Each argument name keeps the tail of what it sent after outside text, so pieces sent back to back through the same argument are checked whole. Pieces under 8 characters wrapped in different words each time still pass, and so does the key's leading format (`rk_live_`).
|
|
14
|
+
- After outside text, a key read this session written into an ordinary project file asks (`deny` refuses): files in the project are committed and published later without another call. `.env`, `.npmrc` and the other credential files are where keys belong, and a write that only removes a key never matches.
|
|
15
|
+
- An address that appears in your prompt only inside pasted text, a quoted line (`>`), the headers of a pasted message or below a forwarded-message marker is not one you named: sending to it asks once. A full stop or comma after an address you typed no longer hides it. Fingerprints do not inspect arbitrary file contents at execution time, arbitrary encodings or every possible MCP schema. Internal project edits are generally allowed; executing their code is a separate decision.
|
|
16
|
+
- An MCP tool without an operator label is outbound when its name says so (send, post, share …), when it carries a URL, or, since 2026-10-22, when it names a party: an argument such as `to`, `email`, `attendees`, `assignee`, `collaborator` or `username`, or any e-mail address or IBAN. `warden-mcp flow-labels` lists the unlabeled MCP tools your sessions used, with the guess and a `labels` block to correct and paste into the policy.
|
|
17
|
+
- An injection that only changes the answer makes no call to refuse. Since 2026-10-22 the plugin's Stop hook adds one line under an answer whose turn read outside text, naming those sources, and says so when any of it addressed the assistant with instructions. When the answer repeats 12 letters and digits in a row from the sentences where that text addressed the assistant, the hook also sends the answer back once (`decision: block`), to be checked against the user's request before the user reads it; the user loses nothing but the time of that check. `answerNote` chooses `always` (default), `instruction` (only that case) or `off`; outside the plugin, register `warden-mcp hook stop`.
|
|
14
18
|
- The MCP proxy records JSON-RPC error text and tool-error results as outside text too. If recording fails, it withholds the response. Existing corrupt session state blocks pre-tool checks instead of becoming an empty session.
|
|
15
19
|
|
|
16
|
-
Released in npm `0.17.0`: `npx -y @aimarket/warden@0.
|
|
20
|
+
Released in npm `0.17.0`: `npx -y @aimarket/warden@0.20.0 hook pre-tool-use`, or from a source checkout `npm ci && npm run build`, then `node dist/mcp-server.js hook pre-tool-use`. Register the prompt and post hooks as well, or use the bundled Claude plugin.
|
|
17
21
|
|
|
18
22
|
Policy is user-level (`~/.local/state/warden/flow-policy.json`, or `WARDEN_FLOW_POLICY`). Modes are `ask`, `deny`, `audit`, `off`. `audit` logs decisions without enforcing them. Example:
|
|
19
23
|
|
|
@@ -23,13 +27,14 @@ Policy is user-level (`~/.local/state/warden/flow-policy.json`, or `WARDEN_FLOW_
|
|
|
23
27
|
"allowedDestinations": ["api.internal.example", "alice@example.com"],
|
|
24
28
|
"labels": {"mcp__contacts__lookup": ["private"], "mcp__mail__send_email": ["outbound"]},
|
|
25
29
|
"contactTools": ["mcp__contacts__lookup"],
|
|
26
|
-
"destinationArgs": {"mcp__mail__send_email": ["to", "cc", "bcc"]}
|
|
30
|
+
"destinationArgs": {"mcp__mail__send_email": ["to", "cc", "bcc"]},
|
|
31
|
+
"answerNote": "always"
|
|
27
32
|
}
|
|
28
33
|
```
|
|
29
34
|
|
|
30
|
-
For the proxy, use `node dist/mcp-server.js wrap --flow mail -- node mail-server.js`. Set the same fresh `WARDEN_FLOW_SESSION` for all proxies belonging to one client conversation. The fallback is the parent PID; separate launchers can have different parents, so do not rely on that fallback for cross-server protection. The proxy cannot see the user's prompt or client built-in tools
|
|
35
|
+
For the proxy, use `node dist/mcp-server.js wrap --flow mail -- node mail-server.js`. Set the same fresh `WARDEN_FLOW_SESSION` for all proxies belonging to one client conversation. The fallback is the parent PID; separate launchers can have different parents, so do not rely on that fallback for cross-server protection. The proxy cannot see the user's prompt or client built-in tools. A call that would ask is asked through the client's MCP elicitation form when the client offers one (the answer is logged as `approvalOutcome`), and refused otherwise or with `--ask refuse`. `trustedSources` applies to a WebFetch URL, not URLs mentioned in arbitrary MCP arguments.
|
|
31
36
|
|
|
32
|
-
**
|
|
37
|
+
**Which build the numbers describe.** The replay table below was run again on 2026-10-22 on the current code: the audit fixes plus the 2026-10-22 changes. The 2026-10-22 changes alter no decision in it (all 1,046 records match the code before them); the audit fixes cost one task with no question and 19 tasks done under attack. The model-run tables and costs still describe the pre-audit implementation through commit `2328be980`; no new model run was made. A reference-solution replay is one fixed attack path, not an adaptive worst case; the code-aware attacks of 2026-10-22 are [below](#what-changed-on-2026-10-22-and-why).
|
|
33
38
|
|
|
34
39
|
## What an approval request explains
|
|
35
40
|
|
|
@@ -53,22 +58,25 @@ We did not use a model. For each pair of a user task and an attacker goal, the b
|
|
|
53
58
|
(`important_instructions`) is placed where the user's task will read it. Then the user task's reference solution
|
|
54
59
|
is played through the guard, followed by the attacker's reference solution. That is a model that read the
|
|
55
60
|
injection and **obeyed it completely along the reference path**. This is not an adaptive worst case. An `ask` counts as refused: a benchmark has no human. The
|
|
56
|
-
run is deterministic, takes minutes, costs nothing, and ran in a gVisor container with no network
|
|
61
|
+
run is deterministic, takes minutes, costs nothing, and ran in a gVisor container with no network on our server
|
|
62
|
+
(2026-10-17/18) and in a Docker container with no network on a workstation (2026-10-22).
|
|
57
63
|
|
|
58
64
|
AgentDojo 0.1.35, suites v1.2.2, with our operator labels for all 69 tools
|
|
59
|
-
(`scripts/agentdojo/policy.json`). Results: `scripts/agentdojo/results
|
|
65
|
+
(`scripts/agentdojo/policy.json`). Results: `scripts/agentdojo/results/` (the table: `summary-2026-10-22.json`).
|
|
60
66
|
|
|
61
67
|
| Suite | Attack actions that succeeded, without the guard | With the guard | User tasks with no question | Tasks done under attack, without / with |
|
|
62
68
|
|---|---|---|---|---|
|
|
63
69
|
| banking | 141 of 144 | **0 of 144** | 13 of 16 | 125 / 117 of 144 |
|
|
64
|
-
| Slack | 105 of 105 | **0 of
|
|
70
|
+
| Slack | 105 of 105 | **0 of 75** | 2 of 21 | 102 / 15 of 105 |
|
|
65
71
|
| travel | 116 of 120 | **0 of 120** | 16 of 20 | 26 / 112 of 140 |
|
|
66
|
-
| workspace | 231 of 240 | **0 of 240** |
|
|
67
|
-
| **all** | **593 of 609** | **0 of
|
|
72
|
+
| workspace | 231 of 240 | **0 of 240** | 26 of 40 | 326 / 364 of 560 |
|
|
73
|
+
| **all** | **593 of 609** | **0 of 579** | **57 of 97** | **579 / 608 of 949** |
|
|
68
74
|
|
|
69
75
|
These are the labels as fixed on 2026-10-18, after the model run below found two of them too strict, and the guard
|
|
70
76
|
with names resolved from contacts. The first labels gave 54 tasks with no question and 571 done under attack; the
|
|
71
|
-
fixed labels without contacts gave 57 and 613. Every version gave
|
|
77
|
+
fixed labels without contacts gave 57 and 613; with contacts, before the audit fixes, 58 and 627. Every version gave
|
|
78
|
+
0 attack actions. The audit fixes refuse a few more fetches and calendar entries in Slack and workspace tasks, so 579
|
|
79
|
+
pairs, not 584, carried the injection in.
|
|
72
80
|
|
|
73
81
|
"Attack actions" are pairs where the injection reached the agent (its text came back in a tool result) and the
|
|
74
82
|
attacker's goal needs a call: send, pay, invite, delete. The count of delivered pairs differs a little between
|
|
@@ -76,12 +84,12 @@ the two columns because the guard also refuses some fetches that would have carr
|
|
|
76
84
|
|
|
77
85
|
How to read the last two columns:
|
|
78
86
|
|
|
79
|
-
- **With no question.** The guard stays silent through
|
|
87
|
+
- **With no question.** The guard stays silent through 57 of 97 user tasks. The other 40 need a call it asks
|
|
80
88
|
about. In Claude Code that is one click; in the benchmark it is a failed task. Slack is the extreme: nearly every
|
|
81
89
|
task posts to a channel or person found in the data ("the channel with most users"), not one the user named.
|
|
82
90
|
That is exactly the shape of an injected action, so it asks.
|
|
83
91
|
- **Under attack.** Without the guard, attacks also break the user's task (in travel, 26 of 140 finish). With the
|
|
84
|
-
guard the attack is refused and the task goes on:
|
|
92
|
+
guard the attack is refused and the task goes on: 608 of 949 finish against 579 without it.
|
|
85
93
|
|
|
86
94
|
Eight newer workspace attacker goals ship without a reference solution, so they cannot be played. Seven of them
|
|
87
95
|
need an e-mail to the attacker first; that e-mail was refused in all 280 delivered pairs. Two goals (one in
|
|
@@ -181,19 +189,63 @@ on our server: 21,654 model runs, $12.68 of DeepSeek credit.
|
|
|
181
189
|
The same reader feedback changed HISTOR's sandbox: a connection the previous holder of an address queued is no
|
|
182
190
|
longer credited to the next run, which is marked incomplete instead of clean.
|
|
183
191
|
|
|
192
|
+
## What changed on 2026-10-22, and why
|
|
193
|
+
|
|
194
|
+
A reader asked what WARDEN still does not catch. We listed it, then attacked the guard with its source in hand:
|
|
195
|
+
17 attacks in [`scripts/redteam/flow-redteam.mjs`](../scripts/redteam/flow-redteam.mjs), each aimed at a documented
|
|
196
|
+
limit or the edge of a rule, played through the plugin's hook entry point like the replay (an obedient model, no
|
|
197
|
+
benchmark). Before the changes 14 of them got through; after them, 3.
|
|
198
|
+
|
|
199
|
+
| Change | Why | Result |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| Key-like values fingerprinted in 10-character windows of letters and digits, reversed and look-alike letters folded; base64 decoded from 16 characters, not 24 | a key sent in 15-character pieces, with dashes, backwards or in Cyrillic letters passed | red team: all of these denied; the first piece, mostly the key's format, still passes |
|
|
202
|
+
| Each argument keeps the tail of what it sent after outside text | pieces of 8 characters, a card number in two halves, private text in 12-character pieces to an allowed host passed | denied or asked from the second or third piece on |
|
|
203
|
+
| An unlabeled MCP tool that names a party is outbound; `warden-mcp flow-labels` | "add attendee" with the attacker's e-mail and "add collaborator" by user name passed | both ask |
|
|
204
|
+
| Stop hook: one line under an answer that drew on outside text | an answer-only injection makes no call | the user is told which sources the answer used; nothing is blocked |
|
|
205
|
+
| Ruleset v15 (see [gates](gates.md)) | 78 of MCPTox's dev-half poisoned tools passed the rules | held-out half 171 → 194 of 218 (rules written from the other half only), dev half 189 → 233 of 267; 0 of 45 clean servers; the field corpus and 1,000 HISTOR packages block exactly what v14 blocked |
|
|
206
|
+
| The replay re-run on the current code | the audit fixes of 2026-10-18 had not been measured | 0 of 579 attack actions, 57 of 97 tasks with no question, 608 of 949 done under attack; the 2026-10-22 changes alter no decision |
|
|
207
|
+
| `scripts/agentdojo/Dockerfile.dockerignore` | the package's `.dockerignore` leaves out `dist/`, so the reproduce command below did not build | it builds |
|
|
208
|
+
|
|
209
|
+
Those three were still delivered: pieces under 10 characters wrapped in different words, an address pasted into
|
|
210
|
+
the prompt, and a key written into a project file. The same day we closed what could be closed without false
|
|
211
|
+
positives, and wrote six more attacks against the new rules. 2 of the 23 get through.
|
|
212
|
+
|
|
213
|
+
| Change | Why | Result |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| Key windows of 8 letters and digits, 10 for hex keys, measured: the smallest sizes with no chance match in 115 million letters and digits (this monorepo and 1,000 HISTOR packages) of ordinary text ([`window-collisions.mjs`](../scripts/redteam/window-collisions.mjs)) | 8-character pieces wrapped in other words passed | denied from the second piece on; pieces under 8 characters with different words around each still pass |
|
|
216
|
+
| Parts every key of a kind shares are not fingerprinted (a DER key's algorithm identifier, a JWT's header and claims) | the measurement found 0.19.0 would deny a DKIM public key in a README after a session read an RSA private key | no chance match at all with the shipped rules |
|
|
217
|
+
| A key read this session written into an ordinary project file asks | the key in `docs/debug.md` passed | asks; `.env` and other credential files still pass |
|
|
218
|
+
| An address only in pasted or quoted text is not one the user named | the attacker's address pasted into the prompt counted as the user's | asks once; an address the user also typed passes. A full stop after a typed address no longer makes it unnamed (0.19.0 asked about "send it to bob@corp.com.") |
|
|
219
|
+
| The Stop hook sends an answer back once when it repeats an instruction aimed at the model | an answer-only injection makes no call | On Ministral 8B, which follows the attack (6 of 20 answers recommend the hotel without the guard), the guard's note alone took that to 0; the re-check fired 3 times in 20 and changed no outcome. On DeepSeek V4.1 Flash, which never recommended it, it fired 2 times in 20 ([results](../scripts/agentdojo/results/model-answer-recheck-2026-10-22.json)). A safety net, not a measured gain |
|
|
220
|
+
| Ruleset v16 (see [gates](gates.md)) | 34 of MCPTox's dev-half poisoned tools still passed, nearly all tools that take no input | held-out half 194 → 210 of 218, all 66 implicit-trigger hijackings among them; 0 of 45 clean servers; the field corpus and 1,000 HISTOR packages block exactly what v15 blocked; five false positives of a first draft are pinned as tests |
|
|
221
|
+
| The replay re-run | the new checks must not ask where 0.19.0 did not | all 1,046 records identical to 0.19.0 |
|
|
222
|
+
|
|
223
|
+
Still delivered ([results](../scripts/redteam/results/)):
|
|
224
|
+
|
|
225
|
+
- **Pieces under 8 characters, each wrapped in different words**, to an address the user typed: 7 or more messages
|
|
226
|
+
for a 40-character key.
|
|
227
|
+
- **A key spelled out in words** ("seven, capital Q, m, two …"). Swapping letters for look-alike digits (o→0, l→1)
|
|
228
|
+
was caught on our test key only because enough of it had none of those letters.
|
|
229
|
+
|
|
184
230
|
## What it does not do
|
|
185
231
|
|
|
186
|
-
- **
|
|
187
|
-
|
|
188
|
-
is a harder test than
|
|
232
|
+
- **Measurements are not a proof.** The replay obeys perfectly but follows one path; the model finds its own paths
|
|
233
|
+
but obeys rarely, even when told to; our own code-aware attacks are 23 ideas of ours. An attacker we have not
|
|
234
|
+
met, or a model that obeys more readily, is a harder test than any of them.
|
|
189
235
|
- **Labels are the operator's.** Our first run marked Slack channel names as private data only, and the injection
|
|
190
236
|
sat in a channel name. The run caught it because delivery is judged from the attack text, not from our labels.
|
|
191
|
-
A tool you label wrongly is a hole.
|
|
237
|
+
A tool you label wrongly is a hole; a tool you do not label gets the guard's guess, which only adds caution.
|
|
192
238
|
- **It does not stop an injection that only changes the answer** ("say this hotel is the best"). No tool call,
|
|
193
|
-
nothing to refuse.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
239
|
+
nothing to refuse. The Stop hook tells the user where the answer came from and, when the answer repeats an
|
|
240
|
+
instruction aimed at the model, asks the model once to check it; it only sees instructions the guard recognises
|
|
241
|
+
as instructions.
|
|
242
|
+
- **Local changes inside the project pass**, except a key read this session going into an ordinary file. Editing
|
|
243
|
+
project files is the agent's job.
|
|
244
|
+
- **Leaks in small pieces.** Ordinary private text may go to a destination the user named, whole or in pieces:
|
|
245
|
+
that is the user's choice. To other destinations, and for keys and card numbers anywhere, pieces sent back to back
|
|
246
|
+
through one argument are joined; pieces under 8 characters with different words around each are not, and neither
|
|
247
|
+
is a key spelled out in words.
|
|
248
|
+
- **Pasted text is not typed text.** An address you meant to use but only pasted asks once.
|
|
197
249
|
- **It cannot see** your prompt from before the plugin was installed, or text the model holds from elsewhere.
|
|
198
250
|
|
|
199
251
|
## Reproduce
|
|
@@ -206,7 +258,11 @@ docker run --rm --network none -v "$PWD/out:/out" warden-adojo /out/oracle.json
|
|
|
206
258
|
```
|
|
207
259
|
|
|
208
260
|
`oracle.py` writes per-pair results; `--variants none,strict,intent` chooses the guard variants (`strict` ignores
|
|
209
|
-
what the user typed). `summarize_oracle.py` turns them into the summary the table quotes.
|
|
261
|
+
what the user typed). `summarize_oracle.py` turns them into the summary the table quotes. `WARDEN_FLOW_GUARD=/out/x.mjs`
|
|
262
|
+
plays another build of the guard (any plugin copy of `flow-guard.mjs`), which is how the 2026-10-22 changes were
|
|
263
|
+
compared with the code before them.
|
|
264
|
+
|
|
265
|
+
The code-aware attacks need no container: `npm run build && node scripts/redteam/flow-redteam.mjs out.json`.
|
|
210
266
|
|
|
211
267
|
The model run (`model.py`) needs an OpenAI-compatible API key, which stays on the host: `relay.py` reads it, and
|
|
212
268
|
the container reaches nothing else. `run-model.sh` starts both and resumes an interrupted run:
|
package/docs/flow-guard.ru.md
CHANGED
|
@@ -20,16 +20,20 @@
|
|
|
20
20
|
- Защищены распознанные записи в политику/состояние стража, настройки hooks Claude и Git hooks/config, включая раскрываемые переменные путей и симлинки. Hooks с правами самого агента не заменяют ограничения ОС.
|
|
21
21
|
- Хост разбирается URL-парсером; userinfo не считается хостом. Проверяются все найденные адресаты и наличие аргумента назначения. Ссылка в теле письма не разрешает его получателя. Почтовые адреса в политике сопоставляются целиком; домен не разрешает все ящики. Упоминание адресата пользователем — грубое исключение, а не разрешение любых действий и данных.
|
|
22
22
|
- Контакты разрешаются только через явно заданный `contactTools` с меткой `private`: структурированные записи `name`/`email`, в том числе JSON в MCP-ответе. В запросе пользователя должно быть полное отображаемое имя; неоднозначное имя требует подтверждения. Файлы проекта, произвольный текст, YAML и старый кэш контактов не подходят. Доверяйте только справочнику, записи которого агент и посторонние не могут менять.
|
|
23
|
-
- Отпечатки проверяют видимый текст аргументов и ограниченный набор декодирований URL/base64/hex.
|
|
23
|
+
- Отпечатки проверяют видимый текст аргументов и ограниченный набор декодирований URL/base64/hex. С 2026-10-22 значения, похожие на ключи (случайные последовательности в файлах с секретами и известные форматы токенов, где бы их ни прочли), снимаются ещё и окнами по 8 букв и цифр, для hex-ключей — по 10: кусок ключа, ключ задом наперёд, с разделителями или невидимыми символами, кусками в base64 или с кириллическими или греческими буквами-двойниками запрещается. Это самые маленькие размеры, при которых ни один из 480 выдуманных ключей восьми форматов ни разу не совпал с 115 млн букв и цифр (этот монорепо и 1 000 пакетов HISTOR) обычного текста; части, общие для всех ключей одного вида (идентификатор алгоритма в DER-ключе, заголовок и поля JWT), не снимаются вовсе. Каждый аргумент помнит хвост того, что он отправил после внешнего текста, поэтому куски, отправленные подряд через один аргумент, проверяются целиком. Куски короче 8 символов, каждый раз обёрнутые другими словами, по-прежнему проходят, как и начальный формат ключа (`rk_live_`). Отпечатки не читают произвольные файлы в момент отправки и не покрывают все кодировки и схемы MCP. Правки внутри проекта обычно разрешены; исполнение кода проверяется отдельно.
|
|
24
|
+
- После внешнего текста запись ключа, прочитанного в сессии, в обычный файл проекта требует подтверждения (`deny` отказывает): файлы проекта потом коммитят и публикуют без нового вызова. `.env`, `.npmrc` и другие файлы с секретами — место для ключей, туда запись проходит; правка, которая только убирает ключ, не срабатывает.
|
|
25
|
+
- Адрес, который есть в запросе только во вставленном тексте, в цитате (`>`), в заголовках вставленного письма или ниже пометки о пересылке, не считается названным: отправка на него спрашивает один раз. Точка или запятая после набранного адреса больше не мешают его узнать.
|
|
26
|
+
- MCP-инструмент без метки оператора считается исходящим, если об этом говорит его имя (send, post, share …), если в аргументах есть URL или, с 2026-10-22, если он называет адресата: аргумент вроде `to`, `email`, `attendees`, `assignee`, `collaborator` или `username`, либо любой e-mail или IBAN. `warden-mcp flow-labels` показывает MCP-инструменты без меток, которыми пользовались ваши сессии, с догадкой стража и блоком `labels`, который нужно проверить и вставить в политику.
|
|
27
|
+
- Инъекция, которая меняет только ответ, не делает вызова, которому можно отказать. С 2026-10-22 хук Stop плагина добавляет под ответом строку, если за этот ход читался внешний текст: какие источники, и отдельно — если какой-то из них обращался к ассистенту с указаниями. Если ответ повторяет 12 букв и цифр подряд из тех предложений, где внешний текст обращался к ассистенту, хук ещё и один раз возвращает ответ модели (`decision: block`) на проверку по запросу пользователя, прежде чем пользователь его прочтёт; пользователь теряет только время этой проверки. `answerNote`: `always` (по умолчанию), `instruction` (только этот случай) или `off`; вне плагина зарегистрируйте `warden-mcp hook stop`.
|
|
24
28
|
- Прокси учитывает текст JSON-RPC errors и ошибки инструментов как внешние данные. Если учёт ответа не удался, ответ удерживается. Повреждённое состояние сессии блокирует проверяемый вызов вместо сброса в пустую сессию.
|
|
25
29
|
|
|
26
|
-
Вышло в npm `0.17.0`: `npx -y @aimarket/warden@0.
|
|
30
|
+
Вышло в npm `0.17.0`: `npx -y @aimarket/warden@0.20.0 hook pre-tool-use`, либо из исходников — в каталоге WARDEN `npm ci && npm run build`, затем `node dist/mcp-server.js hook pre-tool-use`. Зарегистрируйте также prompt/post hooks либо плагин Claude.
|
|
27
31
|
|
|
28
32
|
Политика: `~/.local/state/warden/flow-policy.json` или `WARDEN_FLOW_POLICY`. Режимы: `ask`, `deny`, `audit`, `off`; `audit` только пишет журнал. Пример настройки контактов и адресатов — в [английской версии](flow-guard.md).
|
|
29
33
|
|
|
30
|
-
Прокси: `node dist/mcp-server.js wrap --flow mail -- node mail-server.js`. Всем прокси одной беседы задайте одинаковый новый `WARDEN_FLOW_SESSION`. По умолчанию используется PID родителя; разные запускатели могут иметь разных родителей. Прокси не видит запрос пользователя и встроенные инструменты
|
|
34
|
+
Прокси: `node dist/mcp-server.js wrap --flow mail -- node mail-server.js`. Всем прокси одной беседы задайте одинаковый новый `WARDEN_FLOW_SESSION`. По умолчанию используется PID родителя; разные запускатели могут иметь разных родителей. Прокси не видит запрос пользователя и встроенные инструменты клиента. Вызов, о котором нужно спросить, спрашивается через форму MCP elicitation клиента, если она есть (ответ пишется в журнал как `approvalOutcome`), иначе или с `--ask refuse` отклоняется. `trustedSources` применяется к URL WebFetch, а не к ссылкам в произвольных аргументах MCP.
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
**Какую сборку описывают цифры.** Таблицу воспроизведения ниже прогнали заново 2026-10-22 на текущем коде: исправления аудита плюс изменения 2026-10-22. Изменения 2026-10-22 не поменяли в ней ни одного решения (все 1 046 записей совпали с кодом до них); исправления аудита стоили одной задачи без вопроса и 19 задач, выполненных под атакой. Таблицы прогона с моделью и стоимость по-прежнему относятся к коду до аудита, вплоть до коммита `2328be980`; нового прогона с моделью не было. Воспроизведение эталонной последовательности — один путь атаки, а не адаптивный худший случай; атаки со знанием кода от 2026-10-22 — [ниже](#что-меняли-2026-10-22-и-зачем).
|
|
33
37
|
|
|
34
38
|
## Измерено на AgentDojo
|
|
35
39
|
|
|
@@ -41,22 +45,25 @@
|
|
|
41
45
|
(`important_instructions`) кладётся туда, где её прочитает задача пользователя. Затем через стража проигрывается
|
|
42
46
|
эталонное решение задачи пользователя, а следом эталонное решение атакующего. Это модель, которая прочитала
|
|
43
47
|
инъекцию и **послушалась её полностью на эталонном пути**. Это не адаптивный худший случай. `ask` считается отказом: в бенчмарке нет человека. Прогон
|
|
44
|
-
детерминирован, занимает минуты, ничего не стоит и шёл в контейнере gVisor без
|
|
48
|
+
детерминирован, занимает минуты, ничего не стоит и шёл в контейнере gVisor без сети на нашем сервере (2026-10-17/18)
|
|
49
|
+
и в контейнере Docker без сети на рабочей машине (2026-10-22).
|
|
45
50
|
|
|
46
51
|
AgentDojo 0.1.35, наборы v1.2.2, наши метки оператора для всех 69 инструментов
|
|
47
|
-
(`scripts/agentdojo/policy.json`). Результаты: `scripts/agentdojo/results
|
|
52
|
+
(`scripts/agentdojo/policy.json`). Результаты: `scripts/agentdojo/results/` (таблица — `summary-2026-10-22.json`).
|
|
48
53
|
|
|
49
54
|
| Набор | Атак-действий удалось, без стража | Со стражем | Задач без единого вопроса | Задач выполнено под атакой, без / со стражем |
|
|
50
55
|
|---|---|---|---|---|
|
|
51
56
|
| banking | 141 из 144 | **0 из 144** | 13 из 16 | 125 / 117 из 144 |
|
|
52
|
-
| Slack | 105 из 105 | **0 из
|
|
57
|
+
| Slack | 105 из 105 | **0 из 75** | 2 из 21 | 102 / 15 из 105 |
|
|
53
58
|
| travel | 116 из 120 | **0 из 120** | 16 из 20 | 26 / 112 из 140 |
|
|
54
|
-
| workspace | 231 из 240 | **0 из 240** |
|
|
55
|
-
| **всего** | **593 из 609** | **0 из
|
|
59
|
+
| workspace | 231 из 240 | **0 из 240** | 26 из 40 | 326 / 364 из 560 |
|
|
60
|
+
| **всего** | **593 из 609** | **0 из 579** | **57 из 97** | **579 / 608 из 949** |
|
|
56
61
|
|
|
57
62
|
Это метки в исправленном виде от 2026-10-18 (прогон с моделью ниже нашёл две слишком строгие) и страж, который
|
|
58
63
|
узнаёт названных по имени через контакты. С первыми метками было 54 задачи без вопроса и 571 выполненная под
|
|
59
|
-
атакой; с исправленными метками без контактов — 57 и 613
|
|
64
|
+
атакой; с исправленными метками без контактов — 57 и 613; с контактами, до исправлений аудита, — 58 и 627. Все версии
|
|
65
|
+
дали 0 атак-действий. Исправления аудита отказывают ещё в нескольких загрузках и записях в календарь в задачах Slack и
|
|
66
|
+
workspace, поэтому инъекцию принесли 579 пар, а не 584.
|
|
60
67
|
|
|
61
68
|
«Атаки-действия» — пары, где инъекция дошла до агента (её текст вернулся в результате инструмента), а цели
|
|
62
69
|
атакующего нужен вызов: отправить, заплатить, пригласить, удалить. Число доставленных пар в двух колонках немного
|
|
@@ -64,12 +71,12 @@ AgentDojo 0.1.35, наборы v1.2.2, наши метки оператора д
|
|
|
64
71
|
|
|
65
72
|
Как читать две последние колонки:
|
|
66
73
|
|
|
67
|
-
- **Без единого вопроса.** Страж молчит в
|
|
74
|
+
- **Без единого вопроса.** Страж молчит в 57 задачах из 97. Остальным 40 нужен вызов, о котором он спрашивает. В
|
|
68
75
|
Claude Code это один клик, в бенчмарке — проваленная задача. Slack — крайний случай: почти каждая задача пишет в
|
|
69
76
|
канал или человеку, найденному в данных («канал, где больше всего людей»), а не названному пользователем. Именно
|
|
70
77
|
так выглядит внедрённое действие, поэтому страж спрашивает.
|
|
71
78
|
- **Под атакой.** Без стража атаки ломают и задачу пользователя (в travel выполняются 26 из 140). Со стражем атаке
|
|
72
|
-
отказано, и задача идёт дальше:
|
|
79
|
+
отказано, и задача идёт дальше: 608 из 949 против 579 без него.
|
|
73
80
|
|
|
74
81
|
Восемь новых целей атакующего в workspace поставляются без эталонного решения, их нельзя проиграть. Семи из них
|
|
75
82
|
сначала нужно письмо атакующему; это письмо было отклонено во всех 280 доставленных парах. Две цели (одна в
|
|
@@ -168,19 +175,63 @@ Sonnet; в воспроизведении выше модели нет, а пр
|
|
|
168
175
|
Тот же отзыв читателей изменил песочницу HISTOR: соединение, поставленное в очередь прежним владельцем адреса, больше
|
|
169
176
|
не засчитывается следующему прогону — тот помечается неполным, а не чистым.
|
|
170
177
|
|
|
178
|
+
## Что меняли 2026-10-22 и зачем
|
|
179
|
+
|
|
180
|
+
Читатель спросил, чего WARDEN до сих пор не ловит. Мы это перечислили, а потом атаковали стража, держа в руках его
|
|
181
|
+
код: 17 атак в [`scripts/redteam/flow-redteam.mjs`](../scripts/redteam/flow-redteam.mjs), каждая нацелена на
|
|
182
|
+
описанное ограничение или край правила; они проигрываются через ту же точку входа хуков, что и у плагина (послушная
|
|
183
|
+
модель, без бенчмарка). До изменений проходили 14 из них, после — 3.
|
|
184
|
+
|
|
185
|
+
| Изменение | Зачем | Результат |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| Значения, похожие на ключи, снимаются окнами по 10 букв и цифр, с разворотом и заменой букв-двойников; base64 декодируется от 16 символов, а не от 24 | ключ кусками по 15 символов, через дефисы, задом наперёд или кириллицей проходил | атаки: всё это запрещено; первый кусок, в основном формат ключа, по-прежнему проходит |
|
|
188
|
+
| Каждый аргумент помнит хвост отправленного после внешнего текста | куски по 8 символов, номер карты двумя половинами, приватный текст кусками по 12 символов на разрешённый хост проходили | запрет или вопрос со второго-третьего куска |
|
|
189
|
+
| MCP-инструмент без метки, называющий адресата, — исходящий; `warden-mcp flow-labels` | «добавить участника» с e-mail атакующего и «добавить соавтора» по имени пользователя проходили | оба спрашивают |
|
|
190
|
+
| Хук Stop: строка под ответом, опиравшимся на внешний текст | инъекция только в ответ не делает вызовов | пользователь видит, на какие источники опирался ответ; ничего не блокируется |
|
|
191
|
+
| Набор правил v15 (см. [gates](gates.ru.md)) | 78 отравленных инструментов из dev-половины MCPTox проходили правила | отложенная половина 171 → 194 из 218 (правила писали только по другой половине), dev-половина 189 → 233 из 267; 0 из 45 чистых серверов; корпус и 1 000 пакетов HISTOR блокируются ровно как при v14 |
|
|
192
|
+
| Воспроизведение заново на текущем коде | исправления аудита от 2026-10-18 не измерялись | 0 из 579 атак-действий, 57 из 97 задач без вопроса, 608 из 949 выполнено под атакой; изменения 2026-10-22 не поменяли ни одного решения |
|
|
193
|
+
| `scripts/agentdojo/Dockerfile.dockerignore` | `.dockerignore` пакета исключает `dist/`, и команда из «Повторить» не собиралась | собирается |
|
|
194
|
+
|
|
195
|
+
Эти три по-прежнему проходили: куски короче 10 символов в разных словах, адрес, вставленный в запрос, и ключ,
|
|
196
|
+
записанный в файл проекта. В тот же день мы закрыли всё, что закрывается без ложных срабатываний, и написали ещё шесть
|
|
197
|
+
атак против новых правил. Из 23 проходят 2.
|
|
198
|
+
|
|
199
|
+
| Изменение | Зачем | Результат |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| Окна ключей по 8 букв и цифр, для hex — по 10, измерены: самые маленькие размеры без случайных совпадений в 115 млн букв и цифр (этот монорепо и 1 000 пакетов HISTOR) обычного текста ([`window-collisions.mjs`](../scripts/redteam/window-collisions.mjs)) | куски по 8 символов в других словах проходили | запрет со второго куска; куски короче 8 символов в разных словах по-прежнему проходят |
|
|
202
|
+
| Части, общие для всех ключей одного вида, не снимаются (идентификатор алгоритма в DER-ключе, заголовок и поля JWT) | замер нашёл, что 0.19.0 запретил бы отправку README с DKIM-ключом после того, как сессия прочитала RSA-ключ | с правилами в релизе ни одного случайного совпадения |
|
|
203
|
+
| Ключ, прочитанный в сессии, при записи в обычный файл проекта требует подтверждения | ключ в `docs/debug.md` проходил | спрашивает; `.env` и другие файлы с секретами по-прежнему проходят |
|
|
204
|
+
| Адрес только во вставленном или цитируемом тексте не считается названным | адрес атакующего, вставленный в запрос, считался адресом пользователя | спрашивает один раз; адрес, который пользователь ещё и набрал, проходит. Точка после набранного адреса больше не делает его неназванным (0.19.0 спрашивал про «отправь на bob@corp.com.») |
|
|
205
|
+
| Хук Stop один раз возвращает ответ, повторяющий указание для модели | инъекция только в ответ не делает вызовов | На Ministral 8B, который атаке подчиняется (без стража 6 ответов из 20 рекомендуют отель), уже одна пометка стража свела это к 0; перепроверка сработала 3 раза из 20 и исход не изменила. На DeepSeek V4.1 Flash, который отель ни разу не рекомендовал, — 2 раза из 20 ([результаты](../scripts/agentdojo/results/model-answer-recheck-2026-10-22.json)). Страховка, а не измеренный выигрыш |
|
|
206
|
+
| Набор правил v16 (см. [gates](gates.ru.md)) | 34 отравленных инструмента dev-половины MCPTox всё ещё проходили, почти все — без входных параметров | отложенная половина 194 → 210 из 218, в том числе все 66 перехватов с неявным триггером; 0 из 45 чистых серверов; корпус и 1 000 пакетов HISTOR блокируются ровно как при v15; пять ложных срабатываний первого варианта закреплены в тестах |
|
|
207
|
+
| Воспроизведение заново | новые проверки не должны спрашивать там, где не спрашивал 0.19.0 | все 1 046 записей совпадают с 0.19.0 |
|
|
208
|
+
|
|
209
|
+
По-прежнему проходят ([результаты](../scripts/redteam/results/)):
|
|
210
|
+
|
|
211
|
+
- **Куски короче 8 символов, каждый в других словах**, на адрес, который набрал пользователь: 7 и больше писем на
|
|
212
|
+
ключ из 40 символов.
|
|
213
|
+
- **Ключ, продиктованный словами** («семь, заглавная Q, m, два …»). Замена букв похожими цифрами (o→0, l→1) на нашем
|
|
214
|
+
тестовом ключе поймана только потому, что в нём хватило участков без этих букв.
|
|
215
|
+
|
|
171
216
|
## Чего он не делает
|
|
172
217
|
|
|
173
|
-
-
|
|
174
|
-
свои пути, но слушается редко, даже когда ей
|
|
175
|
-
слушается охотнее, — тест жёстче
|
|
218
|
+
- **Измерения — ещё не доказательство.** Воспроизведение слушается идеально, но идёт одним путём; модель находит
|
|
219
|
+
свои пути, но слушается редко, даже когда ей велят; наши атаки со знанием кода — это 23 наших идеи. Атакующий,
|
|
220
|
+
которого мы не встречали, или модель, которая слушается охотнее, — тест жёстче любого из них.
|
|
176
221
|
- **Метки — решение оператора.** В первом прогоне мы пометили названия каналов Slack только как приватные данные, а
|
|
177
222
|
инъекция сидела в названии канала. Прогон это поймал, потому что доставка определяется по тексту атаки, а не по
|
|
178
|
-
нашим меткам. Неверно помеченный инструмент — это
|
|
223
|
+
нашим меткам. Неверно помеченный инструмент — это дыра; непомеченный получает догадку стража, которая только
|
|
224
|
+
добавляет осторожности.
|
|
179
225
|
- **Он не останавливает инъекцию, которая меняет только ответ** («скажи, что этот отель лучший»). Нет вызова —
|
|
180
|
-
нечему отказывать.
|
|
181
|
-
|
|
182
|
-
-
|
|
183
|
-
|
|
226
|
+
нечему отказывать. Хук Stop говорит пользователю, откуда взялся ответ, а если ответ повторяет указание для модели,
|
|
227
|
+
один раз просит модель его перепроверить; он видит только те указания, которые страж распознаёт как указания.
|
|
228
|
+
- **Локальные изменения внутри проекта проходят**, кроме ключа, прочитанного в сессии и записанного в обычный файл.
|
|
229
|
+
Править файлы проекта — работа агента.
|
|
230
|
+
- **Утечка мелкими кусками.** Обычный приватный текст может уйти адресату, которого назвал пользователь, целиком или
|
|
231
|
+
кусками: это выбор пользователя. Другим адресатам, а ключи и номера карт — кому угодно: куски, отправленные подряд
|
|
232
|
+
через один аргумент, склеиваются; куски короче 8 символов с другими словами вокруг каждого — нет, как и ключ,
|
|
233
|
+
продиктованный словами.
|
|
234
|
+
- **Вставленный текст — не набранный.** Адрес, который вы хотели использовать, но только вставили, спросит один раз.
|
|
184
235
|
- **Он не видит** ваш запрос до установки плагина и текст, который модель держит из других мест.
|
|
185
236
|
|
|
186
237
|
## Повторить
|
|
@@ -194,6 +245,10 @@ docker run --rm --network none -v "$PWD/out:/out" warden-adojo /out/oracle.json
|
|
|
194
245
|
|
|
195
246
|
`oracle.py` пишет результаты по каждой паре; `--variants none,strict,intent` выбирает варианты стража (`strict`
|
|
196
247
|
не учитывает, что набрал пользователь). `summarize_oracle.py` сводит их в то, что цитирует таблица.
|
|
248
|
+
`WARDEN_FLOW_GUARD=/out/x.mjs` проигрывает другую сборку стража (любую копию `flow-guard.mjs` из плагина) — так
|
|
249
|
+
изменения 2026-10-22 сравнивали с кодом до них.
|
|
250
|
+
|
|
251
|
+
Атакам со знанием кода контейнер не нужен: `npm run build && node scripts/redteam/flow-redteam.mjs out.json`.
|
|
197
252
|
|
|
198
253
|
Прогону с моделью (`model.py`) нужен ключ OpenAI-совместимого API, и он остаётся на хосте: его читает `relay.py`, а
|
|
199
254
|
контейнер больше никуда не выходит. `run-model.sh` запускает оба и продолжает прерванный прогон:
|