Ask whether the person may act as the Bot they named - #37
Open
beardthelion wants to merge 2 commits into
Open
Conversation
`canAccessAgent` says a coworker is reachable when it is public, or the person owns it, or they administer the deployment. `canRunAgent` is exported as its alias. Both are called by their own unit test and by nothing else: the roster and the runtime get the rule from the store's read filter, and the surfaces that act as a Bot never asked at all. So `requireUser` was the whole gate on acting. Any signed-in account could name any Bot and reset its computer, drive its pages, read its workspace, watch its screen, and fire its granted MCP tools against the deployment's stored credential, including for a private coworker belonging to somebody else. The Bot travels in the path for the computer and in the body for a tool call, and neither was resolved against a row. The check is asked once per surface rather than per route. On the computer that is a `use` on `/:botId/*`, which is also where the session guard now lives, so a route added later cannot forget either. Reads are gated with actions: a screenshot of somebody's Bot is whatever page it is signed into. The tool call asks before the grant is looked up, because the grant says the Bot may use the tool and says nothing about who is asking. The live-screen socket asks after the session guard it already had. A missing Bot and somebody else's Bot answer the same way, so this cannot be used to find out which coworkers a deployment has. The answer comes from the store's own `get`, which already filters on the same policy, rather than a second copy of the rule that could drift. A deployment with no profile store has no agents table and so no private Bot to protect, and keeps working.
The check landed on the computer, the tool call and the live-screen socket, and the components surface names a Bot the same way and was left open. Three routes: what a Bot may draw, whether it may draw one now, and the data function that runs when it does. All three took the Bot from the request behind `requireUser`, and the last one executes, so a caller borrowing a coworker borrowed whatever its components had been granted. Also `GET /api/plugins/for/:agentId`, which lists what a Bot holds. Same shape as the component listing, and the same disclosure: it says which tools somebody else's private coworker has been given. Granting stays where it was. An administrator putting a component on a Bot is a different question and `requireAdmin` already answers it. The two existing suites that build these routers now pass a permissive check, since what they cover is the decision and the grant rather than who may ask.
beardthelion
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
August 20, 2026 04:27
2 tasks
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.
Closes #35.
canAccessAgentsays a coworker is reachable when it is public, or the person owns it, or they administer the deployment, andcanRunAgentis exported as its alias. Both are called by their own unit test and by nothing else. The roster and the runtime get the rule from the store's read filter; the surfaces that take a Bot id from the request never asked at all, sorequireUserwas the whole gate on acting as one.What it does
Asks the store's existing
get(actor, id), which already applies the same policy, once per surface rather than once per route. The check arrives as aBotAccessCheckthe caller supplies, so a router depends on the question rather than on the agents table, and there is no second copy of the rule to drift from the first.useon/:botId/*. The session guard moves into the same chain, so a route added later cannot forget either, and the per-routerequireUsercomes off, which also drops the second session lookup per request. Reads are gated with actions: a screenshot of somebody's Bot is whatever page it is signed into.POST /api/plugins/call): asked before the grant is looked up. The grant says the Bot may use the tool; it says nothing about who is asking, and the call leaves on the deployment's own credential either way.GET /api/plugins/for/:agentId,GET /api/components/for-agent/:agentId): a grant list is a fact about the Bot it belongs to.Granting is untouched. An administrator putting a skill or a component on a Bot is a different question and
requireAdminalready answers it.A missing Bot and somebody else's Bot answer identically, so none of this can be used to find out which coworkers a deployment has.
A deployment with no profile store has no agents table, so no private Bot to protect, and keeps working.
Verification
16 cases in
server/tests/bot-access.test.ts, driving the real routers. Eight refusals fail before the change and pass after, each also asserting that the store, the gateway, or the vendor call was never reached, since a check that runs after the browser has been wiped is not a check.The cases that must not move are in there too, and passed throughout: the owner still acts, an administrator still reaches any Bot, an unauthenticated caller still gets 401 before anything is asked about a Bot, and a private Bot is indistinguishable from a missing one.
Two existing suites build these routers and now pass a permissive check, because what they cover is the decision and the grant rather than who may ask.
serversuite failure set is identical tomain(integration tests wanting a Postgres).bun run typecheckandbunx biome checkare clean on the changed files.Not covered by a test: the upgrade in
index.ts, which callsserve()at module scope and so cannot be imported by one, and thecreateAppwiring that builds the check from the real store, which needs a database. Both are the same posture as the surrounding code.