Skip to content

Commit 87f0f3f

Browse files
committed
add custom tool blockers based on perm configs
1 parent e2366b1 commit 87f0f3f

File tree

6 files changed

+161
-49
lines changed

6 files changed

+161
-49
lines changed

apps/docs/content/docs/en/enterprise/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Enterprise
3-
description: Enterprise features for organizations with advanced security and compliance requirements
3+
description: Enterprise features for business organizations
44
---
55

66
import { Callout } from 'fumadocs-ui/components/callout'

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useCallback, useMemo, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5-
import { Check, Plus, Search, Users } from 'lucide-react'
5+
import { Check, Plus, Search } from 'lucide-react'
66
import {
77
Avatar,
88
AvatarFallback,
@@ -864,40 +864,25 @@ export function AccessControl() {
864864
</p>
865865
) : (
866866
<div className='flex flex-col gap-[12px]'>
867-
<button
868-
type='button'
869-
onClick={() => {
870-
const allIds = availableMembersToAdd.map((m: any) => m.userId)
871-
const allSelected = allIds.every((id: string) => selectedMemberIds.has(id))
872-
if (allSelected) {
873-
setSelectedMemberIds(new Set())
874-
} else {
875-
setSelectedMemberIds(new Set(allIds))
876-
}
877-
}}
878-
className={cn(
879-
'flex items-center gap-[12px] rounded-[8px] border p-[12px] transition-colors',
880-
selectedMemberIds.size === availableMembersToAdd.length
881-
? 'border-[var(--accent)] bg-[var(--accent)]/5'
882-
: 'border-[var(--border)] hover:bg-[var(--surface-5)]'
883-
)}
884-
>
885-
<div className='flex h-8 w-8 items-center justify-center rounded-full bg-[var(--surface-5)]'>
886-
<Users className='h-4 w-4 text-[var(--text-secondary)]' />
887-
</div>
888-
<div className='min-w-0 flex-1 text-left'>
889-
<div className='font-medium text-[14px] text-[var(--text-primary)]'>
890-
Select All
891-
</div>
892-
<div className='text-[12px] text-[var(--text-muted)]'>
893-
{availableMembersToAdd.length} member
894-
{availableMembersToAdd.length !== 1 ? 's' : ''} available
895-
</div>
896-
</div>
897-
{selectedMemberIds.size === availableMembersToAdd.length && (
898-
<Check className='h-[16px] w-[16px] text-[var(--accent)]' />
899-
)}
900-
</button>
867+
<div className='flex justify-end'>
868+
<button
869+
type='button'
870+
onClick={() => {
871+
const allIds = availableMembersToAdd.map((m: any) => m.userId)
872+
const allSelected = allIds.every((id: string) => selectedMemberIds.has(id))
873+
if (allSelected) {
874+
setSelectedMemberIds(new Set())
875+
} else {
876+
setSelectedMemberIds(new Set(allIds))
877+
}
878+
}}
879+
className='text-[12px] text-[var(--accent)] hover:underline'
880+
>
881+
{selectedMemberIds.size === availableMembersToAdd.length
882+
? 'Deselect All'
883+
: 'Select All'}
884+
</button>
885+
</div>
901886
{availableMembersToAdd.map((member: any) => {
902887
const name = member.user?.name || 'Unknown'
903888
const email = member.user?.email || ''

apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { Check, Loader2, Plus, X, XCircle } from 'lucide-react'
3+
import { client } from '@/lib/auth/auth-client'
34
import {
45
BaseClientTool,
56
type BaseClientToolMetadata,
@@ -31,6 +32,20 @@ interface ManageCustomToolArgs {
3132

3233
const API_ENDPOINT = '/api/tools/custom'
3334

35+
async function checkCustomToolsPermission(): Promise<void> {
36+
const activeOrgResponse = await client.organization.getFullOrganization()
37+
const organizationId = activeOrgResponse.data?.id
38+
if (!organizationId) return
39+
40+
const response = await fetch(`/api/permission-groups/user?organizationId=${organizationId}`)
41+
if (!response.ok) return
42+
43+
const data = await response.json()
44+
if (data?.config?.disableCustomTools) {
45+
throw new Error('Custom tools are not allowed based on your permission group settings')
46+
}
47+
}
48+
3449
/**
3550
* Client tool for creating, editing, and deleting custom tools via the copilot.
3651
*/
@@ -164,7 +179,10 @@ export class ManageCustomToolClientTool extends BaseClientTool {
164179
} catch (e: any) {
165180
logger.error('execute failed', { message: e?.message })
166181
this.setState(ClientToolCallState.error)
167-
await this.markToolComplete(500, e?.message || 'Failed to manage custom tool')
182+
await this.markToolComplete(500, e?.message || 'Failed to manage custom tool', {
183+
success: false,
184+
error: e?.message || 'Failed to manage custom tool',
185+
})
168186
}
169187
}
170188

@@ -189,6 +207,8 @@ export class ManageCustomToolClientTool extends BaseClientTool {
189207
throw new Error('Operation is required')
190208
}
191209

210+
await checkCustomToolsPermission()
211+
192212
const { operation, toolId, schema, code } = args
193213

194214
// Get workspace ID from the workflow registry

apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { Check, Loader2, Server, X, XCircle } from 'lucide-react'
3+
import { client } from '@/lib/auth/auth-client'
34
import {
45
BaseClientTool,
56
type BaseClientToolMetadata,
@@ -25,6 +26,20 @@ interface ManageMcpToolArgs {
2526

2627
const API_ENDPOINT = '/api/mcp/servers'
2728

29+
async function checkMcpToolsPermission(): Promise<void> {
30+
const activeOrgResponse = await client.organization.getFullOrganization()
31+
const organizationId = activeOrgResponse.data?.id
32+
if (!organizationId) return
33+
34+
const response = await fetch(`/api/permission-groups/user?organizationId=${organizationId}`)
35+
if (!response.ok) return
36+
37+
const data = await response.json()
38+
if (data?.config?.disableMcpTools) {
39+
throw new Error('MCP tools are not allowed based on your permission group settings')
40+
}
41+
}
42+
2843
/**
2944
* Client tool for creating, editing, and deleting MCP tool servers via the copilot.
3045
*/
@@ -145,7 +160,10 @@ export class ManageMcpToolClientTool extends BaseClientTool {
145160
} catch (e: any) {
146161
logger.error('execute failed', { message: e?.message })
147162
this.setState(ClientToolCallState.error)
148-
await this.markToolComplete(500, e?.message || 'Failed to manage MCP tool')
163+
await this.markToolComplete(500, e?.message || 'Failed to manage MCP tool', {
164+
success: false,
165+
error: e?.message || 'Failed to manage MCP tool',
166+
})
149167
}
150168
}
151169

@@ -167,6 +185,8 @@ export class ManageMcpToolClientTool extends BaseClientTool {
167185
throw new Error('Operation is required')
168186
}
169187

188+
await checkMcpToolsPermission()
189+
170190
const { operation, serverId, config } = args
171191

172192
const { hydration } = useWorkflowRegistry.getState()

apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type SkippedItemType =
5252
| 'block_not_found'
5353
| 'invalid_block_type'
5454
| 'block_not_allowed'
55+
| 'tool_not_allowed'
5556
| 'invalid_edge_target'
5657
| 'invalid_edge_source'
5758
| 'invalid_source_handle'
@@ -561,7 +562,9 @@ function createBlockFromParams(
561562
blockId: string,
562563
params: any,
563564
parentId?: string,
564-
errorsCollector?: ValidationError[]
565+
errorsCollector?: ValidationError[],
566+
permissionConfig?: PermissionGroupConfig | null,
567+
skippedItems?: SkippedItem[]
565568
): any {
566569
const blockConfig = getAllBlocks().find((b) => b.type === params.type)
567570

@@ -629,9 +632,14 @@ function createBlockFromParams(
629632
}
630633
}
631634

632-
// Special handling for tools - normalize to restore sanitized fields
635+
// Special handling for tools - normalize and filter disallowed
633636
if (key === 'tools' && Array.isArray(value)) {
634-
sanitizedValue = normalizeTools(value)
637+
sanitizedValue = filterDisallowedTools(
638+
normalizeTools(value),
639+
permissionConfig ?? null,
640+
blockId,
641+
skippedItems ?? []
642+
)
635643
}
636644

637645
// Special handling for responseFormat - normalize to ensure consistent format
@@ -1109,6 +1117,49 @@ function isBlockTypeAllowed(
11091117
return permissionConfig.allowedIntegrations.includes(blockType)
11101118
}
11111119

1120+
/**
1121+
* Filters out tools that are not allowed by the permission group config
1122+
* Returns both the allowed tools and any skipped tool items for logging
1123+
*/
1124+
function filterDisallowedTools(
1125+
tools: any[],
1126+
permissionConfig: PermissionGroupConfig | null,
1127+
blockId: string,
1128+
skippedItems: SkippedItem[]
1129+
): any[] {
1130+
if (!permissionConfig) {
1131+
return tools
1132+
}
1133+
1134+
const allowedTools: any[] = []
1135+
1136+
for (const tool of tools) {
1137+
if (tool.type === 'custom-tool' && permissionConfig.disableCustomTools) {
1138+
logSkippedItem(skippedItems, {
1139+
type: 'tool_not_allowed',
1140+
operationType: 'add',
1141+
blockId,
1142+
reason: `Custom tool "${tool.title || tool.customToolId || 'unknown'}" is not allowed by permission group - tool not added`,
1143+
details: { toolType: 'custom-tool', toolId: tool.customToolId },
1144+
})
1145+
continue
1146+
}
1147+
if (tool.type === 'mcp' && permissionConfig.disableMcpTools) {
1148+
logSkippedItem(skippedItems, {
1149+
type: 'tool_not_allowed',
1150+
operationType: 'add',
1151+
blockId,
1152+
reason: `MCP tool "${tool.title || 'unknown'}" is not allowed by permission group - tool not added`,
1153+
details: { toolType: 'mcp', serverId: tool.params?.serverId },
1154+
})
1155+
continue
1156+
}
1157+
allowedTools.push(tool)
1158+
}
1159+
1160+
return allowedTools
1161+
}
1162+
11121163
/**
11131164
* Apply operations directly to the workflow JSON state
11141165
*/
@@ -1314,9 +1365,14 @@ function applyOperationsToWorkflowState(
13141365
}
13151366
}
13161367

1317-
// Special handling for tools - normalize to restore sanitized fields
1368+
// Special handling for tools - normalize and filter disallowed
13181369
if (key === 'tools' && Array.isArray(value)) {
1319-
sanitizedValue = normalizeTools(value)
1370+
sanitizedValue = filterDisallowedTools(
1371+
normalizeTools(value),
1372+
permissionConfig,
1373+
block_id,
1374+
skippedItems
1375+
)
13201376
}
13211377

13221378
// Special handling for responseFormat - normalize to ensure consistent format
@@ -1528,7 +1584,9 @@ function applyOperationsToWorkflowState(
15281584
childId,
15291585
childBlock,
15301586
block_id,
1531-
validationErrors
1587+
validationErrors,
1588+
permissionConfig,
1589+
skippedItems
15321590
)
15331591
modifiedState.blocks[childId] = childBlockState
15341592

@@ -1718,7 +1776,14 @@ function applyOperationsToWorkflowState(
17181776
}
17191777

17201778
// Create new block with proper structure
1721-
const newBlock = createBlockFromParams(block_id, params, undefined, validationErrors)
1779+
const newBlock = createBlockFromParams(
1780+
block_id,
1781+
params,
1782+
undefined,
1783+
validationErrors,
1784+
permissionConfig,
1785+
skippedItems
1786+
)
17221787

17231788
// Set loop/parallel data on parent block BEFORE adding to blocks (strict validation)
17241789
if (params.nestedNodes) {
@@ -1797,7 +1862,9 @@ function applyOperationsToWorkflowState(
17971862
childId,
17981863
childBlock,
17991864
block_id,
1800-
validationErrors
1865+
validationErrors,
1866+
permissionConfig,
1867+
skippedItems
18011868
)
18021869
modifiedState.blocks[childId] = childBlockState
18031870

@@ -1919,9 +1986,14 @@ function applyOperationsToWorkflowState(
19191986
}
19201987
}
19211988

1922-
// Special handling for tools - normalize to restore sanitized fields
1989+
// Special handling for tools - normalize and filter disallowed
19231990
if (key === 'tools' && Array.isArray(value)) {
1924-
sanitizedValue = normalizeTools(value)
1991+
sanitizedValue = filterDisallowedTools(
1992+
normalizeTools(value),
1993+
permissionConfig,
1994+
block_id,
1995+
skippedItems
1996+
)
19251997
}
19261998

19271999
// Special handling for responseFormat - normalize to ensure consistent format
@@ -1970,7 +2042,14 @@ function applyOperationsToWorkflowState(
19702042
}
19712043

19722044
// Create new block as child of subflow
1973-
const newBlock = createBlockFromParams(block_id, params, subflowId, validationErrors)
2045+
const newBlock = createBlockFromParams(
2046+
block_id,
2047+
params,
2048+
subflowId,
2049+
validationErrors,
2050+
permissionConfig,
2051+
skippedItems
2052+
)
19742053
modifiedState.blocks[block_id] = newBlock
19752054
}
19762055

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ export const isCredentialSetsEnabled = isTruthy(env.CREDENTIAL_SETS_ENABLED)
9292
*/
9393
export const isAccessControlEnabled = isTruthy(env.ACCESS_CONTROL_ENABLED)
9494

95+
/**
96+
* Is organizations enabled
97+
* True if billing is enabled (orgs come with billing), OR explicitly enabled via env var,
98+
* OR if access control is enabled (access control requires organizations)
99+
*/
100+
export const isOrganizationsEnabled =
101+
isBillingEnabled || isTruthy(env.ORGANIZATIONS_ENABLED) || isAccessControlEnabled
102+
95103
/**
96104
* Is E2B enabled for remote code execution
97105
*/

0 commit comments

Comments
 (0)