Repository navigation
feat(agents): attach access groups to agents and enforce them for models, MCP servers and agent calls - #41634
Conversation
…els, MCP servers and agent calls Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…e gate Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
…ching it Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ne budget Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…e and cap the model listing Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ments Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…try instead of the DB on the request path Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…deQL sees no fall-through Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai please re-review at 7229fc1: the registry read-through refactor, the model listing cap, and the CodeQL fall-through fix landed since your last pass |
|
bugbot run |
…rough match Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai please re-review at 25729c5: both earlier findings are fixed and resolved, and the agent access intersection is now a typed helper |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 25729c5. Configure here.
…groups Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> # Conflicts: # tests/test_litellm/proxy/auth/test_auth_checks.py
…groups Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai please re-review at d338d3f: main merged twice, re-verified live, Section H typing and comment fixes landed |
An agent key that echoes the x-litellm-user-id / x-litellm-team-id headers forwarded by /a2a is capped at that user's and team's models, MCP servers and agents, on top of its own grants and access group ceiling. The echoed ids only narrow, and nested A2A hops forward the original human caller Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| proxy_logging_obj=proxy_logging_obj, | ||
| ) | ||
| if user_object is None: | ||
| verbose_proxy_logger.debug("agent caller user %r not found; no user ceiling applied", caller.user_id) |
TLDR
Problem this solves:
How it solves it:
access_group_idsfield on agents, persisted through create, update, and PATCH/v1/modelsfor an agent key only lists what the ceiling allows. The attached group ids are read from the in-memory agent registry (the same read-through that already serves agent lookups), so the request path adds no DB query and an agent write applies on the next request; sibling workers pick it up through the existing agents table config syncx-litellm-user-id/x-litellm-team-id(the headers/a2aalready forwards) is further capped at that user's and team's models, MCP servers and agentsUser Flow
Before: an admin attaches an access group to an agent, the request is silently accepted, and the agent's key still reaches everything
{"access_group_ids": ["<group id>"]}and get 200 back, but the response has noaccess_group_idsfieldgpt-4o-minisends POST http://localhost:4000/a2a/{agent_name}/message/send, and the agent answers usingclaude-sonnet, a model that user could never call directlyAfter: the same admin attaches the group in the UI or API, and the agent's key is denied everything the group does not grant
{"access_group_ids": ["<group id>"]}, whose 200 response echoesaccess_group_ids"type": "agent_model_access_denied"access_deniedclaude-sonnet, carrying the forwardedx-litellm-user-id/x-litellm-team-id, returns 403team_model_access_denied, whilegpt-4o-ministill returns 200. Its MCP tool list and agent calls are cut down the same wayRelevant issues
Affected release
Linear ticket
Resolves LIT-8014
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Revalidated at d338d3f (merged with main on 2026-09-20)
The branch was merged with the current
main(two merges, one test-file conflict intest_auth_checks.pyresolved by keeping both appended sections) and the whole matrix was re-run against a proxy started from this head withPYTHONPATHpointing at the checkout, same fixtures as below: one access groupagent-ceiling(9ac89b21-46ce-4297-b153-42808b3f9703) grantinggpt-4o-mini, one MCP server and one target agent, attached tocaller-agentthrough the Admin UIAdmin UI (Chrome, http://localhost:3000/agents/): click
caller-agent, Settings, Edit, pickagent-ceilingin Access Groups, Save Changes. The toast saysAgent updated successfullyand the Overview shows the group with its idAdd New Agent, Configure, Next: the Entitlements step offers the same Access Groups selector with
agent-ceilingselectable (the wizard was not submitted)Live results with the caller agent's key right after the UI save, each line is one curl against http://localhost:4000
Deleted group edge case:
DELETE /v1/access_group/9ac89b21-...returned 204,GET /v1/agents/{caller}then returned"access_group_ids": [], and the same key got 200 onanthropic-haiku-4-5, both models from/v1/modelsand both MCP servers fromtools/list, so the detached agent is back to key and team permissions onlyThe handler branch each leg entered: chat goes through
common_checks->_check_agent_access_group_model_access(the Responses and Anthropic Messages routes sharecommon_checks, not exercised live),/v1/modelsthrough_agent_access_group_visible_models, MCP REST list and call throughMCPRequestHandler._agent_capped_servers(the streamable MCP transport shares that handler, not exercised live), A2A through_intersect_agent_accesswith the ceiling applied as the object permissionTaxonomy audit (A-BB) on the d338d3f diff
Confirmed and fixed: H2 (a banner comment in
test_auth_checks.py, removed), H4 (Optional["Router"]inutils.py->"Router | None", baredictparameter intest_agent_registry.py->PatchAgentRequest, one 121 character docstring reworded)Judged and accepted with reason: B1,
_detach_access_group_from_agentsreads and rewrites the agent'saccess_group_idslist inside the delete transaction; two admins deleting two groups attached to the same agent at the same instant could leave one stale id behind. A stale id points at a group that no longer loads and so contributes nothing to the union, which narrows access rather than widening it, and the next agent update clears it. A2, the ceiling reads group ids from the in-memory agent registry and the group object throughget_access_object, so a sibling worker sees an agent write on the existing agents-table config sync and a group write on the existing access-group cache TTL, the same propagation keys and teams have today; no new cache was introducedNot applicable or clean: F3/O4 (enforcement sits in
common_checksandMCPRequestHandler, shared by the sibling surfaces,/model/infois metadata only), X1/E5/Z1 (access_group_idsusesis Noneversus empty deliberately:Noneor missing means no ceiling, an attached group naming nothing denies that resource kind), W1/W4 (all helpers build new tuples and frozensets, no caller-owned mutation, no mutable defaults), V1 (no api_base or key forwarding in the diff), D4/Z2/O3 (a group that cannot be loaded or a missing DB client contributes nothing to the union, so the failure narrows access), C1-C7 (new optional field with[]default, no status code or default flips, no removed symbols), T1-T5/T17 (the mapped suites exercise each ceiling branch through injected loaders rather than class patches and leave no global state behind), A1 (agent and access-group writes refresh the handling worker's registry and cache), H1/H3/H5-H10 (no docs in source, no em dashes, Prisma column present in all three schemas plus the migration, UI bundle is rebuilt by the Docker and UI workflows, CI path filters cover every changed path)Screenshots (25729c5, the PR tip)
Admin UI: open http://localhost:3000/agents/, click
caller-agent, click Edit, pickagent-ceilingin the new Access Groups selector, click Save Changes. The Overview then shows the attached group with its idModels: with the group attached, the agent's key gets 200 for
gpt-4o-mini(in the group) and 403agent_model_access_deniedforanthropic-haiku-4-5(not in the group)MCP:
mcp-rest/tools/listonly showsmcp_allowed, listingmcp_deniedby name is 403, a tool call onmcp_allowedreturns 200 with the DeepWiki result, and the same call onmcp_deniedis 403Agent to agent:
a2a/target-allowed/message/sendreturns 200,a2a/target-denied/message/sendreturns 403More Admin UI: the Overview goes back to
Noneafter clearing the selector, and the Create Agent form (Add Agent on http://localhost:3000/agents/) offers the same selectorRecording of the UI flow (select, save, overview, clear, reselect, create form)
The three terminal captures are the After leg below, run from the same shell against the proxy at 25729c5; the full commands and outputs follow in text
Setup
Shared setup, same Postgres database and fixtures for both legs. Proxy started with
python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --use_v2_migration_resolver(the Before leg from a worktree at the merge base on port 4001, the After leg from the PR tip on port 4000), Admin UI withnpm run devon port 3000. Real provider calls togpt-4o-mini(OpenAI) andanthropic-haiku-4-5(Anthropic)Fixtures created with the master key: two MCP servers
mcp_allowed($MCP_ALLOWED) andmcp_denied($MCP_DENIED), both pointing at the public DeepWiki MCP endpoint; two target agentstarget-allowedandtarget-deniedbacked by the proxy itself; one caller agentcaller-agent($CALLER) whose key is$AGENT_KEY; and one access groupagent-ceiling($AG) withaccess_model_names: ["gpt-4o-mini"],access_mcp_server_ids: ["$MCP_ALLOWED"],access_agent_ids: ["<target-allowed id>"]. The key, team, and object permissions of the caller agent are left wide open so that every denial below comes from the attached group alone. Every command is run with$AGENT_KEYunless it sayssk-1234Before (dc81cf5)
Attach the group to the agent
curl -X PATCH http://localhost:4001/v1/agents/$CALLER -H 'Authorization: Bearer sk-1234' -d '{"access_group_ids":["$AG"]}'HTTP 200, response has noaccess_group_idsfield:{"agent_name": "caller-agent", "access_group_ids": "<field absent>"}Model in group
curl http://localhost:4001/chat/completions -H 'Authorization: Bearer $AGENT_KEY' -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Reply with the single word ok"}],"max_tokens":5}'"Ok."HTTP 200Model not in group
curl http://localhost:4001/chat/completions -H 'Authorization: Bearer $AGENT_KEY' -d '{"model":"anthropic-haiku-4-5",...}'"ok"HTTP 200(should have been denied)MCP servers visible
curl 'http://localhost:4001/mcp-rest/tools/list' -H 'Authorization: Bearer $AGENT_KEY'servers: ['mcp_allowed', 'mcp_denied'] tools: 6HTTP 200(both servers visible)MCP list scoped to the denied server
curl 'http://localhost:4001/mcp-rest/tools/list?mcp_server_name=mcp_denied' -H 'Authorization: Bearer $AGENT_KEY'servers: ['mcp_denied'] tools: 3HTTP 200MCP tool call on the allowed server
curl http://localhost:4001/mcp-rest/tools/call -H 'Authorization: Bearer $AGENT_KEY' -d '{"server_id":"$MCP_ALLOWED","name":"mcp_allowed-read_wiki_structure","arguments":{"repoName":"BerriAI/litellm"}}'"Available pages for BerriAI/litellm:\n\n- 1 Overview..."HTTP 200MCP tool call on the denied server
curl http://localhost:4001/mcp-rest/tools/call -H 'Authorization: Bearer $AGENT_KEY' -d '{"server_id":"$MCP_DENIED","name":"mcp_denied-read_wiki_structure","arguments":{"repoName":"BerriAI/litellm"}}'"Available pages for BerriAI/litellm:\n\n- 1 Overview..."HTTP 200(should have been denied)A2A agent in group
curl http://localhost:4001/a2a/target-allowed/message/send -H 'Authorization: Bearer $AGENT_KEY' -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"Reply with the single word ok"}],"messageId":"m1"}}}'["Ok"]HTTP 200A2A agent not in group
curl http://localhost:4001/a2a/target-denied/message/send -H 'Authorization: Bearer $AGENT_KEY' -d '{"jsonrpc":"2.0","method":"message/send",...}'["Ok"]HTTP 200(should have been denied)After (25729c5, the PR tip)
The curl matrix below was re-run against a proxy started from 25729c5, the PR tip. The UI screenshots and recording were captured at 433c804; the only dashboard change after that commit is prettier formatting of
add_agent_form.test.tsx(git diff 433c804a6c..25729c521f --stat -- ui/)Attach the group to the agent
caller-agent, click Edit. The form has a new Access Groups section between Rate Limits and MCP Servers. Pickagent-ceilingand click Save Changes (first screenshot above)None, reselecting brings it back, and the Create Agent form at http://localhost:3000/agents/ (Add Agent) offers the same selector (screenshots and recording above)curl http://localhost:4000/v1/agents/$CALLER -H 'Authorization: Bearer sk-1234'HTTP 200,{"agent_name": "caller-agent", "access_group_ids": ["51cebbbb-b237-49fb-9e46-4cd64adf33b7"]}Model listing follows the ceiling
curl http://localhost:4000/v1/models -H 'Authorization: Bearer $AGENT_KEY'HTTP 200,['gpt-4o-mini'](the same key with no group attached lists all 40 deployments in the config, see the PATCH round trip below)PATCH round trip, the change applies on the next request
curl -X PATCH http://localhost:4000/v1/agents/$CALLER -H 'Authorization: Bearer sk-1234' -d '{"access_group_ids": []}'returns{"access_group_ids": []}curl http://localhost:4000/v1/models -H 'Authorization: Bearer $AGENT_KEY'now lists['anthropic-haiku-4-5', 'anthropic-opus-4-5', ..., 'gpt-4o-mini', 'gpt-5.5', ..., 'voyage-4-large'], 40 modelscurl -X PATCH http://localhost:4000/v1/agents/$CALLER -H 'Authorization: Bearer sk-1234' -d '{"access_group_ids": ["$AG"]}'returns{"access_group_ids": ["51cebbbb-b237-49fb-9e46-4cd64adf33b7"]}curl http://localhost:4000/v1/models -H 'Authorization: Bearer $AGENT_KEY'is back to['gpt-4o-mini']Model in group
curl http://localhost:4000/chat/completions -H 'Authorization: Bearer $AGENT_KEY' -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Reply with the single word ok"}],"max_tokens":5}'"Ok."HTTP 200Model not in group
curl http://localhost:4000/chat/completions -H 'Authorization: Bearer $AGENT_KEY' -d '{"model":"anthropic-haiku-4-5",...}'HTTP 403{"message": "The requested model 'anthropic-haiku-4-5' is not available for this API key, or the model name is invalid. Check the models available to you and try again.", "type": "agent_model_access_denied", "param": "model", "code": "403"}MCP servers visible
curl 'http://localhost:4000/mcp-rest/tools/list' -H 'Authorization: Bearer $AGENT_KEY'servers: ['mcp_allowed'] tools: 3HTTP 200(only the granted server)MCP list scoped to the denied server
curl 'http://localhost:4000/mcp-rest/tools/list?mcp_server_name=mcp_denied' -H 'Authorization: Bearer $AGENT_KEY'HTTP 403{"error": "access_denied", "message": "The key is not allowed to access server mcp_denied"}MCP tool call on the allowed server
curl http://localhost:4000/mcp-rest/tools/call -H 'Authorization: Bearer $AGENT_KEY' -d '{"server_id":"$MCP_ALLOWED","name":"mcp_allowed-read_wiki_structure","arguments":{"repoName":"BerriAI/litellm"}}'"Available pages for BerriAI/litellm:\n\n- 1 Overview..."HTTP 200MCP tool call on the denied server
curl http://localhost:4000/mcp-rest/tools/call -H 'Authorization: Bearer $AGENT_KEY' -d '{"server_id":"$MCP_DENIED","name":"mcp_denied-read_wiki_structure","arguments":{"repoName":"BerriAI/litellm"}}'HTTP 403{"detail": {"error": "access_denied", "message": "The key is not allowed to access server bb713243-b7c0-408b-a981-96a6138f3689"}}A2A agent in group
curl http://localhost:4000/a2a/target-allowed/message/send -H 'Authorization: Bearer $AGENT_KEY' -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"Reply with the single word ok"}],"messageId":"m1"}}}'["Ok."]HTTP 200A2A agent not in group
curl http://localhost:4000/a2a/target-denied/message/send -H 'Authorization: Bearer $AGENT_KEY' -d '{"jsonrpc":"2.0","method":"message/send",...}'HTTP 403"Agent 'target-denied' is not allowed for your key/team. Contact proxy admin for access."Type
🆕 New Feature
Caveats (if any)
Severe
Medium
agent_idon the key)x-litellm-user-id/x-litellm-team-idback on its own calls; an agent that drops them runs at its key and access group ceiling onlyLow
agent_callerrejected on the auth model) is covered by unit tests with a mutation check (dropping each caller check turns 7 of them red), not yet by the live run aboveosv-scan(not a required check) is red onsoupsieve 2.8.4(GHSA-gjv8-xp57-g29c, GHSA-j934-xhv5-fg8f). Both advisories were published 2026-09-17T20:32Z; this PR does not touchuv.lock, its own scan passed at 20:40Z on the same lockfile, and every PR scan in the repo has failed since 20:45Z. The bump to 2.9.0 is dependabot PR build(deps): bump soupsieve from 2.8.4 to 2.9.2 #41679, whose scan is the only green oneFinal Attestation
Note
High Risk
Changes authorization for agent-bound API keys across models, MCP, and agent routing; misconfiguration or empty group ceilings can deny all access immediately.
Overview
Agents can now be linked to unified access groups via a new
access_group_idsfield (DB migration, Prisma, agent CRUD/API types). Attached groups define a ceiling (union of each group’s models, MCP servers, and target agents); that ceiling is intersected with existing key/team/object-permission checks for agent keys.Enforcement spans chat completions (
agent_model_access_denied), MCP server allowlists (alongside agentobject_permission), agent-to-agent access, and/v1/modelslisting so agent keys only see models the ceiling allows. Deleting an access group strips its id from agents in the DB and updates the in-memory agent registry.The dashboard Create/Edit Agent flows add an Access Groups selector; overview shows attached groups. Tests cover ceiling resolution, intersections, and delete cleanup.
Reviewed by Cursor Bugbot for commit 7229fc1. Bugbot is set up for automated code reviews on this repo. Configure here.
Link to Devin session: https://app.devin.ai/sessions/47c201b13a3e4c85b3bc8a0d199eb483
Open in Devin Desktop: https://app.devin.ai/desktop/session/47c201b13a3e4c85b3bc8a0d199eb483?variant=devin
Requested by: @yassin-berriai