mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
feat: add regex-based tool_name/tool_type matching for tool-permission (#17164)
* feat: add regex-based tool_name/tool_type matching for tool-permission * docs: update tool permission quick start for UI workflow
This commit is contained in:
parent
d43c077653
commit
334d09b3b2
5 changed files with 286 additions and 111 deletions
|
|
@ -7,9 +7,38 @@ import TabItem from '@theme/TabItem';
|
|||
LiteLLM provides the LiteLLM Tool Permission Guardrail that lets you control which **tool calls** a model is allowed to invoke, using configurable allow/deny rules. This offers fine-grained, provider-agnostic control over tool execution (e.g., OpenAI Chat Completions `tool_calls`, Anthropic Messages `tool_use`, MCP tools).
|
||||
|
||||
## Quick Start
|
||||
### 1. Define Guardrails on your LiteLLM config.yaml
|
||||
|
||||
Define your guardrails under the `guardrails` section
|
||||
### LiteLLM UI
|
||||
|
||||
#### Step 1: Select Tool Permission Guardrail
|
||||
|
||||
Open the LiteLLM Dashboard, click **Add New Guardrail**, and choose **LiteLLM Tool Permission Guardrail**. This loads the rule builder UI.
|
||||
|
||||
<Image img={require('../../../img/create_guard_tool_permission.png')} alt="Configure tool permission guardrail in LiteLLM UI" />
|
||||
|
||||
#### Step 2: Define Regex Rules
|
||||
|
||||
1. Click **Add Rule**.
|
||||
2. Enter a unique Rule ID.
|
||||
3. Provide a regex for the tool name (e.g., `^mcp__github_.*$`).
|
||||
4. Optionally add a regex for tool type (e.g., `^function$`).
|
||||
5. Pick **Allow** or **Deny**.
|
||||
|
||||
<Image img={require('../../../img/create_rule_tool_permission.png')} alt="Configure tool permission guardrail in LiteLLM UI" />
|
||||
|
||||
#### Step 3: Restrict Tool Arguments (Optional)
|
||||
|
||||
Select **+ Restrict tool arguments** to attach regex validations to nested paths (dot + `[]` notation). This enforces that sensitive parameters (such as `arguments.to[]`) conform to pre-approved formats.
|
||||
|
||||
#### Step 4: Choose Defaults & Actions
|
||||
|
||||
- Set the fallback decision (`default_action`) for tools that do not hit any rule.
|
||||
- Decide how disallowed tools behave: **Block** halts the request, **Rewrite** strips forbidden tools and returns an error message inside the response.
|
||||
- Customize `violation_message_template` if you want branded error copy.
|
||||
- Save the guardrail.
|
||||
|
||||
### LiteLLM Config.yaml Setup
|
||||
|
||||
```yaml
|
||||
guardrails:
|
||||
- guardrail_name: "tool-permission-guardrail"
|
||||
|
|
@ -21,16 +50,17 @@ guardrails:
|
|||
tool_name: "Bash"
|
||||
decision: "allow"
|
||||
- id: "allow_github_mcp"
|
||||
tool_name: "mcp__github_*"
|
||||
tool_name: "^mcp__github_.*$"
|
||||
decision: "allow"
|
||||
- id: "allow_aws_documentation"
|
||||
tool_name: "mcp__aws-documentation_*_documentation"
|
||||
tool_name: "^mcp__aws-documentation_.*_documentation$"
|
||||
decision: "allow"
|
||||
- id: "deny_read_commands"
|
||||
tool_name: "Read"
|
||||
decision: "Deny"
|
||||
decision: "deny"
|
||||
- id: "mail-domain"
|
||||
tool_name: "send_email"
|
||||
tool_name: "^send_email$"
|
||||
tool_type: "^function$"
|
||||
decision: "allow"
|
||||
allowed_param_patterns:
|
||||
"to[]": "^.+@berri\\.ai$"
|
||||
|
|
@ -44,7 +74,8 @@ guardrails:
|
|||
|
||||
```yaml
|
||||
- id: "unique_rule_id" # Unique identifier for the rule
|
||||
tool_name: "pattern" # Tool name or pattern to match
|
||||
tool_name: "^regex$" # Regex for tool name (optional, at least one of name/type required)
|
||||
tool_type: "^function$" # Regex for tool type (optional)
|
||||
decision: "allow" # "allow" or "deny"
|
||||
allowed_param_patterns: # Optional - regex map for argument paths (dot + [] notation)
|
||||
"path.to[].field": "^regex$"
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
|
||||
self.rules: List[ToolPermissionRule] = []
|
||||
self._compiled_rule_patterns: Dict[str, Dict[str, re.Pattern]] = {}
|
||||
self._compiled_rule_targets: Dict[str, Dict[str, Optional[re.Pattern]]] = {}
|
||||
if rules:
|
||||
for rule_item in rules:
|
||||
if isinstance(rule_item, ToolPermissionRule):
|
||||
|
|
@ -70,6 +71,30 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
rule = ToolPermissionRule(**rule_item)
|
||||
self.rules.append(rule)
|
||||
|
||||
compiled_target_patterns: Dict[str, Optional[re.Pattern]] = {
|
||||
"tool_name": None,
|
||||
"tool_type": None,
|
||||
}
|
||||
if rule.tool_name is not None:
|
||||
try:
|
||||
compiled_target_patterns["tool_name"] = re.compile(
|
||||
rule.tool_name
|
||||
)
|
||||
except re.error as exc:
|
||||
raise ValueError(
|
||||
f"Invalid regex for tool_name in rule '{rule.id}': {exc}"
|
||||
) from exc
|
||||
if rule.tool_type is not None:
|
||||
try:
|
||||
compiled_target_patterns["tool_type"] = re.compile(
|
||||
rule.tool_type
|
||||
)
|
||||
except re.error as exc:
|
||||
raise ValueError(
|
||||
f"Invalid regex for tool_type in rule '{rule.id}': {exc}"
|
||||
) from exc
|
||||
self._compiled_rule_targets[rule.id] = compiled_target_patterns
|
||||
|
||||
if rule.allowed_param_patterns:
|
||||
compiled_patterns: Dict[str, re.Pattern] = {}
|
||||
for path, pattern in rule.allowed_param_patterns.items():
|
||||
|
|
@ -100,59 +125,75 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
|
||||
return ToolPermissionGuardrailConfigModel
|
||||
|
||||
def _matches_pattern(self, tool_name: str, pattern: str) -> bool:
|
||||
"""
|
||||
Check if a tool name matches a pattern
|
||||
|
||||
Supports patterns like:
|
||||
- "Bash" - exact match
|
||||
- "mcp__*" - prefix pattern (matches names starting wich "mcp__")
|
||||
- "*_read" - suffix wildcard (matches names ending with "_read")
|
||||
- "mcp__github_*_read" - infix wildcard (matches names like "mcp__github_mark_all_notifications_read")
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to check
|
||||
pattern: Pattern to match against
|
||||
|
||||
Returns:
|
||||
True if the tool name matches the pattern
|
||||
"""
|
||||
# Handle exact matches
|
||||
if tool_name == pattern:
|
||||
def _matches_regex(
|
||||
self, pattern: Optional[re.Pattern], value: Optional[str]
|
||||
) -> bool:
|
||||
if pattern is None:
|
||||
return True
|
||||
if value is None:
|
||||
return False
|
||||
return bool(pattern.fullmatch(value))
|
||||
|
||||
if "*" in pattern:
|
||||
# Escape regex special chars except '*'
|
||||
escaped_pattern = re.escape(pattern)
|
||||
# Turn \* into .*
|
||||
regex_pattern = escaped_pattern.replace(r"\*", ".*")
|
||||
return bool(re.fullmatch(regex_pattern, tool_name))
|
||||
def _rule_matches_tool(
|
||||
self,
|
||||
rule: ToolPermissionRule,
|
||||
*,
|
||||
tool_name: Optional[str],
|
||||
tool_type: Optional[str] = None,
|
||||
) -> tuple[bool, bool]:
|
||||
target_patterns = self._compiled_rule_targets.get(rule.id, {})
|
||||
name_pattern = target_patterns.get("tool_name")
|
||||
type_pattern = target_patterns.get("tool_type")
|
||||
|
||||
return False
|
||||
name_required = rule.tool_name is not None
|
||||
type_required = rule.tool_type is not None
|
||||
|
||||
name_matched = (
|
||||
self._matches_regex(name_pattern, tool_name) if name_required else True
|
||||
)
|
||||
type_matched = (
|
||||
self._matches_regex(type_pattern, tool_type) if type_required else True
|
||||
)
|
||||
|
||||
overall_match = name_matched and type_matched
|
||||
should_check_params = name_required and name_matched
|
||||
|
||||
return overall_match, should_check_params
|
||||
|
||||
def _check_tool_permission(
|
||||
self, tool_name: str
|
||||
self,
|
||||
tool_name: Optional[str],
|
||||
tool_type: Optional[str] = None,
|
||||
) -> tuple[bool, Optional[str], Optional[str]]:
|
||||
"""
|
||||
Check if a tool is allowed based on the configured rules
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to check
|
||||
tool_type: Type of the tool to check
|
||||
|
||||
Returns:
|
||||
Tuple of (is_allowed, rule_id, message)
|
||||
"""
|
||||
verbose_proxy_logger.debug(f"Checking permission for tool: {tool_name}")
|
||||
verbose_proxy_logger.debug(
|
||||
f"Checking permission for tool: {tool_name or tool_type}"
|
||||
)
|
||||
|
||||
# Check each rule in order
|
||||
for rule in self.rules:
|
||||
if self._matches_pattern(tool_name, rule.tool_name):
|
||||
matches, _ = self._rule_matches_tool(
|
||||
rule,
|
||||
tool_name=tool_name,
|
||||
tool_type=tool_type,
|
||||
)
|
||||
if matches:
|
||||
is_allowed = rule.decision == "allow"
|
||||
default_message = f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'"
|
||||
tool_identifier = tool_name or tool_type or "unknown_tool"
|
||||
default_message = f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'"
|
||||
message = self.render_violation_message(
|
||||
default=default_message,
|
||||
context={
|
||||
"tool_name": tool_name,
|
||||
"tool_name": tool_name or tool_identifier,
|
||||
"rule_id": rule.id,
|
||||
},
|
||||
)
|
||||
|
|
@ -161,11 +202,12 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
|
||||
# No rule matched, use default action
|
||||
is_allowed = self.default_action == "allow"
|
||||
default_message = f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by default action"
|
||||
tool_identifier = tool_name or tool_type or "unknown_tool"
|
||||
default_message = f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by default action"
|
||||
message = self.render_violation_message(
|
||||
default=default_message,
|
||||
context={
|
||||
"tool_name": tool_name,
|
||||
"tool_name": tool_name or tool_identifier,
|
||||
"rule_id": None,
|
||||
},
|
||||
)
|
||||
|
|
@ -228,7 +270,7 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
*,
|
||||
arguments: Dict[str, Any],
|
||||
rule: ToolPermissionRule,
|
||||
tool_name: str,
|
||||
tool_name: Optional[str],
|
||||
) -> tuple[bool, Optional[str]]:
|
||||
compiled_patterns = self._compiled_rule_patterns.get(rule.id)
|
||||
if not compiled_patterns:
|
||||
|
|
@ -249,7 +291,7 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
return (
|
||||
False,
|
||||
f"Value '{raw_value}' for path '{path}' does not match allowed pattern"
|
||||
f" '{compiled_pattern.pattern}' for tool '{tool_name}'",
|
||||
f" '{compiled_pattern.pattern}' for tool '{tool_name or 'unknown_tool'}'",
|
||||
)
|
||||
|
||||
return True, None
|
||||
|
|
@ -258,19 +300,27 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
self, tool_call: ChatCompletionMessageToolCall
|
||||
) -> tuple[bool, Optional[str], Optional[str]]:
|
||||
tool_name = tool_call.function.name if tool_call.function else None
|
||||
if not tool_name:
|
||||
tool_type = getattr(tool_call, "type", None)
|
||||
if not tool_name and not tool_type:
|
||||
return self.default_action == "allow", None, None
|
||||
|
||||
tool_identifier = tool_name or tool_type or "unknown_tool"
|
||||
|
||||
last_pattern_failure_msg: Optional[str] = None
|
||||
|
||||
for rule in self.rules:
|
||||
if not self._matches_pattern(tool_name, rule.tool_name):
|
||||
matches, should_check_params = self._rule_matches_tool(
|
||||
rule,
|
||||
tool_name=tool_name,
|
||||
tool_type=tool_type,
|
||||
)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
if rule.allowed_param_patterns:
|
||||
if rule.allowed_param_patterns and should_check_params:
|
||||
arguments = self._parse_tool_call_arguments(tool_call)
|
||||
if not arguments:
|
||||
last_pattern_failure_msg = f"Tool '{tool_name}' is missing arguments required by rule '{rule.id}'"
|
||||
last_pattern_failure_msg = f"Tool '{tool_identifier}' is missing arguments required by rule '{rule.id}'"
|
||||
continue
|
||||
|
||||
patterns_match, failure_message = self._patterns_match_for_rule(
|
||||
|
|
@ -283,10 +333,10 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
continue
|
||||
|
||||
is_allowed = rule.decision == "allow"
|
||||
default_message = f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'"
|
||||
default_message = f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by rule '{rule.id}'"
|
||||
message = self.render_violation_message(
|
||||
default=default_message,
|
||||
context={"tool_name": tool_name, "rule_id": rule.id},
|
||||
context={"tool_name": tool_identifier, "rule_id": rule.id},
|
||||
)
|
||||
return is_allowed, rule.id, message
|
||||
|
||||
|
|
@ -294,11 +344,11 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
default_message = (
|
||||
last_pattern_failure_msg
|
||||
if (last_pattern_failure_msg and not is_allowed)
|
||||
else f"Tool '{tool_name}' {'allowed' if is_allowed else 'denied'} by default action"
|
||||
else f"Tool '{tool_identifier}' {'allowed' if is_allowed else 'denied'} by default action"
|
||||
)
|
||||
message = self.render_violation_message(
|
||||
default=default_message,
|
||||
context={"tool_name": tool_name, "rule_id": None},
|
||||
context={"tool_name": tool_identifier, "rule_id": None},
|
||||
)
|
||||
return is_allowed, None, message
|
||||
|
||||
|
|
@ -469,8 +519,9 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
if tool["type"] != "function":
|
||||
continue
|
||||
tool_name: str = tool["function"]["name"]
|
||||
tool_type: Optional[str] = tool.get("type")
|
||||
|
||||
is_allowed, _, message = self._check_tool_permission(tool_name)
|
||||
is_allowed, _, message = self._check_tool_permission(tool_name, tool_type)
|
||||
|
||||
if not is_allowed and message is not None:
|
||||
verbose_proxy_logger.warning(f"Tool Permission Guardrail: {message}")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Tool Permission Guardrail Type Definitions
|
||||
from typing import Dict, List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
|
@ -12,8 +12,13 @@ class ToolPermissionRule(BaseModel):
|
|||
"""
|
||||
|
||||
id: str = Field(description="Unique identifier for the rule")
|
||||
tool_name: str = Field(
|
||||
description="Tool name or pattern (e.g., 'Bash', 'mcp__github_*', 'mcp__github_*_read', '*_read')"
|
||||
tool_name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Regex pattern applied to the tool's function name",
|
||||
)
|
||||
tool_type: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Regex pattern applied to the tool type (e.g., function)",
|
||||
)
|
||||
decision: Literal["allow", "deny"] = Field(
|
||||
description="Whether to allow or deny this tool usage"
|
||||
|
|
@ -23,6 +28,26 @@ class ToolPermissionRule(BaseModel):
|
|||
description="Optional regex map enforcing nested parameter values using dot/[] paths",
|
||||
)
|
||||
|
||||
@field_validator("tool_name", "tool_type", mode="before")
|
||||
@classmethod
|
||||
def _blank_to_none(cls, value: Optional[str]) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if not stripped:
|
||||
return None
|
||||
return stripped
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _ensure_target_present(self):
|
||||
if self.tool_name is None and self.tool_type is None:
|
||||
raise ValueError(
|
||||
"Each rule must specify at least a tool_name or tool_type regex"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class ToolResult(BaseModel):
|
||||
"""
|
||||
|
|
@ -52,7 +77,7 @@ class ToolPermissionGuardrailConfigModel(GuardrailConfigModel):
|
|||
|
||||
rules: Optional[List[ToolPermissionRule]] = Field(
|
||||
default=None,
|
||||
description="Ordered allow/deny rules. Patterns support * wildcards and optional regex constraints on tool arguments.",
|
||||
description="Ordered allow/deny rules. Patterns use regex for tool names/types and optional regex constraints on tool arguments.",
|
||||
)
|
||||
default_action: Literal["allow", "deny"] = Field(
|
||||
default="deny", description="Fallback decision when no rule matches"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Unit tests for Tool Permission Guardrail (OpenAI tool_calls semantics)
|
|||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
@ -36,15 +37,19 @@ class TestToolPermissionGuardrail:
|
|||
def setup_method(self):
|
||||
"""Set up test fixtures"""
|
||||
self.test_rules = [
|
||||
{"id": "allow_bash", "tool_name": "Bash", "decision": "allow"},
|
||||
{"id": "allow_github", "tool_name": "mcp__github_*", "decision": "allow"},
|
||||
{"id": "allow_bash", "tool_name": r"^Bash$", "decision": "allow"},
|
||||
{
|
||||
"id": "allow_documentation",
|
||||
"tool_name": "mcp__aws-documentation_*_documentation",
|
||||
"id": "allow_github",
|
||||
"tool_name": r"^mcp__github_.*$",
|
||||
"decision": "allow",
|
||||
},
|
||||
{"id": "deny_read", "tool_name": "Read", "decision": "deny"},
|
||||
{"id": "deny_get", "tool_name": "*_get", "decision": "deny"},
|
||||
{
|
||||
"id": "allow_documentation",
|
||||
"tool_name": r"^mcp__aws-documentation_.*_documentation$",
|
||||
"decision": "allow",
|
||||
},
|
||||
{"id": "deny_read", "tool_name": r"^Read$", "decision": "deny"},
|
||||
{"id": "deny_get", "tool_name": r".*_get$", "decision": "deny"},
|
||||
]
|
||||
|
||||
self.guardrail = ToolPermissionGuardrail(
|
||||
|
|
@ -64,50 +69,91 @@ class TestToolPermissionGuardrail:
|
|||
self.guardrail.supported_event_hooks or []
|
||||
)
|
||||
|
||||
def test_pattern_matching_exact(self):
|
||||
"""Test exact pattern matching"""
|
||||
assert self.guardrail._matches_pattern("Read", "Read") is True
|
||||
assert self.guardrail._matches_pattern("Write", "Read") is False
|
||||
def test_matches_regex_helper(self):
|
||||
pattern = re.compile(r"^Read$")
|
||||
assert self.guardrail._matches_regex(pattern, "Read") is True
|
||||
assert self.guardrail._matches_regex(pattern, "Write") is False
|
||||
assert self.guardrail._matches_regex(None, "Any") is True
|
||||
assert self.guardrail._matches_regex(pattern, None) is False
|
||||
|
||||
def test_pattern_matching_wildcards(self):
|
||||
"""Test wildcard pattern matching"""
|
||||
assert (
|
||||
self.guardrail._matches_pattern(
|
||||
"mcp__github_add_issue_comment", "mcp__github_*"
|
||||
)
|
||||
is True
|
||||
def test_rule_matches_tool_with_type_only(self):
|
||||
guardrail = ToolPermissionGuardrail(
|
||||
guardrail_name="type-only",
|
||||
rules=[
|
||||
{
|
||||
"id": "allow_functions",
|
||||
"tool_type": r"^function$",
|
||||
"decision": "allow",
|
||||
}
|
||||
],
|
||||
default_action="deny",
|
||||
on_disallowed_action="block",
|
||||
)
|
||||
assert (
|
||||
self.guardrail._matches_pattern(
|
||||
"mcp__github_add_issue_comment", "mcp__github_*_comment"
|
||||
)
|
||||
is True
|
||||
|
||||
is_allowed, rule_id, _ = guardrail._check_tool_permission("AnyTool", "function")
|
||||
assert is_allowed is True
|
||||
assert rule_id == "allow_functions"
|
||||
|
||||
is_allowed, rule_id, _ = guardrail._check_tool_permission("AnyTool", "custom")
|
||||
assert is_allowed is False
|
||||
assert rule_id is None
|
||||
|
||||
def test_rule_matches_tool_with_name_and_type(self):
|
||||
guardrail = ToolPermissionGuardrail(
|
||||
guardrail_name="name-type",
|
||||
rules=[
|
||||
{
|
||||
"id": "allow_specific",
|
||||
"tool_name": r"^Bash$",
|
||||
"tool_type": r"^function$",
|
||||
"decision": "allow",
|
||||
}
|
||||
],
|
||||
default_action="deny",
|
||||
on_disallowed_action="block",
|
||||
)
|
||||
assert (
|
||||
self.guardrail._matches_pattern(
|
||||
"mcp__github_add_issue_comment", "*_comment"
|
||||
|
||||
is_allowed, rule_id, _ = guardrail._check_tool_permission("Bash", "function")
|
||||
assert is_allowed is True
|
||||
assert rule_id == "allow_specific"
|
||||
|
||||
is_allowed, rule_id, _ = guardrail._check_tool_permission("Bash", "custom")
|
||||
assert is_allowed is False
|
||||
assert rule_id is None
|
||||
|
||||
def test_rule_requires_name_or_type(self):
|
||||
with pytest.raises(ValueError):
|
||||
ToolPermissionGuardrail(
|
||||
guardrail_name="invalid-rule",
|
||||
rules=[{"id": "no_target", "decision": "allow"}],
|
||||
default_action="deny",
|
||||
on_disallowed_action="block",
|
||||
)
|
||||
is True
|
||||
|
||||
def test_type_only_rule_skips_param_patterns(self):
|
||||
guardrail = ToolPermissionGuardrail(
|
||||
guardrail_name="type-param",
|
||||
rules=[
|
||||
{
|
||||
"id": "allow_type_only",
|
||||
"tool_type": r"^function$",
|
||||
"decision": "allow",
|
||||
"allowed_param_patterns": {"foo": r"^bar$"},
|
||||
}
|
||||
],
|
||||
default_action="deny",
|
||||
on_disallowed_action="block",
|
||||
)
|
||||
assert (
|
||||
self.guardrail._matches_pattern(
|
||||
"mcp__git_add_issue_comment", "mcp__github_*"
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
self.guardrail._matches_pattern(
|
||||
"mcp__github_assign_copilot_to_issue", "mcp__github_*_comment"
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
self.guardrail._matches_pattern(
|
||||
"mcp__github_assign_copilot_to_issue", "*_comment"
|
||||
)
|
||||
is False
|
||||
|
||||
tool_call = ChatCompletionMessageToolCall(
|
||||
function={"name": "AnyTool", "arguments": "{}"},
|
||||
type="function",
|
||||
)
|
||||
|
||||
is_allowed, rule_id, _ = guardrail._get_permission_for_tool_call(tool_call)
|
||||
assert is_allowed is True
|
||||
assert rule_id == "allow_type_only"
|
||||
|
||||
def test_check_tool_permission_allow(self):
|
||||
is_allowed, rule_id, msg = self.guardrail._check_tool_permission("Bash")
|
||||
assert is_allowed is True
|
||||
|
|
@ -232,7 +278,7 @@ class TestToolPermissionGuardrail:
|
|||
rules=[
|
||||
{
|
||||
"id": "allow_mail",
|
||||
"tool_name": "mail_mcp-send_email",
|
||||
"tool_name": r"^mail_mcp-send_email$",
|
||||
"decision": "allow",
|
||||
"allowed_param_patterns": {
|
||||
"to[]": r"^.+@berri\.ai$",
|
||||
|
|
@ -267,7 +313,7 @@ class TestToolPermissionGuardrail:
|
|||
rules=[
|
||||
{
|
||||
"id": "allow_mail",
|
||||
"tool_name": "mail_mcp-send_email",
|
||||
"tool_name": r"^mail_mcp-send_email$",
|
||||
"decision": "allow",
|
||||
"allowed_param_patterns": {"to[]": r"^.+@berri\.ai$"},
|
||||
}
|
||||
|
|
@ -300,7 +346,7 @@ class TestToolPermissionGuardrail:
|
|||
rules=[
|
||||
{
|
||||
"id": "allow_mail",
|
||||
"tool_name": "mail_mcp-send_email",
|
||||
"tool_name": r"^mail_mcp-send_email$",
|
||||
"decision": "allow",
|
||||
"allowed_param_patterns": {"to[]": r"^.+@berri\.ai$"},
|
||||
}
|
||||
|
|
@ -339,7 +385,7 @@ class TestToolPermissionGuardrail:
|
|||
rules=[
|
||||
{
|
||||
"id": "deny_gmail",
|
||||
"tool_name": "mail_mcp-send_email",
|
||||
"tool_name": r"^mail_mcp-send_email$",
|
||||
"decision": "deny",
|
||||
"allowed_param_patterns": {"to[]": r"^.+@gmail\.com$"},
|
||||
}
|
||||
|
|
@ -486,7 +532,9 @@ class TestToolPermissionGuardrailIntegration:
|
|||
def test_default_action_allow(self):
|
||||
guardrail = ToolPermissionGuardrail(
|
||||
guardrail_name="test-allow-default",
|
||||
rules=[{"id": "deny_read", "tool_name": "Read", "decision": "deny"}],
|
||||
rules=[
|
||||
{"id": "deny_read", "tool_name": r"^Read$", "decision": "deny"}
|
||||
],
|
||||
default_action="allow",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ export type ToolPermissionOnDisallowedAction = "block" | "rewrite";
|
|||
|
||||
export interface ToolPermissionRuleConfig {
|
||||
id: string;
|
||||
tool_name: string;
|
||||
tool_name?: string;
|
||||
tool_type?: string;
|
||||
decision: ToolPermissionDecision;
|
||||
allowed_param_patterns?: Record<string, string>;
|
||||
}
|
||||
|
|
@ -67,7 +68,6 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({
|
|||
...config.rules,
|
||||
{
|
||||
id: `rule_${Math.random().toString(36).slice(2, 8)}`,
|
||||
tool_name: "",
|
||||
decision: "allow" as ToolPermissionDecision,
|
||||
allowed_param_patterns: undefined,
|
||||
},
|
||||
|
|
@ -195,8 +195,8 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({
|
|||
<div>
|
||||
<Text className="text-lg font-semibold">LiteLLM Tool Permission Guardrail</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
Use wildcards (e.g., mcp__github_*) to scope which tools can run and optionally constrain
|
||||
payload fields.
|
||||
Provide regex patterns (e.g., ^mcp__github_.*$) for tool names or types and optionally
|
||||
constrain payload fields.
|
||||
</Text>
|
||||
</div>
|
||||
{!disabled && (
|
||||
|
|
@ -242,12 +242,32 @@ const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium">Tool Name / Pattern</Text>
|
||||
<Text className="text-sm font-medium">Tool Name (optional)</Text>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="mcp__github_*"
|
||||
value={rule.tool_name}
|
||||
onChange={(e) => updateRule(index, { tool_name: e.target.value })}
|
||||
placeholder="^mcp__github_.*$"
|
||||
value={rule.tool_name ?? ""}
|
||||
onChange={(e) =>
|
||||
updateRule(index, {
|
||||
tool_name: e.target.value.trim() === "" ? undefined : e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 mt-4">
|
||||
<div>
|
||||
<Text className="text-sm font-medium">Tool Type (optional)</Text>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="^function$"
|
||||
value={rule.tool_type ?? ""}
|
||||
onChange={(e) =>
|
||||
updateRule(index, {
|
||||
tool_type: e.target.value.trim() === "" ? undefined : e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue