Files
mcpctl/.taskmaster/tasks/tasks.json
Michal 2a85447fac feat(rbac): bound what an agent may DO by whoever invoked it
Closes 177. An agent carries a project's prompts and its MCP tools, and the only
check on a chat turn was run:agents:<name> - so anyone who could talk to an
agent inherited tool access to every project that agent had joined. Membership
was the capability. AgentProject widened it from one pinned project to many.

The rule now: knowledge travels with the agent, action is bounded by the
authority of the run. Prompts and skills still load regardless of who is asking -
a QA reviewer that forgets the project's conventions when a different person
asks is not a reviewer. Every MCP tool call is gated.

Authority is resolved once per run (services/run-authority.ts) and is per-RUN,
not per-agent: the same agent answers a human under that human's authority and,
once a scheduler exists, runs unattended under its own. `caller` intersects the
agent's memberships with the caller's run:projects - reusing the exact grant the
project-chat route already demands, rather than inventing a parallel permission.
`agent` takes every membership without consulting a user. Both are implemented;
`service-account` throws 501 rather than honouring a forgeable x-service-account
header, and task 179 records what it needs first.

Gated in BOTH listTools and callTool, deliberately: withholding a tool from the
list is presentation, not enforcement - a model can invent a tool name it was
never shown, and callTool is what would run it.

The accepted consequence is stated plainly rather than papered over: a user can
ask about a project they cannot reach and the agent will answer from its
knowledge. The system block tells the model when it is holding such context so a
persona can apply it without reciting it. That is guidance to a model, not a
boundary - the boundary is that the tools are absent.

Two sibling holes found while exploring and closed here:

- Thread hijack. resolveThreadId on the agent path had no owner check, so anyone
  with run:agents:<name> could append to another user's thread by id and read
  its history back. The project path already did this correctly; mirrored, 404
  not 403 so ids cannot be probed.
- McpToken project escape. `project` travels in the request BODY, so the
  URL-based token scope check in main.ts never saw it - a token bound to project
  A could drive an agent into B's tools. The binding is now a ceiling.

Tests: 19 authority tests (intersection, fail-closed with no user and with no
RBAC wired, token ceiling, agent authority bypassing RBAC entirely, the 501) and
4 through the real chat path asserting prompts load while tools do not.

Three existing assertions updated for the new dispatcher signature - a
deliberate interface change, not tests bent to fit.

Local: build clean, workspace 2608 passed (+16), lint unchanged at 794 errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-15 23:54:53 +01:00

347 KiB