Skip to content

Commit c53367c

Browse files
authored
feat(permission-groups): add agent default (#8771)
* feat(permission-groups): add agent default * fix(permission-groups): validate defaults before seeding agents * fix(permission-groups): normalize defaults and recover policy reads
1 parent 1f1e25d commit c53367c

29 files changed

Lines changed: 594 additions & 81 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +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+
# 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.
8083
bun run check:audits || { echo "❌ audit(s) failed — do not ship"; exit 1; }
8184
bun run type-check || { echo "❌ type-check failed — do not ship"; exit 1; }
8285
# CI's "Verify docs manifest is in sync" step is not a `check:*` script, so the runner above

‎apps/docs/content/docs/cli/permission-groups.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ Create Permission Group (OAuth login or personal API key required)
119119
| `--organization <value>` | Yes | Organization identifier. |
120120
| `--name <value>` | Yes | Group name, unique within the organization. |
121121
| `--description <value>` | No | Optional group description. |
122-
| `--config <json\|@file>` | No | Permission restrictions to set. Omitted keys use the default permission configuration. (JSON, or @path / @- to read a file or stdin). |
122+
| `--config <json\|@file>` | No | Group settings to set. Omitted keys use the default permission configuration. (JSON, or @path / @- to read a file or stdin). |
123123
| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
124124
| `--no-default` | No | Send --default as false. |
125125
| `--workspace-ids <json\|@file>` | No | Workspace IDs targeted by a non-default group. Required when creating a non-default group; omit for a default group. (JSON, or @path / @- to read a file or stdin). |
@@ -232,7 +232,7 @@ Update Permission Group (OAuth login or personal API key required)
232232
| `--organization <value>` | Yes | Organization identifier. |
233233
| `--name <value>` | No | Group name, unique within the organization. |
234234
| `--description <value>` | No | Group description. Null or an empty string clears it; omission leaves it unchanged. (--description null sends the word, not JSON null). |
235-
| `--config <json\|@file>` | No | Patch of permission restrictions. Omitted keys remain unchanged; each supplied array replaces that entire list. (JSON, or @path / @- to read a file or stdin). |
235+
| `--config <json\|@file>` | No | Patch of group settings. Omitted keys remain unchanged; each supplied array replaces that entire list. (JSON, or @path / @- to read a file or stdin). |
236236
| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
237237
| `--no-default` | No | Send --default as false. |
238238
| `--workspace-ids <json\|@file>` | No | Workspace identifiers for a non-default group. Required on creation; an empty update makes the group inactive. (JSON, or @path / @- to read a file or stdin). |

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3794,7 +3794,7 @@ sim permission-groups create [options]
37943794
| `--organization <value>` | Yes | Organization identifier. |
37953795
| `--name <value>` | Yes | Group name, unique within the organization. |
37963796
| `--description <value>` | No | Optional group description. |
3797-
| `--config <json\|@file>` | No | Permission restrictions to set. Omitted keys use the default permission configuration. (JSON, or @path / @- to read a file or stdin). |
3797+
| `--config <json\|@file>` | No | Group settings to set. Omitted keys use the default permission configuration. (JSON, or @path / @- to read a file or stdin). |
37983798
| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
37993799
| `--no-default` | No | Send --default as false. |
38003800
| `--workspace-ids <json\|@file>` | No | Workspace IDs targeted by a non-default group. Required when creating a non-default group; omit for a default group. (JSON, or @path / @- to read a file or stdin). |
@@ -3907,7 +3907,7 @@ sim permission-groups update <groupId> [options]
39073907
| `--organization <value>` | Yes | Organization identifier. |
39083908
| `--name <value>` | No | Group name, unique within the organization. |
39093909
| `--description <value>` | No | Group description. Null or an empty string clears it; omission leaves it unchanged. (--description null sends the word, not JSON null). |
3910-
| `--config <json\|@file>` | No | Patch of permission restrictions. Omitted keys remain unchanged; each supplied array replaces that entire list. (JSON, or @path / @- to read a file or stdin). |
3910+
| `--config <json\|@file>` | No | Patch of group settings. Omitted keys remain unchanged; each supplied array replaces that entire list. (JSON, or @path / @- to read a file or stdin). |
39113911
| `--default` | No | Whether the group is the organization default. Only one group can be the default. |
39123912
| `--no-default` | No | Send --default as false. |
39133913
| `--workspace-ids <json\|@file>` | No | Workspace identifiers for a non-default group. Required on creation; an empty update makes the group inactive. (JSON, or @path / @- to read a file or stdin). |

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

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5017,7 +5017,7 @@
50175017
"get": {
50185018
"operationId": "getPermissionGroup",
50195019
"summary": "Get Permission Group",
5020-
"description": "Get a permission group and its resolved restrictions. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
5020+
"description": "Get a permission group and its resolved settings. Requires organization admin or owner access and active Access Control. Workspace API keys return `403`; use a personal API key or scoped OAuth token.\n\nOAuth scope: `api:read`.",
50215021
"x-sim-operation": "permission_groups.read",
50225022
"x-oauth-scope": "api:read",
50235023
"tags": ["Permission Groups"],
@@ -16026,6 +16026,19 @@
1602616026
"maxLength": 255
1602716027
},
1602816028
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
16029+
},
16030+
"defaultAgentModel": {
16031+
"default": null,
16032+
"anyOf": [
16033+
{
16034+
"type": "string",
16035+
"minLength": 1
16036+
},
16037+
{
16038+
"type": "null"
16039+
}
16040+
],
16041+
"description": "Default model for new Agent blocks. Null uses the platform default."
1602916042
}
1603016043
},
1603116044
"required": [
@@ -16071,10 +16084,11 @@
1607116084
"hideSandboxesTab",
1607216085
"disableOAuthAppAccess",
1607316086
"disableKnowledgeBaseExport",
16074-
"deniedPartialAccessProjectIssues"
16087+
"deniedPartialAccessProjectIssues",
16088+
"defaultAgentModel"
1607516089
],
1607616090
"additionalProperties": false,
16077-
"description": "Resolved restrictions. True disables a boolean capability; null allowlists permit every value and empty allowlists permit none."
16091+
"description": "Resolved group settings. True disables a boolean capability; null allowlists permit every value and empty allowlists permit none. defaultAgentModel selects the default for new Agent blocks without restricting model selection."
1607816092
},
1607916093
"isDefault": {
1608016094
"type": "boolean",
@@ -16123,7 +16137,7 @@
1612316137
],
1612416138
"additionalProperties": false,
1612516139
"title": "Permission group",
16126-
"description": "An organization permission group and its resolved restrictions."
16140+
"description": "An organization permission group and its resolved settings."
1612716141
},
1612816142
"ListPermissionGroupsResponse": {
1612916143
"type": "object",
@@ -16202,7 +16216,8 @@
1620216216
"hideSandboxesTab": false,
1620316217
"disableOAuthAppAccess": false,
1620416218
"disableKnowledgeBaseExport": false,
16205-
"deniedPartialAccessProjectIssues": []
16219+
"deniedPartialAccessProjectIssues": [],
16220+
"defaultAgentModel": null
1620616221
},
1620716222
"isDefault": false,
1620816223
"membershipMode": "inherit",
@@ -16278,7 +16293,8 @@
1627816293
"hideSandboxesTab": false,
1627916294
"disableOAuthAppAccess": false,
1628016295
"disableKnowledgeBaseExport": false,
16281-
"deniedPartialAccessProjectIssues": []
16296+
"deniedPartialAccessProjectIssues": [],
16297+
"defaultAgentModel": null
1628216298
},
1628316299
"isDefault": false,
1628416300
"membershipMode": "inherit",
@@ -16541,10 +16557,22 @@
1654116557
"maxLength": 255
1654216558
},
1654316559
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
16560+
},
16561+
"defaultAgentModel": {
16562+
"anyOf": [
16563+
{
16564+
"type": "string",
16565+
"minLength": 1
16566+
},
16567+
{
16568+
"type": "null"
16569+
}
16570+
],
16571+
"description": "Default model for new Agent blocks. Null uses the platform default."
1654416572
}
1654516573
},
1654616574
"additionalProperties": false,
16547-
"description": "Permission restrictions to set. Omitted keys use the default permission configuration."
16575+
"description": "Group settings to set. Omitted keys use the default permission configuration."
1654816576
},
1654916577
"isDefault": {
1655016578
"description": "Whether the group is the organization default. Only one group can be the default.",
@@ -16633,7 +16661,8 @@
1663316661
"hideSandboxesTab": false,
1663416662
"disableOAuthAppAccess": false,
1663516663
"disableKnowledgeBaseExport": false,
16636-
"deniedPartialAccessProjectIssues": []
16664+
"deniedPartialAccessProjectIssues": [],
16665+
"defaultAgentModel": null
1663716666
},
1663816667
"isDefault": false,
1663916668
"membershipMode": "inherit",
@@ -16707,7 +16736,8 @@
1670716736
"hideSandboxesTab": false,
1670816737
"disableOAuthAppAccess": false,
1670916738
"disableKnowledgeBaseExport": false,
16710-
"deniedPartialAccessProjectIssues": []
16739+
"deniedPartialAccessProjectIssues": [],
16740+
"defaultAgentModel": null
1671116741
},
1671216742
"isDefault": false,
1671316743
"membershipMode": "inherit",
@@ -16977,10 +17007,22 @@
1697717007
"maxLength": 255
1697817008
},
1697917009
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
17010+
},
17011+
"defaultAgentModel": {
17012+
"anyOf": [
17013+
{
17014+
"type": "string",
17015+
"minLength": 1
17016+
},
17017+
{
17018+
"type": "null"
17019+
}
17020+
],
17021+
"description": "Default model for new Agent blocks. Null uses the platform default."
1698017022
}
1698117023
},
1698217024
"additionalProperties": false,
16983-
"description": "Patch of permission restrictions. Omitted keys remain unchanged; each supplied array replaces that entire list."
17025+
"description": "Patch of group settings. Omitted keys remain unchanged; each supplied array replaces that entire list."
1698417026
},
1698517027
"isDefault": {
1698617028
"description": "Whether the group is the organization default. Only one group can be the default.",
@@ -18263,6 +18305,19 @@
1826318305
"maxLength": 255
1826418306
},
1826518307
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
18308+
},
18309+
"defaultAgentModel": {
18310+
"default": null,
18311+
"anyOf": [
18312+
{
18313+
"type": "string",
18314+
"minLength": 1
18315+
},
18316+
{
18317+
"type": "null"
18318+
}
18319+
],
18320+
"description": "Default model for new Agent blocks. Null uses the platform default."
1826618321
}
1826718322
},
1826818323
"required": [
@@ -18308,15 +18363,16 @@
1830818363
"hideSandboxesTab",
1830918364
"disableOAuthAppAccess",
1831018365
"disableKnowledgeBaseExport",
18311-
"deniedPartialAccessProjectIssues"
18366+
"deniedPartialAccessProjectIssues",
18367+
"defaultAgentModel"
1831218368
],
1831318369
"additionalProperties": false
1831418370
},
1831518371
{
1831618372
"type": "null"
1831718373
}
1831818374
],
18319-
"description": "Effective group restrictions. True disables a boolean capability; null allowlists allow all values and empty allowlists allow none. Null config means no group applies."
18375+
"description": "Effective group settings. True disables a boolean capability; null allowlists allow all values and empty allowlists allow none. Null config means no group applies."
1832018376
},
1832118377
"entitled": {
1832218378
"type": "boolean",

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

Lines changed: 17 additions & 2 deletions
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 } = useOperationAccess()
901+
const { resolveSeedGate, agentDefaultModel, isAgentDefaultReady, refetchModelSettings } =
902+
useOperationAccess()
902903

903904
const addBlock = useCallback(
904905
(
@@ -913,6 +914,11 @@ const WorkflowContent = React.memo(
913914
triggerMode?: boolean,
914915
presetSubBlockValues?: Record<string, unknown>
915916
) => {
917+
if (type === 'agent' && !isAgentDefaultReady) {
918+
void refetchModelSettings({ cancelRefetch: false })
919+
toast({ message: 'Model settings are not ready. Try again.' })
920+
return
921+
}
916922
setPendingSelection([id])
917923
setSelectedEdges(new Map())
918924
pendingFocusBlockIdRef.current = id
@@ -933,6 +939,7 @@ const WorkflowContent = React.memo(
933939
extent,
934940
triggerMode,
935941
isSeededValueAllowed: seedGate,
942+
agentDefaultModel,
936943
})
937944

938945
const subBlockValues: Record<string, Record<string, unknown>> = {}
@@ -976,7 +983,15 @@ const WorkflowContent = React.memo(
976983
)
977984
usePanelEditorStore.getState().setCurrentBlockId(id)
978985
},
979-
[collaborativeBatchAddBlocks, setSelectedEdges, setPendingSelection, resolveSeedGate]
986+
[
987+
collaborativeBatchAddBlocks,
988+
setSelectedEdges,
989+
setPendingSelection,
990+
resolveSeedGate,
991+
agentDefaultModel,
992+
isAgentDefaultReady,
993+
refetchModelSettings,
994+
]
980995
)
981996

982997
const { activeBlockIds, pendingBlocks, isDebugging, isExecuting } = useExecutionStore(
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use client'
2+
3+
import { useMemo, useState } from 'react'
4+
import { ChipCombobox, type ComboboxOption } from '@sim/emcn'
5+
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
6+
import type { PermissionGroupConfig } from '@/lib/permission-groups/fields'
7+
import {
8+
createModelAccessGate,
9+
resolveAgentDefaultModel,
10+
} from '@/lib/permission-groups/model-access'
11+
import { SettingRow } from '@/ee/components/setting-row'
12+
import { useProviderModelLists } from '@/hooks/queries/providers'
13+
import {
14+
DYNAMIC_MODEL_PROVIDERS,
15+
getModelSunsetStatus,
16+
getProviderIcon,
17+
getProviderModels,
18+
SIM_AUTO_MODEL_ID,
19+
} from '@/providers/models'
20+
import type { ProviderId } from '@/providers/types'
21+
22+
const PLATFORM_DEFAULT = '__platform-default__'
23+
24+
function getDefaultModelId(provider: string, model: string): string {
25+
return provider === 'ollama' && !model.toLowerCase().startsWith('ollama/')
26+
? `ollama/${model}`
27+
: model
28+
}
29+
30+
interface AgentDefaultProps {
31+
config: PermissionGroupConfig
32+
providerIds: ProviderId[]
33+
workspaceId?: string
34+
onChange: (model: string | null) => void
35+
}
36+
37+
/** Selects an allowed default for new Agent blocks governed by this group. */
38+
export function AgentDefault({ config, providerIds, workspaceId, onChange }: AgentDefaultProps) {
39+
const { hosted } = useDeploymentShape()
40+
const [open, setOpen] = useState(false)
41+
const dynamicModels = useProviderModelLists(
42+
DYNAMIC_MODEL_PROVIDERS.filter((provider) => providerIds.includes(provider)),
43+
workspaceId,
44+
{ enabled: open }
45+
)
46+
const options = useMemo((): ComboboxOption[] => {
47+
const isAllowed = createModelAccessGate(config)
48+
const models = new Set([
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+
),
55+
])
56+
const modelOptions: ComboboxOption[] = [
57+
{ label: 'Platform default', value: PLATFORM_DEFAULT },
58+
...(hosted && isAllowed(SIM_AUTO_MODEL_ID)
59+
? [{ label: 'Auto', value: SIM_AUTO_MODEL_ID }]
60+
: []),
61+
...[...models]
62+
.filter((model) => isAllowed(model) && getModelSunsetStatus(model) !== 'deprecated')
63+
.map((model) => ({
64+
label: model,
65+
value: model,
66+
icon: getProviderIcon(model) ?? undefined,
67+
})),
68+
]
69+
const savedModel = config.defaultAgentModel
70+
if (savedModel && !modelOptions.some((option) => option.value === savedModel)) {
71+
modelOptions.push({
72+
label: savedModel,
73+
value: savedModel,
74+
icon: getProviderIcon(savedModel) ?? undefined,
75+
hidden: true,
76+
})
77+
}
78+
return modelOptions
79+
}, [config, providerIds, dynamicModels, hosted])
80+
81+
const error =
82+
config.defaultAgentModel &&
83+
!resolveAgentDefaultModel(config, { allowAuto: hosted, availableProviderIds: providerIds })
84+
? 'Agent default must be an available, allowed model'
85+
: undefined
86+
87+
return (
88+
<SettingRow label='Agent default' error={error}>
89+
<ChipCombobox
90+
aria-label='Agent default'
91+
options={options}
92+
value={config.defaultAgentModel ?? PLATFORM_DEFAULT}
93+
onChange={(model) => onChange(model === PLATFORM_DEFAULT ? null : model)}
94+
placeholder='Select a model'
95+
searchable
96+
error={error}
97+
onOpenChange={setOpen}
98+
className='w-full'
99+
/>
100+
</SettingRow>
101+
)
102+
}

0 commit comments

Comments
 (0)