[Feat] UI + Backend - Allow adding policies on Keys/Teams + Viewing on Info panels (#19688)

* ui for policy mgmt

* test_add_guardrails_from_policy_engine_accepts_dynamic_policies_and_pops_from_data
This commit is contained in:
Ishaan Jaff
2026-01-23 19:03:44 -08:00
committed by GitHub
parent 4ed5aa5de0
commit a870722f65
12 changed files with 539 additions and 23 deletions
+19 -6
View File
@@ -1500,10 +1500,13 @@ def add_guardrails_from_policy_engine(
Add guardrails from the policy engine based on request context.
This function:
1. Gets matching policies based on team_alias, key_alias, and model
2. Resolves guardrails from matching policies (including inheritance)
3. Adds guardrails to request metadata
4. Tracks applied policies in metadata for response headers
1. Extracts "policies" from request body (if present) for dynamic policy application
2. Gets matching policies based on team_alias, key_alias, and model (via attachments)
3. Combines dynamic policies with attachment-based policies
4. Resolves guardrails from all policies (including inheritance)
5. Adds guardrails to request metadata
6. Tracks applied policies in metadata for response headers
7. Removes "policies" from request body so it's not forwarded to LLM provider
Args:
data: The request data to update
@@ -1519,6 +1522,10 @@ def add_guardrails_from_policy_engine(
from litellm.proxy.policy_engine.policy_resolver import PolicyResolver
from litellm.types.proxy.policy_engine import PolicyMatchContext
# Extract dynamic policies from request body (if present)
# These will be combined with attachment-based policies
request_body_policies = data.pop("policies", None)
registry = get_policy_registry()
verbose_proxy_logger.debug(
f"Policy engine: registry initialized={registry.is_initialized()}, "
@@ -1545,12 +1552,18 @@ def add_guardrails_from_policy_engine(
verbose_proxy_logger.debug(f"Policy engine: matched policies via attachments: {matching_policy_names}")
if not matching_policy_names:
# Combine attachment-based policies with dynamic request body policies
all_policy_names = set(matching_policy_names)
if request_body_policies and isinstance(request_body_policies, list):
all_policy_names.update(request_body_policies)
verbose_proxy_logger.debug(f"Policy engine: added dynamic policies from request body: {request_body_policies}")
if not all_policy_names:
return
# Filter to only policies whose conditions match the context
applied_policy_names = PolicyMatcher.get_policies_with_matching_conditions(
policy_names=matching_policy_names,
policy_names=list(all_policy_names),
context=context,
)
@@ -1548,3 +1548,50 @@ def test_add_guardrails_from_policy_engine():
policy_registry._initialized = False
attachment_registry._attachments = []
attachment_registry._initialized = False
def test_add_guardrails_from_policy_engine_accepts_dynamic_policies_and_pops_from_data():
"""
Test that add_guardrails_from_policy_engine accepts dynamic 'policies' from the request body
and removes them to prevent forwarding to the LLM provider.
This is critical because 'policies' is a LiteLLM proxy-specific parameter that should
not be sent to the actual LLM API (e.g., OpenAI, Anthropic, etc.).
"""
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
# Setup test data with 'policies' in the request body
data = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
"policies": ["PII-POLICY-GLOBAL", "HIPAA-POLICY"], # Dynamic policies - should be accepted and removed
"metadata": {},
}
user_api_key_dict = UserAPIKeyAuth(
api_key="test-key",
team_alias="test-team",
key_alias="test-key",
)
# Initialize empty policy registry (we're just testing the accept and pop behavior)
policy_registry = get_policy_registry()
policy_registry._policies = {}
policy_registry._initialized = False
# Call the function - should accept dynamic policies and not raise an error
add_guardrails_from_policy_engine(
data=data,
metadata_variable_name="metadata",
user_api_key_dict=user_api_key_dict,
)
# Verify that 'policies' was removed from the request body
assert "policies" not in data, "'policies' should be removed from request body to prevent forwarding to LLM provider"
# Verify that other fields are preserved
assert "model" in data
assert data["model"] == "gpt-4"
assert "messages" in data
assert data["messages"] == [{"role": "user", "content": "Hello"}]
assert "metadata" in data
@@ -14,7 +14,7 @@ import PremiumLoggingSettings from "@/components/common_components/PremiumLoggin
import ModelAliasManager from "@/components/common_components/ModelAliasManager";
import React, { useEffect, useState } from "react";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { fetchMCPAccessGroups, getGuardrailsList, Organization, Team, teamCreateCall } from "@/components/networking";
import { fetchMCPAccessGroups, getGuardrailsList, getPoliciesList, Organization, Team, teamCreateCall } from "@/components/networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions";
@@ -76,6 +76,7 @@ const CreateTeamModal = ({
const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState<Organization | null>(null);
const [modelsToPick, setModelsToPick] = useState<string[]>([]);
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [policiesList, setPoliciesList] = useState<string[]>([]);
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
@@ -136,7 +137,22 @@ const CreateTeamModal = ({
}
};
const fetchPolicies = async () => {
try {
if (accessToken == null) {
return;
}
const response = await getPoliciesList(accessToken);
const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
setPoliciesList(policyNames);
} catch (error) {
console.error("Failed to fetch policies:", error);
}
};
fetchGuardrails();
fetchPolicies();
}, [accessToken]);
const handleCreate = async (formValues: Record<string, any>) => {
@@ -531,6 +547,36 @@ const CreateTeamModal = ({
unCheckedChildren="No"
/>
</Form.Item>
<Form.Item
label={
<span>
Policies{" "}
<Tooltip title="Apply policies to this team to control guardrails and other settings">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="policies"
className="mt-8"
help="Select existing policies or enter new ones"
>
<Select2
mode="tags"
style={{ width: "100%" }}
placeholder="Select or enter policies"
options={policiesList.map((name) => ({
value: name,
label: name,
}))}
/>
</Form.Item>
<Form.Item
label={
<span>
@@ -107,7 +107,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
{
key: "agents",
page: "agents",
label: <span className="flex items-center gap-4">Agents</span>,
label: "Agents",
icon: <RobotOutlined />,
roles: rolesWithWriteAccess,
},
@@ -127,7 +127,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
{
key: "policies",
page: "policies",
label: "Policies",
label: (
<span className="flex items-center gap-4">
Policies <NewBadge />
</span>
),
icon: <AuditOutlined />,
roles: all_admin_roles,
},
@@ -5377,6 +5377,32 @@ export const getPoliciesList = async (accessToken: string) => {
}
};
export const getPolicyInfoWithGuardrails = async (accessToken: string, policyName: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policy/info/${policyName}` : `/policy/info/${policyName}`;
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 for ${policyName}:`, error);
throw error;
}
};
export const createPolicyCall = async (accessToken: string, policyData: any) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies` : `/policies`;
@@ -29,6 +29,7 @@ import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import NotificationsManager from "../molecules/notifications_manager";
import {
getGuardrailsList,
getPoliciesList,
getPossibleUserRoles,
getPromptsList,
keyCreateCall,
@@ -150,6 +151,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
const [keyOwner, setKeyOwner] = useState("you");
const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data));
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [policiesList, setPoliciesList] = useState<string[]>([]);
const [promptsList, setPromptsList] = useState<string[]>([]);
const [loggingSettings, setLoggingSettings] = useState<any[]>([]);
const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState<Team | null>(team);
@@ -211,6 +213,16 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
}
};
const fetchPolicies = async () => {
try {
const response = await getPoliciesList(accessToken);
const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
setPoliciesList(policyNames);
} catch (error) {
console.error("Failed to fetch policies:", error);
}
};
const fetchPrompts = async () => {
try {
const response = await getPromptsList(accessToken);
@@ -221,6 +233,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
};
fetchGuardrails();
fetchPolicies();
fetchPrompts();
}, [accessToken]);
@@ -915,6 +928,42 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
>
<Switch disabled={!premiumUser} checkedChildren="Yes" unCheckedChildren="No" />
</Form.Item>
<Form.Item
label={
<span>
Policies{" "}
<Tooltip title="Apply policies to this key to control guardrails and other settings">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="policies"
className="mt-4"
help={
premiumUser
? "Select existing policies or enter new ones"
: "Premium feature - Upgrade to set policies by key"
}
>
<Select
mode="tags"
style={{ width: "100%" }}
disabled={!premiumUser}
placeholder={
!premiumUser
? "Premium feature - Upgrade to set policies by key"
: "Select or enter policies"
}
options={policiesList.map((name) => ({ value: name, label: name }))}
/>
</Form.Item>
<Form.Item
label={
<span>
@@ -2,7 +2,7 @@ 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 { teamListCall, keyInfoCall } from "../networking";
import { teamListCall, keyInfoCall, modelAvailableCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
@@ -30,24 +30,27 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
const [availableTeams, setAvailableTeams] = useState<string[]>([]);
const [availableKeys, setAvailableKeys] = useState<string[]>([]);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [isLoadingTeams, setIsLoadingTeams] = useState(false);
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
const [isLoadingModels, setIsLoadingModels] = useState(false);
const { userId, userRole } = useAuthorized();
useEffect(() => {
if (visible && accessToken) {
loadTeamsAndKeys();
loadTeamsKeysAndModels();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, accessToken]);
const loadTeamsAndKeys = async () => {
const loadTeamsKeysAndModels = async () => {
if (!accessToken) return;
// Load teams
setIsLoadingTeams(true);
try {
const teamsResponse = await teamListCall(accessToken, userId, userRole);
// Pass null for organizationID since we're loading all teams the user has access to
const teamsResponse = await teamListCall(accessToken, null, userId);
if (teamsResponse?.data) {
const teamAliases = teamsResponse.data
.map((t: any) => t.team_alias)
@@ -75,6 +78,22 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
} finally {
setIsLoadingKeys(false);
}
// Load models
setIsLoadingModels(true);
try {
const modelsResponse = await modelAvailableCall(accessToken, userId || "", userRole || "");
if (modelsResponse?.data) {
const modelIds = modelsResponse.data
.map((m: any) => m.id || m.model_name)
.filter(Boolean);
setAvailableModels(modelIds);
}
} catch (error) {
console.error("Failed to load models:", error);
} finally {
setIsLoadingModels(false);
}
};
const resetForm = () => {
@@ -230,12 +249,21 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<Form.Item
name="models"
label="Models"
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*)"
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*). Leave empty to apply to all models."
>
<Select
mode="tags"
placeholder="Enter model names (e.g., gpt-4, bedrock/*)"
placeholder={isLoadingModels ? "Loading models..." : "Select or enter model names (e.g., gpt-4, bedrock/*)"}
loading={isLoadingModels}
options={availableModels.map((model) => ({
label: model,
value: model,
}))}
tokenSeparators={[","]}
showSearch
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
style={{ width: "100%" }}
/>
</Form.Item>
@@ -344,6 +344,14 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
<Text strong>Conditions (Optional)</Text>
</Divider>
<Alert
message="Model Scope"
description="By default, this policy will run on all models. You can optionally restrict it to specific models below."
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Form.Item label="Model Condition Type">
<Radio.Group
value={modelConditionType}
@@ -359,18 +367,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
<Form.Item
name="model_condition"
label={modelConditionType === "model" ? "Model" : "Regex Pattern"}
label={modelConditionType === "model" ? "Model (Optional)" : "Regex Pattern (Optional)"}
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/.*)"
? "Select a specific model to apply this policy to. Leave empty to apply to all models."
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*). Leave empty to apply to all models."
}
>
{modelConditionType === "model" ? (
<Select
showSearch
allowClear
placeholder="Select a model"
placeholder="Leave empty to apply to all models"
options={availableModels.map((model) => ({
label: model,
value: model,
@@ -381,7 +389,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
style={{ width: "100%" }}
/>
) : (
<TextInput placeholder="e.g., gpt-4.* or bedrock/claude-.*" />
<TextInput placeholder="Leave empty to apply to all models (e.g., gpt-4.* or bedrock/claude-.*)" />
)}
</Form.Item>
@@ -1,7 +1,7 @@
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 { Modal, message, Alert } from "antd";
import { ExclamationCircleOutlined, InfoCircleOutlined } from "@ant-design/icons";
import { isAdminRole } from "@/utils/roles";
import PolicyTable from "./policy_table";
import PolicyInfoView from "./policy_info";
@@ -181,6 +181,36 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
<TabPanels>
<TabPanel>
<Alert
message="About Policies"
description={
<div>
<p className="mb-3">
Use policies to group guardrails and control which ones run for specific teams, keys, or models.
</p>
<p className="mb-2 font-semibold">Why use policies?</p>
<ul className="list-disc list-inside mb-3 space-y-1 ml-2">
<li>Enable/disable specific guardrails for teams, keys, or models</li>
<li>Group guardrails into a single policy</li>
<li>Inherit from existing policies and override what you need</li>
</ul>
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline inline-block mt-1"
>
Learn more in the documentation
</a>
</div>
}
type="info"
icon={<InfoCircleOutlined />}
showIcon
closable
className="mb-6"
/>
<div className="flex justify-between items-center mb-4">
<Button onClick={handleAddPolicy} disabled={!accessToken}>
+ Add New Policy
@@ -244,6 +274,37 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
</TabPanel>
<TabPanel>
<Alert
message="About Policy Attachments"
description={
<div>
<p className="mb-3">
Policy attachments control where your policies apply. Policies don't do anything until you attach them to specific teams, keys, models, or globally.
</p>
<p className="mb-2 font-semibold">Attachment Scopes:</p>
<ul className="list-disc list-inside mb-3 space-y-1 ml-2">
<li><strong>Global (*)</strong> - Applies to all requests</li>
<li><strong>Teams</strong> - Applies only to specific teams</li>
<li><strong>Keys</strong> - Applies only to specific API keys (supports wildcards like dev-*)</li>
<li><strong>Models</strong> - Applies only when specific models are used</li>
</ul>
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies#attachments"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline inline-block mt-1"
>
Learn more about attachments
</a>
</div>
}
type="info"
icon={<InfoCircleOutlined />}
showIcon
closable
className="mb-6"
/>
<div className="flex justify-between items-center mb-4">
<Button
onClick={() => setIsAddAttachmentModalVisible(true)}
@@ -2,6 +2,8 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import UserSearchModal from "@/components/common_components/user_search_modal";
import {
getGuardrailsList,
getPoliciesList,
getPolicyInfoWithGuardrails,
Member,
Organization,
organizationInfoCall,
@@ -171,6 +173,9 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [policiesList, setPoliciesList] = useState<string[]>([]);
const [policyGuardrails, setPolicyGuardrails] = useState<Record<string, string[]>>({});
const [loadingPolicies, setLoadingPolicies] = useState(false);
const [memberToDelete, setMemberToDelete] = useState<Member | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -247,9 +252,54 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
}
};
const fetchPolicies = async () => {
try {
if (!accessToken) return;
const response = await getPoliciesList(accessToken);
const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
setPoliciesList(policyNames);
} catch (error) {
console.error("Failed to fetch policies:", error);
}
};
fetchGuardrails();
fetchPolicies();
}, [accessToken]);
// Fetch resolved guardrails for all policies
useEffect(() => {
const fetchPolicyGuardrails = async () => {
if (!accessToken || !info?.policies || info.policies.length === 0) {
return;
}
setLoadingPolicies(true);
const guardrailsMap: Record<string, string[]> = {};
try {
await Promise.all(
info.policies.map(async (policyName: string) => {
try {
const policyInfo = await getPolicyInfoWithGuardrails(accessToken, policyName);
guardrailsMap[policyName] = policyInfo.resolved_guardrails || [];
} catch (error) {
console.error(`Failed to fetch guardrails for policy ${policyName}:`, error);
guardrailsMap[policyName] = [];
}
})
);
setPolicyGuardrails(guardrailsMap);
} catch (error) {
console.error("Failed to fetch policy guardrails:", error);
} finally {
setLoadingPolicies(false);
}
};
fetchPolicyGuardrails();
}, [accessToken, info?.policies]);
const handleMemberCreate = async (values: any) => {
try {
if (accessToken == null) return;
@@ -603,6 +653,56 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
accessToken={accessToken}
/>
<Card>
<Text className="font-semibold text-gray-900 mb-3">Guardrails</Text>
{info.guardrails && info.guardrails.length > 0 ? (
<div className="flex flex-wrap gap-2">
{info.guardrails.map((guardrail: string, index: number) => (
<Badge key={index} color="blue">
{guardrail}
</Badge>
))}
</div>
) : (
<Text className="text-gray-500">No guardrails configured</Text>
)}
{info.metadata?.disable_global_guardrails && (
<div className="mt-3 pt-3 border-t border-gray-200">
<Badge color="yellow">Global Guardrails Disabled</Badge>
</div>
)}
</Card>
<Card>
<Text className="font-semibold text-gray-900 mb-3">Policies</Text>
{info.policies && info.policies.length > 0 ? (
<div className="space-y-4">
{info.policies.map((policy: string, index: number) => (
<div key={index} className="space-y-2">
<div className="flex items-center gap-2">
<Badge color="purple">{policy}</Badge>
{loadingPolicies && <Text className="text-xs text-gray-400">Loading guardrails...</Text>}
</div>
{!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
<div className="ml-4 pl-3 border-l-2 border-gray-200">
<Text className="text-xs text-gray-500 mb-1">Resolved Guardrails:</Text>
<div className="flex flex-wrap gap-1">
{policyGuardrails[policy].map((guardrail: string, gIndex: number) => (
<Badge key={gIndex} color="blue" size="xs">
{guardrail}
</Badge>
))}
</div>
</div>
)}
</div>
))}
</div>
) : (
<Text className="text-gray-500">No policies configured</Text>
)}
</Card>
<LoggingSettingsView
loggingConfigs={info.metadata?.logging || []}
disabledCallbacks={[]}
@@ -814,6 +914,32 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Switch checkedChildren="Yes" unCheckedChildren="No" />
</Form.Item>
<Form.Item
label={
<span>
Policies{" "}
<Tooltip title="Apply policies to this team to control guardrails and other settings">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/guardrail_policies"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="policies"
help="Select existing policies or enter new ones"
>
<Select
mode="tags"
placeholder="Select or enter policies"
options={policiesList.map((name) => ({ value: name, label: name }))}
/>
</Form.Item>
<Form.Item label="Vector Stores" name="vector_stores" aria-label="Vector Stores">
<VectorStoreSelector
onChange={(values: string[]) => form.setFieldValue("vector_stores", values)}
@@ -1,4 +1,5 @@
import GuardrailSelector from "@/components/guardrails/GuardrailSelector";
import PolicySelector from "@/components/policies/PolicySelector";
import { InfoCircleOutlined } from "@ant-design/icons";
import { TextInput, Button as TremorButton } from "@tremor/react";
import { Form, Input, Select, Switch, Tooltip } from "antd";
@@ -406,6 +407,28 @@ export function KeyEditView({
<Switch disabled={!premiumUser} checkedChildren="Yes" unCheckedChildren="No" />
</Form.Item>
<Form.Item
label={
<span>
Policies{" "}
<Tooltip title="Apply policies to this key to control guardrails and other settings">
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="policies"
>
{accessToken && (
<PolicySelector
onChange={(v) => {
form.setFieldValue("policies", v);
}}
accessToken={accessToken}
disabled={!premiumUser}
/>
)}
</Form.Item>
<Form.Item label="Tags" name="tags">
<Select
mode="tags"
@@ -14,7 +14,7 @@ import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata
import { KeyResponse } from "../key_team_helpers/key_list";
import LoggingSettingsView from "../logging_settings_view";
import NotificationManager from "../molecules/notifications_manager";
import { keyDeleteCall, keyUpdateCall } from "../networking";
import { keyDeleteCall, keyUpdateCall, getPolicyInfoWithGuardrails } from "../networking";
import ObjectPermissionsView from "../object_permissions_view";
import { RegenerateKeyModal } from "../organisms/regenerate_key_modal";
import { parseErrorMessage } from "../shared/errorUtils";
@@ -60,6 +60,8 @@ export default function KeyInfoView({
const [currentKeyData, setCurrentKeyData] = useState<KeyResponse | undefined>(keyData);
const [lastRegeneratedAt, setLastRegeneratedAt] = useState<Date | null>(null);
const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false);
const [policyGuardrails, setPolicyGuardrails] = useState<Record<string, string[]>>({});
const [loadingPolicies, setLoadingPolicies] = useState(false);
// Update local state when keyData prop changes (but don't reset to undefined)
useEffect(() => {
@@ -68,6 +70,39 @@ export default function KeyInfoView({
}
}, [keyData]);
// Fetch resolved guardrails for all policies
useEffect(() => {
const fetchPolicyGuardrails = async () => {
if (!accessToken || !currentKeyData?.metadata?.policies || currentKeyData.metadata.policies.length === 0) {
return;
}
setLoadingPolicies(true);
const guardrailsMap: Record<string, string[]> = {};
try {
await Promise.all(
currentKeyData.metadata.policies.map(async (policyName: string) => {
try {
const policyInfo = await getPolicyInfoWithGuardrails(accessToken, policyName);
guardrailsMap[policyName] = policyInfo.resolved_guardrails || [];
} catch (error) {
console.error(`Failed to fetch guardrails for policy ${policyName}:`, error);
guardrailsMap[policyName] = [];
}
})
);
setPolicyGuardrails(guardrailsMap);
} catch (error) {
console.error("Failed to fetch policy guardrails:", error);
} finally {
setLoadingPolicies(false);
}
};
fetchPolicyGuardrails();
}, [accessToken, currentKeyData?.metadata?.policies]);
// Reset recent regeneration indicator after 5 seconds
useEffect(() => {
if (isRecentlyRegenerated) {
@@ -484,6 +519,56 @@ export default function KeyInfoView({
/>
</Card>
<Card>
<Text className="font-medium mb-3">Guardrails</Text>
{currentKeyData.guardrails && currentKeyData.guardrails.length > 0 ? (
<div className="flex flex-wrap gap-2">
{currentKeyData.guardrails.map((guardrail, index) => (
<Badge key={index} color="blue">
{guardrail}
</Badge>
))}
</div>
) : (
<Text className="text-gray-500">No guardrails configured</Text>
)}
{currentKeyData.metadata?.disable_global_guardrails && (
<div className="mt-3 pt-3 border-t border-gray-200">
<Badge color="yellow">Global Guardrails Disabled</Badge>
</div>
)}
</Card>
<Card>
<Text className="font-medium mb-3">Policies</Text>
{currentKeyData.metadata?.policies && currentKeyData.metadata.policies.length > 0 ? (
<div className="space-y-4">
{currentKeyData.metadata.policies.map((policy: string, index: number) => (
<div key={index} className="space-y-2">
<div className="flex items-center gap-2">
<Badge color="purple">{policy}</Badge>
{loadingPolicies && <Text className="text-xs text-gray-400">Loading guardrails...</Text>}
</div>
{!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
<div className="ml-4 pl-3 border-l-2 border-gray-200">
<Text className="text-xs text-gray-500 mb-1">Resolved Guardrails:</Text>
<div className="flex flex-wrap gap-1">
{policyGuardrails[policy].map((guardrail: string, gIndex: number) => (
<Badge key={gIndex} color="blue" size="xs">
{guardrail}
</Badge>
))}
</div>
</div>
)}
</div>
))}
</div>
) : (
<Text className="text-gray-500">No policies configured</Text>
)}
</Card>
<LoggingSettingsView
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
disabledCallbacks={