From 8913537de0871367b60282ae2dd4bcbe3a84e5c1 Mon Sep 17 00:00:00 2001 From: AriOliv Date: Mon, 17 Aug 2026 19:26:00 -0300 Subject: [PATCH] fix(roles): reflect & preserve wildcard tool grants in the role editor The org role editor rendered a connection's tools as unchecked even when the role already granted them, and re-saving destroyed the grant: convertRoleToFormData expanded a stored ["*"] (all-tools) grant via conn.tools, but COLLECTION_CONNECTIONS_LIST omits tools (fetched lazily per selected connection) -> the grant expanded to [] -> every tool showed unchecked and buildPermission dropped the entry on the next save. Grants for connections absent from the current (paginated) page were likewise dropped. Now the raw grant (including the "*" sentinel) is preserved, and ToolSetSelector understands "*" as all-tools for checked state, counts, and per-tool + per-connection toggles (toggling one tool off first expands "*" to concrete names). buildPermission already collapses a fully-selected connection back to ["*"], so grants round-trip. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/web/src/components/tool-set-selector.tsx | 27 ++++++++++++++----- .../src/views/settings/org-role-detail.tsx | 18 ++++++------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/tool-set-selector.tsx b/apps/web/src/components/tool-set-selector.tsx index 7c3601326e..0077e0bde3 100644 --- a/apps/web/src/components/tool-set-selector.tsx +++ b/apps/web/src/components/tool-set-selector.tsx @@ -315,14 +315,22 @@ export function ToolSetSelector({ const connectionTools = selectedConnectionData?.tools ?? []; - // Check if specific tool is enabled + // Check if specific tool is enabled. A ["*"] grant means "all tools". const isToolSelected = (connectionId: string, toolName: string): boolean => { - return toolSet[connectionId]?.includes(toolName) ?? false; + const set = toolSet[connectionId]; + if (!set) return false; + return set.includes("*") || set.includes(toolName); }; // Toggle a single tool const toggleTool = (connectionId: string, toolName: string) => { - const currentTools = toolSet[connectionId] ?? []; + const rawTools = toolSet[connectionId] ?? []; + // Expand an "all tools" sentinel to concrete names first, so toggling one + // tool off leaves the rest selected. connectionTools is loaded for the open + // (selected) connection, which is the only one whose tools can be toggled. + const currentTools = rawTools.includes("*") + ? connectionTools.map((tt) => tt.name) + : rawTools; const isSelected = currentTools.includes(toolName); const newToolSet = { ...toolSet }; @@ -351,9 +359,11 @@ export function ToolSetSelector({ const currentTools = toolSet[connectionId] ?? []; const allToolNames = tools.map((t) => t.name); + // A ["*"] grant counts as everything selected. const allSelected = - currentTools.length > 0 && - allToolNames.every((name) => currentTools.includes(name)); + currentTools.includes("*") || + (currentTools.length > 0 && + allToolNames.every((name) => currentTools.includes(name))); const newToolSet = { ...toolSet }; @@ -453,7 +463,12 @@ export function ToolSetSelector({ {filteredConnections.map((connection) => { const isSelected = selectedConnectionId === connection.id; const totalTools = isSelected ? connectionTools.length : 0; - const activeTools = toolSet[connection.id]?.length ?? 0; + const rawSet = toolSet[connection.id]; + // "*" means all tools — reflect the full count once known. + const activeTools = + rawSet?.includes("*") && isSelected + ? connectionTools.length + : (rawSet?.length ?? 0); return ( = {}; for (const [key, tools] of Object.entries(permission)) { if (key === "self" || key === "models") continue; + // Keep the RAW grant, including a ["*"] "all tools" sentinel. The + // connection LIST endpoint omits `tools`, so expanding "*" here yields [] + // — which renders the connection as unchecked AND destroys the grant on the + // next save. ToolSetSelector understands "*" directly, so preserve it. if (key === "*") { + // Grant applies to every connection. for (const conn of connections) { - toolSet[conn.id] = tools.includes("*") - ? (conn.tools?.map((t) => t.name) ?? []) - : tools; + toolSet[conn.id] = tools; } } else { - const conn = connections.find((c) => c.id === key); - if (conn) { - toolSet[key] = tools.includes("*") - ? (conn.tools?.map((t) => t.name) ?? []) - : tools; - } + // Preserve unconditionally — even if the connection isn't in the current + // (possibly paginated) list, so a re-save round-trips instead of dropping. + toolSet[key] = tools; } }