mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
feat: add UI support for configuring tool permission guardrails (#17050)
* feat: add UI support for configuring tool permission guardrails * chore: rename UI/Docs references to "LiteLLM Tool Permission Guardrail"
This commit is contained in:
parent
d2b90e6644
commit
f5a3349fef
11 changed files with 675 additions and 98 deletions
|
|
@ -2,9 +2,9 @@ import Image from '@theme/IdealImage';
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Tool Permission Guardrail
|
||||
# LiteLLM Tool Permission Guardrail
|
||||
|
||||
LiteLLM provides a 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).
|
||||
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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from litellm.types.guardrails import (
|
|||
PiiEntityType,
|
||||
PresidioPresidioConfigModelUserInterface,
|
||||
SupportedGuardrailIntegrations,
|
||||
ToolPermissionGuardrailConfigModel,
|
||||
)
|
||||
|
||||
#### GUARDRAILS ENDPOINTS ####
|
||||
|
|
@ -635,7 +636,9 @@ async def get_guardrail_info(guardrail_id: str):
|
|||
raise HTTPException(status_code=500, detail="Prisma client not initialized")
|
||||
|
||||
try:
|
||||
guardrail_definition_location: GUARDRAIL_DEFINITION_LOCATION = GUARDRAIL_DEFINITION_LOCATION.DB
|
||||
guardrail_definition_location: GUARDRAIL_DEFINITION_LOCATION = (
|
||||
GUARDRAIL_DEFINITION_LOCATION.DB
|
||||
)
|
||||
result = await GUARDRAIL_REGISTRY.get_guardrail_by_id_from_db(
|
||||
guardrail_id=guardrail_id, prisma_client=prisma_client
|
||||
)
|
||||
|
|
@ -702,10 +705,12 @@ async def get_guardrail_ui_settings():
|
|||
# Convert the PII_ENTITY_CATEGORIES_MAP to the format expected by the UI
|
||||
category_maps = []
|
||||
for category, entities in PII_ENTITY_CATEGORIES_MAP.items():
|
||||
category_maps.append({
|
||||
"category": category.value,
|
||||
"entities": [entity.value for entity in entities]
|
||||
})
|
||||
category_maps.append(
|
||||
{
|
||||
"category": category.value,
|
||||
"entities": [entity.value for entity in entities],
|
||||
}
|
||||
)
|
||||
|
||||
return GuardrailUIAddGuardrailSettings(
|
||||
supported_entities=[entity.value for entity in PiiEntityType],
|
||||
|
|
@ -728,20 +733,20 @@ async def get_guardrail_ui_settings():
|
|||
async def validate_blocked_words_file(request: Dict[str, str]):
|
||||
"""
|
||||
Validate a blocked_words YAML file content.
|
||||
|
||||
|
||||
Args:
|
||||
request: Dictionary with 'file_content' key containing the YAML string
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with 'valid' boolean and either 'message'/'errors' depending on result
|
||||
|
||||
|
||||
Example Request:
|
||||
```json
|
||||
{
|
||||
"file_content": "blocked_words:\\n - keyword: \\"test\\"\\n action: \\"BLOCK\\""
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Example Success Response:
|
||||
```json
|
||||
{
|
||||
|
|
@ -749,7 +754,7 @@ async def validate_blocked_words_file(request: Dict[str, str]):
|
|||
"message": "Valid YAML file with 2 blocked words"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Example Error Response:
|
||||
```json
|
||||
{
|
||||
|
|
@ -759,56 +764,54 @@ async def validate_blocked_words_file(request: Dict[str, str]):
|
|||
```
|
||||
"""
|
||||
import yaml
|
||||
|
||||
|
||||
try:
|
||||
file_content = request.get("file_content", "")
|
||||
if not file_content:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": "No file content provided"
|
||||
}
|
||||
|
||||
return {"valid": False, "error": "No file content provided"}
|
||||
|
||||
data = yaml.safe_load(file_content)
|
||||
|
||||
|
||||
if not isinstance(data, dict) or "blocked_words" not in data:
|
||||
return {
|
||||
"valid": False,
|
||||
"error": "Invalid format: file must contain 'blocked_words' key with a list"
|
||||
"error": "Invalid format: file must contain 'blocked_words' key with a list",
|
||||
}
|
||||
|
||||
|
||||
blocked_words_list = data["blocked_words"]
|
||||
if not isinstance(blocked_words_list, list):
|
||||
return {
|
||||
"valid": False,
|
||||
"error": "'blocked_words' must be a list"
|
||||
}
|
||||
|
||||
return {"valid": False, "error": "'blocked_words' must be a list"}
|
||||
|
||||
# Validate each entry
|
||||
errors = []
|
||||
for idx, word_data in enumerate(blocked_words_list):
|
||||
if not isinstance(word_data, dict):
|
||||
errors.append(f"Entry {idx}: must be an object")
|
||||
continue
|
||||
|
||||
|
||||
if "keyword" not in word_data:
|
||||
errors.append(f"Entry {idx}: missing 'keyword' field")
|
||||
elif not isinstance(word_data["keyword"], str):
|
||||
errors.append(f"Entry {idx}: 'keyword' must be a string")
|
||||
|
||||
|
||||
if "action" not in word_data:
|
||||
errors.append(f"Entry {idx}: missing 'action' field")
|
||||
elif word_data["action"] not in ["BLOCK", "MASK"]:
|
||||
errors.append(f"Entry {idx}: action must be 'BLOCK' or 'MASK', got '{word_data['action']}'")
|
||||
|
||||
if "description" in word_data and not isinstance(word_data["description"], str):
|
||||
errors.append(
|
||||
f"Entry {idx}: action must be 'BLOCK' or 'MASK', got '{word_data['action']}'"
|
||||
)
|
||||
|
||||
if "description" in word_data and not isinstance(
|
||||
word_data["description"], str
|
||||
):
|
||||
errors.append(f"Entry {idx}: 'description' must be a string")
|
||||
|
||||
|
||||
if errors:
|
||||
return {"valid": False, "errors": errors}
|
||||
|
||||
|
||||
return {
|
||||
"valid": True,
|
||||
"message": f"Valid YAML file with {len(blocked_words_list)} blocked word(s)"
|
||||
"message": f"Valid YAML file with {len(blocked_words_list)} blocked word(s)",
|
||||
}
|
||||
except yaml.YAMLError as e:
|
||||
return {"valid": False, "error": f"Invalid YAML syntax: {str(e)}"}
|
||||
|
|
@ -931,30 +934,32 @@ def _should_skip_optional_params(field_name: str, field_annotation: Any) -> bool
|
|||
"""Check if optional_params field should be skipped (not meaningfully overridden)."""
|
||||
if field_name != "optional_params":
|
||||
return False
|
||||
|
||||
|
||||
if field_annotation is None:
|
||||
return True
|
||||
|
||||
|
||||
# Check if the annotation is still a generic TypeVar (not specialized)
|
||||
if isinstance(field_annotation, TypeVar) or (
|
||||
hasattr(field_annotation, "__origin__")
|
||||
and field_annotation.__origin__ is TypeVar
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
# Also skip if it's a generic type that wasn't specialized
|
||||
if hasattr(field_annotation, "__name__") and field_annotation.__name__ in (
|
||||
"T",
|
||||
"TypeVar",
|
||||
):
|
||||
return True
|
||||
|
||||
|
||||
# Handle Optional[T] where T is still a TypeVar
|
||||
if hasattr(field_annotation, "__args__"):
|
||||
non_none_args = [arg for arg in field_annotation.__args__ if arg is not type(None)]
|
||||
non_none_args = [
|
||||
arg for arg in field_annotation.__args__ if arg is not type(None)
|
||||
]
|
||||
if non_none_args and isinstance(non_none_args[0], TypeVar):
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -1041,9 +1046,11 @@ def _extract_fields_recursive(
|
|||
|
||||
for field_name, field in model.model_fields.items():
|
||||
field_annotation = field.annotation
|
||||
|
||||
|
||||
# Skip optional_params if it's not meaningfully overridden
|
||||
if _should_skip_optional_params(field_name=field_name, field_annotation=field_annotation):
|
||||
if _should_skip_optional_params(
|
||||
field_name=field_name, field_annotation=field_annotation
|
||||
):
|
||||
continue
|
||||
|
||||
# Handle Optional types and get the actual type
|
||||
|
|
@ -1153,12 +1160,18 @@ async def get_provider_specific_params():
|
|||
bedrock_fields = _get_fields_from_model(BedrockGuardrailConfigModel)
|
||||
presidio_fields = _get_fields_from_model(PresidioPresidioConfigModelUserInterface)
|
||||
lakera_v2_fields = _get_fields_from_model(LakeraV2GuardrailConfigModel)
|
||||
tool_permission_fields = _get_fields_from_model(ToolPermissionGuardrailConfigModel)
|
||||
|
||||
tool_permission_fields[
|
||||
"ui_friendly_name"
|
||||
] = ToolPermissionGuardrailConfigModel.ui_friendly_name()
|
||||
|
||||
# Return the provider-specific parameters
|
||||
provider_params = {
|
||||
SupportedGuardrailIntegrations.BEDROCK.value: bedrock_fields,
|
||||
SupportedGuardrailIntegrations.PRESIDIO.value: presidio_fields,
|
||||
SupportedGuardrailIntegrations.LAKERA_V2.value: lakera_v2_fields,
|
||||
SupportedGuardrailIntegrations.TOOL_PERMISSION.value: tool_permission_fields,
|
||||
}
|
||||
|
||||
### get the config model for the guardrail - go through the registry and get the config model for the guardrail
|
||||
|
|
@ -1175,6 +1188,7 @@ async def get_provider_specific_params():
|
|||
|
||||
return provider_params
|
||||
|
||||
|
||||
@router.post("/guardrails/apply_guardrail", response_model=ApplyGuardrailResponse)
|
||||
@router.post("/apply_guardrail", response_model=ApplyGuardrailResponse)
|
||||
async def apply_guardrail(
|
||||
|
|
@ -1183,11 +1197,11 @@ async def apply_guardrail(
|
|||
):
|
||||
"""
|
||||
Apply a guardrail to text input and return the processed result.
|
||||
|
||||
|
||||
This endpoint allows testing guardrails by applying them to custom text inputs.
|
||||
"""
|
||||
from litellm.proxy.utils import handle_exception_on_proxy
|
||||
|
||||
|
||||
try:
|
||||
active_guardrail: Optional[
|
||||
CustomGuardrail
|
||||
|
|
@ -1207,4 +1221,3 @@ async def apply_guardrail(
|
|||
return ApplyGuardrailResponse(response_text=response_text)
|
||||
except Exception as e:
|
||||
raise handle_exception_on_proxy(e)
|
||||
|
||||
|
|
|
|||
|
|
@ -62,8 +62,11 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
self.rules: List[ToolPermissionRule] = []
|
||||
self._compiled_rule_patterns: Dict[str, Dict[str, re.Pattern]] = {}
|
||||
if rules:
|
||||
for rule_dict in rules:
|
||||
rule = ToolPermissionRule(**rule_dict)
|
||||
for rule_item in rules:
|
||||
if isinstance(rule_item, ToolPermissionRule):
|
||||
rule = rule_item
|
||||
else:
|
||||
rule = ToolPermissionRule(**rule_item)
|
||||
self.rules.append(rule)
|
||||
|
||||
if rule.allowed_param_patterns:
|
||||
|
|
@ -88,6 +91,14 @@ class ToolPermissionGuardrail(CustomGuardrail):
|
|||
self.default_action,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_config_model():
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.tool_permission import (
|
||||
ToolPermissionGuardrailConfigModel,
|
||||
)
|
||||
|
||||
return ToolPermissionGuardrailConfigModel
|
||||
|
||||
def _matches_pattern(self, tool_name: str, pattern: str) -> bool:
|
||||
"""
|
||||
Check if a tool name matches a pattern
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
# litellm/proxy/guardrails/guardrail_initializers.py
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import litellm
|
||||
from litellm.proxy._types import CommonProxyErrors
|
||||
from litellm.types.guardrails import *
|
||||
|
|
@ -128,10 +130,19 @@ def initialize_tool_permission(litellm_params: LitellmParams, guardrail: Guardra
|
|||
ToolPermissionGuardrail,
|
||||
)
|
||||
|
||||
rules: Optional[List[Dict[str, Any]]] = None
|
||||
if litellm_params.rules:
|
||||
rules = []
|
||||
for rule in litellm_params.rules:
|
||||
if hasattr(rule, "model_dump"):
|
||||
rules.append(rule.model_dump())
|
||||
else:
|
||||
rules.append(dict(rule))
|
||||
|
||||
_tool_permission_callback = ToolPermissionGuardrail(
|
||||
guardrail_name=guardrail.get("guardrail_name", ""),
|
||||
event_hook=litellm_params.mode,
|
||||
rules=litellm_params.rules,
|
||||
rules=rules,
|
||||
default_action=getattr(litellm_params, "default_action", "deny"),
|
||||
on_disallowed_action=getattr(litellm_params, "on_disallowed_action", "block"),
|
||||
default_on=litellm_params.default_on,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ from litellm.types.proxy.guardrails.guardrail_hooks.grayswan import (
|
|||
from litellm.types.proxy.guardrails.guardrail_hooks.ibm import (
|
||||
IBMGuardrailsBaseConfigModel,
|
||||
)
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.tool_permission import (
|
||||
ToolPermissionGuardrailConfigModel,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
|
|
@ -415,18 +418,6 @@ class NomaGuardrailConfigModel(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class ToolPermissionGuardrailConfigModel(BaseModel):
|
||||
"""Configuration parameters for the Tool Permission guardrail"""
|
||||
|
||||
rules: Optional[List[Dict]] = Field(
|
||||
default=None, description="List of permission rules for tool usage"
|
||||
)
|
||||
default_action: Optional[str] = Field(
|
||||
default="Deny",
|
||||
description="Default action when no rule matches (Allow or Deny)",
|
||||
)
|
||||
|
||||
|
||||
class ZscalerAIGuardConfigModel(BaseModel):
|
||||
"""Configuration parameters for the Zscaler AI Guard guardrail"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
# Tool Permission Guardrail Type Definitions
|
||||
from typing import Dict, Literal, Optional
|
||||
from typing import Dict, List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import GuardrailConfigModel
|
||||
|
||||
|
||||
class ToolPermissionRule(BaseModel):
|
||||
"""
|
||||
|
|
@ -43,3 +45,23 @@ class PermissionError(BaseModel):
|
|||
tool_name: str = Field(description="Name of the denied tool")
|
||||
rule_id: Optional[str] = Field(description="ID of the rule that caused denial")
|
||||
message: str = Field(description="Error message")
|
||||
|
||||
|
||||
class ToolPermissionGuardrailConfigModel(GuardrailConfigModel):
|
||||
"""Configuration parameters exposed to the UI for the Tool Permission guardrail."""
|
||||
|
||||
rules: Optional[List[ToolPermissionRule]] = Field(
|
||||
default=None,
|
||||
description="Ordered allow/deny rules. Patterns support * wildcards and optional regex constraints on tool arguments.",
|
||||
)
|
||||
default_action: Literal["allow", "deny"] = Field(
|
||||
default="deny", description="Fallback decision when no rule matches"
|
||||
)
|
||||
on_disallowed_action: Literal["block", "rewrite"] = Field(
|
||||
default="block",
|
||||
description="Choose whether disallowed tools block the request or get rewritten out of the payload",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def ui_friendly_name() -> str:
|
||||
return "LiteLLM Tool Permission Guardrail"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Form, Typography, Select, Modal, Tag, Steps } from "antd";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
import {
|
||||
|
|
@ -16,6 +16,9 @@ import GuardrailProviderFields from "./guardrail_provider_fields";
|
|||
import GuardrailOptionalParams from "./guardrail_optional_params";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import ContentFilterConfiguration from "./content_filter/ContentFilterConfiguration";
|
||||
import ToolPermissionRulesEditor, {
|
||||
ToolPermissionConfig,
|
||||
} from "./tool_permission/ToolPermissionRulesEditor";
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
const { Option } = Select;
|
||||
|
|
@ -100,6 +103,20 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
// Content Filter state
|
||||
const [selectedPatterns, setSelectedPatterns] = useState<any[]>([]);
|
||||
const [blockedWords, setBlockedWords] = useState<any[]>([]);
|
||||
const [toolPermissionConfig, setToolPermissionConfig] = useState<ToolPermissionConfig>({
|
||||
rules: [],
|
||||
default_action: "deny",
|
||||
on_disallowed_action: "block",
|
||||
violation_message_template: "",
|
||||
});
|
||||
|
||||
const isToolPermissionProvider = useMemo(() => {
|
||||
if (!selectedProvider) {
|
||||
return false;
|
||||
}
|
||||
const providerValue = guardrail_provider_map[selectedProvider];
|
||||
return (providerValue || "").toLowerCase() === "tool_permission";
|
||||
}, [selectedProvider]);
|
||||
|
||||
// Fetch guardrail UI settings + provider params on mount / accessToken change
|
||||
useEffect(() => {
|
||||
|
|
@ -145,6 +162,13 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
setSelectedCategories([]);
|
||||
setGlobalSeverityThreshold(2);
|
||||
setCategorySpecificThresholds({});
|
||||
|
||||
setToolPermissionConfig({
|
||||
rules: [],
|
||||
default_action: "deny",
|
||||
on_disallowed_action: "block",
|
||||
violation_message_template: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleEntitySelect = (entity: string) => {
|
||||
|
|
@ -225,6 +249,14 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
setSelectedCategories([]);
|
||||
setGlobalSeverityThreshold(2);
|
||||
setCategorySpecificThresholds({});
|
||||
setSelectedPatterns([]);
|
||||
setBlockedWords([]);
|
||||
setToolPermissionConfig({
|
||||
rules: [],
|
||||
default_action: "deny",
|
||||
on_disallowed_action: "block",
|
||||
violation_message_template: "",
|
||||
});
|
||||
setCurrentStep(0);
|
||||
};
|
||||
|
||||
|
|
@ -315,6 +347,20 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
}
|
||||
}
|
||||
|
||||
if (guardrailProvider === "tool_permission") {
|
||||
if (toolPermissionConfig.rules.length === 0) {
|
||||
NotificationsManager.fromBackend("Add at least one tool permission rule");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
guardrailData.litellm_params.rules = toolPermissionConfig.rules;
|
||||
guardrailData.litellm_params.default_action = toolPermissionConfig.default_action;
|
||||
guardrailData.litellm_params.on_disallowed_action = toolPermissionConfig.on_disallowed_action;
|
||||
if (toolPermissionConfig.violation_message_template) {
|
||||
guardrailData.litellm_params.violation_message_template = toolPermissionConfig.violation_message_template;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************
|
||||
* Add provider-specific params
|
||||
* ----------------------------------
|
||||
|
|
@ -535,11 +581,13 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
</Form.Item>
|
||||
|
||||
{/* Use the GuardrailProviderFields component to render provider-specific fields */}
|
||||
<GuardrailProviderFields
|
||||
selectedProvider={selectedProvider}
|
||||
accessToken={accessToken}
|
||||
providerParams={providerParams}
|
||||
/>
|
||||
{!isToolPermissionProvider && (
|
||||
<GuardrailProviderFields
|
||||
selectedProvider={selectedProvider}
|
||||
accessToken={accessToken}
|
||||
providerParams={providerParams}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -593,7 +641,20 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
};
|
||||
|
||||
const renderOptionalParams = () => {
|
||||
if (!selectedProvider || !providerParams) return null;
|
||||
if (!selectedProvider) return null;
|
||||
|
||||
if (isToolPermissionProvider) {
|
||||
return (
|
||||
<ToolPermissionRulesEditor
|
||||
value={toolPermissionConfig}
|
||||
onChange={setToolPermissionConfig}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!providerParams) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("guardrail_provider_map: ", guardrail_provider_map);
|
||||
console.log("selectedProvider: ", selectedProvider);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ import PiiConfiguration from "./pii_configuration";
|
|||
import GuardrailProviderFields from "./guardrail_provider_fields";
|
||||
import GuardrailOptionalParams from "./guardrail_optional_params";
|
||||
import ContentFilterManager, { formatContentFilterDataForAPI } from "./content_filter/ContentFilterManager";
|
||||
import ToolPermissionRulesEditor, {
|
||||
ToolPermissionConfig,
|
||||
} from "./tool_permission/ToolPermissionRulesEditor";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
|
|
@ -83,6 +86,14 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
} | null>(null);
|
||||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
|
||||
const [hasUnsavedContentFilterChanges, setHasUnsavedContentFilterChanges] = useState(false);
|
||||
const emptyToolPermissionConfig: ToolPermissionConfig = {
|
||||
rules: [],
|
||||
default_action: "deny",
|
||||
on_disallowed_action: "block",
|
||||
violation_message_template: "",
|
||||
};
|
||||
const [toolPermissionConfig, setToolPermissionConfig] = useState<ToolPermissionConfig>(emptyToolPermissionConfig);
|
||||
const [toolPermissionDirty, setToolPermissionDirty] = useState(false);
|
||||
|
||||
// Content Filter data ref (managed by ContentFilterManager)
|
||||
const contentFilterDataRef = React.useRef<{ patterns: any[]; blockedWords: any[] }>({
|
||||
|
|
@ -180,6 +191,29 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
}
|
||||
}, [guardrailData, guardrailProviderSpecificParams, form]);
|
||||
|
||||
const resetToolPermissionEditor = useCallback(() => {
|
||||
if (guardrailData?.litellm_params?.guardrail === "tool_permission") {
|
||||
setToolPermissionConfig({
|
||||
rules: (guardrailData.litellm_params?.rules as ToolPermissionConfig["rules"]) || [],
|
||||
default_action: ((guardrailData.litellm_params?.default_action || "deny") as ToolPermissionConfig["default_action"]).toLowerCase() as ToolPermissionConfig["default_action"],
|
||||
on_disallowed_action: ((guardrailData.litellm_params?.on_disallowed_action || "block") as ToolPermissionConfig["on_disallowed_action"]).toLowerCase() as ToolPermissionConfig["on_disallowed_action"],
|
||||
violation_message_template: guardrailData.litellm_params?.violation_message_template || "",
|
||||
});
|
||||
} else {
|
||||
setToolPermissionConfig(emptyToolPermissionConfig);
|
||||
}
|
||||
setToolPermissionDirty(false);
|
||||
}, [guardrailData]);
|
||||
|
||||
useEffect(() => {
|
||||
resetToolPermissionEditor();
|
||||
}, [resetToolPermissionEditor]);
|
||||
|
||||
const handleToolPermissionConfigChange = (config: ToolPermissionConfig) => {
|
||||
setToolPermissionConfig(config);
|
||||
setToolPermissionDirty(true);
|
||||
};
|
||||
|
||||
const handlePiiEntitySelect = (entity: string) => {
|
||||
setSelectedPiiEntities((prev) => {
|
||||
if (prev.includes(entity)) {
|
||||
|
|
@ -255,6 +289,31 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
}
|
||||
}
|
||||
|
||||
if (guardrailData.litellm_params?.guardrail === "tool_permission") {
|
||||
const originalRules = guardrailData.litellm_params?.rules || [];
|
||||
const currentRules = toolPermissionConfig.rules || [];
|
||||
const rulesChanged = JSON.stringify(originalRules) !== JSON.stringify(currentRules);
|
||||
|
||||
const originalDefault = (guardrailData.litellm_params?.default_action || "deny").toLowerCase();
|
||||
const currentDefault = (toolPermissionConfig.default_action || "deny").toLowerCase();
|
||||
const defaultChanged = originalDefault !== currentDefault;
|
||||
|
||||
const originalOnDisallowed = (guardrailData.litellm_params?.on_disallowed_action || "block").toLowerCase();
|
||||
const currentOnDisallowed = (toolPermissionConfig.on_disallowed_action || "block").toLowerCase();
|
||||
const onDisallowedChanged = originalOnDisallowed !== currentOnDisallowed;
|
||||
|
||||
const originalMessage = guardrailData.litellm_params?.violation_message_template || "";
|
||||
const currentMessage = toolPermissionConfig.violation_message_template || "";
|
||||
const messageChanged = originalMessage !== currentMessage;
|
||||
|
||||
if (toolPermissionDirty || rulesChanged || defaultChanged || onDisallowedChanged || messageChanged) {
|
||||
updateData.litellm_params.rules = currentRules;
|
||||
updateData.litellm_params.default_action = currentDefault;
|
||||
updateData.litellm_params.on_disallowed_action = currentOnDisallowed;
|
||||
updateData.litellm_params.violation_message_template = currentMessage || null;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************
|
||||
* Add provider-specific params (reusing logic from add_guardrail_form.tsx)
|
||||
* ----------------------------------
|
||||
|
|
@ -273,7 +332,8 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
console.log("currentProvider: ", currentProvider);
|
||||
|
||||
// Use pre-fetched provider params to copy recognised params
|
||||
if (guardrailProviderSpecificParams && currentProvider) {
|
||||
const isToolPermissionGuardrail = guardrailData.litellm_params?.guardrail === "tool_permission";
|
||||
if (guardrailProviderSpecificParams && currentProvider && !isToolPermissionGuardrail) {
|
||||
const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase();
|
||||
const providerSpecificParams = guardrailProviderSpecificParams[providerKey] || {};
|
||||
|
||||
|
|
@ -488,6 +548,12 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
</Card>
|
||||
)}
|
||||
|
||||
{guardrailData.litellm_params?.guardrail === "tool_permission" && (
|
||||
<Card className="mt-6">
|
||||
<ToolPermissionRulesEditor value={toolPermissionConfig} disabled />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Content Filter Configuration Display */}
|
||||
<ContentFilterManager
|
||||
guardrailData={guardrailData}
|
||||
|
|
@ -575,39 +641,48 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
|
||||
<Divider orientation="left">Provider Settings</Divider>
|
||||
|
||||
{/* Provider-specific fields */}
|
||||
<GuardrailProviderFields
|
||||
selectedProvider={
|
||||
Object.keys(guardrail_provider_map).find(
|
||||
(key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
|
||||
) || null
|
||||
}
|
||||
accessToken={accessToken}
|
||||
providerParams={guardrailProviderSpecificParams}
|
||||
value={guardrailData.litellm_params}
|
||||
/>
|
||||
{guardrailData.litellm_params?.guardrail === "tool_permission" ? (
|
||||
<ToolPermissionRulesEditor
|
||||
value={toolPermissionConfig}
|
||||
onChange={setToolPermissionConfig}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Provider-specific fields */}
|
||||
<GuardrailProviderFields
|
||||
selectedProvider={
|
||||
Object.keys(guardrail_provider_map).find(
|
||||
(key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
|
||||
) || null
|
||||
}
|
||||
accessToken={accessToken}
|
||||
providerParams={guardrailProviderSpecificParams}
|
||||
value={guardrailData.litellm_params}
|
||||
/>
|
||||
|
||||
{/* Optional parameters */}
|
||||
{guardrailProviderSpecificParams &&
|
||||
(() => {
|
||||
const currentProvider = Object.keys(guardrail_provider_map).find(
|
||||
(key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
|
||||
);
|
||||
if (!currentProvider) return null;
|
||||
{/* Optional parameters */}
|
||||
{guardrailProviderSpecificParams &&
|
||||
(() => {
|
||||
const currentProvider = Object.keys(guardrail_provider_map).find(
|
||||
(key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail,
|
||||
);
|
||||
if (!currentProvider) return null;
|
||||
|
||||
const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase();
|
||||
const providerFields = guardrailProviderSpecificParams[providerKey];
|
||||
const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase();
|
||||
const providerFields = guardrailProviderSpecificParams[providerKey];
|
||||
|
||||
if (!providerFields || !providerFields.optional_params) return null;
|
||||
if (!providerFields || !providerFields.optional_params) return null;
|
||||
|
||||
return (
|
||||
<GuardrailOptionalParams
|
||||
optionalParams={providerFields.optional_params}
|
||||
parentFieldKey="optional_params"
|
||||
values={guardrailData.litellm_params}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
return (
|
||||
<GuardrailOptionalParams
|
||||
optionalParams={providerFields.optional_params}
|
||||
parentFieldKey="optional_params"
|
||||
values={guardrailData.litellm_params}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider orientation="left">Advanced Settings</Divider>
|
||||
<Form.Item label="Guardrail Information" name="guardrail_info">
|
||||
|
|
@ -619,6 +694,7 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setHasUnsavedContentFilterChanges(false);
|
||||
resetToolPermissionEditor();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
|
|
@ -672,6 +748,10 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
|
|||
<Text className="font-medium">Last Updated</Text>
|
||||
<div>{formatDate(guardrailData.updated_at)}</div>
|
||||
</div>
|
||||
|
||||
{guardrailData.litellm_params?.guardrail === "tool_permission" && (
|
||||
<ToolPermissionRulesEditor value={toolPermissionConfig} disabled />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export const guardrail_provider_map: Record<string, string> = {
|
|||
Bedrock: "bedrock",
|
||||
Lakera: "lakera_v2",
|
||||
LitellmContentFilter: "litellm_content_filter",
|
||||
ToolPermission: "tool_permission",
|
||||
};
|
||||
|
||||
// Function to populate provider map from API response - updates the original map
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
import React from "react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import ToolPermissionRulesEditor, {
|
||||
ToolPermissionConfig,
|
||||
} from "./ToolPermissionRulesEditor";
|
||||
|
||||
describe("ToolPermissionRulesEditor", () => {
|
||||
it("renders empty state and lets users add a new rule", async () => {
|
||||
const onChange = vi.fn();
|
||||
render(<ToolPermissionRulesEditor value={undefined} onChange={onChange} />);
|
||||
|
||||
expect(screen.getByText(/No tool rules added yet/i)).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: /add rule/i }));
|
||||
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
const payload = onChange.mock.calls[0][0] as ToolPermissionConfig;
|
||||
expect(payload.rules).toHaveLength(1);
|
||||
expect(payload.rules[0].decision).toBe("allow");
|
||||
});
|
||||
|
||||
it("captures violation message and argument constraints", async () => {
|
||||
let latestConfig: ToolPermissionConfig | null = null;
|
||||
const initialConfig: ToolPermissionConfig = {
|
||||
rules: [
|
||||
{
|
||||
id: "allow_bash",
|
||||
tool_name: "Bash",
|
||||
decision: "allow",
|
||||
},
|
||||
],
|
||||
default_action: "deny",
|
||||
on_disallowed_action: "block",
|
||||
violation_message_template: "",
|
||||
};
|
||||
|
||||
const Wrapper = () => {
|
||||
const [state, setState] = React.useState(initialConfig);
|
||||
const handleChange = (next: ToolPermissionConfig) => {
|
||||
latestConfig = next;
|
||||
setState(next);
|
||||
};
|
||||
return <ToolPermissionRulesEditor value={state} onChange={handleChange} />;
|
||||
};
|
||||
|
||||
render(<Wrapper />);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: /restrict tool arguments/i }));
|
||||
const initialInput = await screen.findByPlaceholderText(/messages\[0\].content/i);
|
||||
await userEvent.clear(initialInput);
|
||||
fireEvent.change(initialInput, { target: { value: "input.location" } });
|
||||
|
||||
const violationArea = await screen.findByPlaceholderText(/violates our org policy/i);
|
||||
await userEvent.clear(violationArea);
|
||||
fireEvent.change(violationArea, { target: { value: "Do not run bash" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(latestConfig).not.toBeNull();
|
||||
expect(latestConfig?.rules[0].allowed_param_patterns).toEqual({ "input.location": "" });
|
||||
expect(latestConfig?.violation_message_template).toBe("Do not run bash");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
import React from "react";
|
||||
import { Card, Text } from "@tremor/react";
|
||||
import { Button, Divider, Empty, Input, Select, Space, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined, PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
|
||||
export type ToolPermissionDecision = "allow" | "deny";
|
||||
export type ToolPermissionDefaultAction = "allow" | "deny";
|
||||
export type ToolPermissionOnDisallowedAction = "block" | "rewrite";
|
||||
|
||||
export interface ToolPermissionRuleConfig {
|
||||
id: string;
|
||||
tool_name: string;
|
||||
decision: ToolPermissionDecision;
|
||||
allowed_param_patterns?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ToolPermissionConfig {
|
||||
rules: ToolPermissionRuleConfig[];
|
||||
default_action: ToolPermissionDefaultAction;
|
||||
on_disallowed_action: ToolPermissionOnDisallowedAction;
|
||||
violation_message_template?: string;
|
||||
}
|
||||
|
||||
interface ToolPermissionRulesEditorProps {
|
||||
value?: ToolPermissionConfig;
|
||||
onChange?: (config: ToolPermissionConfig) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: ToolPermissionConfig = {
|
||||
rules: [],
|
||||
default_action: "deny",
|
||||
on_disallowed_action: "block",
|
||||
violation_message_template: "",
|
||||
};
|
||||
|
||||
const ensureConfig = (config?: ToolPermissionConfig): ToolPermissionConfig => ({
|
||||
...DEFAULT_CONFIG,
|
||||
...(config || {}),
|
||||
rules: config?.rules ? [...config.rules] : [],
|
||||
});
|
||||
|
||||
const ToolPermissionRulesEditor: React.FC<ToolPermissionRulesEditorProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const config = ensureConfig(value);
|
||||
|
||||
const updateConfig = (partial: Partial<ToolPermissionConfig>) => {
|
||||
const nextConfig: ToolPermissionConfig = {
|
||||
...config,
|
||||
...partial,
|
||||
};
|
||||
onChange?.(nextConfig);
|
||||
};
|
||||
|
||||
const updateRule = (ruleIndex: number, updates: Partial<ToolPermissionRuleConfig>) => {
|
||||
const nextRules = config.rules.map((rule, index) =>
|
||||
index === ruleIndex ? { ...rule, ...updates } : rule,
|
||||
);
|
||||
updateConfig({ rules: nextRules });
|
||||
};
|
||||
|
||||
const addRule = () => {
|
||||
const nextRules = [
|
||||
...config.rules,
|
||||
{
|
||||
id: `rule_${Math.random().toString(36).slice(2, 8)}`,
|
||||
tool_name: "",
|
||||
decision: "allow" as ToolPermissionDecision,
|
||||
allowed_param_patterns: undefined,
|
||||
},
|
||||
];
|
||||
updateConfig({ rules: nextRules });
|
||||
};
|
||||
|
||||
const removeRule = (ruleIndex: number) => {
|
||||
const nextRules = config.rules.filter((_, index) => index !== ruleIndex);
|
||||
updateConfig({ rules: nextRules });
|
||||
};
|
||||
|
||||
const updateAllowedParamEntries = (
|
||||
ruleIndex: number,
|
||||
mutate: (entries: [string, string][]) => void,
|
||||
) => {
|
||||
const targetRule = config.rules[ruleIndex];
|
||||
if (!targetRule) {
|
||||
return;
|
||||
}
|
||||
const entries = Object.entries(targetRule.allowed_param_patterns || {});
|
||||
mutate(entries);
|
||||
const updatedObject: Record<string, string> = {};
|
||||
entries.forEach(([key, value]) => {
|
||||
updatedObject[key] = value;
|
||||
});
|
||||
updateRule(ruleIndex, {
|
||||
allowed_param_patterns:
|
||||
Object.keys(updatedObject).length > 0 ? updatedObject : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const updateAllowedParamPath = (
|
||||
ruleIndex: number,
|
||||
entryIndex: number,
|
||||
nextPath: string,
|
||||
) => {
|
||||
updateAllowedParamEntries(ruleIndex, (entries) => {
|
||||
if (!entries[entryIndex]) {
|
||||
return;
|
||||
}
|
||||
const [, value] = entries[entryIndex];
|
||||
entries[entryIndex] = [nextPath, value];
|
||||
});
|
||||
};
|
||||
|
||||
const updateAllowedParamPattern = (
|
||||
ruleIndex: number,
|
||||
entryIndex: number,
|
||||
pattern: string,
|
||||
) => {
|
||||
updateAllowedParamEntries(ruleIndex, (entries) => {
|
||||
if (!entries[entryIndex]) {
|
||||
return;
|
||||
}
|
||||
const [path] = entries[entryIndex];
|
||||
entries[entryIndex] = [path, pattern];
|
||||
});
|
||||
};
|
||||
|
||||
const renderAllowedParamPatterns = (rule: ToolPermissionRuleConfig, index: number) => {
|
||||
const entries = Object.entries(rule.allowed_param_patterns || {});
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
onClick={() => updateRule(index, { allowed_param_patterns: { "": "" } })}
|
||||
>
|
||||
+ Restrict tool arguments (optional)
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Text className="text-sm text-gray-600">Argument constraints (dot or array paths)</Text>
|
||||
{entries.map(([path, pattern], patternIndex) => (
|
||||
<Space key={`${rule.id || index}-${patternIndex}`} align="start">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="messages[0].content"
|
||||
value={path}
|
||||
onChange={(e) => updateAllowedParamPath(index, patternIndex, e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="^email@.*$"
|
||||
value={pattern}
|
||||
onChange={(e) => updateAllowedParamPattern(index, patternIndex, e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() =>
|
||||
updateAllowedParamEntries(index, (entries) => {
|
||||
entries.splice(patternIndex, 1);
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
))}
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
onClick={() =>
|
||||
updateRule(index, {
|
||||
allowed_param_patterns: {
|
||||
...(rule.allowed_param_patterns || {}),
|
||||
"": "",
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
+ Add another constraint
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center justify-between">
|
||||
<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.
|
||||
</Text>
|
||||
</div>
|
||||
{!disabled && (
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={addRule}
|
||||
className="!bg-blue-600 !text-white hover:!bg-blue-500"
|
||||
>
|
||||
Add Rule
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{config.rules.length === 0 ? (
|
||||
<Empty description="No tool rules added yet" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{config.rules.map((rule, index) => (
|
||||
<Card key={rule.id || index} className="bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Text className="font-semibold">Rule {index + 1}</Text>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
type="text"
|
||||
disabled={disabled}
|
||||
onClick={() => removeRule(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Text className="text-sm font-medium">Rule ID</Text>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="unique_rule_id"
|
||||
value={rule.id}
|
||||
onChange={(e) => updateRule(index, { id: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium">Tool Name / Pattern</Text>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="mcp__github_*"
|
||||
value={rule.tool_name}
|
||||
onChange={(e) => updateRule(index, { tool_name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<Text className="text-sm font-medium">Decision</Text>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
value={rule.decision}
|
||||
style={{ width: 200 }}
|
||||
onChange={(value) => updateRule(index, { decision: value as ToolPermissionDecision })}
|
||||
>
|
||||
<Select.Option value="allow">Allow</Select.Option>
|
||||
<Select.Option value="deny">Deny</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">{renderAllowedParamPatterns(rule, index)}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Text className="text-sm font-medium">Default action</Text>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
value={config.default_action}
|
||||
onChange={(value) => updateConfig({ default_action: value as ToolPermissionDefaultAction })}
|
||||
>
|
||||
<Select.Option value="allow">Allow</Select.Option>
|
||||
<Select.Option value="deny">Deny</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="text-sm font-medium flex items-center gap-1">
|
||||
On disallowed action
|
||||
<Tooltip title="Block returns an error when a forbidden tool is invoked. Rewrite strips the tool call but lets the rest of the response continue.">
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
value={config.on_disallowed_action}
|
||||
onChange={(value) =>
|
||||
updateConfig({ on_disallowed_action: value as ToolPermissionOnDisallowedAction })
|
||||
}
|
||||
>
|
||||
<Select.Option value="block">Block</Select.Option>
|
||||
<Select.Option value="rewrite">Rewrite</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Text className="text-sm font-medium">Violation message (optional)</Text>
|
||||
<Input.TextArea
|
||||
disabled={disabled}
|
||||
rows={3}
|
||||
placeholder="This violates our org policy..."
|
||||
value={config.violation_message_template}
|
||||
onChange={(e) => updateConfig({ violation_message_template: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolPermissionRulesEditor;
|
||||
Loading…
Add table
Reference in a new issue