@aimarket/warden 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -3
- package/README-es.md +10 -4
- package/README-fr.md +10 -4
- package/README-ru.md +10 -4
- package/README-zh.md +10 -4
- package/README.md +10 -4
- package/dist/glob.d.ts +81 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +180 -0
- package/dist/glob.js.map +1 -0
- package/dist/index.d.ts +42 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -29
- package/dist/index.js.map +1 -1
- package/dist/origin.d.ts.map +1 -1
- package/dist/origin.js +7 -2
- package/dist/origin.js.map +1 -1
- package/dist/pinning.d.ts +18 -0
- package/dist/pinning.d.ts.map +1 -1
- package/dist/pinning.js +59 -11
- package/dist/pinning.js.map +1 -1
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +10 -7
- package/dist/sandbox.js.map +1 -1
- package/dist/sanitize.d.ts +38 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +53 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/static-scan.d.ts +46 -2
- package/dist/static-scan.d.ts.map +1 -1
- package/dist/static-scan.js +358 -34
- package/dist/static-scan.js.map +1 -1
- package/dist/threat-feed.d.ts +3 -0
- package/dist/threat-feed.d.ts.map +1 -1
- package/dist/threat-feed.js +118 -29
- package/dist/threat-feed.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/glob.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `*`-glob matching, without a regular expression.
|
|
3
|
+
*
|
|
4
|
+
* WARDEN matches two kinds of glob against attacker-controlled text: threat-feed
|
|
5
|
+
* patterns (a signed publisher chooses the pattern, a hostile server chooses the
|
|
6
|
+
* haystack) and `policy.sensitiveToolPatterns` (the operator chooses the pattern,
|
|
7
|
+
* the server chooses the tool name). Both used to compile `*` into `.*` and run a
|
|
8
|
+
* regex, which is a denial of service:
|
|
9
|
+
*
|
|
10
|
+
* pattern "*a*a*a*a*a*a*a*a*a*a*a*a*a*a*zzz" (32 characters)
|
|
11
|
+
* haystack "aaaa..." (220 characters)
|
|
12
|
+
* -> 112 SECONDS inside one RegExp.test
|
|
13
|
+
*
|
|
14
|
+
* The engine has to try every way of distributing the wildcards over the haystack
|
|
15
|
+
* before it can conclude "no match" - exponential in the number of `*`. That
|
|
16
|
+
* mattered more than an ordinary performance bug: the threat feed's whole trust
|
|
17
|
+
* story is that a compromised publisher can only ever ADD protection, never
|
|
18
|
+
* remove it, and a record that hangs `vet()` forever removes all of it. The same
|
|
19
|
+
* shape let a hostile server freeze the agent inspecting it, by advertising a
|
|
20
|
+
* long enough tool name against an operator's own `*a*b*c*` pattern.
|
|
21
|
+
*
|
|
22
|
+
* The two-pointer algorithm below is the standard wildcard matcher: it walks the
|
|
23
|
+
* haystack once and remembers only the most recent `*` to fall back to, so the
|
|
24
|
+
* work is bounded by `pattern.length x value.length` with no backtracking tree to
|
|
25
|
+
* explode. The same 32/220 case returns in microseconds.
|
|
26
|
+
*
|
|
27
|
+
* Semantics, matching what the regex did: `*` matches any run of zero or more
|
|
28
|
+
* UTF-16 code units (including newlines - the old regex needed the `s` flag for
|
|
29
|
+
* that), every other character is literal, and the match is anchored at both
|
|
30
|
+
* ends. Case folding is the caller's job: both sides arrive already lowercased.
|
|
31
|
+
*/
|
|
32
|
+
export function wildcardMatch(pattern, value) {
|
|
33
|
+
const STAR = 0x2a;
|
|
34
|
+
let p = 0;
|
|
35
|
+
let v = 0;
|
|
36
|
+
let star = -1;
|
|
37
|
+
let resume = 0;
|
|
38
|
+
while (v < value.length) {
|
|
39
|
+
if (p < pattern.length && pattern.charCodeAt(p) === value.charCodeAt(v)) {
|
|
40
|
+
p++;
|
|
41
|
+
v++;
|
|
42
|
+
}
|
|
43
|
+
else if (p < pattern.length && pattern.charCodeAt(p) === STAR) {
|
|
44
|
+
// Remember this wildcard and let it match nothing for now.
|
|
45
|
+
star = p++;
|
|
46
|
+
resume = v;
|
|
47
|
+
}
|
|
48
|
+
else if (star >= 0) {
|
|
49
|
+
// Backtrack: hand one more character to the last wildcard. There is only
|
|
50
|
+
// ever one fallback point, which is what keeps this linear-bounded.
|
|
51
|
+
p = star + 1;
|
|
52
|
+
v = ++resume;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Trailing wildcards may still match the empty remainder.
|
|
59
|
+
while (p < pattern.length && pattern.charCodeAt(p) === STAR)
|
|
60
|
+
p++;
|
|
61
|
+
return p === pattern.length;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* How many wildcards a pattern may carry.
|
|
65
|
+
*
|
|
66
|
+
* The matcher above is not exponential, but `*`-heavy patterns still cost
|
|
67
|
+
* `pattern x value` per match and buy nothing: a real threat pattern names a
|
|
68
|
+
* command, a path or a keyword. A record with more than this many wildcards is
|
|
69
|
+
* refused rather than run - refusing an *addition* to the deny-list is the safe
|
|
70
|
+
* direction, and it is reported loudly.
|
|
71
|
+
*/
|
|
72
|
+
export const MAX_GLOB_WILDCARDS = 12;
|
|
73
|
+
/** Count `*` in a pattern, for the record validator. */
|
|
74
|
+
export function countWildcards(pattern) {
|
|
75
|
+
let n = 0;
|
|
76
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
77
|
+
if (pattern.charCodeAt(i) === 0x2a)
|
|
78
|
+
n++;
|
|
79
|
+
}
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Largest gap an INTERIOR `*` in a threat pattern may span.
|
|
84
|
+
*
|
|
85
|
+
* A field survey of 1 108 public MCP servers (see docs/mcp-survey.md) found every
|
|
86
|
+
* multi-segment built-in firing on unrelated words that merely happened to appear
|
|
87
|
+
* in the same paragraph: `*drain*wallet*` matched an anti-drainer scanner because
|
|
88
|
+
* it says "drainer" in one clause and "wallet" in another, and `*seed*phrase*`
|
|
89
|
+
* matched "for a **seed** topic, returns suggested search **phrases**". A real
|
|
90
|
+
* threat pattern names something whose parts sit next to each other; 24 characters
|
|
91
|
+
* is roughly "a few words apart" and comfortably covers "wallet seed or recovery
|
|
92
|
+
* phrase".
|
|
93
|
+
*/
|
|
94
|
+
export const THREAT_MAX_GAP = 24;
|
|
95
|
+
/**
|
|
96
|
+
* Threat-pattern matching: `wildcardMatch` plus two constraints that only make
|
|
97
|
+
* sense for a deny-list.
|
|
98
|
+
*
|
|
99
|
+
* 1. **Interior gaps are bounded** to {@link THREAT_MAX_GAP}. Leading and trailing
|
|
100
|
+
* `*` stay unbounded — `*id_rsa*` must still find `id_rsa` anywhere.
|
|
101
|
+
* 2. **A segment that starts with a word character must start at a word boundary.**
|
|
102
|
+
* `*sweep*funds*` matched an ENS floor-sweeping tool because `funds` sits inside
|
|
103
|
+
* "re**funds**". Only the START is anchored: a pattern author writing `*drain*`
|
|
104
|
+
* means "drainer" too, so anchoring the end as well would break ordinary
|
|
105
|
+
* stemming.
|
|
106
|
+
*
|
|
107
|
+
* Kept separate from {@link wildcardMatch} on purpose. That function is also what
|
|
108
|
+
* `policy.sensitiveToolPatterns` runs on, where the operator wrote the pattern
|
|
109
|
+
* against their own tool names and plain glob semantics are what they expect.
|
|
110
|
+
*
|
|
111
|
+
* Completeness matters here in a way it does not for a display filter: a missed
|
|
112
|
+
* match is a deny-list entry that silently stopped working. So this explores every
|
|
113
|
+
* reachable position per segment (deduplicated, so the work stays bounded by
|
|
114
|
+
* `segments x value.length`) rather than taking the leftmost occurrence greedily,
|
|
115
|
+
* which would report "no match" for a pattern that does match further along.
|
|
116
|
+
*/
|
|
117
|
+
export function threatMatch(pattern, value) {
|
|
118
|
+
if (!pattern.includes("*"))
|
|
119
|
+
return value.includes(pattern);
|
|
120
|
+
const parts = pattern.split("*");
|
|
121
|
+
const openStart = parts[0] === "";
|
|
122
|
+
const openEnd = parts[parts.length - 1] === "";
|
|
123
|
+
const segments = parts.filter((s) => s !== "");
|
|
124
|
+
if (segments.length === 0)
|
|
125
|
+
return true;
|
|
126
|
+
// Reachable end positions after matching the segments consumed so far.
|
|
127
|
+
let ends = [0];
|
|
128
|
+
for (let i = 0; i < segments.length; i++) {
|
|
129
|
+
const seg = segments[i];
|
|
130
|
+
// The first segment is anchored at 0 unless the pattern opened with `*`.
|
|
131
|
+
const unbounded = i === 0 ? openStart : false;
|
|
132
|
+
const next = new Set();
|
|
133
|
+
for (const from of ends) {
|
|
134
|
+
const limit = unbounded ? value.length : Math.min(value.length, from + THREAT_MAX_GAP + seg.length);
|
|
135
|
+
let at = from;
|
|
136
|
+
for (;;) {
|
|
137
|
+
const found = value.indexOf(seg, at);
|
|
138
|
+
if (found < 0 || found + seg.length > limit)
|
|
139
|
+
break;
|
|
140
|
+
if (i === 0 && !openStart && found !== 0)
|
|
141
|
+
break;
|
|
142
|
+
if (startsOnBoundary(seg, value, found))
|
|
143
|
+
next.add(found + seg.length);
|
|
144
|
+
at = found + 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (next.size === 0)
|
|
148
|
+
return false;
|
|
149
|
+
ends = [...next];
|
|
150
|
+
}
|
|
151
|
+
return openEnd || ends.some((e) => e === value.length);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Is `seg` at `at` starting on a word boundary?
|
|
155
|
+
*
|
|
156
|
+
* Only asked when the segment's own first character is a word character — a
|
|
157
|
+
* pattern starting with `~`, `.` or `/` has no boundary to respect.
|
|
158
|
+
*/
|
|
159
|
+
function startsOnBoundary(seg, value, at) {
|
|
160
|
+
if (!isWordChar(seg.charCodeAt(0)))
|
|
161
|
+
return true;
|
|
162
|
+
if (at === 0)
|
|
163
|
+
return true;
|
|
164
|
+
return !isWordChar(value.charCodeAt(at - 1));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Letters and digits only — `_` and `-` are BOUNDARIES here, not word characters.
|
|
168
|
+
*
|
|
169
|
+
* Tool schemas are written in snake_case and kebab-case, so a field named
|
|
170
|
+
* `seed_phrase` must still satisfy `*seed*phrase*`; treating `_` as a word
|
|
171
|
+
* character silently switched that built-in off. "re**funds**" is still rejected,
|
|
172
|
+
* because there the preceding character is a letter.
|
|
173
|
+
*/
|
|
174
|
+
function isWordChar(c) {
|
|
175
|
+
return ((c >= 0x30 && c <= 0x39) || // 0-9
|
|
176
|
+
(c >= 0x41 && c <= 0x5a) || // A-Z
|
|
177
|
+
(c >= 0x61 && c <= 0x7a) // a-z
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=glob.js.map
|
package/dist/glob.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { McpServerRef, PinStore, ToolDef, WardenGate, WardenLogger, WardenPolicy, WardenVerdict } from "./types.js";
|
|
2
2
|
import { ThreatFeed } from "./threat-feed.js";
|
|
3
3
|
export { StaticScanGate, staticScanRuleset, staticScanRulesetRef, STATIC_SCAN_RULESET_VERSION, } from "./static-scan.js";
|
|
4
|
-
export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
|
|
4
|
+
export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, serverIdentityHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
|
|
5
5
|
export { OriginGate } from "./origin.js";
|
|
6
6
|
export { ThreatFeed, ThreatGate, DEFAULT_FEED_MAX_AGE_MS, FEED_CLOCK_SKEW_MS } from "./threat-feed.js";
|
|
7
7
|
export { EgressGuard, isSensitiveTool, classifyTools } from "./sandbox.js";
|
|
@@ -10,6 +10,8 @@ export type { CanonicalizationCode } from "./jcs.js";
|
|
|
10
10
|
export type { StaticScanRule, StaticScanRuleset } from "./static-scan.js";
|
|
11
11
|
export type { ThreatFeedOptions } from "./threat-feed.js";
|
|
12
12
|
export { silentLogger } from "./logger.js";
|
|
13
|
+
export { displaySafe, DEFAULT_DISPLAY_MAX } from "./sanitize.js";
|
|
14
|
+
export { wildcardMatch } from "./glob.js";
|
|
13
15
|
export * from "./types.js";
|
|
14
16
|
export interface WardenInit {
|
|
15
17
|
gates: WardenGate[];
|
|
@@ -33,10 +35,17 @@ export interface WardenCreateDeps {
|
|
|
33
35
|
*
|
|
34
36
|
* Every MCP server is vetted through an ordered gate chain before its tools are
|
|
35
37
|
* exposed to the agent: cheap static scanning first, then the known-bad threat
|
|
36
|
-
* feed, then
|
|
37
|
-
* gates; the verdict is allow/block
|
|
38
|
-
* per-tool partition so a mostly-trusted
|
|
39
|
-
* quarantined without severing the whole
|
|
38
|
+
* feed, then origin (declared by the operator, or offered by a remote catalog),
|
|
39
|
+
* then drift/pinning. Findings accumulate across gates; the verdict is allow/block
|
|
40
|
+
* plus a composite 0..1 safety score and a per-tool partition so a mostly-trusted
|
|
41
|
+
* server can have one poisoned tool quarantined without severing the whole
|
|
42
|
+
* connection.
|
|
43
|
+
*
|
|
44
|
+
* The score is WARDEN's OWN — the product of what each gate contributed, computed
|
|
45
|
+
* from local facts. No reputation oracle is consulted and no socket is opened while
|
|
46
|
+
* vetting. Slot three used to be a reputation gate that asked an oracle for a score
|
|
47
|
+
* it had no data to compute; it is gone, and test/no-phantom-gate.test.ts fails if
|
|
48
|
+
* anything like it comes back.
|
|
40
49
|
*/
|
|
41
50
|
export declare class Warden {
|
|
42
51
|
private readonly gates;
|
|
@@ -44,11 +53,19 @@ export declare class Warden {
|
|
|
44
53
|
private readonly log;
|
|
45
54
|
constructor(init: WardenInit);
|
|
46
55
|
/**
|
|
47
|
-
* Guard against config typos that silently disable blocking
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* Guard against config typos that silently disable blocking, on a COPY.
|
|
57
|
+
*
|
|
58
|
+
* If `blockAtSeverity` isn't a valid Severity key, `SEVERITY_RANK[bad]` is
|
|
59
|
+
* undefined and every comparison `number >= undefined` is false — zero blocks,
|
|
60
|
+
* zero warnings — so the value is repaired. Repairing it in place used to write
|
|
61
|
+
* into the host's own policy object, which is both a surprise (their config
|
|
62
|
+
* silently changes under them, and any other Warden sharing the object sees it)
|
|
63
|
+
* and a crash: `Object.freeze(policy)` is a reasonable thing for a host to do,
|
|
64
|
+
* and assigning to a frozen property throws a TypeError out of the constructor
|
|
65
|
+
* under ESM strict mode. A firewall that refuses to be constructed because the
|
|
66
|
+
* config was immutable is not a firewall.
|
|
50
67
|
*/
|
|
51
|
-
private
|
|
68
|
+
private normalizePolicy;
|
|
52
69
|
/** Build the standard gate chain: static → threat → origin → pinning. */
|
|
53
70
|
static create(deps: WardenCreateDeps): Warden;
|
|
54
71
|
/**
|
|
@@ -57,6 +74,22 @@ export declare class Warden {
|
|
|
57
74
|
* is fatal or any finding reaches policy.blockAtSeverity.
|
|
58
75
|
*/
|
|
59
76
|
vet(server: McpServerRef, tools: ToolDef[]): Promise<WardenVerdict>;
|
|
77
|
+
/**
|
|
78
|
+
* Run one gate, converting a thrown error into a blocking finding.
|
|
79
|
+
*
|
|
80
|
+
* A gate that crashed did not clear the server, so the honest result is a
|
|
81
|
+
* finding at `high` with a zero score — which blocks at the default threshold
|
|
82
|
+
* and, under a report-only policy (`blockAtSeverity: "critical"`), is reported
|
|
83
|
+
* without blocking, exactly like every other high-severity finding.
|
|
84
|
+
*
|
|
85
|
+
* Letting the exception escape instead was worse than it looks: `vet()`
|
|
86
|
+
* rejected, so the host got no verdict at all — not the findings the earlier
|
|
87
|
+
* gates had already produced, not the per-tool partition — and whether that
|
|
88
|
+
* ended in a blocked connection or an unhandled rejection was up to the host.
|
|
89
|
+
* A store with a full disk or a custom gate with a bug should not be able to
|
|
90
|
+
* decide that.
|
|
91
|
+
*/
|
|
92
|
+
private runGate;
|
|
60
93
|
/**
|
|
61
94
|
* Record the user's approval: pin the current tool defs so future drift is
|
|
62
95
|
* detected. Idempotent — re-approving just refreshes the snapshot.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,QAAQ,EAER,OAAO,EAEP,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,QAAQ,EAER,OAAO,EAEP,UAAU,EAEV,YAAY,EACZ,YAAY,EACZ,aAAa,EACd,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AAK1D,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAClI,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACvG,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACvG,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,cAAc,YAAY,CAAC;AAU3B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,kEAAkE;IAClE,GAAG,CAAC,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,YAAY,CAAC;IACrB;;;;OAIG;IACH,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,CAAC,EAAE,YAAY,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,IAAI,EAAE,UAAU;IAM5B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,eAAe;IAWvB,yEAAyE;IACzE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM;IAU7C;;;;OAIG;IACG,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IAqDzE;;;;;;;;;;;;;;OAcG;YACW,OAAO;IAiCrB;;;OAGG;IACG,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAUpE;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,cAAc;CAGvB"}
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,16 @@ import { OriginGate } from "./origin.js";
|
|
|
4
4
|
import { ThreatGate } from "./threat-feed.js";
|
|
5
5
|
import { classifyTools } from "./sandbox.js";
|
|
6
6
|
import { silentLogger } from "./logger.js";
|
|
7
|
+
import { displaySafe } from "./sanitize.js";
|
|
7
8
|
export { StaticScanGate, staticScanRuleset, staticScanRulesetRef, STATIC_SCAN_RULESET_VERSION, } from "./static-scan.js";
|
|
8
|
-
export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
|
|
9
|
+
export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, serverIdentityHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
|
|
9
10
|
export { OriginGate } from "./origin.js";
|
|
10
11
|
export { ThreatFeed, ThreatGate, DEFAULT_FEED_MAX_AGE_MS, FEED_CLOCK_SKEW_MS } from "./threat-feed.js";
|
|
11
12
|
export { EgressGuard, isSensitiveTool, classifyTools } from "./sandbox.js";
|
|
12
13
|
export { canonicalize, parseJsonStrict, CanonicalizationError, MAX_SAFE_JSON_INTEGER } from "./jcs.js";
|
|
13
14
|
export { silentLogger } from "./logger.js";
|
|
15
|
+
export { displaySafe, DEFAULT_DISPLAY_MAX } from "./sanitize.js";
|
|
16
|
+
export { wildcardMatch } from "./glob.js";
|
|
14
17
|
export * from "./types.js";
|
|
15
18
|
const SEVERITY_RANK = {
|
|
16
19
|
info: 0,
|
|
@@ -24,39 +27,53 @@ const SEVERITY_RANK = {
|
|
|
24
27
|
*
|
|
25
28
|
* Every MCP server is vetted through an ordered gate chain before its tools are
|
|
26
29
|
* exposed to the agent: cheap static scanning first, then the known-bad threat
|
|
27
|
-
* feed, then
|
|
28
|
-
* gates; the verdict is allow/block
|
|
29
|
-
* per-tool partition so a mostly-trusted
|
|
30
|
-
* quarantined without severing the whole
|
|
30
|
+
* feed, then origin (declared by the operator, or offered by a remote catalog),
|
|
31
|
+
* then drift/pinning. Findings accumulate across gates; the verdict is allow/block
|
|
32
|
+
* plus a composite 0..1 safety score and a per-tool partition so a mostly-trusted
|
|
33
|
+
* server can have one poisoned tool quarantined without severing the whole
|
|
34
|
+
* connection.
|
|
35
|
+
*
|
|
36
|
+
* The score is WARDEN's OWN — the product of what each gate contributed, computed
|
|
37
|
+
* from local facts. No reputation oracle is consulted and no socket is opened while
|
|
38
|
+
* vetting. Slot three used to be a reputation gate that asked an oracle for a score
|
|
39
|
+
* it had no data to compute; it is gone, and test/no-phantom-gate.test.ts fails if
|
|
40
|
+
* anything like it comes back.
|
|
31
41
|
*/
|
|
32
42
|
export class Warden {
|
|
33
43
|
gates;
|
|
34
44
|
policy;
|
|
35
45
|
log;
|
|
36
46
|
constructor(init) {
|
|
37
|
-
this.gates = init.gates;
|
|
38
|
-
this.policy = init.policy;
|
|
47
|
+
this.gates = [...init.gates];
|
|
39
48
|
this.log = (init.log ?? silentLogger()).child("warden");
|
|
40
|
-
this.
|
|
49
|
+
this.policy = this.normalizePolicy(init.policy);
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
43
|
-
* Guard against config typos that silently disable blocking
|
|
44
|
-
*
|
|
45
|
-
*
|
|
52
|
+
* Guard against config typos that silently disable blocking, on a COPY.
|
|
53
|
+
*
|
|
54
|
+
* If `blockAtSeverity` isn't a valid Severity key, `SEVERITY_RANK[bad]` is
|
|
55
|
+
* undefined and every comparison `number >= undefined` is false — zero blocks,
|
|
56
|
+
* zero warnings — so the value is repaired. Repairing it in place used to write
|
|
57
|
+
* into the host's own policy object, which is both a surprise (their config
|
|
58
|
+
* silently changes under them, and any other Warden sharing the object sees it)
|
|
59
|
+
* and a crash: `Object.freeze(policy)` is a reasonable thing for a host to do,
|
|
60
|
+
* and assigning to a frozen property throws a TypeError out of the constructor
|
|
61
|
+
* under ESM strict mode. A firewall that refuses to be constructed because the
|
|
62
|
+
* config was immutable is not a firewall.
|
|
46
63
|
*/
|
|
47
|
-
|
|
64
|
+
normalizePolicy(policy) {
|
|
48
65
|
const valid = ["info", "low", "medium", "high", "critical"];
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
66
|
+
if (valid.includes(policy.blockAtSeverity))
|
|
67
|
+
return { ...policy };
|
|
68
|
+
const fallback = "high";
|
|
69
|
+
this.log.warn(`WARDEN: invalid blockAtSeverity "${displaySafe(policy.blockAtSeverity, 40)}" ` +
|
|
70
|
+
`(expected one of ${valid.join(", ")}) — falling back to "${fallback}" to keep blocking enabled`);
|
|
71
|
+
return { ...policy, blockAtSeverity: fallback };
|
|
55
72
|
}
|
|
56
73
|
/** Build the standard gate chain: static → threat → origin → pinning. */
|
|
57
74
|
static create(deps) {
|
|
58
75
|
const gates = [
|
|
59
|
-
new StaticScanGate(),
|
|
76
|
+
new StaticScanGate(deps.log),
|
|
60
77
|
new ThreatGate(deps.threatFeed),
|
|
61
78
|
new OriginGate(),
|
|
62
79
|
new PinningGate(deps.store),
|
|
@@ -75,12 +92,7 @@ export class Warden {
|
|
|
75
92
|
let decidedBy;
|
|
76
93
|
const blockThreshold = SEVERITY_RANK[this.policy.blockAtSeverity];
|
|
77
94
|
for (const gate of this.gates) {
|
|
78
|
-
const result = await
|
|
79
|
-
server,
|
|
80
|
-
tools,
|
|
81
|
-
prior: [...findings],
|
|
82
|
-
policy: this.policy,
|
|
83
|
-
});
|
|
95
|
+
const result = await this.runGate(gate, server, tools, findings);
|
|
84
96
|
findings.push(...result.findings);
|
|
85
97
|
scores.push(clamp01(result.score));
|
|
86
98
|
// Advisory findings never block, at any threshold — see WardenFinding.advisory.
|
|
@@ -91,7 +103,7 @@ export class Warden {
|
|
|
91
103
|
decidedBy = gate.name;
|
|
92
104
|
}
|
|
93
105
|
if (result.fatal) {
|
|
94
|
-
this.log.warn(`gate "${gate.name}" returned fatal for server ${server.id}`);
|
|
106
|
+
this.log.warn(`gate "${displaySafe(gate.name, 60)}" returned fatal for server ${displaySafe(server.id)}`);
|
|
95
107
|
break; // short-circuit only on an explicit fatal
|
|
96
108
|
}
|
|
97
109
|
}
|
|
@@ -100,10 +112,11 @@ export class Warden {
|
|
|
100
112
|
const score = scores.reduce((acc, s) => acc * s, 1);
|
|
101
113
|
const { allowedTools, blockedTools } = this.partitionTools(tools, findings, blockThreshold);
|
|
102
114
|
if (!allow) {
|
|
103
|
-
this.log.warn(`BLOCK ${server.id} (decidedBy=${decidedBy
|
|
115
|
+
this.log.warn(`BLOCK ${displaySafe(server.id)} (decidedBy=${displaySafe(decidedBy ?? "-", 60)}, ` +
|
|
116
|
+
`score=${score.toFixed(3)}, findings=${findings.length})`);
|
|
104
117
|
}
|
|
105
118
|
else {
|
|
106
|
-
this.log.info(`ALLOW ${server.id} (score=${score.toFixed(3)}, sensitive=${blockedTools.length === 0 ? this.sensitiveCount(tools) : "?"})`);
|
|
119
|
+
this.log.info(`ALLOW ${displaySafe(server.id)} (score=${score.toFixed(3)}, sensitive=${blockedTools.length === 0 ? this.sensitiveCount(tools) : "?"})`);
|
|
107
120
|
}
|
|
108
121
|
return {
|
|
109
122
|
allow,
|
|
@@ -115,6 +128,48 @@ export class Warden {
|
|
|
115
128
|
rulesets: { staticScan: staticScanRulesetRef() },
|
|
116
129
|
};
|
|
117
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Run one gate, converting a thrown error into a blocking finding.
|
|
133
|
+
*
|
|
134
|
+
* A gate that crashed did not clear the server, so the honest result is a
|
|
135
|
+
* finding at `high` with a zero score — which blocks at the default threshold
|
|
136
|
+
* and, under a report-only policy (`blockAtSeverity: "critical"`), is reported
|
|
137
|
+
* without blocking, exactly like every other high-severity finding.
|
|
138
|
+
*
|
|
139
|
+
* Letting the exception escape instead was worse than it looks: `vet()`
|
|
140
|
+
* rejected, so the host got no verdict at all — not the findings the earlier
|
|
141
|
+
* gates had already produced, not the per-tool partition — and whether that
|
|
142
|
+
* ended in a blocked connection or an unhandled rejection was up to the host.
|
|
143
|
+
* A store with a full disk or a custom gate with a bug should not be able to
|
|
144
|
+
* decide that.
|
|
145
|
+
*/
|
|
146
|
+
async runGate(gate, server, tools, prior) {
|
|
147
|
+
try {
|
|
148
|
+
const result = await gate.evaluate({ server, tools, prior: [...prior], policy: this.policy });
|
|
149
|
+
// A third-party gate is not obliged to be well-behaved either.
|
|
150
|
+
return {
|
|
151
|
+
findings: Array.isArray(result?.findings) ? result.findings : [],
|
|
152
|
+
score: typeof result?.score === "number" ? result.score : 0,
|
|
153
|
+
...(result?.fatal ? { fatal: true } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
158
|
+
this.log.error(`gate "${displaySafe(gate.name, 60)}" threw: ${displaySafe(detail, 300)}`);
|
|
159
|
+
return {
|
|
160
|
+
findings: [
|
|
161
|
+
{
|
|
162
|
+
gate: gate.name,
|
|
163
|
+
severity: "high",
|
|
164
|
+
code: "GATE_ERROR",
|
|
165
|
+
message: `Gate "${displaySafe(gate.name, 60)}" failed to complete (${displaySafe(detail, 300)}), so this ` +
|
|
166
|
+
`server was not checked by it.`,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
score: 0,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
118
173
|
/**
|
|
119
174
|
* Record the user's approval: pin the current tool defs so future drift is
|
|
120
175
|
* detected. Idempotent — re-approving just refreshes the snapshot.
|
|
@@ -126,7 +181,7 @@ export class Warden {
|
|
|
126
181
|
return;
|
|
127
182
|
}
|
|
128
183
|
await pinning.pin(server, tools);
|
|
129
|
-
this.log.info(`pinned tool defs for ${server.id} (${tools.length} tools)`);
|
|
184
|
+
this.log.info(`pinned tool defs for ${displaySafe(server.id)} (${tools.length} tools)`);
|
|
130
185
|
}
|
|
131
186
|
/**
|
|
132
187
|
* A tool is blocked if a finding naming it reaches the block threshold.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/origin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"origin.d.ts","sourceRoot":"","sources":["../src/origin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"origin.d.ts","sourceRoot":"","sources":["../src/origin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,UAAU,EACV,eAAe,EACf,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,UAAW,YAAW,UAAU;IAC3C,QAAQ,CAAC,IAAI,YAAY;IAGnB,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAyBlE"}
|
package/dist/origin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { displaySafe } from "./sanitize.js";
|
|
1
2
|
/**
|
|
2
3
|
* Origin gate — enforces `policy.allowUnknownServers` against where a server
|
|
3
4
|
* declaration came from.
|
|
@@ -27,14 +28,18 @@ export class OriginGate {
|
|
|
27
28
|
return { findings: [], score: 1 };
|
|
28
29
|
}
|
|
29
30
|
const strict = input.policy.allowUnknownServers !== true;
|
|
31
|
+
// Both the id and the catalog name can come from a remote catalog entry, and
|
|
32
|
+
// this message is printed to the operator's terminal.
|
|
33
|
+
const id = displaySafe(input.server.id);
|
|
34
|
+
const from = displaySafe(catalog);
|
|
30
35
|
const finding = {
|
|
31
36
|
gate: this.name,
|
|
32
37
|
severity: strict ? "high" : "info",
|
|
33
38
|
code: "SERVER_UNDECLARED",
|
|
34
39
|
message: strict
|
|
35
|
-
? `Server "${
|
|
40
|
+
? `Server "${id}" was discovered from catalog "${from}" and is not declared under mcp.servers; ` +
|
|
36
41
|
`policy forbids undeclared servers (allowUnknownServers=false) — blocking. Add it to mcp.servers to allow it.`
|
|
37
|
-
: `Server "${
|
|
42
|
+
: `Server "${id}" was discovered from catalog "${from}", not declared under mcp.servers.`,
|
|
38
43
|
};
|
|
39
44
|
// Catalog provenance on its own is not a defect, so the permissive path does
|
|
40
45
|
// not tax the composite score; it only says where the server came from.
|
package/dist/origin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"origin.js","sourceRoot":"","sources":["../src/origin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"origin.js","sourceRoot":"","sources":["../src/origin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAQ5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,UAAU;IACZ,IAAI,GAAG,QAAQ,CAAC;IAEzB,uFAAuF;IACvF,KAAK,CAAC,QAAQ,CAAC,KAAsB;QACnC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC;QACrC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,iEAAiE;YACjE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QACpC,CAAC;QAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,mBAAmB,KAAK,IAAI,CAAC;QACzD,6EAA6E;QAC7E,sDAAsD;QACtD,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,OAAO,GAAkB;YAC7B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM;YAClC,IAAI,EAAE,mBAAmB;YACzB,OAAO,EAAE,MAAM;gBACb,CAAC,CAAC,WAAW,EAAE,kCAAkC,IAAI,2CAA2C;oBAC9F,8GAA8G;gBAChH,CAAC,CAAC,WAAW,EAAE,kCAAkC,IAAI,oCAAoC;SAC5F,CAAC;QACF,6EAA6E;QAC7E,wEAAwE;QACxE,OAAO,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACvE,CAAC;CACF"}
|
package/dist/pinning.d.ts
CHANGED
|
@@ -14,6 +14,13 @@ export declare class PinningGate implements WardenGate {
|
|
|
14
14
|
readonly name = "pinning";
|
|
15
15
|
constructor(store: PinStore);
|
|
16
16
|
evaluate(input: WardenGateInput): Promise<WardenGateResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Has the program behind this server changed since approval?
|
|
19
|
+
*
|
|
20
|
+
* Silent when the pin predates {@link PinnedServer.identityHash}: an absent
|
|
21
|
+
* value is "not recorded", never "changed". The next `approve()` records it.
|
|
22
|
+
*/
|
|
23
|
+
private identityDrift;
|
|
17
24
|
/**
|
|
18
25
|
* The tool-def set has no canonical form (see {@link canonicalToolsHash}), so no
|
|
19
26
|
* hash can be produced for it. What that means depends on whether a pin exists:
|
|
@@ -70,4 +77,15 @@ export declare function canonicalToolsHash(tools: ToolDef[]): string;
|
|
|
70
77
|
* throwing, for call sites that record a hash rather than enforce one.
|
|
71
78
|
*/
|
|
72
79
|
export declare function tryCanonicalToolsHash(tools: ToolDef[]): string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* sha256 over the server's launch identity: transport, command, args, url, name.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately NOT the whole `McpServerRef`: `catalog` is provenance rather than
|
|
84
|
+
* identity (the origin gate is what has an opinion about it), and `env` holds
|
|
85
|
+
* secrets that must not be hashed into a value stored on disk next to the pin.
|
|
86
|
+
*
|
|
87
|
+
* Missing fields serialize as empty rather than being omitted, so a server that
|
|
88
|
+
* gains a `url` reads as a change instead of hashing the same as before.
|
|
89
|
+
*/
|
|
90
|
+
export declare function serverIdentityHash(server: McpServerRef): string;
|
|
73
91
|
//# sourceMappingURL=pinning.d.ts.map
|
package/dist/pinning.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinning.d.ts","sourceRoot":"","sources":["../src/pinning.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pinning.d.ts","sourceRoot":"","sources":["../src/pinning.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,YAAY,EACZ,QAAQ,EAER,OAAO,EAEP,UAAU,EACV,eAAe,EACf,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;GASG;AACH,qBAAa,WAAY,YAAW,UAAU;IAGhC,OAAO,CAAC,QAAQ,CAAC,KAAK;IAFlC,QAAQ,CAAC,IAAI,aAAa;gBAEG,KAAK,EAAE,QAAQ;IAEtC,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAqDjE;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAerB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,WAAW;IAgCnB;;;;;;;OAOG;IACG,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAUjE;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,wCAAwC,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAS3D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,GAAG,SAAS,CAO1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAS/D"}
|