[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:
Ishaan Jaff
2026-01-23 12:44:22 -08:00
committed by GitHub
parent 28bc83e394
commit fc19085230
32 changed files with 3766 additions and 27 deletions
+1
View File
@@ -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])
+6
View File
@@ -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",
+123
View File
@@ -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))
+369 -1
View File
@@ -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
+35
View File
@@ -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)
+1 -18
View File
@@ -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;
+3
View File
@@ -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;
}