@aimarket/warden 0.4.0 → 0.6.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/README-zh.md CHANGED
@@ -1,37 +1,99 @@
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 版本" /></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 版本" /></a>
7
10
  <img src="docs/badges/deps.svg" alt="零运行时依赖" />
8
- <img src="docs/badges/tests.svg" alt="141 项测试通过" />
11
+ <img src="docs/badges/tests.svg" alt="189 项测试通过" />
9
12
  <img src="docs/badges/node.svg" alt="Node >= 20" />
10
13
  <a href="LICENSE"><img src="docs/badges/license.svg" alt="许可证:MIT" /></a>
11
14
  </p>
12
15
  <!-- /aicom-readme-badges -->
13
16
 
14
17
  <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" />
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>
16
21
  </p>
17
22
 
18
23
 
19
24
  > 🌐 [English](README.md) · [Русский](README-ru.md) · [Español](README-es.md) · [Français](README-fr.md) · **中文** · [术语表](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
20
25
 
26
+ **一个 MCP 服务器。针对已广告工具定义的安全防火墙。附带库。**
27
+
28
+ 传输:**stdio**(`npx -y @aimarket/warden` / `node dist/mcp-server.js`)。兼容主机:Claude Desktop、
29
+ Cursor、Glama,以及任何支持 stdio 的 MCP 客户端。不需要密钥。
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
+
21
40
  MCP 服务器自己告诉你的智能体,它的工具是做什么的。智能体就信了——而这句话正是攻击面。工具描述就是第三方直接
22
41
  投递进模型上下文的提示词文本;而一个名叫 `api_key` 的 schema 字段,就是以 API 形式写出来的索要密钥的请求。
23
42
 
24
43
  WARDEN 在**该服务器的任何工具到达模型之前**审查它,并返回一份可以留档的裁定:允许/阻止、0..1 的评分、导致该
25
44
  评分的各项发现、按工具的划分,以及当时生效的确切规则表。
26
45
 
46
+ **零 npm 运行时依赖。** 库本身唯一的 import 是 `node:crypto`。stdio MCP 服务器会再用到其他 `node:`
47
+ 内建模块(`fs`、`path`、`process`),仍然不拉任何包。它就是 [ARGUS](https://github.com/alexar76/argus)
48
+ 里的那个防火墙,被单独抽出来,好让你把它放在自己的 MCP 宿主前面,而不必换用一个智能体。
49
+
50
+ ## 作为 MCP 服务器运行(stdio)
51
+
27
52
  ```bash
28
- npm install @aimarket/warden
53
+ npx -y @aimarket/warden
54
+ npm run build && node dist/mcp-server.js
29
55
  ```
30
56
 
31
- **零运行时依赖。** 整个包里唯一的 import 是 `node:crypto`。它就是 [ARGUS](https://github.com/alexar76/argus)
32
- 里的那个防火墙,被单独抽出来,好让你把它放在自己的 MCP 宿主前面,而不必换用一个智能体。
57
+ Claude Desktop / Cursor(`mcpServers`):
33
58
 
34
- ## 快速开始
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "warden": {
63
+ "command": "npx",
64
+ "args": ["-y", "@aimarket/warden"]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ 进程**不会**启动、代理或沙箱化另一个 MCP 服务器:你传入 `tools/list` 转储,得到一份裁决。
71
+
72
+ | 工具 | 何时使用 |
73
+ |---|---|
74
+ | `vet_mcp_server` | 完整门控链 |
75
+ | `static_scan_tools` | 仅静态扫描 |
76
+ | `classify_sensitive_tools` | 运营方 glob 分类 |
77
+ | `check_egress_url` | 主机 allowlist(空列表拒绝全部) |
78
+ | `canonicalize_json` | RFC 8785 字节 |
79
+ | `list_scan_rules` | 已发布的规则表 |
80
+
81
+ ### 发布到 Glama
82
+
83
+ 列表页:**[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)** ·
84
+ quality score:**[glama.ai/mcp/servers/alexar76/warden/score](https://glama.ai/mcp/servers/alexar76/warden/score)**
85
+
86
+ [![alexar76/warden MCP server](https://glama.ai/mcp/servers/alexar76/warden/badges/score.svg)](https://glama.ai/mcp/servers/alexar76/warden)
87
+
88
+ 与 [ARGUS](https://github.com/alexar76/argus)、[aimarket-mcp](https://github.com/alexar76/aimarket-mcp)
89
+ 相同:[`glama.json`](glama.json) + [`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`。
90
+ 表单:[`docs/GLAMA.md`](docs/GLAMA.md)。
91
+
92
+ ## 库(嵌入你的宿主)
93
+
94
+ ```bash
95
+ npm install @aimarket/warden
96
+ ```
35
97
 
36
98
  ```ts
37
99
  import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
@@ -69,7 +131,7 @@ await warden.approve(server, tools); // 把用户认可的内容固定(pin)
69
131
 
70
132
  ```mermaid
71
133
  flowchart LR
72
- T["服务器给出的<br/>工具定义"] --> S["static scan<br/>25 条规则"]
134
+ T["服务器给出的<br/>工具定义"] --> S["static scan<br/>26 条规则"]
73
135
  S --> F["threat feed<br/>11 条内置 + 已签名"]
74
136
  F --> O["origin<br/>已声明 vs 来自目录"]
75
137
  O --> P["pinning<br/>漂移 vs 已批准"]
@@ -78,7 +140,7 @@ flowchart LR
78
140
 
79
141
  | 门控 | 判定什么 | 网络 | 是否 fatal |
80
142
  |---|---|---|---|
81
- | **static-scan** | 工具 `name`、`description` 与 `inputSchema` 中的注入、外泄、索要凭据,以及隐藏 Unicode/base64 迹象——25 条规则(v4),其中 15 条可阻止、10 条仅提示,17 条同时覆盖名称,12 条带有上下文 guard | 无 | 否 |
143
+ | **static-scan** | 工具 `name`、`description` 与 `inputSchema` 中的注入、外泄、索要凭据,以及隐藏 Unicode/base64 迹象——26 条规则(v5),其中 15 条可阻止、11 条仅提示,17 条同时覆盖名称,15 条带有上下文 guard。v5 先对文本做归一化(全角、不可见字符、Unicode 标签、形近字母),因此无论何种语言,混淆都绕不过规则 | 无 | 否 |
82
144
  | **threat-feed** | 已知恶意的服务器身份或工具:11 条内置记录,外加可选的已签名 feed | 仅 feed 下载 | 是,服务器范围的 `critical` |
83
145
  | **origin** | 该服务器是运营者声明的,还是来自远端目录 | 无 | 是,当 `allowUnknownServers: false` |
84
146
  | **pinning** | 工具定义是否仍与用户批准过的一致 | 无 | 是,当 `pinToolDefs: true` |
@@ -98,7 +160,7 @@ flowchart LR
98
160
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
99
161
  allowedTools: ["add"],
100
162
  blockedTools: ["sweeper"],
101
- rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
163
+ rulesets: { staticScan: { version: "5", digest: "sha256-HUKSzM6c…" } }
102
164
  }
103
165
  ```
104
166
 
@@ -146,6 +208,9 @@ GET <你的 feed url>
146
208
  | [已签名的威胁情报 feed](docs/threat-feed.zh.md) | 线上契约、三项检查,以及如何发布一个 WARDEN 会接受的 feed |
147
209
  | [集成指南](docs/integration.zh.md) | 如何把 WARDEN 接入自己的 MCP 宿主、策略取舍,以及应当留档什么 |
148
210
  | [实地普查:1 108 个公开 MCP 服务器](docs/mcp-survey.zh.md) | WARDEN 在真实的第三方工具定义上判了什么——拦截 50 个服务器,4 项成立,其余判错的六种方式 |
211
+ | [Glama / Docker](docs/GLAMA.md) | stdio MCP、健康检查、Build steps / CMD |
212
+ | [Security](SECURITY.md) | 如何报告防火墙绕过 |
213
+ | [Contributing](CONTRIBUTING.md) | 零依赖规则、规则表 PR |
149
214
 
150
215
  ## 它不是什么
151
216
 
@@ -157,11 +222,12 @@ GET <你的 feed url>
157
222
  没发出的情况下报告预言机不可达。该门控已被移除,并且只要有任何门控再次声称不可达,
158
223
  `test/no-phantom-gate.test.ts` 就会失败。
159
224
  - **不能替代你亲自读工具定义。** 11 条内置威胁记录是底线,不是目录。
225
+ - **不是代理。** stdio MCP 入口检查的是你传入的定义。它不会连接、下载或执行被扫描的服务器。
160
226
 
161
227
  ## 开发
162
228
 
163
229
  ```bash
164
- npm install && npm run build && npm test # 141 项测试
230
+ npm install && npm run build && npm test # 189 项测试
165
231
  ```
166
232
 
167
233
  `test/packaging.test.ts` 正是让标题保持诚实的东西:一旦出现运行时依赖、任何源文件从包外 import、或者入口点不
package/README.md CHANGED
@@ -1,23 +1,42 @@
1
- # WARDEN — MCP security firewall
1
+ # WARDEN — MCP server
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="141 tests passing" />
11
+ <img src="docs/badges/tests.svg" alt="189 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
 
14
17
  <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" />
18
+ <a href="https://warden.modelmarket.dev/">
19
+ <img src="docs/screenshots/readme/hero-3d.png" alt="WARDEN — 3D gate chain: tools/list through static-scan, threat-feed, origin, and pinning to a recorded verdict" width="100%" />
20
+ </a>
16
21
  </p>
17
22
 
18
23
 
19
24
  > 🌐 **English** · [Русский](README-ru.md) · [Español](README-es.md) · [Français](README-fr.md) · [中文](README-zh.md) · [Glossary](https://github.com/alexar76/aicom/blob/main/docs/localization-glossary.md)
20
25
 
26
+ **One MCP server. Security firewall for advertised tool definitions. Library included.**
27
+
28
+ Transport: **stdio** (`npx -y @aimarket/warden` / `node dist/mcp-server.js`). Compatible hosts:
29
+ Claude Desktop, Cursor, Glama, and any MCP client that speaks stdio. No API keys.
30
+
31
+ | Item | Location |
32
+ |------|----------|
33
+ | MCP entrypoint (stdio) | `warden-mcp` → [`src/mcp-server.ts`](src/mcp-server.ts) |
34
+ | Tools | `vet_mcp_server`, `static_scan_tools`, `classify_sensitive_tools`, `check_egress_url`, `canonicalize_json`, `list_scan_rules` |
35
+ | Library | `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
+
21
40
  An MCP server tells your agent what its tools do. The agent believes it — that sentence is the
22
41
  attack surface. A tool description is prompt text delivered by a third party straight into your
23
42
  model's context, and a schema field named `api_key` is a request for your secrets phrased as an API.
@@ -26,15 +45,63 @@ WARDEN vets a server **before any of its tools reach the model**, and returns a
26
45
  record: allow/block, a 0..1 score, the findings that produced it, a per-tool partition, and the
27
46
  exact rule table that was in force.
28
47
 
48
+ **Zero npm runtime dependencies.** The library's only import is `node:crypto`. The stdio MCP
49
+ server adds other `node:` builtins (`fs`, `path`, `process`) and still pulls in no packages. It is
50
+ the firewall out of [ARGUS](https://github.com/alexar76/argus), extracted so you can put it in front
51
+ of your own MCP host without adopting an agent.
52
+
53
+ ## Run as MCP server (stdio)
54
+
29
55
  ```bash
30
- npm install @aimarket/warden
56
+ npx -y @aimarket/warden # bin: warden-mcp
57
+ # from this repo:
58
+ npm run build && node dist/mcp-server.js
59
+ ```
60
+
61
+ Claude Desktop / Cursor (`mcpServers` entry):
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "warden": {
67
+ "command": "npx",
68
+ "args": ["-y", "@aimarket/warden"]
69
+ }
70
+ }
71
+ }
31
72
  ```
32
73
 
33
- **Zero runtime dependencies.** The only import in the whole package is `node:crypto`. It is the
34
- firewall out of [ARGUS](https://github.com/alexar76/argus), extracted so you can put it in front of
35
- your own MCP host without adopting an agent.
74
+ The process never starts, proxies, or sandboxes another MCP server — you pass a `tools/list` dump
75
+ in, you get a verdict out.
36
76
 
37
- ## Quick start
77
+ | Tool | When to use |
78
+ |---|---|
79
+ | `vet_mcp_server` | Full gate chain on a server identity + advertised tools |
80
+ | `static_scan_tools` | Injection / exfil scan only (no origin / pinning / threat feed) |
81
+ | `classify_sensitive_tools` | Operator glob split — not an injection scan |
82
+ | `check_egress_url` | Hostname allowlist (empty list denies every host) |
83
+ | `canonicalize_json` | RFC 8785 bytes for feeds and pins |
84
+ | `list_scan_rules` | Published rule table + digest |
85
+
86
+ Glama TDQS: MCP `annotations` (readOnly / destructive / idempotent / openWorld), when-to-use /
87
+ when-not naming siblings, every `inputSchema` property described, `outputSchema` on every tool.
88
+
89
+ ### Publish on Glama
90
+
91
+ Listing: **[glama.ai/mcp/servers/alexar76/warden](https://glama.ai/mcp/servers/alexar76/warden)** ·
92
+ quality score: **[glama.ai/mcp/servers/alexar76/warden/score](https://glama.ai/mcp/servers/alexar76/warden/score)**
93
+
94
+ [![alexar76/warden MCP server](https://glama.ai/mcp/servers/alexar76/warden/badges/score.svg)](https://glama.ai/mcp/servers/alexar76/warden)
95
+
96
+ Same pattern as **[ARGUS](https://github.com/alexar76/argus)** and
97
+ **[aimarket-mcp](https://github.com/alexar76/aimarket-mcp)**: repo-root [`glama.json`](glama.json) +
98
+ [`Dockerfile`](Dockerfile) + `node dist/mcp-server.js`. Admin form values: [`docs/GLAMA.md`](docs/GLAMA.md).
99
+
100
+ ## Library (embed in your host)
101
+
102
+ ```bash
103
+ npm install @aimarket/warden
104
+ ```
38
105
 
39
106
  ```ts
40
107
  import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden";
@@ -72,7 +139,7 @@ asked for by passing a URL to `load()`.
72
139
 
73
140
  ```mermaid
74
141
  flowchart LR
75
- T["tool defs<br/>from the server"] --> S["static scan<br/>25 rules"]
142
+ T["tool defs<br/>from the server"] --> S["static scan<br/>26 rules"]
76
143
  S --> F["threat feed<br/>11 built-ins + signed"]
77
144
  F --> O["origin<br/>declared vs catalog"]
78
145
  O --> P["pinning<br/>drift vs approval"]
@@ -81,7 +148,7 @@ flowchart LR
81
148
 
82
149
  | Gate | What it decides | Network | Fatal? |
83
150
  |---|---|---|---|
84
- | **static-scan** | Injection, exfiltration, credential requests and hidden-Unicode/base64 tells in the tool `name`, its `description` and its `inputSchema` — 25 rules, v4, of which 15 can block and 10 are advisory-only, 17 also cover the name, and 12 carry a context guard | none | no |
151
+ | **static-scan** | Injection, exfiltration, credential requests and hidden-Unicode/base64 tells in the tool `name`, its `description` and its `inputSchema` — 26 rules, v5, of which 15 can block and 11 are advisory-only, 17 also cover the name, and 15 carry a context guard. v5 folds the text first (fullwidth, invisible characters, Unicode tags, look-alike letters) so obfuscation cannot dodge a rule in any language | none | no |
85
152
  | **threat-feed** | Known-bad server identity or tool, from 11 built-in records plus an optional signed feed | only the feed fetch | yes, for a server-scoped `critical` |
86
153
  | **origin** | Whether the operator declared this server or it arrived from a remote catalog | none | yes, under `allowUnknownServers: false` |
87
154
  | **pinning** | Whether the tool defs still match what the user approved | none | yes, under `pinToolDefs: true` |
@@ -102,7 +169,7 @@ second as a low severity made it blocking again for anyone who tightened the thr
102
169
  findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }],
103
170
  allowedTools: ["add"],
104
171
  blockedTools: ["sweeper"],
105
- rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } }
172
+ rulesets: { staticScan: { version: "5", digest: "sha256-HUKSzM6c…" } }
106
173
  }
107
174
  ```
108
175
 
@@ -155,6 +222,10 @@ no protection:
155
222
  | [The signed threat feed](docs/threat-feed.md) | The wire contract, the three checks, and how to publish a feed WARDEN will accept |
156
223
  | [Integration guide](docs/integration.md) | Wiring WARDEN into your own MCP host, policy choices, and what to record |
157
224
  | [Field survey: 1 108 public MCP servers](docs/mcp-survey.md) | What WARDEN decided on real third-party tool definitions — 50 servers blocked, 4 substantiated, and the six ways the rest were wrong |
225
+ | [Glama / Docker](docs/GLAMA.md) | stdio MCP server, health check, admin Build steps / CMD |
226
+ | [MCP registries](docs/REGISTRIES.md) | Official Registry, Smithery, mcp.so / Pulse |
227
+ | [Security](SECURITY.md) | How to report a firewall bypass |
228
+ | [Contributing](CONTRIBUTING.md) | Zero-dep rule, ruleset PRs |
158
229
 
159
230
  ## What this is not
160
231
 
@@ -168,16 +239,19 @@ no protection:
168
239
  again.
169
240
  - **Not a substitute for reading the tool defs.** 11 built-in threat records is a floor, not a
170
241
  catalog.
242
+ - **Not a proxy.** The stdio MCP entry inspects advertised definitions you pass it. It does not
243
+ connect to, fetch, or execute the server under scan.
171
244
 
172
245
  ## Development
173
246
 
174
247
  ```bash
175
- npm install && npm run build && npm test # 141 tests
248
+ npm install && npm run build && npm test # 189 tests
176
249
  ```
177
250
 
178
- `test/packaging.test.ts` is what keeps the headline honest: it fails if a runtime dependency appears,
179
- if any source file imports outside the package, or if the entry point stops exporting the
180
- enforcement surface.
251
+ `test/packaging.test.ts` is what keeps the headline honest: it fails if an npm runtime dependency
252
+ appears, if any source file imports outside the package (except `node:` builtins), or if the entry
253
+ point stops exporting the enforcement surface. `test/mcp-server.test.ts` is the Glama health
254
+ check: `initialize` + `tools/list` + a `tools/call`.
181
255
 
182
256
  Used by [ARGUS](https://github.com/alexar76/argus) (the reference host), [MOMUS](https://github.com/alexar76/momus)
183
257
  (the publisher side), and the AICOM MCP-security course.
package/dist/fold.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Unicode folding before the rule table reads a tool definition.
3
+ *
4
+ * A rule written as `ignore previous instructions` must not be defeated by the
5
+ * same words spelled so that a regex no longer sees them while a model still
6
+ * does. Three spellings do that, and none of them depends on the language:
7
+ *
8
+ * 1. Compatibility forms. `ignore previous` (fullwidth), ligatures,
9
+ * mathematical alphanumerics, circled letters. NFKC maps each to the plain
10
+ * letter a reader sees.
11
+ * 2. Invisible characters inside a word. `ig​nore`, a soft hyphen, a
12
+ * variation selector. They are dropped here so they cannot split a rule word.
13
+ * The high-signal ones (zero-width, bidi, word-joiner, BOM, the tag block and
14
+ * the variation-selector supplement) are ALSO reported, because the
15
+ * hidden-character rule runs on the raw text; the purely cosmetic ones (a
16
+ * soft hyphen, an Indic/Arabic joiner control) are neutralised here and not
17
+ * separately flagged.
18
+ * 3. Look-alike letters from another script inside a Latin word: `іgnоre` with a
19
+ * Cyrillic і and о. A token that mixes Latin with Cyrillic or Greek letters has
20
+ * its look-alikes mapped to Latin. A token written wholly in one script is left
21
+ * alone, so Russian and Greek text still reads as Russian and Greek.
22
+ *
23
+ * Unicode TAG characters (U+E0020–U+E007E) are the exception to "dropped": each is
24
+ * an invisible copy of an ASCII character, and a whole instruction can be written in
25
+ * them. They are DECODED to that ASCII here, so the instruction they carry meets the
26
+ * same rules as visible text.
27
+ *
28
+ * The fold is part of the published ruleset (FOLD_ID is in its digest): the same
29
+ * regexes over folded and unfolded text are different scans.
30
+ */
31
+ /** Identity of this fold, carried in the ruleset preimage. Bump on any change below. */
32
+ export declare const FOLD_ID = "nfkc+tags-decoded+invisible-stripped+mixed-script-confusables/1";
33
+ /**
34
+ * Fold `text` for matching. Never shown to anyone: findings quote the folded span,
35
+ * and the raw text is what the hidden-character rules and the reader see.
36
+ */
37
+ export declare function foldForScan(text: string): string;
38
+ //# sourceMappingURL=fold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fold.d.ts","sourceRoot":"","sources":["../src/fold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,wFAAwF;AACxF,eAAO,MAAM,OAAO,oEAAoE,CAAC;AAuCzF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBhD"}
package/dist/fold.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Unicode folding before the rule table reads a tool definition.
3
+ *
4
+ * A rule written as `ignore previous instructions` must not be defeated by the
5
+ * same words spelled so that a regex no longer sees them while a model still
6
+ * does. Three spellings do that, and none of them depends on the language:
7
+ *
8
+ * 1. Compatibility forms. `ignore previous` (fullwidth), ligatures,
9
+ * mathematical alphanumerics, circled letters. NFKC maps each to the plain
10
+ * letter a reader sees.
11
+ * 2. Invisible characters inside a word. `ig​nore`, a soft hyphen, a
12
+ * variation selector. They are dropped here so they cannot split a rule word.
13
+ * The high-signal ones (zero-width, bidi, word-joiner, BOM, the tag block and
14
+ * the variation-selector supplement) are ALSO reported, because the
15
+ * hidden-character rule runs on the raw text; the purely cosmetic ones (a
16
+ * soft hyphen, an Indic/Arabic joiner control) are neutralised here and not
17
+ * separately flagged.
18
+ * 3. Look-alike letters from another script inside a Latin word: `іgnоre` with a
19
+ * Cyrillic і and о. A token that mixes Latin with Cyrillic or Greek letters has
20
+ * its look-alikes mapped to Latin. A token written wholly in one script is left
21
+ * alone, so Russian and Greek text still reads as Russian and Greek.
22
+ *
23
+ * Unicode TAG characters (U+E0020–U+E007E) are the exception to "dropped": each is
24
+ * an invisible copy of an ASCII character, and a whole instruction can be written in
25
+ * them. They are DECODED to that ASCII here, so the instruction they carry meets the
26
+ * same rules as visible text.
27
+ *
28
+ * The fold is part of the published ruleset (FOLD_ID is in its digest): the same
29
+ * regexes over folded and unfolded text are different scans.
30
+ */
31
+ /** Identity of this fold, carried in the ruleset preimage. Bump on any change below. */
32
+ export const FOLD_ID = "nfkc+tags-decoded+invisible-stripped+mixed-script-confusables/1";
33
+ /**
34
+ * Characters with no visible form in running text. Dropped by the fold; the
35
+ * hidden-character rule reports them from the raw text.
36
+ */
37
+ const INVISIBLE_RE = new RegExp("[\\u00AD\\u034F\\u061C\\u115F\\u1160\\u17B4\\u17B5\\u180B-\\u180F\\u200B-\\u200F\\u202A-\\u202E" +
38
+ "\\u2060-\\u2064\\u2066-\\u206F\\u3164\\uFE00-\\uFE0F\\uFEFF\\uFFA0\\u{E0000}-\\u{E001F}" +
39
+ "\\u{E007F}\\u{E0100}-\\u{E01EF}]", "gu");
40
+ /** U+E0020–U+E007E: invisible ASCII. Decoded, not dropped. */
41
+ const TAG_RE = /[\u{E0020}-\u{E007E}]/gu;
42
+ /**
43
+ * Cyrillic and Greek letters that render like a Latin letter. Used only inside a
44
+ * token that already contains Latin letters.
45
+ */
46
+ const CONFUSABLE = {
47
+ // Cyrillic lower case
48
+ "а": "a", "в": "b", "е": "e", "ё": "e", "к": "k", "м": "m", "н": "h", "о": "o", "р": "p",
49
+ "с": "c", "т": "t", "у": "y", "х": "x", "і": "i", "ї": "i", "ј": "j", "ѕ": "s", "ԁ": "d",
50
+ "ԛ": "q", "ԝ": "w", "ӏ": "l", "һ": "h", "ɡ": "g",
51
+ // Cyrillic upper case
52
+ "А": "A", "В": "B", "Е": "E", "Ё": "E", "К": "K", "М": "M", "Н": "H", "О": "O", "Р": "P",
53
+ "С": "C", "Т": "T", "У": "Y", "Х": "X", "І": "I", "Ї": "I", "Ј": "J", "Ѕ": "S", "Һ": "H",
54
+ "Ԛ": "Q", "Ԝ": "W", "Ӏ": "I",
55
+ // Greek
56
+ "α": "a", "ε": "e", "ι": "i", "κ": "k", "ν": "v", "ο": "o", "ρ": "p", "τ": "t", "υ": "u",
57
+ "χ": "x", "Α": "A", "Β": "B", "Ε": "E", "Ζ": "Z", "Η": "H", "Ι": "I", "Κ": "K", "Μ": "M",
58
+ "Ν": "N", "Ο": "O", "Ρ": "P", "Τ": "T", "Υ": "Y", "Χ": "X",
59
+ };
60
+ const TOKEN_RE = /[\p{L}\p{M}]+/gu;
61
+ const LATIN_RE = /[A-Za-z]/;
62
+ const CONFUSABLE_RE = new RegExp(`[${Object.keys(CONFUSABLE).join("")}]`, "gu");
63
+ /**
64
+ * Fold `text` for matching. Never shown to anyone: findings quote the folded span,
65
+ * and the raw text is what the hidden-character rules and the reader see.
66
+ */
67
+ export function foldForScan(text) {
68
+ if (!text)
69
+ return text;
70
+ let out = text.normalize("NFKC");
71
+ out = out.replace(TAG_RE, (ch) => String.fromCharCode((ch.codePointAt(0) ?? 0xe0020) - 0xe0000));
72
+ out = out.replace(INVISIBLE_RE, "");
73
+ CONFUSABLE_RE.lastIndex = 0;
74
+ if (!CONFUSABLE_RE.test(out))
75
+ return out;
76
+ return out.replace(TOKEN_RE, (tok) => {
77
+ // Map a token that mixes Latin with look-alikes (`pаypаl`), OR one written
78
+ // WHOLLY in look-alike letters (`ЅУЅТЕМ`, `рrіvаtе` where every letter is in
79
+ // the table). A token with non-Latin letters that are NOT all confusables —
80
+ // ordinary Russian or Greek — is left alone, so those languages read normally.
81
+ // The scanner also matches the un-mapped raw text, so this only ever ADDS a
82
+ // reading; it never hides the original.
83
+ const marks = tok.replace(/\p{M}/gu, "");
84
+ if (!LATIN_RE.test(tok) && !allConfusable(marks))
85
+ return tok;
86
+ return tok.replace(CONFUSABLE_RE, (ch) => CONFUSABLE[ch] ?? ch);
87
+ });
88
+ }
89
+ /** Every letter of the token is one of the Latin look-alikes in the table. */
90
+ function allConfusable(letters) {
91
+ if (!letters)
92
+ return false;
93
+ for (const ch of letters) {
94
+ if (!(ch in CONFUSABLE) && !/[A-Za-z]/.test(ch))
95
+ return false;
96
+ }
97
+ return /[^A-Za-z]/.test(letters); // at least one non-Latin look-alike, else nothing to map
98
+ }
99
+ //# sourceMappingURL=fold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fold.js","sourceRoot":"","sources":["../src/fold.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * JSON-RPC MCP methods for the stdio server (no transport).
3
+ */
4
+ export declare const PROTOCOL = "2025-03-26";
5
+ export declare function packageVersion(): string;
6
+ export interface JsonRpcReq {
7
+ jsonrpc?: string;
8
+ id?: string | number | null;
9
+ method?: string;
10
+ params?: unknown;
11
+ }
12
+ export interface JsonRpcRes {
13
+ jsonrpc: "2.0";
14
+ id: string | number | null;
15
+ result?: unknown;
16
+ error?: {
17
+ code: number;
18
+ message: string;
19
+ };
20
+ }
21
+ export declare function listedTools(): {
22
+ name: string;
23
+ title: string;
24
+ description: string;
25
+ inputSchema: Record<string, unknown>;
26
+ outputSchema: Record<string, unknown>;
27
+ annotations: {
28
+ title: string;
29
+ readOnlyHint: boolean;
30
+ destructiveHint: boolean;
31
+ idempotentHint: boolean;
32
+ openWorldHint: boolean;
33
+ };
34
+ }[];
35
+ export declare function consumeLsp(buf: Buffer): {
36
+ rest: Buffer;
37
+ bodies: string[];
38
+ };
39
+ export declare function handleRpc(msg: JsonRpcReq): Promise<JsonRpcRes | null>;
40
+ //# sourceMappingURL=mcp-rpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-rpc.d.ts","sourceRoot":"","sources":["../src/mcp-rpc.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,eAAO,MAAM,QAAQ,eAAe,CAAC;AAGrC,wBAAgB,cAAc,IAAI,MAAM,CAQvC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,wBAAgB,WAAW;;;;;;;;;;;;;IAS1B;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAiB1E;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAgE3E"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * JSON-RPC MCP methods for the stdio server (no transport).
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { callMcpTool, MCP_INSTRUCTIONS, MCP_TOOLS, McpToolError, payloadTooLarge } from "./mcp-tools.js";
8
+ export const PROTOCOL = "2025-03-26";
9
+ const FALLBACK_VERSION = "0.5.1";
10
+ export function packageVersion() {
11
+ try {
12
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
13
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
14
+ return typeof pkg.version === "string" ? pkg.version : FALLBACK_VERSION;
15
+ }
16
+ catch {
17
+ return FALLBACK_VERSION;
18
+ }
19
+ }
20
+ export function listedTools() {
21
+ return MCP_TOOLS.map((t) => ({
22
+ name: t.name,
23
+ title: t.title,
24
+ description: t.description,
25
+ inputSchema: t.inputSchema,
26
+ outputSchema: t.outputSchema,
27
+ annotations: t.annotations,
28
+ }));
29
+ }
30
+ export function consumeLsp(buf) {
31
+ const bodies = [];
32
+ while (true) {
33
+ const headerEnd = buf.indexOf("\r\n\r\n");
34
+ if (headerEnd < 0)
35
+ return { rest: buf, bodies };
36
+ const header = buf.subarray(0, headerEnd).toString("ascii");
37
+ const match = /Content-Length:\s*(\d+)/i.exec(header);
38
+ if (!match) {
39
+ buf = buf.subarray(headerEnd + 4);
40
+ continue;
41
+ }
42
+ const len = Number(match[1]);
43
+ const start = headerEnd + 4;
44
+ if (buf.length < start + len)
45
+ return { rest: buf, bodies };
46
+ bodies.push(buf.subarray(start, start + len).toString("utf8"));
47
+ buf = buf.subarray(start + len);
48
+ }
49
+ }
50
+ export async function handleRpc(msg) {
51
+ const method = msg.method ?? "";
52
+ const id = msg.id;
53
+ if (method.startsWith("notifications/"))
54
+ return null;
55
+ const ok = (result) => ({ jsonrpc: "2.0", id: id ?? null, result });
56
+ const fail = (code, message) => ({
57
+ jsonrpc: "2.0",
58
+ id: id ?? null,
59
+ error: { code, message },
60
+ });
61
+ switch (method) {
62
+ case "initialize":
63
+ return ok({
64
+ protocolVersion: PROTOCOL,
65
+ capabilities: { tools: { listChanged: false } },
66
+ serverInfo: {
67
+ name: "warden",
68
+ title: "WARDEN MCP security firewall",
69
+ version: packageVersion(),
70
+ },
71
+ instructions: MCP_INSTRUCTIONS,
72
+ });
73
+ case "ping":
74
+ return ok({});
75
+ case "tools/list":
76
+ return ok({ tools: listedTools() });
77
+ case "tools/call": {
78
+ if (id === undefined)
79
+ return null;
80
+ const params = (msg.params ?? {});
81
+ const name = typeof params.name === "string" ? params.name : "";
82
+ if (!name)
83
+ return fail(-32602, "tools/call requires params.name");
84
+ const args = params.arguments && typeof params.arguments === "object" && !Array.isArray(params.arguments)
85
+ ? params.arguments
86
+ : {};
87
+ if (payloadTooLarge(args)) {
88
+ return ok({ content: [{ type: "text", text: "payload exceeds 256 KiB" }], isError: true });
89
+ }
90
+ try {
91
+ const { structured } = await callMcpTool(name, args);
92
+ return ok({
93
+ content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
94
+ structuredContent: structured,
95
+ });
96
+ }
97
+ catch (err) {
98
+ const message = err instanceof Error ? err.message : String(err);
99
+ if (!(err instanceof McpToolError) && !(err instanceof Error)) {
100
+ return ok({ content: [{ type: "text", text: String(err) }], isError: true });
101
+ }
102
+ return ok({ content: [{ type: "text", text: message }], isError: true });
103
+ }
104
+ }
105
+ case "resources/list":
106
+ return ok({ resources: [] });
107
+ case "resources/templates/list":
108
+ return ok({ resourceTemplates: [] });
109
+ case "prompts/list":
110
+ return ok({ prompts: [] });
111
+ default:
112
+ if (id === undefined)
113
+ return null;
114
+ return fail(-32601, `method not found: ${method}`);
115
+ }
116
+ }
117
+ //# sourceMappingURL=mcp-rpc.js.map