feat(contract): give the request schemas, the control plane, and the self-host endpoints one contract (#9750) - #9757
Conversation
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-29 09:00:08 UTC
Review summary Nits — 7 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionPartially addressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk 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.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Decision record
Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. Scroll preview
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.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Deploying with
|
| 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 Report❌ Patch coverage is
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
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Bundle ReportChanges will increase total bundle size by 9.01kB (0.11%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
3ea6103 to
1c6052f
Compare
88db104 to
9d1c081
Compare
9d1c081 to
cc8fe9d
Compare
…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.
cc8fe9d to
534631e
Compare


Closes #9750.
The three requirements of the now-closed #9531 that never landed with it.
1. The request schemas move to
@loopover/contractAll 52 declarations leave
src/api/routes.ts, which now has zeroz.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 madeFleetRegisterInstallationInputomit theregisteredfield 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.tsrestates nine and a meta-test pins each against its original, extending the posturePREFLIGHT_LIMITSalready 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'scommandAuthorizationdefault 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.jsongenerated from the same route table the Worker registers from, and a drift check intest:ci.control-plane/is not an npm workspace member (its own lockfile, its own CI install), so afile:edge onto@loopover/contractwould 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 posturegen-contract-api-schemas.tsalready uses in the other direction.The admin client stops casting.
TenantRecordwas literallyRecord<string, unknown>, andlistTenantsdegraded 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/callbackare answered bysrc/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 aSelf-host infratag, and the ratchet grants them its one exemption.That exemption cannot be widened by hand: the set is read back out of
src/server.tsand narrowed by whatcreateApp()actually serves. The same rule is why/healthand/v1/github/webhookneed 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 listprinted(unknown)for every tenant name. The renderer readrecord.name, but the control plane has never returned a top-levelname— 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 currentorigin/main.tsc --noEmitclean;control-plane:contract:check,control-plane:openapi:check,ui:openapi:check,docs:drift-check,mcp:tool-reference:check,command-reference:checkall pass.control-plane's ownnode --testsuite: 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.