Skip to content

ams(mcp): the AMS management tool family — governor-gated mutations, doctor, tenant health (#9523) - #9607

Merged
JSONbored merged 5 commits into
mainfrom
feat/ams-management-tools-9523
Jul 29, 2026
Merged

ams(mcp): the AMS management tool family — governor-gated mutations, doctor, tenant health (#9523)#9607
JSONbored merged 5 commits into
mainfrom
feat/ams-management-tools-9523

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #9523. Stacked on #9604 (feat/orb-management-tools-9522) — review that first; this branch contains its commits.

What changed

The miner MCP exposed 11 read-only tools while its entire mutating ops surface was CLI-only. This adds 10: a dedicated doctor (split out of status so status stays cheap and doctor can grow checks), a structured metrics_snapshot, and eight mutations — governor pause/resume, queue release/requeue, claim release, deny-hook decide, store migrations, and a confirmed right-to-be-forgotten purge.

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 — including that a blocked gate never reaches the store operation.

Three claims I had to correct against the real code

Each of these I'd written into the contract from the issue's description, then found the code disagreed:

  • The migrate CLI has no dry-run. Applying a migration is opening the store. So the tool has no apply flag either, rather than advertising a safety mode that does not exist and could only be built as a second implementation of the migration walk.
  • The deny-hook store keys proposals by (repo, id). The decide tool takes the repo rather than scanning every repo's proposals to resolve a bare id.
  • purgeRepoAcrossStores is extracted from runPurge, so the tool runs the CLI's own purge over the CLI's own target list — not a second implementation free to miss a store as targets are added.

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. All 762 engine tests pass unchanged.

validate:mcp earned its keep twice

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.

Catalog decisions, recorded rather than improvised

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 own routes are. A second AMS-only spelling would be two names for one capability — the rot that kept loopover_fleet_get_analytics out of #9522. Only health and wake, the pair with no ORB counterpart, are added.

The issue'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 rather than accidental.

Validation

Typecheck clean; validate:mcp passes across all three servers. chat-miner-ops-actions.ts and miner-ops-actions.ts are at 100% statement and branch, covering the full path (action name → gate → store call), the validator guard clauses, and the fail-closed cases. paramsOf lost its ?? {} — every caller has a validator requiring an object that runs first, so the fallback had no reachable case.

Docs: the miner README's tool list is hand-edited here because #9521's generator is not merged yet; once #9590 lands it owns that table and the entries regenerate.

@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-29 04:47:24 UTC

18 files · 1 AI reviewer · no blockers · CI green · clean

⏸️ Suggested Action - Manual Review

Review summary
This PR adds the miner MCP's mutating ops surface (governor pause/resume, queue release/requeue, claim release, deny-hook decide, migrations, purge) plus doctor/metrics-snapshot reads and two hosted-AMS-tenant tools, all routed through the existing governor-gated chat-action registry so an MCP caller cannot reach a write path the dashboard's own actions couldn't. The safety design is sound and well-tested (structural gating tests, dispatch-shaping tests, purge extraction reused verbatim from the CLI), and the metrics-snapshot refactor (renderMinerPredictionMetrics -> collectMinerPredictionMetrics) is a clean single-aggregation extraction with no behavior change to the text exposition path. The most notable wrinkle is `runPurge`'s exit-code semantics changed from `hadError ? 2 : 0` to `summary.outcome === 'partial' ? 2 : 0`, which is only a real regression if `outcome` can be something other than 'purged'/'partial' while `hadError` was true — worth confirming against `purgeOneStore`'s error shape.

Nits — 6 non-blocking
  • packages/loopover-miner/lib/purge-cli.ts:341 — verify `purgeRepoAcrossStores`'s `hadError` and `outcome` stay in lockstep now that `runPurge`'s exit code branches on `summary.outcome === "partial"` rather than the original `hadError` boolean; if any code path could set `hadError` true while `outcome` stays `"purged"`, the CLI's exit code silently changes from 2 to 0.
  • confirm that the MCP SDK's output-schema `.shape` re-wrap (noted in comments) truly drops the `looseObject` catchall for every registered tool here, since several outputs (e.g. `MinerGovernorActionOutput`) still declare only a subset of fields — the comment says this is intentional but it's easy for a future field to be added to the handler without updating the schema and get silently -32602'd.
  • packages/loopover-contract/src/tools/miner-ops.ts — the magic numbers (200/500/3) for string length bounds are unexplained; a short comment or shared constant would help future contributors know if they're arbitrary or load-bearing.
  • packages/loopover-miner/bin/loopover-miner-mcp.ts and src/mcp/server.ts are both already large files growing further; no action needed now but worth a follow-up split if more tool families land here.
  • Double check `purgeRepoAcrossStores`'s `outcome` field is the only source of truth CI depends on for exit codes (packages/loopover-miner/lib/purge-cli.ts:341), and if `hadError` can diverge, restore checking it directly instead of `outcome`.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9523
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 284 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 284 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: significant
Linked issue satisfaction

Partially addressed
The diff delivers most of the local mutating family (governor pause/resume, queue release/requeue, claim release, deny-hooks decide, migrations, purge with confirm+dispatch through the governor-gated chokepoint), the doctor split, metrics snapshot, and two hosted health/wake tools with a structural test file present, satisfying most catalog items and requirement 2's structural gating goal. However

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is registered but has no active allocation in the current snapshot.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 284 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 1 step in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: ai_consensus_defect
  • config: aa610432c63105b55c14aa5dc79ffab125ec6913de1db000fabd2e966c983f3f · pack: oss-anti-slop · ci: passed
  • model: ollama · prompt: fb2e25403180ae7550fe55742b1d5051563f41abd7fea6c267e6b6c8ae65552d · confidence: 0.85
  • record: 3ad8228f18160926de3b13bda2200d7c9c272d6af674f16ac270e76cfe243468 (schema v5, head bdb8243)

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
loopover-ui e4ea2a7 Jul 29 2026, 04:19 AM

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.27%. Comparing base (3200abc) to head (bdb8243).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9607      +/-   ##
==========================================
+ Coverage   90.24%   90.27%   +0.02%     
==========================================
  Files         900      904       +4     
  Lines      112914   113122     +208     
  Branches    26776    26821      +45     
==========================================
+ Hits       101896   102116     +220     
+ Misses       9687     9676      -11     
+ Partials     1331     1330       -1     
Flag Coverage Δ
backend 95.54% <100.00%> (+0.01%) ⬆️
engine 67.86% <100.00%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-contract/src/tools/ams-tenant.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/tools/index.ts 100.00% <ø> (ø)
packages/loopover-contract/src/tools/miner-ops.ts 100.00% <100.00%> (ø)
packages/loopover-engine/src/index.ts 100.00% <100.00%> (ø)
...es/loopover-engine/src/miner-prediction-metrics.ts 100.00% <100.00%> (+14.86%) ⬆️
packages/loopover-miner/bin/loopover-miner-mcp.ts 97.40% <100.00%> (+0.97%) ⬆️
...kages/loopover-miner/lib/chat-miner-ops-actions.ts 100.00% <100.00%> (ø)
packages/loopover-miner/lib/miner-ops-actions.ts 100.00% <100.00%> (ø)
packages/loopover-miner/lib/purge-cli.ts 100.00% <100.00%> (ø)
src/mcp/server.ts 97.36% <100.00%> (+0.02%) ⬆️
... and 1 more

... and 1 file with indirect coverage changes

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

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.
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.
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.
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.
@JSONbored

Copy link
Copy Markdown
Owner Author

Both blockers cite files that do not exist in this repo, and I could not reproduce either against the real code. Details, since I'd rather show the evidence than just assert it:

1. adminRotateSecretTool.ts:12 — no such file. The tool lives in packages/loopover-contract/src/tools/admin-config.ts.

On the substance: it is correct that there is no confirm, but that is not a defect this PR introduced or should change.

  • The tool is pre-existing (Runtime credential rotation for subscription CLI providers (no restart, single-box + fleet) #9543). This PR migrated it into the contract registry — its input surface is byte-identical to what already shipped. Adding a required confirm here would be a breaking behavior change to a live tool, not a fix.
  • It is gated by auth: "mcp-admin" (LOOPOVER_MCP_ADMIN_TOKEN), and only registered at all when LOOPOVER_MCP_ADMIN_ENABLED is truthy — it is one of the few conditionally-registered tools on the server.
  • Rotation backs the previous value up first (backupPath in its output), so it is recoverable. That is the material difference from purge_repo, which is not, and which is exactly why purge carries the confirm and this does not.
  • It already carries destructiveHint: true.

2. mineralPurgeRepoTool.ts:24 — no such file, and no such symbol. grep -rn "mineral" across packages/, src/, and test/ returns nothing. The real tool is minerPurgeRepoTool in packages/loopover-contract/src/tools/miner-ops.ts.

Both claims about it are wrong:

  • It does require confirmation — MinerPurgeRepoInput is { repoFullName, confirm: DestructiveConfirm }, where DestructiveConfirm is z.literal(true) (a plain boolean would let an omitted field read as false and pass). Two regression tests cover it: confirm absent and confirm: false are both rejected before any dispatch reaches a store.
  • A repo with no rows is not a silent failure. purgeRepoAcrossStores returns { outcome: "purged", totalPurged: 0, stores: [...] } — an explicit per-store breakdown. That is the CLI's own core, extracted in this PR precisely so both surfaces answer identically. For a right-to-be-forgotten operation, "there was nothing to forget" is a correct answer, not an error.

The two findings also appear to have been swapped: the one told it lacks confirm is the one that has it.

I have pushed real fixes for the two genuine problems in the meantime — the merge conflict (resolved against #9522, now merged) and patch coverage, which is now at zero uncovered changed lines across every file in the diff.

…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.
@JSONbored

Copy link
Copy Markdown
Owner Author

Confirming the runPurge exit-code question from the review — it is not a behavior change.

outcome is computed from the same predicate the old code used, at the only site that constructs a PurgeSummary (purge-cli.ts:311-314):

const hadError = perStoreResults.some((entry) => "error" in entry);
return { outcome: hadError ? "partial" : "purged", ... };

And PurgeSummary.outcome is typed "purged" | "partial", so there is no third value for it to take — summary.outcome === "partial" is hadError, by construction and by the type. The extraction moved that predicate from runPurge into purgeRepoAcrossStores precisely so the MCP tool reports the same partial-failure state the CLI exits 2 on, rather than recomputing it.

The 2-on-partial contract stays pinned by the existing suite (test/unit/miner-purge-cli.test.ts), including "is audit-observable on a PARTIAL failure: reports what succeeded, flags the failed store, and exits 2" plus the per-store error cases at lines 563/576/602/626.

@JSONbored
JSONbored merged commit f36dc51 into main Jul 29, 2026
10 checks passed
@JSONbored
JSONbored deleted the feat/ams-management-tools-9523 branch July 29, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ams(mcp): AMS management tool family — governor-gated mutations, doctor split, hosted tenant tools

1 participant