Skip to content

feat(contract): give the request schemas, the control plane, and the self-host endpoints one contract (#9750) - #9757

Merged
JSONbored merged 1 commit into
mainfrom
feat/contract-schemas-control-plane-9750
Jul 29, 2026
Merged

feat(contract): give the request schemas, the control plane, and the self-host endpoints one contract (#9750)#9757
JSONbored merged 1 commit into
mainfrom
feat/contract-schemas-control-plane-9750

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #9750.

The three requirements of the now-closed #9531 that never landed with it.

1. The request schemas move to @loopover/contract

All 52 declarations leave src/api/routes.ts, which now has zero z.object( literals — asserted by a test, not trusted. An MCP tool wrapping one of these routes references the same object instead of an independently-authored copy of the shape. That is the drift that made FleetRegisterInstallationInput omit the registered field its route accepts, leaving the opt-out unreachable over MCP until #9518 found it.

A move, not a rewrite. Each schema is the declaration that was there, verbatim, so a payload the API accepted is still accepted and one it rejected is still rejected with the same status. The whole suite passing unchanged is the evidence.

Their bounds could not travel with them — the contract is a zod-only leaf that cannot import the Worker or the engine — so limits.ts restates nine and a meta-test pins each against its original, extending the posture PREFLIGHT_LIMITS already established there. A restated bound is only safe while something fails when the two disagree, and drift there fails in the worst direction: a schema that rejects input the server would have accepted, or accepts input the server then truncates.

One schema is not a plain move. repositorySettingsSchema's commandAuthorization default is a twenty-key policy the engine owns, so the contract exports the shape as a factory and the src layer applies the engine's own value once — restating a policy that large in a leaf package would be the duplication this move exists to remove.

Two more were declared inside a handler and rebuilt on every request (the queue-intelligence payload); one more, z.object(simulateOpenPrPressureShape), was constructed per request from a shape already shared with its MCP tool and is now built once next to it.

2. The control plane gets a contract

It had none of any kind — no zod, no spec — and its only description was prose plus the hardcoded fetches in the miner's admin client. It now has zod schemas, a committed control-plane/openapi.json generated from the same route table the Worker registers from, and a drift check in test:ci.

control-plane/ is not an npm workspace member (its own lockfile, its own CI install), so a file: edge onto @loopover/contract would need that package built before a separate install runs. It reads a generated mirror instead — byte-identical by construction, with its own --check, the same posture gen-contract-api-schemas.ts already uses in the other direction.

The admin client stops casting. TenantRecord was literally Record<string, unknown>, and listTenants degraded a malformed response to [] — for an admin listing, "you have no tenants" is the most misleading possible answer. Both now parse against the published schema and fail loud, which is the posture that module's own header already claimed.

3. The self-host infra endpoints get specced

/ready, /metrics, /setup, and /setup/callback are answered by src/server.ts's own fetch handler ahead of the Hono app, so the route-spec ratchet cannot see them — they have been served and undocumented since self-host shipped. They are specced under a Self-host infra tag, and the ratchet grants them its one exemption.

That exemption cannot be widened by hand: the set is read back out of src/server.ts and narrowed by what createApp() actually serves. The same rule is why /health and /v1/github/webhook need no entry — the app serves and documents both, the entrypoint merely reaches them first. (My first extractor treated the webhook's dedup check as an interception; deriving the rule instead of listing exceptions is what caught that.)

The defect this surfaced

loopover-miner tenant list printed (unknown) for every tenant name. The renderer read record.name, but the control plane has never returned a top-level name — its projection is { tenant: { name }, ... }. The bug survived because the test fixtures were hand-written in the flat shape, so they agreed with the renderer and neither agreed with the server. Typing the record from the published schema is what surfaced it: the fixtures stopped compiling.

Validation

  • npx vitest run --changed=origin/main: 24646 passed, 21 skipped, 1292 files, on a tree rebased onto current origin/main.
  • tsc --noEmit clean; control-plane:contract:check, control-plane:openapi:check, ui:openapi:check, docs:drift-check, mcp:tool-reference:check, command-reference:check all pass.
  • control-plane's own node --test suite: 234 passed — the schema-backed validators reject exactly what the hand-rolled field walks rejected, with the same messages, which is what an operator reads out of the CLI.
  • New suites: the restated-bounds pins plus accept/reject cases per moved schema (25), the control-plane contract (18), and the self-host infra parity (12).

@loopover-orb

loopover-orb Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-29 09:00:08 UTC

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

⏸️ Suggested Action - Manual Review

  • Required AI review was skipped by a submitter-reputation downgrade: This repository requires blocking AI review, and the submitter's recent-submission signal downgraded this PR to deterministic-only checks. Those checks do not read code semantics, so the gate is held for human review instead of passing automatically.

Review summary
This PR closes #9750 by relocating 52 zod request schemas out of src/api/routes.ts into a new @​loopover/contract leaf package, adding a hand-authored (schema-driven) OpenAPI spec for the previously-undocumented control plane, and generating a mirrored contract module for the non-workspace control-plane package via a codegen script with a --check drift guard. The stated approach is sound: schemas are moved verbatim (not rewritten), bounds that can't travel to a zod-only leaf are restated in limits.ts with a meta-test pinning them, and the one non-portable default (commandAuthorization) is handled via a factory function rather than duplicating a 20-key policy. I cannot verify from the truncated diff that api-requests.ts in routes.ts now has zero z.object( literals as claimed, or that the generated control-plane-contract.ts mirror is actually asserted in CI to match its source, since those files were omitted from the provided content due to review budget.

Nits — 7 non-blocking
  • packages/loopover-contract/src/api-requests.ts:486,509 use 'blacklist' terminology (via COMMAND_AUTHORIZATION_ROLES-adjacent comments/exports); consider 'denylist' per the project's inclusive-terminology convention.
  • control-plane/src/generated/control-plane-contract.ts and control-plane/openapi.json are large generated artifacts; confirm the --check drift guard actually runs in CI so the mirror can't silently diverge from packages/loopover-contract/src/control-plane.ts.
  • The branch is 5 commits behind default; worth rebasing before merge even though all checks currently pass, to catch any late-landing conflicts in routes.ts given how large that file's diff churn likely is.
  • Since I could not see the full diff for src/api/routes.ts, src/mcp/server.ts, or the contract source files (control-plane.ts, limits.ts, index.ts) due to truncation, a follow-up pass focused specifically on those files (verifying the 'zero z.object( literals' claim and the meta-test pinning bounds) would close the gap this review couldn't cover.
  • Confirm scripts/gen-control-plane-openapi.ts's depth-5 nesting at line 112 doesn't hide a branch that silently skips a route from the generated spec.
  • 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.
  • Required AI review was skipped by a submitter-reputation downgrade — Review this PR manually, or run AI review with a trusted override, before merging.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ⚠️ Gate result — Not blocking (Advisory; not blocking this PR.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9750, #9531
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (2 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 370 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 370 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Partially addressed
The visible diff shows a substantial, faithful relocation of request schemas into @​loopover/contract with provenance comments tying back to the issue's exact concerns (e.g. FleetRegisterInstallationInput/registered), plus a comprehensive control-plane OpenAPI spec covering all seven routes described in the issue. However, the diff is truncated before the tenant-client.ts typing, the self-host infr

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: not available
  • Official Gittensor activity: 14 PR(s), 370 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 4 steps 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 &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; 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_review_inconclusive
  • config: 4f94fc11da0abde795c029e7826e1fa61cde55d54f1b04458822b15837816929 · pack: oss-anti-slop · ci: passed
  • record: 809a310287c01020c6cb0d33169710d416de2757e923c870f0bf976817276d9a (schema v5, head 534631e)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

🟩 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

@JSONbored JSONbored self-assigned this Jul 29, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

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

@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 Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 534631e Commit Preview URL

Branch Preview URL
Jul 29 2026, 08:23 AM

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.76190% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.33%. Comparing base (bf654cf) to head (534631e).
⚠️ Report is 6 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/loopover-contract/src/api-requests.ts 98.61% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9757      +/-   ##
==========================================
+ Coverage   90.30%   90.33%   +0.02%     
==========================================
  Files         912      916       +4     
  Lines      113555   113860     +305     
  Branches    26955    26957       +2     
==========================================
+ Hits       102544   102850     +306     
+ Misses       9682     9681       -1     
  Partials     1329     1329              
Flag Coverage Δ
backend 95.57% <99.20%> (+<0.01%) ⬆️
control-plane 100.00% <100.00%> (ø)

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

Files with missing lines Coverage Δ
...trol-plane/src/generated/control-plane-contract.ts 100.00% <100.00%> (ø)
control-plane/src/http-app.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/control-plane.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/limits.ts 100.00% <100.00%> (ø)
packages/loopover-miner/lib/tenant-cli.ts 100.00% <100.00%> (ø)
packages/loopover-miner/lib/tenant-client.ts 100.00% <100.00%> (ø)
src/api/routes.ts 95.59% <100.00%> (-0.05%) ⬇️
src/mcp/server.ts 97.36% <100.00%> (+<0.01%) ⬆️
src/openapi/route-inventory.ts 90.47% <100.00%> (+0.47%) ⬆️
src/openapi/selfhost-infra-route-specs.ts 100.00% <100.00%> (ø)
... and 2 more

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 9.01kB (0.11%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.87MB 9.01kB (0.11%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-BZ8qhSqy.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-YMw-sTHM.js (New) 925.9kB 925.9kB 100.0% 🚀
openapi.json 5.38kB 740.07kB 0.73%
assets/docs.fumadocs-spike-api-reference-BL_qDaRo.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-CCU0oT68.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-CgyuHiFj.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-DTk6TGiX.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-C2FPvqPR.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-DOABpBSp.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-BznUAVOe.js (New) 27.97kB 27.97kB 100.0% 🚀
assets/app-DxpnUa6C.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-KQ9vkBip.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-9XDKV_Hj.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-B-VKEcTu.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-Dx_DTCdb.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-CECM_3Lc.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-CoUUkoAM.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-mry5HGOg.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-CM2Rlp_c.js (New) 12.16kB 12.16kB 100.0% 🚀
assets/app.audit-DzqQ6zYl.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-Yvfy2Wl9.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-DvYRsiSs.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-Br9jaw0d.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DY2EBhsu.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-CBrDrTCw.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow--2WUWRfR.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-BQUC6hRw.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BOoBxAHW.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-Cev28Cc5.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-DLrZ6Vs7.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-CfBHvBXZ.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-3KIsB2oh.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-C4UYtxcy.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-D2cuQnSN.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-CK_7excl.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-BzA1iuAH.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-DT_nEeah.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-6mIrtDOQ.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-CuYOoLVN.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-BWMdS4wa.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-C2q5Dvbz.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-D7xCqwv0.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-DxRQ2mKO.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-DGZuh5Fi.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-KkgYxtrg.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-CZHHjk0a.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-APv1vgSr.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-K-17t3YS.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-DsAZKJyK.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-56O8i3fT.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-BbLM16kR.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-O5D324Px.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-DbvwZRS8.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-Cd9c2TXc.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-DRjT5Hn8.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-BljvdExp.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-GGyUuvxp.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-C1goKiLM.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-BH1SKERP.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-Bl162C5u.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-DDHTQ7JG.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-n4qYmiYC.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-CURBXySL.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-C_TVxaA9.js (Deleted) -922.27kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-ByQEOLVM.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-DGurA78u.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-D9Kfy5Qu.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-dCikoPul.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-CUtWF2RC.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-DE9g-uXj.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-CahX6y_z.js (Deleted) -27.97kB 0 bytes -100.0% 🗑️
assets/app-duUYvOJJ.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-CMbWZ0G1.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-B5GzAfWI.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-BUiy8JRG.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-BEm3cgIq.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-D9n5ouXv.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-B2J22McP.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-BiEfcLM1.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-CaMw_FRY.js (Deleted) -12.16kB 0 bytes -100.0% 🗑️
assets/app.audit-CeQnzt3u.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-CDc-Z_YP.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-QoZteGf-.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-DJfvvm7J.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-B9FcscYT.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-Drhzsu-_.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-D1I26v4-.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-DybRNdmH.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-DRnnXb1Q.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-B9XXQEr0.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-rdwCij2Z.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-CPdFHm2F.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-DcDtXSlm.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-eyKLQPIB.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-Cl0pDd9J.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-Ba4JIPpS.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-BBiq0DeL.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-C9MM08QY.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-Dy5ucQcb.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-BV3-JPjD.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-DK4b8x2H.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-DiBlImtl.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-6bAEMVwL.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-XyIehAXz.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-CzFU3sjk.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-B0H2_xWN.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-CW7n-apw.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-C5Td8Cx-.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-fiIn2QZY.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-AzHW3phz.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-CR5EtpGq.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-DnW_uwsw.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-C4_rXFb6.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-Md2U_8OX.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-jo0AwWin.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-CvKbbaTJ.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-BAsi1uDT.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-BnjyhquM.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-C2OggXBJ.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-BSD83AYG.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-Biqy5YYv.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-D7Kuj2Xt.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-kR-1A4h2.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

…self-host endpoints one contract (#9750)

The three requirements of #9531 that did not land with it.

REQUEST SCHEMAS. All 52 declarations move out of src/api/routes.ts into @loopover/contract, which
leaves that file with zero z.object literals (asserted, not trusted). An MCP tool wrapping one of
these routes now references the same object rather than an independently-authored copy of the
shape -- the drift that made FleetRegisterInstallationInput omit the `registered` field its route
accepts, leaving the opt-out unreachable over MCP until #9518 found it. Every schema is the
declaration that was there, verbatim, so a payload the API accepted is still accepted and one it
rejected is still rejected with the same status; the full suite passing unchanged is the evidence.

Their bounds could not travel with them -- the contract is a zod-only leaf that cannot import the
Worker or the engine -- so limits.ts restates nine of them and a meta-test pins each against its
original, extending the posture PREFLIGHT_LIMITS already established there. The settings write
schema is the one exception to a plain move: its commandAuthorization default is a twenty-key
policy the engine owns, so the contract exports the shape as a factory and the src layer applies
the engine's own value once, rather than the leaf restating a policy that large.

CONTROL PLANE. It had no machine-readable contract of any kind -- no zod, no spec -- and its only
description was prose plus the hardcoded fetches in the miner's admin client. It now has zod
schemas, a committed openapi.json generated from the same route table the Worker registers from,
and a drift check in test:ci. Because control-plane/ is not a workspace member and installs from
its own lockfile, it reads a GENERATED mirror of the contract module rather than importing the
package; the mirror is byte-identical by construction and its own check proves it.

The admin client stops casting. TenantRecord was `Record<string, unknown>`, so a field the control
plane renamed reached a printed table as undefined with nothing reporting it -- and listTenants
degraded a malformed response to `[]`, which for an admin listing is the most misleading possible
answer. Both now parse against the published schema and fail loud, matching the posture that
module's own header already claimed.

Which surfaced a live defect: `loopover-miner tenant list` printed `(unknown)` for EVERY tenant
name. The renderer read `record.name`, but the control plane has never returned a top-level name
-- its projection is `{ tenant: { name }, ... }`. The bug survived because the fixtures were
hand-written in the flat shape, so they agreed with the renderer and neither agreed with the
server. Typing the record from the schema is what surfaced it: the fixtures stopped compiling.

SELF-HOST INFRA. /ready, /metrics, /setup, and /setup/callback are answered by src/server.ts's own
fetch handler, ahead of the Hono app, so the route-spec ratchet cannot see them and they have been
served and undocumented since self-host shipped. They are specced under a `Self-host infra` tag,
and the ratchet grants them its one exemption -- an exemption that cannot be widened by hand,
because the set is read back out of src/server.ts and narrowed by what createApp() actually
serves. That rule is also why /health and /v1/github/webhook need no entry: the app serves and
documents both, the entrypoint merely reaches them first.
@JSONbored
JSONbored force-pushed the feat/contract-schemas-control-plane-9750 branch from cc8fe9d to 534631e Compare July 29, 2026 08:20
@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 29, 2026
@JSONbored
JSONbored merged commit 5220a35 into main Jul 29, 2026
11 checks passed
@JSONbored
JSONbored deleted the feat/contract-schemas-control-plane-9750 branch July 29, 2026 09:05
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

1 participant