Skip to content

Commit 33577f4

Browse files
committed
feat(agent): add variable permission modes behind a feature flag
1 parent a483bfa commit 33577f4

25 files changed

Lines changed: 824 additions & 46 deletions

File tree

apps/sim/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic
203203
# FORKING_ENABLED= # Workspace forks
204204
# CREDENTIAL_GROUPS= # Enterprise managed OAuth collections
205205
# TABLE_ROW_TTL= # Table TTL columns and expired-row cleanup
206+
# AGENT_TOOL_PERMISSION_MODE=false # Variable-capable agent tool Permission Mode editor
206207
# KNOWLEDGE_MEMBER_ACCESS= # Per-member knowledge connectors and hybrid-by-default retrieval
207208
# ORGANIZATIONS_ENABLED= / NEXT_PUBLIC_ORGANIZATIONS_ENABLED= # Organizations only
208209

apps/sim/app/workspace/[workspaceId]/layout.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { cookies } from 'next/headers'
33
import { redirect } from 'next/navigation'
44
import { getSession } from '@/lib/auth'
55
import { getActiveOrganizationId } from '@/lib/auth/session-response'
6+
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
67
import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability'
78
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
89
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner'
@@ -46,25 +47,32 @@ export default async function WorkspaceLayout({
4647
}
4748

4849
const activeOrganizationId = getActiveOrganizationId(session)
49-
const [cookieStore, initialOrgSettings, , tableRowTtlEnabled] = await Promise.all([
50-
cookies(),
51-
hostContext.hostOrganizationId
52-
? getOrgWhitelabelSettings(hostContext.hostOrganizationId)
53-
: Promise.resolve(null),
54-
prefetchWorkspaceSidebar(
55-
queryClient,
56-
workspaceId,
57-
session.user.id,
58-
hostContext,
59-
activeOrganizationId
60-
),
61-
isTableRowTtlEnabled(),
62-
])
50+
const [cookieStore, initialOrgSettings, , tableRowTtlEnabled, agentToolPermissionModeEnabled] =
51+
await Promise.all([
52+
cookies(),
53+
hostContext.hostOrganizationId
54+
? getOrgWhitelabelSettings(hostContext.hostOrganizationId)
55+
: Promise.resolve(null),
56+
prefetchWorkspaceSidebar(
57+
queryClient,
58+
workspaceId,
59+
session.user.id,
60+
hostContext,
61+
activeOrganizationId
62+
),
63+
isTableRowTtlEnabled(),
64+
isFeatureEnabled('agent-tool-permission-mode'),
65+
])
6366
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'
6467

6568
return (
6669
<HydrationBoundary state={dehydrate(queryClient)}>
67-
<FeatureFlagsProvider flags={{ 'table-row-ttl': tableRowTtlEnabled }}>
70+
<FeatureFlagsProvider
71+
flags={{
72+
'table-row-ttl': tableRowTtlEnabled,
73+
'agent-tool-permission-mode': agentToolPermissionModeEnabled,
74+
}}
75+
>
6876
<WorkspaceHostProvider workspaceId={workspaceId} initialContext={hostContext}>
6977
<BrandingProvider
7078
hostOrganizationId={hostContext.hostOrganizationId}

apps/sim/app/workspace/[workspaceId]/providers/feature-flags-provider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { createContext, type ReactNode, useContext } from 'react'
44

55
export interface WorkspaceFeatureFlags {
6+
'agent-tool-permission-mode': boolean
67
'table-row-ttl': boolean
78
}
89

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Combobox, Label } from '@sim/emcn'
2+
import type { CanonicalMode } from '@/lib/workflows/subblocks/visibility'
3+
import type { StoredTool } from '@/lib/workflows/tool-input/types'
4+
import { CanonicalModeToggle } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/canonical-mode-toggle'
5+
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input'
6+
7+
interface ToolUsageControlProps {
8+
blockId: string
9+
aggregateSubBlockId: string
10+
toolIndex: number
11+
tool: StoredTool
12+
mode: CanonicalMode
13+
supportsForce: boolean
14+
disabled: boolean
15+
onFixedChange: (value: NonNullable<StoredTool['usageControl']>) => void
16+
onExpressionChange: (value: string) => void
17+
onModeToggle: () => void
18+
}
19+
20+
const MODE_OPTIONS = [
21+
{
22+
value: 'auto',
23+
label: 'Auto',
24+
suffixElement: <span className='text-[var(--text-tertiary)]'>(model decides)</span>,
25+
},
26+
{
27+
value: 'force',
28+
label: 'Force',
29+
suffixElement: <span className='text-[var(--text-tertiary)]'>(always use)</span>,
30+
},
31+
{
32+
value: 'none',
33+
label: 'None',
34+
suffixElement: <span className='text-[var(--text-tertiary)]'>(disable tool)</span>,
35+
},
36+
] as const
37+
38+
export function ToolUsageControl({
39+
blockId,
40+
aggregateSubBlockId,
41+
toolIndex,
42+
tool,
43+
mode,
44+
supportsForce,
45+
disabled,
46+
onFixedChange,
47+
onExpressionChange,
48+
onModeToggle,
49+
}: ToolUsageControlProps) {
50+
return (
51+
<div className='subblock-content flex w-full min-w-0 flex-col gap-2.5'>
52+
<div className='flex items-center justify-between gap-1.5 pl-0.5'>
53+
<Label>Permission Mode</Label>
54+
<CanonicalModeToggle mode={mode} disabled={disabled} onToggle={onModeToggle} />
55+
</div>
56+
{mode === 'advanced' ? (
57+
<ShortInput
58+
blockId={blockId}
59+
subBlockId={aggregateSubBlockId}
60+
config={{
61+
id: 'usageControlExpression',
62+
title: 'Permission Mode',
63+
type: 'short-input',
64+
}}
65+
value={tool.usageControlExpression ?? ''}
66+
onChange={onExpressionChange}
67+
placeholder='"auto", "force", or "none"'
68+
disabled={disabled}
69+
workflowSearchValuePath={[toolIndex, 'usageControlExpression']}
70+
/>
71+
) : (
72+
<Combobox
73+
options={MODE_OPTIONS.map((option) => ({
74+
...option,
75+
disabled: option.value === 'force' && !supportsForce,
76+
suffixElement:
77+
option.value === 'force' && !supportsForce ? (
78+
<span className='text-[var(--text-tertiary)]'>(not supported by model)</span>
79+
) : (
80+
option.suffixElement
81+
),
82+
onSelect: () => onFixedChange(option.value),
83+
}))}
84+
value={tool.usageControl ?? 'auto'}
85+
disabled={disabled}
86+
aria-label='Permission Mode'
87+
/>
88+
)}
89+
</div>
90+
)
91+
}

0 commit comments

Comments
 (0)