mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
execution time enforcement of mcp and custom tools
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label>Allowed Integrations</Label>
|
||||
<Label>Allowed Blocks</Label>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
@@ -545,13 +546,12 @@ export function AccessControl() {
|
||||
</button>
|
||||
</div>
|
||||
<p className='text-[12px] text-[var(--text-muted)]'>
|
||||
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.
|
||||
</p>
|
||||
<div className='flex items-center gap-[8px] rounded-[8px] border border-[var(--border)] bg-transparent px-[8px] py-[5px]'>
|
||||
<Search className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-tertiary)]' />
|
||||
<BaseInput
|
||||
placeholder='Search integrations...'
|
||||
placeholder='Search blocks...'
|
||||
value={integrationSearchTerm}
|
||||
onChange={(e) => 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() {
|
||||
<div className='flex flex-col gap-[16px] rounded-[8px] border border-[var(--border)] p-[12px]'>
|
||||
{/* Sidebar */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='text-[11px] font-medium uppercase tracking-wide text-[var(--text-tertiary)]'>
|
||||
<span className='font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
Sidebar
|
||||
</span>
|
||||
<div className='flex flex-col gap-[8px] pl-[2px]'>
|
||||
@@ -649,7 +649,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='hide-knowledge-base'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
Knowledge Base
|
||||
</Label>
|
||||
@@ -666,7 +666,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='hide-templates'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
Templates
|
||||
</Label>
|
||||
@@ -676,7 +676,7 @@ export function AccessControl() {
|
||||
|
||||
{/* Workflow Panel */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='text-[11px] font-medium uppercase tracking-wide text-[var(--text-tertiary)]'>
|
||||
<span className='font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
Workflow Panel
|
||||
</span>
|
||||
<div className='flex flex-col gap-[8px] pl-[2px]'>
|
||||
@@ -692,7 +692,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='hide-copilot'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
Copilot
|
||||
</Label>
|
||||
@@ -702,7 +702,7 @@ export function AccessControl() {
|
||||
|
||||
{/* Settings Tabs */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='text-[11px] font-medium uppercase tracking-wide text-[var(--text-tertiary)]'>
|
||||
<span className='font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
Settings Tabs
|
||||
</span>
|
||||
<div className='flex flex-col gap-[8px] pl-[2px]'>
|
||||
@@ -718,7 +718,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='hide-api-keys'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
API Keys
|
||||
</Label>
|
||||
@@ -735,7 +735,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='hide-environment'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
Environment
|
||||
</Label>
|
||||
@@ -752,7 +752,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='hide-files'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
Files
|
||||
</Label>
|
||||
@@ -762,7 +762,7 @@ export function AccessControl() {
|
||||
|
||||
{/* Tools */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='text-[11px] font-medium uppercase tracking-wide text-[var(--text-tertiary)]'>
|
||||
<span className='font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
Tools
|
||||
</span>
|
||||
<div className='flex flex-col gap-[8px] pl-[2px]'>
|
||||
@@ -778,7 +778,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='disable-mcp'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
MCP Tools
|
||||
</Label>
|
||||
@@ -795,7 +795,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='disable-custom-tools'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
Custom Tools
|
||||
</Label>
|
||||
@@ -805,7 +805,7 @@ export function AccessControl() {
|
||||
|
||||
{/* Logs */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<span className='text-[11px] font-medium uppercase tracking-wide text-[var(--text-tertiary)]'>
|
||||
<span className='font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
Logs
|
||||
</span>
|
||||
<div className='flex flex-col gap-[8px] pl-[2px]'>
|
||||
@@ -821,7 +821,7 @@ export function AccessControl() {
|
||||
/>
|
||||
<Label
|
||||
htmlFor='hide-trace-spans'
|
||||
className='cursor-pointer text-[13px] font-normal'
|
||||
className='cursor-pointer font-normal text-[13px]'
|
||||
>
|
||||
Trace spans
|
||||
</Label>
|
||||
|
||||
@@ -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<void> {
|
||||
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[]
|
||||
|
||||
@@ -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<PermissionGroupConfig | null> {
|
||||
@@ -99,3 +113,37 @@ export async function validateBlockType(
|
||||
throw new IntegrationNotAllowedError(blockType)
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateMcpToolsAllowed(userId: string | undefined): Promise<void> {
|
||||
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<void> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user