@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/README-es.md +16 -14
  3. package/README-fr.md +16 -14
  4. package/README-ru.md +16 -14
  5. package/README-zh.md +16 -14
  6. package/README.md +17 -15
  7. package/dist/claude-hook.d.ts.map +1 -1
  8. package/dist/claude-hook.js +9 -3
  9. package/dist/claude-hook.js.map +1 -1
  10. package/dist/flow-guard.d.ts +91 -5
  11. package/dist/flow-guard.d.ts.map +1 -1
  12. package/dist/flow-guard.js +433 -19
  13. package/dist/flow-guard.js.map +1 -1
  14. package/dist/mcp-server.js +4 -0
  15. package/dist/mcp-server.js.map +1 -1
  16. package/dist/review-diff.d.ts +5 -4
  17. package/dist/review-diff.d.ts.map +1 -1
  18. package/dist/review-diff.js.map +1 -1
  19. package/dist/static-scan.d.ts +1 -1
  20. package/dist/static-scan.d.ts.map +1 -1
  21. package/dist/static-scan.js +109 -3
  22. package/dist/static-scan.js.map +1 -1
  23. package/dist/wrap-cli.d.ts +6 -0
  24. package/dist/wrap-cli.d.ts.map +1 -1
  25. package/dist/wrap-cli.js +8 -2
  26. package/dist/wrap-cli.js.map +1 -1
  27. package/dist/wrap.d.ts.map +1 -1
  28. package/dist/wrap.js +125 -19
  29. package/dist/wrap.js.map +1 -1
  30. package/docs/flow-guard.es.md +77 -21
  31. package/docs/flow-guard.fr.md +78 -21
  32. package/docs/flow-guard.md +78 -22
  33. package/docs/flow-guard.ru.md +75 -20
  34. package/docs/flow-guard.zh.md +57 -18
  35. package/docs/gates.es.md +36 -6
  36. package/docs/gates.fr.md +36 -5
  37. package/docs/gates.md +36 -6
  38. package/docs/gates.ru.md +35 -6
  39. package/docs/gates.zh.md +10 -5
  40. package/docs/scanner-comparison.es.md +25 -1
  41. package/docs/scanner-comparison.fr.md +25 -1
  42. package/docs/scanner-comparison.md +24 -1
  43. package/docs/scanner-comparison.ru.md +25 -1
  44. package/docs/scanner-comparison.zh.md +20 -1
  45. package/package.json +1 -1
@@ -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.18.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).
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 et refuse les appels qui demanderaient confirmation. `trustedSources` s’applique à l’URL WebFetch, pas aux liens des arguments MCP.
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
- **Mesures historiques :** les tableaux, images et coûts ci-dessous concernent les versions avant audit jusqu’à `2328be980`, pas ce code ni ruleset v14. Aucun nouveau modèle, replay ou benchmark de corpus n’a été exécuté ; uniquement des tests locaux de régression. Un replay suit un chemin fixe, pas le pire cas adaptatif. Les artefacts d’origine restent inchangés.
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 80** | 2 sur 21 | 102 / 20 sur 105 |
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** | 27 sur 40 | 326 / 378 sur 560 |
51
- | **total** | **593 sur 609** | **0 sur 584** | **58 sur 97** | **579 / 627 sur 949** |
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. Toutes les
56
- versions ont donné 0 sur 584.
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 58 tâches sur 97. Les 39 autres ont besoin d’un appel
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 : 627 sur 949 aboutissent, contre 579 sans
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
- - **Deux mesures ne font pas une preuve.** Le rejeu obéit parfaitement mais suit un seul chemin ; le modèle trouve
182
- ses propres chemins mais obéit rarement, même quand on le lui ordonne. Un attaquant qui étudie la garde, ou un
183
- modèle qui obéit plus volontiers, est un test plus dur que l’un et l’autre.
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
- - **Les modifications locales dans le projet passent.** Modifier les fichiers du projet est le travail de l’agent.
190
- - **Les fuites en petits morceaux**, sous la taille d’une empreinte (moins d’environ 23 caractères par appel), ne
191
- sont pas repérées par les empreintes ; l’appel demande quand même si son destinataire n’a pas été nommé.
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
@@ -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. They do not inspect arbitrary file contents at execution time, arbitrary encodings or every possible MCP schema. Short fragments, unknown tools and unsupported sources remain limitations. Internal project edits are generally allowed; executing their code is a separate decision.
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.18.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.
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 and refuses calls that would ask. `trustedSources` applies to a WebFetch URL, not URLs mentioned in arbitrary MCP arguments.
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
- **Historical measurements below:** all retained AgentDojo tables, charts and costs describe the pre-audit implementations through commit `2328be980`. They are not measurements of this hardened implementation or ruleset v14. No new model, replay or corpus benchmark was run for the audit fixes; only local regression tests. A reference-solution replay is one fixed attack path, not an adaptive worst case. The older policy and result artifacts are preserved unchanged.
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 80** | 2 of 21 | 102 / 20 of 105 |
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** | 27 of 40 | 326 / 378 of 560 |
67
- | **all** | **593 of 609** | **0 of 584** | **58 of 97** | **579 / 627 of 949** |
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 0 of 584.
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 58 of 97 user tasks. The other 39 need a call it asks
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: 627 of 949 finish against 579 without it.
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
- - **Two measurements are not a proof.** The replay obeys perfectly but follows one path; the model finds its own
187
- paths but obeys rarely, even when told to. An attacker who studies the guard, or a model that obeys more readily,
188
- is a harder test than either.
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
- - **Local changes inside the project pass.** Editing project files is the agent's job.
195
- - **Leaks in small pieces** below the fingerprint size (fewer than about 23 characters per call) are not caught by
196
- fingerprints; the call still asks if its destination was not named.
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:
@@ -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. Они не читают произвольные файлы в момент отправки и не покрывают все кодировки и схемы MCP. Короткие фрагменты, неизвестные инструменты и неподдерживаемые источники остаются ограничениями. Правки внутри проекта обычно разрешены; исполнение кода проверяется отдельно.
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.18.0 hook pre-tool-use`, либо из исходников — в каталоге WARDEN `npm ci && npm run build`, затем `node dist/mcp-server.js hook pre-tool-use`. Зарегистрируйте также prompt/post hooks либо плагин Claude.
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 родителя; разные запускатели могут иметь разных родителей. Прокси не видит запрос пользователя и встроенные инструменты клиента; вместо вопроса отказывает. `trustedSources` применяется к URL WebFetch, а не к ссылкам в произвольных аргументах MCP.
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
- **Исторические измерения ниже:** сохранённые таблицы, графики и стоимость относятся к реализациям до исправлений аудита, вплоть до коммита `2328be980`. Это не измерения нового кода и ruleset v14. Новых прогонов моделей, replay или корпусов не делали; запускали только локальные регрессионные тесты. Воспроизведение эталонной последовательности — один путь атаки, а не адаптивный худший случай. Старые политики и результаты сохранены без изменений.
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 из 80** | 2 из 21 | 102 / 20 из 105 |
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** | 27 из 40 | 326 / 378 из 560 |
55
- | **всего** | **593 из 609** | **0 из 584** | **58 из 97** | **579 / 627 из 949** |
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. Все версии дали 0 из 584.
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
- - **Без единого вопроса.** Страж молчит в 58 задачах из 97. Остальным 39 нужен вызов, о котором он спрашивает. В
74
+ - **Без единого вопроса.** Страж молчит в 57 задачах из 97. Остальным 40 нужен вызов, о котором он спрашивает. В
68
75
  Claude Code это один клик, в бенчмарке — проваленная задача. Slack — крайний случай: почти каждая задача пишет в
69
76
  канал или человеку, найденному в данных («канал, где больше всего людей»), а не названному пользователем. Именно
70
77
  так выглядит внедрённое действие, поэтому страж спрашивает.
71
78
  - **Под атакой.** Без стража атаки ломают и задачу пользователя (в travel выполняются 26 из 140). Со стражем атаке
72
- отказано, и задача идёт дальше: 627 из 949 против 579 без него.
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
- - **Утечку мелкими кусками** меньше размера отпечатка (примерно до 23 символов за вызов) отпечатки не ловят; вызов
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` запускает оба и продолжает прерванный прогон: