Skip to content

Commit 95e0607

Browse files
j15zclaude
andauthored
feat(agent): allow variable tool permission modes (#7538)
* feat(agent): allow variable tool permission modes * fix(agent): keep permission modes with reordered tools across API edits Apply each agent tool's Permission Mode after the edit batch reindexes tool canonical modes, so a mode chosen for a tool's final position is not moved again as if it were keyed by the original tool list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(agent): document the tool Permission Mode control Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4ef52fd commit 95e0607

22 files changed

Lines changed: 1123 additions & 108 deletions

File tree

apps/docs/openapi-v2-workflows.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7995,6 +7995,11 @@
79957995
"enum": ["auto", "force", "none"],
79967996
"description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`."
79977997
},
7998+
"usageControlExpression": {
7999+
"type": "string",
8000+
"maxLength": 2048,
8001+
"description": "Variable-capable tool mode value used when the matching canonical mode is `advanced`. It must resolve to `auto`, `force`, or `none` at execution time."
8002+
},
79988003
"params": {
79998004
"type": "object",
80008005
"propertyNames": {
@@ -8041,6 +8046,11 @@
80418046
"type": "string",
80428047
"enum": ["auto", "force", "none"],
80438048
"description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`."
8049+
},
8050+
"usageControlExpression": {
8051+
"type": "string",
8052+
"maxLength": 2048,
8053+
"description": "Variable-capable tool mode value used when the matching canonical mode is `advanced`. It must resolve to `auto`, `force`, or `none` at execution time."
80448054
}
80458055
},
80468056
"required": ["type", "customToolId"],
@@ -8109,6 +8119,11 @@
81098119
"type": "string",
81108120
"enum": ["auto", "force", "none"],
81118121
"description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`."
8122+
},
8123+
"usageControlExpression": {
8124+
"type": "string",
8125+
"maxLength": 2048,
8126+
"description": "Variable-capable tool mode value used when the matching canonical mode is `advanced`. It must resolve to `auto`, `force`, or `none` at execution time."
81128127
}
81138128
},
81148129
"required": ["type", "schema", "code"],
@@ -8174,6 +8189,11 @@
81748189
"type": "string",
81758190
"enum": ["auto", "force", "none"],
81768191
"description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`."
8192+
},
8193+
"usageControlExpression": {
8194+
"type": "string",
8195+
"maxLength": 2048,
8196+
"description": "Variable-capable tool mode value used when the matching canonical mode is `advanced`. It must resolve to `auto`, `force`, or `none` at execution time."
81778197
}
81788198
},
81798199
"required": ["type", "params"],
@@ -8282,6 +8302,11 @@
82828302
"type": "string",
82838303
"enum": ["auto", "force", "none"],
82848304
"description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`."
8305+
},
8306+
"usageControlExpression": {
8307+
"type": "string",
8308+
"maxLength": 2048,
8309+
"description": "Variable-capable tool mode value used when the matching canonical mode is `advanced`. It must resolve to `auto`, `force`, or `none` at execution time."
82858310
}
82868311
},
82878312
"required": ["type", "params"],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Button, ChipCombobox, cn, Label, Tooltip } from '@sim/emcn'
2+
import { ArrowLeftRight } from '@sim/emcn/icons'
3+
import type { CanonicalMode } from '@/lib/workflows/subblocks/visibility'
4+
import type { StoredTool } from '@/lib/workflows/tool-input/types'
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+
/**
39+
* Permission Mode control for one agent tool. Selector mode picks a fixed `usageControl`, and
40+
* Variable mode edits a `usageControlExpression` that must resolve to auto, force, or none.
41+
* Both values are kept so toggling modes does not discard the inactive one.
42+
*/
43+
export function ToolUsageControl({
44+
blockId,
45+
aggregateSubBlockId,
46+
toolIndex,
47+
tool,
48+
mode,
49+
supportsForce,
50+
disabled,
51+
onFixedChange,
52+
onExpressionChange,
53+
onModeToggle,
54+
}: ToolUsageControlProps) {
55+
const toggleLabel = mode === 'advanced' ? 'Switch to selector' : 'Switch to variable'
56+
57+
return (
58+
<div className='subblock-content flex w-full min-w-0 flex-col gap-2.5'>
59+
<div className='flex items-center justify-between gap-1.5 pl-0.5'>
60+
<Label>Permission Mode</Label>
61+
<Tooltip.Root>
62+
<Tooltip.Trigger asChild>
63+
<Button
64+
type='button'
65+
variant='ghost'
66+
size='icon'
67+
className='shrink-0'
68+
onClick={onModeToggle}
69+
disabled={disabled}
70+
aria-label={toggleLabel}
71+
>
72+
<ArrowLeftRight
73+
className={cn(
74+
'size-[12px]!',
75+
mode === 'advanced'
76+
? 'text-[var(--text-primary)]'
77+
: 'text-[var(--text-secondary)]'
78+
)}
79+
/>
80+
</Button>
81+
</Tooltip.Trigger>
82+
<Tooltip.Content side='top'>{toggleLabel}</Tooltip.Content>
83+
</Tooltip.Root>
84+
</div>
85+
{mode === 'advanced' ? (
86+
<ShortInput
87+
blockId={blockId}
88+
subBlockId={aggregateSubBlockId}
89+
config={{
90+
id: 'usageControlExpression',
91+
title: 'Permission Mode',
92+
type: 'short-input',
93+
}}
94+
value={tool.usageControlExpression ?? ''}
95+
onChange={onExpressionChange}
96+
placeholder='"auto", "force", or "none"'
97+
disabled={disabled}
98+
workflowSearchValuePath={[toolIndex, 'usageControlExpression']}
99+
/>
100+
) : (
101+
<ChipCombobox
102+
options={MODE_OPTIONS.map((option) => ({
103+
...option,
104+
disabled: option.value === 'force' && !supportsForce,
105+
suffixElement:
106+
option.value === 'force' && !supportsForce ? (
107+
<span className='text-[var(--text-tertiary)]'>(not supported by model)</span>
108+
) : (
109+
option.suffixElement
110+
),
111+
onSelect: () => onFixedChange(option.value),
112+
}))}
113+
value={tool.usageControl ?? 'auto'}
114+
disabled={disabled}
115+
aria-label='Permission Mode'
116+
/>
117+
)}
118+
</div>
119+
)
120+
}

0 commit comments

Comments
 (0)