clembot-doorman 0.1.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 (54) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +951 -0
  4. package/WALKTHROUGH.md +224 -0
  5. package/doorman/.claude/hooks/mcp-gate.sh +205 -0
  6. package/doorman/.claude/settings.json +16 -0
  7. package/doorman/.claude-plugin/plugin.json +22 -0
  8. package/doorman/.mcp.json +24 -0
  9. package/doorman/README.md +259 -0
  10. package/doorman/agents/doorman.md +104 -0
  11. package/doorman/cli/agents.mjs +128 -0
  12. package/doorman/cli/allow.mjs +128 -0
  13. package/doorman/cli/cost.mjs +119 -0
  14. package/doorman/cli/discover.mjs +265 -0
  15. package/doorman/cli/doctor.mjs +282 -0
  16. package/doorman/cli/doorman.mjs +345 -0
  17. package/doorman/cli/eval.mjs +320 -0
  18. package/doorman/cli/harness.mjs +179 -0
  19. package/doorman/cli/install.mjs +175 -0
  20. package/doorman/cli/needs.mjs +116 -0
  21. package/doorman/cli/report.mjs +89 -0
  22. package/doorman/cli/sandbox.mjs +177 -0
  23. package/doorman/cli/task.mjs +239 -0
  24. package/doorman/cli/verdict.mjs +199 -0
  25. package/doorman/cli/watch.mjs +218 -0
  26. package/doorman/commands/doorman.md +116 -0
  27. package/doorman/commands/vet.md +69 -0
  28. package/doorman/hooks/hooks.json +30 -0
  29. package/doorman/install.sh +186 -0
  30. package/doorman/package.json +38 -0
  31. package/doorman/recipes/README.md +36 -0
  32. package/doorman/recipes/deepwiki.md +10 -0
  33. package/doorman/recipes/planted-bad.md +27 -0
  34. package/doorman/recipes/scorecard.md +10 -0
  35. package/doorman/registry/allowlist.json +37 -0
  36. package/doorman/registry/denylist.json +23 -0
  37. package/doorman/registry/ledger.jsonl +1 -0
  38. package/doorman/scripts/poller.mjs +292 -0
  39. package/doorman/scripts/resolve-cli.sh +58 -0
  40. package/doorman/scripts/vet.mjs +190 -0
  41. package/doorman/skills/doorman-guide/SKILL.md +69 -0
  42. package/doorman/src/budget.mjs +236 -0
  43. package/doorman/src/candidate.mjs +132 -0
  44. package/doorman/src/fit-review.mjs +255 -0
  45. package/doorman/src/injection.mjs +189 -0
  46. package/doorman/src/instructions.mjs +134 -0
  47. package/doorman/src/inventory.mjs +411 -0
  48. package/doorman/src/llm.mjs +87 -0
  49. package/doorman/src/needs.mjs +491 -0
  50. package/doorman/src/note.mjs +213 -0
  51. package/doorman/src/reviews.mjs +120 -0
  52. package/doorman/src/scorecard.mjs +123 -0
  53. package/doorman/src/vet.mjs +174 -0
  54. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,951 @@
1
+ <img src="logo.png" alt="Clembot Doorman" width="150" align="right">
2
+
3
+ # Clembot Doorman
4
+
5
+ > **The Package Manager & Security Doorman for Clembot.**
6
+ > Inspect your build. Recommend vetted MCPs from prompt history. Block rogue tools before they reach context.
7
+
8
+ [![Live Explainer & Simulator](https://img.shields.io/badge/Live%20Explainer-clembot--doorman.wanessalabs.com-green.svg)](https://clembot-doorman.wanessalabs.com)
9
+ [![Gateway](https://img.shields.io/badge/Bazantic%20Gateway-active-blue.svg)](https://clembot-doorman.bazgateway.com)
10
+ [![Tests](https://img.shields.io/badge/Tests-403%20passing-brightgreen.svg)](RUNBOOK.md)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
12
+
13
+ ---
14
+
15
+ ## TL;DR · What is Clembot Doorman?
16
+
17
+ **Clembot is built in many versions, with the latest utilizing Claude Code and a custom combination of agent harnesses.** While AI agents need tools to do real work, loading raw MCP servers blindly introduces **in-context steering ads**, prompt injection risks, and token-draining schema hallucination.
18
+
19
+ 1. **Inspect & Recommend (`doorman doctor` & `doorman needs`)**
20
+ Inspects your active build (`doorman doctor`). Reads local prompt history (`~/.claude/projects/`), identifies capability gaps across 12 taxonomies, and recommends safe, pre-graded MCP tools from `scorecard.wanessalabs.com/feed`.
21
+ 2. **Zero-Dependency Offline Security Gate (`PreToolUse` hook)**
22
+ A 180-line offline hook blocks hostile or unapproved tools locally (`exit 2`). Deny beats allow; rogue servers never reach your agent's context window.
23
+ 3. **Arm with Bazantic Recipes (`recipe.md`)**
24
+ Turns raw, unpredictable APIs into bounded, high-performing tools via structured `recipe.md` guidance. Built and verified against the Bazantic gateway (`clembot-doorman.bazgateway.com`).
25
+
26
+ ![The doorman declining a server it just graded F](media/doorman-decline.gif)
27
+
28
+ *A real run against a real server. The grade, the hard fail, and the arithmetic are live values, not a mockup: `webzum.com/api/mcp` scores **89.9% on configuration** and still fails, because one tool description injects **6,290 characters of unprompted upsell and competitor steering** directly into your agent's context window. One cent through the Bazantic gateway answered a question that would have cost $54.47 to measure in an unguided LLM loop. [Run it live in the simulator](https://clembot-doorman.wanessalabs.com/#flow).*
29
+
30
+ ---
31
+
32
+ ## What this is for
33
+
34
+ Doorman measures whether a candidate tool actually helps **your** agent, and gives you a report about **your** build.
35
+
36
+ That emphasis is the whole design. A benchmark someone else ran tells you whether a tool helped *their* agent. Whether it helps yours depends on your harness, your model, your existing servers, and what your agents actually do. Those differ enough that a central verdict is close to meaningless.
37
+
38
+ So this is a thing you install, not a service you ask:
39
+
40
+ - **It runs on your machine.** Local static checks, prompt history parsing, and throwaway Docker sandboxes.
41
+ - **It drives the harness you already run.** Native integration with Claude Code and generic agent harnesses.
42
+ - **It spends from your own account**, bounded by a ceiling you set (`--max-cost`), while the inspection and recommendation layers spend nothing at all.
43
+ - **Nothing is sent to us.** There is no account here to create, no telemetry, and no tracking server in the path. We never see your prompt history or results.
44
+
45
+ The verdict is yours, produced on your machine, from numbers we never receive.
46
+
47
+ ## The Toolchain: Five Layers, Cheapest First
48
+
49
+ | Layer | Command | Needs | Answers |
50
+ |---|---|---|---|
51
+ | **L0** | `doorman doctor` | nothing | **What is in my build?** Which harness, which MCP servers are reachable, how many subagents hold tools, and is the gate wired? |
52
+ | **L0.5** | `doorman needs [path]` | nothing (local history) | **What does my build keep asking for?** Analyzes prompt history (`~/.claude/projects/`), identifies unmet capabilities, and recommends safe, pre-graded tools from the feed. |
53
+ | **L1** | `doorman report <link>` | nothing (read-only) | **Is this candidate safe?** Audits protocol compliance, schema bloat, and scans tool descriptions for in-context steering ads and prompt injection. |
54
+ | **L2** | `PreToolUse hook` (`gate`) | local registry | **Block rogue tools offline.** 180-line zero-dependency hook that intercepts tool calls before LLM execution (`exit 2`). Deny beats allow. |
55
+ | **L3** | `doorman eval <link> --task <f>` | Docker + agent key | **Does this tool actually improve output?** A/B comparative benchmark across two isolated containers, bounded by a strict cost ceiling (`--max-cost`). |
56
+
57
+ **L0, L0.5, and L1 need no model key, no Docker, and no network spend.** Finding out whether a tool is safe, or what your agents are missing, costs nothing.
58
+
59
+ ## What is in this repo
60
+
61
+ | Path | What it is |
62
+ |---|---|
63
+ | `doorman/cli/` | The CLI: `doctor`, `needs`, `report`, `watch`, `eval`. Zero runtime dependencies. |
64
+ | `doorman/` | The gate you install: a 180-line `PreToolUse` hook that blocks unapproved MCP tools, a subagent that vets them, and a registry you own. |
65
+ | `mcp-scorecard/` | The grading service behind L1. Cloudflare Worker + D1 + a local probe runner. |
66
+ | `site/` | The live product & explainer at [clembot-doorman.wanessalabs.com](https://clembot-doorman.wanessalabs.com). |
67
+ | `fixtures/planted-bad-mcp/` | A deliberately hostile MCP server, deployed, so the demo denies something real instead of a line in a JSON file. |
68
+
69
+ ## Quickstart & Installation
70
+
71
+ **The answer depends on your stack, so run it on yours.**
72
+
73
+ There are two halves and you want both. The **plugin** is the gate that blocks
74
+ untrusted servers at the point of use. The **CLI** is the measurement that
75
+ decides what belongs on your trust list. Installing one does not install the
76
+ other.
77
+
78
+ ### 1. The plugin: the gate, `/doorman`, `/vet`, and the subagent
79
+
80
+ ```bash
81
+ claude plugin marketplace add clemenswan/clembot-doorman
82
+ claude plugin install clembot-doorman
83
+ ```
84
+
85
+ Then, in Claude Code:
86
+
87
+ ```
88
+ /doorman
89
+ ```
90
+
91
+ That is the front door. With no arguments it tells you what is gating this
92
+ build, which trust list is actually in force, and how many of the servers it
93
+ trusts were **graded** versus simply allowed by you. Everything else is a branch
94
+ off it: `/doorman allow <server>`, `/doorman check <url>`, `/doorman needs`.
95
+
96
+ > **The gate starts strict, and you should expect to be blocked.** It ships
97
+ > trusting two servers. Anything else, including connectors you already use, is
98
+ > UNKNOWN and fails closed. That is the design: an ungraded server is not a
99
+ > trusted one. When it blocks something, it prints the exact command to allow
100
+ > it.
101
+
102
+ Verify what actually loaded, because a manifest that validates can still ship
103
+ components that never register:
104
+
105
+ ```bash
106
+ claude plugin details clembot-doorman
107
+ # Skills (3) doorman, doorman-guide, vet
108
+ # Agents (1) doorman
109
+ # Hooks (1) PreToolUse
110
+ # MCP servers (1) scorecard
111
+ ```
112
+
113
+ ### 2. The CLI: doctor, needs, report, watch, eval
114
+
115
+ ```bash
116
+ git clone https://github.com/clemenswan/clembot-doorman
117
+ npm i -g ./clembot-doorman
118
+ doorman --version
119
+ ```
120
+
121
+ Node 20+. Zero runtime dependencies, deliberately: every dependency is one more thing that can fail to install on your machine.
122
+
123
+ **New here?** [`WALKTHROUGH.md`](WALKTHROUGH.md) runs the first ten minutes
124
+ against three real builds: one with a long history, one brand new, and one in
125
+ between. The output in it is captured from real runs, not written by hand.
126
+
127
+ ### Core Workflow
128
+
129
+ ```bash
130
+ # 1. What is in YOUR build. Free, local, read-only.
131
+ # No model, no container, no network.
132
+ doorman doctor
133
+
134
+ # 2. What do your prompts keep reaching for?
135
+ # Reads prompt history, identifies capability gaps, recommends vetted tools.
136
+ doorman needs
137
+
138
+ # 3. Grade a candidate server by its implementation before installing.
139
+ # Scans for hidden steering ads, injection patterns, and protocol violations.
140
+ doorman report https://webzum.com/api/mcp
141
+
142
+ # 4. Stream newly graded servers from the feed and flag blocked/unreviewed tools.
143
+ doorman watch --all
144
+
145
+ # 5. Does a candidate actually help YOUR agent? Two sandboxes, identical
146
+ # except one install layer, driving the harness you already run.
147
+ doorman eval npm:some-candidate --task evals/tasks/url-to-note.yaml --agent claude-code --max-cost 2
148
+ ```
149
+
150
+ ### What `doorman doctor` tells you
151
+
152
+ Which harness the project is set up for, every MCP server your agents can reach
153
+ and where each was declared, how many subagents hold MCP tools, and whether the
154
+ gate is installed **and wired**. Those last two are different states, and the
155
+ dangerous one is the middle: a gate that is present but not wired is not
156
+ running, and looks exactly like one that is. Both are quiet.
157
+
158
+ ### Your key, your machine
159
+
160
+ `--agent claude-code` drives the agent you already run. The credential your
161
+ harness already uses is passed straight into a local container. It is never
162
+ written to a file, never logged, and never leaves your machine except to the
163
+ provider you already pay.
164
+
165
+ An adapter that cannot measure something reports it as **not measured** rather
166
+ than estimating it. `--agent exec "<command>"` will drive any harness at all,
167
+ and reports success rate and wall time only, because a command doorman knows
168
+ nothing about cannot be asked how many turns it took.
169
+
170
+ ### The bill is bounded before it starts
171
+
172
+ An agent loop resends the whole conversation every turn, so cost grows with the
173
+ **square** of the turn count. A 24-turn cap authorises far more than it looks
174
+ like: on Sonnet, three runs per arm is **$54 at worst**.
175
+
176
+ So `--max-cost` is the input and the turn cap is **derived from it**. A ceiling
177
+ of $2 is a ceiling of $2. Add `--estimate` to print the worst case and spend
178
+ nothing:
179
+
180
+ ```bash
181
+ doorman eval npm:some-candidate --task <file> --max-cost 2 --estimate
182
+ ```
183
+
184
+ It refuses rather than shaving: a ceiling too small for even a three-turn run
185
+ stops and shows the arithmetic. The permit ledger is the same one the doorman
186
+ uses on its own outbound spend.
187
+
188
+ Fewer than three runs per arm cannot reach ADOPT, because one sample cannot be
189
+ told apart from luck. DECLINE stays reachable at any run count, so a cheap run
190
+ is still worth doing: it can tell you a candidate is bad, just not that one is
191
+ good.
192
+
193
+ ---
194
+
195
+ ## Step-by-Step Developer Walkthrough & Skill Management
196
+
197
+ A complete guide for net-new Claude Code / Clembot builds, prompt history analysis, and managing existing skill rosters.
198
+
199
+ ### 1. Fresh Init Walkthrough (`claude init` → `doorman doctor` → `doorman install`)
200
+
201
+ When you run `claude init` in an empty repository, Claude Code sets up baseline configuration (`CLAUDE.md`, `.claude/settings.json`). Before installing tools, run `doorman doctor`:
202
+
203
+ ```bash
204
+ doorman doctor
205
+ ```
206
+
207
+ **Verbatim output on a fresh build:**
208
+ ```markdown
209
+ # Your build
210
+
211
+ `C:\Users\username\my-new-project`
212
+
213
+ Read-only. Nothing here was executed, sent anywhere, or billed.
214
+
215
+ ## Harness
216
+
217
+ - **Claude Code** (CLAUDE.md)
218
+
219
+ ## MCP servers this project can reach
220
+
221
+ None declared. Nothing to grade yet, and nothing to gate.
222
+
223
+ ## Agents
224
+
225
+ No .claude/agents/ directory.
226
+
227
+ ## The gate
228
+
229
+ **not installed**
230
+
231
+ - hook present: no
232
+ - wired in settings.json: no
233
+ - registry present: no
234
+
235
+ ---
236
+ _doorman doctor. Static, local, free. It reports what is here; it does not
237
+ say whether any of it works. That is doorman report and doorman eval._
238
+ ```
239
+
240
+ To wire the local deterministic gate before adding any external MCP servers:
241
+ ```bash
242
+ doorman install
243
+ ```
244
+ This registers `doorman/.claude/hooks/mcp-gate.sh` into `.claude/settings.json` and creates `registry/allowlist.json`. Unapproved tools or payload injections are stopped at **exit 2** before reaching context.
245
+
246
+ ---
247
+
248
+ ### 2. Elevate Your Build with Prompt Recommendations (`doorman needs`)
249
+
250
+ After working in your project for a few sessions, run `doorman needs`:
251
+
252
+ ```bash
253
+ doorman needs
254
+ ```
255
+
256
+ **How it works:**
257
+ 1. **Local Transcript Ingestion**: Reads `~/.claude/projects/<slug>/*.jsonl`. Filters out tool results, compacted summaries, and slash command templates to isolate the sentences *you actually typed*.
258
+ 2. **12 Capability Taxonomies**: Categorizes asks into `docs-lookup`, `web-search`, `database`, `browser-automation`, `cloud-deploy`, `observability`, `payments`, `comms`, `design-assets`, `knowledge-base`, `code-host`, `data-files`.
259
+ 3. **Public Feed Matching**: Compares unmet capabilities against the free [Scorecard Feed](https://scorecard.wanessalabs.com/feed).
260
+
261
+ **Verbatim output:**
262
+ ```text
263
+ doorman needs — 42 prompts read from this build’s own history
264
+ history: ~/.claude/projects/C--my-new-project
265
+ feed: 24 graded rows
266
+
267
+ UNMET Current documentation for a library it does not know
268
+ 14 prompts across 3 sessions · matched "latest docs", "deepwiki"
269
+ > …can you look up the latest docs for drizzle orm…
270
+ > …check the documentation for cloudflare workers assets…
271
+ worth-measuring DeepWiki MCP [A (85.7)] matched "docs"
272
+ https://mcp.deepwiki.com/mcp
273
+ worth-measuring Cloudflare Docs MCP [A (94.2)] matched "documentation"
274
+ https://docs.mcp.cloudflare.com/mcp
275
+
276
+ UNMET Driving a real browser
277
+ 6 prompts across 2 sessions · matched "screenshot the page", "playwright"
278
+ > …take a screenshot of the landing page at 390px…
279
+ worth-measuring Peek Browser MCP [C (66.7)] matched "screenshot"
280
+ https://mcp.peek.com
281
+
282
+ UNMET Reading the live web
283
+ 5 prompts across 1 sessions · matched "search online"
284
+ > …search online for the error code…
285
+ blocked WebZum Search [F (49.0)] matched "search"
286
+ hard fail: injection-shaped content in tool:host_site.description
287
+ https://webzum.com/api/mcp
288
+
289
+ GAP Production errors and logs
290
+ 3 prompts across 1 sessions · matched "tail the logs"
291
+ > …tail the logs from production…
292
+ nothing graded covers this. The feed has the gap, not your build.
293
+
294
+ 3 unmet, 1 of them with nothing graded to offer.
295
+
296
+ What this is: your own prompts, counted, against capability text those
297
+ candidates published about themselves. Nothing here was driven, so
298
+ nothing here is a claim that a server works. `worth-measuring` means
299
+ exactly that — run `doorman eval` with your key and find out.
300
+ ```
301
+
302
+ - **`worth-measuring`**: Verified Grade A/B servers matching your exact needs.
303
+ - **`blocked`**: Identifies hostile or compromised servers (e.g. WebZum prompt injection).
304
+ - **`GAP`**: Honestly states when the ecosystem has no graded server for that need yet.
305
+
306
+ ---
307
+
308
+ ### 3. What If Your Build Already Has Skills or Tools?
309
+
310
+ Doorman is specifically designed to stop "skill sprawl" and prevent duplicate tools:
311
+
312
+ 1. **Automatic Suppression (`coveredBy(inv)`)**:
313
+ In `doorman needs`, if a capability term matches a tool already declared in `.mcp.json` or `.claude/settings.json`, it labels the need:
314
+ ```text
315
+ COVERED Current documentation for a library it does not know
316
+ already covered by: DeepWiki MCP (mcp__deepwiki_lookup)
317
+ ```
318
+ Candidate recommendations for that need are **suppressed** so your output stays focused on real gaps.
319
+
320
+ 2. **Adverse Drift Detection (`doorman watch`)**:
321
+ When you run `doorman watch`, any candidate server already in your inventory is tagged `already-installed`. If an installed server is downgraded or caught with prompt injection on the feed, `watch` raises an immediate security alert.
322
+
323
+ 3. **Frontmatter Arithmetic (30 KB vs 640 KB)**:
324
+ Doorman reads only YAML frontmatter (`name`, `description`) from `.claude/skills/*/SKILL.md` and `.claude/agents/*.md`. In our production vault, reading full markdown bodies was **642 KB**; reading frontmatter was **30 KB**. This allows the complete roster to be reviewed by a model in a single prompt without bloating context.
325
+
326
+ 4. **The Two-Phase Fit Review (`node scripts/vet.mjs <candidate> --dry-run`)**:
327
+ Before spending any money or tokens on external audits, the Fit Review compares the candidate against your existing skills. If an existing skill already covers it, it returns `REDUNDANT` and halts at Phase 1 ($0.00 spent):
328
+ ```text
329
+ candidate https://github.com/example/git-mcp
330
+ inventory C:\Users\username\my-project — 3 agents, 22 skills, 1 mcp servers
331
+
332
+ FIT REDUNDANT
333
+ Already covered by existing skill: git-pr covers reviewing and merging pull requests.
334
+
335
+ Already covered by:
336
+ - skill git-pr: handles GitHub pull requests and diff review locally
337
+
338
+ STOPPED before the paid grade. $0.00 spent.
339
+ Nothing was sent to the scorecard, and no client was built.
340
+ ```
341
+
342
+ ---
343
+
344
+ ### 4. Running Doorman as an Agent Skill or Slash Command
345
+
346
+ - **As a Slash Command (`/vet <url>`)**: Create `.claude/commands/vet.md` calling `node scripts/vet.mjs $ARG --dry-run`. Type `/vet <url>` directly in your Claude Code chat to run the two-phase check.
347
+ - **As a Dedicated Subagent (`.claude/agents/doorman.md`)**: Sandbox tool evaluation by assigning a dedicated `doorman` agent holding only read tools and the Bazantic Scorecard gateway.
348
+ - **As a Harness Skill (`.claude/skills/doorman/SKILL.md`)**: Equip your agents to run `doorman doctor`, `doorman needs`, or `doorman report` during planning turns before proposing new tool installs.
349
+
350
+ ---
351
+
352
+ ### 5. Implementation Status: How Built Out Is This?
353
+
354
+ | Component | Status | Verification & Evidence |
355
+ |---|---|---|
356
+ | **Static Scanner (`doorman report <url>`)** | **Production Ready** | Live SSE handshake, tool schema linting, 6-pattern injection detection. [Caught WebZum injection](evidence/needs-demo/watch-blocked.txt) on live internet. |
357
+ | **Needs Engine (`doorman needs`)** | **Production Ready** | Parses real `~/.claude/projects/` JSONL prompts, deduplicates resumes, maps to 12 capability taxonomies, matches against [Scorecard Feed](https://scorecard.wanessalabs.com/feed). |
358
+ | **Harness Doctor (`doorman doctor`)** | **Production Ready** | Zero-dependency local scan. Detects Claude Code, Cursor, Windsurf, Copilot, Gemini; audits MCP configs and agent exposure. |
359
+ | **Security Gate (`mcp-gate.sh`)** | **Production Ready** | 180-line offline Bash hook. Passed 29/29 test suites in `test-gate.sh` (blocks unallowlisted tools, prevents shell escapes, enforces 5s timeout). |
360
+ | **Fit Review Engine (`fitReview`)** | **Production Ready** | Compares candidates against `.claude/skills/*/SKILL.md` frontmatter. Enforces temperature 0, strict JSON schema, and hallucination rejection. |
361
+ | **Budget & Spend Ledger** | **Production Ready** | Enforces per-run ($1) and per-day ($5) caps in USDC on Base; auto-releases unspent reserves on error. |
362
+ | **Bazantic x402 Gateway** | **Live in Production** | `clembot-doorman.bazgateway.com` live x402 challenge ($0.01 USDC on Base) + MCP SSE tool stream. |
363
+ | **Multi-Directory Skill Reading** | **Configuration Detail** | Currently looks in `.claude/skills/*/SKILL.md` by default. Set `DOORMAN_INVENTORY_ROOT` for alternate paths like `.agents/skills/`. |
364
+
365
+ ---
366
+
367
+ ## Live
368
+
369
+ | Surface | URL |
370
+ |---|---|
371
+ | Explainer site | https://clembot-doorman.wanessalabs.com |
372
+ | Developer Walkthrough | https://clembot-doorman.wanessalabs.com/guide.html |
373
+ | Scorecard API | https://scorecard.wanessalabs.com |
374
+ | OpenAPI spec | https://scorecard.wanessalabs.com/openapi.json (3.1.0) |
375
+ | Same spec as 3.0.3 | https://scorecard.wanessalabs.com/openapi-3.0.json |
376
+ | End-to-end runbook | [`RUNBOOK.md`](RUNBOOK.md) |
377
+ | Embeddable demo | https://clembot-doorman.wanessalabs.com/embed/flow.html |
378
+ | Where this is headed | https://clembot-doorman.wanessalabs.com/direction.html |
379
+ | Example badge | https://scorecard.wanessalabs.com/badge/https%3A%2F%2Fmcp.deepwiki.com%2Fmcp.svg |
380
+
381
+ Two real production audits, both queued through the API, claimed by a laptop
382
+ runner, graded, posted back:
383
+
384
+ | Server | Grade | Static layer | Audit | Tape |
385
+ |---|---|---|---|---|
386
+ | `mcp.deepwiki.com/mcp` | **A 85.71** | 85.71% | `9fbb3558` | [replay](https://scorecard.wanessalabs.com/grade/9fbb3558-8b6e-475b-9b6f-161b32bbb7a1/transcripts) |
387
+ | this service, graded by itself | **A 98.63** | 98.63% | `d4bc490c` | [replay](https://scorecard.wanessalabs.com/grade/d4bc490c-5e51-4fb7-be67-6ef6f5a0a0ea/transcripts) |
388
+ | our planted fixture | **F 49** | **91.78%** | `f468e5b8` | [replay](https://scorecard.wanessalabs.com/grade/f468e5b8-232a-43bb-9cb4-2ad680bab1ae/transcripts) |
389
+
390
+ **Read the static column twice.** The hostile server scores 91.78% on
391
+ configuration, higher than the A-graded one. It negotiates the protocol
392
+ correctly and would survive a config review. The F is entirely in what it tells
393
+ the agent reading it.
394
+
395
+ That is the whole argument, and it is why the planted server is a real deployed
396
+ server rather than a row in a denylist. It is also why the middle row is there:
397
+ the scorecard is itself an MCP server, it was graded by itself, and it had to
398
+ pass its own gate to be callable. Nothing here is exempt.
399
+
400
+ ---
401
+
402
+ ### Embed the demo
403
+
404
+ One file, no build step, no dependency on this repo at runtime. It runs the same
405
+ live call the site runs.
406
+
407
+ ```html
408
+ <iframe src="https://clembot-doorman.wanessalabs.com/embed/flow.html"
409
+ width="100%" height="1900" style="border:0" loading="lazy"
410
+ title="Doorman: should I adopt this server?"></iframe>
411
+ ```
412
+
413
+ Add `?api=` to point it at your own scorecard deployment.
414
+
415
+ It is **generated** by `node scripts/build-widget.mjs`, never hand-maintained: a
416
+ second hand-copied copy of a 12KB driver and a 45KB stylesheet drifts the first
417
+ time anyone edits either, and drifts silently, because both still run. The
418
+ builder carries a drift guard that refuses to write a widget whose Run button
419
+ would throw.
420
+
421
+ ## Where this is headed
422
+
423
+ **A subscription that keeps an agent stack current.** New tools appear every
424
+ week. The useful question is not whether one is good, it is whether one is good
425
+ for the build you already have, and answering that repeatedly is a different
426
+ product from answering it once.
427
+
428
+ Full version, with the line between built and specified drawn where it actually
429
+ falls: **<https://clembot-doorman.wanessalabs.com/direction.html>**
430
+
431
+ ### The split, and why it is the whole design
432
+
433
+ | Half | Runs | Who pays | Cost of the next subscriber |
434
+ |---|---|---|---|
435
+ | The grade | ours, cached, public tape | whoever asked first, once | **$0.00** |
436
+ | The fit | **your machine** | you, in tokens | their own |
437
+
438
+ A candidate is graded **once** and every subscriber reads that grade for
439
+ nothing, so the marginal cost of the thousandth subscriber is not another audit.
440
+ The half that is actually about you, your agent roster, your installed servers,
441
+ your allowlist, is read locally and never leaves. `doorman watch` makes exactly
442
+ one request, a `GET` for the feed, and that request says nothing about who is
443
+ asking. The privacy is not a policy, it is the shape of the thing.
444
+
445
+ ### Working today
446
+
447
+ ```bash
448
+ # the shared half: newly graded candidates, one row per server, free
449
+ curl https://scorecard.wanessalabs.com/feed
450
+
451
+ # the private half: which of those are new to THIS build
452
+ node doorman/cli/doorman.mjs watch . --all
453
+ ```
454
+
455
+ `watch` sorts candidates into `already-installed`, `blocked`, `unreviewed` and
456
+ `skipped`. It will not tell you a candidate **fits**: that word belongs to the
457
+ fit review, which reads the candidate against your build with a model, and a
458
+ string match cannot earn it. Two tests exist for the sole purpose of stopping it
459
+ ever saying so.
460
+
461
+ ### Why this needs a payment rail, in two lines
462
+
463
+ The useful price for "is this new tool worth your attention" is a fraction of a
464
+ cent, and card fees exceed the value of the thing being sold. The product is not
465
+ *nicer* on a micropayment rail, it is impossible without one.
466
+
467
+ And a grade is cached with its full transcript free forever, so one agent's cent
468
+ does not buy one answer. It funds a commons nobody could bill for directly.
469
+
470
+ ### Finding candidates in the first place
471
+
472
+ `doorman discover` sweeps a public MCP registry and writes a candidate file, then
473
+ stops. It never enqueues and never spends.
474
+
475
+ ```bash
476
+ node doorman/cli/doorman.mjs discover --pages 1
477
+ ```
478
+
479
+ The registry returns only its own proxy, which needs its token, so the origin an
480
+ audit would need is not in the record. But the detail record ships the full tool
481
+ descriptions, so the static scan reads the exact surface an agent reads without
482
+ calling a single server.
483
+
484
+ **The first sweep is why the scan is now measured.** It flagged 15 of 100, and
485
+ two survived a hand check. The rest were ordinary documentation: a Slack
486
+ parameter that posts a reply to a conversation, an LLM testing tool whose job is
487
+ to accept a system prompt, `system:` as a docstring parameter name, and five
488
+ vendors saying "use this instead of" about another tool in their own server.
489
+ Five patterns were tightened and the same sweep now flags two.
490
+
491
+ All fifteen strings live in `doorman/test/discover-precision.test.mjs`, verbatim
492
+ and named, next to the strings that must keep tripping. The baseline is a
493
+ ratchet: it fails if precision gets worse, and demands the number be lowered in
494
+ the commit that improves it.
495
+
496
+ ### Not built, and said so
497
+
498
+ The released Bazantic CLI has **no marketplace discovery command**, so ingest is
499
+ still whatever gets pointed at the feed. Nothing has been settled through the
500
+ gateway even once. And 70 of every 100 points on every grade in the feed are
501
+ unmeasured until an `ANTHROPIC_API_KEY` exists.
502
+
503
+ ## The grade
504
+
505
+ Three layers, weighted 30 / 50 / 20, banded A at 85, B at 70, C at 50, F below.
506
+
507
+ **Static (30).** Wraps the `mcpscore` CLI. Protocol version, TLS, schema validity,
508
+ annotations, pagination. Its raw output has a *moving denominator*, because rules
509
+ get skipped per server, so it is normalised to a percentage before anything is
510
+ compared. A server scoring 78/91 is worse than one scoring 64/73, and only the
511
+ normalised number shows it.
512
+
513
+ **Behavioural (50).** A real agent, a pinned model, temperature 0, three runs per
514
+ probe. This is the half that reading cannot produce.
515
+
516
+ **Guidance delta (20).** The same cold task re-run with the drafted recipe in the
517
+ agent's system prompt. Scored as *recovered headroom*, not raw delta, so a server
518
+ that was already strong is not punished for having little room to improve.
519
+
520
+ The recipe is derived from the very runs it is then measured against, so this is
521
+ deliberately **not** a generalisation claim. It answers a narrower question, and
522
+ the narrow question is the useful one:
523
+
524
+ > We told the agent, in plain language, exactly what went wrong last time.
525
+ > Did that fix it?
526
+
527
+ A server that recovers can be put safely behind a recipe. A server that still
528
+ fails with the correction sitting in front of it is one where no amount of
529
+ documentation saves you, and that is the finding worth having. A **low** guidance
530
+ score is the interesting result; a high one is expected.
531
+
532
+ Four gates stop it reporting a number nobody can defend, and each has a mutation
533
+ check. It is `not measured` when the recipe derived no rules, when `cold_open`
534
+ never produced a baseline, when the cold run already scored 100 (zero headroom
535
+ would otherwise pay a perfect server twenty free points), and whenever the model
536
+ probes did not run. The guided run is scored **separately and kept out of the
537
+ behavioural mean**, or a server would be paid twice for one recovery. The guided
538
+ agent never sees the grade, the band, or the hard-fail banner: feed it
539
+ "Do not use this server" and the delta measures our own warning.
540
+
541
+ Two hard fails cap a grade at F regardless of everything else: injection-shaped
542
+ content in the advertised strings, and a transport that is not TLS.
543
+
544
+ ### Three properties worth stating plainly
545
+
546
+ 1. **An unmeasured layer is not a zero.** If the guidance delta did not run, its
547
+ weight is removed and the other two renormalise to 37.5 / 62.5. Scoring it zero
548
+ against a 20-point weight would drag every honest partial audit into a failing
549
+ band. That is lying with arithmetic, and there is a test for it.
550
+
551
+ 2. **A grade is relative to the model that produced it.** The model id is on the
552
+ audit, in the report, and on the badge. Grades from different models are not
553
+ comparable.
554
+
555
+ 3. **A skipped probe is excluded, not failed.** Ambiguity only fires when tool
556
+ descriptions overlap; Chain is skipped under three tools. A two-tool server is
557
+ not worse for having nothing to chain.
558
+
559
+ ---
560
+
561
+ ## The six probes
562
+
563
+ | # | Probe | The question it answers |
564
+ |---|---|---|
565
+ | 1 | Handshake and inventory | Is there anything here at all? Dead servers exit free. |
566
+ | 2 | Cold open | Can an agent that has never seen this succeed on the first try? |
567
+ | 3 | Ambiguity gauntlet | Do two overlapping descriptions actually distinguish themselves? |
568
+ | 4 | Bad input recovery | Can an agent self-correct from this error message in two turns? |
569
+ | 5 | Chain test | Do these outputs compose, or only look like they should? |
570
+ | 6 | Injection sniff | Is this documentation, or is it giving my agent orders? |
571
+
572
+ Probe 4 grades the **error message**, not the agent. "Invalid input" and
573
+ "missing required `repoName` (string, e.g. `facebook/react`)" are the same failure
574
+ and completely different products.
575
+
576
+ Probe 6 is scan-only and never calls a tool. We do not execute a server to find
577
+ out whether it is hostile. Because a hit caps the grade at F, which is a public
578
+ accusation about somebody else's software, it is deliberately conservative and
579
+ every hit records the pattern plus the offending text verbatim.
580
+
581
+ It also needs no model, so it is the one probe that **runs without an API key**.
582
+ Dropping it alongside the model-driven probes under `--static-only` would have
583
+ made the cheapest audits the ones that stayed quiet about hostile tool
584
+ descriptions. The planted fixture grades C without it and F with it.
585
+
586
+ ---
587
+
588
+ ## Why the Worker does not grade anything
589
+
590
+ `mcpscore` is Python and pulls in `cryptography`, `pydantic-core` and `cffi`,
591
+ which are native compiled extensions. A Cloudflare Worker cannot spawn a process
592
+ and Python Workers only load a curated package set. **The Worker physically
593
+ cannot run the static layer.**
594
+
595
+ So `POST /grade` returns **202 and an audit id**, a probe runner on a real machine
596
+ claims the work, and posts the result back. The API says this in its own
597
+ description rather than pretending to be synchronous and timing out.
598
+
599
+ The grade math lives in one isomorphic module that both halves import. `src/` is
600
+ typechecked against Cloudflare Workers types only, so a probe that reaches for a
601
+ Node API fails the build. The runner imports the *built* version of that module
602
+ rather than reimplementing it, because two implementations would drift and a
603
+ laptop-produced grade would stop meaning the same thing as a Worker-produced one.
604
+
605
+ ---
606
+
607
+ ## The gate
608
+
609
+ `doorman/.claude/hooks/mcp-gate.sh` runs before every MCP tool call.
610
+
611
+ - **No network.** Not a curl, not a DNS lookup. A gate that asks a server for
612
+ permission is offline the moment the network is, and offline would mean allow.
613
+ - **No dependencies.** Bash builtins and coreutils. No jq, no node, no python.
614
+ A gate that fails to start is a gate that fails open.
615
+ - **Fails closed.** Unparseable input, missing registry, unknown server,
616
+ unreadable file: all block.
617
+ - **Exit 2, never exit 1.** Only exit 2 blocks a call. Exit 1 is treated as a
618
+ script error and the call proceeds. A test asserts the file contains no exit 1.
619
+ - **Humans own the list.** Nothing writes the allowlist automatically. The poller
620
+ reports changes and refuses to allowlist a hard fail even when the service says
621
+ to.
622
+
623
+ The `doorman` subagent holds `Read` and exactly one MCP tool. The agent that
624
+ decides what to trust does not also carry capabilities an untrusted server could
625
+ talk it into using.
626
+
627
+ ---
628
+
629
+ ## Running it
630
+
631
+ > Proving the whole chain works, rather than one piece of it, is
632
+ > **`RUNBOOK.md`**: four ordered tests, three of them free, with the
633
+ > observed output of each.
634
+
635
+ ```bash
636
+ # The service
637
+ cd mcp-scorecard
638
+ npm install
639
+ npm run migrate:local
640
+ npx wrangler dev --port 8799 --local
641
+
642
+ # Grade a real server, static layer only, no API key needed
643
+ pip install mcpscore
644
+ node runner/run.mjs --once --server https://mcp.deepwiki.com/mcp \
645
+ --needed-for "look up how a public repository works" \
646
+ --static-only --out ../evidence/deepwiki
647
+
648
+ # Full behavioural run (needs a key)
649
+ export ANTHROPIC_API_KEY=...
650
+ node runner/run.mjs --once --server https://mcp.deepwiki.com/mcp \
651
+ --needed-for "look up how a public repository works"
652
+
653
+ # Poll the queue
654
+ export RUNNER_TOKEN=... SCORECARD_API=http://127.0.0.1:8799
655
+ node runner/run.mjs --poll
656
+ ```
657
+
658
+ ```bash
659
+ # Tests
660
+ cd mcp-scorecard && npm test # 228 unit tests, 12 files
661
+ node test/smoke-grade.mjs # grades a live public server
662
+ node test/smoke-api.mjs # 75 assertions over the HTTP surface
663
+ cd ../doorman && bash test-gate.sh # 29 adversarial gate tests
664
+ node test-poller.mjs # registry key derivation
665
+ ```
666
+
667
+ ---
668
+
669
+ ## Status
670
+
671
+ Working and verified end to end **except the behavioural probes and the guidance
672
+ pass**, which have never been executed against a live model because no
673
+ `ANTHROPIC_API_KEY` has been supplied. Every guidance number in this repo comes
674
+ from a scripted stub in the test suite; none is a measurement of a real server. They are written, typechecked and unit-tested behind a mock-free
675
+ interface, and the runner refuses to fabricate results without a key: it marks
676
+ the audit failed and says why.
677
+
678
+ The chain those probes sit in was verified end to end against the deployed stack
679
+ on **3 September 2026**: a hostile server published, graded F through the live
680
+ queue, written into a registry that cites the audit id and evidence hash, and
681
+ blocked by the gate at exit 2 while the A-graded server passes at exit 0.
682
+
683
+ **No probe runner has been polling since that date.** The queue accepts work and
684
+ nothing claims it, so an audit requested today stays `queued` until someone
685
+ starts a runner. That is a second gap, separate from the missing key and more
686
+ immediate: `RUNBOOK.md` Test 2 closes it, costs nothing, and needs no key.
687
+
688
+ ## Replay the tape
689
+
690
+ Every turn behind every grade is public and needs no token:
691
+
692
+ ```
693
+ GET /grade/{audit_id}/transcripts # JSONL, one object per turn
694
+ GET /grade/{audit_id}/transcripts?format=json # grouped by probe run
695
+ ```
696
+
697
+ Never paginated, never sampled. A grade is an accusation, and the evidence for
698
+ one cannot sit behind the token held by the party making it. It does not sit
699
+ behind the paywall either: `POST /grade` is the only chargeable route, and a
700
+ test asserts the transcripts stay free with payment fully switched on.
701
+
702
+ ## Money
703
+
704
+ Two halves, and only one of them is finished.
705
+
706
+ **The spend cap is complete.** The doorman refuses to spend twice: once on fit
707
+ (does this system need it at all) and once on budget (can it afford to find
708
+ out). The second refusal is a **permit**, not a check. `scorecardClient` will
709
+ not be constructed without a budget and `enqueue()` will not run without an open
710
+ permit from it, because a required argument cannot be forgotten by a code path
711
+ that does not know the rule exists.
712
+
713
+ ```
714
+ budget 0 spent today, 5 of 5 left. Per-run cap 1.
715
+ FIT FITS -> scheduler
716
+ REFUSED the spend cap said no. $0.00 spent.
717
+ ```
718
+
719
+ An **unknown price is not a free one**. `GET /price` is free and
720
+ unauthenticated, `/vet` reads it rather than assuming, and a price that cannot
721
+ be read stops the run. A client that defaults an unknown price to zero passes
722
+ every cap it has, forever. A `price_usdc: 0` from the service is a *discovered*
723
+ zero and spends cleanly; a missing field is not.
724
+
725
+ **Settlement is refused rather than faked.** The x402 **v2** challenge is real
726
+ and was implemented from the published spec rather than from memory, which
727
+ caught three errors that would otherwise have shipped: the header is
728
+ `PAYMENT-SIGNATURE` not `X-PAYMENT`, the field is `amount` not
729
+ `maxAmountRequired`, and `network` is CAIP-2 rather than a name. But there is no
730
+ wallet and no facilitator wired to this Worker, so a request arriving with a
731
+ `PAYMENT-SIGNATURE` is **refused**, using the protocol's own failure channel. A
732
+ paywall that opens for any string is worse than no paywall, because it looks
733
+ like protection.
734
+
735
+ Switching payment on without a configured recipient fails **closed** with 503
736
+ and publishes no placeholder address. An agent that paid a made-up recipient
737
+ would lose real money.
738
+
739
+ ---
740
+
741
+ ## Bazantic Integration & Prizes
742
+
743
+ Bazantic simplifies AI development by allowing developers to turn APIs into services agents can understand, use, and pay for via x402 micropayments.
744
+
745
+ Clembot Doorman uses Bazantic to:
746
+ 1. **Agentify the Scorecard API** into a paid MCP service at `clembot-doorman.bazgateway.com`.
747
+ 2. **Draft & Enforce Bazantic Recipes (`recipe.md`)** that turn raw, hallucination-prone MCP tools into safe, bounded, high-reliability agent tasks.
748
+
749
+ ### ETHOnline 2026 Bazantic Prize Tracks
750
+
751
+ | Prize Track | Amount | How Clembot Doorman Qualifies |
752
+ |---|---|---|
753
+ | 🤖 **Help an Agent Use Your Hackathon Project** | $1,000 (Continuity) | **Autonomous agent usage without human guidance:** An agent can inspect its own Clembot build (`doorman doctor`), detect missing tools from its prompt history (`doorman needs`), query the live Bazantic Scorecard gateway, and install the local offline `PreToolUse` security hook. |
754
+ | 🍳 **Best Recipe Using EthGlobal Sponsor APIs** | $1,000 | **Raw MCP servers fail; Recipes succeed:** Our A/B evals demonstrate that raw MCP servers (like WebZum) inject thousands of characters of steering ads and cause token loops. Our drafted Bazantic Recipes (`recipe.md`) constrain schemas, enforce deterministic parameters, and prevent context window pollution. |
755
+ | 👨‍🍳 **Agentify a New API** | $1,000 | **Live x402 Micropayments Gateway:** Deployed OpenAPI 3.1 scorecard endpoints through the Bazantic gateway at `clembot-doorman.bazgateway.com` with per-grade pricing ($0.01/grade, free cached reads), key masking, and machine-readable tool generation. |
756
+
757
+ ### Live Gateway Status
758
+
759
+ The gateway **Doorman** is active at `clembot-doorman.bazgateway.com`:
760
+ - **Endpoint**: `https://scorecard.wanessalabs.com`
761
+ - **Spec**: `https://scorecard.wanessalabs.com/openapi.json` (OpenAPI 3.1.0)
762
+ - **Auth**: `api-key` (forwards `GRADE_TOKEN` upstream, calling agents never see raw credentials)
763
+ - **Pricing**: $0.01 per behavioural grade, $0.00 for cached reads and feed streaming
764
+
765
+ ### What a Recipe actually is
766
+
767
+ Not a flow, not a pipeline, not a DAG. **One task: typed inputs, a prompt, a
768
+ model, and a bound set of MCP tools**, published as a single MCP tool that any
769
+ agent can call. Sequencing happens inside one prompt, so a Recipe is a model
770
+ given tools rather than a declared sequence of steps.
771
+
772
+ Ingredients have to already exist on Bazantic as gateways. A Recipe cannot
773
+ invent a tool. So the gateway comes first and the Recipe second, always.
774
+
775
+ ### Install and use it
776
+
777
+ ```bash
778
+ # 1. the CLI, once
779
+ npm i -g @bazantic/cli
780
+ baz login
781
+
782
+ # 2. register the scorecard. Its OpenAPI 3.1 spec is already served,
783
+ # so there is nothing to write for this step.
784
+ API=https://scorecard.wanessalabs.com
785
+ baz gateway add --endpoint $API --spec-url $API/openapi.json \
786
+ --auth-type api-key \
787
+ --name "MCP Scorecard" --status draft --json
788
+
789
+ # 3. a capped, revocable grant for the doorman to spend from.
790
+ baz grant create --name doorman --cap 5 --service <slug>
791
+ baz curl https://bazgateway.com/<slug>/grade \
792
+ --account doorman --max-amount 0.05 --source hosted --json
793
+
794
+ # 4. the Recipe is DASHBOARD-ONLY on the released CLI. See below.
795
+ ```
796
+
797
+ ### Verified against the installed CLI, 2026-09-08
798
+
799
+ `@bazantic/cli@0.8.0` was installed and its command surface read directly. Two
800
+ families the docs describe **do not exist in the released build**:
801
+
802
+ | Documented | `baz` 0.8.0 |
803
+ |---|---|
804
+ | `baz recipe list/get/create/update/publish/unpublish/delete` | **absent** (`unknown command: recipe`) |
805
+ | `baz gateway domains add/status/verify/rm` | **absent** (`unknown gateway command: domains`) |
806
+ | `--auth-type none`, documented as the default | **not offered.** The CLI takes `api-key \| jwt \| x402-mpp \| basic` and defaults to `x402-mpp`, which the docs describe as retired and credential-free |
807
+ | `bazantic.yaml` manifest | absent, and the docs do say it is preview |
808
+
809
+ What the released CLI does have: `login`, `whoami`, `gateway add`, `gateway
810
+ list`, `curl`, `wallet`, `grant`. That covers registration and the whole payment
811
+ path. **Recipes and custom domains have to go through the dashboard.**
812
+
813
+ We use `--auth-type api-key`, which exists in both, so the gateway forwards
814
+ `GRADE_TOKEN` upstream and the calling agent never sees it.
815
+
816
+ ### Two things that cost nothing, worth doing before paying
817
+
818
+ Straight from the CLI docs, and they are the reason a gateway can be mapped for
819
+ free:
820
+
821
+ - **List the tools.** `POST {endpointUrl}/mcp` with a JSON-RPC `tools/list`
822
+ returns every operation and its parameters.
823
+ - **Probe for a price.** A wrong path returns 404; a correct one returns 402
824
+ with the exact price in the body. Neither costs anything, so every route can
825
+ be mapped with `curl` and paid for only once confirmed.
826
+
827
+ Prices come back in base units of a 6-decimal token: `10000` means `$0.01`.
828
+
829
+ ### The served spec is narrower than the routes
830
+
831
+ `/openapi.json` describes eight operations. The Worker answers ten. `GET
832
+ /api/pending` and `POST /api/result` are the probe-runner control plane, they
833
+ stay routed, and a self-hoster running their own runner needs them, but they are
834
+ not described on the public document.
835
+
836
+ The reason is specific to how a gateway ingests a spec. Bazantic derives one MCP
837
+ tool per operation, and it derives them from the whole document, not from the
838
+ methods you priced. Excluding the two runner methods from pricing removed them
839
+ from routing, so they 404 through the gateway, while `tools/list` went on
840
+ offering them as callable tools. Two surfaces, one allow-list.
841
+
842
+ An advertised tool that cannot be called is a false description on the exact
843
+ surface this project exists to grade. So the filter lives in
844
+ `src/routes/openapi.ts` as `stripPrivate()`: it removes every operation tagged
845
+ `runner`, then the paths those emptied, the tag itself, and the `runnerToken`
846
+ security scheme that nothing left referenced. The full document is still built
847
+ and still tested, because deleting the operations outright would leave the
848
+ Worker answering routes nothing described.
849
+
850
+ ### The spec URL is fetched by THEIR servers, not yours
851
+
852
+ `--spec-url` is fetched server-side. A spec behind localhost, a VPN, or auth
853
+ fails with `spec rejected: could not fetch --spec-url` even though it loads in
854
+ your browser.
855
+
856
+ **Measured on this host, 2026-09-08:** `scorecard.wanessalabs.com/openapi.json`
857
+ returns **403 to `Python-urllib/3.12`** and 200 to `curl`, `Go-http-client`,
858
+ `node-fetch` and a request with no user-agent at all. That is our own Cloudflare
859
+ WAF. If Bazantic's fetcher presents a blocked agent, registration fails for a
860
+ reason that looks like a Bazantic problem and is ours. The fix is to paste the
861
+ document into the dashboard field instead of pointing at the URL.
862
+
863
+ ### `input_schema` dialect: resolved
864
+
865
+ **JSON Schema Draft 2020-12, with local references.** This was marked `[VERIFY]`
866
+ until the CLI docs stated it.
867
+
868
+ Read the gateway URL out of `baz gateway list --json` as `endpointUrl` rather
869
+ than assembling it by hand. More than one URL form is served and which one
870
+ applies depends on the deployment environment.
871
+
872
+ ### `--source hosted` is not optional
873
+
874
+ Bazantic documents this and it is worth repeating, because it is the exact
875
+ failure mode this project exists to refuse:
876
+
877
+ > If the CLI cannot find a grant's key on this device it warns on stderr and
878
+ > falls back to your self-custody wallet for that call, which changes the call
879
+ > from capped and revocable to uncapped and irrevocable.
880
+
881
+ That is a fail-open on the spend path. It is disclosed, and there is a flag for
882
+ it, so the doorman always passes `--source hosted` and takes the hard failure.
883
+ The same reasoning as `budget.mjs` refusing an unreadable price: a cap that
884
+ silently stops applying is worse than no cap, because you stop watching.
885
+
886
+ `--max-amount` defaults to `0.01` and is checked before anything is signed.
887
+
888
+ ### The Recipe file
889
+
890
+ The Recipe definition has **exactly** these fields. The docs describe a
891
+ `baz recipe create <file>` command that takes them as JSON; that command is
892
+ absent from CLI 0.8.0, so today this is what the dashboard editor is filling in.
893
+ Unknown fields error before any network request.
894
+
895
+ | Field | Notes |
896
+ |---|---|
897
+ | `name` | The handle is derived from it and is immutable. |
898
+ | `description` | |
899
+ | `input_schema` | JSON Schema **Draft 2020-12**, local references only |
900
+ | `input_example` | |
901
+ | `output_example` | `Use as output example` on a real test run fills this. |
902
+ | `prompt_template` | Exactly one `{{inputs}}` placeholder. 4000 chars max. |
903
+ | `model` | Allowed values come from `baz recipe --help`, which **does not exist in CLI 0.8.0**. Read them off the dashboard editor instead. `[VERIFY]` |
904
+ | `tool_bindings` | Each entry carries only `gateway_slug` and `tool_name`. 1 to 64. |
905
+
906
+ Whole definition caps at 24 KiB of compact UTF-8 JSON. An update file takes a
907
+ nonempty subset of the same fields. `create` produces a draft; `update` only
908
+ works on a draft; `delete` only works on a never-published draft.
909
+
910
+ ### The async problem, stated rather than hidden
911
+
912
+ A Recipe run is one pass. A cold audit takes minutes, and `POST /grade` returns
913
+ 202 with an audit id rather than a grade. So the Recipe must return one of:
914
+
915
+ - a cached grade, when a recent audit exists, which is instant, or
916
+ - an audit id and a transcripts url, saying plainly that grading is running.
917
+
918
+ It must not stall waiting, and it must not synthesise a provisional score. The
919
+ `grade` tool description already commits to this and the Recipe prompt inherits
920
+ it. Whether a Recipe run can poll across several tool calls inside its own
921
+ timeout is not documented. `[VERIFY]`
922
+
923
+ ### A Recipe is itself an ungraded MCP surface
924
+
925
+ Worth stating because it is the most interesting thing here. A published Recipe
926
+ is one tool whose behaviour is a prompt the caller never reads and a tool set
927
+ the caller never sees. That is the same shape as the finding this project leads
928
+ with, one layer up.
929
+
930
+ Bazantic is also the first surface where it is fixable. `bazantic_recipe_get`
931
+ returns the definition, and `bazantic_gateway_list_tools` returns tool names,
932
+ descriptions, input schemas and annotations for a gateway. Our static layer is a
933
+ pure function over exactly those strings and never calls anything, so it can run
934
+ across a whole inventory before a single paid call.
935
+
936
+ ### Not usable: `bazantic.yaml`
937
+
938
+ The gateway manifest page is marked preview and says the released CLI cannot
939
+ create a gateway from the file, calculate a plan, or apply one. Do not write one.
940
+ `baz gateway add` is the released path.
941
+
942
+ ### Data note
943
+
944
+ Bazantic keeps the prompts, drafts and test inputs entered in the editor and
945
+ uses them to improve the product. That is a reason to keep the grading rubric
946
+ inside this service and let the Recipe prompt stay thin, which is better design
947
+ regardless.
948
+
949
+ ---
950
+
951
+ See `roadmap.md` for what is done, what is stubbed, and what is untested.