Skip to content

Commit f1cab01

Browse files
committed
fix(permission-groups): validate defaults before seeding agents
1 parent 3d8d9ac commit f1cab01

16 files changed

Lines changed: 201 additions & 100 deletions

File tree

‎.agents/skills/ship/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ When the user runs `/ship`:
7777
}
7878
# Runs every audit CI runs, concurrently, and replays the output of any that fail.
7979
# The audit list is derived in scripts/run-audits.ts — do not hand-list audits here.
80-
# Run the current "Lint workflows" block from .github/workflows/checks.yml locally too,
81-
# using its pinned actionlint version and checksum in a temporary directory. Preserve
82-
# its -shellcheck= -pyflakes= flags; lint all workflows and abort ship if it fails.
80+
# Install CI's pinned actionlint version for the host OS/architecture and verify its
81+
# artifact against the official release checksums in a local mktemp directory.
82+
# Preserve CI's -shellcheck= -pyflakes= flags; lint all workflows and abort ship if it fails.
8383
bun run check:audits || { echo "❌ audit(s) failed — do not ship"; exit 1; }
8484
bun run type-check || { echo "❌ type-check failed — do not ship"; exit 1; }
8585
# CI's "Verify docs manifest is in sync" step is not a `check:*` script, so the runner above

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16032,8 +16032,7 @@
1603216032
"anyOf": [
1603316033
{
1603416034
"type": "string",
16035-
"minLength": 1,
16036-
"maxLength": 255
16035+
"minLength": 1
1603716036
},
1603816037
{
1603916038
"type": "null"
@@ -16563,8 +16562,7 @@
1656316562
"anyOf": [
1656416563
{
1656516564
"type": "string",
16566-
"minLength": 1,
16567-
"maxLength": 255
16565+
"minLength": 1
1656816566
},
1656916567
{
1657016568
"type": "null"
@@ -17014,8 +17012,7 @@
1701417012
"anyOf": [
1701517013
{
1701617014
"type": "string",
17017-
"minLength": 1,
17018-
"maxLength": 255
17015+
"minLength": 1
1701917016
},
1702017017
{
1702117018
"type": "null"
@@ -18314,8 +18311,7 @@
1831418311
"anyOf": [
1831518312
{
1831618313
"type": "string",
18317-
"minLength": 1,
18318-
"maxLength": 255
18314+
"minLength": 1
1831918315
},
1832018316
{
1832118317
"type": "null"

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

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Combobox, type ComboboxOption, cn } from '@sim/emcn'
33
import { Plus } from '@sim/emcn/icons'
44
import { useReactFlow } from '@xyflow/react'
55
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
6-
import { resolveAgentDefaultModel } from '@/lib/permission-groups/model-access'
76
import type { SelectorKey } from '@/lib/selectors/manifest'
87
import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state'
98
import { getDependsOnFields } from '@/lib/workflows/subblocks/dependencies'
@@ -22,7 +21,6 @@ import { useDebounce } from '@/hooks/use-debounce'
2221
import { usePermissionConfig } from '@/hooks/use-permission-config'
2322
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
2423
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
25-
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2624

2725
/**
2826
* Constants for ComboBox component behavior
@@ -108,7 +106,6 @@ export const ComboBox = memo(function ComboBox({
108106
const activeSearchTarget = useActiveSearchTarget()
109107
// Hooks and context
110108
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
111-
const blockType = useWorkflowStore((state) => state.blocks[blockId]?.type)
112109
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
113110
const reactFlowInstance = useReactFlow()
114111

@@ -119,11 +116,7 @@ export const ComboBox = memo(function ComboBox({
119116
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
120117

121118
// Permission-based filtering for model dropdowns
122-
const {
123-
config: permissionConfig,
124-
isModelUsable,
125-
isLoading: isPermissionLoading,
126-
} = usePermissionConfig()
119+
const { isModelUsable, isLoading: isPermissionLoading } = usePermissionConfig()
127120

128121
// Evaluate static options if provided as a function
129122
// Derived option lists read the block's own values (a model's valid reasoning efforts);
@@ -303,19 +296,9 @@ export const ComboBox = memo(function ComboBox({
303296

304297
/**
305298
* Determines the default option value to use.
306-
* Priority: group Agent default > explicit defaultValue > DEFAULT_MODEL > first option.
299+
* Priority: explicit defaultValue > DEFAULT_MODEL for model field > first option
307300
*/
308301
const defaultOptionValue = useMemo(() => {
309-
const agentDefault =
310-
config.id === 'model' && blockType === 'agent'
311-
? resolveAgentDefaultModel(permissionConfig)
312-
: null
313-
if (
314-
agentDefault &&
315-
evaluatedOptions.some((option) => getOptionValue(option) === agentDefault)
316-
) {
317-
return agentDefault
318-
}
319302
if (defaultValue !== undefined) {
320303
// Validate that the default value exists in the available (filtered) options
321304
const defaultInOptions = evaluatedOptions.find((opt) => getOptionValue(opt) === defaultValue)
@@ -347,16 +330,7 @@ export const ComboBox = memo(function ComboBox({
347330
}
348331

349332
return undefined
350-
}, [
351-
defaultValue,
352-
evaluatedOptions,
353-
subBlockId,
354-
getOptionValue,
355-
config.emptyIsValid,
356-
config.id,
357-
blockType,
358-
permissionConfig,
359-
])
333+
}, [defaultValue, evaluatedOptions, subBlockId, getOptionValue, config.emptyIsValid])
360334

361335
/**
362336
* Resolve the user-facing text for the current stored value.

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

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

901-
const { resolveSeedGate, agentDefaultModel } = useOperationAccess()
901+
const { resolveSeedGate, agentDefaultModel, isAgentDefaultReady } = useOperationAccess()
902902

903903
const addBlock = useCallback(
904904
(
@@ -913,6 +913,10 @@ const WorkflowContent = React.memo(
913913
triggerMode?: boolean,
914914
presetSubBlockValues?: Record<string, unknown>
915915
) => {
916+
if (type === 'agent' && !isAgentDefaultReady) {
917+
toast({ message: 'Model settings are not ready. Try again.' })
918+
return
919+
}
916920
setPendingSelection([id])
917921
setSelectedEdges(new Map())
918922
pendingFocusBlockIdRef.current = id
@@ -983,6 +987,7 @@ const WorkflowContent = React.memo(
983987
setPendingSelection,
984988
resolveSeedGate,
985989
agentDefaultModel,
990+
isAgentDefaultReady,
986991
]
987992
)
988993

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

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
'use client'
22

3-
import { useId, useMemo, useState } from 'react'
3+
import { useMemo, useState } from 'react'
44
import { ChipCombobox, type ComboboxOption } from '@sim/emcn'
5+
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
56
import type { PermissionGroupConfig } from '@/lib/permission-groups/fields'
6-
import { createModelAccessGate } from '@/lib/permission-groups/model-access'
7+
import {
8+
createModelAccessGate,
9+
resolveAgentDefaultModel,
10+
} from '@/lib/permission-groups/model-access'
711
import { SettingRow } from '@/ee/components/setting-row'
812
import { useProviderModelLists } from '@/hooks/queries/providers'
913
import {
1014
DYNAMIC_MODEL_PROVIDERS,
1115
getModelSunsetStatus,
1216
getProviderIcon,
1317
getProviderModels,
18+
SIM_AUTO_MODEL_ID,
1419
} from '@/providers/models'
1520
import type { ProviderId } from '@/providers/types'
1621

@@ -25,7 +30,7 @@ interface AgentDefaultProps {
2530

2631
/** Selects an allowed default for new Agent blocks governed by this group. */
2732
export function AgentDefault({ config, providerIds, workspaceId, onChange }: AgentDefaultProps) {
28-
const id = useId()
33+
const { hosted } = useDeploymentShape()
2934
const [open, setOpen] = useState(false)
3035
const dynamicModels = useProviderModelLists(
3136
DYNAMIC_MODEL_PROVIDERS.filter((provider) => providerIds.includes(provider)),
@@ -38,8 +43,11 @@ export function AgentDefault({ config, providerIds, workspaceId, onChange }: Age
3843
...providerIds.flatMap((provider) => getProviderModels(provider)),
3944
...dynamicModels,
4045
])
41-
return [
46+
const modelOptions: ComboboxOption[] = [
4247
{ label: 'Platform default', value: PLATFORM_DEFAULT },
48+
...(hosted && isAllowed(SIM_AUTO_MODEL_ID)
49+
? [{ label: 'Auto', value: SIM_AUTO_MODEL_ID }]
50+
: []),
4351
...[...models]
4452
.filter((model) => isAllowed(model) && getModelSunsetStatus(model) !== 'deprecated')
4553
.map((model) => ({
@@ -48,17 +56,28 @@ export function AgentDefault({ config, providerIds, workspaceId, onChange }: Age
4856
icon: getProviderIcon(model) ?? undefined,
4957
})),
5058
]
51-
}, [config, providerIds, dynamicModels])
59+
const savedModel = config.defaultAgentModel
60+
if (savedModel && !modelOptions.some((option) => option.value === savedModel)) {
61+
modelOptions.push({
62+
label: savedModel,
63+
value: savedModel,
64+
icon: getProviderIcon(savedModel) ?? undefined,
65+
hidden: true,
66+
})
67+
}
68+
return modelOptions
69+
}, [config, providerIds, dynamicModels, hosted])
5270

5371
const error =
54-
config.defaultAgentModel && !createModelAccessGate(config)(config.defaultAgentModel)
55-
? 'Agent default must be an allowed model'
72+
config.defaultAgentModel &&
73+
!resolveAgentDefaultModel(config, { allowAuto: hosted, availableProviderIds: providerIds })
74+
? 'Agent default must be an available, allowed model'
5675
: undefined
5776

5877
return (
59-
<SettingRow label='Agent default' htmlFor={id} error={error}>
78+
<SettingRow label='Agent default' error={error}>
6079
<ChipCombobox
61-
id={id}
80+
aria-label='Agent default'
6281
options={options}
6382
value={config.defaultAgentModel ?? PLATFORM_DEFAULT}
6483
onChange={(model) => onChange(model === PLATFORM_DEFAULT ? null : model)}

‎apps/sim/ee/access-control/components/group-detail.tsx‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { useQueryState } from 'nuqs'
3232
import { saveDiscardActions } from '@/components/settings/save-discard-actions'
3333
import { useSettingsUnsavedGuard } from '@/components/settings/use-settings-unsaved-guard'
3434
import type { ShareAuthType } from '@/lib/api/contracts/public-shares'
35+
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
3536
import { isAccessControlAllowlistRow } from '@/lib/permission-groups/block-access'
3637
import {
3738
isFeatureInertForGroup,
@@ -720,6 +721,7 @@ export function GroupDetail({
720721
onBack,
721722
onDeleted,
722723
}: GroupDetailProps) {
724+
const { hosted } = useDeploymentShape()
723725
const updatePermissionGroup = useUpdatePermissionGroup()
724726
const deletePermissionGroup = useDeletePermissionGroup()
725727
const removeMember = useRemovePermissionGroupMember()
@@ -824,6 +826,12 @@ export function GroupDetail({
824826
const blacklist = blacklistedProviders.data.blacklistedProviders
825827
return getAllProviderIds().filter((id) => !blacklist.includes(id.toLowerCase()))
826828
}, [blacklistedProviders.data, blacklistedProviders.isSuccess])
829+
const isAgentDefaultValid =
830+
!editingConfig.defaultAgentModel ||
831+
!!resolveAgentDefaultModel(editingConfig, {
832+
allowAuto: hosted,
833+
availableProviderIds: allProviderIds,
834+
})
827835

828836
/** Maps every tool id to ALL block types that expose it (some tools are shared across blocks). */
829837
const toolBlockTypes = useMemo(() => {
@@ -1281,11 +1289,7 @@ export function GroupDetail({
12811289

12821290
/** Persists the editing buffer — name/description are only sent when they changed. */
12831291
const handleSaveConfig = async () => {
1284-
if (
1285-
!trimmedName ||
1286-
(editingConfig.defaultAgentModel && !resolveAgentDefaultModel(editingConfig))
1287-
)
1288-
return
1292+
if (!trimmedName || !isAgentDefaultValid) return
12891293
try {
12901294
const result = await updatePermissionGroup.mutateAsync({
12911295
id: viewingGroup.id,
@@ -1483,9 +1487,7 @@ export function GroupDetail({
14831487
saving: updatePermissionGroup.isPending,
14841488
onSave: handleSaveConfig,
14851489
onDiscard: handleDiscardConfig,
1486-
saveDisabled:
1487-
!trimmedName ||
1488-
!!(editingConfig.defaultAgentModel && !resolveAgentDefaultModel(editingConfig)),
1490+
saveDisabled: !trimmedName || !isAgentDefaultValid,
14891491
}),
14901492
{
14911493
id: 'delete',

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

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

33
import { useMemo } from 'react'
4+
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
45
import { resolveAgentDefaultModel } from '@/lib/permission-groups/model-access'
56
import {
67
collectDeniedOperationIds,
@@ -12,10 +13,13 @@ import {
1213
pickDefaultOperation,
1314
type SeedValueGate,
1415
} from '@/lib/permission-groups/operation-access'
16+
import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers'
1517
import { usePermissionConfig } from '@/hooks/use-permission-config'
18+
import { PROVIDER_DEFINITIONS } from '@/providers/models'
1619

1720
export interface OperationAccess {
1821
agentDefaultModel: string | null
22+
isAgentDefaultReady: boolean
1923
/**
2024
* Whether the permission config is still loading. Every list this module
2125
* filters reads as unrestricted until it resolves, so a surface that
@@ -73,12 +77,24 @@ export interface OperationAccess {
7377
* canvas search, block creation — agrees.
7478
*/
7579
export function useOperationAccess(): OperationAccess {
76-
const { config, isToolAllowed, isModelUsable, isLoading } = usePermissionConfig()
80+
const { hosted } = useDeploymentShape()
81+
const { config, isToolAllowed, isModelUsable, isLoading, isPermissionFetching } =
82+
usePermissionConfig()
83+
const blacklistedProviders = useBlacklistedProviders()
7784

7885
return useMemo(() => {
7986
const isReady = !isLoading
87+
const isAgentDefaultReady = isReady && !isPermissionFetching && blacklistedProviders.isSuccess
8088
return {
81-
agentDefaultModel: isReady ? resolveAgentDefaultModel(config) : null,
89+
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,
8298
isPermissionLoading: isLoading,
8399
getDeniedOperations: (block, operationIds) =>
84100
isReady
@@ -98,5 +114,14 @@ export function useOperationAccess(): OperationAccess {
98114
: isModelUsable(value)
99115
},
100116
}
101-
}, [config, isToolAllowed, isModelUsable, isLoading])
117+
}, [
118+
config,
119+
isToolAllowed,
120+
isModelUsable,
121+
isLoading,
122+
isPermissionFetching,
123+
hosted,
124+
blacklistedProviders.isSuccess,
125+
blacklistedProviders.data,
126+
])
102127
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { useIntegrationAvailability } from '@/hooks/queries/integration-availabi
2828
export interface PermissionConfigResult {
2929
config: PermissionGroupConfig
3030
isLoading: boolean
31+
isPermissionFetching: boolean
3132
isInPermissionGroup: boolean
3233
filterBlocks: <T extends { type: string }>(blocks: T[]) => T[]
3334
filterProviders: (providerIds: string[]) => string[]
@@ -58,8 +59,11 @@ export function usePermissionConfig(): PermissionConfigResult {
5859
const blockOverlayVersion = useCustomBlockOverlayVersion()
5960
const hostContext = useOptionalWorkspaceHostContext()
6061

61-
const { data: permissionData, isLoading: isPermissionLoading } =
62-
useUserPermissionConfig(workspaceId)
62+
const {
63+
data: permissionData,
64+
isLoading: isPermissionLoading,
65+
isFetching: isPermissionFetching,
66+
} = useUserPermissionConfig(workspaceId)
6367
const {
6468
data: envAllowlistData,
6569
isLoading: isEnvAllowlistLoading,
@@ -226,6 +230,7 @@ export function usePermissionConfig(): PermissionConfigResult {
226230
() => ({
227231
config: mergedConfig,
228232
isLoading,
233+
isPermissionFetching,
229234
isInPermissionGroup,
230235
filterBlocks,
231236
filterProviders,
@@ -246,6 +251,7 @@ export function usePermissionConfig(): PermissionConfigResult {
246251
[
247252
mergedConfig,
248253
isLoading,
254+
isPermissionFetching,
249255
isInPermissionGroup,
250256
filterBlocks,
251257
filterProviders,

0 commit comments

Comments
 (0)