diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts
index 16a18596e7..5e1486ff26 100644
--- a/apps/sim/app/api/permission-groups/[id]/route.ts
+++ b/apps/sim/app/api/permission-groups/[id]/route.ts
@@ -17,6 +17,14 @@ const configSchema = z.object({
allowedIntegrations: z.array(z.string()).nullable().optional(),
allowedModelProviders: z.array(z.string()).nullable().optional(),
hideTraceSpans: z.boolean().optional(),
+ hideKnowledgeBaseTab: z.boolean().optional(),
+ hideCopilot: z.boolean().optional(),
+ hideApiKeysTab: z.boolean().optional(),
+ hideEnvironmentTab: z.boolean().optional(),
+ hideFilesTab: z.boolean().optional(),
+ disableMcpTools: z.boolean().optional(),
+ disableCustomTools: z.boolean().optional(),
+ hideTemplates: z.boolean().optional(),
})
const updateSchema = z.object({
diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts
index 1e513442a9..a3c3a7512b 100644
--- a/apps/sim/app/api/permission-groups/route.ts
+++ b/apps/sim/app/api/permission-groups/route.ts
@@ -18,6 +18,14 @@ const configSchema = z.object({
allowedIntegrations: z.array(z.string()).nullable().optional(),
allowedModelProviders: z.array(z.string()).nullable().optional(),
hideTraceSpans: z.boolean().optional(),
+ hideKnowledgeBaseTab: z.boolean().optional(),
+ hideCopilot: z.boolean().optional(),
+ hideApiKeysTab: z.boolean().optional(),
+ hideEnvironmentTab: z.boolean().optional(),
+ hideFilesTab: z.boolean().optional(),
+ disableMcpTools: z.boolean().optional(),
+ disableCustomTools: z.boolean().optional(),
+ hideTemplates: z.boolean().optional(),
})
const createSchema = z.object({
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx
index 6be9f4ef93..442984fb3d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx
@@ -112,7 +112,8 @@ export function AccessControl() {
const [integrationSearchTerm, setIntegrationSearchTerm] = useState('')
const allBlocks = useMemo(() => {
- const blocks = getAllBlocks().filter((b) => !b.hideFromToolbar)
+ // Filter out hidden blocks and start_trigger (which should never be disabled)
+ const blocks = getAllBlocks().filter((b) => !b.hideFromToolbar && b.type !== 'start_trigger')
return blocks.sort((a, b) => {
// Group by category: triggers first, then blocks, then tools
const categoryOrder = { triggers: 0, blocks: 1, tools: 2 }
@@ -525,7 +526,7 @@ export function AccessControl() {
- Allowed Integrations
+ Allowed Blocks
{
@@ -545,13 +546,12 @@ export function AccessControl() {
- Select which integrations are visible in the toolbar. All are visible by
- default.
+ Select which blocks are visible in the toolbar. All are visible by default.
setIntegrationSearchTerm(e.target.value)}
className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
@@ -633,7 +633,7 @@ export function AccessControl() {
{/* Sidebar */}
-
+
Sidebar
@@ -649,7 +649,7 @@ export function AccessControl() {
/>
Knowledge Base
@@ -666,7 +666,7 @@ export function AccessControl() {
/>
Templates
@@ -676,7 +676,7 @@ export function AccessControl() {
{/* Workflow Panel */}
-
+
Workflow Panel
@@ -692,7 +692,7 @@ export function AccessControl() {
/>
Copilot
@@ -702,7 +702,7 @@ export function AccessControl() {
{/* Settings Tabs */}
-
+
Settings Tabs
@@ -718,7 +718,7 @@ export function AccessControl() {
/>
API Keys
@@ -735,7 +735,7 @@ export function AccessControl() {
/>
Environment
@@ -752,7 +752,7 @@ export function AccessControl() {
/>
Files
@@ -762,7 +762,7 @@ export function AccessControl() {
{/* Tools */}
-
+
Tools
@@ -778,7 +778,7 @@ export function AccessControl() {
/>
MCP Tools
@@ -795,7 +795,7 @@ export function AccessControl() {
/>
Custom Tools
@@ -805,7 +805,7 @@ export function AccessControl() {
{/* Logs */}
-
+
Logs
@@ -821,7 +821,7 @@ export function AccessControl() {
/>
Trace spans
diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts
index 206684b169..6640546f4a 100644
--- a/apps/sim/executor/handlers/agent/agent-handler.ts
+++ b/apps/sim/executor/handlers/agent/agent-handler.ts
@@ -25,7 +25,12 @@ import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/execu
import { collectBlockData } from '@/executor/utils/block-data'
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
import { stringifyJSON } from '@/executor/utils/json'
-import { validateBlockType, validateModelProvider } from '@/executor/utils/permission-check'
+import {
+ validateBlockType,
+ validateCustomToolsAllowed,
+ validateMcpToolsAllowed,
+ validateModelProvider,
+} from '@/executor/utils/permission-check'
import { executeProviderRequest } from '@/providers'
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
import type { SerializedBlock } from '@/serializer/types'
@@ -51,6 +56,9 @@ export class AgentBlockHandler implements BlockHandler {
const filteredTools = await this.filterUnavailableMcpTools(ctx, inputs.tools || [])
const filteredInputs = { ...inputs, tools: filteredTools }
+ // Validate tool permissions before processing
+ await this.validateToolPermissions(ctx, filteredInputs.tools || [])
+
const responseFormat = this.parseResponseFormat(filteredInputs.responseFormat)
const model = filteredInputs.model || AGENT.DEFAULT_MODEL
@@ -147,6 +155,21 @@ export class AgentBlockHandler implements BlockHandler {
return undefined
}
+ private async validateToolPermissions(ctx: ExecutionContext, tools: ToolInput[]): Promise
{
+ if (!Array.isArray(tools) || tools.length === 0) return
+
+ const hasMcpTools = tools.some((t) => t.type === 'mcp')
+ const hasCustomTools = tools.some((t) => t.type === 'custom-tool')
+
+ if (hasMcpTools) {
+ await validateMcpToolsAllowed(ctx.userId)
+ }
+
+ if (hasCustomTools) {
+ await validateCustomToolsAllowed(ctx.userId)
+ }
+ }
+
private async filterUnavailableMcpTools(
ctx: ExecutionContext,
tools: ToolInput[]
diff --git a/apps/sim/executor/utils/permission-check.ts b/apps/sim/executor/utils/permission-check.ts
index 50eef90882..14205c1e26 100644
--- a/apps/sim/executor/utils/permission-check.ts
+++ b/apps/sim/executor/utils/permission-check.ts
@@ -26,6 +26,20 @@ export class IntegrationNotAllowedError extends Error {
}
}
+export class McpToolsNotAllowedError extends Error {
+ constructor() {
+ super('MCP tools are not allowed based on your permission group settings')
+ this.name = 'McpToolsNotAllowedError'
+ }
+}
+
+export class CustomToolsNotAllowedError extends Error {
+ constructor() {
+ super('Custom tools are not allowed based on your permission group settings')
+ this.name = 'CustomToolsNotAllowedError'
+ }
+}
+
export async function getUserPermissionConfig(
userId: string
): Promise {
@@ -99,3 +113,37 @@ export async function validateBlockType(
throw new IntegrationNotAllowedError(blockType)
}
}
+
+export async function validateMcpToolsAllowed(userId: string | undefined): Promise {
+ if (!userId) {
+ return
+ }
+
+ const config = await getUserPermissionConfig(userId)
+
+ if (!config) {
+ return
+ }
+
+ if (config.disableMcpTools) {
+ logger.warn('MCP tools blocked by permission group', { userId })
+ throw new McpToolsNotAllowedError()
+ }
+}
+
+export async function validateCustomToolsAllowed(userId: string | undefined): Promise {
+ if (!userId) {
+ return
+ }
+
+ const config = await getUserPermissionConfig(userId)
+
+ if (!config) {
+ return
+ }
+
+ if (config.disableCustomTools) {
+ logger.warn('Custom tools blocked by permission group', { userId })
+ throw new CustomToolsNotAllowedError()
+ }
+}