@aimarket/warden 0.9.0 → 0.14.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 +47 -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 +44 -10
- package/dist/classifier.d.ts.map +1 -1
- package/dist/classifier.js +299 -48
- 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 +79 -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 +272 -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
|
@@ -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.14.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
|
+
}
|