Make the API usable by an agent, and stop the contract going stale

Someone writing a script against ShopDB had two options: read a 705 KB spec, or
guess. The spec is the reference, not an index, and handing an agent the whole
thing means either truncating it or spending a context window on 479 operations
nobody asked about.

Three additions, each feeding the next.

/api/docs/operations.txt is the index: every operation on one line with its
method, path, tag, auth and summary, at around 70 KB for all 486. A caller can
hold the entire API at once, find the four operations it needs, and only then
fetch their schemas.

/api/docs/openapi.json?tag=... narrows the spec to one of the 41 tag groups.
Components are kept intact deliberately: pruning the schema graph to match would
leave references resolving to nothing, and a generator that fails confusingly is
worse than a file that is larger than strictly necessary.

tools/mcp/ is an MCP server built on both. Five tools rather than 483, because a
tool per operation is unusable - clients degrade past a few dozen and models
start picking the wrong one. It reads everything from the live instance at call
time, so enabling a plugin adds operations it can already reach with no change
here. The token is the security boundary and the README says so plainly: every
operation the credential permits is reachable, so exploratory work wants a
read-scoped token.

The rest of this is about the contract not drifting, which is what makes any of
the above trustworthy.

Two CI jobs:

- api-contract runs the parity gates and then regenerates the spec and fails if
  anything changed. Those gates already existed and already worked - they caught
  both routes added here, unprompted - but nothing ran them except by hand, and
  the committed spec had silently drifted by three operations, including two
  setup endpoints added the day before.
- migrations-mysql is the job the header comment has been claiming for some
  time while not existing. It described "the REAL multi-site deploy path" and
  "what catches an Alembic regression on MySQL before it ships", and there was
  no such job. That is worse than no comment: it is the false confidence that
  let a migration ship which could not build a schema on a fresh MySQL database.
  The suite runs on SQLite, and SQLite accepts a foreign key onto a table that
  does not exist.

CONTRIBUTING.md now says to run gen_openapi.py --refresh after changing a route,
and that a route the sweep cannot classify belongs in PUBLIC_ENDPOINTS with a
reason rather than hand-edited into the inventory, which the next refresh would
overwrite. The new index route is declared there alongside the spec it indexes.
This commit is contained in:
cproudlock
2026-09-20 12:59:49 -04:00
parent 6c00b10618
commit 762d1b7210
11 changed files with 894 additions and 4 deletions

View File

@@ -2445,7 +2445,22 @@
"auth": "none (reviewed: named in the public-read or mutation sweep)",
"params": "not documented",
"purpose": "The generated OpenAPI 3.1 spec (machine + LLM readable).",
"example": "curl http://localhost:5001/api/docs/openapi.json"
"example": "curl http://localhost:5001/api/docs/openapi.json",
"queryparams": [
{
"name": "tag",
"type": "string",
"required": false
}
]
},
{
"method": "GET",
"path": "/api/docs/operations.txt",
"auth": "none (reviewed: named in the public-read or mutation sweep)",
"params": "not documented",
"purpose": "Every operation, one line each: method, path, tag, auth, summary.",
"example": "curl http://localhost:5001/api/docs/operations.txt"
},
{
"method": "GET",
@@ -4780,6 +4795,14 @@
{
"surface": "core-setup",
"endpoints": [
{
"method": "GET",
"path": "/api/setup/checklist",
"auth": "jwt + role:admin",
"params": "not documented",
"purpose": "What is configured and what is not, with a link to each fix.",
"example": "curl http://localhost:5001/api/setup/checklist -H \"Authorization: Bearer $TOK\""
},
{
"method": "POST",
"path": "/api/setup/complete",
@@ -4797,6 +4820,14 @@
"example": "curl -X POST http://localhost:5001/api/setup/create-admin -H 'Content-Type: application/json' -d '{}'",
"readsbody": true
},
{
"method": "GET",
"path": "/api/setup/health",
"auth": "jwt + role:admin",
"params": "not documented",
"purpose": "Post-setup reality check, including whether a restart is outstanding.",
"example": "curl http://localhost:5001/api/setup/health -H \"Authorization: Bearer $TOK\""
},
{
"method": "GET",
"path": "/api/setup/needs-admin",