Commit f36dc51
authored
* feat(mcp): add the AMS management tool family behind the governor gate
The miner MCP exposed 11 read-only tools while its entire mutating ops surface was
CLI-only. This adds 10 tools: a dedicated doctor (split out of status so status stays
cheap), a structured metrics snapshot, and eight mutations.
Every mutation dispatches through the miner's existing governor-gated chat-action
chokepoint -- the same boundary the dashboard's own actions use. The MCP layer never
touches a store: it dispatches an action NAME, and the registry structurally refuses any
handler not produced by governorGatedHandler(), whose brand is a private symbol a raw
function cannot forge. That is what makes "an MCP caller cannot reach a write path the
dashboard could not" a property of the code rather than of review discipline, and the
structural test asserts it from both ends.
Three claims I had to correct against the real code rather than ship as written. The
migrate CLI has no dry-run, because applying a migration IS opening the store -- so the
tool has no apply flag either, instead of advertising a safety mode that does not exist.
The deny-hook store keys proposals by (repo, id), so the decide tool takes the repo rather
than scanning every repo's proposals to resolve an id. And purgeRepoAcrossStores is
extracted from runPurge so the tool runs the CLI's own purge over the CLI's own target
list, rather than a second implementation free to miss a store.
collectMinerPredictionMetrics is likewise extracted in the engine: the Prometheus text
renderer now formats those families, so the scrape and the JSON snapshot share one
aggregation and cannot disagree about what a counter means.
validate:mcp earned its keep twice here. It caught the two AMS tenant tools declared but
never registered, and it caught loopover_miner_run_migrations/_purge_repo returning fields
their output schemas did not declare -- the .shape re-wrap that drops looseObject's
catchall, the same -32602 class this epic already fixed once. Both outputs now declare
every field their handlers return.
AMS tenant create/list/destroy are deliberately absent: #9522's loopover_tenant_* tools are
product-parameterized and already serve product "ams", because the control plane's routes
are. Only health and wake -- the pair with no ORB counterpart -- are added. The catalog's
recorded exclusions (calibration floors, raw run-state set, the one-way kill switch) stay
CLI-only, pinned by a test so reversing that decision has to be deliberate.
* test(mcp): cover the miner ops actions and the full dispatch path
The store operations and the registered handlers were only reachable through the MCP tools,
so nothing measured them directly. These drive each action end to end -- action name
through the governor gate to the store call -- plus the validator guard clauses for null,
primitive, and array params, and the fail-closed paths (flag disabled, unknown action,
malformed params never reaching a store).
registerMinerOpsChatActions gains the same evaluateGate seam the dashboard's own
registerPortfolioQueueChatActions already exposes, so a test can drive the dispatch path
without the real chokepoint; production passes none and gets the real one.
paramsOf loses its `?? {}`: every action that calls it has a validator requiring an object,
and dispatchChatAction runs that validator first, so the fallback had no reachable case.
* chore(docs): let #9521's generator own the miner tool table
The hand-edited list this branch added is replaced by the generated one now that #9590 has
landed -- which is what the issue asked for: the tool tables pick new tools up with no
hand-edits. 163 registry entries across the three servers.
* test(mcp): cover the AMS tools' registration and result-shaping layers
codecov/patch was short because the new tool REGISTRATIONS were never driven in-process --
the miner bin's 169 changed lines had 54 uncovered statements, and the AMS tenant handlers
had none of their own tests at all.
These drive every new tool over the in-memory transport against injected seams: the doctor
mapping from status.js's {name, ok, detail} onto the contract's pass/warn/fail, the metrics
snapshot's shared aggregation, and each mutating tool's dispatch and result shaping --
including that a governor refusal comes back as a structured blocked result rather than a
thrown error, and that a purge with confirm absent or false is rejected by the schema
BEFORE any dispatch happens.
The AMS tenant pair gets its not-configured, healthy, and throttled paths, the last of
which is the one that must not be audited as a cycle: the schedule guard refusing a
too-soon wake is the guard working, not work that ran.
Writing the metrics test surfaced a fixture trap worth naming: toPredictionRecords reads
the LEDGER row shape (conclusion/targetId/ts), so a fixture shaped like the downstream
record yields conclusion: undefined and fails output validation. The test now uses the
ledger's own shape.
* test(engine): cover the prediction-metrics split in the engine's OWN suite
codecov reported this file at 35.84% under the `engine` flag while the root vitest suite
showed it fully covered. Both were right: @loopover/engine uploads coverage from its own
node:test suite (packages/loopover-engine/test/**), and that suite had no test for this
file at all — the vitest one lands under `backend` and does not lift the engine flag.
So the aggregation and the renderer get tests where the engine actually measures itself,
including the property the split exists for: every sample `collectMinerPredictionMetrics`
reports appears verbatim in what `renderMinerPredictionMetrics` emits, so the JSON snapshot
and the Prometheus scrape cannot report different numbers.
Also pins the escaping, which is a correctness rule rather than cosmetics: a conclusion is
DATA, so a quote, backslash, or newline inside one must not forge a second series.
1 parent edf0883 commit f36dc51
18 files changed
Lines changed: 1773 additions & 30 deletions
File tree
- apps/loopover-ui/src/lib
- packages
- loopover-contract/src/tools
- loopover-engine
- src
- test
- loopover-miner
- bin
- lib
- src
- mcp
- orb
- test/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
111 | 125 | | |
112 | 126 | | |
113 | 127 | | |
| |||
696 | 710 | | |
697 | 711 | | |
698 | 712 | | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
699 | 734 | | |
700 | 735 | | |
701 | 736 | | |
| |||
724 | 759 | | |
725 | 760 | | |
726 | 761 | | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
727 | 769 | | |
728 | 770 | | |
729 | 771 | | |
| |||
745 | 787 | | |
746 | 788 | | |
747 | 789 | | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
748 | 804 | | |
749 | 805 | | |
750 | 806 | | |
| |||
766 | 822 | | |
767 | 823 | | |
768 | 824 | | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
769 | 853 | | |
770 | 854 | | |
771 | 855 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
| 150 | + | |
| 151 | + | |
150 | 152 | | |
151 | 153 | | |
152 | 154 | | |
| |||
282 | 284 | | |
283 | 285 | | |
284 | 286 | | |
| 287 | + | |
| 288 | + | |
285 | 289 | | |
286 | 290 | | |
287 | 291 | | |
| |||
318 | 322 | | |
319 | 323 | | |
320 | 324 | | |
| 325 | + | |
| 326 | + | |
0 commit comments