@aimarket/warden 0.0.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/LICENSE +21 -0
  3. package/README-es.md +190 -0
  4. package/README-fr.md +190 -0
  5. package/README-ru.md +189 -0
  6. package/README-zh.md +173 -0
  7. package/README.md +179 -6
  8. package/dist/glob.d.ts +81 -0
  9. package/dist/glob.d.ts.map +1 -0
  10. package/dist/glob.js +180 -0
  11. package/dist/glob.js.map +1 -0
  12. package/dist/index.d.ts +106 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +218 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/jcs.d.ts +86 -0
  17. package/dist/jcs.d.ts.map +1 -0
  18. package/dist/jcs.js +296 -0
  19. package/dist/jcs.js.map +1 -0
  20. package/dist/logger.d.ts +10 -0
  21. package/dist/logger.d.ts.map +1 -0
  22. package/dist/logger.js +19 -0
  23. package/dist/logger.js.map +1 -0
  24. package/dist/origin.d.ts +25 -0
  25. package/dist/origin.d.ts.map +1 -0
  26. package/dist/origin.js +49 -0
  27. package/dist/origin.js.map +1 -0
  28. package/dist/pinning.d.ts +91 -0
  29. package/dist/pinning.d.ts.map +1 -0
  30. package/dist/pinning.js +227 -0
  31. package/dist/pinning.js.map +1 -0
  32. package/dist/sandbox.d.ts +38 -0
  33. package/dist/sandbox.d.ts.map +1 -0
  34. package/dist/sandbox.js +79 -0
  35. package/dist/sandbox.js.map +1 -0
  36. package/dist/sanitize.d.ts +38 -0
  37. package/dist/sanitize.d.ts.map +1 -0
  38. package/dist/sanitize.js +53 -0
  39. package/dist/sanitize.js.map +1 -0
  40. package/dist/static-scan.d.ts +110 -0
  41. package/dist/static-scan.d.ts.map +1 -0
  42. package/dist/static-scan.js +539 -0
  43. package/dist/static-scan.js.map +1 -0
  44. package/dist/threat-feed.d.ts +138 -0
  45. package/dist/threat-feed.d.ts.map +1 -0
  46. package/dist/threat-feed.js +546 -0
  47. package/dist/threat-feed.js.map +1 -0
  48. package/dist/types.d.ts +173 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +11 -0
  51. package/dist/types.js.map +1 -0
  52. package/package.json +67 -9
  53. package/index.js +0 -6
package/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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,KAAa;IAC1D,MAAM,IAAI,GAAG,IAAI,CAAC;IAClB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC;IACd,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,CAAC,EAAE,CAAC;YACJ,CAAC,EAAE,CAAC;QACN,CAAC;aAAM,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAChE,2DAA2D;YAC3D,IAAI,GAAG,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;YACrB,yEAAyE;YACzE,oEAAoE;YACpE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;YACb,CAAC,GAAG,EAAE,MAAM,CAAC;QACf,CAAC;aAAM,CAAC;YACN,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,0DAA0D;IAC1D,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,CAAC,EAAE,CAAC;IACjE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC;AAC9B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,wDAAwD;AACxD,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,CAAC,EAAE,CAAC;IAC1C,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,KAAa;IACxD,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAE3D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC;IAC/C,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,uEAAuE;IACvE,IAAI,IAAI,GAAa,CAAC,CAAC,CAAC,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC;QACzB,yEAAyE;QACzE,MAAM,SAAS,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;QAC9C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,GAAG,cAAc,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;YACpG,IAAI,EAAE,GAAG,IAAI,CAAC;YACd,SAAS,CAAC;gBACR,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACrC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,GAAG,CAAC,MAAM,GAAG,KAAK;oBAAE,MAAM;gBACnD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,KAAK,KAAK,CAAC;oBAAE,MAAM;gBAChD,IAAI,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC;oBAAE,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;gBACtE,EAAE,GAAG,KAAK,GAAG,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAClC,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,OAAO,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,GAAW,EAAE,KAAa,EAAE,EAAU;IAC9D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,EAAE,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1B,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CACL,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,MAAM;QAClC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,MAAM;QAClC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM;KAChC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,106 @@
1
+ import type { McpServerRef, PinStore, ToolDef, WardenGate, WardenLogger, WardenPolicy, WardenVerdict } from "./types.js";
2
+ import { ThreatFeed } from "./threat-feed.js";
3
+ export { StaticScanGate, staticScanRuleset, staticScanRulesetRef, STATIC_SCAN_RULESET_VERSION, } from "./static-scan.js";
4
+ export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, serverIdentityHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
5
+ export { OriginGate } from "./origin.js";
6
+ export { ThreatFeed, ThreatGate, DEFAULT_FEED_MAX_AGE_MS, FEED_CLOCK_SKEW_MS } from "./threat-feed.js";
7
+ export { EgressGuard, isSensitiveTool, classifyTools } from "./sandbox.js";
8
+ export { canonicalize, parseJsonStrict, CanonicalizationError, MAX_SAFE_JSON_INTEGER } from "./jcs.js";
9
+ export type { CanonicalizationCode } from "./jcs.js";
10
+ export type { StaticScanRule, StaticScanRuleset } from "./static-scan.js";
11
+ export type { ThreatFeedOptions } from "./threat-feed.js";
12
+ export { silentLogger } from "./logger.js";
13
+ export { displaySafe, DEFAULT_DISPLAY_MAX } from "./sanitize.js";
14
+ export { wildcardMatch } from "./glob.js";
15
+ export * from "./types.js";
16
+ export interface WardenInit {
17
+ gates: WardenGate[];
18
+ policy: WardenPolicy;
19
+ /** Where decisions are reported. Defaults to `silentLogger()`. */
20
+ log?: WardenLogger;
21
+ }
22
+ export interface WardenCreateDeps {
23
+ store: PinStore;
24
+ policy: WardenPolicy;
25
+ /**
26
+ * The known-bad feed. Required, not defaulted: a `ThreatFeed` the host never
27
+ * loaded still carries the built-in floor, but silently constructing one here
28
+ * would hide from the host that no external intel is in play.
29
+ */
30
+ threatFeed: ThreatFeed;
31
+ log?: WardenLogger;
32
+ }
33
+ /**
34
+ * WARDEN — the MCP security firewall.
35
+ *
36
+ * Every MCP server is vetted through an ordered gate chain before its tools are
37
+ * exposed to the agent: cheap static scanning first, then the known-bad threat
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.
49
+ */
50
+ export declare class Warden {
51
+ private readonly gates;
52
+ private readonly policy;
53
+ private readonly log;
54
+ constructor(init: WardenInit);
55
+ /**
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.
67
+ */
68
+ private normalizePolicy;
69
+ /** Build the standard gate chain: static → threat → origin → pinning. */
70
+ static create(deps: WardenCreateDeps): Warden;
71
+ /**
72
+ * Vet a server and its advertised tools. Runs every gate in order, short-
73
+ * circuiting only on a fatal gate result. A connection is blocked if any gate
74
+ * is fatal or any finding reaches policy.blockAtSeverity.
75
+ */
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;
93
+ /**
94
+ * Record the user's approval: pin the current tool defs so future drift is
95
+ * detected. Idempotent — re-approving just refreshes the snapshot.
96
+ */
97
+ approve(server: McpServerRef, tools: ToolDef[]): Promise<void>;
98
+ /**
99
+ * A tool is blocked if a finding naming it reaches the block threshold.
100
+ * Sensitive tools (per policy) stay allowed but are surfaced as flagged so the
101
+ * agent loop can demand per-call approval at run time.
102
+ */
103
+ private partitionTools;
104
+ private sensitiveCount;
105
+ }
106
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,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 ADDED
@@ -0,0 +1,218 @@
1
+ import { StaticScanGate, staticScanRulesetRef } from "./static-scan.js";
2
+ import { PinningGate } from "./pinning.js";
3
+ import { OriginGate } from "./origin.js";
4
+ import { ThreatGate } from "./threat-feed.js";
5
+ import { classifyTools } from "./sandbox.js";
6
+ import { silentLogger } from "./logger.js";
7
+ import { displaySafe } from "./sanitize.js";
8
+ export { StaticScanGate, staticScanRuleset, staticScanRulesetRef, STATIC_SCAN_RULESET_VERSION, } from "./static-scan.js";
9
+ export { PinningGate, canonicalToolsHash, tryCanonicalToolsHash, serverIdentityHash, UNCANONICAL_TOOLS_HASH } from "./pinning.js";
10
+ export { OriginGate } from "./origin.js";
11
+ export { ThreatFeed, ThreatGate, DEFAULT_FEED_MAX_AGE_MS, FEED_CLOCK_SKEW_MS } from "./threat-feed.js";
12
+ export { EgressGuard, isSensitiveTool, classifyTools } from "./sandbox.js";
13
+ export { canonicalize, parseJsonStrict, CanonicalizationError, MAX_SAFE_JSON_INTEGER } from "./jcs.js";
14
+ export { silentLogger } from "./logger.js";
15
+ export { displaySafe, DEFAULT_DISPLAY_MAX } from "./sanitize.js";
16
+ export { wildcardMatch } from "./glob.js";
17
+ export * from "./types.js";
18
+ const SEVERITY_RANK = {
19
+ info: 0,
20
+ low: 1,
21
+ medium: 2,
22
+ high: 3,
23
+ critical: 4,
24
+ };
25
+ /**
26
+ * WARDEN — the MCP security firewall.
27
+ *
28
+ * Every MCP server is vetted through an ordered gate chain before its tools are
29
+ * exposed to the agent: cheap static scanning first, then the known-bad threat
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.
41
+ */
42
+ export class Warden {
43
+ gates;
44
+ policy;
45
+ log;
46
+ constructor(init) {
47
+ this.gates = [...init.gates];
48
+ this.log = (init.log ?? silentLogger()).child("warden");
49
+ this.policy = this.normalizePolicy(init.policy);
50
+ }
51
+ /**
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.
63
+ */
64
+ normalizePolicy(policy) {
65
+ const valid = ["info", "low", "medium", "high", "critical"];
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 };
72
+ }
73
+ /** Build the standard gate chain: static → threat → origin → pinning. */
74
+ static create(deps) {
75
+ const gates = [
76
+ new StaticScanGate(deps.log),
77
+ new ThreatGate(deps.threatFeed),
78
+ new OriginGate(),
79
+ new PinningGate(deps.store),
80
+ ];
81
+ return new Warden({ gates, policy: deps.policy, log: deps.log });
82
+ }
83
+ /**
84
+ * Vet a server and its advertised tools. Runs every gate in order, short-
85
+ * circuiting only on a fatal gate result. A connection is blocked if any gate
86
+ * is fatal or any finding reaches policy.blockAtSeverity.
87
+ */
88
+ async vet(server, tools) {
89
+ const findings = [];
90
+ const scores = [];
91
+ let allow = true;
92
+ let decidedBy;
93
+ const blockThreshold = SEVERITY_RANK[this.policy.blockAtSeverity];
94
+ for (const gate of this.gates) {
95
+ const result = await this.runGate(gate, server, tools, findings);
96
+ findings.push(...result.findings);
97
+ scores.push(clamp01(result.score));
98
+ // Advisory findings never block, at any threshold — see WardenFinding.advisory.
99
+ const tripped = result.findings.some((f) => !f.advisory && SEVERITY_RANK[f.severity] >= blockThreshold);
100
+ if (result.fatal || tripped) {
101
+ if (allow) {
102
+ allow = false;
103
+ decidedBy = gate.name;
104
+ }
105
+ if (result.fatal) {
106
+ this.log.warn(`gate "${displaySafe(gate.name, 60)}" returned fatal for server ${displaySafe(server.id)}`);
107
+ break; // short-circuit only on an explicit fatal
108
+ }
109
+ }
110
+ }
111
+ // Composite score: product of gate contributions (one bad gate drags it down).
112
+ const score = scores.reduce((acc, s) => acc * s, 1);
113
+ const { allowedTools, blockedTools } = this.partitionTools(tools, findings, blockThreshold);
114
+ if (!allow) {
115
+ this.log.warn(`BLOCK ${displaySafe(server.id)} (decidedBy=${displaySafe(decidedBy ?? "-", 60)}, ` +
116
+ `score=${score.toFixed(3)}, findings=${findings.length})`);
117
+ }
118
+ else {
119
+ this.log.info(`ALLOW ${displaySafe(server.id)} (score=${score.toFixed(3)}, sensitive=${blockedTools.length === 0 ? this.sensitiveCount(tools) : "?"})`);
120
+ }
121
+ return {
122
+ allow,
123
+ score: clamp01(score),
124
+ decidedBy,
125
+ findings,
126
+ allowedTools,
127
+ blockedTools,
128
+ rulesets: { staticScan: staticScanRulesetRef() },
129
+ };
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
+ }
173
+ /**
174
+ * Record the user's approval: pin the current tool defs so future drift is
175
+ * detected. Idempotent — re-approving just refreshes the snapshot.
176
+ */
177
+ async approve(server, tools) {
178
+ const pinning = this.gates.find((g) => g instanceof PinningGate);
179
+ if (!pinning) {
180
+ this.log.warn("approve() called but no PinningGate in the chain; nothing to pin");
181
+ return;
182
+ }
183
+ await pinning.pin(server, tools);
184
+ this.log.info(`pinned tool defs for ${displaySafe(server.id)} (${tools.length} tools)`);
185
+ }
186
+ /**
187
+ * A tool is blocked if a finding naming it reaches the block threshold.
188
+ * Sensitive tools (per policy) stay allowed but are surfaced as flagged so the
189
+ * agent loop can demand per-call approval at run time.
190
+ */
191
+ partitionTools(tools, findings, blockThreshold) {
192
+ const blockedByFinding = new Set();
193
+ for (const f of findings) {
194
+ if (f.advisory)
195
+ continue; // report-only, never costs a tool
196
+ if (f.tool && SEVERITY_RANK[f.severity] >= blockThreshold)
197
+ blockedByFinding.add(f.tool);
198
+ }
199
+ const allowedTools = [];
200
+ const blockedTools = [];
201
+ for (const tool of tools) {
202
+ if (blockedByFinding.has(tool.name))
203
+ blockedTools.push(tool.name);
204
+ else
205
+ allowedTools.push(tool.name);
206
+ }
207
+ return { allowedTools, blockedTools };
208
+ }
209
+ sensitiveCount(tools) {
210
+ return classifyTools(tools, this.policy).sensitive.length;
211
+ }
212
+ }
213
+ function clamp01(n) {
214
+ if (!Number.isFinite(n))
215
+ return 0;
216
+ return Math.max(0, Math.min(1, n));
217
+ }
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/jcs.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * RFC 8785 (JSON Canonicalization Scheme) as profiled by AWR/2 (`awr/SPEC.md` §4).
3
+ *
4
+ * Two places in WARDEN turn a JSON value into bytes that a *different*
5
+ * implementation has to reproduce exactly: the signed threat feed (a publisher
6
+ * signs, ARGUS verifies) and the tool-def pin (a sha256 quoted in receipts and
7
+ * re-checked by `argus verify`). `JSON.stringify` cannot do that job — its output
8
+ * depends on the key order the wire happened to use, so one logical document
9
+ * serialises to different bytes on the two sides and the signature fails for no
10
+ * security reason at all.
11
+ *
12
+ * The profile implemented here, rule by rule:
13
+ *
14
+ * 1. **Property names sort as arrays of UTF-16 code units** (RFC 8785 §3.2.3).
15
+ * JavaScript's default `Array.prototype.sort()` on strings compares exactly
16
+ * that, so this is the one rule the platform gives away for free.
17
+ * `String.prototype.localeCompare` does *not*: it depends on the host locale
18
+ * and ICU version, which makes it unusable as a digest input.
19
+ * 2. **No Unicode normalization, ever** (SPEC §4.1 item 2). NFC would collide two
20
+ * property names that RFC 8785 keeps distinct.
21
+ * 3. **Two-character escapes plus lowercase `\uXXXX` for the remaining C0
22
+ * controls** (§3.2.2.2) — which is precisely what `JSON.stringify` emits for a
23
+ * string, so string bodies are delegated to it rather than re-implemented.
24
+ * 4. **Lone surrogates terminate with an error** instead of being substituted
25
+ * (§4.1 item 4). `JSON.stringify` would quietly escape them into
26
+ * valid-looking bytes, so they are rejected before it is called.
27
+ * 5. **Integers only, within ±(2^53−1)**; every non-integer number is refused
28
+ * rather than arbitrated (§4.3), because whether `1` parses as an integer or
29
+ * as a double is a language accident that silently changes the signed bytes.
30
+ *
31
+ * Scope: this module is *only* the byte-level canonicalization. It knows nothing
32
+ * about AWR documents, Data Integrity proofs, `did:key` or profiles.
33
+ */
34
+ /** Reason codes from the SPEC §11.2 `AWR-CANON-*` registry that this module raises. */
35
+ export type CanonicalizationCode =
36
+ /** Non-integer JSON number present. */
37
+ "AWR-CANON-001"
38
+ /** Integer outside ±(2^53−1). */
39
+ | "AWR-CANON-002"
40
+ /** Invalid Unicode (lone surrogate) in string data. */
41
+ | "AWR-CANON-003"
42
+ /** Duplicate object property name. */
43
+ | "AWR-CANON-004"
44
+ /** Input is not well-formed JSON / not representable in JSON. */
45
+ | "AWR-CANON-005";
46
+ /**
47
+ * A value could not be canonicalized (or parsed) under the AWR profile.
48
+ *
49
+ * Callers are expected to *handle* this rather than let it escape: refusing to
50
+ * produce bytes is the honest outcome, but it must not crash a connection check.
51
+ */
52
+ export declare class CanonicalizationError extends Error {
53
+ readonly code: CanonicalizationCode;
54
+ constructor(code: CanonicalizationCode, detail: string);
55
+ }
56
+ /** Largest magnitude of a JSON integer permitted in canonical bytes (2^53−1). */
57
+ export declare const MAX_SAFE_JSON_INTEGER = 9007199254740991;
58
+ /**
59
+ * Return the RFC 8785 canonical serialization of `value` as a UTF-8-ready string.
60
+ *
61
+ * Throws {@link CanonicalizationError} rather than emitting bytes it cannot
62
+ * defend: a non-integer number, an integer beyond 2^53−1, a lone surrogate, an
63
+ * `undefined` property (which `JSON.stringify` would silently drop), a
64
+ * non-plain object, or a cycle.
65
+ */
66
+ export declare function canonicalize(value: unknown): string;
67
+ /**
68
+ * Parse JSON with the profile's parser requirements, for input whose bytes were
69
+ * signed by someone else.
70
+ *
71
+ * `JSON.parse` alone is not enough on a verification path:
72
+ *
73
+ * - **Duplicate property names** (§4.1 item 5, `AWR-CANON-004`) are resolved
74
+ * last-wins and silently, which would let the parser — not the signer — decide
75
+ * which of two records was the signed one.
76
+ * - **Non-integer number literals** (§4.3, `AWR-CANON-001`) are rejected
77
+ * *lexically*, not by value: `2340.0` and `2340` parse to the same JavaScript
78
+ * number, and a publisher written in a language that distinguishes them would
79
+ * have signed different bytes.
80
+ *
81
+ * Both checks run over the received text after `JSON.parse` has confirmed it is
82
+ * well-formed, so the scanner below only has to find tokens, never validate
83
+ * grammar.
84
+ */
85
+ export declare function parseJsonStrict(text: string): unknown;
86
+ //# sourceMappingURL=jcs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jcs.d.ts","sourceRoot":"","sources":["../src/jcs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,uFAAuF;AACvF,MAAM,MAAM,oBAAoB;AAC9B,uCAAuC;AACrC,eAAe;AACjB,iCAAiC;GAC/B,eAAe;AACjB,uDAAuD;GACrD,eAAe;AACjB,sCAAsC;GACpC,eAAe;AACjB,iEAAiE;GAC/D,eAAe,CAAC;AAEpB;;;;;GAKG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAClC,QAAQ,CAAC,IAAI,EAAE,oBAAoB;gBAA1B,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM;CAIhE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,mBAAmB,CAAC;AAStD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAInD;AA8HD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASrD"}