mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 06:22:48 +00:00
[Feat] Guardrail Policy Management - Allow using UI to manage guardrail policies (#19668)
* init UI * init schema.prisma * fix: policy_crud_router * UI fixes * update gitignore * working v0 for policy mgmt * fix: endpoints to resolve guardrails * fix code QA checks * ui build issues * schema fixes * fix checks
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
.python-version
|
||||
.venv
|
||||
.venv_policy_test
|
||||
.env
|
||||
.newenv
|
||||
newenv/*
|
||||
|
||||
@@ -124,7 +124,7 @@ model LiteLLM_TeamTable {
|
||||
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||
|
||||
@@ -73,6 +73,7 @@ class SupportedDBObjectType(str, enum.Enum):
|
||||
MODELS = "models"
|
||||
MCP = "mcp"
|
||||
GUARDRAILS = "guardrails"
|
||||
POLICIES = "policies"
|
||||
VECTOR_STORES = "vector_stores"
|
||||
PASS_THROUGH_ENDPOINTS = "pass_through_endpoints"
|
||||
PROMPTS = "prompts"
|
||||
@@ -844,6 +845,7 @@ class GenerateRequestBase(LiteLLMPydanticObjectBase):
|
||||
model_rpm_limit: Optional[dict] = None
|
||||
model_tpm_limit: Optional[dict] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
prompts: Optional[List[str]] = None
|
||||
blocked: Optional[bool] = None
|
||||
aliases: Optional[dict] = {}
|
||||
@@ -1477,6 +1479,7 @@ class NewTeamRequest(TeamBase):
|
||||
model_aliases: Optional[dict] = None
|
||||
tags: Optional[list] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
prompts: Optional[List[str]] = None
|
||||
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
|
||||
allowed_passthrough_routes: Optional[list] = None
|
||||
@@ -1526,6 +1529,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
|
||||
blocked: Optional[bool] = None
|
||||
budget_duration: Optional[str] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
"""
|
||||
|
||||
team_id: str # required
|
||||
@@ -1541,6 +1545,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
|
||||
tags: Optional[list] = None
|
||||
model_aliases: Optional[dict] = None
|
||||
guardrails: Optional[List[str]] = None
|
||||
policies: Optional[List[str]] = None
|
||||
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
|
||||
team_member_budget: Optional[float] = None
|
||||
team_member_budget_duration: Optional[str] = None
|
||||
@@ -3499,6 +3504,7 @@ LiteLLM_ManagementEndpoint_MetadataFields = [
|
||||
|
||||
LiteLLM_ManagementEndpoint_MetadataFields_Premium = [
|
||||
"guardrails",
|
||||
"policies",
|
||||
"tags",
|
||||
"team_member_key_duration",
|
||||
"prompts",
|
||||
|
||||
@@ -1311,6 +1311,118 @@ def _add_guardrails_from_key_or_team_metadata(
|
||||
data[metadata_variable_name]["guardrails"] = list(combined_guardrails)
|
||||
|
||||
|
||||
def _add_guardrails_from_policies_in_metadata(
|
||||
key_metadata: Optional[dict],
|
||||
team_metadata: Optional[dict],
|
||||
data: dict,
|
||||
metadata_variable_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Helper to resolve guardrails from policies attached to key/team metadata.
|
||||
|
||||
This function:
|
||||
1. Gets policy names from key and team metadata
|
||||
2. Resolves guardrails from those policies (including inheritance)
|
||||
3. Adds resolved guardrails to request metadata
|
||||
|
||||
Args:
|
||||
key_metadata: The key metadata dictionary to check for policies
|
||||
team_metadata: The team metadata dictionary to check for policies
|
||||
data: The request data to update
|
||||
metadata_variable_name: The name of the metadata field in data
|
||||
"""
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
|
||||
from litellm.proxy.utils import _premium_user_check
|
||||
from litellm.types.proxy.policy_engine import PolicyMatchContext
|
||||
|
||||
# Collect policy names from key and team metadata
|
||||
policy_names: set = set()
|
||||
|
||||
# Add key-level policies first
|
||||
if key_metadata and "policies" in key_metadata:
|
||||
if (
|
||||
isinstance(key_metadata["policies"], list)
|
||||
and len(key_metadata["policies"]) > 0
|
||||
):
|
||||
_premium_user_check()
|
||||
policy_names.update(key_metadata["policies"])
|
||||
|
||||
# Add team-level policies
|
||||
if team_metadata and "policies" in team_metadata:
|
||||
if (
|
||||
isinstance(team_metadata["policies"], list)
|
||||
and len(team_metadata["policies"]) > 0
|
||||
):
|
||||
_premium_user_check()
|
||||
policy_names.update(team_metadata["policies"])
|
||||
|
||||
if not policy_names:
|
||||
return
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Policy engine: resolving guardrails from key/team policies: {policy_names}"
|
||||
)
|
||||
|
||||
# Check if policy registry is initialized
|
||||
registry = get_policy_registry()
|
||||
if not registry.is_initialized():
|
||||
verbose_proxy_logger.debug(
|
||||
"Policy engine not initialized, skipping policy resolution from metadata"
|
||||
)
|
||||
return
|
||||
|
||||
# Build context for policy resolution (model from request data)
|
||||
context = PolicyMatchContext(model=data.get("model"))
|
||||
|
||||
# Get all policies from registry
|
||||
all_policies = registry.get_all_policies()
|
||||
|
||||
# Resolve guardrails from the specified policies
|
||||
resolved_guardrails: set = set()
|
||||
for policy_name in policy_names:
|
||||
if registry.has_policy(policy_name):
|
||||
resolved_policy = PolicyResolver.resolve_policy_guardrails(
|
||||
policy_name=policy_name,
|
||||
policies=all_policies,
|
||||
context=context,
|
||||
)
|
||||
resolved_guardrails.update(resolved_policy.guardrails)
|
||||
verbose_proxy_logger.debug(
|
||||
f"Policy engine: resolved guardrails from policy '{policy_name}': {resolved_policy.guardrails}"
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Policy engine: policy '{policy_name}' not found in registry"
|
||||
)
|
||||
|
||||
if not resolved_guardrails:
|
||||
return
|
||||
|
||||
# Add resolved guardrails to request metadata
|
||||
if metadata_variable_name not in data:
|
||||
data[metadata_variable_name] = {}
|
||||
|
||||
existing_guardrails = data[metadata_variable_name].get("guardrails", [])
|
||||
if not isinstance(existing_guardrails, list):
|
||||
existing_guardrails = []
|
||||
|
||||
# Combine existing guardrails with policy-resolved guardrails (no duplicates)
|
||||
combined = set(existing_guardrails)
|
||||
combined.update(resolved_guardrails)
|
||||
data[metadata_variable_name]["guardrails"] = list(combined)
|
||||
|
||||
# Store applied policies in metadata for tracking
|
||||
if "applied_policies" not in data[metadata_variable_name]:
|
||||
data[metadata_variable_name]["applied_policies"] = []
|
||||
data[metadata_variable_name]["applied_policies"].extend(list(policy_names))
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
f"Policy engine: added guardrails from key/team policies to request metadata: {list(resolved_guardrails)}"
|
||||
)
|
||||
|
||||
|
||||
def move_guardrails_to_metadata(
|
||||
data: dict,
|
||||
_metadata_variable_name: str,
|
||||
@@ -1321,6 +1433,7 @@ def move_guardrails_to_metadata(
|
||||
|
||||
- If guardrails set on API Key metadata then sets guardrails on request metadata
|
||||
- If guardrails not set on API key, then checks request metadata
|
||||
- Adds guardrails from policies attached to key/team metadata
|
||||
- Adds guardrails from policy engine based on team/key/model context
|
||||
"""
|
||||
# Check key-level guardrails
|
||||
@@ -1331,6 +1444,16 @@ def move_guardrails_to_metadata(
|
||||
metadata_variable_name=_metadata_variable_name,
|
||||
)
|
||||
|
||||
#########################################################################################
|
||||
# Add guardrails from policies attached to key/team metadata
|
||||
#########################################################################################
|
||||
_add_guardrails_from_policies_in_metadata(
|
||||
key_metadata=user_api_key_dict.metadata,
|
||||
team_metadata=user_api_key_dict.team_metadata,
|
||||
data=data,
|
||||
metadata_variable_name=_metadata_variable_name,
|
||||
)
|
||||
|
||||
#########################################################################################
|
||||
# Add guardrails from policy engine based on team/key/model context
|
||||
#########################################################################################
|
||||
|
||||
@@ -14,7 +14,6 @@ These are members of a Team on LiteLLM
|
||||
import asyncio
|
||||
import json
|
||||
import traceback
|
||||
from litellm._uuid import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
@@ -23,6 +22,7 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.hooks.user_management_event_hooks import UserManagementEventHooks
|
||||
@@ -355,6 +355,7 @@ async def new_user(
|
||||
- allowed_cache_controls: Optional[list] - List of allowed cache control values. Example - ["no-cache", "no-store"]. See all values - https://docs.litellm.ai/docs/proxy/caching#turn-on--off-caching-per-request-
|
||||
- blocked: Optional[bool] - [Not Implemented Yet] Whether the user is blocked.
|
||||
- guardrails: Optional[List[str]] - [Not Implemented Yet] List of active guardrails for the user
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the user. Policies define guardrails, conditions, and inheritance rules.
|
||||
- permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking.
|
||||
- metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
|
||||
- max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
|
||||
@@ -1060,6 +1061,7 @@ async def user_update(
|
||||
- allowed_cache_controls: Optional[list] - List of allowed cache control values. Example - ["no-cache", "no-store"]. See all values - https://docs.litellm.ai/docs/proxy/caching#turn-on--off-caching-per-request-
|
||||
- blocked: Optional[bool] - [Not Implemented Yet] Whether the user is blocked.
|
||||
- guardrails: Optional[List[str]] - [Not Implemented Yet] List of active guardrails for the user
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the user. Policies define guardrails, conditions, and inheritance rules.
|
||||
- permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking.
|
||||
- metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
|
||||
- max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
|
||||
|
||||
@@ -14,11 +14,11 @@ import copy
|
||||
import json
|
||||
import secrets
|
||||
import traceback
|
||||
import yaml
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, cast
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
|
||||
import fastapi
|
||||
import yaml
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
|
||||
|
||||
import litellm
|
||||
@@ -31,6 +31,7 @@ from litellm.constants import (
|
||||
UI_SESSION_TOKEN_TEAM_ID,
|
||||
)
|
||||
from litellm.litellm_core_utils.duration_parser import duration_in_seconds
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy._experimental.mcp_server.db import (
|
||||
rotate_mcp_server_credentials_master_key,
|
||||
)
|
||||
@@ -1010,6 +1011,7 @@ async def generate_key_fn(
|
||||
- max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x.
|
||||
- metadata: Optional[dict] - Metadata for key, store information for key. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
|
||||
- guardrails: Optional[List[str]] - List of active guardrails for the key
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- permissions: Optional[dict] - key-specific permissions. Currently just used for turning off pii masking (if connected). Example - {"pii": false}
|
||||
- model_max_budget: Optional[Dict[str, BudgetConfig]] - Model-specific budgets {"gpt-4": {"budget_limit": 0.0005, "time_period": "30d"}}}. IF null or {} then no model specific budget.
|
||||
@@ -1480,6 +1482,7 @@ async def update_key_fn(
|
||||
- permissions: Optional[dict] - Key-specific permissions
|
||||
- send_invite_email: Optional[bool] - Send invite email to user_id
|
||||
- guardrails: Optional[List[str]] - List of active guardrails for the key
|
||||
- policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- prompts: Optional[List[str]] - List of prompts that the key is allowed to use.
|
||||
- blocked: Optional[bool] - Whether the key is blocked
|
||||
@@ -2077,6 +2080,7 @@ async def generate_key_helper_fn( # noqa: PLR0915
|
||||
model_rpm_limit: Optional[dict] = None,
|
||||
model_tpm_limit: Optional[dict] = None,
|
||||
guardrails: Optional[list] = None,
|
||||
policies: Optional[list] = None,
|
||||
prompts: Optional[list] = None,
|
||||
teams: Optional[list] = None,
|
||||
organization_id: Optional[str] = None,
|
||||
@@ -2139,6 +2143,9 @@ async def generate_key_helper_fn( # noqa: PLR0915
|
||||
if guardrails is not None:
|
||||
metadata = metadata or {}
|
||||
metadata["guardrails"] = guardrails
|
||||
if policies is not None:
|
||||
metadata = metadata or {}
|
||||
metadata["policies"] = policies
|
||||
if prompts is not None:
|
||||
metadata = metadata or {}
|
||||
metadata["prompts"] = prompts
|
||||
|
||||
@@ -22,11 +22,13 @@ from pydantic import BaseModel
|
||||
import litellm
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm._uuid import uuid
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.proxy._types import (
|
||||
BlockTeamRequest,
|
||||
CommonProxyErrors,
|
||||
DeleteTeamRequest,
|
||||
LiteLLM_AuditLogs,
|
||||
LiteLLM_DeletedTeamTable,
|
||||
LiteLLM_ManagementEndpoint_MetadataFields,
|
||||
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
|
||||
LiteLLM_ModelTable,
|
||||
@@ -34,7 +36,6 @@ from litellm.proxy._types import (
|
||||
LiteLLM_OrganizationTableWithMembers,
|
||||
LiteLLM_TeamMembership,
|
||||
LiteLLM_TeamTable,
|
||||
LiteLLM_DeletedTeamTable,
|
||||
LiteLLM_TeamTableCachedObj,
|
||||
LiteLLM_UserTable,
|
||||
LiteLLM_VerificationToken,
|
||||
@@ -102,7 +103,7 @@ from litellm.types.proxy.management_endpoints.team_endpoints import (
|
||||
TeamMemberAddResult,
|
||||
UpdateTeamMemberPermissionsRequest,
|
||||
)
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -689,6 +690,7 @@ async def new_team( # noqa: PLR0915
|
||||
- organization_id: Optional[str] - The organization id of the team. Default is None. Create via `/organization/new`.
|
||||
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias)
|
||||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- policies: Optional[List[str]] - Policies for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies)
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"], "agents": ["agent_1", "agent_2"], "agent_access_groups": ["dev_group"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
@@ -1228,6 +1230,7 @@ async def update_team( # noqa: PLR0915
|
||||
- organization_id: Optional[str] - The organization id of the team. Default is None. Create via `/organization/new`.
|
||||
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias)
|
||||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- policies: Optional[List[str]] - Policies for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies)
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"], "agents": ["agent_1", "agent_2"], "agent_access_groups": ["dev_group"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
|
||||
@@ -5,14 +5,20 @@ Attachments define WHERE policies apply, separate from the policy definitions.
|
||||
This allows the same policy to be attached to multiple scopes.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.types.proxy.policy_engine import (
|
||||
PolicyAttachment,
|
||||
PolicyAttachmentCreateRequest,
|
||||
PolicyAttachmentDBResponse,
|
||||
PolicyMatchContext,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
|
||||
|
||||
class AttachmentRegistry:
|
||||
"""
|
||||
@@ -188,6 +194,238 @@ class AttachmentRegistry:
|
||||
)
|
||||
return removed_count
|
||||
|
||||
def remove_attachment_by_id(self, attachment_id: str) -> bool:
|
||||
"""
|
||||
Remove an attachment by its ID (for DB-synced attachments).
|
||||
|
||||
Args:
|
||||
attachment_id: The ID of the attachment to remove
|
||||
|
||||
Returns:
|
||||
True if removed, False if not found
|
||||
"""
|
||||
# Note: In-memory attachments don't have IDs, so this is primarily
|
||||
# for consistency after DB operations
|
||||
return False
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Database CRUD Methods
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def add_attachment_to_db(
|
||||
self,
|
||||
attachment_request: PolicyAttachmentCreateRequest,
|
||||
prisma_client: "PrismaClient",
|
||||
created_by: Optional[str] = None,
|
||||
) -> PolicyAttachmentDBResponse:
|
||||
"""
|
||||
Add a policy attachment to the database.
|
||||
|
||||
Args:
|
||||
attachment_request: The attachment creation request
|
||||
prisma_client: The Prisma client instance
|
||||
created_by: User who created the attachment
|
||||
|
||||
Returns:
|
||||
PolicyAttachmentDBResponse with the created attachment
|
||||
"""
|
||||
try:
|
||||
created_attachment = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.create(
|
||||
data={
|
||||
"policy_name": attachment_request.policy_name,
|
||||
"scope": attachment_request.scope,
|
||||
"teams": attachment_request.teams or [],
|
||||
"keys": attachment_request.keys or [],
|
||||
"models": attachment_request.models or [],
|
||||
"created_at": datetime.now(timezone.utc),
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"created_by": created_by,
|
||||
"updated_by": created_by,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Also add to in-memory registry
|
||||
attachment = PolicyAttachment(
|
||||
policy=attachment_request.policy_name,
|
||||
scope=attachment_request.scope,
|
||||
teams=attachment_request.teams,
|
||||
keys=attachment_request.keys,
|
||||
models=attachment_request.models,
|
||||
)
|
||||
self.add_attachment(attachment)
|
||||
|
||||
return PolicyAttachmentDBResponse(
|
||||
attachment_id=created_attachment.attachment_id,
|
||||
policy_name=created_attachment.policy_name,
|
||||
scope=created_attachment.scope,
|
||||
teams=created_attachment.teams or [],
|
||||
keys=created_attachment.keys or [],
|
||||
models=created_attachment.models or [],
|
||||
created_at=created_attachment.created_at,
|
||||
updated_at=created_attachment.updated_at,
|
||||
created_by=created_attachment.created_by,
|
||||
updated_by=created_attachment.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error adding attachment to DB: {e}")
|
||||
raise Exception(f"Error adding attachment to DB: {str(e)}")
|
||||
|
||||
async def delete_attachment_from_db(
|
||||
self,
|
||||
attachment_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Delete a policy attachment from the database.
|
||||
|
||||
Args:
|
||||
attachment_id: The ID of the attachment to delete
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
Dict with success message
|
||||
"""
|
||||
try:
|
||||
# Get attachment before deleting
|
||||
attachment = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.find_unique(
|
||||
where={"attachment_id": attachment_id}
|
||||
)
|
||||
)
|
||||
|
||||
if attachment is None:
|
||||
raise Exception(f"Attachment with ID {attachment_id} not found")
|
||||
|
||||
# Delete from DB
|
||||
await prisma_client.db.litellm_policyattachmenttable.delete(
|
||||
where={"attachment_id": attachment_id}
|
||||
)
|
||||
|
||||
# Note: In-memory attachments don't have IDs, so we need to sync from DB
|
||||
# to properly update in-memory state
|
||||
await self.sync_attachments_from_db(prisma_client)
|
||||
|
||||
return {"message": f"Attachment {attachment_id} deleted successfully"}
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting attachment from DB: {e}")
|
||||
raise Exception(f"Error deleting attachment from DB: {str(e)}")
|
||||
|
||||
async def get_attachment_by_id_from_db(
|
||||
self,
|
||||
attachment_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Optional[PolicyAttachmentDBResponse]:
|
||||
"""
|
||||
Get a policy attachment by ID from the database.
|
||||
|
||||
Args:
|
||||
attachment_id: The ID of the attachment to retrieve
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
PolicyAttachmentDBResponse if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
attachment = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.find_unique(
|
||||
where={"attachment_id": attachment_id}
|
||||
)
|
||||
)
|
||||
|
||||
if attachment is None:
|
||||
return None
|
||||
|
||||
return PolicyAttachmentDBResponse(
|
||||
attachment_id=attachment.attachment_id,
|
||||
policy_name=attachment.policy_name,
|
||||
scope=attachment.scope,
|
||||
teams=attachment.teams or [],
|
||||
keys=attachment.keys or [],
|
||||
models=attachment.models or [],
|
||||
created_at=attachment.created_at,
|
||||
updated_at=attachment.updated_at,
|
||||
created_by=attachment.created_by,
|
||||
updated_by=attachment.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting attachment from DB: {e}")
|
||||
raise Exception(f"Error getting attachment from DB: {str(e)}")
|
||||
|
||||
async def get_all_attachments_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> List[PolicyAttachmentDBResponse]:
|
||||
"""
|
||||
Get all policy attachments from the database.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
List of PolicyAttachmentDBResponse objects
|
||||
"""
|
||||
try:
|
||||
attachments = (
|
||||
await prisma_client.db.litellm_policyattachmenttable.find_many(
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
PolicyAttachmentDBResponse(
|
||||
attachment_id=a.attachment_id,
|
||||
policy_name=a.policy_name,
|
||||
scope=a.scope,
|
||||
teams=a.teams or [],
|
||||
keys=a.keys or [],
|
||||
models=a.models or [],
|
||||
created_at=a.created_at,
|
||||
updated_at=a.updated_at,
|
||||
created_by=a.created_by,
|
||||
updated_by=a.updated_by,
|
||||
)
|
||||
for a in attachments
|
||||
]
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting attachments from DB: {e}")
|
||||
raise Exception(f"Error getting attachments from DB: {str(e)}")
|
||||
|
||||
async def sync_attachments_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> None:
|
||||
"""
|
||||
Sync policy attachments from the database to in-memory registry.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
"""
|
||||
try:
|
||||
attachments = await self.get_all_attachments_from_db(prisma_client)
|
||||
|
||||
# Clear existing attachments and reload from DB
|
||||
self._attachments = []
|
||||
|
||||
for attachment_response in attachments:
|
||||
attachment = PolicyAttachment(
|
||||
policy=attachment_response.policy_name,
|
||||
scope=attachment_response.scope,
|
||||
teams=attachment_response.teams if attachment_response.teams else None,
|
||||
keys=attachment_response.keys if attachment_response.keys else None,
|
||||
models=attachment_response.models if attachment_response.models else None,
|
||||
)
|
||||
self._attachments.append(attachment)
|
||||
|
||||
self._initialized = True
|
||||
verbose_proxy_logger.info(
|
||||
f"Synced {len(attachments)} attachments from DB to in-memory registry"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error syncing attachments from DB: {e}")
|
||||
raise Exception(f"Error syncing attachments from DB: {str(e)}")
|
||||
|
||||
|
||||
# Global singleton instance
|
||||
_attachment_registry: Optional[AttachmentRegistry] = None
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
"""
|
||||
CRUD ENDPOINTS FOR POLICIES
|
||||
|
||||
Provides REST API endpoints for managing policies and policy attachments.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.policy_engine.attachment_registry import get_attachment_registry
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
from litellm.types.proxy.policy_engine import (
|
||||
PolicyAttachmentCreateRequest,
|
||||
PolicyAttachmentDBResponse,
|
||||
PolicyAttachmentListResponse,
|
||||
PolicyCreateRequest,
|
||||
PolicyDBResponse,
|
||||
PolicyListDBResponse,
|
||||
PolicyUpdateRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Get singleton instances
|
||||
POLICY_REGISTRY = get_policy_registry()
|
||||
ATTACHMENT_REGISTRY = get_attachment_registry()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Policy CRUD Endpoints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/list",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyListDBResponse,
|
||||
)
|
||||
async def list_policies():
|
||||
"""
|
||||
List all policies from the database.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/list" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"policies": [
|
||||
{
|
||||
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"inherit": null,
|
||||
"description": "Base guardrails for all requests",
|
||||
"guardrails_add": ["pii_masking"],
|
||||
"guardrails_remove": [],
|
||||
"condition": null,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 1
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
policies = await POLICY_REGISTRY.get_all_policies_from_db(prisma_client)
|
||||
return PolicyListDBResponse(policies=policies, total_count=len(policies))
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error listing policies: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/policies",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyDBResponse,
|
||||
)
|
||||
async def create_policy(
|
||||
request: PolicyCreateRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Create a new policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/policies" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"policy_name": "global-baseline",
|
||||
"description": "Base guardrails for all requests",
|
||||
"guardrails_add": ["pii_masking", "prompt_injection"],
|
||||
"guardrails_remove": []
|
||||
}'
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"inherit": null,
|
||||
"description": "Base guardrails for all requests",
|
||||
"guardrails_add": ["pii_masking", "prompt_injection"],
|
||||
"guardrails_remove": [],
|
||||
"condition": null,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
created_by = user_api_key_dict.user_id
|
||||
result = await POLICY_REGISTRY.add_policy_to_db(
|
||||
policy_request=request,
|
||||
prisma_client=prisma_client,
|
||||
created_by=created_by,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error creating policy: {e}")
|
||||
if "unique constraint" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Policy with name '{request.policy_name}' already exists",
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/{policy_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyDBResponse,
|
||||
)
|
||||
async def get_policy(policy_id: str):
|
||||
"""
|
||||
Get a policy by ID.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
result = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policy: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put(
|
||||
"/policies/{policy_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyDBResponse,
|
||||
)
|
||||
async def update_policy(
|
||||
policy_id: str,
|
||||
request: PolicyUpdateRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update an existing policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X PUT "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"description": "Updated description",
|
||||
"guardrails_add": ["pii_masking", "toxicity_filter"]
|
||||
}'
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Check if policy exists
|
||||
existing = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if existing is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
|
||||
updated_by = user_api_key_dict.user_id
|
||||
result = await POLICY_REGISTRY.update_policy_in_db(
|
||||
policy_id=policy_id,
|
||||
policy_request=request,
|
||||
prisma_client=prisma_client,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error updating policy: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/policies/{policy_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def delete_policy(policy_id: str):
|
||||
"""
|
||||
Delete a policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"message": "Policy 123e4567-e89b-12d3-a456-426614174000 deleted successfully"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Check if policy exists
|
||||
existing = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if existing is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
|
||||
result = await POLICY_REGISTRY.delete_policy_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting policy: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/{policy_id}/resolved-guardrails",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_resolved_guardrails(policy_id: str):
|
||||
"""
|
||||
Get the resolved guardrails for a policy (including inherited guardrails).
|
||||
|
||||
This endpoint resolves the full inheritance chain and returns the final
|
||||
set of guardrails that would be applied for this policy.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/123e4567-e89b-12d3-a456-426614174000/resolved-guardrails" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"policy_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "healthcare-compliance",
|
||||
"resolved_guardrails": ["pii_masking", "prompt_injection", "toxicity_filter"]
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Get the policy
|
||||
policy = await POLICY_REGISTRY.get_policy_by_id_from_db(
|
||||
policy_id=policy_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if policy is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Policy with ID {policy_id} not found"
|
||||
)
|
||||
|
||||
# Resolve guardrails
|
||||
resolved = await POLICY_REGISTRY.resolve_guardrails_from_db(
|
||||
policy_name=policy.policy_name,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
|
||||
return {
|
||||
"policy_id": policy.policy_id,
|
||||
"policy_name": policy.policy_name,
|
||||
"resolved_guardrails": resolved,
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error resolving guardrails: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Policy Attachment CRUD Endpoints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/attachments/list",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyAttachmentListResponse,
|
||||
)
|
||||
async def list_policy_attachments():
|
||||
"""
|
||||
List all policy attachments from the database.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/attachments/list" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"attachments": [
|
||||
{
|
||||
"attachment_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"scope": "*",
|
||||
"teams": [],
|
||||
"keys": [],
|
||||
"models": [],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"total_count": 1
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
attachments = await ATTACHMENT_REGISTRY.get_all_attachments_from_db(
|
||||
prisma_client
|
||||
)
|
||||
return PolicyAttachmentListResponse(
|
||||
attachments=attachments, total_count=len(attachments)
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error listing policy attachments: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/policies/attachments",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyAttachmentDBResponse,
|
||||
)
|
||||
async def create_policy_attachment(
|
||||
request: PolicyAttachmentCreateRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Create a new policy attachment.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/policies/attachments" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"policy_name": "global-baseline",
|
||||
"scope": "*"
|
||||
}'
|
||||
```
|
||||
|
||||
Example with team-specific attachment:
|
||||
```bash
|
||||
curl -X POST "http://localhost:4000/policies/attachments" \\
|
||||
-H "Authorization: Bearer <your_api_key>" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"policy_name": "healthcare-compliance",
|
||||
"teams": ["healthcare-team", "medical-research"]
|
||||
}'
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"attachment_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"policy_name": "global-baseline",
|
||||
"scope": "*",
|
||||
"teams": [],
|
||||
"keys": [],
|
||||
"models": [],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Verify the policy exists
|
||||
policy = await POLICY_REGISTRY.get_all_policies_from_db(prisma_client)
|
||||
policy_names = [p.policy_name for p in policy]
|
||||
if request.policy_name not in policy_names:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Policy '{request.policy_name}' not found. Create the policy first.",
|
||||
)
|
||||
|
||||
created_by = user_api_key_dict.user_id
|
||||
result = await ATTACHMENT_REGISTRY.add_attachment_to_db(
|
||||
attachment_request=request,
|
||||
prisma_client=prisma_client,
|
||||
created_by=created_by,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error creating policy attachment: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get(
|
||||
"/policies/attachments/{attachment_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=PolicyAttachmentDBResponse,
|
||||
)
|
||||
async def get_policy_attachment(attachment_id: str):
|
||||
"""
|
||||
Get a policy attachment by ID.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X GET "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
result = await ATTACHMENT_REGISTRY.get_attachment_by_id_from_db(
|
||||
attachment_id=attachment_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if result is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Attachment with ID {attachment_id} not found",
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policy attachment: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/policies/attachments/{attachment_id}",
|
||||
tags=["Policies"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def delete_policy_attachment(attachment_id: str):
|
||||
"""
|
||||
Delete a policy attachment.
|
||||
|
||||
Example Request:
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:4000/policies/attachments/123e4567-e89b-12d3-a456-426614174000" \\
|
||||
-H "Authorization: Bearer <your_api_key>"
|
||||
```
|
||||
|
||||
Example Response:
|
||||
```json
|
||||
{
|
||||
"message": "Attachment 123e4567-e89b-12d3-a456-426614174000 deleted successfully"
|
||||
}
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail="Database not connected")
|
||||
|
||||
try:
|
||||
# Check if attachment exists
|
||||
existing = await ATTACHMENT_REGISTRY.get_attachment_by_id_from_db(
|
||||
attachment_id=attachment_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
if existing is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Attachment with ID {attachment_id} not found",
|
||||
)
|
||||
|
||||
result = await ATTACHMENT_REGISTRY.delete_attachment_from_db(
|
||||
attachment_id=attachment_id,
|
||||
prisma_client=prisma_client,
|
||||
)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting policy attachment: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -7,15 +7,22 @@ Policies define WHAT guardrails to apply. WHERE they apply is defined
|
||||
by policy_attachments (see AttachmentRegistry).
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.types.proxy.policy_engine import (
|
||||
Policy,
|
||||
PolicyCondition,
|
||||
PolicyCreateRequest,
|
||||
PolicyDBResponse,
|
||||
PolicyGuardrails,
|
||||
PolicyUpdateRequest,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
|
||||
|
||||
class PolicyRegistry:
|
||||
"""
|
||||
@@ -178,6 +185,367 @@ class PolicyRegistry:
|
||||
return True
|
||||
return False
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Database CRUD Methods
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def add_policy_to_db(
|
||||
self,
|
||||
policy_request: PolicyCreateRequest,
|
||||
prisma_client: "PrismaClient",
|
||||
created_by: Optional[str] = None,
|
||||
) -> PolicyDBResponse:
|
||||
"""
|
||||
Add a policy to the database.
|
||||
|
||||
Args:
|
||||
policy_request: The policy creation request
|
||||
prisma_client: The Prisma client instance
|
||||
created_by: User who created the policy
|
||||
|
||||
Returns:
|
||||
PolicyDBResponse with the created policy
|
||||
"""
|
||||
try:
|
||||
from prisma import Json
|
||||
|
||||
# Build data dict, only include condition if it's set
|
||||
data: Dict[str, Any] = {
|
||||
"policy_name": policy_request.policy_name,
|
||||
"guardrails_add": policy_request.guardrails_add or [],
|
||||
"guardrails_remove": policy_request.guardrails_remove or [],
|
||||
"created_at": datetime.now(timezone.utc),
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
}
|
||||
|
||||
# Only add optional fields if they have values
|
||||
if policy_request.inherit is not None:
|
||||
data["inherit"] = policy_request.inherit
|
||||
if policy_request.description is not None:
|
||||
data["description"] = policy_request.description
|
||||
if created_by is not None:
|
||||
data["created_by"] = created_by
|
||||
data["updated_by"] = created_by
|
||||
if policy_request.condition is not None:
|
||||
data["condition"] = Json(policy_request.condition.model_dump())
|
||||
|
||||
created_policy = await prisma_client.db.litellm_policytable.create(
|
||||
data=data
|
||||
)
|
||||
|
||||
# Also add to in-memory registry
|
||||
policy = self._parse_policy(
|
||||
policy_request.policy_name,
|
||||
{
|
||||
"inherit": policy_request.inherit,
|
||||
"description": policy_request.description,
|
||||
"guardrails": {
|
||||
"add": policy_request.guardrails_add,
|
||||
"remove": policy_request.guardrails_remove,
|
||||
},
|
||||
"condition": policy_request.condition.model_dump()
|
||||
if policy_request.condition
|
||||
else None,
|
||||
},
|
||||
)
|
||||
self.add_policy(policy_request.policy_name, policy)
|
||||
|
||||
return PolicyDBResponse(
|
||||
policy_id=created_policy.policy_id,
|
||||
policy_name=created_policy.policy_name,
|
||||
inherit=created_policy.inherit,
|
||||
description=created_policy.description,
|
||||
guardrails_add=created_policy.guardrails_add or [],
|
||||
guardrails_remove=created_policy.guardrails_remove or [],
|
||||
condition=created_policy.condition,
|
||||
created_at=created_policy.created_at,
|
||||
updated_at=created_policy.updated_at,
|
||||
created_by=created_policy.created_by,
|
||||
updated_by=created_policy.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error adding policy to DB: {e}")
|
||||
raise Exception(f"Error adding policy to DB: {str(e)}")
|
||||
|
||||
async def update_policy_in_db(
|
||||
self,
|
||||
policy_id: str,
|
||||
policy_request: PolicyUpdateRequest,
|
||||
prisma_client: "PrismaClient",
|
||||
updated_by: Optional[str] = None,
|
||||
) -> PolicyDBResponse:
|
||||
"""
|
||||
Update a policy in the database.
|
||||
|
||||
Args:
|
||||
policy_id: The ID of the policy to update
|
||||
policy_request: The policy update request
|
||||
prisma_client: The Prisma client instance
|
||||
updated_by: User who updated the policy
|
||||
|
||||
Returns:
|
||||
PolicyDBResponse with the updated policy
|
||||
"""
|
||||
try:
|
||||
# Build update data - only include fields that are set
|
||||
update_data: Dict[str, Any] = {
|
||||
"updated_at": datetime.now(timezone.utc),
|
||||
"updated_by": updated_by,
|
||||
}
|
||||
|
||||
if policy_request.policy_name is not None:
|
||||
update_data["policy_name"] = policy_request.policy_name
|
||||
if policy_request.inherit is not None:
|
||||
update_data["inherit"] = policy_request.inherit
|
||||
if policy_request.description is not None:
|
||||
update_data["description"] = policy_request.description
|
||||
if policy_request.guardrails_add is not None:
|
||||
update_data["guardrails_add"] = policy_request.guardrails_add
|
||||
if policy_request.guardrails_remove is not None:
|
||||
update_data["guardrails_remove"] = policy_request.guardrails_remove
|
||||
if policy_request.condition is not None:
|
||||
from prisma import Json
|
||||
update_data["condition"] = Json(policy_request.condition.model_dump())
|
||||
|
||||
updated_policy = await prisma_client.db.litellm_policytable.update(
|
||||
where={"policy_id": policy_id},
|
||||
data=update_data,
|
||||
)
|
||||
|
||||
# Update in-memory registry
|
||||
policy = self._parse_policy(
|
||||
updated_policy.policy_name,
|
||||
{
|
||||
"inherit": updated_policy.inherit,
|
||||
"description": updated_policy.description,
|
||||
"guardrails": {
|
||||
"add": updated_policy.guardrails_add,
|
||||
"remove": updated_policy.guardrails_remove,
|
||||
},
|
||||
"condition": updated_policy.condition,
|
||||
},
|
||||
)
|
||||
self.add_policy(updated_policy.policy_name, policy)
|
||||
|
||||
return PolicyDBResponse(
|
||||
policy_id=updated_policy.policy_id,
|
||||
policy_name=updated_policy.policy_name,
|
||||
inherit=updated_policy.inherit,
|
||||
description=updated_policy.description,
|
||||
guardrails_add=updated_policy.guardrails_add or [],
|
||||
guardrails_remove=updated_policy.guardrails_remove or [],
|
||||
condition=updated_policy.condition,
|
||||
created_at=updated_policy.created_at,
|
||||
updated_at=updated_policy.updated_at,
|
||||
created_by=updated_policy.created_by,
|
||||
updated_by=updated_policy.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error updating policy in DB: {e}")
|
||||
raise Exception(f"Error updating policy in DB: {str(e)}")
|
||||
|
||||
async def delete_policy_from_db(
|
||||
self,
|
||||
policy_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Delete a policy from the database.
|
||||
|
||||
Args:
|
||||
policy_id: The ID of the policy to delete
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
Dict with success message
|
||||
"""
|
||||
try:
|
||||
# Get policy name before deleting
|
||||
policy = await prisma_client.db.litellm_policytable.find_unique(
|
||||
where={"policy_id": policy_id}
|
||||
)
|
||||
|
||||
if policy is None:
|
||||
raise Exception(f"Policy with ID {policy_id} not found")
|
||||
|
||||
# Delete from DB
|
||||
await prisma_client.db.litellm_policytable.delete(
|
||||
where={"policy_id": policy_id}
|
||||
)
|
||||
|
||||
# Remove from in-memory registry
|
||||
self.remove_policy(policy.policy_name)
|
||||
|
||||
return {"message": f"Policy {policy_id} deleted successfully"}
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting policy from DB: {e}")
|
||||
raise Exception(f"Error deleting policy from DB: {str(e)}")
|
||||
|
||||
async def get_policy_by_id_from_db(
|
||||
self,
|
||||
policy_id: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> Optional[PolicyDBResponse]:
|
||||
"""
|
||||
Get a policy by ID from the database.
|
||||
|
||||
Args:
|
||||
policy_id: The ID of the policy to retrieve
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
PolicyDBResponse if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
policy = await prisma_client.db.litellm_policytable.find_unique(
|
||||
where={"policy_id": policy_id}
|
||||
)
|
||||
|
||||
if policy is None:
|
||||
return None
|
||||
|
||||
return PolicyDBResponse(
|
||||
policy_id=policy.policy_id,
|
||||
policy_name=policy.policy_name,
|
||||
inherit=policy.inherit,
|
||||
description=policy.description,
|
||||
guardrails_add=policy.guardrails_add or [],
|
||||
guardrails_remove=policy.guardrails_remove or [],
|
||||
condition=policy.condition,
|
||||
created_at=policy.created_at,
|
||||
updated_at=policy.updated_at,
|
||||
created_by=policy.created_by,
|
||||
updated_by=policy.updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policy from DB: {e}")
|
||||
raise Exception(f"Error getting policy from DB: {str(e)}")
|
||||
|
||||
async def get_all_policies_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> List[PolicyDBResponse]:
|
||||
"""
|
||||
Get all policies from the database.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
List of PolicyDBResponse objects
|
||||
"""
|
||||
try:
|
||||
policies = await prisma_client.db.litellm_policytable.find_many(
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
|
||||
return [
|
||||
PolicyDBResponse(
|
||||
policy_id=p.policy_id,
|
||||
policy_name=p.policy_name,
|
||||
inherit=p.inherit,
|
||||
description=p.description,
|
||||
guardrails_add=p.guardrails_add or [],
|
||||
guardrails_remove=p.guardrails_remove or [],
|
||||
condition=p.condition,
|
||||
created_at=p.created_at,
|
||||
updated_at=p.updated_at,
|
||||
created_by=p.created_by,
|
||||
updated_by=p.updated_by,
|
||||
)
|
||||
for p in policies
|
||||
]
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error getting policies from DB: {e}")
|
||||
raise Exception(f"Error getting policies from DB: {str(e)}")
|
||||
|
||||
async def sync_policies_from_db(
|
||||
self,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> None:
|
||||
"""
|
||||
Sync policies from the database to in-memory registry.
|
||||
|
||||
Args:
|
||||
prisma_client: The Prisma client instance
|
||||
"""
|
||||
try:
|
||||
policies = await self.get_all_policies_from_db(prisma_client)
|
||||
|
||||
for policy_response in policies:
|
||||
policy = self._parse_policy(
|
||||
policy_response.policy_name,
|
||||
{
|
||||
"inherit": policy_response.inherit,
|
||||
"description": policy_response.description,
|
||||
"guardrails": {
|
||||
"add": policy_response.guardrails_add,
|
||||
"remove": policy_response.guardrails_remove,
|
||||
},
|
||||
"condition": policy_response.condition,
|
||||
},
|
||||
)
|
||||
self.add_policy(policy_response.policy_name, policy)
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
f"Synced {len(policies)} policies from DB to in-memory registry"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error syncing policies from DB: {e}")
|
||||
raise Exception(f"Error syncing policies from DB: {str(e)}")
|
||||
|
||||
async def resolve_guardrails_from_db(
|
||||
self,
|
||||
policy_name: str,
|
||||
prisma_client: "PrismaClient",
|
||||
) -> List[str]:
|
||||
"""
|
||||
Resolve all guardrails for a policy from the database.
|
||||
|
||||
Uses the existing PolicyResolver to handle inheritance chain resolution.
|
||||
|
||||
Args:
|
||||
policy_name: Name of the policy to resolve
|
||||
prisma_client: The Prisma client instance
|
||||
|
||||
Returns:
|
||||
List of resolved guardrail names
|
||||
"""
|
||||
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
|
||||
|
||||
try:
|
||||
# Load all policies from DB to ensure we have the full inheritance chain
|
||||
policies = await self.get_all_policies_from_db(prisma_client)
|
||||
|
||||
# Build a temporary in-memory map for resolution
|
||||
temp_policies = {}
|
||||
for policy_response in policies:
|
||||
policy = self._parse_policy(
|
||||
policy_response.policy_name,
|
||||
{
|
||||
"inherit": policy_response.inherit,
|
||||
"description": policy_response.description,
|
||||
"guardrails": {
|
||||
"add": policy_response.guardrails_add,
|
||||
"remove": policy_response.guardrails_remove,
|
||||
},
|
||||
"condition": policy_response.condition,
|
||||
},
|
||||
)
|
||||
temp_policies[policy_response.policy_name] = policy
|
||||
|
||||
# Use the existing PolicyResolver to resolve guardrails
|
||||
resolved_policy = PolicyResolver.resolve_policy_guardrails(
|
||||
policy_name=policy_name,
|
||||
policies=temp_policies,
|
||||
context=None, # No context needed for simple resolution
|
||||
)
|
||||
|
||||
return sorted(resolved_policy.guardrails)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error resolving guardrails from DB: {e}")
|
||||
raise Exception(f"Error resolving guardrails from DB: {str(e)}")
|
||||
|
||||
|
||||
# Global singleton instance
|
||||
_policy_registry: Optional[PolicyRegistry] = None
|
||||
|
||||
@@ -381,6 +381,7 @@ from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
|
||||
router as pass_through_router,
|
||||
)
|
||||
from litellm.proxy.policy_engine.policy_endpoints import router as policy_crud_router
|
||||
from litellm.proxy.prompts.prompt_endpoints import router as prompts_router
|
||||
from litellm.proxy.public_endpoints import router as public_endpoints_router
|
||||
from litellm.proxy.rag_endpoints.endpoints import router as rag_router
|
||||
@@ -3801,6 +3802,9 @@ class ProxyConfig:
|
||||
if self._should_load_db_object(object_type="guardrails"):
|
||||
await self._init_guardrails_in_db(prisma_client=prisma_client)
|
||||
|
||||
if self._should_load_db_object(object_type="policies"):
|
||||
await self._init_policies_in_db(prisma_client=prisma_client)
|
||||
|
||||
if self._should_load_db_object(object_type="vector_stores"):
|
||||
await self._init_vector_stores_in_db(prisma_client=prisma_client)
|
||||
|
||||
@@ -4024,6 +4028,36 @@ class ProxyConfig:
|
||||
)
|
||||
)
|
||||
|
||||
async def _init_policies_in_db(self, prisma_client: PrismaClient):
|
||||
"""
|
||||
Initialize policies and policy attachments from database into the in-memory registries.
|
||||
"""
|
||||
from litellm.proxy.policy_engine.attachment_registry import (
|
||||
get_attachment_registry,
|
||||
)
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
|
||||
try:
|
||||
# Get the global singleton instances
|
||||
policy_registry = get_policy_registry()
|
||||
attachment_registry = get_attachment_registry()
|
||||
|
||||
# Sync policies from DB to in-memory registry
|
||||
await policy_registry.sync_policies_from_db(prisma_client=prisma_client)
|
||||
|
||||
# Sync attachments from DB to in-memory registry
|
||||
await attachment_registry.sync_attachments_from_db(prisma_client=prisma_client)
|
||||
|
||||
verbose_proxy_logger.debug(
|
||||
"Successfully synced policies and attachments from DB"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(
|
||||
"litellm.proxy.proxy_server.py::ProxyConfig:_init_policies_in_db - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
|
||||
async def _init_vector_stores_in_db(self, prisma_client: PrismaClient):
|
||||
from litellm.vector_stores.vector_store_registry import VectorStoreRegistry
|
||||
|
||||
@@ -10702,6 +10736,7 @@ app.include_router(caching_router)
|
||||
app.include_router(analytics_router)
|
||||
app.include_router(guardrails_router)
|
||||
app.include_router(policy_router)
|
||||
app.include_router(policy_crud_router)
|
||||
app.include_router(search_tool_management_router)
|
||||
app.include_router(prompts_router)
|
||||
app.include_router(callback_management_endpoints_router)
|
||||
|
||||
@@ -124,7 +124,7 @@ model LiteLLM_TeamTable {
|
||||
updated_at DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
model_spend Json @default("{}")
|
||||
model_max_budget Json @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
router_settings Json? @default("{}")
|
||||
team_member_permissions String[] @default([])
|
||||
model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases
|
||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||
@@ -863,20 +863,3 @@ model LiteLLM_SkillsTable {
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
updated_by String?
|
||||
}
|
||||
|
||||
// Claude Code Marketplace - stores plugins for Claude Code integration
|
||||
model LiteLLM_ClaudeCodePluginTable {
|
||||
id String @id @default(uuid())
|
||||
name String @unique // Plugin name (kebab-case)
|
||||
version String? // Semantic version
|
||||
description String? // Plugin description
|
||||
manifest_json String // Full plugin.json as JSON string
|
||||
files_json String // All files as JSON: {"path": "content"}
|
||||
enabled Boolean @default(true)
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @default(now()) @updatedAt
|
||||
created_by String?
|
||||
|
||||
@@index([name])
|
||||
@@map("litellm_claudecodeplugin")
|
||||
}
|
||||
|
||||
@@ -19,13 +19,21 @@ from litellm.types.proxy.policy_engine.policy_types import (
|
||||
PolicyScope,
|
||||
)
|
||||
from litellm.types.proxy.policy_engine.resolver_types import (
|
||||
PolicyAttachmentCreateRequest,
|
||||
PolicyAttachmentDBResponse,
|
||||
PolicyAttachmentListResponse,
|
||||
PolicyConditionRequest,
|
||||
PolicyCreateRequest,
|
||||
PolicyDBResponse,
|
||||
PolicyGuardrailsResponse,
|
||||
PolicyInfoResponse,
|
||||
PolicyListDBResponse,
|
||||
PolicyListResponse,
|
||||
PolicyMatchContext,
|
||||
PolicyScopeResponse,
|
||||
PolicySummaryItem,
|
||||
PolicyTestResponse,
|
||||
PolicyUpdateRequest,
|
||||
ResolvedPolicy,
|
||||
)
|
||||
from litellm.types.proxy.policy_engine.validation_types import (
|
||||
@@ -58,4 +66,13 @@ __all__ = [
|
||||
"PolicyScopeResponse",
|
||||
"PolicySummaryItem",
|
||||
"PolicyTestResponse",
|
||||
# CRUD Request/Response types
|
||||
"PolicyConditionRequest",
|
||||
"PolicyCreateRequest",
|
||||
"PolicyUpdateRequest",
|
||||
"PolicyDBResponse",
|
||||
"PolicyListDBResponse",
|
||||
"PolicyAttachmentCreateRequest",
|
||||
"PolicyAttachmentDBResponse",
|
||||
"PolicyAttachmentListResponse",
|
||||
]
|
||||
|
||||
@@ -5,7 +5,8 @@ These types are used for matching requests to policies and resolving
|
||||
the final guardrails list.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
@@ -108,3 +109,168 @@ class PolicyTestResponse(BaseModel):
|
||||
matching_policies: List[str]
|
||||
resolved_guardrails: List[str]
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CRUD Request/Response Types for Policy Endpoints
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class PolicyConditionRequest(BaseModel):
|
||||
"""Condition for when a policy applies."""
|
||||
|
||||
model: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Model name pattern (exact match or regex) for when policy applies.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyCreateRequest(BaseModel):
|
||||
"""Request body for creating a new policy."""
|
||||
|
||||
policy_name: str = Field(description="Unique name for the policy.")
|
||||
inherit: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Name of parent policy to inherit from.",
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Human-readable description of the policy.",
|
||||
)
|
||||
guardrails_add: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to add.",
|
||||
)
|
||||
guardrails_remove: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to remove (from inherited).",
|
||||
)
|
||||
condition: Optional[PolicyConditionRequest] = Field(
|
||||
default=None,
|
||||
description="Condition for when this policy applies.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyUpdateRequest(BaseModel):
|
||||
"""Request body for updating a policy."""
|
||||
|
||||
policy_name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="New name for the policy.",
|
||||
)
|
||||
inherit: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Name of parent policy to inherit from.",
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Human-readable description of the policy.",
|
||||
)
|
||||
guardrails_add: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to add.",
|
||||
)
|
||||
guardrails_remove: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="List of guardrail names to remove (from inherited).",
|
||||
)
|
||||
condition: Optional[PolicyConditionRequest] = Field(
|
||||
default=None,
|
||||
description="Condition for when this policy applies.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyDBResponse(BaseModel):
|
||||
"""Response for a policy from the database."""
|
||||
|
||||
policy_id: str = Field(description="Unique ID of the policy.")
|
||||
policy_name: str = Field(description="Name of the policy.")
|
||||
inherit: Optional[str] = Field(default=None, description="Parent policy name.")
|
||||
description: Optional[str] = Field(default=None, description="Policy description.")
|
||||
guardrails_add: List[str] = Field(
|
||||
default_factory=list, description="Guardrails to add."
|
||||
)
|
||||
guardrails_remove: List[str] = Field(
|
||||
default_factory=list, description="Guardrails to remove."
|
||||
)
|
||||
condition: Optional[Dict[str, Any]] = Field(
|
||||
default=None, description="Policy condition."
|
||||
)
|
||||
created_at: Optional[datetime] = Field(
|
||||
default=None, description="When the policy was created."
|
||||
)
|
||||
updated_at: Optional[datetime] = Field(
|
||||
default=None, description="When the policy was last updated."
|
||||
)
|
||||
created_by: Optional[str] = Field(default=None, description="Who created the policy.")
|
||||
updated_by: Optional[str] = Field(
|
||||
default=None, description="Who last updated the policy."
|
||||
)
|
||||
|
||||
|
||||
class PolicyListDBResponse(BaseModel):
|
||||
"""Response for listing policies from the database."""
|
||||
|
||||
policies: List[PolicyDBResponse] = Field(
|
||||
default_factory=list, description="List of policies."
|
||||
)
|
||||
total_count: int = Field(default=0, description="Total number of policies.")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Policy Attachment CRUD Types
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class PolicyAttachmentCreateRequest(BaseModel):
|
||||
"""Request body for creating a policy attachment."""
|
||||
|
||||
policy_name: str = Field(description="Name of the policy to attach.")
|
||||
scope: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Use '*' for global scope (applies to all requests).",
|
||||
)
|
||||
teams: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Team aliases or patterns this attachment applies to.",
|
||||
)
|
||||
keys: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Key aliases or patterns this attachment applies to.",
|
||||
)
|
||||
models: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Model names or patterns this attachment applies to.",
|
||||
)
|
||||
|
||||
|
||||
class PolicyAttachmentDBResponse(BaseModel):
|
||||
"""Response for a policy attachment from the database."""
|
||||
|
||||
attachment_id: str = Field(description="Unique ID of the attachment.")
|
||||
policy_name: str = Field(description="Name of the attached policy.")
|
||||
scope: Optional[str] = Field(default=None, description="Scope of the attachment.")
|
||||
teams: List[str] = Field(default_factory=list, description="Team patterns.")
|
||||
keys: List[str] = Field(default_factory=list, description="Key patterns.")
|
||||
models: List[str] = Field(default_factory=list, description="Model patterns.")
|
||||
created_at: Optional[datetime] = Field(
|
||||
default=None, description="When the attachment was created."
|
||||
)
|
||||
updated_at: Optional[datetime] = Field(
|
||||
default=None, description="When the attachment was last updated."
|
||||
)
|
||||
created_by: Optional[str] = Field(
|
||||
default=None, description="Who created the attachment."
|
||||
)
|
||||
updated_by: Optional[str] = Field(
|
||||
default=None, description="Who last updated the attachment."
|
||||
)
|
||||
|
||||
|
||||
class PolicyAttachmentListResponse(BaseModel):
|
||||
"""Response for listing policy attachments."""
|
||||
|
||||
attachments: List[PolicyAttachmentDBResponse] = Field(
|
||||
default_factory=list, description="List of policy attachments."
|
||||
)
|
||||
total_count: int = Field(default=0, description="Total number of attachments.")
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ExperimentOutlined,
|
||||
ToolOutlined,
|
||||
TagsOutlined,
|
||||
AuditOutlined,
|
||||
} from "@ant-design/icons";
|
||||
// import {
|
||||
// all_admin_roles,
|
||||
@@ -102,6 +103,8 @@ const routeFor = (slug: string): string => {
|
||||
return "logs";
|
||||
case "guardrails":
|
||||
return "guardrails";
|
||||
case "policies":
|
||||
return "policies";
|
||||
|
||||
// tools
|
||||
case "mcp-servers":
|
||||
@@ -202,6 +205,13 @@ const menuItems: MenuItemCfg[] = [
|
||||
icon: <SafetyOutlined style={{ fontSize: 18 }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "28",
|
||||
page: "policies",
|
||||
label: "Policies",
|
||||
icon: <AuditOutlined style={{ fontSize: 18 }} />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "26",
|
||||
page: "tools",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import PoliciesPanel from "@/components/policies";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import {
|
||||
getPoliciesList,
|
||||
createPolicyCall,
|
||||
updatePolicyCall,
|
||||
deletePolicyCall,
|
||||
getPolicyInfo,
|
||||
getPolicyAttachmentsList,
|
||||
createPolicyAttachmentCall,
|
||||
deletePolicyAttachmentCall,
|
||||
getGuardrailsList,
|
||||
} from "@/components/networking";
|
||||
|
||||
const PoliciesPage = () => {
|
||||
const { accessToken, userRole } = useAuthorized();
|
||||
|
||||
return (
|
||||
<PoliciesPanel
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
getPoliciesList={getPoliciesList}
|
||||
createPolicy={createPolicyCall}
|
||||
updatePolicy={updatePolicyCall}
|
||||
deletePolicy={deletePolicyCall}
|
||||
getPolicy={getPolicyInfo}
|
||||
getAttachmentsList={getPolicyAttachmentsList}
|
||||
createAttachment={createPolicyAttachmentCall}
|
||||
deleteAttachment={deletePolicyAttachmentCall}
|
||||
getGuardrailsList={getGuardrailsList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoliciesPage;
|
||||
@@ -14,6 +14,7 @@ import LoadingScreen from "@/components/common_components/LoadingScreen";
|
||||
import { CostTrackingSettings } from "@/components/CostTrackingSettings";
|
||||
import GeneralSettings from "@/components/general_settings";
|
||||
import GuardrailsPanel from "@/components/guardrails";
|
||||
import PoliciesPanel from "@/components/policies";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { MCPServers } from "@/components/mcp_tools";
|
||||
import ModelHubTable from "@/components/AIHub/ModelHubTable";
|
||||
@@ -472,6 +473,8 @@ export default function CreateKeyPage() {
|
||||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "guardrails" ? (
|
||||
<GuardrailsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "policies" ? (
|
||||
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "agents" ? (
|
||||
<AgentsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "prompts" ? (
|
||||
|
||||
@@ -3,6 +3,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreOutlined,
|
||||
AuditOutlined,
|
||||
BankOutlined,
|
||||
BarChartOutlined,
|
||||
BgColorsOutlined,
|
||||
@@ -123,6 +124,13 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
||||
icon: <SafetyOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "policies",
|
||||
page: "policies",
|
||||
label: "Policies",
|
||||
icon: <AuditOutlined />,
|
||||
roles: all_admin_roles,
|
||||
},
|
||||
{
|
||||
key: "tools",
|
||||
page: "tools",
|
||||
|
||||
@@ -5347,6 +5347,249 @@ export const getGuardrailsList = async (accessToken: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Policy CRUD API Calls
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const getPoliciesList = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/list` : `/policies/list`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get policies list:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createPolicyCall = async (accessToken: string, policyData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(policyData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create policy:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePolicyCall = async (accessToken: string, policyId: string, policyData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(policyData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to update policy:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePolicyCall = async (accessToken: string, policyId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete policy:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPolicyInfo = async (accessToken: string, policyId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get policy info:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Policy Attachments API Calls
|
||||
|
||||
export const getPolicyAttachmentsList = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/list` : `/policies/attachments/list`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get policy attachments list:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createPolicyAttachmentCall = async (accessToken: string, attachmentData: any) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments` : `/policies/attachments`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(attachmentData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create policy attachment:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePolicyAttachmentCall = async (accessToken: string, attachmentId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/${attachmentId}` : `/policies/attachments/${attachmentId}`;
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to delete policy attachment:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getResolvedGuardrails = async (accessToken: string, policyId: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}/resolved-guardrails` : `/policies/${policyId}/resolved-guardrails`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get resolved guardrails:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPromptsList = async (accessToken: string): Promise<ListPromptsResponse> => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/prompts/list` : `/prompts/list`;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { coy } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { truncateString } from "../../../utils/textUtils";
|
||||
import GuardrailSelector from "../../guardrails/GuardrailSelector";
|
||||
import PolicySelector from "../../policies/PolicySelector";
|
||||
import { MCPServer } from "../../mcp_tools/types";
|
||||
import NotificationsManager from "../../molecules/notifications_manager";
|
||||
import { fetchMCPServers, listMCPTools } from "../../networking";
|
||||
@@ -188,6 +189,15 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const [selectedPolicies, setSelectedPolicies] = useState<string[]>(() => {
|
||||
const saved = sessionStorage.getItem("selectedPolicies");
|
||||
try {
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch (error) {
|
||||
console.error("Error parsing selectedPolicies from sessionStorage", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const [messageTraceId, setMessageTraceId] = useState<string | null>(
|
||||
() => sessionStorage.getItem("messageTraceId") || null,
|
||||
);
|
||||
@@ -261,6 +271,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
selectedMCPServers,
|
||||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
@@ -283,6 +294,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
selectedMCPServers,
|
||||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
@@ -308,6 +320,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
sessionStorage.setItem("selectedTags", JSON.stringify(selectedTags));
|
||||
sessionStorage.setItem("selectedVectorStores", JSON.stringify(selectedVectorStores));
|
||||
sessionStorage.setItem("selectedGuardrails", JSON.stringify(selectedGuardrails));
|
||||
sessionStorage.setItem("selectedPolicies", JSON.stringify(selectedPolicies));
|
||||
sessionStorage.setItem("selectedMCPServers", JSON.stringify(selectedMCPServers));
|
||||
sessionStorage.setItem("mcpServerToolRestrictions", JSON.stringify(mcpServerToolRestrictions));
|
||||
sessionStorage.setItem("selectedVoice", selectedVoice);
|
||||
@@ -338,6 +351,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
messageTraceId,
|
||||
responsesSessionId,
|
||||
useApiSessionManagement,
|
||||
@@ -897,6 +911,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
traceId,
|
||||
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
selectedMCPServers,
|
||||
updateChatImageUI,
|
||||
updateSearchResults,
|
||||
@@ -977,6 +992,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
traceId,
|
||||
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
selectedMCPServers, // Pass the selected servers array
|
||||
useApiSessionManagement ? responsesSessionId : null, // Only pass session ID if API mode is enabled
|
||||
handleResponseId, // Pass callback to capture new response ID
|
||||
@@ -1008,6 +1024,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
traceId,
|
||||
selectedVectorStores.length > 0 ? selectedVectorStores : undefined,
|
||||
selectedGuardrails.length > 0 ? selectedGuardrails : undefined,
|
||||
selectedPolicies.length > 0 ? selectedPolicies : undefined,
|
||||
selectedMCPServers, // Pass the selected tools array
|
||||
customProxyBaseUrl || undefined,
|
||||
);
|
||||
@@ -1587,6 +1604,32 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium block mb-2 text-gray-700 flex items-center">
|
||||
<SafetyOutlined className="mr-2" /> Policies
|
||||
<Tooltip
|
||||
className="ml-1"
|
||||
title={
|
||||
<span>
|
||||
Select policy/policies to apply to this LLM API call. Policies define which guardrails are applied based on conditions. You can set up your policies{" "}
|
||||
<a href="?page=policies" style={{ color: "#1890ff" }}>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<PolicySelector
|
||||
value={selectedPolicies}
|
||||
onChange={setSelectedPolicies}
|
||||
className="mb-4"
|
||||
accessToken={accessToken || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Code Interpreter Toggle - Only for Responses endpoint */}
|
||||
{endpointType === EndpointType.RESPONSES && (
|
||||
<div>
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CodeGenMetadata {
|
||||
tags?: string[];
|
||||
vector_stores?: string[];
|
||||
guardrails?: string[];
|
||||
policies?: string[];
|
||||
}
|
||||
|
||||
interface GenerateCodeParams {
|
||||
@@ -17,6 +18,7 @@ interface GenerateCodeParams {
|
||||
selectedTags: string[];
|
||||
selectedVectorStores: string[];
|
||||
selectedGuardrails: string[];
|
||||
selectedPolicies: string[];
|
||||
selectedMCPServers: string[];
|
||||
mcpServers?: MCPServer[];
|
||||
mcpServerToolRestrictions?: Record<string, string[]>;
|
||||
@@ -40,6 +42,7 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
|
||||
selectedTags,
|
||||
selectedVectorStores,
|
||||
selectedGuardrails,
|
||||
selectedPolicies,
|
||||
selectedMCPServers,
|
||||
mcpServers,
|
||||
mcpServerToolRestrictions,
|
||||
@@ -72,6 +75,7 @@ export const generateCodeSnippet = (params: GenerateCodeParams): string => {
|
||||
if (selectedTags.length > 0) metadata.tags = selectedTags;
|
||||
if (selectedVectorStores.length > 0) metadata.vector_stores = selectedVectorStores;
|
||||
if (selectedGuardrails.length > 0) metadata.guardrails = selectedGuardrails;
|
||||
if (selectedPolicies.length > 0) metadata.policies = selectedPolicies;
|
||||
|
||||
const modelNameForCode = selectedModel || "your-model-name";
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export async function makeAnthropicMessagesRequest(
|
||||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
policies?: string[],
|
||||
selectedMCPTools?: string[],
|
||||
customBaseUrl?: string,
|
||||
) {
|
||||
@@ -59,6 +60,7 @@ export async function makeAnthropicMessagesRequest(
|
||||
|
||||
if (vector_store_ids) requestBody.vector_store_ids = vector_store_ids;
|
||||
if (guardrails) requestBody.guardrails = guardrails;
|
||||
if (policies) requestBody.policies = policies;
|
||||
// Use the streaming helper method for cleaner async iteration
|
||||
// @ts-ignore - The SDK types might not include all litellm-specific parameters
|
||||
const stream = client.messages.stream(requestBody, { signal });
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function makeOpenAIChatCompletionRequest(
|
||||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
policies?: string[],
|
||||
selectedMCPServers?: string[],
|
||||
onImageGenerated?: (imageUrl: string, model?: string) => void,
|
||||
onSearchResults?: (searchResults: VectorStoreSearchResponse[]) => void,
|
||||
@@ -110,6 +111,7 @@ export async function makeOpenAIChatCompletionRequest(
|
||||
messages: chatHistory as ChatCompletionMessageParam[],
|
||||
...(vector_store_ids ? { vector_store_ids } : {}),
|
||||
...(guardrails ? { guardrails } : {}),
|
||||
...(policies ? { policies } : {}),
|
||||
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
|
||||
...(temperature !== undefined ? { temperature } : {}),
|
||||
...(max_tokens !== undefined ? { max_tokens } : {}),
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function makeOpenAIResponsesRequest(
|
||||
traceId?: string,
|
||||
vector_store_ids?: string[],
|
||||
guardrails?: string[],
|
||||
policies?: string[],
|
||||
selectedMCPServers?: string[],
|
||||
previousResponseId?: string | null,
|
||||
onResponseId?: (responseId: string) => void,
|
||||
@@ -137,6 +138,7 @@ export async function makeOpenAIResponsesRequest(
|
||||
...(previousResponseId ? { previous_response_id: previousResponseId } : {}),
|
||||
...(vector_store_ids ? { vector_store_ids } : {}),
|
||||
...(guardrails ? { guardrails } : {}),
|
||||
...(policies ? { policies } : {}),
|
||||
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
|
||||
},
|
||||
{ signal },
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
import { Policy } from "./types";
|
||||
import { getPoliciesList } from "../networking";
|
||||
|
||||
interface PolicySelectorProps {
|
||||
onChange: (selectedPolicies: string[]) => void;
|
||||
value?: string[];
|
||||
className?: string;
|
||||
accessToken: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const PolicySelector: React.FC<PolicySelectorProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
className,
|
||||
accessToken,
|
||||
disabled
|
||||
}) => {
|
||||
const [policies, setPolicies] = useState<Policy[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPolicies = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getPoliciesList(accessToken);
|
||||
console.log("Policies response:", response);
|
||||
if (response.policies) {
|
||||
console.log("Policies data:", response.policies);
|
||||
setPolicies(response.policies);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching policies:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPolicies();
|
||||
}, [accessToken]);
|
||||
|
||||
const handlePolicyChange = (selectedValues: string[]) => {
|
||||
console.log("Selected policies:", selectedValues);
|
||||
onChange(selectedValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
mode="multiple"
|
||||
disabled={disabled}
|
||||
placeholder={disabled ? "Setting policies is a premium feature." : "Select policies"}
|
||||
onChange={handlePolicyChange}
|
||||
value={value}
|
||||
loading={loading}
|
||||
className={className}
|
||||
allowClear
|
||||
options={policies.map((policy) => {
|
||||
console.log("Mapping policy:", policy);
|
||||
return {
|
||||
label: `${policy.policy_name}${policy.description ? ` - ${policy.description}` : ""}`,
|
||||
value: policy.policy_name,
|
||||
};
|
||||
})}
|
||||
optionFilterProp="label"
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicySelector;
|
||||
@@ -0,0 +1,254 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
|
||||
import { Button } from "@tremor/react";
|
||||
import { Policy, PolicyAttachmentCreateRequest } from "./types";
|
||||
import { createPolicyAttachmentCall, teamListCall, keyInfoCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface AddAttachmentFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
policies: Policy[];
|
||||
}
|
||||
|
||||
const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
accessToken,
|
||||
policies,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
|
||||
const [availableTeams, setAvailableTeams] = useState<string[]>([]);
|
||||
const [availableKeys, setAvailableKeys] = useState<string[]>([]);
|
||||
const [isLoadingTeams, setIsLoadingTeams] = useState(false);
|
||||
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && accessToken) {
|
||||
loadTeamsAndKeys();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, accessToken]);
|
||||
|
||||
const loadTeamsAndKeys = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
// Load teams
|
||||
setIsLoadingTeams(true);
|
||||
try {
|
||||
const teamsResponse = await teamListCall(accessToken);
|
||||
if (teamsResponse?.data) {
|
||||
const teamAliases = teamsResponse.data
|
||||
.map((t: any) => t.team_alias)
|
||||
.filter(Boolean);
|
||||
setAvailableTeams(teamAliases);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load teams:", error);
|
||||
} finally {
|
||||
setIsLoadingTeams(false);
|
||||
}
|
||||
|
||||
// Load keys
|
||||
setIsLoadingKeys(true);
|
||||
try {
|
||||
const keysResponse = await keyInfoCall(accessToken, null, null);
|
||||
if (keysResponse?.data) {
|
||||
const keyAliases = keysResponse.data
|
||||
.map((k: any) => k.key_alias)
|
||||
.filter(Boolean);
|
||||
setAvailableKeys(keyAliases);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load keys:", error);
|
||||
} finally {
|
||||
setIsLoadingKeys(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.resetFields();
|
||||
setScopeType("global");
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error("No access token available");
|
||||
}
|
||||
|
||||
const data: PolicyAttachmentCreateRequest = {
|
||||
policy_name: values.policy_name,
|
||||
};
|
||||
|
||||
if (scopeType === "global") {
|
||||
data.scope = "*";
|
||||
} else {
|
||||
if (values.teams && values.teams.length > 0) {
|
||||
data.teams = values.teams;
|
||||
}
|
||||
if (values.keys && values.keys.length > 0) {
|
||||
data.keys = values.keys;
|
||||
}
|
||||
if (values.models && values.models.length > 0) {
|
||||
data.models = values.models;
|
||||
}
|
||||
}
|
||||
|
||||
await createPolicyAttachmentCall(accessToken, data);
|
||||
NotificationsManager.success("Attachment created successfully");
|
||||
|
||||
resetForm();
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to create attachment:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to create attachment: " + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const policyOptions = policies.map((p) => ({
|
||||
label: p.policy_name,
|
||||
value: p.policy_name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Create Policy Attachment"
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
scope_type: "global",
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="policy_name"
|
||||
label="Policy"
|
||||
rules={[{ required: true, message: "Please select a policy" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="Select a policy to attach"
|
||||
options={policyOptions}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Scope</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label="Scope Type">
|
||||
<Radio.Group
|
||||
value={scopeType}
|
||||
onChange={(e) => setScopeType(e.target.value)}
|
||||
>
|
||||
<Radio value="global">Global (applies to all requests)</Radio>
|
||||
<Radio value="specific">Specific (teams, keys, or models)</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{scopeType === "specific" && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="teams"
|
||||
label="Teams"
|
||||
tooltip="Select team aliases or enter custom patterns. Supports wildcards (e.g., healthcare-*)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={isLoadingTeams ? "Loading teams..." : "Select or enter team aliases"}
|
||||
loading={isLoadingTeams}
|
||||
options={availableTeams.map((team) => ({
|
||||
label: team,
|
||||
value: team,
|
||||
}))}
|
||||
tokenSeparators={[","]}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="keys"
|
||||
label="Keys"
|
||||
tooltip="Select key aliases or enter custom patterns. Supports wildcards (e.g., dev-*)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder={isLoadingKeys ? "Loading keys..." : "Select or enter key aliases"}
|
||||
loading={isLoadingKeys}
|
||||
options={availableKeys.map((key) => ({
|
||||
label: key,
|
||||
value: key,
|
||||
}))}
|
||||
tokenSeparators={[","]}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="models"
|
||||
label="Models"
|
||||
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*)"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
placeholder="Enter model names (e.g., gpt-4, bedrock/*)"
|
||||
tokenSeparators={[","]}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={isSubmitting}>
|
||||
Create Attachment
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddAttachmentForm;
|
||||
@@ -0,0 +1,395 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Select, Modal, Divider, Typography, Tag, Alert, Radio } from "antd";
|
||||
import { Button, TextInput, Textarea } from "@tremor/react";
|
||||
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
|
||||
import { Guardrail } from "../guardrails/types";
|
||||
import { createPolicyCall, updatePolicyCall, getResolvedGuardrails, modelAvailableCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
const { Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
interface AddPolicyFormProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
accessToken: string | null;
|
||||
editingPolicy?: Policy | null;
|
||||
existingPolicies: Policy[];
|
||||
availableGuardrails: Guardrail[];
|
||||
}
|
||||
|
||||
const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSuccess,
|
||||
accessToken,
|
||||
editingPolicy,
|
||||
existingPolicies,
|
||||
availableGuardrails,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
|
||||
const [isLoadingResolved, setIsLoadingResolved] = useState(false);
|
||||
const [modelConditionType, setModelConditionType] = useState<"model" | "regex">("model");
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
|
||||
const isEditing = !!editingPolicy;
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && editingPolicy) {
|
||||
const modelCondition = editingPolicy.condition?.model;
|
||||
// Detect if it's a regex pattern (contains *, ., [, ], etc.)
|
||||
const isRegex = modelCondition && /[.*+?^${}()|[\]\\]/.test(modelCondition);
|
||||
setModelConditionType(isRegex ? "regex" : "model");
|
||||
|
||||
form.setFieldsValue({
|
||||
policy_name: editingPolicy.policy_name,
|
||||
description: editingPolicy.description,
|
||||
inherit: editingPolicy.inherit,
|
||||
guardrails_add: editingPolicy.guardrails_add || [],
|
||||
guardrails_remove: editingPolicy.guardrails_remove || [],
|
||||
model_condition: modelCondition,
|
||||
});
|
||||
// Load resolved guardrails for editing
|
||||
if (editingPolicy.policy_id && accessToken) {
|
||||
loadResolvedGuardrails(editingPolicy.policy_id);
|
||||
}
|
||||
} else if (visible) {
|
||||
form.resetFields();
|
||||
setResolvedGuardrails([]);
|
||||
setModelConditionType("model");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, editingPolicy, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && accessToken) {
|
||||
loadAvailableModels();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, accessToken]);
|
||||
|
||||
const loadAvailableModels = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
const response = await modelAvailableCall(accessToken, null, null, null);
|
||||
if (response?.data) {
|
||||
const models = response.data.map((m: any) => m.id || m.model_name).filter(Boolean);
|
||||
setAvailableModels(models);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load available models:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadResolvedGuardrails = async (policyId: string) => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setIsLoadingResolved(true);
|
||||
try {
|
||||
const data = await getResolvedGuardrails(accessToken, policyId);
|
||||
setResolvedGuardrails(data.resolved_guardrails || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to load resolved guardrails:", error);
|
||||
} finally {
|
||||
setIsLoadingResolved(false);
|
||||
}
|
||||
};
|
||||
|
||||
const computeResolvedGuardrails = (): string[] => {
|
||||
const values = form.getFieldsValue(true);
|
||||
const inheritFrom = values.inherit;
|
||||
const guardrailsAdd = values.guardrails_add || [];
|
||||
const guardrailsRemove = values.guardrails_remove || [];
|
||||
|
||||
let resolved = new Set<string>();
|
||||
|
||||
// If inheriting, find parent policy and get its guardrails
|
||||
if (inheritFrom) {
|
||||
const parentPolicy = existingPolicies.find(p => p.policy_name === inheritFrom);
|
||||
if (parentPolicy) {
|
||||
// Recursively resolve parent's guardrails
|
||||
const parentResolved = resolveParentGuardrails(parentPolicy);
|
||||
parentResolved.forEach(g => resolved.add(g));
|
||||
}
|
||||
}
|
||||
|
||||
// Add guardrails
|
||||
guardrailsAdd.forEach((g: string) => resolved.add(g));
|
||||
|
||||
// Remove guardrails
|
||||
guardrailsRemove.forEach((g: string) => resolved.delete(g));
|
||||
|
||||
return Array.from(resolved).sort();
|
||||
};
|
||||
|
||||
const resolveParentGuardrails = (policy: Policy): string[] => {
|
||||
let resolved = new Set<string>();
|
||||
|
||||
// If parent inherits, resolve recursively
|
||||
if (policy.inherit) {
|
||||
const grandparent = existingPolicies.find(p => p.policy_name === policy.inherit);
|
||||
if (grandparent) {
|
||||
const grandparentResolved = resolveParentGuardrails(grandparent);
|
||||
grandparentResolved.forEach(g => resolved.add(g));
|
||||
}
|
||||
}
|
||||
|
||||
// Add parent's guardrails
|
||||
if (policy.guardrails_add) {
|
||||
policy.guardrails_add.forEach(g => resolved.add(g));
|
||||
}
|
||||
|
||||
// Remove parent's removed guardrails
|
||||
if (policy.guardrails_remove) {
|
||||
policy.guardrails_remove.forEach(g => resolved.delete(g));
|
||||
}
|
||||
|
||||
return Array.from(resolved);
|
||||
};
|
||||
|
||||
// Recompute resolved guardrails when form values change
|
||||
const handleFormChange = () => {
|
||||
const resolved = computeResolvedGuardrails();
|
||||
setResolvedGuardrails(resolved);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error("No access token available");
|
||||
}
|
||||
|
||||
const data: PolicyCreateRequest | PolicyUpdateRequest = {
|
||||
policy_name: values.policy_name,
|
||||
description: values.description || undefined,
|
||||
inherit: values.inherit || undefined,
|
||||
guardrails_add: values.guardrails_add || [],
|
||||
guardrails_remove: values.guardrails_remove || [],
|
||||
condition: values.model_condition
|
||||
? { model: values.model_condition }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (isEditing && editingPolicy) {
|
||||
await updatePolicyCall(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
|
||||
NotificationsManager.success("Policy updated successfully");
|
||||
} else {
|
||||
await createPolicyCall(accessToken, data as PolicyCreateRequest);
|
||||
NotificationsManager.success("Policy created successfully");
|
||||
}
|
||||
|
||||
resetForm();
|
||||
onSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to save policy:", error);
|
||||
NotificationsManager.fromBackend(
|
||||
"Failed to save policy: " + (error instanceof Error ? error.message : String(error))
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const guardrailOptions = availableGuardrails.map((g) => ({
|
||||
label: g.guardrail_name || g.guardrail_id,
|
||||
value: g.guardrail_name || g.guardrail_id,
|
||||
}));
|
||||
|
||||
const policyOptions = existingPolicies
|
||||
.filter((p) => !editingPolicy || p.policy_id !== editingPolicy.policy_id)
|
||||
.map((p) => ({
|
||||
label: p.policy_name,
|
||||
value: p.policy_name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEditing ? "Edit Policy" : "Create New Policy"}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
guardrails_add: [],
|
||||
guardrails_remove: [],
|
||||
}}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="policy_name"
|
||||
label="Policy Name"
|
||||
rules={[
|
||||
{ required: true, message: "Please enter a policy name" },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_-]+$/,
|
||||
message:
|
||||
"Policy name can only contain letters, numbers, hyphens, and underscores",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="e.g., global-baseline, healthcare-compliance"
|
||||
disabled={isEditing}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="Description">
|
||||
<Textarea
|
||||
rows={2}
|
||||
placeholder="Describe what this policy does..."
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Inheritance</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="inherit"
|
||||
label="Inherit From"
|
||||
tooltip="Inherit guardrails from another policy. The child policy will include all guardrails from the parent."
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="Select a parent policy (optional)"
|
||||
options={policyOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Guardrails</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="guardrails_add"
|
||||
label="Guardrails to Add"
|
||||
tooltip="These guardrails will be added to requests matching this policy"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Select guardrails to add"
|
||||
options={guardrailOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="guardrails_remove"
|
||||
label="Guardrails to Remove"
|
||||
tooltip="These guardrails will be removed from inherited guardrails"
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder="Select guardrails to remove (from inherited)"
|
||||
options={guardrailOptions}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{resolvedGuardrails.length > 0 && (
|
||||
<Alert
|
||||
message="Resolved Guardrails"
|
||||
description={
|
||||
<div>
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
|
||||
These are the final guardrails that will be applied (including inheritance):
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{resolvedGuardrails.map((g) => (
|
||||
<Tag key={g} color="blue">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Conditions (Optional)</Text>
|
||||
</Divider>
|
||||
|
||||
<Form.Item label="Model Condition Type">
|
||||
<Radio.Group
|
||||
value={modelConditionType}
|
||||
onChange={(e) => {
|
||||
setModelConditionType(e.target.value);
|
||||
form.setFieldValue("model_condition", undefined);
|
||||
}}
|
||||
>
|
||||
<Radio value="model">Select Model</Radio>
|
||||
<Radio value="regex">Custom Regex Pattern</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="model_condition"
|
||||
label={modelConditionType === "model" ? "Model" : "Regex Pattern"}
|
||||
tooltip={
|
||||
modelConditionType === "model"
|
||||
? "Select a specific model to apply this policy to"
|
||||
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*)"
|
||||
}
|
||||
>
|
||||
{modelConditionType === "model" ? (
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
placeholder="Select a model"
|
||||
options={availableModels.map((model) => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}))}
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<TextInput placeholder="e.g., gpt-4.* or bedrock/claude-.*" />
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-4">
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} loading={isSubmitting}>
|
||||
{isEditing ? "Update Policy" : "Create Policy"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPolicyForm;
|
||||
@@ -0,0 +1,281 @@
|
||||
import React, { useState } from "react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Badge } from "@tremor/react";
|
||||
import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { PolicyAttachment } from "./types";
|
||||
|
||||
interface AttachmentTableProps {
|
||||
attachments: PolicyAttachment[];
|
||||
isLoading: boolean;
|
||||
onDeleteClick: (attachmentId: string) => void;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const AttachmentTable: React.FC<AttachmentTableProps> = ({
|
||||
attachments,
|
||||
isLoading,
|
||||
onDeleteClick,
|
||||
isAdmin,
|
||||
}) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
|
||||
|
||||
// Format date helper function
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const columns: ColumnDef<PolicyAttachment>[] = [
|
||||
{
|
||||
header: "Attachment ID",
|
||||
accessorKey: "attachment_id",
|
||||
cell: (info: any) => (
|
||||
<Tooltip title={String(info.getValue() || "")}>
|
||||
<span className="font-mono text-xs text-gray-600">
|
||||
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
|
||||
</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Policy",
|
||||
accessorKey: "policy_name",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
return (
|
||||
<Badge color="blue" size="xs">
|
||||
{attachment.policy_name}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Scope",
|
||||
accessorKey: "scope",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
if (attachment.scope === "*") {
|
||||
return (
|
||||
<Badge color="amber" size="xs">
|
||||
Global (*)
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return attachment.scope ? (
|
||||
<span className="text-xs">{attachment.scope}</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Teams",
|
||||
accessorKey: "teams",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
const teams = attachment.teams || [];
|
||||
if (teams.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{teams.slice(0, 2).map((t, i) => (
|
||||
<Tag key={i} color="cyan" className="text-xs">
|
||||
{t}
|
||||
</Tag>
|
||||
))}
|
||||
{teams.length > 2 && (
|
||||
<Tooltip title={teams.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{teams.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Keys",
|
||||
accessorKey: "keys",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
const keys = attachment.keys || [];
|
||||
if (keys.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{keys.slice(0, 2).map((k, i) => (
|
||||
<Tag key={i} color="purple" className="text-xs">
|
||||
{k}
|
||||
</Tag>
|
||||
))}
|
||||
{keys.length > 2 && (
|
||||
<Tooltip title={keys.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{keys.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Models",
|
||||
accessorKey: "models",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
const models = attachment.models || [];
|
||||
if (models.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(0, 2).map((m, i) => (
|
||||
<Tag key={i} color="green" className="text-xs">
|
||||
{m}
|
||||
</Tag>
|
||||
))}
|
||||
{models.length > 2 && (
|
||||
<Tooltip title={models.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{models.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
return (
|
||||
<Tooltip title={attachment.created_at}>
|
||||
<span className="text-xs">{formatDate(attachment.created_at)}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const attachment = row.original;
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{isAdmin && (
|
||||
<Tooltip title="Delete attachment">
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => onDeleteClick(attachment.attachment_id)}
|
||||
className="cursor-pointer hover:text-red-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: attachments,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
className={`py-1 h-8 ${
|
||||
header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : ""
|
||||
}`}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== "actions" && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : attachments.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
|
||||
cell.column.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No attachments found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AttachmentTable;
|
||||
@@ -0,0 +1,270 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import { Modal, message } from "antd";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import PolicyTable from "./policy_table";
|
||||
import PolicyInfoView from "./policy_info";
|
||||
import AddPolicyForm from "./add_policy_form";
|
||||
import AttachmentTable from "./attachment_table";
|
||||
import AddAttachmentForm from "./add_attachment_form";
|
||||
import {
|
||||
getPoliciesList,
|
||||
deletePolicyCall,
|
||||
getPolicyAttachmentsList,
|
||||
deletePolicyAttachmentCall,
|
||||
getGuardrailsList,
|
||||
} from "../networking";
|
||||
import {
|
||||
Policy,
|
||||
PolicyAttachment,
|
||||
} from "./types";
|
||||
import { Guardrail } from "../guardrails/types";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
|
||||
interface PoliciesPanelProps {
|
||||
accessToken: string | null;
|
||||
userRole?: string;
|
||||
}
|
||||
|
||||
const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
}) => {
|
||||
const [policiesList, setPoliciesList] = useState<Policy[]>([]);
|
||||
const [attachmentsList, setAttachmentsList] = useState<PolicyAttachment[]>([]);
|
||||
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAttachmentsLoading, setIsAttachmentsLoading] = useState(false);
|
||||
const [isAddPolicyModalVisible, setIsAddPolicyModalVisible] = useState(false);
|
||||
const [isAddAttachmentModalVisible, setIsAddAttachmentModalVisible] = useState(false);
|
||||
const [editingPolicy, setEditingPolicy] = useState<Policy | null>(null);
|
||||
const [selectedPolicyId, setSelectedPolicyId] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [policyToDelete, setPolicyToDelete] = useState<Policy | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const isAdmin = userRole ? isAdminRole(userRole) : false;
|
||||
|
||||
const fetchPolicies = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await getPoliciesList(accessToken);
|
||||
setPoliciesList(response.policies || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching policies:", error);
|
||||
message.error("Failed to fetch policies");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
const fetchAttachments = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setIsAttachmentsLoading(true);
|
||||
try {
|
||||
const response = await getPolicyAttachmentsList(accessToken);
|
||||
setAttachmentsList(response.attachments || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching attachments:", error);
|
||||
message.error("Failed to fetch attachments");
|
||||
} finally {
|
||||
setIsAttachmentsLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
const fetchGuardrails = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
try {
|
||||
const response = await getGuardrailsList(accessToken);
|
||||
setGuardrailsList(response.guardrails || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching guardrails:", error);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicies();
|
||||
fetchAttachments();
|
||||
fetchGuardrails();
|
||||
}, [fetchPolicies, fetchAttachments, fetchGuardrails]);
|
||||
|
||||
const handleAddPolicy = () => {
|
||||
if (selectedPolicyId) {
|
||||
setSelectedPolicyId(null);
|
||||
}
|
||||
setEditingPolicy(null);
|
||||
setIsAddPolicyModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsAddPolicyModalVisible(false);
|
||||
setEditingPolicy(null);
|
||||
};
|
||||
|
||||
const handleSuccess = () => {
|
||||
fetchPolicies();
|
||||
setEditingPolicy(null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (policyId: string, policyName: string) => {
|
||||
const policy = policiesList.find((p) => p.policy_id === policyId) || null;
|
||||
setPolicyToDelete(policy);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (!policyToDelete || !accessToken) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deletePolicyCall(accessToken, policyToDelete.policy_id);
|
||||
message.success(`Policy "${policyToDelete.policy_name}" deleted successfully`);
|
||||
await fetchPolicies();
|
||||
} catch (error) {
|
||||
console.error("Error deleting policy:", error);
|
||||
message.error("Failed to delete policy");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setIsDeleteModalOpen(false);
|
||||
setPolicyToDelete(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCancel = () => {
|
||||
setIsDeleteModalOpen(false);
|
||||
setPolicyToDelete(null);
|
||||
};
|
||||
|
||||
const handleDeleteAttachment = (attachmentId: string) => {
|
||||
Modal.confirm({
|
||||
title: "Delete Attachment",
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: "Are you sure you want to delete this attachment? This action cannot be undone.",
|
||||
okText: "Delete",
|
||||
okType: "danger",
|
||||
cancelText: "Cancel",
|
||||
onOk: async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
await deletePolicyAttachmentCall(accessToken, attachmentId);
|
||||
message.success("Attachment deleted successfully");
|
||||
fetchAttachments();
|
||||
} catch (error) {
|
||||
console.error("Error deleting attachment:", error);
|
||||
message.error("Failed to delete attachment");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleAttachmentSuccess = () => {
|
||||
fetchAttachments();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||
<TabGroup index={activeTab} onIndexChange={setActiveTab}>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Policies</Tab>
|
||||
<Tab>Attachments</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Button onClick={handleAddPolicy} disabled={!accessToken}>
|
||||
+ Add New Policy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedPolicyId ? (
|
||||
<PolicyInfoView
|
||||
policyId={selectedPolicyId}
|
||||
onClose={() => setSelectedPolicyId(null)}
|
||||
onEdit={(policy) => {
|
||||
setEditingPolicy(policy);
|
||||
setIsAddPolicyModalVisible(true);
|
||||
setSelectedPolicyId(null);
|
||||
}}
|
||||
accessToken={accessToken}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
) : (
|
||||
<PolicyTable
|
||||
policies={policiesList}
|
||||
isLoading={isLoading}
|
||||
onDeleteClick={handleDeleteClick}
|
||||
onEditClick={(policy) => {
|
||||
setEditingPolicy(policy);
|
||||
setIsAddPolicyModalVisible(true);
|
||||
}}
|
||||
onViewClick={(policyId) => setSelectedPolicyId(policyId)}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddPolicyForm
|
||||
visible={isAddPolicyModalVisible}
|
||||
onClose={handleCloseModal}
|
||||
onSuccess={handleSuccess}
|
||||
accessToken={accessToken}
|
||||
editingPolicy={editingPolicy}
|
||||
existingPolicies={policiesList}
|
||||
availableGuardrails={guardrailsList}
|
||||
/>
|
||||
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Policy"
|
||||
message={`Are you sure you want to delete policy: ${policyToDelete?.policy_name}? This action cannot be undone.`}
|
||||
resourceInformationTitle="Policy Information"
|
||||
resourceInformation={[
|
||||
{ label: "Name", value: policyToDelete?.policy_name },
|
||||
{ label: "ID", value: policyToDelete?.policy_id, code: true },
|
||||
{ label: "Description", value: policyToDelete?.description || "-" },
|
||||
{ label: "Inherits From", value: policyToDelete?.inherit || "-" },
|
||||
]}
|
||||
onCancel={handleDeleteCancel}
|
||||
onOk={handleDeleteConfirm}
|
||||
confirmLoading={isDeleting}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Button
|
||||
onClick={() => setIsAddAttachmentModalVisible(true)}
|
||||
disabled={!accessToken || policiesList.length === 0}
|
||||
>
|
||||
+ Add New Attachment
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AttachmentTable
|
||||
attachments={attachmentsList}
|
||||
isLoading={isAttachmentsLoading}
|
||||
onDeleteClick={handleDeleteAttachment}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
|
||||
<AddAttachmentForm
|
||||
visible={isAddAttachmentModalVisible}
|
||||
onClose={() => setIsAddAttachmentModalVisible(false)}
|
||||
onSuccess={handleAttachmentSuccess}
|
||||
accessToken={accessToken}
|
||||
policies={policiesList}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PoliciesPanel;
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Card, Badge, Button } from "@tremor/react";
|
||||
import { ArrowLeftIcon, PencilIcon } from "@heroicons/react/outline";
|
||||
import { Descriptions, Tag, Spin, Divider, Typography, Alert } from "antd";
|
||||
import { Policy } from "./types";
|
||||
import { getPolicyInfo, getResolvedGuardrails } from "../networking";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface PolicyInfoViewProps {
|
||||
policyId: string;
|
||||
onClose: () => void;
|
||||
onEdit: (policy: Policy) => void;
|
||||
accessToken: string | null;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
|
||||
policyId,
|
||||
onClose,
|
||||
onEdit,
|
||||
accessToken,
|
||||
isAdmin,
|
||||
}) => {
|
||||
const [policy, setPolicy] = useState<Policy | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
|
||||
const [isLoadingResolved, setIsLoadingResolved] = useState(false);
|
||||
|
||||
const fetchPolicy = useCallback(async () => {
|
||||
if (!accessToken || !policyId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await getPolicyInfo(accessToken, policyId);
|
||||
setPolicy(data);
|
||||
|
||||
// Also fetch resolved guardrails
|
||||
setIsLoadingResolved(true);
|
||||
try {
|
||||
const resolvedData = await getResolvedGuardrails(accessToken, policyId);
|
||||
setResolvedGuardrails(resolvedData.resolved_guardrails || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching resolved guardrails:", error);
|
||||
} finally {
|
||||
setIsLoadingResolved(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching policy:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [policyId, accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicy();
|
||||
}, [fetchPolicy]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center p-12">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!policy) {
|
||||
return (
|
||||
<Card>
|
||||
<Text type="danger">Policy not found</Text>
|
||||
<br />
|
||||
<Button onClick={onClose} className="mt-4">
|
||||
Go Back
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={ArrowLeftIcon}
|
||||
onClick={onClose}
|
||||
>
|
||||
Back to Policies
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
icon={PencilIcon}
|
||||
onClick={() => onEdit(policy)}
|
||||
>
|
||||
Edit Policy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Title level={4}>{policy.policy_name}</Title>
|
||||
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Policy ID">
|
||||
<code className="text-xs bg-gray-100 px-2 py-1 rounded">{policy.policy_id}</code>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Description">
|
||||
{policy.description || <Text type="secondary">No description</Text>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Inherits From">
|
||||
{policy.inherit ? (
|
||||
<Badge color="blue" size="sm">{policy.inherit}</Badge>
|
||||
) : (
|
||||
<Text type="secondary">None</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Created At">
|
||||
{policy.created_at
|
||||
? new Date(policy.created_at).toLocaleString()
|
||||
: "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Updated At">
|
||||
{policy.updated_at
|
||||
? new Date(policy.updated_at).toLocaleString()
|
||||
: "-"}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Guardrails Configuration</Text>
|
||||
</Divider>
|
||||
|
||||
{resolvedGuardrails.length > 0 && (
|
||||
<Alert
|
||||
message="Resolved Guardrails"
|
||||
description={
|
||||
<div>
|
||||
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
|
||||
Final guardrails that will be applied (including inheritance):
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{resolvedGuardrails.map((g) => (
|
||||
<Tag key={g} color="blue">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Guardrails to Add">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policy.guardrails_add && policy.guardrails_add.length > 0 ? (
|
||||
policy.guardrails_add.map((g) => (
|
||||
<Tag key={g} color="green">
|
||||
{g}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary">None</Text>
|
||||
)}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Guardrails to Remove">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policy.guardrails_remove && policy.guardrails_remove.length > 0 ? (
|
||||
policy.guardrails_remove.map((g) => (
|
||||
<Tag key={g} color="red">
|
||||
{g}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<Text type="secondary">None</Text>
|
||||
)}
|
||||
</div>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider orientation="left">
|
||||
<Text strong>Conditions</Text>
|
||||
</Divider>
|
||||
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label="Model Condition">
|
||||
{policy.condition?.model ? (
|
||||
<Tag color="purple">
|
||||
{typeof policy.condition.model === "string"
|
||||
? policy.condition.model
|
||||
: JSON.stringify(policy.condition.model)}
|
||||
</Tag>
|
||||
) : (
|
||||
<Text type="secondary">No model condition (applies to all models)</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicyInfoView;
|
||||
@@ -0,0 +1,309 @@
|
||||
import React, { useState } from "react";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button, Badge } from "@tremor/react";
|
||||
import { TrashIcon, PencilIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { Policy } from "./types";
|
||||
|
||||
interface PolicyTableProps {
|
||||
policies: Policy[];
|
||||
isLoading: boolean;
|
||||
onDeleteClick: (policyId: string, policyName: string) => void;
|
||||
onEditClick: (policy: Policy) => void;
|
||||
onViewClick: (policyId: string) => void;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
const PolicyTable: React.FC<PolicyTableProps> = ({
|
||||
policies,
|
||||
isLoading,
|
||||
onDeleteClick,
|
||||
onEditClick,
|
||||
onViewClick,
|
||||
isAdmin = false,
|
||||
}) => {
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
|
||||
|
||||
// Format date helper function
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return "-";
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
const columns: ColumnDef<Policy>[] = [
|
||||
{
|
||||
header: "Policy ID",
|
||||
accessorKey: "policy_id",
|
||||
cell: (info: any) => (
|
||||
<Tooltip title={String(info.getValue() || "")}>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||
onClick={() => info.getValue() && onViewClick(info.getValue())}
|
||||
>
|
||||
{info.getValue() ? `${String(info.getValue()).slice(0, 7)}...` : ""}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "policy_name",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<Tooltip title={policy.policy_name}>
|
||||
<span className="text-xs font-medium">{policy.policy_name || "-"}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<Tooltip title={policy.description}>
|
||||
<span className="text-xs truncate max-w-[200px] block">
|
||||
{policy.description || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Inherits From",
|
||||
accessorKey: "inherit",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return policy.inherit ? (
|
||||
<Badge color="blue" size="xs">
|
||||
{policy.inherit}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">-</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Guardrails (Add)",
|
||||
accessorKey: "guardrails_add",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
const guardrails = policy.guardrails_add || [];
|
||||
if (guardrails.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{guardrails.slice(0, 2).map((g, i) => (
|
||||
<Tag key={i} color="green" className="text-xs">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
{guardrails.length > 2 && (
|
||||
<Tooltip title={guardrails.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{guardrails.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Guardrails (Remove)",
|
||||
accessorKey: "guardrails_remove",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
const guardrails = policy.guardrails_remove || [];
|
||||
if (guardrails.length === 0) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{guardrails.slice(0, 2).map((g, i) => (
|
||||
<Tag key={i} color="red" className="text-xs">
|
||||
{g}
|
||||
</Tag>
|
||||
))}
|
||||
{guardrails.length > 2 && (
|
||||
<Tooltip title={guardrails.slice(2).join(", ")}>
|
||||
<Tag className="text-xs">+{guardrails.length - 2}</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Model Condition",
|
||||
accessorKey: "condition",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
const modelCondition = policy.condition?.model;
|
||||
if (!modelCondition) {
|
||||
return <span className="text-xs text-gray-400">-</span>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={typeof modelCondition === "string" ? modelCondition : JSON.stringify(modelCondition)}>
|
||||
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
|
||||
{typeof modelCondition === "string"
|
||||
? modelCondition.length > 20
|
||||
? modelCondition.slice(0, 20) + "..."
|
||||
: modelCondition
|
||||
: "Multiple"}
|
||||
</code>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<Tooltip title={policy.created_at}>
|
||||
<span className="text-xs">{formatDate(policy.created_at)}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Tooltip title="Edit policy">
|
||||
<Icon
|
||||
icon={PencilIcon}
|
||||
size="sm"
|
||||
onClick={() => onEditClick(policy)}
|
||||
className="cursor-pointer hover:text-blue-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete policy">
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
policy.policy_id &&
|
||||
onDeleteClick(policy.policy_id, policy.policy_name || "Unnamed Policy")
|
||||
}
|
||||
className="cursor-pointer hover:text-red-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: policies,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-lg custom-border relative">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
className={`py-1 h-8 ${
|
||||
header.id === "actions" ? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]" : ""
|
||||
}`}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</div>
|
||||
{header.id !== "actions" && (
|
||||
<div className="w-4">
|
||||
{header.column.getIsSorted() ? (
|
||||
{
|
||||
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
|
||||
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />,
|
||||
}[header.column.getIsSorted() as string]
|
||||
) : (
|
||||
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : policies.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
|
||||
cell.column.id === "actions"
|
||||
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No policies found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicyTable;
|
||||
@@ -0,0 +1,66 @@
|
||||
export interface Policy {
|
||||
policy_id: string;
|
||||
policy_name: string;
|
||||
inherit: string | null;
|
||||
description: string | null;
|
||||
guardrails_add: string[];
|
||||
guardrails_remove: string[];
|
||||
condition: PolicyCondition | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface PolicyCondition {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface PolicyAttachment {
|
||||
attachment_id: string;
|
||||
policy_name: string;
|
||||
scope: string | null;
|
||||
teams: string[];
|
||||
keys: string[];
|
||||
models: string[];
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
created_by?: string;
|
||||
updated_by?: string;
|
||||
}
|
||||
|
||||
export interface PolicyCreateRequest {
|
||||
policy_name: string;
|
||||
inherit?: string;
|
||||
description?: string;
|
||||
guardrails_add?: string[];
|
||||
guardrails_remove?: string[];
|
||||
condition?: PolicyCondition;
|
||||
}
|
||||
|
||||
export interface PolicyUpdateRequest {
|
||||
policy_name?: string;
|
||||
inherit?: string;
|
||||
description?: string;
|
||||
guardrails_add?: string[];
|
||||
guardrails_remove?: string[];
|
||||
condition?: PolicyCondition;
|
||||
}
|
||||
|
||||
export interface PolicyAttachmentCreateRequest {
|
||||
policy_name: string;
|
||||
scope?: string;
|
||||
teams?: string[];
|
||||
keys?: string[];
|
||||
models?: string[];
|
||||
}
|
||||
|
||||
export interface PolicyListResponse {
|
||||
policies: Policy[];
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
export interface PolicyAttachmentListResponse {
|
||||
attachments: PolicyAttachment[];
|
||||
total_count: number;
|
||||
}
|
||||
Reference in New Issue
Block a user