Skip to content

Commit 0e73061

Browse files
committed
fix(custom-model): enforce super-user boundary
1 parent fbad5f6 commit 0e73061

20 files changed

Lines changed: 327 additions & 407 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type { SubBlockConfig } from '@/blocks/types'
1616
import { getDependsOnFields } from '@/blocks/utils'
1717
import { useGeneralSettings } from '@/hooks/queries/general-settings'
1818
import { usePermissionConfig } from '@/hooks/use-permission-config'
19-
import { isCustomModel } from '@/providers/custom-model'
19+
import { getCustomModelDisplayValue, isCustomModel } from '@/providers/custom-model'
2020
import { getProviderFromModel } from '@/providers/utils'
2121
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
2222

@@ -314,14 +314,18 @@ export const ComboBox = memo(function ComboBox({
314314
const displayValue = useMemo(() => {
315315
const raw = value?.toString() ?? ''
316316
if (!raw) return ''
317+
if (subBlockId === 'model') {
318+
const visibleModel = getCustomModelDisplayValue(raw, effectiveSuperUser)
319+
if (visibleModel !== raw) return visibleModel
320+
}
317321

318322
const match = evaluatedOptions.find((option) =>
319323
typeof option === 'string' ? option === raw : option.id === raw
320324
)
321325

322326
if (!match) return raw
323327
return typeof match === 'string' ? match : match.label
324-
}, [value, evaluatedOptions])
328+
}, [value, evaluatedOptions, subBlockId, effectiveSuperUser])
325329

326330
const [inputValue, setInputValue] = useState(displayValue)
327331
const [prevDisplayValue, setPrevDisplayValue] = useState(displayValue)

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -397,15 +397,15 @@ describe('AgentBlockHandler', () => {
397397
superUserModeEnabled: false,
398398
})
399399

400-
await expect(
401-
handler.execute(mockContext, mockBlock, {
402-
model: CUSTOM_MODEL_ID,
403-
customModelConfig: {
404-
provider: 'openai',
405-
model: 'gpt-future',
406-
},
407-
})
408-
).rejects.toThrow('only while Super User mode is enabled')
400+
const execution = handler.execute(mockContext, mockBlock, {
401+
model: CUSTOM_MODEL_ID,
402+
customModelConfig: {
403+
provider: 'openai',
404+
model: 'gpt-future',
405+
},
406+
})
407+
await expect(execution).rejects.toThrow('The selected model is unavailable')
408+
await expect(execution).rejects.not.toThrow(/Custom|Super User/)
409409
expect(mockExecuteProviderRequest).not.toHaveBeenCalled()
410410
})
411411

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ export class AgentBlockHandler implements BlockHandler {
102102
let autoRouting: AutoRoutingResult | null = null
103103
if (isCustomModel(configuredModel)) {
104104
if (!ctx.userId) {
105-
throw new Error('Custom models require an authenticated Super User')
105+
throw new Error('The selected model is unavailable for this execution')
106106
}
107107
const { effectiveSuperUser } = await verifyEffectiveSuperUser(ctx.userId)
108108
if (!effectiveSuperUser) {
109-
throw new Error('Custom models are available only while Super User mode is enabled')
109+
throw new Error('The selected model is unavailable for this execution')
110110
}
111111

112112
customModelConfig = parseCustomModelConfig(filteredInputs.customModelConfig)

apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('get blocks metadata', () => {
1515
expect(definitions).not.toHaveProperty('mountedSecrets')
1616
})
1717

18-
it('omits Super User-only definitions unless the caller is effective', () => {
18+
it('always omits Super User-only definitions from Copilot metadata', () => {
1919
const block = {
2020
type: 'agent',
2121
subBlocks: [
@@ -29,9 +29,5 @@ describe('get blocks metadata', () => {
2929
} as unknown as BlockConfig
3030

3131
expect(computeBlockLevelInputs(block)).toEqual({ model: { type: 'string' } })
32-
expect(computeBlockLevelInputs(block, true)).toEqual({
33-
model: { type: 'string' },
34-
customModelConfig: { type: 'json' },
35-
})
3632
})
3733
})

apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions'
77
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
88
import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/env-flags'
99
import { getServiceAccountProviderForProviderId } from '@/lib/oauth/utils'
10-
import { verifyEffectiveSuperUser } from '@/lib/permissions/super-user'
1110
import { isCustomBlockType } from '@/blocks/custom/build-config'
1211
import { getBlock } from '@/blocks/registry'
1312
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
@@ -127,18 +126,6 @@ export const getBlocksMetadataServerTool: BaseServerTool<
127126
: null
128127
const allowedIntegrations =
129128
permissionConfig?.allowedIntegrations ?? getAllowedIntegrationsFromEnv()
130-
let effectiveSuperUser = false
131-
if (context?.userId) {
132-
try {
133-
effectiveSuperUser = (await verifyEffectiveSuperUser(context.userId)).effectiveSuperUser
134-
} catch (error) {
135-
// Metadata remains available, but privileged fields fail closed.
136-
logger.warn('Failed to verify effective Super User for block metadata', {
137-
userId: context.userId,
138-
error: toError(error).message,
139-
})
140-
}
141-
}
142129

143130
const result: Record<string, CopilotBlockMetadata> = {}
144131
for (const blockId of blockIds || []) {
@@ -153,8 +140,7 @@ export const getBlocksMetadataServerTool: BaseServerTool<
153140
const specialBlock = SPECIAL_BLOCKS_METADATA[blockId]
154141
const { commonParameters, operationParameters } = splitParametersByOperation(
155142
specialBlock.subBlocks || [],
156-
specialBlock.inputs || {},
157-
effectiveSuperUser
143+
specialBlock.inputs || {}
158144
)
159145
metadata = {
160146
id: specialBlock.id,
@@ -195,7 +181,7 @@ export const getBlocksMetadataServerTool: BaseServerTool<
195181
// Present it as self-contained: its visible input fields + curated outputs,
196182
// no tools/operations.
197183
const visibleSubBlocks = (blockConfig.subBlocks || []).filter(
198-
(sb) => !sb.hidden && !sb.hideFromCopilot && (!sb.superUserOnly || effectiveSuperUser)
184+
(sb) => !sb.hidden && !sb.hideFromCopilot && !sb.superUserOnly
199185
)
200186
const outputs = blockConfig.outputs
201187
? Object.fromEntries(
@@ -207,9 +193,7 @@ export const getBlocksMetadataServerTool: BaseServerTool<
207193
name: blockConfig.name || blockId,
208194
description: blockConfig.longDescription || blockConfig.description || '',
209195
bestPractices: blockConfig.bestPractices,
210-
inputSchema: visibleSubBlocks.map((subBlock) =>
211-
processSubBlock(subBlock, effectiveSuperUser)
212-
),
196+
inputSchema: visibleSubBlocks.map(processSubBlock),
213197
inputDefinitions: {},
214198
tools: [],
215199
triggers: [],
@@ -252,7 +236,7 @@ export const getBlocksMetadataServerTool: BaseServerTool<
252236
if (
253237
(subBlock.mode === 'trigger' || subBlock.mode === 'trigger-advanced') &&
254238
!SYSTEM_SUBBLOCK_IDS.includes(subBlock.id) &&
255-
(!subBlock.superUserOnly || effectiveSuperUser)
239+
!subBlock.superUserOnly
256240
) {
257241
const fieldDef: any = {
258242
type: subBlock.type,
@@ -292,27 +276,22 @@ export const getBlocksMetadataServerTool: BaseServerTool<
292276
})
293277
}
294278

295-
const hiddenParamKeys = getCopilotHiddenParamKeys(blockConfig, effectiveSuperUser)
296-
const blockInputs = computeBlockLevelInputs(
297-
blockConfig,
298-
effectiveSuperUser,
299-
hiddenParamKeys
300-
)
279+
const hiddenParamKeys = getCopilotHiddenParamKeys(blockConfig)
280+
const blockInputs = computeBlockLevelInputs(blockConfig, hiddenParamKeys)
301281
const { commonParameters, operationParameters } = splitParametersByOperation(
302282
Array.isArray(blockConfig.subBlocks)
303283
? blockConfig.subBlocks.filter(
304284
(sb) =>
305285
!sb.hideFromCopilot &&
306-
(!sb.superUserOnly || effectiveSuperUser) &&
286+
!sb.superUserOnly &&
307287
sb.mode !== 'trigger' &&
308288
sb.mode !== 'trigger-advanced'
309289
)
310290
: [],
311-
blockInputs,
312-
effectiveSuperUser
291+
blockInputs
313292
)
314293

315-
const operationInputs = computeOperationLevelInputs(blockConfig, effectiveSuperUser)
294+
const operationInputs = computeOperationLevelInputs(blockConfig)
316295
const operationIds = resolveOperationIds(blockConfig, operationParameters)
317296
const operations: Record<string, any> = {}
318297
for (const opId of operationIds) {
@@ -700,7 +679,7 @@ function generateInputExample(schema: CopilotSubblockMetadata, inputDef?: any):
700679
}
701680
}
702681

703-
function processSubBlock(sb: any, effectiveSuperUser = false): CopilotSubblockMetadata {
682+
function processSubBlock(sb: any): CopilotSubblockMetadata {
704683
const processed: CopilotSubblockMetadata = {
705684
id: sb.id,
706685
type: sb.type,
@@ -770,7 +749,7 @@ function processSubBlock(sb: any, effectiveSuperUser = false): CopilotSubblockMe
770749
}
771750

772751
// Process options with icon detection
773-
const options = resolveSubblockOptions(sb, effectiveSuperUser)
752+
const options = resolveSubblockOptions(sb)
774753
if (options) {
775754
processed.options = options
776755
}
@@ -880,8 +859,7 @@ function callOptionsWithFallback(
880859
}
881860

882861
function resolveSubblockOptions(
883-
sb: any,
884-
effectiveSuperUser = false
862+
sb: any
885863
): { id: string; label?: string; hasIcon?: boolean }[] | undefined {
886864
// Skip if subblock uses fetchOptions (async network calls)
887865
if (sb.fetchOptions) {
@@ -907,7 +885,7 @@ function resolveSubblockOptions(
907885
}
908886

909887
const normalized = rawOptions
910-
.filter((opt: any) => effectiveSuperUser || !opt?.requiresSuperUser)
888+
.filter((opt: any) => !opt?.requiresSuperUser)
911889
.map((opt: any) => {
912890
if (!opt) return undefined
913891

@@ -961,8 +939,7 @@ function normalizeCondition(condition: any): any | undefined {
961939

962940
function splitParametersByOperation(
963941
subBlocks: any[],
964-
blockInputsForDescriptions?: Record<string, any>,
965-
effectiveSuperUser = false
942+
blockInputsForDescriptions?: Record<string, any>
966943
): {
967944
commonParameters: CopilotSubblockMetadata[]
968945
operationParameters: Record<string, CopilotSubblockMetadata[]>
@@ -972,7 +949,7 @@ function splitParametersByOperation(
972949

973950
for (const sb of subBlocks || []) {
974951
const cond = normalizeCondition(sb.condition)
975-
const processed = processSubBlock(sb, effectiveSuperUser)
952+
const processed = processSubBlock(sb)
976953

977954
if (cond && cond.field === 'operation' && !cond.not && cond.value !== undefined) {
978955
const values: any[] = Array.isArray(cond.value) ? cond.value : [cond.value]
@@ -1000,13 +977,10 @@ function splitParametersByOperation(
1000977
return { commonParameters, operationParameters }
1001978
}
1002979

1003-
function getCopilotHiddenParamKeys(
1004-
blockConfig: BlockConfig,
1005-
effectiveSuperUser = false
1006-
): Set<string> {
980+
function getCopilotHiddenParamKeys(blockConfig: BlockConfig): Set<string> {
1007981
const hiddenParamKeys = new Set<string>()
1008982
for (const subBlock of blockConfig.subBlocks ?? []) {
1009-
if (!subBlock.hideFromCopilot && (!subBlock.superUserOnly || effectiveSuperUser)) continue
983+
if (!subBlock.hideFromCopilot && !subBlock.superUserOnly) continue
1010984
if (subBlock.id) hiddenParamKeys.add(subBlock.id)
1011985
if (subBlock.canonicalParamId) hiddenParamKeys.add(subBlock.canonicalParamId)
1012986
}
@@ -1015,15 +989,14 @@ function getCopilotHiddenParamKeys(
1015989

1016990
export function computeBlockLevelInputs(
1017991
blockConfig: BlockConfig,
1018-
effectiveSuperUser = false,
1019-
hiddenParamKeys = getCopilotHiddenParamKeys(blockConfig, effectiveSuperUser)
992+
hiddenParamKeys = getCopilotHiddenParamKeys(blockConfig)
1020993
): Record<string, any> {
1021994
const inputs = blockConfig.inputs || {}
1022995
const subBlocks: any[] = Array.isArray(blockConfig.subBlocks)
1023996
? blockConfig.subBlocks.filter(
1024997
(sb) =>
1025998
!sb.hideFromCopilot &&
1026-
(!sb.superUserOnly || effectiveSuperUser) &&
999+
!sb.superUserOnly &&
10271000
sb.mode !== 'trigger' &&
10281001
sb.mode !== 'trigger-advanced'
10291002
)
@@ -1058,15 +1031,14 @@ export function computeBlockLevelInputs(
10581031
}
10591032

10601033
function computeOperationLevelInputs(
1061-
blockConfig: BlockConfig,
1062-
effectiveSuperUser = false
1034+
blockConfig: BlockConfig
10631035
): Record<string, Record<string, any>> {
10641036
const inputs = blockConfig.inputs || {}
10651037
const subBlocks = Array.isArray(blockConfig.subBlocks)
10661038
? blockConfig.subBlocks.filter(
10671039
(sb) =>
10681040
!sb.hideFromCopilot &&
1069-
(!sb.superUserOnly || effectiveSuperUser) &&
1041+
!sb.superUserOnly &&
10701042
sb.mode !== 'trigger' &&
10711043
sb.mode !== 'trigger-advanced'
10721044
)

0 commit comments

Comments
 (0)