@aimarket/warden 0.9.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/README-es.md +36 -11
- package/README-fr.md +36 -11
- package/README-ru.md +38 -11
- package/README-zh.md +36 -11
- package/README.md +38 -11
- package/dist/classifier.d.ts +39 -10
- package/dist/classifier.d.ts.map +1 -1
- package/dist/classifier.js +241 -45
- package/dist/classifier.js.map +1 -1
- package/dist/encoded-text.d.ts +7 -0
- package/dist/encoded-text.d.ts.map +1 -0
- package/dist/encoded-text.js +63 -0
- package/dist/encoded-text.js.map +1 -0
- package/dist/fold.d.ts +1 -2
- package/dist/fold.d.ts.map +1 -1
- package/dist/fold.js +3 -2
- package/dist/fold.js.map +1 -1
- package/dist/histor.d.ts +39 -4
- package/dist/histor.d.ts.map +1 -1
- package/dist/histor.js +11 -7
- package/dist/histor.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +29 -4
- package/dist/mcp-tools.js.map +1 -1
- package/dist/pin-store.d.ts.map +1 -1
- package/dist/pin-store.js +1 -0
- package/dist/pin-store.js.map +1 -1
- package/dist/pinning.d.ts +1 -1
- package/dist/pinning.d.ts.map +1 -1
- package/dist/pinning.js +12 -4
- package/dist/pinning.js.map +1 -1
- package/dist/result-screen.d.ts +19 -0
- package/dist/result-screen.d.ts.map +1 -0
- package/dist/result-screen.js +86 -0
- package/dist/result-screen.js.map +1 -0
- package/dist/scan-lock.d.ts.map +1 -1
- package/dist/scan-lock.js +2 -1
- package/dist/scan-lock.js.map +1 -1
- package/dist/scan-report.d.ts.map +1 -1
- package/dist/scan-report.js +15 -0
- package/dist/scan-report.js.map +1 -1
- package/dist/scan.d.ts +40 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +245 -25
- package/dist/scan.js.map +1 -1
- package/dist/static-scan.d.ts +4 -3
- package/dist/static-scan.d.ts.map +1 -1
- package/dist/static-scan.js +40 -9
- package/dist/static-scan.js.map +1 -1
- package/dist/toxic-flow.d.ts +33 -0
- package/dist/toxic-flow.d.ts.map +1 -0
- package/dist/toxic-flow.js +117 -0
- package/dist/toxic-flow.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wrap-cli.d.ts +3 -0
- package/dist/wrap-cli.d.ts.map +1 -1
- package/dist/wrap-cli.js +17 -6
- package/dist/wrap-cli.js.map +1 -1
- package/dist/wrap-state.js +2 -2
- package/dist/wrap-state.js.map +1 -1
- package/dist/wrap.d.ts.map +1 -1
- package/dist/wrap.js +20 -2
- package/dist/wrap.js.map +1 -1
- package/docs/scan.es.md +29 -14
- package/docs/scan.fr.md +29 -14
- package/docs/scan.md +29 -14
- package/docs/scan.ru.md +29 -14
- package/docs/scan.zh.md +29 -14
- package/docs/scanner-comparison.es.md +3 -12
- package/docs/scanner-comparison.fr.md +3 -13
- package/docs/scanner-comparison.md +3 -12
- package/docs/scanner-comparison.ru.md +3 -12
- package/docs/scanner-comparison.zh.md +3 -7
- package/docs/security-hardening-language-boundary.md +306 -0
- package/package.json +6 -3
- package/scripts/check-quality-gate.mjs +64 -0
- package/scripts/quality-public-key.pem +3 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scanner-comparison.md) · [Русский](scanner-comparison.ru.md) · **Español** · [Français](scanner-comparison.fr.md) · [中文](scanner-comparison.zh.md)
|
|
4
4
|
|
|
5
|
-
El 9 de octubre de 2026 ejecutamos WARDEN y dos escáneres MCP de código abierto, mcp-audit y mcp-shield,
|
|
5
|
+
El 9 de octubre de 2026 ejecutamos WARDEN y dos escáneres MCP de código abierto, mcp-audit y mcp-shield,[^snyk]
|
|
6
6
|
sobre los mismos servidores. De cada uno queríamos dos números: cuántas herramientas envenenadas bloquea y
|
|
7
7
|
cuántos servidores honestos bloquea por error. El segundo número decide si alguien puede dejar un escáner
|
|
8
8
|
encendido.
|
|
@@ -171,17 +171,6 @@ con `deepseek-flash`, el modelo que usa HISTOR:
|
|
|
171
171
|
parámetro y `rm -rf ~`. Funcionan en pareja.
|
|
172
172
|
- **Costó poco.** Toda la medición fueron 816 solicitudes; según nuestra estimación, menos de un dólar.
|
|
173
173
|
|
|
174
|
-
## Snyk Agent Scan
|
|
175
|
-
|
|
176
|
-
Snyk Agent Scan, antes Invariant mcp-scan, es el escáner MCP más usado. Juzga las descripciones de
|
|
177
|
-
herramientas en los servidores de Snyk y necesita un token de cuenta. El 9 de octubre de 2026 teníamos un
|
|
178
|
-
token válido. El servicio respondió a nuestra primera solicitud con HTTP 429, «The public quota for this
|
|
179
|
-
service has been exceeded». Esa cuota es compartida por todos los usuarios gratuitos, y ya estaba agotada
|
|
180
|
-
cuando llegamos. El propio mensaje del escáner lo llama límite de uso diario. Nuestro banco de pruebas
|
|
181
|
-
reintenta cada hora y va despacio cuando consigue pasar. Añadiremos la columna cuando haya terminado.
|
|
182
|
-
|
|
183
|
-
Es una nota sobre el acceso, no sobre la detección. No hemos medido la detección de Snyk.
|
|
184
|
-
|
|
185
174
|
## Límites, y nuestro interés
|
|
186
175
|
|
|
187
176
|
- **No somos neutrales.** Publicamos WARDEN, gratis y con licencia MIT. Escribimos los 23 ataques y los 55
|
|
@@ -216,3 +205,5 @@ npx -y @aimarket/warden@0.9.0 scan
|
|
|
216
205
|
La [guía de scan](scan.es.md) cubre el archivo lock, la GitHub Action, los hooks de pre-commit y el
|
|
217
206
|
clasificador. El [estudio de campo](mcp-survey.es.md) es el estudio anterior sobre en qué se equivocó WARDEN
|
|
218
207
|
con 1 108 servidores públicos.
|
|
208
|
+
|
|
209
|
+
[^snyk]: Snyk Agent Scan, el escáner MCP más usado, no está en la comparación. El 9 de octubre de 2026 su versión gratuita rechazó cada solicitud con HTTP 429, «The public quota for this service has been exceeded»: desde la primera solicitud, con las dos versiones de la CLI (0.6.8 y 0.5.17) y desde dos direcciones de red.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scanner-comparison.md) · [Русский](scanner-comparison.ru.md) · [Español](scanner-comparison.es.md) · **Français** · [中文](scanner-comparison.zh.md)
|
|
4
4
|
|
|
5
|
-
Le 9 octobre 2026, nous avons exécuté WARDEN et deux scanners MCP open source, mcp-audit et mcp-shield, sur
|
|
5
|
+
Le 9 octobre 2026, nous avons exécuté WARDEN et deux scanners MCP open source, mcp-audit et mcp-shield,[^snyk] sur
|
|
6
6
|
les mêmes serveurs. Nous voulions deux chiffres de chacun : combien d’outils empoisonnés il bloque, et
|
|
7
7
|
combien de serveurs honnêtes il bloque par erreur. Le second chiffre décide si quelqu’un peut laisser un
|
|
8
8
|
scanner allumé.
|
|
@@ -175,18 +175,6 @@ du journal HISTOR, avec les mêmes quatre catégories. Ses verdicts sont consult
|
|
|
175
175
|
comme paramètre et `rm -rf ~`. Les deux fonctionnent en paire.
|
|
176
176
|
- **Il a peu coûté.** Toute la mesure a pris 816 requêtes ; selon notre estimation, moins d’un dollar.
|
|
177
177
|
|
|
178
|
-
## Snyk Agent Scan
|
|
179
|
-
|
|
180
|
-
Snyk Agent Scan, anciennement Invariant mcp-scan, est le scanner MCP le plus utilisé. Il juge les
|
|
181
|
-
descriptions d’outils sur les serveurs de Snyk et exige un jeton de compte. Le 9 octobre 2026, nous avions
|
|
182
|
-
un jeton valide. Le service a répondu à notre première requête par HTTP 429, « The public quota for this
|
|
183
|
-
service has been exceeded ». Ce quota est partagé par tous les utilisateurs gratuits, et il était épuisé
|
|
184
|
-
avant notre arrivée. Le message du scanner lui-même parle d’une limite d’usage quotidienne. Notre banc
|
|
185
|
-
d’essai réessaie toutes les heures et avance lentement quand il passe. Nous ajouterons la colonne une fois
|
|
186
|
-
l’exécution terminée.
|
|
187
|
-
|
|
188
|
-
C’est une remarque sur l’accès, pas sur la détection. Nous n’avons pas mesuré la détection de Snyk.
|
|
189
|
-
|
|
190
178
|
## Limites, et notre intérêt
|
|
191
179
|
|
|
192
180
|
- **Nous ne sommes pas neutres.** Nous publions WARDEN, gratuit et sous licence MIT. Nous avons écrit les 23
|
|
@@ -221,3 +209,5 @@ npx -y @aimarket/warden@0.9.0 scan
|
|
|
221
209
|
Le [guide de scan](scan.fr.md) couvre le fichier lock, la GitHub Action, les hooks pre-commit et le
|
|
222
210
|
classificateur. L’[étude de terrain](mcp-survey.fr.md) est l’étude précédente sur les erreurs de WARDEN
|
|
223
211
|
face à 1 108 serveurs publics.
|
|
212
|
+
|
|
213
|
+
[^snyk]: Snyk Agent Scan, le scanner MCP le plus utilisé, n’est pas comparé. Le 9 octobre 2026, sa version gratuite a refusé chaque requête par HTTP 429, « The public quota for this service has been exceeded » : dès la première requête, avec les deux versions de la CLI (0.6.8 et 0.5.17) et depuis deux adresses réseau.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 **English** · [Русский](scanner-comparison.ru.md) · [Español](scanner-comparison.es.md) · [Français](scanner-comparison.fr.md) · [中文](scanner-comparison.zh.md)
|
|
4
4
|
|
|
5
|
-
On 2026-10-09 we ran WARDEN and two open-source MCP scanners, mcp-audit and mcp-shield, over the same
|
|
5
|
+
On 2026-10-09 we ran WARDEN and two open-source MCP scanners, mcp-audit and mcp-shield,[^snyk] over the same
|
|
6
6
|
servers. We wanted two numbers from each: how many poisoned tools it blocks, and how many honest servers
|
|
7
7
|
it blocks by mistake. The second number decides whether anyone can leave a scanner switched on.
|
|
8
8
|
|
|
@@ -164,17 +164,6 @@ uses:
|
|
|
164
164
|
and `rm -rf ~`. The two work as a pair.
|
|
165
165
|
- **It cost little.** The whole measurement was 816 requests; by our estimate it cost under a dollar.
|
|
166
166
|
|
|
167
|
-
## Snyk Agent Scan
|
|
168
|
-
|
|
169
|
-
Snyk Agent Scan, formerly Invariant mcp-scan, is the most used MCP scanner. It judges tool descriptions on
|
|
170
|
-
Snyk's servers and needs an account token. On 2026-10-09 we had a valid token. The service answered our
|
|
171
|
-
first request with HTTP 429, "The public quota for this service has been exceeded". That quota is shared by
|
|
172
|
-
every free user, and it was spent before we arrived. The scanner's own message calls it a daily usage
|
|
173
|
-
limit. Our harness retries every hour and goes slowly when it gets through. We will add the column when it
|
|
174
|
-
has run.
|
|
175
|
-
|
|
176
|
-
That is a note about access, not about detection. We have not measured Snyk's detection.
|
|
177
|
-
|
|
178
167
|
## Limits, and our interest
|
|
179
168
|
|
|
180
169
|
- **We are not neutral.** We publish WARDEN, MIT-licensed and free. We wrote the 23 attacks and the 55
|
|
@@ -207,3 +196,5 @@ npx -y @aimarket/warden@0.9.0 scan
|
|
|
207
196
|
|
|
208
197
|
The [scan guide](scan.md) covers the lock file, the GitHub Action, the pre-commit hooks and the classifier.
|
|
209
198
|
The [field survey](mcp-survey.md) is the earlier study of what WARDEN got wrong on 1 108 public servers.
|
|
199
|
+
|
|
200
|
+
[^snyk]: Snyk Agent Scan, the most used MCP scanner, is not compared. On 2026-10-09 its free version refused every request with HTTP 429, "The public quota for this service has been exceeded": from the first request, with both CLI versions (0.6.8 and 0.5.17) and from two network addresses.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scanner-comparison.md) · **Русский** · [Español](scanner-comparison.es.md) · [Français](scanner-comparison.fr.md) · [中文](scanner-comparison.zh.md)
|
|
4
4
|
|
|
5
|
-
9 октября 2026 года мы прогнали WARDEN и два MCP-сканера с открытым кодом, mcp-audit и mcp-shield, по одним
|
|
5
|
+
9 октября 2026 года мы прогнали WARDEN и два MCP-сканера с открытым кодом, mcp-audit и mcp-shield,[^snyk] по одним
|
|
6
6
|
и тем же серверам. От каждого нам нужны были два числа: сколько отравленных инструментов он блокирует и
|
|
7
7
|
сколько честных серверов блокирует по ошибке. Второе число решает, оставит ли кто-нибудь сканер включённым.
|
|
8
8
|
|
|
@@ -168,17 +168,6 @@ credential file … keep this step to yourself») и «include the output of env
|
|
|
168
168
|
и `rm -rf ~`. Они работают в паре.
|
|
169
169
|
- **Он обошёлся дёшево.** Весь замер занял 816 запросов; по нашей оценке, это меньше доллара.
|
|
170
170
|
|
|
171
|
-
## Snyk Agent Scan
|
|
172
|
-
|
|
173
|
-
Snyk Agent Scan, бывший Invariant mcp-scan, — самый используемый MCP-сканер. Он судит описания инструментов
|
|
174
|
-
на серверах Snyk и требует токен аккаунта. 9 октября 2026 года у нас был действующий токен. На первый же
|
|
175
|
-
запрос сервис ответил HTTP 429: «The public quota for this service has been exceeded». Эта квота общая для
|
|
176
|
-
всех бесплатных пользователей, и она закончилась до нас. Собственное сообщение сканера называет это
|
|
177
|
-
дневным лимитом. Наш стенд повторяет попытку каждый час и идёт медленно, когда квота открывается. Столбец
|
|
178
|
-
добавим, когда прогон пройдёт.
|
|
179
|
-
|
|
180
|
-
Это заметка о доступе, а не о качестве обнаружения. Обнаружение у Snyk мы не измеряли.
|
|
181
|
-
|
|
182
171
|
## Ограничения и наш интерес
|
|
183
172
|
|
|
184
173
|
- **Мы не нейтральны.** Мы выпускаем WARDEN, бесплатно и под лицензией MIT. 23 атаки и 55 разметок написали
|
|
@@ -212,3 +201,5 @@ npx -y @aimarket/warden@0.9.0 scan
|
|
|
212
201
|
[Руководство по scan](scan.ru.md) описывает лок-файл, GitHub Action, хуки pre-commit и классификатор.
|
|
213
202
|
[Полевой обзор](mcp-survey.ru.md) — более раннее исследование того, в чём WARDEN ошибся на 1 108 публичных
|
|
214
203
|
серверах.
|
|
204
|
+
|
|
205
|
+
[^snyk]: Snyk Agent Scan, самый используемый MCP-сканер, в сравнении не участвует. 9 октября 2026 года его бесплатная версия отказала на каждый запрос с HTTP 429 «The public quota for this service has been exceeded»: с первого же запроса, в обеих версиях CLI (0.6.8 и 0.5.17) и с двух сетевых адресов.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 🌐 [English](scanner-comparison.md) · [Русский](scanner-comparison.ru.md) · [Español](scanner-comparison.es.md) · [Français](scanner-comparison.fr.md) · **中文**
|
|
4
4
|
|
|
5
|
-
2026 年 10 月 9 日,我们在同一批服务器上运行了 WARDEN 和两款开源 MCP 扫描器:mcp-audit 与 mcp-shield。我们想从每款扫描器得到两个数字:它阻止了多少投毒工具,以及它误阻止了多少诚实的服务器。第二个数字决定了有没有人敢让扫描器一直开着。
|
|
5
|
+
2026 年 10 月 9 日,我们在同一批服务器上运行了 WARDEN 和两款开源 MCP 扫描器:mcp-audit 与 mcp-shield[^snyk]。我们想从每款扫描器得到两个数字:它阻止了多少投毒工具,以及它误阻止了多少诚实的服务器。第二个数字决定了有没有人敢让扫描器一直开着。
|
|
6
6
|
|
|
7
7
|
用自己的测试集衡量扫描器,结果总是好看,我们的也一样。所以本页大部分内容讲的是一个三家作者都没写过的样本集:MCPTox,一个发表于 AAAI 2026 的工具投毒基准。我们依据它一半的服务器编写了 WARDEN 最新的规则,并在另一半上测量。
|
|
8
8
|
|
|
@@ -105,12 +105,6 @@ v10 漏掉了 218 个留出工具中的 47 个,其中 41 个会以建议的形
|
|
|
105
105
|
- **它会漏掉规则能抓到的东西:** 注解(annotations)里的注入、作为参数索取的私钥,以及 `rm -rf ~`。两者要搭配使用。
|
|
106
106
|
- **成本很低。** 整个测量共 816 次请求;按我们的估算,不到一美元。
|
|
107
107
|
|
|
108
|
-
## Snyk Agent Scan
|
|
109
|
-
|
|
110
|
-
Snyk Agent Scan(原 Invariant mcp-scan)是使用最广的 MCP 扫描器。它在 Snyk 的服务器上判定工具描述,需要账号令牌。2026 年 10 月 9 日,我们持有有效的令牌。服务对我们的第一次请求就返回了 HTTP 429:“The public quota for this service has been exceeded”。这份配额由所有免费用户共享,在我们到来之前就已用完。扫描器自己的提示把它称作每日使用上限。我们的测试工具每小时重试一次,通过之后会放慢节奏运行。运行完成后,我们会补上这一列。
|
|
111
|
-
|
|
112
|
-
这只是关于访问的说明,不涉及检测能力。我们没有测量 Snyk 的检测效果。
|
|
113
|
-
|
|
114
108
|
## 局限,以及我们的利益
|
|
115
109
|
|
|
116
110
|
- **我们并不中立。** WARDEN 由我们发布,免费,采用 MIT 许可。23 个攻击和 55 条判定由我们编写。正因如此,样本集、重放测试工具、原始结果和每一条判定都是公开的。
|
|
@@ -133,3 +127,5 @@ npx -y @aimarket/warden@0.9.0 scan
|
|
|
133
127
|
```
|
|
134
128
|
|
|
135
129
|
[scan 指南](scan.zh.md)介绍 lock 文件、GitHub Action、pre-commit 钩子和分类器。[实地普查](mcp-survey.zh.md)是更早的一项研究,讲的是 WARDEN 在 1 108 台公开服务器上判错了什么。
|
|
130
|
+
|
|
131
|
+
[^snyk]: 使用最广的 MCP 扫描器 Snyk Agent Scan 未参与对比。2026 年 10 月 9 日,它的免费版本对每一次请求都返回 HTTP 429:“The public quota for this service has been exceeded”。从第一次请求起即如此,两个 CLI 版本(0.6.8 和 0.5.17)均如此,从两个网络地址发出也一样。
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Language-independent admission, WARDEN 0.12.0
|
|
2
|
+
|
|
3
|
+
No finite keyword list understands every language. Normalizing an encoding does
|
|
4
|
+
not translate its meaning, and an LLM's language coverage is also finite.
|
|
5
|
+
WARDEN therefore separates **recognition** from **permission to expose a tool
|
|
6
|
+
definition**.
|
|
7
|
+
|
|
8
|
+
## Revision 0.12.0
|
|
9
|
+
|
|
10
|
+
A confirmed semantic authority-boundary violation now always has `high` severity
|
|
11
|
+
at the host. The provider's original level is retained only as `modelSeverity`.
|
|
12
|
+
This closes the threshold mismatch behind the 16 known allowed attacks in 0.11.0;
|
|
13
|
+
uncertainty remains a separate failure of inspection.
|
|
14
|
+
|
|
15
|
+
With `scan --classifier-blocks`, a complete clean review can resolve exactly two
|
|
16
|
+
ambiguous static cases: a readable base64 blob, and quotation inside a structured
|
|
17
|
+
JSON field. The existing rules still inspect raw and decoded content. Unquoted
|
|
18
|
+
instructions and the other threat, origin, drift and approval gates remain in
|
|
19
|
+
force. This is model-dependent disambiguation, not a language-independent proof
|
|
20
|
+
of safety. The model is explicitly prompted to distinguish protective negation
|
|
21
|
+
and detection examples from directives to perform the quoted attack.
|
|
22
|
+
|
|
23
|
+
A review is bound to a pre-request snapshot of **all** advertised fields. Its
|
|
24
|
+
clearance lives in a private WeakMap, not in provider JSON or a serializable
|
|
25
|
+
policy flag. Mutating the input, copying an inspection object, inventing a clean
|
|
26
|
+
result, an advisory-only call, truncation, uncertainty or a failed request cannot
|
|
27
|
+
satisfy the guard. The same review is used by scan and its lock-update recheck.
|
|
28
|
+
No broad static allowlist is introduced. The offline gate chain and wrap do not
|
|
29
|
+
obtain a clean semantic review and retain conservative detection.
|
|
30
|
+
|
|
31
|
+
An early candidate that exempted every decodable base64 string was rejected:
|
|
32
|
+
it lost 900 existing offline detections. A second candidate relaxed structured
|
|
33
|
+
quotes offline; a mixed-language imperative exposed an unsafe exception. Both
|
|
34
|
+
partial experiments were stopped, and the final comparison keeps offline
|
|
35
|
+
blocking unchanged on the frozen attack and control sets. The final semantic run
|
|
36
|
+
uses a separate build identity and output directory; partial runs are not counted
|
|
37
|
+
as completed measurements.
|
|
38
|
+
|
|
39
|
+
Historical 0.11.0 measurements below are retained for comparison. The final
|
|
40
|
+
0.12.0 measurements are recorded separately in the companion report.
|
|
41
|
+
|
|
42
|
+
## Enforce the trust boundary
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
node dist/mcp-server.js wrap --require-approval --id notes -- node /path/to/server.mjs
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
First contact records the observed definitions for review but withholds them from
|
|
49
|
+
the MCP client and refuses their calls. In an operator terminal:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
node dist/mcp-server.js pins status --id notes
|
|
53
|
+
node dist/mcp-server.js pins approve --id notes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use the same `--state-dir` on both commands if overriding the default. Approval
|
|
57
|
+
requires the explicit TTY confirmation and rechecks non-pinning security gates.
|
|
58
|
+
The observed snapshot must still match what the operator reviewed. Relist the
|
|
59
|
+
tools after approval.
|
|
60
|
+
|
|
61
|
+
The policy equivalent is:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"requireApproval": true,
|
|
66
|
+
"pinToolDefs": true,
|
|
67
|
+
"blockAtSeverity": "high",
|
|
68
|
+
"allowUnknownServers": true,
|
|
69
|
+
"sensitiveToolPatterns": []
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The pin records `approvalMode: "operator"`, v2 hashing of every advertised tool
|
|
74
|
+
field, and the launch identity. Automatic first-contact pins, old pins, and
|
|
75
|
+
incomplete pins do not qualify. Any changed field or launch identity is refused,
|
|
76
|
+
even if `pinToolDefs` is separately disabled or a high severity threshold would
|
|
77
|
+
otherwise ignore a finding. No language identification is involved.
|
|
78
|
+
|
|
79
|
+
`scan --require-approval --lock warden.lock.json` applies the same admission
|
|
80
|
+
requirement. `--update-lock` is the explicit operator action that records a new
|
|
81
|
+
snapshot for repository review. A model verdict alone never changes trust.
|
|
82
|
+
`--no-launch` cannot verify the current definitions and refuses strict admission.
|
|
83
|
+
|
|
84
|
+
Compatibility: strict approval is **opt-in**. Ordinary wrap still automatically
|
|
85
|
+
pins a clean first contact, now marked `automatic`. `--audit-only` observes and
|
|
86
|
+
does not enforce. A host calling the library's `approve()` is a trusted authority;
|
|
87
|
+
it must not expose that operation to an untrusted model. MCP pin mutation tools
|
|
88
|
+
remain disabled unless the operator explicitly enables them.
|
|
89
|
+
|
|
90
|
+
## Semantic inspection assists review
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
WARDEN_CLASSIFIER_API_KEY=… node dist/mcp-server.js scan --project \
|
|
94
|
+
--classifier-url https://your-approved-provider.example/v1 \
|
|
95
|
+
--classifier-model YOUR_MODEL --classifier-blocks
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The classifier reads names, titles, descriptions, input/output schemas,
|
|
99
|
+
annotations and all extension metadata, including normalized readings of common
|
|
100
|
+
encodings. Its prompt asks about overriding user authority, credentials,
|
|
101
|
+
exfiltration, concealment and control of other tools. It has no language keyword
|
|
102
|
+
table. Random fences delimit untrusted input; they are not a security guarantee.
|
|
103
|
+
|
|
104
|
+
The field-reference protocol requires exactly one `clean`, `attack` or `uncertain`
|
|
105
|
+
decision per trusted tool index. Attack evidence refers to host-assigned field
|
|
106
|
+
IDs. WARDEN validates those IDs and extracts the source previews itself: admission
|
|
107
|
+
no longer depends on the model copying Unicode quotations exactly. Missing,
|
|
108
|
+
duplicate, contradictory and invalid decisions never become clean results.
|
|
109
|
+
Valid decisions survive a malformed decision about a different tool. Each
|
|
110
|
+
incomplete tool gets at most one isolated retry, also for length-limited responses.
|
|
111
|
+
An entire malformed batch can therefore use at most nine requests (one plus eight
|
|
112
|
+
single-tool retries). Authentication errors, rate limits, network/provider errors
|
|
113
|
+
and content filtering do not trigger this fan-out. Inputs exceeding the inspection
|
|
114
|
+
budget are rejected before transmission.
|
|
115
|
+
|
|
116
|
+
Enforcing mode rejects remaining incomplete coverage, uncertainty, timeouts and
|
|
117
|
+
provider errors with `CLASSIFIER_INCOMPLETE`. These are inspection failures, not
|
|
118
|
+
attack detections; they also prevent `--update-lock` from recording an approval.
|
|
119
|
+
Advisory mode reports failures without enforcing them.
|
|
120
|
+
|
|
121
|
+
`--classifier-reasoning-effort none|low|medium|high` is optional and requires provider
|
|
122
|
+
support; omitting it preserves the provider default. For the DeepSeek experiment
|
|
123
|
+
we explicitly use `none`: its default thinking mode otherwise shares the 4,000-token
|
|
124
|
+
output budget with the final JSON ([provider documentation](https://api-docs.deepseek.com/guides/thinking_mode/)).
|
|
125
|
+
Responses and optional diagnostics are size-bounded and never contain API headers
|
|
126
|
+
or keys. Diagnostic source text is recorded only through an explicit callback.
|
|
127
|
+
|
|
128
|
+
The classifier is part of `scan`, not the synchronous offline `vet()` gate chain
|
|
129
|
+
or the wrap call path. It does not silently send tool text from wrap to a vendor.
|
|
130
|
+
Choose and configure the endpoint explicitly. A clean model verdict is still not
|
|
131
|
+
an operator approval.
|
|
132
|
+
|
|
133
|
+
## Reproducible measurements
|
|
134
|
+
|
|
135
|
+
The companion MOMUS comparison freezes 8,244 synthetic attack definitions and
|
|
136
|
+
362 benign controls in 11 languages before evaluating both builds. The original
|
|
137
|
+
4,194 attack inputs are preserved byte-for-byte. Extra languages: Arabic, Hindi,
|
|
138
|
+
Korean, Swahili and Turkish. Extra encodings: percent, hexadecimal entities and
|
|
139
|
+
nested base64/percent encoding. These fixtures are author-labelled synthetic
|
|
140
|
+
data, not independent human-reviewed natural traffic or successful agent attacks.
|
|
141
|
+
|
|
142
|
+
Initial static comparison, old ruleset 10 versus ruleset 11:
|
|
143
|
+
|
|
144
|
+
| Set | Before | After |
|
|
145
|
+
|---|---:|---:|
|
|
146
|
+
| Original attacks detected / 4,194 | 1,620 | 1,692 |
|
|
147
|
+
| Expanded attacks detected / 8,244 | 2,664 | 2,808 |
|
|
148
|
+
| Benign controls blocked / 362 | 28 | 31 |
|
|
149
|
+
| Real public snapshots blocked / 986 | 3 | 3 |
|
|
150
|
+
|
|
151
|
+
No previously detected synthetic attack became allowed. Exact replay produced
|
|
152
|
+
identical decisions. The three added false blocks are encoded quotations of an
|
|
153
|
+
attack in extension metadata: decoding exposes the quote to a pre-existing
|
|
154
|
+
lexical rule. Existing false blocks include the base64 entropy heuristic on
|
|
155
|
+
legitimate encoded examples. These controls deliberately test such weaknesses;
|
|
156
|
+
they are not removed to improve the score. The public snapshots are unlabelled:
|
|
157
|
+
their block count is not a measured false-positive rate.
|
|
158
|
+
|
|
159
|
+
Strict admission refuses all 8,244 unapproved attack definitions and every changed
|
|
160
|
+
definition after approval, but also refuses all 362 unapproved benign controls.
|
|
161
|
+
Once explicitly approved, 331/362 benign controls pass; 31 still trip static
|
|
162
|
+
rules. These are admission measurements, **not 100% semantic recall**.
|
|
163
|
+
|
|
164
|
+
MOMUS reproduces the comparison and preserves per-case decisions:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
python -m momus.engine.coverage_compare --before /path/to/warden-v10 \
|
|
168
|
+
--after /path/to/warden-v11 --out data/warden-v11-comparison
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The optional live runner uses the actual compiled WARDEN classifier, with eight
|
|
172
|
+
independent fixtures per request and four concurrent requests. It never sends
|
|
173
|
+
fixture labels to the model. Keys come from environment variables; reports record
|
|
174
|
+
model, endpoint, corpus hash, build hash, prompt hash, findings and incomplete
|
|
175
|
+
inspections. A pilot is possible with a final numeric argument. Partial runs can
|
|
176
|
+
resume only with the identical corpus/model/build.
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
node momus/engine/coverage_semantic.mjs /path/to/warden \
|
|
180
|
+
data/warden-v11-comparison/cases.jsonl.gz data/semantic-pilot 80
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Protocol pilot
|
|
184
|
+
|
|
185
|
+
After replacing transcribed quotes with field IDs, fixing the uncertainty schema
|
|
186
|
+
and explicitly configuring the output budget, the same frozen, balanced 80-case
|
|
187
|
+
pilot was run through both providers:
|
|
188
|
+
|
|
189
|
+
| Provider / model | Attack high findings | Benign high findings | Incomplete |
|
|
190
|
+
|---|---:|---:|---:|
|
|
191
|
+
| DeepSeek / `deepseek-flash`, effort `none` | 40/40 | 0/40 | 0/80 |
|
|
192
|
+
| OpenRouter / `minimax/minimax-m3`, provider default | 40/40 | 0/40 | 0/80 |
|
|
193
|
+
|
|
194
|
+
Each used ten requests, all with `finish_reason=stop`; no retry was needed in
|
|
195
|
+
these pilots. Earlier iterations had incomplete inspections (24/80 and 40/80 in
|
|
196
|
+
the initial quote protocol, 16/80 and 3/80 in the first field protocol). Those
|
|
197
|
+
failed inspections were never counted as detected attacks. These are successive
|
|
198
|
+
engineering pilots, not a controlled causal comparison: the model is
|
|
199
|
+
nondeterministic, the protocol was tuned using pilot results, and the final pilot
|
|
200
|
+
is not evidence of universal reliability.
|
|
201
|
+
|
|
202
|
+
## Historical 0.11.0 full semantic run — 2026-10-10
|
|
203
|
+
|
|
204
|
+
The completed run used `deepseek-flash` with explicit reasoning effort `none`,
|
|
205
|
+
the frozen 8,606 fixtures and the same build digest as the static comparison.
|
|
206
|
+
At the unchanged blocking threshold `high`:
|
|
207
|
+
|
|
208
|
+
| Set | Ruleset 10 alone | Ruleset 11 alone | Ruleset 11 + DeepSeek |
|
|
209
|
+
|---|---:|---:|---:|
|
|
210
|
+
| Original attacks blocked / 4,194 | 1,620 | 1,692 | 4,184 |
|
|
211
|
+
| Expanded attacks blocked / 8,244 | 2,664 | 2,808 | 8,228 |
|
|
212
|
+
| Benign controls blocked / 362 | 28 | 31 | 32 |
|
|
213
|
+
|
|
214
|
+
Combined blocking is **99.76%** on the original set and **99.81%** on the expanded
|
|
215
|
+
set. The semantic layer alone produced 8,223 high findings on attacks, one high
|
|
216
|
+
finding on a benign control, and **zero incomplete inspections out of 8,606**.
|
|
217
|
+
All 1,076 requests finished with `stop`; no isolated retry was needed in this run.
|
|
218
|
+
Fault-injection unit tests separately verify bounded retries and closed admission
|
|
219
|
+
when retries fail.
|
|
220
|
+
|
|
221
|
+
All attacks received some semantic finding; 21 were below `high`. Static rules
|
|
222
|
+
blocked five of those, leaving **16 allowed attacks at the configured threshold**.
|
|
223
|
+
That is a remaining policy/model-severity limitation, not a protocol failure.
|
|
224
|
+
The added semantic false block is a German HTML-encoded tool title that says never
|
|
225
|
+
to send credentials or conversation history externally. Together with 31 static
|
|
226
|
+
false blocks, the combined false-positive rate on these stress controls is
|
|
227
|
+
**32/362 (8.84%)**. In that 0.11.0 run, static decisions were never disambiguated by a clean model verdict.
|
|
228
|
+
|
|
229
|
+
The recorded semantic result requires explicitly enabling the classifier; ordinary
|
|
230
|
+
offline `vet()` and wrap retain the static numbers. Neither these results nor
|
|
231
|
+
operator approval demonstrate safety in every language or on future model versions.
|
|
232
|
+
The inputs are related synthetic templates, including the pilot used for protocol
|
|
233
|
+
tuning. This is a reproducible stress evaluation, not a held-out generalization claim.
|
|
234
|
+
|
|
235
|
+
The companion [machine-readable report](../../momus/docs/coverage-v11.json) contains
|
|
236
|
+
build/corpus/prompt identities, per-language counts, missed IDs, false-block IDs,
|
|
237
|
+
provider pilots and validation results. Exact inputs and individual decisions are
|
|
238
|
+
in `momus/data/warden-v11-comparison/` and
|
|
239
|
+
`momus/data/warden-semantic-full-deepseek-v11/` in the working checkout.
|
|
240
|
+
|
|
241
|
+
## Final 0.12.0 measurement — 2026-10-10
|
|
242
|
+
|
|
243
|
+
The same frozen corpus (SHA-256
|
|
244
|
+
`f3333cd9243fc7b9e62cd760609124d1fefc97d103b706cbd67750be291b3add`)
|
|
245
|
+
was rerun with `deepseek-flash`, effort `none`, `scan --classifier-blocks`
|
|
246
|
+
semantics and threshold `high`. The live runner uses the production WARDEN
|
|
247
|
+
gates with the bound inspection; this is not a benchmark-only allowlist.
|
|
248
|
+
|
|
249
|
+
| Outcome | 0.11.0 combined | 0.12.0 combined |
|
|
250
|
+
|---|---:|---:|
|
|
251
|
+
| Original attacks blocked / 4,194 | 4,184 | 4,194 |
|
|
252
|
+
| Expanded attacks blocked / 8,244 | 8,228 | 8,244 |
|
|
253
|
+
| Allowed attacks | 16 | 0 |
|
|
254
|
+
| Benign controls blocked / 362 | 32 (8.84%) | 1 (0.28%) |
|
|
255
|
+
| Incomplete inspections / 8,606 | 0 | 0 |
|
|
256
|
+
|
|
257
|
+
All 16 previously missed attacks are blocked, and all 32 previously false-blocked
|
|
258
|
+
controls are allowed. **One new false block remains**:
|
|
259
|
+
`control.es.0.description.base64`, an encoded Spanish instruction to summarize
|
|
260
|
+
user-provided text. The model incorrectly treated this ordinary task description
|
|
261
|
+
as an authority violation. It remains in the denominator and saved report.
|
|
262
|
+
The number of false blocks fell by 96.875%; this is not zero-error protection.
|
|
263
|
+
|
|
264
|
+
All 1,076 provider answers completed with `stop`, no singleton retries. The model
|
|
265
|
+
received no fixture labels. Provider usage for the final full run was 3,694,186
|
|
266
|
+
prompt tokens and 466,642 completion tokens; stopped development runs are separate.
|
|
267
|
+
The balanced final pilot passed 40 attacks and 40 controls without incompletes.
|
|
268
|
+
|
|
269
|
+
Offline outcomes are deliberately unchanged: 2,808/8,244 attacks blocked,
|
|
270
|
+
31/362 controls blocked, zero attack regressions, zero replay differences.
|
|
271
|
+
The 986 unlabelled public snapshots keep the same three blocks. Their block count
|
|
272
|
+
is not a false-positive estimate. Ordinary wrap is not part of the live semantic
|
|
273
|
+
measurement.
|
|
274
|
+
|
|
275
|
+
[Full report](../../momus/docs/coverage-v12.json),
|
|
276
|
+
[implementation audit of the nine-part plan](../../momus/docs/mcp-quality-review.md),
|
|
277
|
+
and [safe local reproductions](../../momus/docs/mcp-quality-reproductions.json).
|
|
278
|
+
Final decisions are in `momus/data/warden-semantic-full-v12-bound/`; frozen
|
|
279
|
+
comparison inputs and offline decisions are in `momus/data/warden-v12-comparison/`.
|
|
280
|
+
The report binds corpus, prompt and build hashes and records previous-error
|
|
281
|
+
transitions and the abandoned experiments. This corpus influenced the changes;
|
|
282
|
+
its related synthetic translations are not an independent held-out distribution
|
|
283
|
+
or evidence of understanding every possible language.
|
|
284
|
+
|
|
285
|
+
## Scope of the guarantee
|
|
286
|
+
|
|
287
|
+
Strict approval prevents an unreviewed or changed **tool definition** from reaching
|
|
288
|
+
the client. It does not prove that approved prose is safe, that the implementation
|
|
289
|
+
matches its description, or that a call is authorized by the user's actual intent.
|
|
290
|
+
It does not pin executable bytes or inherited environment variables.
|
|
291
|
+
|
|
292
|
+
Strict wrap strips `initialize.instructions`, which is outside the approved tool
|
|
293
|
+
snapshot. Other MCP surfaces—resources, prompts, server metadata, sampling and
|
|
294
|
+
elicitation—are outside this definition-approval guarantee. Tool results are
|
|
295
|
+
separate untrusted data; the existing result screen is lexical, and withholding a
|
|
296
|
+
result cannot undo a tool call. A complete host also needs scoped filesystem and
|
|
297
|
+
network access, per-call authorization outside the model, and process isolation.
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
## Revision 0.13.0 — ordinary imperatives and honest coverage
|
|
301
|
+
|
|
302
|
+
The generic classifier criterion now requires a concrete authority violation. An imperative describing an operation on user-provided input remains ordinary documentation after decoding. There is no Spanish allowlist. On the same frozen 8,606 fixtures, DeepSeek (`deepseek-flash`, effort `none`) blocked all 8,244 attacks and allowed all 362 controls, including `control.es.0.description.base64`; no inspection was incomplete. [Full report](../../momus/docs/coverage-v13.json) includes prompt/build/corpus hashes and token counts. Offline recognition remains 2,808/8,244 with 31/362 false blocks; the semantic result must not be attributed to offline mode.
|
|
303
|
+
|
|
304
|
+
`wrap` reports oversized results as incomplete rather than scanning head/tail and clearing the unseen middle. This result layer remains static; no claim of universal multilingual result recognition is made. `block` withholds the result after the tool ran. `warn` annotates it.
|
|
305
|
+
|
|
306
|
+
Composition analysis accepts `policy.capabilityBindings`: one object per server with `serverId`, `identityHash`, `toolsHash` and a `tools` map from exact tool names to arrays containing `private`, `untrusted`, `outbound`. The hashes must match the reviewed launch identity and every definition field; identity/definition changes invalidate the binding. Empty arrays are explicit operator declarations. Names and server-provided capability annotations cannot grant trust. Unknown tools produce a possible-flow advisory. Known combinations produce a confirmed-capability advisory; neither proves a particular data leak.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aimarket/warden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "WARDEN — MCP security firewall. Stdio wrap proxy, inspection tools and library. Vets definitions before model exposure. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"dist",
|
|
36
|
+
"scripts/check-quality-gate.mjs",
|
|
37
|
+
"scripts/quality-public-key.pem",
|
|
36
38
|
"README.md",
|
|
37
39
|
"docs/scan*.md",
|
|
38
40
|
"README-ru.md",
|
|
@@ -56,7 +58,8 @@
|
|
|
56
58
|
"test": "vitest run",
|
|
57
59
|
"test:coverage": "vitest run --coverage",
|
|
58
60
|
"test:watch": "vitest",
|
|
59
|
-
"prepublishOnly": "npm run build && npm test && npm run check:ruleset",
|
|
61
|
+
"prepublishOnly": "npm run build && npm test && npm run check:ruleset && npm run check:quality",
|
|
62
|
+
"check:quality": "node scripts/check-quality-gate.mjs",
|
|
60
63
|
"check:ruleset": "node scripts/check-published-ruleset.mjs",
|
|
61
64
|
"bench:wrap": "npm run build && node scripts/benchmark-wrap.mjs"
|
|
62
65
|
},
|
|
@@ -79,7 +82,7 @@
|
|
|
79
82
|
"access": "public"
|
|
80
83
|
},
|
|
81
84
|
"devDependencies": {
|
|
82
|
-
"@types/node": "^
|
|
85
|
+
"@types/node": "^26.6.4",
|
|
83
86
|
"@vitest/coverage-v8": "^4.1.11",
|
|
84
87
|
"typescript": "^5.7.2",
|
|
85
88
|
"vitest": "^4.1.11"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Release evidence is signed by the isolated production evaluator, never by a PR runner.
|
|
2
|
+
import { createHash, verify } from 'node:crypto';
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export function buildDigest(build) {
|
|
8
|
+
const hash = createHash('sha256');
|
|
9
|
+
const files = readdirSync(join(build, 'dist')).filter(n => n.endsWith('.js')).sort();
|
|
10
|
+
if (!files.length) throw Error('WARDEN build missing');
|
|
11
|
+
if (files.some(name => name.startsWith('._'))) throw Error('Build contains AppleDouble transfer metadata');
|
|
12
|
+
for (const name of files) hash.update(name + '\0').update(readFileSync(join(build, 'dist', name))).update('\0');
|
|
13
|
+
return hash.digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function checkEvidence(envelope, publicKey, expectedBuild, now = Date.now() / 1000) {
|
|
17
|
+
const payload = Buffer.from(envelope.payload, 'base64');
|
|
18
|
+
if (!verify(null, payload, publicKey, Buffer.from(envelope.signature, 'base64'))) throw Error('Invalid quality signature');
|
|
19
|
+
const r = JSON.parse(payload);
|
|
20
|
+
if (r.type !== 'momus.release-quality/v1' || r.policy !== 'zero-regressions-v1' || r.status !== 'passed')
|
|
21
|
+
throw Error('Quality cycle has not passed');
|
|
22
|
+
if (r.buildDigest !== expectedBuild) throw Error('This exact WARDEN build has not passed');
|
|
23
|
+
if (!Number.isFinite(r.evaluatedAt) || !Number.isFinite(r.expiresAt) || r.evaluatedAt > now + 60
|
|
24
|
+
|| now - r.evaluatedAt > 48 * 3600 || r.expiresAt < now) throw Error('Quality evidence expired');
|
|
25
|
+
const full = r.fullEvidence;
|
|
26
|
+
if (!full || full.status !== 'passed' || full.buildDigest !== expectedBuild
|
|
27
|
+
|| full.holdoutSha256 !== r.holdoutSha256 || !Number.isFinite(full.evaluatedAt)
|
|
28
|
+
|| full.evaluatedAt > now + 60 || now - full.evaluatedAt > 35 * 86400) throw Error('Fresh full evaluation required');
|
|
29
|
+
for (const name of ['development', 'holdout', 'candidates']) {
|
|
30
|
+
const suite = r.suites?.[name];
|
|
31
|
+
if (!suite?.passed || !suite.counts || ['missed', 'falsePositives', 'incomplete'].some(k => suite.counts[k] !== 0))
|
|
32
|
+
throw Error(`Quality failure in ${name}`);
|
|
33
|
+
}
|
|
34
|
+
if (r.suites.holdout.counts.attacks < 20 || r.suites.holdout.counts.benign < 20) throw Error('Holdout coverage missing');
|
|
35
|
+
if (Object.values(r.suites).some(s => !s.passed)) throw Error('Regression suite failed');
|
|
36
|
+
const b = r.behaviour;
|
|
37
|
+
if (!b?.passed || b.cases !== 72 || b.executed !== 72 || !Number.isFinite(b.evaluatedAt)
|
|
38
|
+
|| now - b.evaluatedAt > 35 * 86400 || b.evaluatedAt > now + 60
|
|
39
|
+
|| ['missed', 'falsePositives', 'incomplete'].some(k => b[k] !== 0)) throw Error('Behaviour campaign missing or failed');
|
|
40
|
+
if (r.discovery?.status !== 'complete' || r.discovery.generation?.status !== 'ok') throw Error('Adversarial discovery unavailable');
|
|
41
|
+
const smoke = r.behaviourSmoke;
|
|
42
|
+
if (!smoke?.passed || smoke.executed < 3 || !Number.isFinite(smoke.evaluatedAt)
|
|
43
|
+
|| smoke.evaluatedAt > now + 60 || now - smoke.evaluatedAt > 48 * 3600
|
|
44
|
+
|| smoke.observerSha256 !== b.observerSha256 || smoke.fixturesSha256 !== b.fixturesSha256
|
|
45
|
+
|| ['missed', 'falsePositives', 'incomplete'].some(k => smoke[k] !== 0)) throw Error('Daily live sandbox check missing or failed');
|
|
46
|
+
return r;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
50
|
+
try {
|
|
51
|
+
const build = resolve(process.argv[2] || fileURLToPath(new URL('..', import.meta.url)));
|
|
52
|
+
const response = await fetch('https://histor.modelmarket.dev/security-quality/latest.json',
|
|
53
|
+
{ redirect: 'error', signal: AbortSignal.timeout(15000), cache: 'no-store' });
|
|
54
|
+
if (!response.ok) throw Error('Quality service unavailable');
|
|
55
|
+
const bytes = await response.arrayBuffer();
|
|
56
|
+
if (bytes.byteLength > 128 * 1024) throw Error('Oversized quality receipt');
|
|
57
|
+
const envelope = JSON.parse(Buffer.from(bytes).toString());
|
|
58
|
+
const report = checkEvidence(envelope, readFileSync(new URL('./quality-public-key.pem', import.meta.url)), buildDigest(build));
|
|
59
|
+
console.log(`MOMUS release gate passed: ${report.run}, ${report.buildDigest}`);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(`Release blocked: ${error.message}`);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
}
|
|
64
|
+
}
|