Skip to content

Commit fcbccd2

Browse files
icecrasher321claude
andcommitted
fix(tools): require every input the caller is the only source for
The previous commit gated the check on `user-only`, which reads the visibility taxonomy as if it constrained who may send a value. It does not. `visibility` describes editor roles — a human filling a block field, the agent block's model choosing an argument, either, or neither — and a direct call has no editor and no agent block, so those roles collapse to one caller. `createUserToolSchema`, which this endpoint and Copilot's `call_integration_tool` both publish, already says so by omitting `hidden` and nothing else. So the rule is not about roles: Sim supplies it, or the caller must. Skipping `hidden` stays safe because `check-tool-param-reachability` fails any required hidden parameter without a declared filler. Concretely this closes `thinking_tool.thought`, the one required `llm-only` parameter in the registry, which the narrower check let through as `undefined`. It also moves required `user-or-llm` inputs to a pre-dispatch failure naming every missing field at once, rather than the merge validator's first-failure mid-execution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ee22f27 commit fcbccd2

2 files changed

Lines changed: 57 additions & 14 deletions

File tree

apps/sim/lib/tool-execution/application/execute-tool.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ const TOOL_METADATA: Record<string, Record<string, unknown>> = {
111111
},
112112
hosting: { apiKeyParam: 'apiKey' },
113113
},
114+
thinking_tool: {
115+
id: 'thinking_tool',
116+
name: 'Thinking',
117+
params: { thought: { type: 'string', required: true, visibility: 'llm-only' } },
118+
},
114119
zendesk_get_ticket: {
115120
id: 'zendesk_get_ticket',
116121
name: 'Zendesk Get Ticket',
@@ -167,6 +172,7 @@ const previewBlock = block({
167172
tools: { access: ['preview_call'] },
168173
})
169174
const zendeskBlock = block({ type: 'zendesk', tools: { access: ['zendesk_get_ticket'] } })
175+
const thinkingBlock = block({ type: 'thinking', tools: { access: ['thinking_tool'] } })
170176
const confluenceBlock = block({
171177
type: 'confluence_v2',
172178
tools: { access: ['confluence_read_v2'] },
@@ -203,6 +209,7 @@ describe('executeToolForCaller', () => {
203209
previewBlock,
204210
confluenceBlock,
205211
zendeskBlock,
212+
thinkingBlock,
206213
])
207214
mocks.executeRegistryTool.mockResolvedValue({ success: true, output: { markdown: '# Hi' } })
208215
mocks.resolveBillingAttribution.mockResolvedValue({ workspaceId: WORKSPACE_ID })
@@ -342,6 +349,27 @@ describe('executeToolForCaller', () => {
342349
})
343350
})
344351

352+
/**
353+
* `visibility` describes editor roles, and a direct call has no editor: the
354+
* caller is the only source, so an `llm-only` parameter is as much theirs to
355+
* send as a `user-only` one. Gating the check on `user-only` alone left
356+
* `thinking_tool.thought` dispatching as `undefined`.
357+
*/
358+
it('refuses a missing llm-only input too — the caller is the only source here', async () => {
359+
await expect(run({ toolId: 'thinking_tool', input: {} })).rejects.toMatchObject({
360+
code: 'validation',
361+
message: expect.stringContaining('input.thought'),
362+
})
363+
expect(mocks.executeRegistryTool).not.toHaveBeenCalled()
364+
})
365+
366+
it('refuses a missing user-or-llm input before dispatch rather than mid-execution', async () => {
367+
await expect(run({ input: {} })).rejects.toMatchObject({
368+
code: 'validation',
369+
message: expect.stringContaining('input.url'),
370+
})
371+
})
372+
345373
it('accepts a {{VAR}} reference as a present value, leaving resolution to the executor', async () => {
346374
await run({
347375
toolId: 'zendesk_get_ticket',

apps/sim/lib/tool-execution/application/execute-tool.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,35 +82,50 @@ function assertNoInlineCredential(args: Record<string, unknown>): void {
8282
}
8383

8484
/**
85-
* Refuses a call missing a required parameter only its caller can supply.
85+
* Refuses a call missing a required parameter the caller was supposed to send.
8686
*
87-
* `validateRequiredParametersAfterMerge` deliberately checks `user-or-llm`
88-
* alone, because on the workflow path a `user-only` parameter was already
89-
* validated during serialization against the block field that holds it. This
90-
* path has no serialization step, so nothing had checked them — a caller
91-
* omitting `zendesk_get_ticket`'s `subdomain` reached Zendesk as `undefined`
92-
* and came back a provider authentication failure, which is the same
93-
* undiagnosable shape this PR set out to remove.
87+
* `visibility` is an editor-role concept: it says whether a value comes from a
88+
* human filling a block field (`user-only`), the agent block's model choosing an
89+
* argument (`llm-only`), either (`user-or-llm`), or neither (`hidden`). A direct
90+
* call has no editor and no agent block, so those roles collapse — the caller is
91+
* the only source there is. `createUserToolSchema`, which is what both this
92+
* endpoint and Copilot's `call_integration_tool` publish, already says as much
93+
* by omitting `hidden` and nothing else.
9494
*
95-
* A parameter Sim itself supplies is not missing: `hosting` fills its
96-
* `apiKeyParam` on a key-hosting deployment, so `firecrawl_scrape` must stay
97-
* callable with no `apiKey`. The condition mirrors `injectHostedKeyIfNeeded`
98-
* exactly — same three tests, same order — so the two cannot disagree about
99-
* whether a value is coming.
95+
* So the rule here is not about roles: **Sim supplies it, or the caller must.**
96+
* `hidden` is skipped because Sim fills it from a resolved credential or a
97+
* hosted key — and `check-tool-param-reachability` is what makes that safe to
98+
* assume, since it fails any required `hidden` parameter without a declared
99+
* filler.
100+
*
101+
* Nothing else had checked these. `validateRequiredParametersAfterMerge` covers
102+
* `user-or-llm` alone, because on the workflow path the rest were validated
103+
* during serialization against the block fields holding them, and this path has
104+
* no serialization step. Omitting `zendesk_get_ticket`'s `subdomain` reached
105+
* Zendesk as `undefined` and came back a provider authentication failure — the
106+
* same undiagnosable shape this branch set out to remove.
100107
*/
101108
function assertRequiredCallerInputsPresent(
102109
tool: ExecutableToolConfig,
103110
toolId: string,
104111
params: Record<string, unknown>
105112
): void {
113+
/**
114+
* Mirrors `injectHostedKeyIfNeeded`'s three tests, in its order, so the two
115+
* cannot disagree about whether a value is coming. Rejecting the omission
116+
* would make every hosted-key tool uncallable without a key the caller does
117+
* not need to hold; accepting it on a deployment that hosts none would
118+
* promise a key nothing supplies.
119+
*/
106120
const hostedKeyParam =
107121
isHosted && tool.hosting && (!tool.hosting.enabled || tool.hosting.enabled(params))
108122
? tool.hosting.apiKeyParam
109123
: undefined
110124

111125
const missing = Object.entries(tool.params ?? {})
112126
.filter(([name, declaration]) => {
113-
if (!declaration?.required || declaration.visibility !== 'user-only') return false
127+
if (!declaration?.required) return false
128+
if (declaration.visibility === 'hidden') return false
114129
if (name === hostedKeyParam) return false
115130
const value = params[name]
116131
return value === undefined || value === null || value === ''

0 commit comments

Comments
 (0)