docs: generate the route reference from every place routes are registered - #191
Merged
Conversation
…ered
The route reference was built by scanning `internal/api` alone, so it omitted
sixteen routes on a page whose own `read_when` invites an operator to audit
what an instance exposes. Someone using it to enumerate exposure concluded
`/debug/pprof/*` and `/-/metrics` were not served.
Missing were the eleven operational, protocol and SPA routes wired up in
`cmd/fanout` — `/-/metrics`, six `/debug/pprof/*`, `/mcp`, `/api/mcp`, and the
SPA catch-all — and the five `/api/agent` routes in the agent runtime, which
the caveat did not even mention because it named only the first group.
`POST /api/agent` was missing for a second reason worth stating separately: a
group's own root is registered as `group.POST("", ...)`, and the collector
required a leading slash, so the one route that actually runs the investigator
was dropped silently rather than refused. An empty relative path is now a real
route when the receiver is a known group.
`Any` registrations are collapsed to one row only when the middleware gives
every method the same answer; where it would not, the build fails rather than
publish a requirement that is wrong for some verb. Both current `Any` routes
qualify.
The scan list is a flag with each directory named. A directory in it that
registers nothing is an error, so the list going stale is loud rather than a
quietly shorter table — the same property the existing per-run check had, moved
to per-directory. It counts what a directory registers rather than how much it
adds to the total, because a route registered in two places would dedupe to
nothing new and read as a directory registering none.
The page now says what it covers instead of carrying a caveat, and states which
groups are conditional: pprof off by default, MCP on by default, the agent only
with a provider key. That distinction is what an exposure audit needs, and the
previous text gave none of it.
Each guard was verified by breaking it: an unregistered group prefix, a scan
directory that registers nothing, and the per-directory count — the last fails
the build over a duplicated directory if it counts newness instead. Routes went
45 to 61.
Closes #188.
vishr
added a commit
that referenced
this pull request
Aug 24, 2026
…ered (#191) The route reference was built by scanning `internal/api` alone, so it omitted sixteen routes on a page whose own `read_when` invites an operator to audit what an instance exposes. Someone using it to enumerate exposure concluded `/debug/pprof/*` and `/-/metrics` were not served. Missing were the eleven operational, protocol and SPA routes wired up in `cmd/fanout` — `/-/metrics`, six `/debug/pprof/*`, `/mcp`, `/api/mcp`, and the SPA catch-all — and the five `/api/agent` routes in the agent runtime, which the caveat did not even mention because it named only the first group. `POST /api/agent` was missing for a second reason worth stating separately: a group's own root is registered as `group.POST("", ...)`, and the collector required a leading slash, so the one route that actually runs the investigator was dropped silently rather than refused. An empty relative path is now a real route when the receiver is a known group. `Any` registrations are collapsed to one row only when the middleware gives every method the same answer; where it would not, the build fails rather than publish a requirement that is wrong for some verb. Both current `Any` routes qualify. The scan list is a flag with each directory named. A directory in it that registers nothing is an error, so the list going stale is loud rather than a quietly shorter table — the same property the existing per-run check had, moved to per-directory. It counts what a directory registers rather than how much it adds to the total, because a route registered in two places would dedupe to nothing new and read as a directory registering none. The page now says what it covers instead of carrying a caveat, and states which groups are conditional: pprof off by default, MCP on by default, the agent only with a provider key. That distinction is what an exposure audit needs, and the previous text gave none of it. Each guard was verified by breaking it: an unregistered group prefix, a scan directory that registers nothing, and the per-directory count — the last fails the build over a duplicated directory if it counts newness instead. Routes went 45 to 61. Closes #188.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The route reference was generated by scanning
internal/apialone. Itsread_wheninvites an operator to audit what an instance exposes, and itomitted sixteen routes — so someone using it for exactly that concluded
/debug/pprof/*and/-/metricswere not served.What was missing
Eleven operational, protocol and SPA routes registered in
cmd/fanout:/-/metrics, six/debug/pprof/*,/mcp,/api/mcp, and the SPA catch-all.And five
/api/agentroutes in the agent runtime. The page's caveat did notmention those at all — it named only the first group, so the table was missing
routes the page did not admit to missing.
POST /api/agentwas missing for a second reason worth separating out. Agroup's own root is registered as
group.POST("", ...), and the collectorrequired a leading slash — so the one route that actually runs the investigator
was dropped silently rather than refused. Same shape as the group-prefix bug
fixed in #187, in the same function: a path the collector could not read became
an omission instead of an error.
Routes went 45 → 61.
What this changes
(
internal/api,internal/agent,cmd/fanout). A directory in it that registersnothing fails the run, so the list going stale is loud rather than a quietly
shorter table. It counts what a directory registers rather than how much it
adds to the total — a route registered in two places would otherwise dedupe to
nothing new and read as a directory registering none.
Anycollapses to one row only when the middleware answers identically forevery method it distinguishes. Where it would not, the build fails rather than
publish a requirement that is wrong for some verb. Both current
Anyroutesqualify.
groups are conditional: pprof off by default, MCP on by default, the agent
only with a provider key. That is the distinction an exposure audit needs, and
the previous text gave none of it.
The property that matters is unchanged: paths are parsed from the source, the
authorization requirement is asked of the middleware. A generator that re-read
classifyRoutewould be a second authorization model free to drift from the onethat runs.
Verification
Each guard was verified by breaking it deliberately:
Runtime.RegisterfromgroupPrefixes.to the scan listjust checkgreen. Six collector tests, including one that pinsPOST /api/agentand one that pins the routes registered outsideinternal/api, so neither omission can return quietly.Two things I corrected mid-work
conditional. Three groups are. That is the same class of error as the bug
being fixed.
/api/agentreturns 404 rather than 403.With
e.Usemiddleware and aGET /*catch-all registered that is likelywrong, and I could not verify it, so it is gone rather than guessed.
Also fixes a stale error message pointing at
routes.goforgroupPrefixes,which lives in
groups.go.Closes #188.