Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions apps/web/src/components/tool-set-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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 };

Expand Down Expand Up @@ -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 (
<ConnectionItem
Expand Down
18 changes: 9 additions & 9 deletions apps/web/src/views/settings/org-role-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1087,19 +1087,19 @@ function convertRoleToFormData(
const toolSet: Record<string, string[]> = {};
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;
}
}

Expand Down
Loading