@aimarket/warden 0.0.1 → 0.4.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 (53) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/LICENSE +21 -0
  3. package/README-es.md +190 -0
  4. package/README-fr.md +190 -0
  5. package/README-ru.md +189 -0
  6. package/README-zh.md +173 -0
  7. package/README.md +179 -6
  8. package/dist/glob.d.ts +81 -0
  9. package/dist/glob.d.ts.map +1 -0
  10. package/dist/glob.js +180 -0
  11. package/dist/glob.js.map +1 -0
  12. package/dist/index.d.ts +106 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +218 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/jcs.d.ts +86 -0
  17. package/dist/jcs.d.ts.map +1 -0
  18. package/dist/jcs.js +296 -0
  19. package/dist/jcs.js.map +1 -0
  20. package/dist/logger.d.ts +10 -0
  21. package/dist/logger.d.ts.map +1 -0
  22. package/dist/logger.js +19 -0
  23. package/dist/logger.js.map +1 -0
  24. package/dist/origin.d.ts +25 -0
  25. package/dist/origin.d.ts.map +1 -0
  26. package/dist/origin.js +49 -0
  27. package/dist/origin.js.map +1 -0
  28. package/dist/pinning.d.ts +91 -0
  29. package/dist/pinning.d.ts.map +1 -0
  30. package/dist/pinning.js +227 -0
  31. package/dist/pinning.js.map +1 -0
  32. package/dist/sandbox.d.ts +38 -0
  33. package/dist/sandbox.d.ts.map +1 -0
  34. package/dist/sandbox.js +79 -0
  35. package/dist/sandbox.js.map +1 -0
  36. package/dist/sanitize.d.ts +38 -0
  37. package/dist/sanitize.d.ts.map +1 -0
  38. package/dist/sanitize.js +53 -0
  39. package/dist/sanitize.js.map +1 -0
  40. package/dist/static-scan.d.ts +110 -0
  41. package/dist/static-scan.d.ts.map +1 -0
  42. package/dist/static-scan.js +539 -0
  43. package/dist/static-scan.js.map +1 -0
  44. package/dist/threat-feed.d.ts +138 -0
  45. package/dist/threat-feed.d.ts.map +1 -0
  46. package/dist/threat-feed.js +546 -0
  47. package/dist/threat-feed.js.map +1 -0
  48. package/dist/types.d.ts +173 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +11 -0
  51. package/dist/types.js.map +1 -0
  52. package/package.json +67 -9
  53. package/index.js +0 -6
package/CHANGELOG.md ADDED
@@ -0,0 +1,206 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@aimarket/warden`.
4
+
5
+ ## 0.4.0 — 2026-08-24
6
+
7
+ Calibrated against the ecosystem, not against fixtures. WARDEN was pointed at every public MCP
8
+ server it could legitimately reach — 1 108 of them answered a live `tools/list` with 17 491 tool
9
+ definitions — and the result is written up in [`docs/mcp-survey.md`](docs/mcp-survey.md).
10
+
11
+ Ruleset `v3` blocked 50 of those 1 108 servers. Four held up on review. The other 46 were blocked
12
+ for saying the right thing: *"Never send a private key"*, *"the private key never leaves your
13
+ machine"*, a security scanner listing the attacks it detects, a Persian tool description spelled with
14
+ the ZERO WIDTH NON-JOINER its language requires. A scanner with that false-positive profile does not
15
+ get tuned by its users; it gets uninstalled.
16
+
17
+ Ruleset **v4** brings blocking from 50 servers down to **6** on the same corpus, with all four real
18
+ findings still caught.
19
+
20
+ ### Rules read context now
21
+
22
+ Rules gained **guards** — named context checks that decide whether a match is the thing the rule is
23
+ looking for. A guard is part of the published rule table and therefore of the digest: the same regex
24
+ with and without `polarity` is a different ruleset, and a recorded verdict has to be able to say
25
+ which one it was.
26
+
27
+ - **`polarity`** — a credential noun inside a refusal is a promise, not a request. This one
28
+ distinction accounted for 390 of the 492 blocking findings in the survey.
29
+ - **`mention`** — a phrase in quotes, in backticks, or as a bare JSON `enum` value is cited, not said.
30
+ - **`detection`** — a secret named as the object of `detect`/`scan`/`find`/`leaked` is what a scanner
31
+ looks for, not what it wants.
32
+ - **`identifierFragment`** — `mnemonic` inside `bip39-mnemonic-checksum` is that identifier's name.
33
+ - **`harvestTarget`** — a harvest instruction says *whose* secret or *where* it lives. Without it,
34
+ "Obtain a permanent anonymous API key" read as theft rather than issuance.
35
+ - **`uri`** — `javascript:` is now matched case-**sensitively** and needs a payload. Under `/i` it
36
+ matched the word *JavaScript* followed by a colon, i.e. every language list ever written.
37
+ - **`payload`** — a `data:…;base64,` URI with fewer than 32 characters behind the comma is the format
38
+ being documented.
39
+ - **`blob`** — `/` is in the base64 alphabet, so a JSON Schema `$ref` pointer read as a hidden blob.
40
+ Now gated on entropy and on schema keywords.
41
+ - **`zeroWidth`** — U+200C/U+200D adjacent to Arabic, Persian or Indic script is orthography. U+200B,
42
+ U+FEFF and the bidi overrides still block.
43
+ - **`publicKeyPath`** — `authorized_keys`, `known_hosts` and `*.pub` are public by definition.
44
+
45
+ ### Rules demoted from block to advise
46
+
47
+ Each of these was measured selecting for honest servers. They are still reported.
48
+
49
+ - **`exfiltrat*`** as a bare noun. An attacker does not name the attack; a defender names it in every
50
+ sentence. All three hits were defensive tools. The anchored *"send X to <external destination>"*
51
+ rules keep the blocking weight.
52
+ - **`system prompt` / `developer message`**. 15 findings across 6 servers, every one an LLM proxy,
53
+ persona manager or agent-configuration tool that declares a `system` parameter because setting a
54
+ system prompt is its job.
55
+ - **`do not tell the user`**. Four real uses, four of them the opposite of concealment — *"no refund
56
+ is issued automatically … do not tell the user a refund is coming"*. A blocking rule needs a
57
+ concealment target that refers to the tool's own action; the bare phrase carries none.
58
+ - The **credential nouns** keep blocking but drop from `critical` to `high`: still over the default
59
+ threshold, no longer zeroing the gate score. One noun in a schema template shared by 377 tools
60
+ should not read as "maximally compromised".
61
+
62
+ ### Threat feed
63
+
64
+ - Wildcard threat patterns now use `threatMatch`: **interior gaps are bounded** to 24 characters and
65
+ a segment starting with a letter must **start on a word boundary**. `*sweep*funds*` was matching
66
+ `funds` inside "re**funds**", and `*drain*wallet*` was joining two words from different clauses.
67
+ Leading and trailing `*` stay unbounded, and `_`/`-` count as boundaries so a `seed_phrase` schema
68
+ field still matches. `policy.sensitiveToolPatterns` keeps plain glob semantics — that is the
69
+ operator's own pattern against their own tool names.
70
+
71
+ ### Findings say what they matched
72
+
73
+ - Every finding message now quotes the matched text: `… signature (\b(?:read|extract|…) at "obtain
74
+ the redacted credential"`. Without it a reviewer cannot tell which alternative of an alternation
75
+ fired, or on what. Recovering that by hand is most of the work of judging a finding, and it cost
76
+ hours in the survey itself.
77
+ - Dropped matches are logged at `debug` with the guard's reason, because a guard silently discarding a
78
+ finding is the one behaviour in this gate that a verdict cannot show.
79
+
80
+ ### Release gate
81
+
82
+ - `npm run check:ruleset` fails if the version in `package.json` is already on the registry with a
83
+ different ruleset ref, and runs in CI and in `prepublishOnly`. **0.3.0 was published carrying
84
+ ruleset v2 sixty-four seconds before the extraction commit, v3 landed in the source 52 minutes
85
+ later, and nothing republished** — so `npm install @aimarket/warden@0.3.0` handed strangers a
86
+ scanner with no rules on the tool-name surface while the README in that same tarball documented v3.
87
+ Measured after the fact, that cost nothing on the 1 108-server corpus; the ambiguity in every
88
+ recorded verdict was the real defect.
89
+
90
+ ### Tests
91
+
92
+ - `test/field-survey-regression.test.ts` — 19 cases built from the survey's real text. Both
93
+ directions: the 46 false positives must not block, and the four real findings must.
94
+
95
+ ## 0.3.0 — 2026-08-24
96
+
97
+ First standalone release. The gates, the threat feed, the pinning store contract and the RFC 8785
98
+ canonicalizer were extracted from [ARGUS](https://github.com/alexar76/argus) 0.3.0, where they had
99
+ lived as `src/warden/`. The version number is deliberately continuous with the agent that shipped
100
+ them, not reset to 0.1.0: this is the same enforcement code, with the same behavioural tests that
101
+ guarded it there. The extraction itself changed no rule — it moved ruleset `v2` byte for byte — and
102
+ the security review below, run before this package was ever published, is what took the rules to
103
+ `v3` and closed six holes in the machinery around them.
104
+
105
+ ### Why it moved
106
+
107
+ A host that wants an MCP firewall had to install an agent — with an MCP SDK, a wallet library and a
108
+ post-quantum keystore in tow — to get one. This package has **zero runtime dependencies** and imports
109
+ nothing but `node:crypto`.
110
+
111
+ ### Changed for standalone use
112
+
113
+ - **Host seams are narrowed and named.** Pinning needs `PinStore` (`getPin`/`putPin`) instead of
114
+ ARGUS's full `MemoryStore`, and logging needs `WardenLogger` instead of ARGUS's `Logger`. Both are
115
+ structural, so an existing host store/logger usually satisfies them unchanged.
116
+ - **`log` is now optional** on `WardenInit` and `WardenCreateDeps`, defaulting to the new
117
+ `silentLogger()`. A host with no logger of its own still gets enforcement — verdicts are returned as
118
+ data from `vet()`, never inferred from log output.
119
+ - **`threatFeed` stays required.** Constructing one silently would hide from the host that no external
120
+ intel is in play.
121
+ - **Subpath export `@aimarket/warden/jcs`** so another implementation can be byte-checked against the
122
+ canonicalizer without pulling in the gate chain.
123
+ - **Types are exported from the entry point** (`WardenPolicy`, `WardenVerdict`, `WardenFinding`,
124
+ `ThreatRecord`, `ToolDef`, …). ARGUS now re-exports them from here rather than declaring its own
125
+ copies, so a change on this side breaks its build instead of drifting silently.
126
+ - No behavioural change to any gate. The rule table, the severities, the tiers, the score arithmetic
127
+ and every finding code are the ones ARGUS 0.3.0 shipped.
128
+
129
+ ### Security review before first release
130
+
131
+ The extracted code was read end to end and the findings reproduced. Six were real; all six are fixed
132
+ here and each has a regression test in `test/hardening.test.ts` that fails on the code as extracted.
133
+
134
+ - **Denial of service through glob matching (critical).** Threat-feed patterns and
135
+ `policy.sensitiveToolPatterns` compiled `*` into `.*` and ran a regex. A 32-character pattern
136
+ against a 220-character haystack took **112 seconds**; the sensitive-tool path took **89 seconds**
137
+ from a long tool name. A signed feed record could therefore hang every connection check, which is
138
+ precisely what the feed's trust model says a publisher must not be able to do — "a compromised
139
+ publisher can only add protection" is false if one record stops the firewall answering. Replaced
140
+ with a linear two-pointer matcher (`src/glob.ts`): the same case now returns in about a
141
+ millisecond. Feed records are additionally refused past `MAX_GLOB_WILDCARDS` (12), and a feed is
142
+ refused past 2000 records.
143
+ - **Terminal injection through finding messages (high).** Tool names, server ids, catalog names and
144
+ a feed's `reason` string were interpolated raw into `finding.message`, which hosts print to a TTY
145
+ and store in receipts. A tool named `<ESC>[2K<ESC>[1A…` overwrote the BLOCK line WARDEN had just
146
+ written. Control characters and invisible characters are now escaped visibly (`src/sanitize.ts`,
147
+ exported as `displaySafe`) — escaped rather than stripped, because a name containing `U+202E`
148
+ should look suspicious in the report, not clean. `finding.tool` deliberately keeps the raw name:
149
+ it is the key a host filters its tool list with.
150
+ - **The tool NAME was scanned by nothing (high).** An injection phrase, a zero-width character or a
151
+ base64 blob in the first field the model reads produced zero findings. Ruleset **v3** gives every
152
+ rule a `surfaces` list and adds the name to 17 of the 25 — every phrase-keyed and hidden-payload
153
+ rule. The three noun-keyed codes stay off the name on purpose (`sign_with_private_key` is a
154
+ plausible tool, and refusing it would be the v1 calibration error on a new surface).
155
+ - **A gate that threw took the whole verdict with it (high).** `vet()` rejected, so the host got no
156
+ verdict at all — not even the findings the earlier gates had already produced — and a
157
+ full-disk pin store or a bug in a custom gate decided whether the connection was blocked. A throw
158
+ is now a `GATE_ERROR` finding at `high` with a zero score: a gate that crashed cleared nothing.
159
+ - **A frozen policy crashed the constructor (medium).** The `blockAtSeverity` typo fallback assigned
160
+ into the caller's own object, so `Object.freeze(policy)` — a reasonable thing for a host to do —
161
+ raised a `TypeError` out of `new Warden()` under ESM strict mode. The policy is now normalized
162
+ into a private copy and the caller's object is never touched.
163
+ - **Pinning covered the advertisement but not the program (medium).** Tool-def pinning caught a
164
+ server that changed what it advertises, not one that kept the advertisement and changed the
165
+ command behind it — which a remote **catalog** can do to an already-approved id. `PinnedServer`
166
+ gained an optional `identityHash` (transport, command, args, url, name) and a
167
+ `SERVER_IDENTITY_DRIFT` finding. `env` is excluded on purpose: it holds secrets, and the pin is
168
+ written to disk. Pins from before the field exists stay silent — absent is "not recorded", never
169
+ "changed".
170
+
171
+ Two smaller fixes came out of the same pass: a `feedPublicKey` that parses but is not an Ed25519 key
172
+ is now refused with a warning instead of failing inside `verify()` and being logged at `debug`
173
+ (indistinguishable from "the feed had nothing new"), and an oversized feed body is now streamed and
174
+ aborted at the cap instead of being buffered whole by `res.text()` — `content-length` is advisory,
175
+ so the old check was one missing header away from being no check at all.
176
+
177
+ Two things the review deliberately did **not** change: `EgressGuard` passed every case put to it
178
+ (userinfo tricks, `file:`/`data:` schemes, suffix confusion, case folding), and the scan's cost over
179
+ large tool definitions is linear (500 tools × 240 KB in 1.3 s), so no truncation was introduced for
180
+ a problem that does not exist.
181
+
182
+ ### Added
183
+
184
+ - `test/packaging.test.ts` — fails if a runtime dependency appears, if any source file imports outside
185
+ the package, or if the entry point stops exporting the enforcement surface. The zero-dependency claim
186
+ is the reason this package exists, so it is a test and not a sentence in a README.
187
+ - `test/docs.test.ts` — resolves every relative link across the five-language doc set and holds the
188
+ quoted test count to what the runner actually reports.
189
+ - `test/no-phantom-gate.test.ts` — the gate-chain half of ARGUS's regression guard for a removed
190
+ reputation gate: vetting opens no socket, and no gate may report a service as unreachable without
191
+ having sent a request. (The oracle-side half stayed with the oracle client, in ARGUS.)
192
+ - Documentation in English, Russian, Spanish, French and Chinese: the gate chain, the signed threat-feed
193
+ contract, and an integration guide.
194
+
195
+ ### Upgrading from ARGUS internals
196
+
197
+ If you imported the gates through ARGUS's source tree, the mapping is mechanical:
198
+
199
+ | Before | After |
200
+ |---|---|
201
+ | `src/warden/index.js` | `@aimarket/warden` |
202
+ | `src/warden/sandbox.js` | `@aimarket/warden` (`EgressGuard`, `isSensitiveTool`, `classifyTools`) |
203
+ | `src/warden/pinning.js` | `@aimarket/warden` (`PinningGate`, `canonicalToolsHash`, …) |
204
+ | `src/warden/jcs.js` | `@aimarket/warden/jcs` or the root export |
205
+ | `MemoryStore` (for pins) | `PinStore` |
206
+ | `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,190 @@
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="141 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
+ <p align="center">
15
+ <img src="docs/assets/hero.svg" alt="WARDEN — MCP security firewall: tool defs pass static-scan, threat-feed, origin, and pinning before a recorded verdict" width="920" />
16
+ </p>
17
+
18
+
19
+ > 🌐 [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)
20
+
21
+ Un servidor MCP le dice a tu agente qué hacen sus herramientas. El agente se lo cree — y esa frase
22
+ es la superficie de ataque. La descripción de una herramienta es texto de prompt que un tercero
23
+ entrega directamente al contexto de tu modelo, y un campo de esquema llamado `api_key` es una
24
+ petición de tus secretos redactada como si fuera una API.
25
+
26
+ WARDEN examina un servidor **antes de que ninguna de sus herramientas llegue al modelo**, y devuelve
27
+ un veredicto que puedes registrar: permitir/bloquear, una puntuación 0..1, los hallazgos que la
28
+ produjeron, una partición por herramienta y la tabla de reglas exacta que estaba en vigor.
29
+
30
+ ```bash
31
+ npm install @aimarket/warden
32
+ ```
33
+
34
+ **Cero dependencias de ejecución.** El único import de todo el paquete es `node:crypto`. Es el
35
+ cortafuegos de [ARGUS](https://github.com/alexar76/argus), extraído para que puedas ponerlo delante
36
+ de tu propio host MCP sin adoptar un agente.
37
+
38
+ ## Inicio rápido
39
+
40
+ ```ts
41
+ import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
42
+
43
+ const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
44
+ await threatFeed.load(process.env.FEED_URL); // sin URL → solo la lista de denegación integrada, sin red
45
+
46
+ const pins = new Map();
47
+ const warden = Warden.create({
48
+ policy: {
49
+ blockAtSeverity: "high",
50
+ sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
51
+ allowUnknownServers: false, // fail-closed: solo los servidores que declaraste
52
+ pinToolDefs: true,
53
+ },
54
+ threatFeed,
55
+ store: {
56
+ getPin: async (id) => pins.get(id),
57
+ putPin: async (p) => void pins.set(p.serverId, p),
58
+ },
59
+ log: silentLogger(), // o tu propio logger
60
+ });
61
+
62
+ const verdict = await warden.vet(server, await client.listTools());
63
+
64
+ if (!verdict.allow) throw new Error(`bloqueado por ${verdict.decidedBy}`);
65
+ const usable = verdict.allowedTools; // una herramienta envenenada puede quedar aislada sola
66
+ await warden.approve(server, tools); // fija (pin) lo que el usuario aceptó
67
+ ```
68
+
69
+ `vet()` **no hace ninguna petición de red**. La única petición que WARDEN llega a hacer es la
70
+ descarga del threat feed que tú pediste al pasar una URL a `load()`.
71
+
72
+ ## La cadena de puertas
73
+
74
+ ```mermaid
75
+ flowchart LR
76
+ T["definiciones<br/>de herramientas"] --> S["static scan<br/>25 reglas"]
77
+ S --> F["threat feed<br/>11 integradas + firmado"]
78
+ F --> O["origin<br/>declarado vs catálogo"]
79
+ O --> P["pinning<br/>deriva vs aprobación"]
80
+ P --> V["veredicto<br/>allow · score · findings<br/>allowedTools / blockedTools"]
81
+ ```
82
+
83
+ | Puerta | Qué decide | Red | ¿Fatal? |
84
+ |---|---|---|---|
85
+ | **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 |
86
+ | **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 |
87
+ | **origin** | Si el operador declaró este servidor o llegó desde un catálogo remoto | ninguna | sí, con `allowUnknownServers: false` |
88
+ | **pinning** | Si las definiciones de herramientas siguen coincidiendo con lo que el usuario aprobó | ninguna | sí, con `pinToolDefs: true` |
89
+
90
+ La puntuación compuesta es el **producto** de las contribuciones de cada puerta: una puerta mala
91
+ arrastra al servidor entero en lugar de promediarse. Severidad y bloqueo son ejes distintos: un
92
+ hallazgo `advisory` se reporta y nunca bloquea ni cuesta una herramienta, con cualquier
93
+ `blockAtSeverity` — porque «cuánta atención merece esto» y «¿es esto un defecto?» son preguntas
94
+ distintas, y codificar la segunda como una severidad baja volvía a hacerla bloqueante para quien
95
+ endureciera el umbral.
96
+
97
+ ## El veredicto está pensado para quedar registrado
98
+
99
+ ```ts
100
+ {
101
+ allow: false,
102
+ score: 0,
103
+ decidedBy: "threat-feed",
104
+ findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
105
+ allowedTools: ["add"],
106
+ blockedTools: ["sweeper"],
107
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
108
+ }
109
+ ```
110
+
111
+ `rulesets` no es decoración. El mismo servidor puntúa distinto bajo una tabla de reglas posterior, y
112
+ sin la versión *y* un digest sobre las reglas no hay forma de distinguir eso de que el servidor haya
113
+ cambiado. Un escaneo guardado sin ellos no es reproducible.
114
+
115
+ ## Threat feed firmado
116
+
117
+ WARDEN no leerá un feed remoto sin firma. El contrato es deliberadamente aburrido:
118
+
119
+ ```
120
+ GET <la url de tu feed>
121
+ { "records": [ {pattern, severity, code, reason, source, scope}, … ],
122
+ "timestamp": 1786205907380, // epoch ms, entero — obligatorio
123
+ "signature": "f588d5a4…" // Ed25519 (hex) sobre la forma canónica
124
+ } // RFC 8785 de {records, timestamp}
125
+ ```
126
+
127
+ Se comprueban tres propiedades, y **cualquier fallo conserva el suelo integrado** en lugar de
128
+ degradar a ninguna protección:
129
+
130
+ 1. **autenticidad** — Ed25519 contra la clave que fijaste de antemano (`feedPublicKey`);
131
+ 2. **frescura** — el timestamp *firmado* debe caer dentro de `maxAgeMs` (24 h por defecto), para que
132
+ quien sirve la URL no pueda reproducir una instantánea de hace meses y borrar en silencio cada
133
+ registro añadido desde entonces. Una firma dice quién escribió un documento, nunca cuándo te lo
134
+ entregaron;
135
+ 3. **determinismo** — bytes canónicos RFC 8785, para que editor y verificador coincidan sin importar
136
+ el orden de las claves JSON.
137
+
138
+ [MOMUS](https://github.com/alexar76/momus) es un editor de referencia de este contrato
139
+ (`/warden/threat-feed`) si necesitas algo a lo que apuntar con `load()`.
140
+
141
+ ## También incluido
142
+
143
+ - **`EgressGuard`** — una lista de permitidos de salida con la que envolver cualquier petición que
144
+ haga una herramienta. Una herramienta que alcanza un host que nunca listaste es la señal clásica de
145
+ phone-home. `*.example.com` cubre subdominios; una lista vacía lo bloquea todo, no lo permite todo.
146
+ - **`isSensitiveTool` / `classifyTools`** — clasificación por glob de las herramientas que deben
147
+ exigir aprobación en cada llamada. Las herramientas sensibles siguen *anunciadas*: simplemente no
148
+ pueden ejecutarse sin supervisión.
149
+ - **`canonicalize` / `parseJsonStrict`** — una implementación estricta de RFC 8785 (JCS), exportada
150
+ también como `@aimarket/warden/jcs` para poder comparar otra implementación byte a byte. Solo
151
+ enteros más allá de `MAX_SAFE_JSON_INTEGER`, rechazo (no escapado) de surrogados solitarios y un
152
+ código de motivo en cada rechazo.
153
+
154
+ ## Documentación
155
+
156
+ | | |
157
+ |---|---|
158
+ | [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 |
159
+ | [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 |
160
+ | [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 |
161
+ | [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ó |
162
+
163
+ ## Lo que esto no es
164
+
165
+ - **No es un sandbox.** Son decisiones JS dentro del proceso. Aquí no hay confinamiento del proceso
166
+ hijo MCP a nivel de sistema operativo (seccomp/Landlock, `sandbox-exec`).
167
+ - **No es un modelo.** En ninguna parte de la cadena se llama a un LLM. Por eso `vet()` es rápido,
168
+ offline y determinista — y por eso el escaneo estático tiene forma de regex y se le escapará una
169
+ paráfrasis que ninguna regla cubra.
170
+ - **No es un servicio de reputación.** Una versión anterior tenía una puerta que pedía a un oráculo
171
+ de confianza una puntuación para la que no tenía datos, y luego informaba de que el oráculo era
172
+ inalcanzable sin haber enviado ninguna petición. Se eliminó, y
173
+ `test/no-phantom-gate.test.ts` falla si alguna puerta vuelve a declarar inalcanzabilidad.
174
+ - **No sustituye a leer las definiciones de herramientas.** 11 registros integrados de amenazas son
175
+ un suelo, no un catálogo.
176
+
177
+ ## Desarrollo
178
+
179
+ ```bash
180
+ npm install && npm run build && npm test # 141 pruebas
181
+ ```
182
+
183
+ `test/packaging.test.ts` es lo que mantiene honesto el titular: falla si aparece una dependencia de
184
+ ejecución, si algún fichero fuente importa fuera del paquete o si el punto de entrada deja de
185
+ exportar la superficie de aplicación.
186
+
187
+ Usado por [ARGUS](https://github.com/alexar76/argus) (el host de referencia),
188
+ [MOMUS](https://github.com/alexar76/momus) (el lado editor) y el curso de seguridad MCP de AICOM.
189
+
190
+ MIT © AICOM (alexar76)
package/README-fr.md ADDED
@@ -0,0 +1,190 @@
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="141 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
+ <p align="center">
15
+ <img src="docs/assets/hero.svg" alt="WARDEN — MCP security firewall: tool defs pass static-scan, threat-feed, origin, and pinning before a recorded verdict" width="920" />
16
+ </p>
17
+
18
+
19
+ > 🌐 [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)
20
+
21
+ Un serveur MCP indique lui-même à votre agent ce que font ses outils. L'agent le croit — et cette
22
+ phrase est la surface d'attaque. La description d'un outil est du texte de prompt qu'un tiers livre
23
+ directement dans le contexte de votre modèle, et un champ de schéma nommé `api_key` est une demande
24
+ de vos secrets formulée comme une API.
25
+
26
+ WARDEN examine un serveur **avant qu'aucun de ses outils n'atteigne le modèle**, et renvoie un verdict
27
+ que vous pouvez consigner : autoriser/bloquer, un score 0..1, les constats qui l'ont produit, une
28
+ partition par outil et la table de règles exacte qui était en vigueur.
29
+
30
+ ```bash
31
+ npm install @aimarket/warden
32
+ ```
33
+
34
+ **Zéro dépendance d'exécution.** Le seul import de tout le paquet est `node:crypto`. C'est le pare-feu
35
+ d'[ARGUS](https://github.com/alexar76/argus), extrait pour que vous puissiez le placer devant votre
36
+ propre hôte MCP sans adopter d'agent.
37
+
38
+ ## Démarrage rapide
39
+
40
+ ```ts
41
+ import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
42
+
43
+ const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY });
44
+ await threatFeed.load(process.env.FEED_URL); // sans URL → uniquement la liste de refus intégrée, sans réseau
45
+
46
+ const pins = new Map();
47
+ const warden = Warden.create({
48
+ policy: {
49
+ blockAtSeverity: "high",
50
+ sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"],
51
+ allowUnknownServers: false, // fail-closed : uniquement les serveurs que vous avez déclarés
52
+ pinToolDefs: true,
53
+ },
54
+ threatFeed,
55
+ store: {
56
+ getPin: async (id) => pins.get(id),
57
+ putPin: async (p) => void pins.set(p.serverId, p),
58
+ },
59
+ log: silentLogger(), // ou votre propre logger
60
+ });
61
+
62
+ const verdict = await warden.vet(server, await client.listTools());
63
+
64
+ if (!verdict.allow) throw new Error(`bloqué par ${verdict.decidedBy}`);
65
+ const usable = verdict.allowedTools; // un outil empoisonné peut être isolé seul
66
+ await warden.approve(server, tools); // épingler (pin) ce que l'utilisateur a accepté
67
+ ```
68
+
69
+ `vet()` **n'effectue aucune requête réseau**. La seule requête que WARDEN émet jamais est le
70
+ téléchargement du threat feed que vous avez demandé en passant une URL à `load()`.
71
+
72
+ ## La chaîne de portes
73
+
74
+ ```mermaid
75
+ flowchart LR
76
+ T["définitions<br/>des outils"] --> S["static scan<br/>25 règles"]
77
+ S --> F["threat feed<br/>11 intégrées + signé"]
78
+ F --> O["origin<br/>déclaré vs catalogue"]
79
+ O --> P["pinning<br/>dérive vs approbation"]
80
+ P --> V["verdict<br/>allow · score · findings<br/>allowedTools / blockedTools"]
81
+ ```
82
+
83
+ | Porte | Ce qu'elle décide | Réseau | Fatale ? |
84
+ |---|---|---|---|
85
+ | **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 |
86
+ | **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 |
87
+ | **origin** | Si l'opérateur a déclaré ce serveur ou s'il provient d'un catalogue distant | aucun | oui, avec `allowUnknownServers: false` |
88
+ | **pinning** | Si les définitions d'outils correspondent encore à ce que l'utilisateur a approuvé | aucun | oui, avec `pinToolDefs: true` |
89
+
90
+ Le score composite est le **produit** des contributions de chaque porte : une seule porte mauvaise
91
+ entraîne tout le serveur vers le bas au lieu d'être moyennée. Sévérité et blocage sont deux axes
92
+ distincts : un constat `advisory` est rapporté et ne bloque jamais et ne coûte jamais un outil, quel
93
+ que soit `blockAtSeverity` — parce que « quelle attention cela mérite-t-il » et « est-ce un défaut »
94
+ sont deux questions différentes, et encoder la seconde par une sévérité basse la rendait de nouveau
95
+ bloquante pour quiconque durcissait le seuil.
96
+
97
+ ## Le verdict est fait pour être consigné
98
+
99
+ ```ts
100
+ {
101
+ allow: false,
102
+ score: 0,
103
+ decidedBy: "threat-feed",
104
+ findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
105
+ allowedTools: ["add"],
106
+ blockedTools: ["sweeper"],
107
+ rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
108
+ }
109
+ ```
110
+
111
+ `rulesets` n'est pas un ornement. Le même serveur obtient un autre score sous une table de règles
112
+ plus récente, et sans la version *et* une empreinte des règles, rien ne permet de distinguer cela
113
+ d'un serveur qui a changé. Un scan conservé sans elles n'est pas reproductible.
114
+
115
+ ## Threat feed signé
116
+
117
+ WARDEN ne lira pas un feed distant non signé. Le contrat est délibérément ennuyeux :
118
+
119
+ ```
120
+ GET <l'url de votre feed>
121
+ { "records": [ {pattern, severity, code, reason, source, scope}, … ],
122
+ "timestamp": 1786205907380, // epoch ms, entier — obligatoire
123
+ "signature": "f588d5a4…" // Ed25519 (hex) sur la forme canonique
124
+ } // RFC 8785 de {records, timestamp}
125
+ ```
126
+
127
+ Trois propriétés sont vérifiées, et **tout échec conserve le socle intégré** au lieu de dégrader vers
128
+ une absence de protection :
129
+
130
+ 1. **authenticité** — Ed25519 contre la clé que vous avez épinglée à l'avance (`feedPublicKey`) ;
131
+ 2. **fraîcheur** — le timestamp *signé* doit tomber dans `maxAgeMs` (24 h par défaut), afin que celui
132
+ qui sert l'URL ne puisse pas rejouer un instantané vieux de plusieurs mois et effacer en silence
133
+ chaque enregistrement ajouté depuis. Une signature dit qui a écrit un document, jamais quand il
134
+ vous a été remis ;
135
+ 3. **déterminisme** — octets canoniques RFC 8785, pour que l'éditeur et le vérificateur s'accordent
136
+ quel que soit l'ordre des clés JSON.
137
+
138
+ [MOMUS](https://github.com/alexar76/momus) est un éditeur de référence de ce contrat
139
+ (`/warden/threat-feed`) si vous cherchez une cible pour `load()`.
140
+
141
+ ## Également inclus
142
+
143
+ - **`EgressGuard`** — une liste blanche de sortie pour envelopper toute requête émise par un outil.
144
+ Un outil qui joint un hôte que vous n'avez jamais listé est le signe classique du phone-home.
145
+ `*.example.com` couvre les sous-domaines ; une liste vide bloque tout, au lieu de tout autoriser.
146
+ - **`isSensitiveTool` / `classifyTools`** — classification par glob des outils qui doivent exiger une
147
+ approbation à chaque appel. Les outils sensibles restent *annoncés* : ils ne peuvent simplement pas
148
+ s'exécuter sans surveillance.
149
+ - **`canonicalize` / `parseJsonStrict`** — une implémentation stricte de RFC 8785 (JCS), également
150
+ exportée sous `@aimarket/warden/jcs` afin qu'une autre implémentation puisse être vérifiée octet par
151
+ octet. Entiers uniquement au-delà de `MAX_SAFE_JSON_INTEGER`, refus (et non échappement) des
152
+ surrogates isolés, et un code de motif sur chaque refus.
153
+
154
+ ## Documentation
155
+
156
+ | | |
157
+ |---|---|
158
+ | [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 |
159
+ | [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 |
160
+ | [Guide d'intégration](docs/integration.fr.md) | Brancher WARDEN sur votre propre hôte MCP, choix de politique, et quoi consigner |
161
+ | [É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 |
162
+
163
+ ## Ce que ce n'est pas
164
+
165
+ - **Pas un sandbox.** Ce sont des décisions JS intra-processus. Le confinement du processus enfant MCP
166
+ au niveau du système d'exploitation (seccomp/Landlock, `sandbox-exec`) n'est pas ici.
167
+ - **Pas un modèle.** Aucun LLM n'est appelé dans la chaîne. C'est pourquoi `vet()` est rapide, hors
168
+ ligne et déterministe — et pourquoi le scan statique a la forme d'expressions régulières et laissera
169
+ passer une paraphrase qu'aucune règle ne couvre.
170
+ - **Pas un service de réputation.** Une version antérieure avait une porte qui demandait à un oracle
171
+ de confiance un score qu'il n'avait aucune donnée pour calculer, puis signalait l'oracle comme
172
+ injoignable sans avoir envoyé la moindre requête. Elle a été supprimée, et
173
+ `test/no-phantom-gate.test.ts` échoue si une porte déclare de nouveau une injoignabilité.
174
+ - **Pas un substitut à la lecture des définitions d'outils.** 11 enregistrements de menaces intégrés
175
+ sont un socle, pas un catalogue.
176
+
177
+ ## Développement
178
+
179
+ ```bash
180
+ npm install && npm run build && npm test # 141 tests
181
+ ```
182
+
183
+ `test/packaging.test.ts` est ce qui tient l'accroche honnête : il échoue si une dépendance d'exécution
184
+ apparaît, si un fichier source importe hors du paquet, ou si le point d'entrée cesse d'exporter la
185
+ surface d'application.
186
+
187
+ Utilisé par [ARGUS](https://github.com/alexar76/argus) (l'hôte de référence),
188
+ [MOMUS](https://github.com/alexar76/momus) (le côté éditeur) et le cours AICOM sur la sécurité MCP.
189
+
190
+ MIT © AICOM (alexar76)