Keep a Bot from being named after a deployment route - #152
Conversation
The computer router's bot-access guard steps aside for the paths that are about the deployment rather than about a Bot. It has to: Hono matches `/*` against zero segments, so `/policy` and `/fleet` arrive at `/:botId/*` as Bot ids, and nothing in the request tells them apart from a Bot that really is called that. A Bot with one of those ids is therefore served to anybody who can sign in, and the guard is not bypassed so much as never asked. Closed at the three places it is reachable. A package id is the only way a Bot gets a chosen id, since everything created through the API is `agent_<uuid>`, so the YAML is refused first. That is also the answer an operator can act on, and it sits with this file's other cross-file checks rather than adding a lookup to a per-request guard. Refusing the file does not remove a Bot that already has the name. Nothing here deletes a canonical agent when a package stops declaring it, so a deployment that once shipped `policy` keeps the row after the operator corrects the YAML. So the table is checked too, inside the synchronise transaction, and a deployment holding one refuses to start rather than serving it. And the guard itself now steps aside only for the deployment path, not for everything beneath it. Both routes are a single segment, so `/policy/status` is `/:botId/status` and nothing else; skipping the subtree was the actual hole, and it stays closed whether or not such a Bot can be created. The list moves into its own module because both ends now have to agree on it: adding a third deployment route should reserve the name at the same moment it starts being answered.
davidmckayv
left a comment
There was a problem hiding this comment.
Approving. This closes a hole I opened earlier tonight when I added fleet beside policy and skipped on the name alone, so thank you for catching it, and for the comment in the original that asked a later deployment route to think about that line. It did not, and you noticed.
Driven both ways against a running deployment rather than only read:
On main, GET /api/computers/policy/status answers {"botId":"policy","state":"absent"}. A non-existent Bot at /api/computers/notarealbot/status answers {"error":"There is no such Bot."}. Same shape of request, opposite treatment, canUseBot never asked for the first.
On this branch, /api/computers/policy/status answers {"error":"There is no such Bot."}, and both routes the exception exists for still work: /admin/boundaries renders its rules and /admin/computers lists the running fleet. A fix that closed the subtree by breaking those two would have looked identical from the API alone.
Locally: lint, format, typecheck clean, full suite green over ten consecutive runs.
The three-places argument is right, and so is refusing at start-up rather than renaming somebody's Bot. deployment-routes.ts earns its own file for exactly the reason given, which this PR is the evidence for.
Closes #151.
The bot-access guard in
server/src/computer/routes.tssteps aside forpolicyandfleet, which are the router's own paths and not about a Bot. It has to: Hono matches/*against zero segments, so/policyarrives at/:botId/*as a Bot id, and without the exception the Boundaries screen 404s for everybody including an administrator.It stepped aside by name alone, so it covered the whole subtree.
/policy/statusis/:botId/statuswith a Bot calledpolicy, and for every path under those two namescanUseBotwas not called at all.What it does
Three places, because the name can be reached from three directions.
The guard now steps aside only for the deployment path itself. Both routes are a single segment, so anything below one of them is a Bot path and is guarded like any other. This is the actual hole and it closes whether or not such a Bot can exist.
validateTenantPackagerefuses an agent whose id is one of those names. A package id is the only way a Bot gets a chosen id, since everything created through the API isagent_<uuid>, so this is where the name stops being available. Refusing at load is also an answer an operator can act on, and it sits with this file's other cross-file checks rather than adding a lookup to a per-request guard.synchronizeTenantPackagechecks theagentstable as well, inside its transaction. Refusing the YAML does not remove a Bot that already has the name: nothing here deletes a canonical agent when a package stops declaring it, so a deployment that once shippedpolicykeeps the row after the operator corrects the file. A deployment holding one now refuses to start, naming the Bot, rather than serving it.The list moves into
computer/deployment-routes.tsbecause both ends of the system have to agree on it. Adding a third deployment route should reserve the name at the same moment it starts being answered, rather than reopening this and waiting for somebody to notice.Verification
deployment-route-bot-end-to-end.test.tsdrives the real router through the real gateway and the real transport, with a listener on a socket reporting what arrived. With the guard denying, a request underpolicyorfleetanswers 404 and nothing reaches the computer, withcanUseBotrecorded as having been asked. With the guard permitting that same id, the request arrives at/read. The refusal is therefore the guard's answer rather than a route that could not be reached anyway, and a permitted ordinary Bot arriving is the control that makes the empty arrivals mean something.Six cases in
bot-access.test.tsfor the subtree, including/policyand/fleetstill being served to an administrator, since a guard that closed this by breaking the two routes it exists to let through would have passed a narrower test. Five intenant-package.test.ts: both reserved ids refused,policy-deskstill accepted so the check is exact rather than a prefix, the table check refusing with nothing half-applied behind it, and a permissive twin proving that check is about the reserved id and not about any pre-existing row. One inagent-profile-store.integration.test.tspinning thatcreateandduplicatemint their own ids, because the argument for fixing this at the package rests on that and it should be a test rather than something a reader notices.Each guard was checked by removing it: the subtree skip restored turns six red, the package check off turns two red, the table check off turns one red.
Suite is 871 passing against 855 on
mainwith a database, no failures either side, and the sixteen added tests are the whole difference. Typecheck and biome clean.What this does not close
An operator upgrading into this with such a Bot already in the table gets a refusal to start rather than a migration. That is deliberate, since renaming somebody's Bot and repointing whatever refers to it is not this function's call to make, but it does mean the fix arrives as a startup error naming the Bot rather than as a silent repair.