Skip to content

Commit c41aee5

Browse files
committed
fix(permission-groups): normalize defaults and recover policy reads
1 parent f1cab01 commit c41aee5

9 files changed

Lines changed: 111 additions & 42 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,8 @@ const WorkflowContent = React.memo(
898898
*/
899899
const pendingFocusBlockIdRef = useRef<string | null>(null)
900900

901-
const { resolveSeedGate, agentDefaultModel, isAgentDefaultReady } = useOperationAccess()
901+
const { resolveSeedGate, agentDefaultModel, isAgentDefaultReady, refetchModelSettings } =
902+
useOperationAccess()
902903

903904
const addBlock = useCallback(
904905
(
@@ -914,6 +915,7 @@ const WorkflowContent = React.memo(
914915
presetSubBlockValues?: Record<string, unknown>
915916
) => {
916917
if (type === 'agent' && !isAgentDefaultReady) {
918+
void refetchModelSettings({ cancelRefetch: false })
917919
toast({ message: 'Model settings are not ready. Try again.' })
918920
return
919921
}
@@ -988,6 +990,7 @@ const WorkflowContent = React.memo(
988990
resolveSeedGate,
989991
agentDefaultModel,
990992
isAgentDefaultReady,
993+
refetchModelSettings,
991994
]
992995
)
993996

‎apps/sim/ee/access-control/components/agent-default.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ import type { ProviderId } from '@/providers/types'
2121

2222
const PLATFORM_DEFAULT = '__platform-default__'
2323

24+
function getDefaultModelId(provider: string, model: string): string {
25+
return provider === 'ollama' && !model.toLowerCase().startsWith('ollama/')
26+
? `ollama/${model}`
27+
: model
28+
}
29+
2430
interface AgentDefaultProps {
2531
config: PermissionGroupConfig
2632
providerIds: ProviderId[]
@@ -40,8 +46,12 @@ export function AgentDefault({ config, providerIds, workspaceId, onChange }: Age
4046
const options = useMemo((): ComboboxOption[] => {
4147
const isAllowed = createModelAccessGate(config)
4248
const models = new Set([
43-
...providerIds.flatMap((provider) => getProviderModels(provider)),
44-
...dynamicModels,
49+
...providerIds.flatMap((provider) =>
50+
getProviderModels(provider).map((model) => getDefaultModelId(provider, model))
51+
),
52+
...dynamicModels.flatMap(({ provider, models }) =>
53+
models.map((model) => getDefaultModelId(provider, model))
54+
),
4555
])
4656
const modelOptions: ComboboxOption[] = [
4757
{ label: 'Platform default', value: PLATFORM_DEFAULT },

‎apps/sim/hooks/queries/providers.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ interface UseProviderModelsOptions {
2323
enabled?: boolean
2424
}
2525

26+
interface ProviderModelList {
27+
provider: ProviderName
28+
models: string[]
29+
}
30+
2631
const logger = createLogger('ProviderModelsQuery')
2732

2833
export const PROVIDER_MODELS_STALE_TIME = 5 * 60 * 1000
@@ -116,8 +121,8 @@ export function useProviderModels(
116121
})
117122
}
118123

119-
function combineProviderModelLists(results: UseQueryResult<ProviderModelsResponse>[]) {
120-
return results.flatMap((result) => result.data?.models ?? [])
124+
function combineProviderModelLists(results: UseQueryResult<ProviderModelList>[]) {
125+
return results.flatMap((result) => (result.data ? [result.data] : []))
121126
}
122127

123128
/** Fetches multiple provider catalogs with the shared workspace-scoped query cache. */
@@ -130,6 +135,10 @@ export function useProviderModelLists(
130135
queries: providers.map((provider) => ({
131136
...providerModelsQueryOptions(provider, workspaceId),
132137
enabled: options?.enabled ?? true,
138+
select: (data: ProviderModelsResponse): ProviderModelList => ({
139+
provider,
140+
models: data.models,
141+
}),
133142
})),
134143
combine: combineProviderModelLists,
135144
})

‎apps/sim/hooks/use-operation-access.ts‎

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

3-
import { useMemo } from 'react'
3+
import { useCallback, useMemo } from 'react'
4+
import type { RefetchOptions } from '@tanstack/react-query'
45
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
56
import { resolveAgentDefaultModel } from '@/lib/permission-groups/model-access'
67
import {
@@ -20,6 +21,7 @@ import { PROVIDER_DEFINITIONS } from '@/providers/models'
2021
export interface OperationAccess {
2122
agentDefaultModel: string | null
2223
isAgentDefaultReady: boolean
24+
refetchModelSettings: (options?: RefetchOptions) => Promise<void>
2325
/**
2426
* Whether the permission config is still loading. Every list this module
2527
* filters reads as unrestricted until it resolves, so a surface that
@@ -78,23 +80,42 @@ export interface OperationAccess {
7880
*/
7981
export function useOperationAccess(): OperationAccess {
8082
const { hosted } = useDeploymentShape()
81-
const { config, isToolAllowed, isModelUsable, isLoading, isPermissionFetching } =
82-
usePermissionConfig()
83+
const {
84+
config,
85+
isToolAllowed,
86+
isModelUsable,
87+
isLoading,
88+
isPermissionFetching,
89+
isPermissionReady,
90+
refetchPermissionConfig,
91+
} = usePermissionConfig()
8392
const blacklistedProviders = useBlacklistedProviders()
93+
const refetchModelSettings = useCallback(
94+
async (options?: RefetchOptions) => {
95+
await Promise.all([refetchPermissionConfig(options), blacklistedProviders.refetch(options)])
96+
},
97+
[refetchPermissionConfig, blacklistedProviders.refetch]
98+
)
8499

85100
return useMemo(() => {
86101
const isReady = !isLoading
87-
const isAgentDefaultReady = isReady && !isPermissionFetching && blacklistedProviders.isSuccess
102+
const isAgentDefaultReady =
103+
isReady &&
104+
isPermissionReady &&
105+
!isPermissionFetching &&
106+
(!config.defaultAgentModel || blacklistedProviders.isSuccess)
88107
return {
89108
isAgentDefaultReady,
90-
agentDefaultModel: isAgentDefaultReady
91-
? resolveAgentDefaultModel(config, {
92-
allowAuto: hosted,
93-
availableProviderIds: Object.keys(PROVIDER_DEFINITIONS).filter(
94-
(provider) => !blacklistedProviders.data.blacklistedProviders.includes(provider)
95-
),
96-
})
97-
: null,
109+
refetchModelSettings,
110+
agentDefaultModel:
111+
isAgentDefaultReady && blacklistedProviders.isSuccess
112+
? resolveAgentDefaultModel(config, {
113+
allowAuto: hosted,
114+
availableProviderIds: Object.keys(PROVIDER_DEFINITIONS).filter(
115+
(provider) => !blacklistedProviders.data.blacklistedProviders.includes(provider)
116+
),
117+
})
118+
: null,
98119
isPermissionLoading: isLoading,
99120
getDeniedOperations: (block, operationIds) =>
100121
isReady
@@ -120,6 +141,8 @@ export function useOperationAccess(): OperationAccess {
120141
isModelUsable,
121142
isLoading,
122143
isPermissionFetching,
144+
isPermissionReady,
145+
refetchModelSettings,
123146
hosted,
124147
blacklistedProviders.isSuccess,
125148
blacklistedProviders.data,

‎apps/sim/hooks/use-permission-config.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface PermissionConfigResult {
2929
config: PermissionGroupConfig
3030
isLoading: boolean
3131
isPermissionFetching: boolean
32+
isPermissionReady: boolean
33+
refetchPermissionConfig: ReturnType<typeof useUserPermissionConfig>['refetch']
3234
isInPermissionGroup: boolean
3335
filterBlocks: <T extends { type: string }>(blocks: T[]) => T[]
3436
filterProviders: (providerIds: string[]) => string[]
@@ -63,6 +65,8 @@ export function usePermissionConfig(): PermissionConfigResult {
6365
data: permissionData,
6466
isLoading: isPermissionLoading,
6567
isFetching: isPermissionFetching,
68+
isSuccess: isPermissionReady,
69+
refetch: refetchPermissionConfig,
6670
} = useUserPermissionConfig(workspaceId)
6771
const {
6872
data: envAllowlistData,
@@ -231,6 +235,8 @@ export function usePermissionConfig(): PermissionConfigResult {
231235
config: mergedConfig,
232236
isLoading,
233237
isPermissionFetching,
238+
isPermissionReady,
239+
refetchPermissionConfig,
234240
isInPermissionGroup,
235241
filterBlocks,
236242
filterProviders,
@@ -252,6 +258,8 @@ export function usePermissionConfig(): PermissionConfigResult {
252258
mergedConfig,
253259
isLoading,
254260
isPermissionFetching,
261+
isPermissionReady,
262+
refetchPermissionConfig,
255263
isInPermissionGroup,
256264
filterBlocks,
257265
filterProviders,

‎apps/sim/lib/permission-groups/application/management.test.ts‎

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -91,30 +91,30 @@ beforeEach(() => {
9191
mocks.allConflict.mockResolvedValue(null)
9292
})
9393
describe('permission group current organization authority', () => {
94-
it.each([{ BLACKLISTED_PROVIDERS: 'openai' }, { BLACKLISTED_MODELS: 'gpt-4*' }])(
95-
'refuses a deployment-blocked Agent default: %j',
96-
async (env) => {
97-
queueTableRows(member, [{ role: 'admin' }])
98-
dbChainMockFns.returning.mockResolvedValueOnce([
99-
{ ...group, config: { defaultAgentModel: 'gpt-4o' } },
100-
])
101-
setEnv(env)
102-
envFlagsMockFns.getBlacklistedProvidersFromEnv.mockReturnValue(
103-
env.BLACKLISTED_PROVIDERS ? [env.BLACKLISTED_PROVIDERS] : []
104-
)
105-
try {
106-
await expect(
107-
updatePermissionGroup.execute({
108-
principal,
109-
input: { ...scope, changes: { config: { defaultAgentModel: 'gpt-4o' } } },
110-
})
111-
).rejects.toMatchObject({ code: 'validation' })
112-
} finally {
113-
resetEnvMock()
114-
envFlagsMockFns.getBlacklistedProvidersFromEnv.mockReset()
115-
}
94+
it.each([
95+
{ env: { BLACKLISTED_PROVIDERS: 'openai' }, defaultAgentModel: 'gpt-4o' },
96+
{ env: { BLACKLISTED_MODELS: 'gpt-4*' }, defaultAgentModel: 'gpt-4o' },
97+
{ env: { BLACKLISTED_MODELS: 'llama*' }, defaultAgentModel: 'ollama/llama3' },
98+
{ env: { BLACKLISTED_MODELS: 'ollama/llama*' }, defaultAgentModel: 'ollama/llama3' },
99+
])('refuses a deployment-blocked Agent default: %j', async ({ env, defaultAgentModel }) => {
100+
queueTableRows(member, [{ role: 'admin' }])
101+
dbChainMockFns.returning.mockResolvedValueOnce([{ ...group, config: { defaultAgentModel } }])
102+
setEnv(env)
103+
envFlagsMockFns.getBlacklistedProvidersFromEnv.mockReturnValue(
104+
env.BLACKLISTED_PROVIDERS ? [env.BLACKLISTED_PROVIDERS] : []
105+
)
106+
try {
107+
await expect(
108+
updatePermissionGroup.execute({
109+
principal,
110+
input: { ...scope, changes: { config: { defaultAgentModel } } },
111+
})
112+
).rejects.toMatchObject({ code: 'validation' })
113+
} finally {
114+
resetEnvMock()
115+
envFlagsMockFns.getBlacklistedProvidersFromEnv.mockReset()
116116
}
117-
)
117+
})
118118
it.each(['gpt-5-chat-latest', 'unsupported-agent-model'])(
119119
'refuses an unavailable Agent default: %s',
120120
async (defaultAgentModel) => {

‎apps/sim/lib/permission-groups/model-access.server.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ export function resolveAvailableAgentDefaultModel(
1111
const model = resolveAgentDefaultModel(config, { allowAuto: isHosted })
1212
if (!model) return null
1313
const provider = findProviderFromModel(model)
14+
const modelIds = provider === 'ollama' ? [model, model.replace(/^ollama\//i, '')] : [model]
1415
return (!provider || !isProviderBlacklisted(provider)) &&
15-
filterBlacklistedModels([model]).length > 0
16+
filterBlacklistedModels(modelIds).length === modelIds.length
1617
? model
1718
: null
1819
}

‎apps/sim/lib/permission-groups/model-access.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ describe('createModelAccessGate', () => {
88
expect(gate('claude-sonnet-4-5')).toBe(true)
99
})
1010

11+
it.each(['llama3', 'ollama/llama3', 'OLLAMA/LLAMA3'])(
12+
'denies a namespaced Ollama model when its denylist entry is %s',
13+
(deniedModel) => {
14+
const gate = createModelAccessGate({
15+
deniedModels: [deniedModel],
16+
allowedModelProviders: ['ollama'],
17+
})
18+
expect(gate('ollama/llama3')).toBe(false)
19+
expect(gate('ollama/other-model')).toBe(true)
20+
}
21+
)
22+
1123
it('denies a model whose provider is not allowlisted', () => {
1224
const gate = createModelAccessGate({ deniedModels: [], allowedModelProviders: ['anthropic'] })
1325
expect(gate('gpt-4o')).toBe(false)

‎apps/sim/lib/permission-groups/model-access.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ export function createModelAccessGate(config: ModelGateConfig | null | undefined
3232

3333
const denied = new Set(deniedModels?.map((model) => model.toLowerCase()))
3434
return (model: string) => {
35-
if (denied.has(model.toLowerCase())) return false
35+
const normalizedModel = model.toLowerCase()
36+
if (denied.has(normalizedModel) || denied.has(normalizedModel.replace(/^ollama\//, ''))) {
37+
return false
38+
}
3639
if (allowedProviders === null) return true
3740
const providerId = findProviderFromModel(model)
3841
if (!providerId) return true

0 commit comments

Comments
 (0)