A tool description is prompt text a third party delivers straight into your model's context, and a schema field named api_key is a request for your secrets phrased as an API. WARDEN vets a server before any of its tools reach the model, and returns a verdict you can record.
WebGL is unavailable here, so the gate chain is not drawn. The table below names all four gates and what each one decides.
Hours after the first release we reached every public MCP server we legitimately could: 2 787 registry entries with a network endpoint, of which 1 108 answered a real tools/list and handed over 17 491 tool definitions. No third-party code was executed — every definition came from the server's own answer.
The headline is not about the ecosystem, it is about us. Ruleset v3 blocked 50 of those servers and only 4 held up on review. The other 46 were blocked for saying the right thing: "Never send a private key", "the private key never leaves your machine", a security scanner listing the attacks it detects, a Persian description spelled with the ZERO WIDTH NON-JOINER its language requires.
| Re-measured on the same 1 108 servers | ruleset v3 | ruleset v4 |
|---|---|---|
| servers blocked | 50 | 6 |
| of those, substantiated | 4 | 4 |
| blocking findings | 492 | 12 |
| advisory findings | 3 472 | 3 494 |
Ruleset v4 gets there with guards — named context checks that are part of the published rule table, and therefore of its digest. polarity alone accounted for 390 of those 492 blocking findings: a credential noun inside a refusal is a promise, not a request. mention knows that a phrase in quotes, in backticks or as a bare JSON enum value is cited rather than said.
Nothing in the chain performs a network request. The only fetch WARDEN ever makes is the threat feed you asked for by passing a URL. The composite score is the product of gate contributions, so one bad gate drags a server down instead of being averaged away.
| Gate | What it decides | Network |
|---|---|---|
| static-scan | Injection, exfiltration, credential requests and hidden-Unicode or base64 payloads in the tool name, its description and its input schema. 25 rules in ruleset v4 — 15 can block, 10 are advisory-only, and 17 also cover the name. | none |
| threat-feed | A known-bad server identity or tool, from 11 built-in records plus an optional signed feed. Fatal for a server-scoped critical. | the feed fetch only |
| origin | Whether the operator declared this server or it arrived from a remote catalog. Fail-closed under allowUnknownServers: false. | none |
| pinning | Whether the tool definitions — and the server's launch identity — still match what the user approved. A catalog can repoint an approved id at a different command; that is drift too. | none |
advise-tier finding is reported and never blocks and never costs a tool, at any threshold — because "how much attention does this deserve" and "is this a defect at all" are different questions, and encoding the second as a low severity made it blocking again for anyone who tightened the threshold.// warden.vet(server, tools) { allow: false, score: 0, decidedBy: "threat-feed", findings: [{ gate, severity, code: "THREAT_TOOL_MATCH", message, tool, advisory? }], allowedTools: ["add"], blockedTools: ["sweeper"], rulesets: { staticScan: { version: "4", digest: "sha256-klRyTiD3…" } } }
rulesets is not decoration. The same server scores differently under a later rule table, and without the version and a digest over the rules there is no way to tell that apart from the server having changed. A stored scan without them is not reproducible — and because guards are part of the table, the same regex with and without polarity is a different ruleset.
A server with one poisoned tool and nine good ones stays usable: only what the verdict named is dropped.
Control and invisible characters in a tool name are escaped in the message — a name carrying ESC[2K used to overwrite the operator's terminal line.
No LLM anywhere in the chain. Same inputs, same verdict, offline, in milliseconds.
WARDEN ships 11 built-in threat records as a floor, and will not read an unsigned remote feed at all. Three properties are checked, and any failure keeps the floor rather than degrading to nothing.
Ed25519 against a key you pinned in advance, over the RFC 8785 canonical form of {records, timestamp} — not over the raw body.
The signed timestamp must be inside the window. A signature says who wrote a document, never when you were handed it.
Canonical bytes, so publisher and verifier agree regardless of JSON key order.
Remote records are appended to the built-ins. A feed can never remove one, so a compromised publisher can only add protection — and the download is bounded and aborted past its cap, because a feed URL is otherwise a memory-exhaustion vector. MOMUS is a reference publisher of the contract.
sandbox-exec — is not here.vet() is fast, offline and deterministic — and why the static scan is regex-shaped and will miss a paraphrase no rule covers.import { Warden, ThreatFeed, silentLogger } from "@aimarket/warden"; const threatFeed = new ThreatFeed({ feedPublicKey: process.env.FEED_PUBKEY }); await threatFeed.load(process.env.FEED_URL); // omit → built-in floor only, no network const pins = new Map(); const warden = Warden.create({ policy: { blockAtSeverity: "high", sensitiveToolPatterns: ["*delete*", "*transfer*", "*key*"], allowUnknownServers: false, // fail-closed: only servers you declared pinToolDefs: true, }, threatFeed, store: { getPin: async (id) => pins.get(id), putPin: async (p) => void pins.set(p.serverId, p) }, log: silentLogger(), }); const verdict = await warden.vet(server, await client.listTools()); if (!verdict.allow) throw new Error(`blocked by ${verdict.decidedBy}`); const usable = verdict.allowedTools; // a poisoned tool can be quarantined alone await warden.approve(server, tools); // pin what the user accepted
Used by ARGUS as its reference host, by MOMUS on the publisher side, and by the AICOM MCP-security course. It is a library, not a service — which is why on the ecosystem map it is drawn as a layer rather than a host.