mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-01 02:02:20 +00:00
fix(mcp): apply convention ceiling to echoed caller team and read overrides before field delete
Co-Authored-By: bot_apk <apk@cognition.ai>
This commit is contained in:
parent
b7a82bb7b9
commit
1f48e23e31
3 changed files with 48 additions and 13 deletions
|
|
@ -3312,27 +3312,18 @@ class MCPRequestHandler:
|
|||
axis twin of ``_apply_agent_caller_ceiling``, so the headers only ever narrow. Denies every tool
|
||||
on the server when the caller's team cannot be loaded, since a caller we cannot resolve must not
|
||||
read as unrestricted."""
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
caller_auth: Final = agent_caller_auth(user_api_key_auth) if user_api_key_auth else None
|
||||
if caller_auth is None:
|
||||
return allowed_tools
|
||||
try:
|
||||
team_obj_perm: Final = await MCPRequestHandler._get_team_object_permission(caller_auth)
|
||||
team_toolset_tools: Final = await MCPRequestHandler._toolset_tools_for_server(team_obj_perm, server_id)
|
||||
resolved_inventory: Final = await MCPRequestHandler._manager_inventory(server_id)
|
||||
team_tools: Final = await MCPRequestHandler._row_level_tools(team_obj_perm, server_id, resolved_inventory)
|
||||
except Exception as e: # noqa: BLE001 # an unresolved caller team must deny, not widen
|
||||
verbose_logger.warning(
|
||||
"MCP agent caller team tool ceiling unresolvable, denying tools on %r: %s", server_id, e
|
||||
)
|
||||
return ()
|
||||
team_direct_tools: Final = (
|
||||
global_mcp_server_manager.expand_tool_permissions(team_obj_perm.mcp_tool_permissions).get(server_id)
|
||||
if team_obj_perm
|
||||
else None
|
||||
)
|
||||
team_tools: Final = MCPRequestHandler._union_tool_grants(team_direct_tools, team_toolset_tools)
|
||||
team_capped: Final = (
|
||||
allowed_tools
|
||||
if team_tools is None
|
||||
|
|
@ -3340,7 +3331,9 @@ class MCPRequestHandler:
|
|||
if allowed_tools is None
|
||||
else tuple(frozenset(allowed_tools) & frozenset(team_tools))
|
||||
)
|
||||
return await MCPRequestHandler._apply_user_tool_ceiling(team_capped, server_id, caller_auth)
|
||||
return await MCPRequestHandler._apply_user_tool_ceiling(
|
||||
team_capped, server_id, caller_auth, inventory=resolved_inventory
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _apply_end_user_tool_ceiling(
|
||||
|
|
|
|||
|
|
@ -4384,6 +4384,48 @@ class TestAgentMCPPermissions:
|
|||
assert await MCPRequestHandler.get_allowed_tools_for_server("server-a", agent_key) == ["ask_wiki_question"]
|
||||
assert await MCPRequestHandler.get_allowed_tools_for_server("server-c", agent_key) == []
|
||||
|
||||
async def test_agent_key_acting_for_a_user_is_capped_at_the_callers_convention_tools(self):
|
||||
"""A converted caller team granting server-a with no tool list still caps the agent to the
|
||||
convention set: inventory non-deletes minus explicit denies."""
|
||||
agent_key = self._agent_key_acting_for(user_id="alice", team_id="callers")
|
||||
|
||||
async def caller_team_permission(
|
||||
user_api_key_auth: UserAPIKeyAuth | None = None,
|
||||
) -> LiteLLM_ObjectPermissionTable | None:
|
||||
assert user_api_key_auth is not None
|
||||
if user_api_key_auth.team_id != "callers":
|
||||
return None
|
||||
return LiteLLM_ObjectPermissionTable(
|
||||
object_permission_id="perm-callers",
|
||||
mcp_servers=["server-a"],
|
||||
mcp_permission_version=1,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object( # test-quality-ok: the level loaders read proxy_server globals with no injection seam
|
||||
MCPRequestHandler, "_get_key_object_permission", return_value=None
|
||||
),
|
||||
patch.object( # test-quality-ok: same seam, keyed by which team is being asked about
|
||||
MCPRequestHandler, "_get_team_object_permission", AsyncMock(side_effect=caller_team_permission)
|
||||
),
|
||||
patch.object( # test-quality-ok: same seam
|
||||
MCPRequestHandler, "_get_user_object_permission", AsyncMock(return_value=None)
|
||||
),
|
||||
patch.object( # test-quality-ok: agent object_permission lookup hits the DB, not under test here
|
||||
MCPRequestHandler, "_get_agent_object_permission", AsyncMock(return_value=None)
|
||||
),
|
||||
patch.object( # test-quality-ok: the discovered catalog is process state with no injection seam
|
||||
MCPRequestHandler,
|
||||
"_manager_inventory",
|
||||
AsyncMock(
|
||||
return_value={"read_wiki_structure": "reads a page", "delete_wiki_page": "deletes a page"}
|
||||
),
|
||||
),
|
||||
):
|
||||
assert await MCPRequestHandler.get_allowed_tools_for_server("server-a", agent_key) == [
|
||||
"read_wiki_structure"
|
||||
]
|
||||
|
||||
async def test_agent_key_not_acting_for_anyone_ignores_the_caller_tool_ceiling(self):
|
||||
agent_key = UserAPIKeyAuth(api_key="agent-key", user_id="agent-owner", team_id="agent-team", agent_id="agent-1")
|
||||
|
||||
|
|
|
|||
|
|
@ -484,7 +484,6 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
if (toolsets && toolsets.length > 0) {
|
||||
formValues.object_permission.mcp_toolsets = toolsets;
|
||||
}
|
||||
delete formValues.allowed_mcp_servers_and_groups;
|
||||
}
|
||||
|
||||
if (formValues.mcp_tool_permissions && Object.keys(formValues.mcp_tool_permissions).length > 0) {
|
||||
|
|
@ -505,6 +504,7 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
}
|
||||
delete formValues.mcp_tool_overrides;
|
||||
}
|
||||
delete formValues.allowed_mcp_servers_and_groups;
|
||||
}
|
||||
|
||||
// Transform allowed_mcp_access_groups into object_permission
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue