From 2b86688c3a1e05430adc533d009d69cca80ff42c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Mar 2026 00:55:39 +0000 Subject: [PATCH] feat(ui): add Permissions step to Create Agent wizard for team/key RBAC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new 'Permissions' step (step 3) to the Add Agent wizard that allows users to select which teams and keys are allowed to invoke the agent. - New step between 'MCP Tools' and 'Assign Key' with multi-select dropdowns for teams and keys - After agent creation, updates selected teams/keys' object_permission to include the new agent ID in their agents list - Shows informational message when no restrictions are set - Wizard now has 5 steps: Configure → MCP Tools → Permissions → Assign Key → Ready Co-authored-by: Ishaan Jaff --- .../src/components/agents/add_agent_form.tsx | 172 ++++++++++++++++-- 1 file changed, 159 insertions(+), 13 deletions(-) diff --git a/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx b/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx index 10fbfa4615a..c6081e83504 100644 --- a/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx +++ b/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { Modal, Form, message, Select, Input, Steps, Radio, Tag, Divider } from "antd"; import { Button } from "@tremor/react"; -import { CheckCircleFilled, KeyOutlined, RobotOutlined, AppstoreOutlined, InfoCircleOutlined } from "@ant-design/icons"; +import { CheckCircleFilled, KeyOutlined, RobotOutlined, AppstoreOutlined, InfoCircleOutlined, TeamOutlined, LockOutlined } from "@ant-design/icons"; import CreatedKeyDisplay from "../shared/CreatedKeyDisplay"; import { createAgentCall, @@ -9,6 +9,8 @@ import { keyCreateForAgentCall, keyListCall, keyUpdateCall, + teamListCall, + teamUpdateCall, modelAvailableCall, AgentCreateInfo, } from "../networking"; @@ -45,7 +47,15 @@ const AddAgentForm: React.FC = ({ const [agentTypeMetadata, setAgentTypeMetadata] = useState([]); const [loadingMetadata, setLoadingMetadata] = useState(false); - // Step 1: key assignment state + // Step 2: allowed teams/keys state + const [allowedTeams, setAllowedTeams] = useState([]); + const [allowedKeys, setAllowedKeys] = useState([]); + const [teamsData, setTeamsData] = useState([]); + const [keysData, setKeysData] = useState([]); + const [loadingTeams, setLoadingTeams] = useState(false); + const [loadingKeysForPerms, setLoadingKeysForPerms] = useState(false); + + // Step 3: key assignment state const [keyAssignOption, setKeyAssignOption] = useState<"create_new" | "existing_key" | "skip">("create_new"); const [newKeyName, setNewKeyName] = useState(""); const [newKeyModels, setNewKeyModels] = useState([]); @@ -55,7 +65,7 @@ const AddAgentForm: React.FC = ({ const [availableModels, setAvailableModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); - // Step 2: results + // Step 4: results const [createdAgentName, setCreatedAgentName] = useState(""); const [createdKeyValue, setCreatedKeyValue] = useState(null); const [assignedKeyAlias, setAssignedKeyAlias] = useState(null); @@ -76,9 +86,29 @@ const AddAgentForm: React.FC = ({ fetchMetadata(); }, []); - // Fetch existing keys when assign key step becomes active (step 2) + // Fetch teams and keys when Allowed Teams & Keys step becomes active (step 2) useEffect(() => { - if (currentStep === 2 && accessToken && existingKeys.length === 0) { + if (currentStep === 2 && accessToken) { + if (teamsData.length === 0) { + setLoadingTeams(true); + teamListCall(accessToken, null) + .then((data) => setTeamsData(data || [])) + .catch((err) => console.error("Error fetching teams:", err)) + .finally(() => setLoadingTeams(false)); + } + if (keysData.length === 0) { + setLoadingKeysForPerms(true); + keyListCall(accessToken, null, null, null, null, null, 1, 200) + .then((result) => setKeysData(result?.keys || [])) + .catch((err) => console.error("Error fetching keys:", err)) + .finally(() => setLoadingKeysForPerms(false)); + } + } + }, [currentStep, accessToken]); + + // Fetch existing keys when assign key step becomes active (step 3) + useEffect(() => { + if (currentStep === 3 && accessToken && existingKeys.length === 0) { const fetchKeys = async () => { setLoadingKeys(true); try { @@ -96,7 +126,7 @@ const AddAgentForm: React.FC = ({ // Fetch available models when Assign Key step is active (same list as key generation) useEffect(() => { - if (currentStep !== 2 || !accessToken || !userId || !userRole) return; + if (currentStep !== 3 || !accessToken || !userId || !userRole) return; let cancelled = false; setLoadingModels(true); modelAvailableCall(accessToken, userId, userRole) @@ -245,7 +275,39 @@ const AddAgentForm: React.FC = ({ setAssignedKeyAlias(keyInfo?.key_alias || selectedExistingKey.slice(0, 12) + "…"); } - setCurrentStep(3); + // Update allowed keys' object_permission to include this agent + for (const keyToken of allowedKeys) { + try { + const keyInfo = keysData.find((k: any) => k.token === keyToken); + const existingAgents = keyInfo?.object_permission?.agents || []; + await keyUpdateCall(accessToken, { + key: keyToken, + object_permission: { + agents: [...existingAgents, agentId], + }, + }); + } catch (err) { + console.error(`Failed to update key ${keyToken} permissions:`, err); + } + } + + // Update allowed teams' object_permission to include this agent + for (const teamId of allowedTeams) { + try { + const teamInfo = teamsData.find((t: any) => t.team_id === teamId); + const existingAgents = teamInfo?.object_permission?.agents || []; + await teamUpdateCall(accessToken, { + team_id: teamId, + object_permission: { + agents: [...existingAgents, agentId], + }, + }); + } catch (err) { + console.error(`Failed to update team ${teamId} permissions:`, err); + } + } + + setCurrentStep(4); onSuccess(); } catch (error) { console.error("Error creating agent:", error); @@ -260,6 +322,8 @@ const AddAgentForm: React.FC = ({ form.resetFields(); setAgentType("a2a"); setCurrentStep(0); + setAllowedTeams([]); + setAllowedKeys([]); setKeyAssignOption("create_new"); setNewKeyName(""); setNewKeyModels([]); @@ -318,6 +382,81 @@ const AddAgentForm: React.FC = ({ ); + const renderAllowedTeamsKeysStep = () => ( +
+

+ Control which teams and keys are allowed to invoke this agent. Leave empty to allow all (no restrictions). +

+ +
+ + + Allowed Teams + + } + tooltip="Only these teams will be able to invoke this agent. Leave empty for unrestricted access." + > + setAllowedKeys(val)} + filterOption={(input, option) => + (option?.label as string ?? "").toLowerCase().includes(input.toLowerCase()) + } + options={keysData.map((k: any) => ({ + label: k.key_alias || (k.token ? k.token.slice(0, 12) + "…" : k.key_name || "unnamed"), + value: k.token, + }))} + showSearch + allowClear + /> + +
+ + {allowedTeams.length === 0 && allowedKeys.length === 0 && ( +
+ + No restrictions set — all teams and keys will be able to invoke this agent. +
+ )} +
+ ); + const handleAgentTypeChange = (value: string) => { setAgentType(value); form.resetFields(); @@ -623,7 +762,7 @@ const AddAgentForm: React.FC = ({ - {selectedLogo && currentStep < 1 && ( + {selectedLogo && currentStep === 0 && ( Agent )}

Add New Agent

@@ -644,6 +783,7 @@ const AddAgentForm: React.FC = ({ + @@ -660,14 +800,15 @@ const AddAgentForm: React.FC = ({ > {currentStep === 0 && renderConfigureStep()} {currentStep === 1 && renderMCPToolsStep()} - {currentStep === 2 && renderAssignKeyStep()} - {currentStep === 3 && renderReadyStep()} + {currentStep === 2 && renderAllowedTeamsKeysStep()} + {currentStep === 3 && renderAssignKeyStep()} + {currentStep === 4 && renderReadyStep()} {/* Footer navigation */}
- {currentStep > 0 && currentStep < 3 && ( + {currentStep > 0 && currentStep < 4 && (
- {currentStep < 3 && ( + {currentStep < 4 && ( @@ -694,11 +835,16 @@ const AddAgentForm: React.FC = ({ )} {currentStep === 2 && ( + + )} + {currentStep === 3 && ( )} - {currentStep === 3 && ( + {currentStep === 4 && (