Skip to content

openapi(#9531 remainder): relocate the inline request schemas, spec the control plane, and spec the self-host infra endpoints #9750

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

#9531 drove the route↔spec ratchet to zero and deleted its baseline: every route createApp() serves now has an operation, isProtectedPath is gone, and the settings-parity script has been replaced by a compile-time assertion. Three of its deliverables did not land with it, and the issue is closed, so they have no home.

1. The 38 inline request schemas are still in src/api/routes.ts. Measured on main: 39 z.object( literals and 75 .safeParse(/.parse( call sites in that one file. #9531's requirement 2 was to relocate them to @loopover/contract "so the 38 inline schemas in routes.ts are gone at the end and any MCP tool wrapping the same route references the same schema object rather than a copy." Today an MCP tool and its route validate against two independently-authored shapes, which is exactly the duplication #9515 exists to remove — and it is not theoretical: #9518's migration found FleetRegisterInstallationInput never declaring the registered field its route accepts, making the opt-out unreachable over MCP.

2. control-plane/src/http-app.ts has no zod and no spec at all. Seven routes (GET /health, POST /v1/tenants, GET /v1/tenants, POST /v1/tenants/rollout, PATCH /v1/tenants/:name/orb-installation, DELETE /v1/tenants/:name, POST /v1/orb/webhook) and zero z.object in the file. Its only contract is prose plus the hardcoded fetches in packages/loopover-miner/lib/tenant-client.ts, whose TenantRecord is literally Record<string, unknown> — the client cannot tell a caller what a tenant even has on it. #9531 requirement 7.

3. The four self-host infra endpoints in src/server.ts are unspecced. /health (:1100), /ready (:1105), /metrics (:1112), and /setup + /setup/callback (:1120, :1134, :1154) are served by the Node entrypoint's own fetch handler, before the Hono app, so the ratchet does not see them and the published document does not mention them. A self-host operator wiring a healthcheck has prose only. #9531 requirement 7.

Requirements

  • Relocate every inline request schema in src/api/routes.ts into @loopover/contract, and have the routes import them. Where an MCP tool already declares a schema for the same payload, the two must become one exported object — not two that happen to agree.
  • Give control-plane/src/http-app.ts zod request/response schemas, register its routes through the defineRoute seam, emit its own committed OpenAPI document, and add a drift check for it to test:ci mirroring ui:openapi:check.
  • Type packages/loopover-miner/lib/tenant-client.ts from those schemas: TenantRecord stops being Record<string, unknown> and becomes z.infer of the control-plane's own tenant schema, and each call parses its response rather than casting it.
  • Spec the four self-host infra endpoints under a selfhost-infra tag. They are served outside the Hono app, so they need spec entries plus a test asserting the specced set matches the paths src/server.ts actually intercepts — the ratchet cannot see them, and an unwatched hand-list is the rot this epic keeps finding.
  • npm run ui:openapi regenerated and committed; test:ci green.

⚠️ Required pattern: relocated schemas keep their current runtime behavior exactly — this is a move, not a re-validation. A payload the route accepts today must still be accepted, and one it rejects must still be rejected with the same status. Where a relocated schema and an MCP tool's copy genuinely disagree, the disagreement is a defect: fix it and name it in the PR, do not widen one side to match the other.

Deliverables

  • Zero z.object( request-schema literals left in src/api/routes.ts; all imported from @loopover/contract
  • Every MCP tool wrapping one of those routes references the same exported schema object
  • control-plane routes registered through the seam with zod schemas, its own committed spec, and a drift check wired into test:ci
  • tenant-client.ts types itself from the control-plane schemas and parses responses; TenantRecord is no longer Record<string, unknown>
  • The four self-host infra endpoints specced under selfhost-infra, with a test pinning the specced set against what src/server.ts intercepts
  • Any route-vs-schema disagreement surfaced by the move is fixed in-PR and named in the PR body
  • apps/loopover-ui/public/openapi.json regenerated and committed

Test Coverage Requirements

src/** is inside Codecov's src/** include and the 99% branch-counted patch gate applies; packages/loopover-contract/src/** and packages/loopover-miner/lib/** are also in vitest's coverage.include. Every relocated schema needs both an accept and a reject case at its route, because a move that silently loosens validation looks identical to a correct one. The self-host infra parity test is the named regression test for deliverable 5 and must derive the intercepted path list from src/server.ts, not restate it.

Expected Outcome

No request shape in this repo is written down twice. The control plane stops being the one surface with no machine-readable contract, its admin client knows what a tenant is, and a self-host operator can read the endpoints their deployment serves out of the published document.

Links & Resources

src/api/routes.ts:479 onward; control-plane/src/http-app.ts:117-308; packages/loopover-miner/lib/tenant-client.ts:31; src/server.ts:1100-1160. Completes the unlanded requirements 2, 3, and 7 of the closed #9531. Part of #9515.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions