mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-17 18:48:36 +00:00
1f412bc6d8
* fix: fix ui render * fix: fix minor bugs * refactor: use prisma functions instead of raw sql (safer) * fix(add-new-tiles-to-tool-policies): allow developer to see what's available * feat: ensure tool allowlist runs correctly for tool names + mcp's * refactor: more ui improvements * feat: working key tool blocking * feat(tools): show tool logs * refactor: backend code improvements * refactor: improve log viewer for tools * fix: address PR review feedback for tool access control - Add missing blocked_tools column to root schema.prisma (schema drift) - Invalidate ToolPolicyRegistry after policy mutations so changes take effect immediately - Remove dead code: unused get_effective_policies, get_tool_policies_cached, and helpers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: race condition in permission resolution and remove duplicate allowlist check - Use atomic update_many with object_permission_id=None to prevent concurrent requests from creating orphaned permission rows and losing tool blocks - Remove duplicate allowed_tools enforcement from guardrail (already enforced in auth layer via check_tools_allowlist) - Move inline uuid import to module level Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update to account for userAgent * UI - Add ToolDetails * input/output policy * LiteLLM_PolicyAttachmentTable * LiteLLM_PolicyAttachmentTable * fix: add _enqueue_tool_registry_upsert * fix: tool mgmt endpoints * tool mgmt endpoints * Update tests/test_litellm/proxy/db/test_tool_registry_writer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update tests/test_litellm/proxy/db/test_tool_registry_writer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Update tests/test_litellm/proxy/db/test_tool_registry_writer.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: sync root schema.prisma and fix test_tool_registry_writer for input/output policy - Migrate root schema.prisma LiteLLM_ToolTable from call_policy to input_policy/output_policy, add missing user_agent and last_used_at columns (now consistent with litellm/proxy/schema.prisma and litellm-proxy-extras) - Fix SpendLogToolIndex comment across all three schema files - Fix all call_policy references in test_tool_registry_writer.py: swapped update_tool_policy arguments, wrong get_tools_by_names return type assertions, _mock_tool_row setting call_policy instead of input_policy Addresses Greptile review feedback on PR #22732. Made-with: Cursor --------- Co-authored-by: Krrish Dholakia <krrishdholakia@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Standalone script to test tool allowlist enforcement and tool name extraction.
|
||
|
||
Run from repo root:
|
||
poetry run python scripts/test_tool_allowlist_script.py
|
||
|
||
Or run the unit tests:
|
||
poetry run pytest tests/test_litellm/proxy/test_tools_allowlist_enforcement.py -v
|
||
"""
|
||
|
||
import asyncio
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# Ensure repo root is on path
|
||
repo_root = Path(__file__).resolve().parent.parent
|
||
if str(repo_root) not in sys.path:
|
||
sys.path.insert(0, str(repo_root))
|
||
|
||
|
||
def test_extraction():
|
||
"""Test extract_request_tool_names for each API shape."""
|
||
from litellm.proxy.guardrails.tool_name_extraction import extract_request_tool_names
|
||
|
||
cases = [
|
||
("OpenAI chat tools", "/v1/chat/completions", {"tools": [{"type": "function", "function": {"name": "get_weather"}}]}),
|
||
("OpenAI chat functions", "/v1/chat/completions", {"functions": [{"name": "run_sql"}]}),
|
||
("OpenAI responses function", "/v1/responses", {"tools": [{"type": "function", "name": "get_current_weather"}]}),
|
||
("OpenAI responses MCP", "/v1/responses", {"tools": [{"type": "mcp", "server_label": "dmcp"}]}),
|
||
("Anthropic", "/v1/messages", {"tools": [{"name": "get_weather"}, {"name": "run_sql"}]}),
|
||
("Google generateContent", "/generate_content", {"tools": [{"functionDeclarations": [{"name": "schedule_meeting"}]}]}),
|
||
("MCP call_tool", "/mcp/call_tool", {"name": "my_tool", "arguments": {}}),
|
||
("Non-tool route", "/v1/embeddings", {"tools": [{"type": "function", "function": {"name": "x"}}]}),
|
||
]
|
||
print("=== extract_request_tool_names(route, data) ===\n")
|
||
for label, route, data in cases:
|
||
names = extract_request_tool_names(route, data)
|
||
print(f" {label}: {names}")
|
||
print()
|
||
|
||
|
||
async def test_check_tools_allowlist():
|
||
"""Test check_tools_allowlist with mock tokens."""
|
||
from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth
|
||
from litellm.proxy.auth.auth_checks import check_tools_allowlist
|
||
|
||
def token(metadata=None, team_metadata=None):
|
||
return UserAPIKeyAuth(
|
||
api_key="test-key",
|
||
user_id="user",
|
||
team_id="team",
|
||
org_id=None,
|
||
models=["*"],
|
||
metadata=metadata or {},
|
||
team_metadata=team_metadata or {},
|
||
)
|
||
|
||
print("=== check_tools_allowlist (auth) ===\n")
|
||
|
||
# No allowlist -> pass
|
||
await check_tools_allowlist(
|
||
request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]},
|
||
valid_token=token(),
|
||
team_object=None,
|
||
route="/v1/chat/completions",
|
||
)
|
||
print(" No allowlist, body has tools: PASS")
|
||
|
||
# Allowed tool -> pass
|
||
await check_tools_allowlist(
|
||
request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]},
|
||
valid_token=token(metadata={"allowed_tools": ["get_weather"]}),
|
||
team_object=None,
|
||
route="/v1/chat/completions",
|
||
)
|
||
print(" allowed_tools=['get_weather'], body has get_weather: PASS")
|
||
|
||
# Disallowed tool -> raise
|
||
try:
|
||
await check_tools_allowlist(
|
||
request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]},
|
||
valid_token=token(metadata={"allowed_tools": ["other_tool"]}),
|
||
team_object=None,
|
||
route="/v1/chat/completions",
|
||
)
|
||
print(" DISALLOWED: expected ProxyException")
|
||
except ProxyException as e:
|
||
if e.type == ProxyErrorTypes.tool_access_denied:
|
||
print(" allowed_tools=['other_tool'], body has get_weather: PASS (raised tool_access_denied)")
|
||
else:
|
||
print(f" Unexpected ProxyException type: {e.type}")
|
||
except Exception as e:
|
||
print(f" Unexpected: {e}")
|
||
|
||
# Team allowlist when key empty
|
||
await check_tools_allowlist(
|
||
request_body={"tools": [{"type": "function", "function": {"name": "get_weather"}}]},
|
||
valid_token=token(team_metadata={"allowed_tools": ["get_weather"]}),
|
||
team_object=None,
|
||
route="/v1/chat/completions",
|
||
)
|
||
print(" team_metadata.allowed_tools=['get_weather']: PASS")
|
||
print()
|
||
|
||
|
||
def main():
|
||
print("Tool allowlist / tool name extraction – script checks\n")
|
||
test_extraction()
|
||
asyncio.run(test_check_tools_allowlist())
|
||
print("Done. For full unit tests run:")
|
||
print(" poetry run pytest tests/test_litellm/proxy/test_tools_allowlist_enforcement.py -v")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|