@aimarket/warden 0.0.1 → 0.3.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 ADDED
@@ -0,0 +1,62 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@aimarket/warden`.
4
+
5
+ ## 0.3.0 — 2026-08-24
6
+
7
+ First standalone release. The gates, the threat feed, the pinning store contract and the RFC 8785
8
+ canonicalizer were extracted from [ARGUS](https://github.com/alexar76/argus) 0.3.0, where they had
9
+ lived as `src/warden/`. The version number is deliberately continuous with the agent that shipped
10
+ them, not reset to 0.1.0: this is the same enforcement code, at the same ruleset version (`v2`,
11
+ digest `sha256-gWC14PR4kUylkJaAGMnIYYX6tPhZTJ60cSB61UZxuWc=`), with the same 85 behavioural tests
12
+ that guarded it there.
13
+
14
+ ### Why it moved
15
+
16
+ A host that wants an MCP firewall had to install an agent — with an MCP SDK, a wallet library and a
17
+ post-quantum keystore in tow — to get one. This package has **zero runtime dependencies** and imports
18
+ nothing but `node:crypto`.
19
+
20
+ ### Changed for standalone use
21
+
22
+ - **Host seams are narrowed and named.** Pinning needs `PinStore` (`getPin`/`putPin`) instead of
23
+ ARGUS's full `MemoryStore`, and logging needs `WardenLogger` instead of ARGUS's `Logger`. Both are
24
+ structural, so an existing host store/logger usually satisfies them unchanged.
25
+ - **`log` is now optional** on `WardenInit` and `WardenCreateDeps`, defaulting to the new
26
+ `silentLogger()`. A host with no logger of its own still gets enforcement — verdicts are returned as
27
+ data from `vet()`, never inferred from log output.
28
+ - **`threatFeed` stays required.** Constructing one silently would hide from the host that no external
29
+ intel is in play.
30
+ - **Subpath export `@aimarket/warden/jcs`** so another implementation can be byte-checked against the
31
+ canonicalizer without pulling in the gate chain.
32
+ - **Types are exported from the entry point** (`WardenPolicy`, `WardenVerdict`, `WardenFinding`,
33
+ `ThreatRecord`, `ToolDef`, …). ARGUS now re-exports them from here rather than declaring its own
34
+ copies, so a change on this side breaks its build instead of drifting silently.
35
+ - No behavioural change to any gate. The rule table, the severities, the tiers, the score arithmetic
36
+ and every finding code are the ones ARGUS 0.3.0 shipped.
37
+
38
+ ### Added
39
+
40
+ - `test/packaging.test.ts` — fails if a runtime dependency appears, if any source file imports outside
41
+ the package, or if the entry point stops exporting the enforcement surface. The zero-dependency claim
42
+ is the reason this package exists, so it is a test and not a sentence in a README.
43
+ - `test/docs.test.ts` — resolves every relative link across the five-language doc set and holds the
44
+ quoted test count to what the runner actually reports.
45
+ - `test/no-phantom-gate.test.ts` — the gate-chain half of ARGUS's regression guard for a removed
46
+ reputation gate: vetting opens no socket, and no gate may report a service as unreachable without
47
+ having sent a request. (The oracle-side half stayed with the oracle client, in ARGUS.)
48
+ - Documentation in English, Russian, Spanish, French and Chinese: the gate chain, the signed threat-feed
49
+ contract, and an integration guide.
50
+
51
+ ### Upgrading from ARGUS internals
52
+
53
+ If you imported the gates through ARGUS's source tree, the mapping is mechanical:
54
+
55
+ | Before | After |
56
+ |---|---|
57
+ | `src/warden/index.js` | `@aimarket/warden` |
58
+ | `src/warden/sandbox.js` | `@aimarket/warden` (`EgressGuard`, `isSensitiveTool`, `classifyTools`) |
59
+ | `src/warden/pinning.js` | `@aimarket/warden` (`PinningGate`, `canonicalToolsHash`, …) |
60
+ | `src/warden/jcs.js` | `@aimarket/warden/jcs` or the root export |
61
+ | `MemoryStore` (for pins) | `PinStore` |
62
+ | `Logger` | `WardenLogger` |
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AICOM (alexar76)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README-es.md ADDED
@@ -0,0 +1,184 @@
1
+ # WARDEN — cortafuegos de seguridad para MCP
2
+
3
+ <!-- aicom-readme-badges -->
4
+ <p align="center">
5
+ <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
+ <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
+ <img src="docs/badges/deps.svg" alt="Cero dependencias de ejecución" />
8
+ <img src="docs/badges/tests.svg" alt="95 pruebas en verde" />
9
+ <img src="docs/badges/node.svg" alt="Node >= 20" />
10
+ <a href="LICENSE"><img src="docs/badges/license.svg" alt="Licencia: MIT" /></a>
11
+ </p>
12
+ <!-- /aicom-readme-badges -->
13
+
14
+ > 🌐 [English](README.md) · [Русский](README-ru.md) · **Español** · [Français](README-fr.md) · [中文](README-zh.md) · [Glosario](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
+
16
+ Un servidor MCP le dice a tu agente qué hacen sus herramientas. El agente se lo cree — y esa frase
17
+ es la superficie de ataque. La descripción de una herramienta es texto de prompt que un tercero
18
+ entrega directamente al contexto de tu modelo, y un campo de esquema llamado `api_key` es una
19
+ petición de tus secretos redactada como si fuera una API.
20
+
21
+ WARDEN examina un servidor **antes de que ninguna de sus herramientas llegue al modelo**, y devuelve
22
+ un veredicto que puedes registrar: permitir/bloquear, una puntuación 0..1, los hallazgos que la
23
+ produjeron, una partición por herramienta y la tabla de reglas exacta que estaba en vigor.
24
+
25
+ ```bash
26
+ npm install @aimarket/warden
27
+ ```
28
+
29
+ **Cero dependencias de ejecución.** El único import de todo el paquete es `node:crypto`. Es el
30
+ cortafuegos de [ARGUS](https://github.com/alexar76/argus), extraído para que puedas ponerlo delante
31
+ de tu propio host MCP sin adoptar un agente.
32
+
33
+ ## Inicio rápido
34
+
35
+ ```ts
36
+ import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
37
+
38
+ const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
39
+ await threatFeed.load(process.env.FEED_URL); // sin URL → solo la lista de denegación integrada, sin red
40
+
41
+ const pins = new Map();
42
+ const warden = Warden.create({
43
+ policy: {
44
+ blockAtSeverity: "high",
45
+ sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
46
+ allowUnknownServers: false, // fail-closed: solo los servidores que declaraste
47
+ pinToolDefs: true,
48
+ },
49
+ threatFeed,
50
+ store: {
51
+ getPin: async (id) => pins.get(id),
52
+ putPin: async (p) => void pins.set(p.serverId, p),
53
+ },
54
+ log: silentLogger(), // o tu propio logger
55
+ });
56
+
57
+ const verdict = await warden.vet(server, await client.listTools());
58
+
59
+ if (!verdict.allow) throw new Error(`bloqueado por ${verdict.decidedBy}`);
60
+ const usable = verdict.allowedTools; // una herramienta envenenada puede quedar aislada sola
61
+ await warden.approve(server, tools); // fija (pin) lo que el usuario aceptó
62
+ ```
63
+
64
+ `vet()` **no hace ninguna petición de red**. La única petición que WARDEN llega a hacer es la
65
+ descarga del threat feed que tú pediste al pasar una URL a `load()`.
66
+
67
+ ## La cadena de puertas
68
+
69
+ ```mermaid
70
+ flowchart LR
71
+ T["definiciones<br/>de herramientas"] --> S["static scan<br/>25 reglas"]
72
+ S --> F["threat feed<br/>11 integradas + firmado"]
73
+ F --> O["origin<br/>declarado vs catálogo"]
74
+ O --> P["pinning<br/>deriva vs aprobación"]
75
+ P --> V["veredicto<br/>allow · score · findings<br/>allowedTools / blockedTools"]
76
+ ```
77
+
78
+ | Puerta | Qué decide | Red | ¿Fatal? |
79
+ |---|---|---|---|
80
+ | **static-scan** | Inyección, exfiltración, peticiones de credenciales y señales de Unicode oculto/base64 en `description` e `inputSchema` — 25 reglas, v2, de las cuales 18 pueden bloquear y 7 son solo de aviso | ninguna | no |
81
+ | **threat-feed** | Identidad de servidor o herramienta conocida como maliciosa: 11 registros integrados más un feed firmado opcional | solo la descarga del feed | sí, para un `critical` con alcance de servidor |
82
+ | **origin** | Si el operador declaró este servidor o llegó desde un catálogo remoto | ninguna | sí, con `allowUnknownServers: false` |
83
+ | **pinning** | Si las definiciones de herramientas siguen coincidiendo con lo que el usuario aprobó | ninguna | sí, con `pinToolDefs: true` |
84
+
85
+ La puntuación compuesta es el **producto** de las contribuciones de cada puerta: una puerta mala
86
+ arrastra al servidor entero en lugar de promediarse. Severidad y bloqueo son ejes distintos: un
87
+ hallazgo `advisory` se reporta y nunca bloquea ni cuesta una herramienta, con cualquier
88
+ `blockAtSeverity` — porque «cuánta atención merece esto» y «¿es esto un defecto?» son preguntas
89
+ distintas, y codificar la segunda como una severidad baja volvía a hacerla bloqueante para quien
90
+ endureciera el umbral.
91
+
92
+ ## El veredicto está pensado para quedar registrado
93
+
94
+ ```ts
95
+ {
96
+ allow: false,
97
+ score: 0,
98
+ decidedBy: "threat-feed",
99
+ findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
100
+ allowedTools: ["add"],
101
+ blockedTools: ["sweeper"],
102
+ rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
103
+ }
104
+ ```
105
+
106
+ `rulesets` no es decoración. El mismo servidor puntúa distinto bajo una tabla de reglas posterior, y
107
+ sin la versión *y* un digest sobre las reglas no hay forma de distinguir eso de que el servidor haya
108
+ cambiado. Un escaneo guardado sin ellos no es reproducible.
109
+
110
+ ## Threat feed firmado
111
+
112
+ WARDEN no leerá un feed remoto sin firma. El contrato es deliberadamente aburrido:
113
+
114
+ ```
115
+ GET <la url de tu feed>
116
+ { "records": [ {pattern, severity, code, reason, source, scope}, … ],
117
+ "timestamp": 1786205907380, // epoch ms, entero — obligatorio
118
+ "signature": "f588d5a4…" // Ed25519 (hex) sobre la forma canónica
119
+ } // RFC 8785 de {records, timestamp}
120
+ ```
121
+
122
+ Se comprueban tres propiedades, y **cualquier fallo conserva el suelo integrado** en lugar de
123
+ degradar a ninguna protección:
124
+
125
+ 1. **autenticidad** — Ed25519 contra la clave que fijaste de antemano (`feedPublicKey`);
126
+ 2. **frescura** — el timestamp *firmado* debe caer dentro de `maxAgeMs` (24 h por defecto), para que
127
+ quien sirve la URL no pueda reproducir una instantánea de hace meses y borrar en silencio cada
128
+ registro añadido desde entonces. Una firma dice quién escribió un documento, nunca cuándo te lo
129
+ entregaron;
130
+ 3. **determinismo** — bytes canónicos RFC 8785, para que editor y verificador coincidan sin importar
131
+ el orden de las claves JSON.
132
+
133
+ [MOMUS](https://github.com/alexar76/momus) es un editor de referencia de este contrato
134
+ (`/warden/threat-feed`) si necesitas algo a lo que apuntar con `load()`.
135
+
136
+ ## También incluido
137
+
138
+ - **`EgressGuard`** — una lista de permitidos de salida con la que envolver cualquier petición que
139
+ haga una herramienta. Una herramienta que alcanza un host que nunca listaste es la señal clásica de
140
+ phone-home. `*.example.com` cubre subdominios; una lista vacía lo bloquea todo, no lo permite todo.
141
+ - **`isSensitiveTool` / `classifyTools`** — clasificación por glob de las herramientas que deben
142
+ exigir aprobación en cada llamada. Las herramientas sensibles siguen *anunciadas*: simplemente no
143
+ pueden ejecutarse sin supervisión.
144
+ - **`canonicalize` / `parseJsonStrict`** — una implementación estricta de RFC 8785 (JCS), exportada
145
+ también como `@aimarket/warden/jcs` para poder comparar otra implementación byte a byte. Solo
146
+ enteros más allá de `MAX_SAFE_JSON_INTEGER`, rechazo (no escapado) de surrogados solitarios y un
147
+ código de motivo en cada rechazo.
148
+
149
+ ## Documentación
150
+
151
+ | | |
152
+ |---|---|
153
+ | [La cadena de puertas](docs/gates.es.md) | Cada nivel de regla, cada código de hallazgo, cómo se construye la puntuación compuesta y cómo añadir una puerta |
154
+ | [El threat feed firmado](docs/threat-feed.es.md) | El contrato en el cable, las tres comprobaciones y cómo publicar un feed que WARDEN acepte |
155
+ | [Guía de integración](docs/integration.es.md) | Cómo conectar WARDEN a tu propio host MCP, elecciones de política y qué registrar |
156
+
157
+ ## Lo que esto no es
158
+
159
+ - **No es un sandbox.** Son decisiones JS dentro del proceso. Aquí no hay confinamiento del proceso
160
+ hijo MCP a nivel de sistema operativo (seccomp/Landlock, `sandbox-exec`).
161
+ - **No es un modelo.** En ninguna parte de la cadena se llama a un LLM. Por eso `vet()` es rápido,
162
+ offline y determinista — y por eso el escaneo estático tiene forma de regex y se le escapará una
163
+ paráfrasis que ninguna regla cubra.
164
+ - **No es un servicio de reputación.** Una versión anterior tenía una puerta que pedía a un oráculo
165
+ de confianza una puntuación para la que no tenía datos, y luego informaba de que el oráculo era
166
+ inalcanzable sin haber enviado ninguna petición. Se eliminó, y
167
+ `test/no-phantom-gate.test.ts` falla si alguna puerta vuelve a declarar inalcanzabilidad.
168
+ - **No sustituye a leer las definiciones de herramientas.** 11 registros integrados de amenazas son
169
+ un suelo, no un catálogo.
170
+
171
+ ## Desarrollo
172
+
173
+ ```bash
174
+ npm install && npm run build && npm test # 95 pruebas
175
+ ```
176
+
177
+ `test/packaging.test.ts` es lo que mantiene honesto el titular: falla si aparece una dependencia de
178
+ ejecución, si algún fichero fuente importa fuera del paquete o si el punto de entrada deja de
179
+ exportar la superficie de aplicación.
180
+
181
+ Usado por [ARGUS](https://github.com/alexar76/argus) (el host de referencia),
182
+ [MOMUS](https://github.com/alexar76/momus) (el lado editor) y el curso de seguridad MCP de AICOM.
183
+
184
+ MIT © AICOM (alexar76)
package/README-fr.md ADDED
@@ -0,0 +1,184 @@
1
+ # WARDEN — pare-feu de sécurité MCP
2
+
3
+ <!-- aicom-readme-badges -->
4
+ <p align="center">
5
+ <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
+ <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="version npm" /></a>
7
+ <img src="docs/badges/deps.svg" alt="Zéro dépendance d'exécution" />
8
+ <img src="docs/badges/tests.svg" alt="95 tests au vert" />
9
+ <img src="docs/badges/node.svg" alt="Node >= 20" />
10
+ <a href="LICENSE"><img src="docs/badges/license.svg" alt="Licence : MIT" /></a>
11
+ </p>
12
+ <!-- /aicom-readme-badges -->
13
+
14
+ > 🌐 [English](README.md) · [Русский](README-ru.md) · [Español](README-es.md) · **Français** · [中文](README-zh.md) · [Glossaire](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
+
16
+ Un serveur MCP indique lui-même à votre agent ce que font ses outils. L'agent le croit — et cette
17
+ phrase est la surface d'attaque. La description d'un outil est du texte de prompt qu'un tiers livre
18
+ directement dans le contexte de votre modèle, et un champ de schéma nommé `api_key` est une demande
19
+ de vos secrets formulée comme une API.
20
+
21
+ WARDEN examine un serveur **avant qu'aucun de ses outils n'atteigne le modèle**, et renvoie un verdict
22
+ que vous pouvez consigner : autoriser/bloquer, un score 0..1, les constats qui l'ont produit, une
23
+ partition par outil et la table de règles exacte qui était en vigueur.
24
+
25
+ ```bash
26
+ npm install @aimarket/warden
27
+ ```
28
+
29
+ **Zéro dépendance d'exécution.** Le seul import de tout le paquet est `node:crypto`. C'est le pare-feu
30
+ d'[ARGUS](https://github.com/alexar76/argus), extrait pour que vous puissiez le placer devant votre
31
+ propre hôte MCP sans adopter d'agent.
32
+
33
+ ## Démarrage rapide
34
+
35
+ ```ts
36
+ import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
37
+
38
+ const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
39
+ await threatFeed.load(process.env.FEED_URL); // sans URL → uniquement la liste de refus intégrée, sans réseau
40
+
41
+ const pins = new Map();
42
+ const warden = Warden.create({
43
+ policy: {
44
+ blockAtSeverity: "high",
45
+ sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
46
+ allowUnknownServers: false, // fail-closed : uniquement les serveurs que vous avez déclarés
47
+ pinToolDefs: true,
48
+ },
49
+ threatFeed,
50
+ store: {
51
+ getPin: async (id) => pins.get(id),
52
+ putPin: async (p) => void pins.set(p.serverId, p),
53
+ },
54
+ log: silentLogger(), // ou votre propre logger
55
+ });
56
+
57
+ const verdict = await warden.vet(server, await client.listTools());
58
+
59
+ if (!verdict.allow) throw new Error(`bloqué par ${verdict.decidedBy}`);
60
+ const usable = verdict.allowedTools; // un outil empoisonné peut être isolé seul
61
+ await warden.approve(server, tools); // épingler (pin) ce que l'utilisateur a accepté
62
+ ```
63
+
64
+ `vet()` **n'effectue aucune requête réseau**. La seule requête que WARDEN émet jamais est le
65
+ téléchargement du threat feed que vous avez demandé en passant une URL à `load()`.
66
+
67
+ ## La chaîne de portes
68
+
69
+ ```mermaid
70
+ flowchart LR
71
+ T["définitions<br/>des outils"] --> S["static scan<br/>25 règles"]
72
+ S --> F["threat feed<br/>11 intégrées + signé"]
73
+ F --> O["origin<br/>déclaré vs catalogue"]
74
+ O --> P["pinning<br/>dérive vs approbation"]
75
+ P --> V["verdict<br/>allow · score · findings<br/>allowedTools / blockedTools"]
76
+ ```
77
+
78
+ | Porte | Ce qu'elle décide | Réseau | Fatale ? |
79
+ |---|---|---|---|
80
+ | **static-scan** | Injection, exfiltration, demandes d'identifiants et indices d'Unicode masqué/base64 dans `description` et `inputSchema` — 25 règles, v2, dont 18 peuvent bloquer et 7 sont purement indicatives | aucun | non |
81
+ | **threat-feed** | Identité de serveur ou outil connu comme malveillant : 11 enregistrements intégrés plus un feed signé optionnel | seulement le téléchargement du feed | oui, pour un `critical` de portée serveur |
82
+ | **origin** | Si l'opérateur a déclaré ce serveur ou s'il provient d'un catalogue distant | aucun | oui, avec `allowUnknownServers: false` |
83
+ | **pinning** | Si les définitions d'outils correspondent encore à ce que l'utilisateur a approuvé | aucun | oui, avec `pinToolDefs: true` |
84
+
85
+ Le score composite est le **produit** des contributions de chaque porte : une seule porte mauvaise
86
+ entraîne tout le serveur vers le bas au lieu d'être moyennée. Sévérité et blocage sont deux axes
87
+ distincts : un constat `advisory` est rapporté et ne bloque jamais et ne coûte jamais un outil, quel
88
+ que soit `blockAtSeverity` — parce que « quelle attention cela mérite-t-il » et « est-ce un défaut »
89
+ sont deux questions différentes, et encoder la seconde par une sévérité basse la rendait de nouveau
90
+ bloquante pour quiconque durcissait le seuil.
91
+
92
+ ## Le verdict est fait pour être consigné
93
+
94
+ ```ts
95
+ {
96
+ allow: false,
97
+ score: 0,
98
+ decidedBy: "threat-feed",
99
+ findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
100
+ allowedTools: ["add"],
101
+ blockedTools: ["sweeper"],
102
+ rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
103
+ }
104
+ ```
105
+
106
+ `rulesets` n'est pas un ornement. Le même serveur obtient un autre score sous une table de règles
107
+ plus récente, et sans la version *et* une empreinte des règles, rien ne permet de distinguer cela
108
+ d'un serveur qui a changé. Un scan conservé sans elles n'est pas reproductible.
109
+
110
+ ## Threat feed signé
111
+
112
+ WARDEN ne lira pas un feed distant non signé. Le contrat est délibérément ennuyeux :
113
+
114
+ ```
115
+ GET <l'url de votre feed>
116
+ { "records": [ {pattern, severity, code, reason, source, scope}, … ],
117
+ "timestamp": 1786205907380, // epoch ms, entier — obligatoire
118
+ "signature": "f588d5a4…" // Ed25519 (hex) sur la forme canonique
119
+ } // RFC 8785 de {records, timestamp}
120
+ ```
121
+
122
+ Trois propriétés sont vérifiées, et **tout échec conserve le socle intégré** au lieu de dégrader vers
123
+ une absence de protection :
124
+
125
+ 1. **authenticité** — Ed25519 contre la clé que vous avez épinglée à l'avance (`feedPublicKey`) ;
126
+ 2. **fraîcheur** — le timestamp *signé* doit tomber dans `maxAgeMs` (24 h par défaut), afin que celui
127
+ qui sert l'URL ne puisse pas rejouer un instantané vieux de plusieurs mois et effacer en silence
128
+ chaque enregistrement ajouté depuis. Une signature dit qui a écrit un document, jamais quand il
129
+ vous a été remis ;
130
+ 3. **déterminisme** — octets canoniques RFC 8785, pour que l'éditeur et le vérificateur s'accordent
131
+ quel que soit l'ordre des clés JSON.
132
+
133
+ [MOMUS](https://github.com/alexar76/momus) est un éditeur de référence de ce contrat
134
+ (`/warden/threat-feed`) si vous cherchez une cible pour `load()`.
135
+
136
+ ## Également inclus
137
+
138
+ - **`EgressGuard`** — une liste blanche de sortie pour envelopper toute requête émise par un outil.
139
+ Un outil qui joint un hôte que vous n'avez jamais listé est le signe classique du phone-home.
140
+ `*.example.com` couvre les sous-domaines ; une liste vide bloque tout, au lieu de tout autoriser.
141
+ - **`isSensitiveTool` / `classifyTools`** — classification par glob des outils qui doivent exiger une
142
+ approbation à chaque appel. Les outils sensibles restent *annoncés* : ils ne peuvent simplement pas
143
+ s'exécuter sans surveillance.
144
+ - **`canonicalize` / `parseJsonStrict`** — une implémentation stricte de RFC 8785 (JCS), également
145
+ exportée sous `@aimarket/warden/jcs` afin qu'une autre implémentation puisse être vérifiée octet par
146
+ octet. Entiers uniquement au-delà de `MAX_SAFE_JSON_INTEGER`, refus (et non échappement) des
147
+ surrogates isolés, et un code de motif sur chaque refus.
148
+
149
+ ## Documentation
150
+
151
+ | | |
152
+ |---|---|
153
+ | [La chaîne de portes](docs/gates.fr.md) | Chaque niveau de règle, chaque code de constat, la construction du score composite et comment ajouter une porte |
154
+ | [Le threat feed signé](docs/threat-feed.fr.md) | Le contrat sur le fil, les trois vérifications, et comment publier un feed que WARDEN acceptera |
155
+ | [Guide d'intégration](docs/integration.fr.md) | Brancher WARDEN sur votre propre hôte MCP, choix de politique, et quoi consigner |
156
+
157
+ ## Ce que ce n'est pas
158
+
159
+ - **Pas un sandbox.** Ce sont des décisions JS intra-processus. Le confinement du processus enfant MCP
160
+ au niveau du système d'exploitation (seccomp/Landlock, `sandbox-exec`) n'est pas ici.
161
+ - **Pas un modèle.** Aucun LLM n'est appelé dans la chaîne. C'est pourquoi `vet()` est rapide, hors
162
+ ligne et déterministe — et pourquoi le scan statique a la forme d'expressions régulières et laissera
163
+ passer une paraphrase qu'aucune règle ne couvre.
164
+ - **Pas un service de réputation.** Une version antérieure avait une porte qui demandait à un oracle
165
+ de confiance un score qu'il n'avait aucune donnée pour calculer, puis signalait l'oracle comme
166
+ injoignable sans avoir envoyé la moindre requête. Elle a été supprimée, et
167
+ `test/no-phantom-gate.test.ts` échoue si une porte déclare de nouveau une injoignabilité.
168
+ - **Pas un substitut à la lecture des définitions d'outils.** 11 enregistrements de menaces intégrés
169
+ sont un socle, pas un catalogue.
170
+
171
+ ## Développement
172
+
173
+ ```bash
174
+ npm install && npm run build && npm test # 95 tests
175
+ ```
176
+
177
+ `test/packaging.test.ts` est ce qui tient l'accroche honnête : il échoue si une dépendance d'exécution
178
+ apparaît, si un fichier source importe hors du paquet, ou si le point d'entrée cesse d'exporter la
179
+ surface d'application.
180
+
181
+ Utilisé par [ARGUS](https://github.com/alexar76/argus) (l'hôte de référence),
182
+ [MOMUS](https://github.com/alexar76/momus) (le côté éditeur) et le cours AICOM sur la sécurité MCP.
183
+
184
+ MIT © AICOM (alexar76)
package/README-ru.md ADDED
@@ -0,0 +1,183 @@
1
+ # WARDEN — файрвол безопасности MCP
2
+
3
+ <!-- aicom-readme-badges -->
4
+ <p align="center">
5
+ <a href="https://github.com/alexar76/warden/actions/workflows/ci.yml"><img src="docs/badges/ci.svg" alt="CI" /></a>
6
+ <a href="https://www.npmjs.com/package/@aimarket/warden"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
+ <img src="docs/badges/deps.svg" alt="Zero runtime dependencies" />
8
+ <img src="docs/badges/tests.svg" alt="95 tests passing" />
9
+ <img src="docs/badges/node.svg" alt="Node >= 20" />
10
+ <a href="LICENSE"><img src="docs/badges/license.svg" alt="License: MIT" /></a>
11
+ </p>
12
+ <!-- /aicom-readme-badges -->
13
+
14
+ > 🌐 [English](README.md) · **Русский** · [Español](README-es.md) · [Français](README-fr.md) · [中文](README-zh.md) · [Глоссарий](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
15
+
16
+ MCP-сервер сам сообщает вашему агенту, что делают его инструменты. Агент этому верит — и вот эта
17
+ фраза и есть поверхность атаки. Описание инструмента — это текст промпта, который третья сторона
18
+ доставляет прямо в контекст модели, а поле схемы с именем `api_key` — это запрос ваших секретов,
19
+ сформулированный как API.
20
+
21
+ WARDEN проверяет сервер **до того, как его инструменты дойдут до модели**, и возвращает вердикт,
22
+ который можно записать: allow/block, оценку 0..1, найденные проблемы, разбиение по инструментам и
23
+ точную таблицу правил, которая действовала в момент проверки.
24
+
25
+ ```bash
26
+ npm install @aimarket/warden
27
+ ```
28
+
29
+ **Ноль рантайм-зависимостей.** Единственный импорт во всём пакете — `node:crypto`. Это файрвол из
30
+ [ARGUS](https://github.com/alexar76/argus), вынесенный отдельно, чтобы его можно было поставить
31
+ перед своим MCP-хостом, не переезжая на агента.
32
+
33
+ ## Быстрый старт
34
+
35
+ ```ts
36
+ import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
37
+
38
+ const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
39
+ await threatFeed.load(process.env.FEED_URL); // без URL → только встроенный deny-list, без сети
40
+
41
+ const pins = new Map();
42
+ const warden = Warden.create({
43
+ policy: {
44
+ blockAtSeverity: "high",
45
+ sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
46
+ allowUnknownServers: false, // fail-closed: только объявленные вами серверы
47
+ pinToolDefs: true,
48
+ },
49
+ threatFeed,
50
+ store: {
51
+ getPin: async (id) => pins.get(id),
52
+ putPin: async (p) => void pins.set(p.serverId, p),
53
+ },
54
+ log: silentLogger(), // или ваш собственный логгер
55
+ });
56
+
57
+ const verdict = await warden.vet(server, await client.listTools());
58
+
59
+ if (!verdict.allow) throw new Error(`заблокировано гейтом ${verdict.decidedBy}`);
60
+ const usable = verdict.allowedTools; // отравленный инструмент можно изолировать в одиночку
61
+ await warden.approve(server, tools); // зафиксировать (pin) то, что подтвердил пользователь
62
+ ```
63
+
64
+ `vet()` **не делает сетевых запросов**. Единственный запрос, который WARDEN вообще выполняет, — это
65
+ загрузка threat feed, о которой вы попросили сами, передав URL в `load()`.
66
+
67
+ ## Цепочка гейтов
68
+
69
+ ```mermaid
70
+ flowchart LR
71
+ T["определения<br/>инструментов"] --> S["static scan<br/>25 правил"]
72
+ S --> F["threat feed<br/>11 встроенных + подписанный"]
73
+ F --> O["origin<br/>объявлен или из каталога"]
74
+ O --> P["pinning<br/>дрейф против подтверждения"]
75
+ P --> V["вердикт<br/>allow · score · findings<br/>allowedTools / blockedTools"]
76
+ ```
77
+
78
+ | Гейт | Что решает | Сеть | Fatal? |
79
+ |---|---|---|---|
80
+ | **static-scan** | Инъекции, эксфильтрация, запросы учётных данных, скрытый Unicode и base64-признаки в `description` и `inputSchema` — 25 правил, версия 2, из них 18 могут блокировать и 7 только сообщают | нет | нет |
81
+ | **threat-feed** | Известный плохой сервер или инструмент: 11 встроенных записей плюс опциональный подписанный feed | только загрузка feed | да, для `critical` на уровне сервера |
82
+ | **origin** | Объявил ли оператор этот сервер, или он пришёл из удалённого каталога | нет | да, при `allowUnknownServers: false` |
83
+ | **pinning** | Совпадают ли определения инструментов с тем, что подтвердил пользователь | нет | да, при `pinToolDefs: true` |
84
+
85
+ Composite score — это **произведение** вкладов гейтов: один плохой гейт тянет весь сервер вниз, а не
86
+ усредняется. Severity и блокировка — разные оси: находка с `advisory` сообщается, но никогда не
87
+ блокирует и никогда не стоит инструмента, при любом `blockAtSeverity`. Потому что «сколько внимания
88
+ это заслуживает» и «дефект ли это вообще» — разные вопросы, а кодирование второго через низкую
89
+ severity снова делало находку блокирующей для всех, кто ужесточил порог.
90
+
91
+ ## Вердикт рассчитан на то, чтобы его записали
92
+
93
+ ```ts
94
+ {
95
+ allow: false,
96
+ score: 0,
97
+ decidedBy: "threat-feed",
98
+ findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
99
+ allowedTools: ["add"],
100
+ blockedTools: ["sweeper"],
101
+ rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
102
+ }
103
+ ```
104
+
105
+ `rulesets` — не украшение. Один и тот же сервер получит другую оценку под более поздней таблицей
106
+ правил, и без версии *и* дайджеста по правилам невозможно отличить это от того, что изменился сам
107
+ сервер. Сохранённый скан без них невоспроизводим.
108
+
109
+ ## Подписанный threat feed
110
+
111
+ WARDEN не станет читать неподписанный удалённый feed. Контракт намеренно скучный:
112
+
113
+ ```
114
+ GET <ваш feed url>
115
+ { "records": [ {pattern, severity, code, reason, source, scope}, … ],
116
+ "timestamp": 1786205907380, // epoch ms, целое — обязательно
117
+ "signature": "f588d5a4…" // Ed25519 (hex) по канонической форме
118
+ } // RFC 8785 от {records, timestamp}
119
+ ```
120
+
121
+ Проверяются три свойства, и **любой сбой сохраняет встроенный минимум**, а не деградирует до полного
122
+ отсутствия защиты:
123
+
124
+ 1. **подлинность** — Ed25519 против ключа, закреплённого заранее (`feedPublicKey`);
125
+ 2. **свежесть** — *подписанный* timestamp должен попадать в окно `maxAgeMs` (по умолчанию 24 ч),
126
+ чтобы тот, кто раздаёт URL, не мог подсунуть снимок месячной давности и молча стереть все
127
+ добавленные с тех пор записи. Подпись говорит, кто написал документ, но никогда — когда его вам
128
+ вручили;
129
+ 3. **детерминизм** — канонические байты RFC 8785, чтобы издатель и проверяющий совпадали независимо
130
+ от порядка ключей в JSON.
131
+
132
+ [MOMUS](https://github.com/alexar76/momus) — референсный издатель этого контракта
133
+ (`/warden/threat-feed`), если нужно, на что направить `load()`.
134
+
135
+ ## Что ещё внутри
136
+
137
+ - **`EgressGuard`** — allowlist исходящих запросов, которым можно обернуть любой запрос инструмента.
138
+ Инструмент, тянущийся к хосту, которого вы не перечисляли, — классический признак phone-home.
139
+ `*.example.com` совпадает с поддоменами; пустой allowlist блокирует всё, а не разрешает всё.
140
+ - **`isSensitiveTool` / `classifyTools`** — glob-классификация инструментов, которые обязаны требовать
141
+ подтверждения на каждый вызов. Чувствительные инструменты остаются *объявленными* — они просто не
142
+ могут выполняться без присмотра.
143
+ - **`canonicalize` / `parseJsonStrict`** — строгая реализация RFC 8785 (JCS), экспортируется и как
144
+ `@aimarket/warden/jcs`, чтобы другую реализацию можно было сверить с ней побайтово. Только целые
145
+ за пределами `MAX_SAFE_JSON_INTEGER`, отказ (а не экранирование) на одиночных суррогатах и код
146
+ причины на каждом отказе.
147
+
148
+ ## Документация
149
+
150
+ | | |
151
+ |---|---|
152
+ | [Цепочка гейтов](docs/gates.ru.md) | Все уровни правил, все коды находок, как складывается composite score и как добавить свой гейт |
153
+ | [Подписанный threat feed](docs/threat-feed.ru.md) | Контракт на проводе, три проверки и как публиковать feed, который WARDEN примет |
154
+ | [Руководство по интеграции](docs/integration.ru.md) | Как встроить WARDEN в свой MCP-хост, выбор политики и что записывать |
155
+
156
+ ## Чем это не является
157
+
158
+ - **Не песочница.** Это внутрипроцессные решения на JS. Изоляции дочернего MCP-процесса на уровне ОС
159
+ (seccomp/Landlock, `sandbox-exec`) здесь нет.
160
+ - **Не модель.** В цепочке нигде не вызывается LLM. Именно поэтому `vet()` быстрый, офлайновый и
161
+ детерминированный — и именно поэтому static scan имеет форму регулярок и пропустит перефразировку,
162
+ которую не покрывает ни одно правило.
163
+ - **Не сервис репутации.** В более ранней версии был гейт, который спрашивал у trust-оракула оценку,
164
+ для вычисления которой у того не было данных, а затем сообщал, что оракул недоступен, так и не
165
+ отправив запроса. Гейт удалён, и `test/no-phantom-gate.test.ts` падает, если какой-нибудь гейт
166
+ снова заявит о недоступности.
167
+ - **Не замена чтению определений инструментов.** 11 встроенных записей об угрозах — это минимум, а не
168
+ каталог.
169
+
170
+ ## Разработка
171
+
172
+ ```bash
173
+ npm install && npm run build && npm test # 95 тестов
174
+ ```
175
+
176
+ `test/packaging.test.ts` — то, что удерживает заголовок честным: он падает, если появляется
177
+ рантайм-зависимость, если какой-то файл импортирует что-то за пределами пакета или если точка входа
178
+ перестаёт экспортировать поверхность энфорсмента.
179
+
180
+ Используется в [ARGUS](https://github.com/alexar76/argus) (референсный хост),
181
+ [MOMUS](https://github.com/alexar76/momus) (сторона издателя) и в курсе AICOM по безопасности MCP.
182
+
183
+ MIT © AICOM (alexar76)