Skip to content

Commit 7fe34b7

Browse files
fix(api): bound agent tool inputs
1 parent 2b629cb commit 7fe34b7

4 files changed

Lines changed: 95 additions & 2 deletions

File tree

apps/docs/openapi-v2-workflows.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6197,6 +6197,7 @@
61976197
"description": "One semantic edit against a workflow graph."
61986198
},
61996199
"AgentToolInput": {
6200+
"maxItems": 100,
62006201
"type": "array",
62016202
"items": {
62026203
"$ref": "#/components/schemas/AgentTool"
@@ -6315,6 +6316,7 @@
63156316
"name": {
63166317
"type": "string",
63176318
"minLength": 1,
6319+
"maxLength": 64,
63186320
"description": "Function name presented to the model."
63196321
},
63206322
"description": {
@@ -6387,11 +6389,13 @@
63876389
"serverId": {
63886390
"type": "string",
63896391
"minLength": 1,
6392+
"maxLength": 128,
63906393
"description": "MCP server id returned by `GET /api/v2/mcp-servers`."
63916394
},
63926395
"toolName": {
63936396
"type": "string",
63946397
"minLength": 1,
6398+
"maxLength": 256,
63956399
"description": "Tool name returned by the MCP server’s tools endpoint."
63966400
}
63976401
},

apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5+
import { MAX_ID_LENGTH } from '@/lib/api/contracts/primitives'
56
import {
7+
MAX_AGENT_TOOLS_PER_BLOCK,
68
v2AgentToolInputSchema,
79
v2ApplyWorkflowOperationsBodySchema,
810
} from '@/lib/api/contracts/v2/workflows'
11+
import { MAX_MCP_TOOL_NAME_BYTES } from '@/lib/mcp/constants'
912

1013
describe('v2AgentToolInputSchema', () => {
1114
it('accepts catalog integration, custom-tool reference, and MCP tool shapes', () => {
@@ -57,6 +60,57 @@ describe('v2AgentToolInputSchema', () => {
5760
])('rejects a malformed reserved tool shape', (tools) => {
5861
expect(v2AgentToolInputSchema.safeParse(tools).success).toBe(false)
5962
})
63+
64+
it('rejects a tool list above the workflow-operation ceiling', () => {
65+
const tools = Array.from({ length: MAX_AGENT_TOOLS_PER_BLOCK + 1 }, (_, index) => ({
66+
type: `integration-${index}`,
67+
}))
68+
69+
expect(v2AgentToolInputSchema.safeParse(tools).success).toBe(false)
70+
})
71+
72+
it.each([
73+
[
74+
'inline function name',
75+
{
76+
type: 'custom-tool',
77+
schema: {
78+
type: 'function',
79+
function: { name: 'a'.repeat(65), parameters: { type: 'object' } },
80+
},
81+
code: 'return null',
82+
},
83+
],
84+
[
85+
'MCP server id',
86+
{
87+
type: 'mcp',
88+
params: { serverId: 'a'.repeat(MAX_ID_LENGTH + 1), toolName: 'search_docs' },
89+
},
90+
],
91+
[
92+
'MCP tool name',
93+
{
94+
type: 'mcp',
95+
params: {
96+
serverId: 'mcp_123',
97+
toolName: 'a'.repeat(MAX_MCP_TOOL_NAME_BYTES + 1),
98+
},
99+
},
100+
],
101+
[
102+
'MCP multibyte tool name',
103+
{
104+
type: 'mcp',
105+
params: {
106+
serverId: 'mcp_123',
107+
toolName: '💡'.repeat(Math.floor(MAX_MCP_TOOL_NAME_BYTES / 4) + 1),
108+
},
109+
},
110+
],
111+
])('rejects an overlong %s', (_label, tool) => {
112+
expect(v2AgentToolInputSchema.safeParse([tool]).success).toBe(false)
113+
})
60114
})
61115

62116
describe('workflow operation Agent tools contract', () => {

apps/sim/lib/api/contracts/v2/workflows.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from '@/lib/api/contracts/deployments'
1818
import {
1919
booleanQueryFlagSchema,
20+
MAX_ID_LENGTH,
2021
missingFieldError,
2122
noInputSchema,
2223
runIdSchema,
@@ -58,6 +59,7 @@ import {
5859
import { MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS } from '@/lib/billing/execution-timeout-defaults'
5960
import { MAX_INLINE_MATERIALIZATION_BYTES } from '@/lib/execution/payloads/limits'
6061
import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types'
62+
import { MAX_MCP_TOOL_NAME_BYTES } from '@/lib/mcp/constants'
6163
import { WORKFLOW_SKIPPED_ITEM_TYPES } from '@/lib/workflows/editing/types'
6264

6365
export const V2_WORKFLOW_RUN_ID_HEADER = 'X-Run-Id'
@@ -2053,6 +2055,8 @@ export const MAX_WORKFLOW_GRAPH_BLOCKS = 2000
20532055
export const MAX_WORKFLOW_GRAPH_EDGES = 10_000
20542056
/** Ceiling on one `POST /operations` batch. */
20552057
export const MAX_WORKFLOW_EDIT_OPERATIONS = 200
2058+
/** Ceiling on the complete tool list assigned to one Agent block. */
2059+
export const MAX_AGENT_TOOLS_PER_BLOCK = 100
20562060
/** Ceiling on one `PATCH /variables` batch; mirrors the application use case's own cap. */
20572061
export const MAX_WORKFLOW_VARIABLE_OPERATIONS = 100
20582062
/** Ceiling on one bulk move; mirrors the application use case's own cap. */
@@ -2727,6 +2731,7 @@ const v2AgentInlineCustomToolSchema = z
27272731
.string()
27282732
.trim()
27292733
.min(1, 'Inline custom tool function name cannot be empty')
2734+
.max(64, 'Inline custom tool function name must be at most 64 characters')
27302735
.describe('Function name presented to the model.'),
27312736
description: z.string().optional().describe('What the inline custom tool does.'),
27322737
parameters: z
@@ -2779,11 +2784,21 @@ export const v2AgentMcpToolSchema = z
27792784
.string()
27802785
.trim()
27812786
.min(1, 'Agent MCP serverId cannot be empty')
2787+
.max(MAX_ID_LENGTH, `Agent MCP serverId must be at most ${MAX_ID_LENGTH} characters`)
27822788
.describe('MCP server id returned by `GET /api/v2/mcp-servers`.'),
27832789
toolName: z
27842790
.string()
27852791
.trim()
27862792
.min(1, 'Agent MCP toolName cannot be empty')
2793+
.max(
2794+
MAX_MCP_TOOL_NAME_BYTES,
2795+
`Agent MCP toolName must be at most ${MAX_MCP_TOOL_NAME_BYTES} characters`
2796+
)
2797+
.refine(
2798+
(toolName) =>
2799+
new TextEncoder().encode(toolName).byteLength <= MAX_MCP_TOOL_NAME_BYTES,
2800+
`Agent MCP toolName must be at most ${MAX_MCP_TOOL_NAME_BYTES} bytes`
2801+
)
27872802
.describe('Tool name returned by the MCP server’s tools endpoint.'),
27882803
})
27892804
.catchall(z.unknown().describe('One parameter fixed by the workflow author.')),
@@ -2824,6 +2839,7 @@ export type V2AgentTool = z.input<typeof v2AgentToolSchema>
28242839
/** The stored value of an Agent block's `tools` input. */
28252840
export const v2AgentToolInputSchema = z
28262841
.array(v2AgentToolSchema)
2842+
.max(MAX_AGENT_TOOLS_PER_BLOCK, `Agent tools cannot exceed ${MAX_AGENT_TOOLS_PER_BLOCK} entries`)
28272843
.describe(
28282844
'Tools the Agent may call. Integration `type` and `operation` values come from `GET /api/v2/blocks/{blockId}`; custom and MCP identifiers come from their workspace catalog endpoints.'
28292845
)

scripts/openapi/documents.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFileSync } from 'node:fs'
22
import path from 'node:path'
33
import { describe, expect, it } from 'vitest'
4+
import { MAX_ID_LENGTH } from '../../apps/sim/lib/api/contracts/primitives'
45
import { billingOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/billing'
56
import { filesAuditOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/files-audit'
67
import { knowledgeOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/knowledge'
@@ -14,6 +15,8 @@ import {
1415
} from '../../apps/sim/lib/api/contracts/v2/openapi/shared'
1516
import { tablesOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/tables'
1617
import { workflowsOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/workflows'
18+
import { MAX_AGENT_TOOLS_PER_BLOCK } from '../../apps/sim/lib/api/contracts/v2/workflows'
19+
import { MAX_MCP_TOOL_NAME_BYTES } from '../../apps/sim/lib/mcp/constants'
1720
import { generateOpenApiDocument, serializeOpenApiDocument } from './generator'
1821

1922
type JsonObject = Record<string, unknown>
@@ -296,11 +299,18 @@ describe('generated OpenAPI documents', () => {
296299
it('publishes Agent tools as named integration, custom, and MCP schemas', () => {
297300
const workflowsSpec = generateOpenApiDocument(workflowsOpenApiDocument)
298301
const schemas = (workflowsSpec.components as JsonObject).schemas as JsonObject
302+
const agentToolInput = schemas.AgentToolInput as JsonObject
299303
const agentTool = schemas.AgentTool as JsonObject
300304
const agentToolVariants = agentTool.oneOf as JsonObject[]
301305
const integrationTool = schemas.AgentIntegrationTool as JsonObject
302306
const integrationProperties = integrationTool.properties as JsonObject
303307
const customTool = schemas.AgentCustomTool as JsonObject
308+
const customToolVariants = customTool.anyOf as JsonObject[]
309+
const inlineCustomToolProperties = customToolVariants[1].properties as JsonObject
310+
const inlineCustomToolSchema = inlineCustomToolProperties.schema as JsonObject
311+
const inlineCustomToolSchemaProperties = inlineCustomToolSchema.properties as JsonObject
312+
const inlineFunction = inlineCustomToolSchemaProperties.function as JsonObject
313+
const inlineFunctionProperties = inlineFunction.properties as JsonObject
304314
const mcpTool = schemas.AgentMcpTool as JsonObject
305315
const mcpProperties = mcpTool.properties as JsonObject
306316
const mcpParams = mcpProperties.params as JsonObject
@@ -312,6 +322,9 @@ describe('generated OpenAPI documents', () => {
312322
{ $ref: '#/components/schemas/AgentCustomTool' },
313323
{ $ref: '#/components/schemas/AgentMcpTool' },
314324
])
325+
expect(agentToolInput).toEqual(
326+
expect.objectContaining({ type: 'array', maxItems: MAX_AGENT_TOOLS_PER_BLOCK })
327+
)
315328
expect(integrationProperties).toEqual(
316329
expect.objectContaining({
317330
type: expect.objectContaining({ type: 'string', pattern: expect.any(String) }),
@@ -321,11 +334,17 @@ describe('generated OpenAPI documents', () => {
321334
})
322335
)
323336
expect(customTool).toHaveProperty('anyOf')
337+
expect(inlineFunctionProperties.name).toEqual(
338+
expect.objectContaining({ type: 'string', maxLength: 64 })
339+
)
324340
expect((mcpProperties.type as JsonObject).const).toBe('mcp')
325341
expect(mcpParamProperties).toEqual(
326342
expect.objectContaining({
327-
serverId: expect.objectContaining({ type: 'string' }),
328-
toolName: expect.objectContaining({ type: 'string' }),
343+
serverId: expect.objectContaining({ type: 'string', maxLength: MAX_ID_LENGTH }),
344+
toolName: expect.objectContaining({
345+
type: 'string',
346+
maxLength: MAX_MCP_TOOL_NAME_BYTES,
347+
}),
329348
})
330349
)
331350
expect(JSON.stringify(schemas.WorkflowEditOperation)).toContain(

0 commit comments

Comments
 (0)