Type the REST API's responses

Every tool now returns a Go struct (apitypes.go) instead of a map, and
typeschema.go derives the OpenAPI response schemas from those types by
reflection (json, doc and enum tags), so the document cannot drift from
what is sent. Text results are documented as text/plain, settings in
answers use the typed settings schema, and edits answer with EditResponse.

TestResponsesMatchTheirSchemas checks real responses against the
generated schemas.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-11-05 08:10:59 +01:00
co-authored by Claude Sonnet 5.5
parent e3209569bd
commit 98b0ebc130
8 changed files with 436 additions and 79 deletions
+2 -1
View File
@@ -125,6 +125,7 @@ Learned along the way:
- `/api/v1/…` with an OpenAPI 3.1 description at `GET /api/openapi.json` (no token: it describes the interface only). Same tokens, scopes, origin check, limits and audit log as MCP.
- **One implementation**: each operation runs an MCP tool (`apiRoutes` in `api.go` maps method and path to a tool; query parameters or `If-Match` and the body become its arguments), and `TestOpenAPI` checks that every tool has an operation. Status, settings (`ETag` header), `PATCH /settings` (merge patch, `If-Match`; 409 when stale), `settings/check`, `changes` (GET the diff as text, DELETE to discard), `interfaces`, `dhcp-leases`, `update`, `apply-log`, `logs`, `options`.
- **Settings areas** (`/api/v1/settings/router/firewall`, `.../router/lans/<name>`, `.../system`, down to a single option): `GET` reads, `PUT` replaces, `PATCH` merges (objects), `DELETE` removes, with the settings' `ETag`/`If-Match` and the same limits as the whole-document edit (`editSettings`). Values are checked against a JSON Schema made from the option documentation (`schema.go` reads the types nixosOptionsDoc describes in words, e.g. "null or integer between 1 and 4094"; a type it does not know becomes "anything", so it never refuses a valid setting): a wrong type, a value out of range or a key that is not an option is refused with its place. The OpenAPI document has these paths (three levels deep) with their schemas; lists are replaced whole (elements have no paths). Tested with the real option file (`ROUTER_OPTIONS_JSON`, `LIVE_SETTINGS_JSON` for a router's own settings) and in `apply`.
- **Typed responses**: every tool returns a Go struct (`apitypes.go`); `typeschema.go` derives the OpenAPI response schemas from those types by reflection (`json` tags, `doc` and `enum` tags), so the document cannot drift from what is sent. `TestResponsesMatchTheirSchemas` checks real responses against the generated schemas. The settings in answers use the typed settings schema when the option documentation is present. Hand-written reflection, not a library: the web UI is stdlib-only (`vendorHash = null`).
- **API reference** page in the web UI (`/apireference`, linked from API access): Redoc (pinned `redoc@2.5.2` bundle, fetched by `pkgs/router-webui/package.nix`, served from the UI's own address) rendering the generated `/api/openapi.json`. Only that page gets a nonce for Redoc's styles and `worker-src blob:` in its CSP. The spec's own text explains authentication, saved-not-applied, ETags/`If-Match`/409 and errors; every operation has a curl sample. Established tooling instead of a hand-built page (swaggo rejected: spec is generated at runtime from the MCP tools and the option docs). No "try it" form.
- Errors are `{"error": "…"}` with 400 (refused or bad argument), 401, 403 (scope), 409 (stale ETag), 500.
@@ -225,7 +226,7 @@ verity. The image module and its test (`checks.image`) stay as they are for anyo
Remote access
- **MCP and REST: an `apply` scope**: build, activate and confirm. A token with it changes the running system, so it needs its own design (probably a short expiry and a confirmation the token cannot give).
- **More read tools**: metrics history, WireGuard status (handshakes, transfer), certificate status.
- **REST API**: the rest of what the web UI does (apply and confirm, metrics, updates); typed schemas for the responses of the read operations (status, leases, interfaces; settings areas are typed); the web UI could use the same handlers.
- **REST API**: the rest of what the web UI does (apply and confirm, metrics, updates); the web UI could use the same handlers.
- **GraphQL** on top of it, possibly: one query for the overview and for related settings.
- Rule for all of it: changes go through `settings.json` and `router-apply` (commit-confirm), never straight to the live system; tokens have scopes; every call is logged.