Skip to content

Ask whether the person may act as the Bot they named - #37

Open
beardthelion wants to merge 2 commits into
CopilotKit:mainfrom
beardthelion:fix/acting-routes-check-bot-access
Open

Ask whether the person may act as the Bot they named#37
beardthelion wants to merge 2 commits into
CopilotKit:mainfrom
beardthelion:fix/acting-routes-check-bot-access

Conversation

@beardthelion

Copy link
Copy Markdown

Closes #35.

canAccessAgent says a coworker is reachable when it is public, or the person owns it, or they administer the deployment, and 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; the surfaces that take a Bot id from the request never asked at all, so requireUser was 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 a BotAccessCheck the 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.

  • Computer: a use on /:botId/*. The session guard moves into the same chain, so a route added later cannot forget either, and the per-route requireUser comes 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.
  • Tool call (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.
  • Components: what a Bot may draw, whether it may draw one now, and the data function that runs when it does.
  • Listings (GET /api/plugins/for/:agentId, GET /api/components/for-agent/:agentId): a grant list is a fact about the Bot it belongs to.
  • Live screen: the websocket upgrade, after the session guard it already had.

Granting is untouched. An administrator putting a skill or a component on a Bot is a different question and requireAdmin already 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.

server suite failure set is identical to main (integration tests wanting a Postgres). bun run typecheck and bunx biome check are clean on the changed files.

Not covered by a test: the upgrade in index.ts, which calls serve() at module scope and so cannot be imported by one, and the createApp wiring that builds the check from the real store, which needs a database. Both are the same posture as the surrounding code.

`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.
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.

Any signed-in user can act as any Bot: canRunAgent is defined, tested, and never called

1 participant