Found while landing #16781. Pre-existing, unchanged by that PR, and filed rather than fixed there: removing a currently-mounted route surface is a different change with a different blast radius than adding a missing door.
The contract, verbatim
content/docs/api/environment-routing.mdx, the projectResolution table:
| required | Registers only environment-scoped routes for data/meta/AI/automation/package handlers. | Hardened clients that always pass an environment id. |
"only environment-scoped" and "package handlers" are both explicit.
What the code does
packages/runtime/src/dispatcher-plugin.ts honours that for three of the four families named — registerAutomationRoutes, registerActionRoutes, registerAIRoutes all sit inside the required branch and drop their unscoped mounts:
if (enableProjectScoping && projectResolution === 'required') {
registerAutomationRoutes(`${prefix}/environments/:environmentId`);
registerActionRoutes(`${prefix}/environments/:environmentId`);
registerAIRoutes(`${prefix}/environments/:environmentId`);
} else {
…
}
The package routes are not in that branch at all. registerPackageRoutes(prefix) is called unconditionally, several hundred lines earlier, so GET/POST/PATCH/DELETE /api/v1/packages* stay mounted on a required host regardless.
Why it matters rather than being a doc nit
required is the posture a host picks to guarantee every caller names an environment. A host that selected it to close the unscoped surface still serves the whole unscoped /packages family — including DELETE /api/v1/packages/:id and the destructive lifecycle verbs — with no environment segment in the URL. The operator's mental model and the served surface disagree, and the doc is what set the model.
#16781 added the scoped package mount, so on a required host the scoped routes the doc promises now exist. The unscoped ones the doc says should be absent still do too. The asymmetry is recorded in a comment at the mount site rather than silently tidied into a removal.
Two options, both a decision rather than a repair
- Move the code to the doc — put
registerPackageRoutes in the same three-way branch as its siblings. This REMOVES a mounted route surface on required hosts; it needs a ruling on whether any host relies on it, since it can 404 a caller that works today.
- Move the doc to the code — state that package routes are always mounted unscoped and that
required narrows only data/meta/AI/automation.
⛔ Not choosing here; option 1 is a wire removal and belongs to whoever owns that call.
Related: #16781 · #14503 · content/docs/api/environment-routing.mdx.
Found while landing #16781. Pre-existing, unchanged by that PR, and filed rather than fixed there: removing a currently-mounted route surface is a different change with a different blast radius than adding a missing door.
The contract, verbatim
content/docs/api/environment-routing.mdx, theprojectResolutiontable:"only environment-scoped" and "package handlers" are both explicit.
What the code does
packages/runtime/src/dispatcher-plugin.tshonours that for three of the four families named —registerAutomationRoutes,registerActionRoutes,registerAIRoutesall sit inside therequiredbranch and drop their unscoped mounts:The package routes are not in that branch at all.
registerPackageRoutes(prefix)is called unconditionally, several hundred lines earlier, soGET/POST/PATCH/DELETE /api/v1/packages*stay mounted on arequiredhost regardless.Why it matters rather than being a doc nit
requiredis the posture a host picks to guarantee every caller names an environment. A host that selected it to close the unscoped surface still serves the whole unscoped/packagesfamily — includingDELETE /api/v1/packages/:idand the destructive lifecycle verbs — with no environment segment in the URL. The operator's mental model and the served surface disagree, and the doc is what set the model.State after #16781
#16781 added the scoped package mount, so on a
requiredhost the scoped routes the doc promises now exist. The unscoped ones the doc says should be absent still do too. The asymmetry is recorded in a comment at the mount site rather than silently tidied into a removal.Two options, both a decision rather than a repair
registerPackageRoutesin the same three-way branch as its siblings. This REMOVES a mounted route surface onrequiredhosts; it needs a ruling on whether any host relies on it, since it can 404 a caller that works today.requirednarrows only data/meta/AI/automation.⛔ Not choosing here; option 1 is a wire removal and belongs to whoever owns that call.
Related: #16781 · #14503 ·
content/docs/api/environment-routing.mdx.