@aimarket/warden 0.3.0 → 0.5.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 (51) hide show
  1. package/CHANGELOG.md +155 -3
  2. package/README-es.md +82 -11
  3. package/README-fr.md +82 -11
  4. package/README-ru.md +82 -11
  5. package/README-zh.md +79 -10
  6. package/README.md +91 -14
  7. package/dist/glob.d.ts +81 -0
  8. package/dist/glob.d.ts.map +1 -0
  9. package/dist/glob.js +180 -0
  10. package/dist/glob.js.map +1 -0
  11. package/dist/index.d.ts +42 -9
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +84 -29
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp-rpc.d.ts +40 -0
  16. package/dist/mcp-rpc.d.ts.map +1 -0
  17. package/dist/mcp-rpc.js +117 -0
  18. package/dist/mcp-rpc.js.map +1 -0
  19. package/dist/mcp-server.d.ts +10 -0
  20. package/dist/mcp-server.d.ts.map +1 -0
  21. package/dist/mcp-server.js +89 -0
  22. package/dist/mcp-server.js.map +1 -0
  23. package/dist/mcp-tools.d.ts +42 -0
  24. package/dist/mcp-tools.d.ts.map +1 -0
  25. package/dist/mcp-tools.js +652 -0
  26. package/dist/mcp-tools.js.map +1 -0
  27. package/dist/origin.d.ts.map +1 -1
  28. package/dist/origin.js +7 -2
  29. package/dist/origin.js.map +1 -1
  30. package/dist/pinning.d.ts +18 -0
  31. package/dist/pinning.d.ts.map +1 -1
  32. package/dist/pinning.js +59 -11
  33. package/dist/pinning.js.map +1 -1
  34. package/dist/sandbox.d.ts.map +1 -1
  35. package/dist/sandbox.js +10 -7
  36. package/dist/sandbox.js.map +1 -1
  37. package/dist/sanitize.d.ts +38 -0
  38. package/dist/sanitize.d.ts.map +1 -0
  39. package/dist/sanitize.js +53 -0
  40. package/dist/sanitize.js.map +1 -0
  41. package/dist/static-scan.d.ts +46 -2
  42. package/dist/static-scan.d.ts.map +1 -1
  43. package/dist/static-scan.js +358 -34
  44. package/dist/static-scan.js.map +1 -1
  45. package/dist/threat-feed.d.ts +3 -0
  46. package/dist/threat-feed.d.ts.map +1 -1
  47. package/dist/threat-feed.js +118 -29
  48. package/dist/threat-feed.js.map +1 -1
  49. package/dist/types.d.ts +15 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +11 -4
package/CHANGELOG.md CHANGED
@@ -2,14 +2,113 @@
2
2
 
3
3
  All notable changes to `@aimarket/warden`.
4
4
 
5
+ ## 0.5.0 — 2026-09-03
6
+
7
+ stdio MCP server for Glama / Claude Desktop / Cursor (`warden-mcp` / `node dist/mcp-server.js`).
8
+ Six tools with TDQS-oriented definitions (title, when-to-use / when-not naming siblings, MCP
9
+ annotations, per-parameter descriptions, `outputSchema`). Dockerfile + `glama.json`. No secrets,
10
+ no npm runtime dependencies. The process inspects a `tools/list` dump; it does not launch the
11
+ server under scan.
12
+
13
+ ## 0.4.0 — 2026-08-24
14
+
15
+ Calibrated against the ecosystem, not against fixtures. WARDEN was pointed at every public MCP
16
+ server it could legitimately reach — 1 108 of them answered a live `tools/list` with 17 491 tool
17
+ definitions — and the result is written up in [`docs/mcp-survey.md`](docs/mcp-survey.md).
18
+
19
+ Ruleset `v3` blocked 50 of those 1 108 servers. Four held up on review. The other 46 were blocked
20
+ for saying the right thing: *"Never send a private key"*, *"the private key never leaves your
21
+ machine"*, a security scanner listing the attacks it detects, a Persian tool description spelled with
22
+ the ZERO WIDTH NON-JOINER its language requires. A scanner with that false-positive profile does not
23
+ get tuned by its users; it gets uninstalled.
24
+
25
+ Ruleset **v4** brings blocking from 50 servers down to **6** on the same corpus, with all four real
26
+ findings still caught.
27
+
28
+ ### Rules read context now
29
+
30
+ Rules gained **guards** — named context checks that decide whether a match is the thing the rule is
31
+ looking for. A guard is part of the published rule table and therefore of the digest: the same regex
32
+ with and without `polarity` is a different ruleset, and a recorded verdict has to be able to say
33
+ which one it was.
34
+
35
+ - **`polarity`** — a credential noun inside a refusal is a promise, not a request. This one
36
+ distinction accounted for 390 of the 492 blocking findings in the survey.
37
+ - **`mention`** — a phrase in quotes, in backticks, or as a bare JSON `enum` value is cited, not said.
38
+ - **`detection`** — a secret named as the object of `detect`/`scan`/`find`/`leaked` is what a scanner
39
+ looks for, not what it wants.
40
+ - **`identifierFragment`** — `mnemonic` inside `bip39-mnemonic-checksum` is that identifier's name.
41
+ - **`harvestTarget`** — a harvest instruction says *whose* secret or *where* it lives. Without it,
42
+ "Obtain a permanent anonymous API key" read as theft rather than issuance.
43
+ - **`uri`** — `javascript:` is now matched case-**sensitively** and needs a payload. Under `/i` it
44
+ matched the word *JavaScript* followed by a colon, i.e. every language list ever written.
45
+ - **`payload`** — a `data:…;base64,` URI with fewer than 32 characters behind the comma is the format
46
+ being documented.
47
+ - **`blob`** — `/` is in the base64 alphabet, so a JSON Schema `$ref` pointer read as a hidden blob.
48
+ Now gated on entropy and on schema keywords.
49
+ - **`zeroWidth`** — U+200C/U+200D adjacent to Arabic, Persian or Indic script is orthography. U+200B,
50
+ U+FEFF and the bidi overrides still block.
51
+ - **`publicKeyPath`** — `authorized_keys`, `known_hosts` and `*.pub` are public by definition.
52
+
53
+ ### Rules demoted from block to advise
54
+
55
+ Each of these was measured selecting for honest servers. They are still reported.
56
+
57
+ - **`exfiltrat*`** as a bare noun. An attacker does not name the attack; a defender names it in every
58
+ sentence. All three hits were defensive tools. The anchored *"send X to <external destination>"*
59
+ rules keep the blocking weight.
60
+ - **`system prompt` / `developer message`**. 15 findings across 6 servers, every one an LLM proxy,
61
+ persona manager or agent-configuration tool that declares a `system` parameter because setting a
62
+ system prompt is its job.
63
+ - **`do not tell the user`**. Four real uses, four of them the opposite of concealment — *"no refund
64
+ is issued automatically … do not tell the user a refund is coming"*. A blocking rule needs a
65
+ concealment target that refers to the tool's own action; the bare phrase carries none.
66
+ - The **credential nouns** keep blocking but drop from `critical` to `high`: still over the default
67
+ threshold, no longer zeroing the gate score. One noun in a schema template shared by 377 tools
68
+ should not read as "maximally compromised".
69
+
70
+ ### Threat feed
71
+
72
+ - Wildcard threat patterns now use `threatMatch`: **interior gaps are bounded** to 24 characters and
73
+ a segment starting with a letter must **start on a word boundary**. `*sweep*funds*` was matching
74
+ `funds` inside "re**funds**", and `*drain*wallet*` was joining two words from different clauses.
75
+ Leading and trailing `*` stay unbounded, and `_`/`-` count as boundaries so a `seed_phrase` schema
76
+ field still matches. `policy.sensitiveToolPatterns` keeps plain glob semantics — that is the
77
+ operator's own pattern against their own tool names.
78
+
79
+ ### Findings say what they matched
80
+
81
+ - Every finding message now quotes the matched text: `… signature (\b(?:read|extract|…) at "obtain
82
+ the redacted credential"`. Without it a reviewer cannot tell which alternative of an alternation
83
+ fired, or on what. Recovering that by hand is most of the work of judging a finding, and it cost
84
+ hours in the survey itself.
85
+ - Dropped matches are logged at `debug` with the guard's reason, because a guard silently discarding a
86
+ finding is the one behaviour in this gate that a verdict cannot show.
87
+
88
+ ### Release gate
89
+
90
+ - `npm run check:ruleset` fails if the version in `package.json` is already on the registry with a
91
+ different ruleset ref, and runs in CI and in `prepublishOnly`. **0.3.0 was published carrying
92
+ ruleset v2 sixty-four seconds before the extraction commit, v3 landed in the source 52 minutes
93
+ later, and nothing republished** — so `npm install @aimarket/warden@0.3.0` handed strangers a
94
+ scanner with no rules on the tool-name surface while the README in that same tarball documented v3.
95
+ Measured after the fact, that cost nothing on the 1 108-server corpus; the ambiguity in every
96
+ recorded verdict was the real defect.
97
+
98
+ ### Tests
99
+
100
+ - `test/field-survey-regression.test.ts` — 19 cases built from the survey's real text. Both
101
+ directions: the 46 false positives must not block, and the four real findings must.
102
+
5
103
  ## 0.3.0 — 2026-08-24
6
104
 
7
105
  First standalone release. The gates, the threat feed, the pinning store contract and the RFC 8785
8
106
  canonicalizer were extracted from [ARGUS](https://github.com/alexar76/argus) 0.3.0, where they had
9
107
  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.
108
+ them, not reset to 0.1.0: this is the same enforcement code, with the same behavioural tests that
109
+ guarded it there. The extraction itself changed no rule — it moved ruleset `v2` byte for byte — and
110
+ the security review below, run before this package was ever published, is what took the rules to
111
+ `v3` and closed six holes in the machinery around them.
13
112
 
14
113
  ### Why it moved
15
114
 
@@ -35,6 +134,59 @@ nothing but `node:crypto`.
35
134
  - No behavioural change to any gate. The rule table, the severities, the tiers, the score arithmetic
36
135
  and every finding code are the ones ARGUS 0.3.0 shipped.
37
136
 
137
+ ### Security review before first release
138
+
139
+ The extracted code was read end to end and the findings reproduced. Six were real; all six are fixed
140
+ here and each has a regression test in `test/hardening.test.ts` that fails on the code as extracted.
141
+
142
+ - **Denial of service through glob matching (critical).** Threat-feed patterns and
143
+ `policy.sensitiveToolPatterns` compiled `*` into `.*` and ran a regex. A 32-character pattern
144
+ against a 220-character haystack took **112 seconds**; the sensitive-tool path took **89 seconds**
145
+ from a long tool name. A signed feed record could therefore hang every connection check, which is
146
+ precisely what the feed's trust model says a publisher must not be able to do — "a compromised
147
+ publisher can only add protection" is false if one record stops the firewall answering. Replaced
148
+ with a linear two-pointer matcher (`src/glob.ts`): the same case now returns in about a
149
+ millisecond. Feed records are additionally refused past `MAX_GLOB_WILDCARDS` (12), and a feed is
150
+ refused past 2000 records.
151
+ - **Terminal injection through finding messages (high).** Tool names, server ids, catalog names and
152
+ a feed's `reason` string were interpolated raw into `finding.message`, which hosts print to a TTY
153
+ and store in receipts. A tool named `<ESC>[2K<ESC>[1A…` overwrote the BLOCK line WARDEN had just
154
+ written. Control characters and invisible characters are now escaped visibly (`src/sanitize.ts`,
155
+ exported as `displaySafe`) — escaped rather than stripped, because a name containing `U+202E`
156
+ should look suspicious in the report, not clean. `finding.tool` deliberately keeps the raw name:
157
+ it is the key a host filters its tool list with.
158
+ - **The tool NAME was scanned by nothing (high).** An injection phrase, a zero-width character or a
159
+ base64 blob in the first field the model reads produced zero findings. Ruleset **v3** gives every
160
+ rule a `surfaces` list and adds the name to 17 of the 25 — every phrase-keyed and hidden-payload
161
+ rule. The three noun-keyed codes stay off the name on purpose (`sign_with_private_key` is a
162
+ plausible tool, and refusing it would be the v1 calibration error on a new surface).
163
+ - **A gate that threw took the whole verdict with it (high).** `vet()` rejected, so the host got no
164
+ verdict at all — not even the findings the earlier gates had already produced — and a
165
+ full-disk pin store or a bug in a custom gate decided whether the connection was blocked. A throw
166
+ is now a `GATE_ERROR` finding at `high` with a zero score: a gate that crashed cleared nothing.
167
+ - **A frozen policy crashed the constructor (medium).** The `blockAtSeverity` typo fallback assigned
168
+ into the caller's own object, so `Object.freeze(policy)` — a reasonable thing for a host to do —
169
+ raised a `TypeError` out of `new Warden()` under ESM strict mode. The policy is now normalized
170
+ into a private copy and the caller's object is never touched.
171
+ - **Pinning covered the advertisement but not the program (medium).** Tool-def pinning caught a
172
+ server that changed what it advertises, not one that kept the advertisement and changed the
173
+ command behind it — which a remote **catalog** can do to an already-approved id. `PinnedServer`
174
+ gained an optional `identityHash` (transport, command, args, url, name) and a
175
+ `SERVER_IDENTITY_DRIFT` finding. `env` is excluded on purpose: it holds secrets, and the pin is
176
+ written to disk. Pins from before the field exists stay silent — absent is "not recorded", never
177
+ "changed".
178
+
179
+ Two smaller fixes came out of the same pass: a `feedPublicKey` that parses but is not an Ed25519 key
180
+ is now refused with a warning instead of failing inside `verify()` and being logged at `debug`
181
+ (indistinguishable from "the feed had nothing new"), and an oversized feed body is now streamed and
182
+ aborted at the cap instead of being buffered whole by `res.text()` — `content-length` is advisory,
183
+ so the old check was one missing header away from being no check at all.
184
+
185
+ Two things the review deliberately did **not** change: `EgressGuard` passed every case put to it
186
+ (userinfo tricks, `file:`/`data:` schemes, suffix confusion, case folding), and the scan's cost over
187
+ large tool definitions is linear (500 tools × 240 KB in 1.3 s), so no truncation was introduced for
188
+ a problem that does not exist.
189
+
38
190
  ### Added
39
191
 
40
192
  - `test/packaging.test.ts` — fails if a runtime dependency appears, if any source file imports outside
package/README-es.md CHANGED
@@ -1,18 +1,42 @@
1
- # WARDEN — cortafuegos de seguridad para MCP
1
+ # WARDEN — servidor MCP
2
+
3
+ <!-- mcp-name: io.github.alexar76/warden -->
2
4
 
3
5
  <!-- aicom-readme-badges -->
4
6
  <p align="center">
5
7
  <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>
8
+ <a href="https://glama.ai/mcp/servers/alexar76/warden"><img src="https://glama.ai/mcp/servers/alexar76/warden/badges/score.svg" alt="warden MCP server" /></a>
9
+ <a href="https://warden.modelmarket.dev/"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
10
  <img src="docs/badges/deps.svg" alt="Cero dependencias de ejecución" />
8
- <img src="docs/badges/tests.svg" alt="95 pruebas en verde" />
11
+ <img src="docs/badges/tests.svg" alt="166 pruebas en verde" />
9
12
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
13
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="Licencia: MIT" /></a>
11
14
  </p>
12
15
  <!-- /aicom-readme-badges -->
13
16
 
17
+ <p align="center">
18
+ <a href="https://warden.modelmarket.dev/">
19
+ <img src="docs/screenshots/readme/hero-3d.png" alt="WARDEN — cadena 3D de puertas: tools/list por static-scan, threat-feed, origin y pinning hasta un veredicto registrado" width="100%" />
20
+ </a>
21
+ </p>
22
+
23
+
14
24
  > 🌐 [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
25
 
26
+ **Un servidor MCP. Cortafuegos de las definiciones de herramienta anunciadas. Biblioteca incluida.**
27
+
28
+ Transporte: **stdio** (`npx -y @aimarket/warden` / `node dist/mcp-server.js`). Hosts compatibles:
29
+ Claude Desktop, Cursor, Glama y cualquier cliente MCP con stdio. Sin claves.
30
+
31
+ | | |
32
+ |------|----------|
33
+ | Entrada MCP (stdio) | `warden-mcp` → [`src/mcp-server.ts`](src/mcp-server.ts) |
34
+ | Herramientas | `vet_mcp_server`, `static_scan_tools`, `classify_sensitive_tools`, `check_egress_url`, `canonicalize_json`, `list_scan_rules` |
35
+ | Biblioteca | `import { Warden } from "@aimarket/warden"` |
36
+ | Glama / Docker (stdio) | [`Dockerfile`](Dockerfile), [`glama.json`](glama.json) |
37
+ | Official MCP Registry | [`server.json`](server.json) → `io.github.alexar76/warden` |
38
+ | Smithery | [`smithery.yaml`](smithery.yaml) |
39
+
16
40
  Un servidor MCP le dice a tu agente qué hacen sus herramientas. El agente se lo cree — y esa frase
17
41
  es la superficie de ataque. La descripción de una herramienta es texto de prompt que un tercero
18
42
  entrega directamente al contexto de tu modelo, y un campo de esquema llamado `api_key` es una
@@ -22,15 +46,56 @@ WARDEN examina un servidor **antes de que ninguna de sus herramientas llegue al
22
46
  un veredicto que puedes registrar: permitir/bloquear, una puntuación 0..1, los hallazgos que la
23
47
  produjeron, una partición por herramienta y la tabla de reglas exacta que estaba en vigor.
24
48
 
49
+ **Cero dependencias npm de ejecución.** El único import de la biblioteca es `node:crypto`. El
50
+ servidor MCP stdio añade otros builtins `node:` (`fs`, `path`, `process`) y sigue sin traer paquetes.
51
+ Es el cortafuegos de [ARGUS](https://github.com/alexar76/argus), extraído para que puedas ponerlo
52
+ delante de tu propio host MCP sin adoptar un agente.
53
+
54
+ ## Ejecutar como servidor MCP (stdio)
55
+
25
56
  ```bash
26
- npm install @aimarket/warden
57
+ npx -y @aimarket/warden
58
+ npm run build && node dist/mcp-server.js
59
+ ```
60
+
61
+ Claude Desktop / Cursor (`mcpServers`):
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "warden": {
67
+ "command": "npx",
68
+ "args": ["-y", "@aimarket/warden"]
69
+ }
70
+ }
71
+ }
27
72
  ```
28
73
 
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.
74
+ El proceso **no** arranca, no hace de proxy ni aísla otro servidor MCP: pasas un dump de
75
+ `tools/list` y recibes un veredicto.
76
+
77
+ | Herramienta | Cuándo usarla |
78
+ |---|---|
79
+ | `vet_mcp_server` | Cadena completa de puertas |
80
+ | `static_scan_tools` | Solo el escaneo estático |
81
+ | `classify_sensitive_tools` | Partición por globs del operador |
82
+ | `check_egress_url` | Allowlist de hosts (lista vacía = denegar todos) |
83
+ | `canonicalize_json` | Bytes RFC 8785 |
84
+ | `list_scan_rules` | Tabla de reglas publicada |
85
+
86
+ ### Publicar en Glama
32
87
 
33
- ## Inicio rápido
88
+ Listado: **[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)**
89
+
90
+ El mismo patrón que [ARGUS](https://github.com/alexar76/argus) y
91
+ [aimarket-mcp](https://github.com/alexar76/aimarket-mcp): [`glama.json`](glama.json) +
92
+ [`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`. Formulario: [`docs/GLAMA.md`](docs/GLAMA.md).
93
+
94
+ ## Biblioteca (incrustar en tu host)
95
+
96
+ ```bash
97
+ npm install @aimarket/warden
98
+ ```
34
99
 
35
100
  ```ts
36
101
  import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
@@ -77,7 +142,7 @@ flowchart LR
77
142
 
78
143
  | Puerta | Qué decide | Red | ¿Fatal? |
79
144
  |---|---|---|---|
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 |
145
+ | **static-scan** | Inyección, exfiltración, peticiones de credenciales y señales de Unicode oculto/base64 en el `name`, la `description` y el `inputSchema` de la herramienta — 25 reglas, v4, de las cuales 15 pueden bloquear y 10 son solo de aviso, 17 cubren también el nombre y 12 llevan un guard de contexto | ninguna | no |
81
146
  | **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
147
  | **origin** | Si el operador declaró este servidor o llegó desde un catálogo remoto | ninguna | sí, con `allowUnknownServers: false` |
83
148
  | **pinning** | Si las definiciones de herramientas siguen coincidiendo con lo que el usuario aprobó | ninguna | sí, con `pinToolDefs: true` |
@@ -99,7 +164,7 @@ endureciera el umbral.
99
164
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
100
165
  allowedTools: ["add"],
101
166
  blockedTools: ["sweeper"],
102
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
167
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
103
168
  }
104
169
  ```
105
170
 
@@ -153,6 +218,10 @@ degradar a ninguna protección:
153
218
  | [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
219
  | [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
220
  | [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 |
221
+ | [Estudio de campo: 1 108 servidores MCP públicos](docs/mcp-survey.es.md) | Qué decidió WARDEN sobre definiciones de herramienta reales de terceros — 50 servidores bloqueados, 4 sustentados, y las seis formas en que el resto se equivocó |
222
+ | [Glama / Docker](docs/GLAMA.md) | MCP stdio, health check, Build steps / CMD |
223
+ | [Security](SECURITY.md) | Cómo informar de un bypass del cortafuegos |
224
+ | [Contributing](CONTRIBUTING.md) | Regla de cero dependencias, PRs de la tabla de reglas |
156
225
 
157
226
  ## Lo que esto no es
158
227
 
@@ -167,11 +236,13 @@ degradar a ninguna protección:
167
236
  `test/no-phantom-gate.test.ts` falla si alguna puerta vuelve a declarar inalcanzabilidad.
168
237
  - **No sustituye a leer las definiciones de herramientas.** 11 registros integrados de amenazas son
169
238
  un suelo, no un catálogo.
239
+ - **No es un proxy.** La entrada MCP stdio inspecciona las definiciones que le pasas. No se conecta
240
+ al servidor bajo examen, no lo descarga ni lo ejecuta.
170
241
 
171
242
  ## Desarrollo
172
243
 
173
244
  ```bash
174
- npm install && npm run build && npm test # 95 pruebas
245
+ npm install && npm run build && npm test # 166 pruebas
175
246
  ```
176
247
 
177
248
  `test/packaging.test.ts` es lo que mantiene honesto el titular: falla si aparece una dependencia de
package/README-fr.md CHANGED
@@ -1,18 +1,42 @@
1
- # WARDEN — pare-feu de sécurité MCP
1
+ # WARDEN — serveur MCP
2
+
3
+ <!-- mcp-name: io.github.alexar76/warden -->
2
4
 
3
5
  <!-- aicom-readme-badges -->
4
6
  <p align="center">
5
7
  <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>
8
+ <a href="https://glama.ai/mcp/servers/alexar76/warden"><img src="https://glama.ai/mcp/servers/alexar76/warden/badges/score.svg" alt="warden MCP server" /></a>
9
+ <a href="https://warden.modelmarket.dev/"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="version npm" /></a>
7
10
  <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" />
11
+ <img src="docs/badges/tests.svg" alt="166 tests au vert" />
9
12
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
13
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="Licence : MIT" /></a>
11
14
  </p>
12
15
  <!-- /aicom-readme-badges -->
13
16
 
17
+ <p align="center">
18
+ <a href="https://warden.modelmarket.dev/">
19
+ <img src="docs/screenshots/readme/hero-3d.png" alt="WARDEN — chaîne 3D de portes : tools/list via static-scan, threat-feed, origin et pinning jusqu’à un verdict enregistré" width="100%" />
20
+ </a>
21
+ </p>
22
+
23
+
14
24
  > 🌐 [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
25
 
26
+ **Un serveur MCP. Pare-feu des définitions d'outils annoncées. Bibliothèque incluse.**
27
+
28
+ Transport : **stdio** (`npx -y @aimarket/warden` / `node dist/mcp-server.js`). Hôtes compatibles :
29
+ Claude Desktop, Cursor, Glama et tout client MCP en stdio. Aucune clé.
30
+
31
+ | | |
32
+ |------|----------|
33
+ | Entrée MCP (stdio) | `warden-mcp` → [`src/mcp-server.ts`](src/mcp-server.ts) |
34
+ | Outils | `vet_mcp_server`, `static_scan_tools`, `classify_sensitive_tools`, `check_egress_url`, `canonicalize_json`, `list_scan_rules` |
35
+ | Bibliothèque | `import { Warden } from "@aimarket/warden"` |
36
+ | Glama / Docker (stdio) | [`Dockerfile`](Dockerfile), [`glama.json`](glama.json) |
37
+ | Official MCP Registry | [`server.json`](server.json) → `io.github.alexar76/warden` |
38
+ | Smithery | [`smithery.yaml`](smithery.yaml) |
39
+
16
40
  Un serveur MCP indique lui-même à votre agent ce que font ses outils. L'agent le croit — et cette
17
41
  phrase est la surface d'attaque. La description d'un outil est du texte de prompt qu'un tiers livre
18
42
  directement dans le contexte de votre modèle, et un champ de schéma nommé `api_key` est une demande
@@ -22,15 +46,56 @@ WARDEN examine un serveur **avant qu'aucun de ses outils n'atteigne le modèle**
22
46
  que vous pouvez consigner : autoriser/bloquer, un score 0..1, les constats qui l'ont produit, une
23
47
  partition par outil et la table de règles exacte qui était en vigueur.
24
48
 
49
+ **Zéro dépendance npm d'exécution.** Le seul import de la bibliothèque est `node:crypto`. Le serveur
50
+ MCP stdio ajoute d'autres builtins `node:` (`fs`, `path`, `process`) et ne tire toujours aucun paquet.
51
+ C'est le pare-feu d'[ARGUS](https://github.com/alexar76/argus), extrait pour que vous puissiez le
52
+ placer devant votre propre hôte MCP sans adopter d'agent.
53
+
54
+ ## Lancer comme serveur MCP (stdio)
55
+
25
56
  ```bash
26
- npm install @aimarket/warden
57
+ npx -y @aimarket/warden
58
+ npm run build && node dist/mcp-server.js
59
+ ```
60
+
61
+ Claude Desktop / Cursor (`mcpServers`) :
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "warden": {
67
+ "command": "npx",
68
+ "args": ["-y", "@aimarket/warden"]
69
+ }
70
+ }
71
+ }
27
72
  ```
28
73
 
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.
74
+ Le processus ne démarre, ne proxifie ni n'isole un autre serveur MCP : vous passez un dump
75
+ `tools/list`, vous récupérez un verdict.
76
+
77
+ | Outil | Quand l'appeler |
78
+ |---|---|
79
+ | `vet_mcp_server` | Chaîne complète de portes |
80
+ | `static_scan_tools` | Scan statique uniquement |
81
+ | `classify_sensitive_tools` | Partition par globs opérateur |
82
+ | `check_egress_url` | Allowlist d'hôtes (liste vide = tout refuser) |
83
+ | `canonicalize_json` | Octets RFC 8785 |
84
+ | `list_scan_rules` | Table de règles publiée |
85
+
86
+ ### Publier sur Glama
32
87
 
33
- ## Démarrage rapide
88
+ Listing : **[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)**
89
+
90
+ Même modèle que [ARGUS](https://github.com/alexar76/argus) et
91
+ [aimarket-mcp](https://github.com/alexar76/aimarket-mcp) : [`glama.json`](glama.json) +
92
+ [`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`. Formulaire : [`docs/GLAMA.md`](docs/GLAMA.md).
93
+
94
+ ## Bibliothèque (embarquer dans votre hôte)
95
+
96
+ ```bash
97
+ npm install @aimarket/warden
98
+ ```
34
99
 
35
100
  ```ts
36
101
  import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
@@ -77,7 +142,7 @@ flowchart LR
77
142
 
78
143
  | Porte | Ce qu'elle décide | Réseau | Fatale ? |
79
144
  |---|---|---|---|
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 |
145
+ | **static-scan** | Injection, exfiltration, demandes d'identifiants et indices d'Unicode masqué/base64 dans le `name`, la `description` et l'`inputSchema` de l'outil — 25 règles, v4, dont 15 peuvent bloquer et 10 sont purement indicatives, 17 couvrent aussi le nom et 12 portent un guard de contexte | aucun | non |
81
146
  | **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
147
  | **origin** | Si l'opérateur a déclaré ce serveur ou s'il provient d'un catalogue distant | aucun | oui, avec `allowUnknownServers: false` |
83
148
  | **pinning** | Si les définitions d'outils correspondent encore à ce que l'utilisateur a approuvé | aucun | oui, avec `pinToolDefs: true` |
@@ -99,7 +164,7 @@ bloquante pour quiconque durcissait le seuil.
99
164
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
100
165
  allowedTools: ["add"],
101
166
  blockedTools: ["sweeper"],
102
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
167
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
103
168
  }
104
169
  ```
105
170
 
@@ -153,6 +218,10 @@ une absence de protection :
153
218
  | [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
219
  | [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
220
  | [Guide d'intégration](docs/integration.fr.md) | Brancher WARDEN sur votre propre hôte MCP, choix de politique, et quoi consigner |
221
+ | [Étude de terrain : 1 108 serveurs MCP publics](docs/mcp-survey.fr.md) | Ce que WARDEN a décidé sur de vraies définitions d'outil tierces — 50 serveurs bloqués, 4 étayés, et les six façons dont le reste était faux |
222
+ | [Glama / Docker](docs/GLAMA.md) | MCP stdio, health check, Build steps / CMD |
223
+ | [Security](SECURITY.md) | Signaler un contournement du pare-feu |
224
+ | [Contributing](CONTRIBUTING.md) | Règle zéro dépendance, PRs de table de règles |
156
225
 
157
226
  ## Ce que ce n'est pas
158
227
 
@@ -167,11 +236,13 @@ une absence de protection :
167
236
  `test/no-phantom-gate.test.ts` échoue si une porte déclare de nouveau une injoignabilité.
168
237
  - **Pas un substitut à la lecture des définitions d'outils.** 11 enregistrements de menaces intégrés
169
238
  sont un socle, pas un catalogue.
239
+ - **Pas un proxy.** L'entrée MCP stdio inspecte les définitions que vous lui passez. Elle ne se
240
+ connecte pas au serveur examiné, ne le télécharge pas et ne l'exécute pas.
170
241
 
171
242
  ## Développement
172
243
 
173
244
  ```bash
174
- npm install && npm run build && npm test # 95 tests
245
+ npm install && npm run build && npm test # 166 tests
175
246
  ```
176
247
 
177
248
  `test/packaging.test.ts` est ce qui tient l'accroche honnête : il échoue si une dépendance d'exécution
package/README-ru.md CHANGED
@@ -1,18 +1,42 @@
1
- # WARDEN — файрвол безопасности MCP
1
+ # WARDEN — MCP-сервер
2
+
3
+ <!-- mcp-name: io.github.alexar76/warden -->
2
4
 
3
5
  <!-- aicom-readme-badges -->
4
6
  <p align="center">
5
7
  <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>
8
+ <a href="https://glama.ai/mcp/servers/alexar76/warden"><img src="https://glama.ai/mcp/servers/alexar76/warden/badges/score.svg" alt="warden MCP server" /></a>
9
+ <a href="https://warden.modelmarket.dev/"><img src="https://img.shields.io/npm/v/@aimarket/warden?color=cb3837&label=npm" alt="npm version" /></a>
7
10
  <img src="docs/badges/deps.svg" alt="Zero runtime dependencies" />
8
- <img src="docs/badges/tests.svg" alt="95 tests passing" />
11
+ <img src="docs/badges/tests.svg" alt="166 tests passing" />
9
12
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
13
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="License: MIT" /></a>
11
14
  </p>
12
15
  <!-- /aicom-readme-badges -->
13
16
 
17
+ <p align="center">
18
+ <a href="https://warden.modelmarket.dev/">
19
+ <img src="docs/screenshots/readme/hero-3d.png" alt="WARDEN — 3D-цепочка гейтов: tools/list через static-scan, threat-feed, origin и pinning к записанному вердикту" width="100%" />
20
+ </a>
21
+ </p>
22
+
23
+
14
24
  > 🌐 [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
25
 
26
+ **Один MCP-сервер. Файрвол для объявленных определений инструментов. Библиотека в комплекте.**
27
+
28
+ Транспорт: **stdio** (`npx -y @aimarket/warden` / `node dist/mcp-server.js`). Совместимые хосты:
29
+ Claude Desktop, Cursor, Glama и любой MCP-клиент со stdio. Ключи не нужны.
30
+
31
+ | | |
32
+ |------|----------|
33
+ | Точка входа MCP (stdio) | `warden-mcp` → [`src/mcp-server.ts`](src/mcp-server.ts) |
34
+ | Инструменты | `vet_mcp_server`, `static_scan_tools`, `classify_sensitive_tools`, `check_egress_url`, `canonicalize_json`, `list_scan_rules` |
35
+ | Библиотека | `import { Warden } from "@aimarket/warden"` |
36
+ | Glama / Docker (stdio) | [`Dockerfile`](Dockerfile), [`glama.json`](glama.json) |
37
+ | Official MCP Registry | [`server.json`](server.json) → `io.github.alexar76/warden` |
38
+ | Smithery | [`smithery.yaml`](smithery.yaml) |
39
+
16
40
  MCP-сервер сам сообщает вашему агенту, что делают его инструменты. Агент этому верит — и вот эта
17
41
  фраза и есть поверхность атаки. Описание инструмента — это текст промпта, который третья сторона
18
42
  доставляет прямо в контекст модели, а поле схемы с именем `api_key` — это запрос ваших секретов,
@@ -22,15 +46,56 @@ WARDEN проверяет сервер **до того, как его инстр
22
46
  который можно записать: allow/block, оценку 0..1, найденные проблемы, разбиение по инструментам и
23
47
  точную таблицу правил, которая действовала в момент проверки.
24
48
 
49
+ **Ноль npm-зависимостей в рантайме.** В библиотеке единственный импорт — `node:crypto`. stdio
50
+ MCP-сервер добавляет другие `node:` builtins (`fs`, `path`, `process`) и по-прежнему не тянет пакеты.
51
+ Это файрвол из [ARGUS](https://github.com/alexar76/argus), вынесенный отдельно, чтобы его можно было
52
+ поставить перед своим MCP-хостом, не переезжая на агента.
53
+
54
+ ## Запуск как MCP-сервер (stdio)
55
+
25
56
  ```bash
26
- npm install @aimarket/warden
57
+ npx -y @aimarket/warden
58
+ npm run build && node dist/mcp-server.js
59
+ ```
60
+
61
+ Claude Desktop / Cursor (`mcpServers`):
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "warden": {
67
+ "command": "npx",
68
+ "args": ["-y", "@aimarket/warden"]
69
+ }
70
+ }
71
+ }
27
72
  ```
28
73
 
29
- **Ноль рантайм-зависимостей.** Единственный импорт во всём пакете — `node:crypto`. Это файрвол из
30
- [ARGUS](https://github.com/alexar76/argus), вынесенный отдельно, чтобы его можно было поставить
31
- перед своим MCP-хостом, не переезжая на агента.
74
+ Процесс **не** запускает, не проксирует и не изолирует чужой MCP-сервер: вы передаёте дамп
75
+ `tools/list`, получаете вердикт.
76
+
77
+ | Инструмент | Когда вызывать |
78
+ |---|---|
79
+ | `vet_mcp_server` | Полная цепочка гейтов |
80
+ | `static_scan_tools` | Только static-scan |
81
+ | `classify_sensitive_tools` | Glob-разбиение оператора |
82
+ | `check_egress_url` | Allowlist хостов (пустой список — отказ всем) |
83
+ | `canonicalize_json` | Байты RFC 8785 |
84
+ | `list_scan_rules` | Опубликованная таблица правил |
85
+
86
+ ### Публикация на Glama
32
87
 
33
- ## Быстрый старт
88
+ Листинг: **[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)**
89
+
90
+ Тот же шаблон, что у [ARGUS](https://github.com/alexar76/argus) и
91
+ [aimarket-mcp](https://github.com/alexar76/aimarket-mcp): [`glama.json`](glama.json) +
92
+ [`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`. Форма: [`docs/GLAMA.md`](docs/GLAMA.md).
93
+
94
+ ## Библиотека (встроить в свой хост)
95
+
96
+ ```bash
97
+ npm install @aimarket/warden
98
+ ```
34
99
 
35
100
  ```ts
36
101
  import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
@@ -77,7 +142,7 @@ flowchart LR
77
142
 
78
143
  | Гейт | Что решает | Сеть | Fatal? |
79
144
  |---|---|---|---|
80
- | **static-scan** | Инъекции, эксфильтрация, запросы учётных данных, скрытый Unicode и base64-признаки в `description` и `inputSchema` — 25 правил, версия 2, из них 18 могут блокировать и 7 только сообщают | нет | нет |
145
+ | **static-scan** | Инъекции, эксфильтрация, запросы учётных данных, скрытый Unicode и base64-признаки в `name`, `description` и `inputSchema` инструмента — 25 правил, версия 4, из них 15 могут блокировать и 10 только сообщают, 17 покрывают и имя, а у 12 есть контекстный guard | нет | нет |
81
146
  | **threat-feed** | Известный плохой сервер или инструмент: 11 встроенных записей плюс опциональный подписанный feed | только загрузка feed | да, для `critical` на уровне сервера |
82
147
  | **origin** | Объявил ли оператор этот сервер, или он пришёл из удалённого каталога | нет | да, при `allowUnknownServers: false` |
83
148
  | **pinning** | Совпадают ли определения инструментов с тем, что подтвердил пользователь | нет | да, при `pinToolDefs: true` |
@@ -98,7 +163,7 @@ severity снова делало находку блокирующей для в
98
163
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
99
164
  allowedTools: ["add"],
100
165
  blockedTools: ["sweeper"],
101
- rulesets: { staticScan: { version: "2", digest: "sha256-gWC14PR4…" } }
166
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
102
167
  }
103
168
  ```
104
169
 
@@ -152,6 +217,10 @@ GET <ваш feed url>
152
217
  | [Цепочка гейтов](docs/gates.ru.md) | Все уровни правил, все коды находок, как складывается composite score и как добавить свой гейт |
153
218
  | [Подписанный threat feed](docs/threat-feed.ru.md) | Контракт на проводе, три проверки и как публиковать feed, который WARDEN примет |
154
219
  | [Руководство по интеграции](docs/integration.ru.md) | Как встроить WARDEN в свой MCP-хост, выбор политики и что записывать |
220
+ | [Полевой обзор: 1 108 публичных MCP-серверов](docs/mcp-survey.ru.md) | Что WARDEN решил на настоящих чужих определениях инструментов — 50 серверов заблокировано, 4 подтверждено, и шесть способов, которыми остальные оказались ошибкой |
221
+ | [Glama / Docker](docs/GLAMA.md) | stdio MCP, health check, Build steps / CMD |
222
+ | [Security](SECURITY.md) | Как сообщать об обходе файрвола |
223
+ | [Contributing](CONTRIBUTING.md) | Правило нулевых зависимостей, PR на таблицу правил |
155
224
 
156
225
  ## Чем это не является
157
226
 
@@ -166,11 +235,13 @@ GET <ваш feed url>
166
235
  снова заявит о недоступности.
167
236
  - **Не замена чтению определений инструментов.** 11 встроенных записей об угрозах — это минимум, а не
168
237
  каталог.
238
+ - **Не прокси.** stdio MCP-вход смотрит на определения, которые вы ему передали. Он не
239
+ подключается к проверяемому серверу, не качает его и не исполняет.
169
240
 
170
241
  ## Разработка
171
242
 
172
243
  ```bash
173
- npm install && npm run build && npm test # 95 тестов
244
+ npm install && npm run build && npm test # 166 тестов
174
245
  ```
175
246
 
176
247
  `test/packaging.test.ts` — то, что удерживает заголовок честным: он падает, если появляется