Skip to content

Keep a Bot from being named after a deployment route - #152

Merged
davidmckayv merged 1 commit into
CopilotKit:mainfrom
beardthelion:fix/reserve-deployment-route-bot-ids
Aug 22, 2026
Merged

Keep a Bot from being named after a deployment route#152
davidmckayv merged 1 commit into
CopilotKit:mainfrom
beardthelion:fix/reserve-deployment-route-bot-ids

Conversation

@beardthelion

Copy link
Copy Markdown
Contributor

Closes #151.

The bot-access guard in server/src/computer/routes.ts steps aside for policy and fleet, which are the router's own paths and not about a Bot. It has to: Hono matches /* against zero segments, so /policy arrives 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/status is /:botId/status with a Bot called policy, and for every path under those two names canUseBot was 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.

validateTenantPackage refuses 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 is agent_<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.

synchronizeTenantPackage checks the agents table 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 shipped policy keeps 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.ts because 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.ts drives 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 under policy or fleet answers 404 and nothing reaches the computer, with canUseBot recorded 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.ts for the subtree, including /policy and /fleet still 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 in tenant-package.test.ts: both reserved ids refused, policy-desk still 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 in agent-profile-store.integration.test.ts pinning that create and duplicate mint 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 main with 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.

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 davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@davidmckayv
davidmckayv merged commit be18bc9 into CopilotKit:main Aug 22, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A Bot named after a deployment route is served without the access check being asked

2 participants