Skip to content

api: one route-registration seam — runtime zod validation + spec emission, with a route↔spec ratchet to zero unspecced routes #9519

Description

@JSONbored

Context

Part of #9515. The Worker's route table and its OpenAPI spec are two disconnected hand-written lists: createApp() (src/api/routes.ts:1154) registers 242 routes; buildOpenApiSpec() (src/openapi/spec.ts:141) hand-registers 128 paths / 151 operations. Measured delta: 91 live routes have no spec entry — including every ORB management surface this epic wants MCP to drive (POST /v1/orb/token, /v1/orb/relay*, /v1/orb/webhook, /v1/orb/oauth/callback, all /v1/internal/orb/*, /v1/internal/fleet/analytics, /v1/internal/ops/stats, /v1/app/fleet/config-push, /v1/app/kill-switch, the DLQ admin quartet, PUT /v1/app/installations/:id/agent/bulk-settings, ~20 /v1/internal/jobs/*). Nothing in CI can detect this: ui:openapi:check only compares the generated file to the generator, and test/unit/openapi.test.ts is a hand-allowlist that never enumerates app.routes.

Compounding it: request bodies are validated by 38 inline zod schemas in src/api/routes.ts (:479+) that never appear in the spec; response schemas exist only in src/openapi/schemas.ts and never validate anything at runtime; security metadata is bolted on post-hoc by a path-prefix list (isProtectedPath(), spec.ts:2090) that is a second implementation of the real gates (requiresApiToken, requireAppRole, the /v1/internal/* middleware) and already disagrees with them; the spec has tags: [] and no systematic operationIds; scripts/check-openapi-settings-parity.ts papers over hand-mirroring for exactly 2 of ~150 schemas. The self-host infra endpoints (/health, /ready, /metrics, /setup* in src/server.ts:1052-1180) and the entire control-plane/ Worker (control-plane/src/http-app.ts — no zod at all; its only client contract is prose + the hardcoded fetches in packages/loopover-miner/lib/tenant-client.ts) are outside the contract entirely.

Decision record — recorded here, not re-litigated per PR

A local registration shim, not @hono/zod-openapi. One wrapper — e.g. defineRoute({ method, path, tags, operationId, auth, request, responses }, handler) — that (i) registers on the Hono app, (ii) parses/validates the request with the zod schemas, (iii) contributes the spec entry to the registry the existing @asteasolutions/zod-to-openapi pipeline already consumes, and (iv) derives the security stanza from the same auth value the runtime middleware enforces. Rationale: 242 routes migrate incrementally (a shim coexists with plain app.get(...); @hono/zod-openapi would force a createApp() rewrite and re-couple the zod version), and the emit pipeline already exists. Overturn condition: only if the shim ends up reimplementing a majority of @hono/zod-openapi's surface — record here first.

Runtime response validation posture (program principle 2): responses are validated against their declared schema (a) always in vitest/CI, (b) at runtime behind LOOPOVER_VALIDATE_RESPONSES — default ON for the self-host container and local dev, default OFF on the production Worker (latency/CPU budget), failures logged + captured as errors, never 500ing an otherwise-good response. Exact env-var semantics documented in the generated env reference.

Requirements

  1. Build the shim (with tags + stable operationIds mandatory per route) and migrate a pilot of 5 routes end to end, including at least one currently-unspecced ORB management route (GET /v1/internal/orb/instances) and one /v1/app/* role-gated route.
  2. The ratchet check (new, wired into test:ci): enumerate the real Hono route table (app.routes) and fail if any route lacks a spec entry, with a committed baseline allowlist of the currently-unspecced 91 that may only shrink (same ratchet mechanics as branding-drift-baseline.json). Reaching zero retires the baseline file.
  3. Relocate the pilot routes' request schemas into @loopover/contract, establishing the pattern the bulk migration follows. Migrating the remaining ~237 routes is deliberately NOT in this issue — it is a mechanical, reviewable-in-batches job that the ratchet baseline drives to zero, tracked separately so this issue stays a single reviewable PR.
  4. isProtectedPath() and applySecurityMetadata() are deleted; security stanzas derive from each route's declared auth, and a meta-test asserts declared auth matches the enforcing middleware's behavior for every route class (public / token / session+role / internal).
  5. check-openapi-settings-parity.ts is retired once RepositorySettingsSchema/RepoSettingsPreviewSchema are z.infer-derived from the same source the handlers serialize (its 2-schema regex diff becomes obsolete by construction).
  6. Same seam for the other HTTP surfaces: control-plane/src/http-app.ts adopts zod + the shim and emits its own committed control-plane/openapi.json (with its own drift check); packages/loopover-miner/lib/tenant-client.ts types its requests/responses from those contract schemas. The four self-host infra endpoints in src/server.ts get spec entries (a selfhost-infra tag) so the self-host deployment's full HTTP surface is described.
  7. Full branch-counted coverage on the shim + every migrated batch; defects found in migrated handlers fixed in-batch.

Non-goals

  • MCP tool registration (that's the contract/migration sub-issues — though route request/response schemas landing in @loopover/contract is exactly what lets MCP tools reference them later).
  • New endpoints; behavior changes beyond validation-rejecting inputs that were already invalid per the existing inline schemas.

Deliverables

  • defineRoute shim + pilot 5 routes
  • Route↔spec ratchet in test:ci with shrinking baseline → zero
  • Pilot routes registered through the shim with their request schemas relocated; tags/operationId mandatory on every route the shim registers
  • Security stanzas derived from real auth declarations; isProtectedPath deleted; settings-parity script retired
  • control-plane spec + typed tenant client; self-host infra endpoints specced
  • LOOPOVER_VALIDATE_RESPONSES runtime gate implemented + documented

Expected outcome

The seam exists and is proven on real routes, and adding a new route without a spec entry fails CI — the baseline can only shrink. Request and response shapes are runtime-validated wherever the posture above says so. Driving the baseline to zero (and thereby fully describing the ORB management surface) is the follow-on migration issue's job; this issue makes that work mechanical and makes regression impossible.

References

Part of #9515. Independent of #9517 (can run in parallel). Unblocks #9282's full migration and the ORB management tool family.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.orbGittensory Orb related - maintainer self-hosting analytics.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions