From 2b629cb6784fe85b93edc7167dd70448beeba24c Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 26 Aug 2026 10:45:27 -0700 Subject: [PATCH 1/2] feat(api): publish agent tool input schema --- apps/docs/openapi-v2-workflows.json | 357 +++++++++++++++++- .../v2/__tests__/workflow-agent-tools.test.ts | 114 ++++++ .../lib/api/contracts/v2/openapi/workflows.ts | 2 +- apps/sim/lib/api/contracts/v2/workflows.ts | 259 ++++++++++++- packages/sim-cli/src/generated/v2-api.ts | 55 ++- scripts/openapi/documents.test.ts | 40 ++ 6 files changed, 812 insertions(+), 15 deletions(-) create mode 100644 apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index 9e4c9a60e92..4160d3a7a05 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -437,7 +437,7 @@ "post": { "operationId": "applyWorkflowOperations", "summary": "Apply Workflow Operations", - "description": "Apply a batch of semantic edits — add, edit, delete, and subflow membership changes — to a workflow graph, plus an optional set of block enable/disable changes.\n\nBest-effort per operation, atomic per write. The engine applies what it can to an in-memory graph and reports the rest in `skipped`, each with a machine-readable `type`; exactly one write of the fully-resolved graph then happens, so there is never a partially-applied graph. `deferred` is **not** a failure list: a forward-referencing edge is wired automatically once its target block exists, in this batch or a later one, so re-issuing a deferred edge is wrong.\n\nSet `atomic` to fail closed: any genuine skipped item, or any block input that would be dropped rather than persisted, then aborts before the write and answers `409` with `error.details.code: \"OPERATIONS_NOT_APPLIED\"`, the same `skipped` array, and a `droppedInputs` array, having persisted nothing.\n\nA `block_id` you supply on an `add` or `insert_into_subflow` is only a label unless it is already a UUID: the engine mints one and returns the pairing in `mintedBlockIds`. References between operations in the same batch are remapped for you, so `triage` can be wired up in the same call it is created in — but a later request must use the minted id. Send your own UUIDs when you want an id you chose to survive across requests.\n\nOperation `params` is an open object because the accepted inputs come from the block registry, not from this contract — see the per-operation schemas for the envelope: `inputs` keyed by sub-block id, with `retry`, `triggerMode` and `advancedMode` beside it rather than inside it, and `connections` keyed by source handle. `GET /blocks/{blockId}` publishes the inputs a given block type accepts.\n\n`lint` is advisory and never blocks the write. `lint.fieldIssues` is the most actionable part for a headless builder — it names blocks missing a required field, which fail at run time — and `lint.unresolvedReferences` names credential, resource, tool, and skill values that do not resolve. Those values stay persisted; only `inputValidationErrors` lists inputs that were actually dropped.\n\nAs with `PUT /workflows/{workflowId}/state`, this changes only the draft; deploy to publish it. A workspace API key is rejected with `403`; use a personal API key.\n\nSet `?dryRun=true` to validate and lint without persisting: nothing is written, no audit entry is recorded, and collaborators are not notified. The response carries the same shape and the same validation and `lint` findings the committed write would, with `dryRun: true` — but `needsRedeployment` describes the state before the write, and warnings raised by persistence itself are necessarily absent.", + "description": "Apply a batch of semantic edits — add, edit, delete, and subflow membership changes — to a workflow graph, plus an optional set of block enable/disable changes.\n\nBest-effort per operation, atomic per write. The engine applies what it can to an in-memory graph and reports the rest in `skipped`, each with a machine-readable `type`; exactly one write of the fully-resolved graph then happens, so there is never a partially-applied graph. `deferred` is **not** a failure list: a forward-referencing edge is wired automatically once its target block exists, in this batch or a later one, so re-issuing a deferred edge is wrong.\n\nSet `atomic` to fail closed: any genuine skipped item, or any block input that would be dropped rather than persisted, then aborts before the write and answers `409` with `error.details.code: \"OPERATIONS_NOT_APPLIED\"`, the same `skipped` array, and a `droppedInputs` array, having persisted nothing.\n\nA `block_id` you supply on an `add` or `insert_into_subflow` is only a label unless it is already a UUID: the engine mints one and returns the pairing in `mintedBlockIds`. References between operations in the same batch are remapped for you, so `triage` can be wired up in the same call it is created in — but a later request must use the minted id. Send your own UUIDs when you want an id you chose to survive across requests.\n\nOperation `params` is an open object because the accepted inputs come from the block registry, not from this contract — see the per-operation schemas for the envelope: `inputs` keyed by sub-block id, with `retry`, `triggerMode` and `advancedMode` beside it rather than inside it, and `connections` keyed by source handle. `GET /blocks/{blockId}` publishes the inputs a given block type accepts. The Agent block’s `inputs.tools` value is the important exception to that open catalog shape: it is published here as the named `AgentToolInput` union, covering catalog integrations, workspace custom tools, and MCP tools.\n\n`lint` is advisory and never blocks the write. `lint.fieldIssues` is the most actionable part for a headless builder — it names blocks missing a required field, which fail at run time — and `lint.unresolvedReferences` names credential, resource, tool, and skill values that do not resolve. Those values stay persisted; only `inputValidationErrors` lists inputs that were actually dropped.\n\nAs with `PUT /workflows/{workflowId}/state`, this changes only the draft; deploy to publish it. A workspace API key is rejected with `403`; use a personal API key.\n\nSet `?dryRun=true` to validate and lint without persisting: nothing is written, no audit entry is recorded, and collaborators are not notified. The response carries the same shape and the same validation and `lint` findings the committed write would, with `dryRun: true` — but `needsRedeployment` describes the state before the write, and warnings raised by persistence itself are necessarily absent.", "tags": ["Workflows"], "parameters": [ { @@ -5972,6 +5972,32 @@ "type": "string", "minLength": 1, "description": "Block display name." + }, + "inputs": { + "allOf": [ + { + "type": "object", + "properties": { + "tools": { + "description": "Agent tools configuration. Applies to a `tool-input` field; other block inputs remain catalog-defined.", + "$ref": "#/components/schemas/AgentToolInput" + } + }, + "additionalProperties": { + "description": "One block-specific input whose accepted shape is published by the block catalog." + } + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "description": "One block-specific input whose accepted shape is published by the block catalog." + } + } + ], + "description": "Block configuration keyed by sub-block id." } }, "required": ["type", "name"], @@ -5998,13 +6024,51 @@ "description": "Block the operation targets. For `add`, the id the new block will be given." }, "params": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "description": "One operation parameter; see the description for the accepted keys." - }, + "allOf": [ + { + "type": "object", + "properties": { + "inputs": { + "allOf": [ + { + "type": "object", + "properties": { + "tools": { + "description": "Agent tools configuration. Applies to a `tool-input` field; other block inputs remain catalog-defined.", + "$ref": "#/components/schemas/AgentToolInput" + } + }, + "additionalProperties": { + "description": "One block-specific input whose accepted shape is published by the block catalog." + } + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "description": "One block-specific input whose accepted shape is published by the block catalog." + } + } + ], + "description": "Block configuration keyed by sub-block id." + } + }, + "additionalProperties": { + "description": "One operation parameter; see the description for the accepted keys." + } + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "description": "One operation parameter; see the description for the accepted keys." + } + } + ], "description": "Fields to change on the target block. Send only what changes. Accepted keys: `inputs`, `name`, `connections`, `removeEdges`, `nestedNodes`, `retry`, `triggerMode`, `advancedMode`. `inputs` carries the block's own configuration keyed by sub-block id, for example `inputs: { model: \"gpt-4o\", systemPrompt: \"...\" }` — never wrapped in `subBlocks`. Block-level settings sit beside `inputs`, never inside it: `retry`, `triggerMode`, `advancedMode`. `connections` is keyed by source handle and each value is a target block id, `{ block, handle }`, or an array of either; `success` is accepted as an alias for the `source` handle. Re-sending `connections` replaces that block's outgoing edges, so use `removeEdges` — `[{ targetBlockId, sourceHandle? }]`, `sourceHandle` defaulting to `source` — to drop one edge without restating the rest." } }, @@ -6058,6 +6122,32 @@ "type": "string", "minLength": 1, "description": "Block display name." + }, + "inputs": { + "allOf": [ + { + "type": "object", + "properties": { + "tools": { + "description": "Agent tools configuration. Applies to a `tool-input` field; other block inputs remain catalog-defined.", + "$ref": "#/components/schemas/AgentToolInput" + } + }, + "additionalProperties": { + "description": "One block-specific input whose accepted shape is published by the block catalog." + } + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "description": "One block-specific input whose accepted shape is published by the block catalog." + } + } + ], + "description": "Block configuration keyed by sub-block id." } }, "required": ["subflowId", "type", "name"], @@ -6106,6 +6196,245 @@ "title": "Workflow edit operation", "description": "One semantic edit against a workflow graph." }, + "AgentToolInput": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentTool" + }, + "description": "The complete value stored in an Agent block’s `tools` input.", + "title": "Agent tools input" + }, + "AgentTool": { + "oneOf": [ + { + "$ref": "#/components/schemas/AgentIntegrationTool" + }, + { + "$ref": "#/components/schemas/AgentCustomTool" + }, + { + "$ref": "#/components/schemas/AgentMcpTool" + } + ], + "title": "Agent tool", + "description": "A catalog integration operation, workspace custom tool, or MCP tool available to an Agent." + }, + "AgentIntegrationTool": { + "type": "object", + "properties": { + "type": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^(?!(?:custom-tool|mcp)$).+$", + "description": "Catalog block id, such as `cloudwatch` or `slack`. Use the block id, never an underlying tool id." + }, + "operation": { + "description": "Operation id from `GET /api/v2/blocks/{blockId}`. Required when the block exposes multiple operations; it may differ from the underlying tool id.", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "usageControl": { + "type": "string", + "enum": ["auto", "force", "none"], + "description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`." + }, + "params": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "description": "One tool parameter value." + }, + "description": "Parameters fixed by the workflow author. Parameters left out remain available for the model to supply when the tool declares them." + } + }, + "required": ["type"], + "additionalProperties": { + "description": "Forward-compatible integration tool metadata preserved by the workflow editor." + }, + "title": "Agent integration tool", + "description": "A catalog integration operation the Agent may call. Resolve valid block and operation ids through the block catalog.", + "examples": [ + { + "type": "cloudwatch", + "operation": "describe_alarm_history", + "usageControl": "auto", + "params": {} + } + ] + }, + "AgentCustomTool": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "custom-tool", + "description": "Custom-tool discriminator." + }, + "customToolId": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Custom tool id returned by `GET /api/v2/custom-tools`." + }, + "usageControl": { + "type": "string", + "enum": ["auto", "force", "none"], + "description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`." + } + }, + "required": ["type", "customToolId"], + "additionalProperties": { + "description": "Forward-compatible custom tool metadata preserved by the workflow editor." + } + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "custom-tool", + "description": "Custom-tool discriminator." + }, + "schema": { + "type": "object", + "properties": { + "type": { + "description": "Function declaration discriminator.", + "type": "string", + "const": "function" + }, + "function": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "Function name presented to the model." + }, + "description": { + "description": "What the inline custom tool does.", + "type": "string" + }, + "parameters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "description": "One JSON Schema keyword on the function parameters." + }, + "description": "JSON Schema describing the function arguments." + } + }, + "required": ["name", "parameters"], + "additionalProperties": { + "description": "Additional function declaration metadata." + }, + "description": "OpenAI-style function definition." + } + }, + "required": ["function"], + "additionalProperties": { + "description": "Additional custom tool declaration metadata." + }, + "description": "Inline OpenAI-style function declaration." + }, + "code": { + "type": "string", + "description": "Inline tool implementation executed by the Function runtime." + }, + "usageControl": { + "type": "string", + "enum": ["auto", "force", "none"], + "description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`." + } + }, + "required": ["type", "schema", "code"], + "additionalProperties": { + "description": "Forward-compatible custom tool metadata preserved by the workflow editor." + } + } + ], + "title": "Agent custom tool", + "description": "A workspace custom tool. Reference `customToolId` is the preferred shape; the inline declaration is retained for legacy workflow round trips.", + "examples": [ + { + "type": "custom-tool", + "customToolId": "cst_01J9X2ABCDEF", + "usageControl": "auto" + } + ] + }, + "AgentMcpTool": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp", + "description": "MCP-tool discriminator." + }, + "params": { + "allOf": [ + { + "type": "object", + "properties": { + "serverId": { + "type": "string", + "minLength": 1, + "description": "MCP server id returned by `GET /api/v2/mcp-servers`." + }, + "toolName": { + "type": "string", + "minLength": 1, + "description": "Tool name returned by the MCP server’s tools endpoint." + } + }, + "required": ["serverId", "toolName"], + "additionalProperties": { + "description": "One parameter fixed by the workflow author." + } + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "description": "One parameter fixed by the workflow author." + } + } + ], + "description": "MCP server and tool identity plus any tool arguments fixed by the workflow author." + }, + "usageControl": { + "type": "string", + "enum": ["auto", "force", "none"], + "description": "When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`." + } + }, + "required": ["type", "params"], + "additionalProperties": { + "description": "Forward-compatible MCP tool metadata preserved by the workflow editor." + }, + "title": "Agent MCP tool", + "description": "One tool discovered from a workspace MCP server.", + "examples": [ + { + "type": "mcp", + "params": { + "serverId": "mcp_01J9X2ABCDEF", + "toolName": "search_docs" + }, + "usageControl": "auto" + } + ] + }, "ApplyWorkflowOperationsRequest": { "type": "object", "properties": { @@ -6163,7 +6492,17 @@ "block_id": "agent-1", "params": { "type": "agent", - "name": "Triage" + "name": "Triage", + "inputs": { + "tools": [ + { + "type": "cloudwatch", + "operation": "describe_alarm_history", + "usageControl": "auto", + "params": {} + } + ] + } } } ] diff --git a/apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts b/apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts new file mode 100644 index 00000000000..8cf44dc7921 --- /dev/null +++ b/apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts @@ -0,0 +1,114 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + v2AgentToolInputSchema, + v2ApplyWorkflowOperationsBodySchema, +} from '@/lib/api/contracts/v2/workflows' + +describe('v2AgentToolInputSchema', () => { + it('accepts catalog integration, custom-tool reference, and MCP tool shapes', () => { + const tools = [ + { + type: 'cloudwatch', + operation: 'describe_alarm_history', + usageControl: 'auto', + params: { region: 'us-east-1' }, + }, + { + type: 'custom-tool', + customToolId: 'cst_123', + usageControl: 'force', + }, + { + type: 'mcp', + params: { serverId: 'mcp_123', toolName: 'search_docs', collection: 'incidents' }, + usageControl: 'none', + }, + ] + + expect(v2AgentToolInputSchema.parse(tools)).toEqual(tools) + }) + + it('keeps the legacy inline custom-tool shape available for workflow round trips', () => { + const tools = [ + { + type: 'custom-tool', + schema: { + type: 'function', + function: { + name: 'lookup_incident', + description: 'Look up an incident.', + parameters: { type: 'object', properties: { id: { type: 'string' } } }, + }, + }, + code: 'return params.id', + }, + ] + + expect(v2AgentToolInputSchema.parse(tools)).toEqual(tools) + }) + + it.each([ + [{ type: 'custom-tool', usageControl: 'auto' }], + [{ type: 'mcp', params: { serverId: 'mcp_123' }, usageControl: 'auto' }], + [{ type: 'slack', operation: 'send', usageControl: 'sometimes' }], + ])('rejects a malformed reserved tool shape', (tools) => { + expect(v2AgentToolInputSchema.safeParse(tools).success).toBe(false) + }) +}) + +describe('workflow operation Agent tools contract', () => { + it('publishes and validates tools under params.inputs without closing other catalog inputs', () => { + const body = { + operations: [ + { + operation_type: 'add', + block_id: 'triage', + params: { + type: 'agent', + name: 'Triage', + inputs: { + model: 'gpt-5', + tools: [ + { + type: 'cloudwatch', + operation: 'describe_alarms', + params: { region: 'us-west-2' }, + usageControl: 'auto', + futureMetadata: { preserved: true }, + }, + ], + }, + futureOperationSetting: true, + }, + }, + ], + } + + expect(v2ApplyWorkflowOperationsBodySchema.parse(body)).toEqual({ + ...body, + atomic: false, + layout: 'targeted', + }) + }) + + it('rejects malformed Agent tools before the edit engine runs', () => { + const parsed = v2ApplyWorkflowOperationsBodySchema.safeParse({ + operations: [ + { + operation_type: 'add', + block_id: 'triage', + params: { + type: 'agent', + name: 'Triage', + inputs: { tools: [{ type: 'mcp', params: { serverId: 'mcp_123' } }] }, + }, + }, + ], + }) + + expect(parsed.success).toBe(false) + }) +}) diff --git a/apps/sim/lib/api/contracts/v2/openapi/workflows.ts b/apps/sim/lib/api/contracts/v2/openapi/workflows.ts index 8e605b270dd..78671062a96 100644 --- a/apps/sim/lib/api/contracts/v2/openapi/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/openapi/workflows.ts @@ -364,7 +364,7 @@ const declaredRoutes = [ workflowOperation({ operationId: 'applyWorkflowOperations', summary: 'Apply Workflow Operations', - description: `Apply a batch of semantic edits — add, edit, delete, and subflow membership changes — to a workflow graph, plus an optional set of block enable/disable changes.\n\nBest-effort per operation, atomic per write. The engine applies what it can to an in-memory graph and reports the rest in \`skipped\`, each with a machine-readable \`type\`; exactly one write of the fully-resolved graph then happens, so there is never a partially-applied graph. \`deferred\` is **not** a failure list: a forward-referencing edge is wired automatically once its target block exists, in this batch or a later one, so re-issuing a deferred edge is wrong.\n\nSet \`atomic\` to fail closed: any genuine skipped item, or any block input that would be dropped rather than persisted, then aborts before the write and answers \`409\` with \`error.details.code: "OPERATIONS_NOT_APPLIED"\`, the same \`skipped\` array, and a \`droppedInputs\` array, having persisted nothing.\n\nA \`block_id\` you supply on an \`add\` or \`insert_into_subflow\` is only a label unless it is already a UUID: the engine mints one and returns the pairing in \`mintedBlockIds\`. References between operations in the same batch are remapped for you, so \`triage\` can be wired up in the same call it is created in — but a later request must use the minted id. Send your own UUIDs when you want an id you chose to survive across requests.\n\nOperation \`params\` is an open object because the accepted inputs come from the block registry, not from this contract — see the per-operation schemas for the envelope: \`inputs\` keyed by sub-block id, with \`retry\`, \`triggerMode\` and \`advancedMode\` beside it rather than inside it, and \`connections\` keyed by source handle. \`GET /blocks/{blockId}\` publishes the inputs a given block type accepts.\n\n\`lint\` is advisory and never blocks the write. \`lint.fieldIssues\` is the most actionable part for a headless builder — it names blocks missing a required field, which fail at run time — and \`lint.unresolvedReferences\` names credential, resource, tool, and skill values that do not resolve. Those values stay persisted; only \`inputValidationErrors\` lists inputs that were actually dropped.\n\nAs with \`PUT /workflows/{workflowId}/state\`, this changes only the draft; deploy to publish it. ${WORKSPACE_API_KEY_DENIED}\n\nSet \`?dryRun=true\` to validate and lint without persisting: nothing is written, no audit entry is recorded, and collaborators are not notified. The response carries the same shape and the same validation and \`lint\` findings the committed write would, with \`dryRun: true\` — but \`needsRedeployment\` describes the state before the write, and warnings raised by persistence itself are necessarily absent.`, + description: `Apply a batch of semantic edits — add, edit, delete, and subflow membership changes — to a workflow graph, plus an optional set of block enable/disable changes.\n\nBest-effort per operation, atomic per write. The engine applies what it can to an in-memory graph and reports the rest in \`skipped\`, each with a machine-readable \`type\`; exactly one write of the fully-resolved graph then happens, so there is never a partially-applied graph. \`deferred\` is **not** a failure list: a forward-referencing edge is wired automatically once its target block exists, in this batch or a later one, so re-issuing a deferred edge is wrong.\n\nSet \`atomic\` to fail closed: any genuine skipped item, or any block input that would be dropped rather than persisted, then aborts before the write and answers \`409\` with \`error.details.code: "OPERATIONS_NOT_APPLIED"\`, the same \`skipped\` array, and a \`droppedInputs\` array, having persisted nothing.\n\nA \`block_id\` you supply on an \`add\` or \`insert_into_subflow\` is only a label unless it is already a UUID: the engine mints one and returns the pairing in \`mintedBlockIds\`. References between operations in the same batch are remapped for you, so \`triage\` can be wired up in the same call it is created in — but a later request must use the minted id. Send your own UUIDs when you want an id you chose to survive across requests.\n\nOperation \`params\` is an open object because the accepted inputs come from the block registry, not from this contract — see the per-operation schemas for the envelope: \`inputs\` keyed by sub-block id, with \`retry\`, \`triggerMode\` and \`advancedMode\` beside it rather than inside it, and \`connections\` keyed by source handle. \`GET /blocks/{blockId}\` publishes the inputs a given block type accepts. The Agent block’s \`inputs.tools\` value is the important exception to that open catalog shape: it is published here as the named \`AgentToolInput\` union, covering catalog integrations, workspace custom tools, and MCP tools.\n\n\`lint\` is advisory and never blocks the write. \`lint.fieldIssues\` is the most actionable part for a headless builder — it names blocks missing a required field, which fail at run time — and \`lint.unresolvedReferences\` names credential, resource, tool, and skill values that do not resolve. Those values stay persisted; only \`inputValidationErrors\` lists inputs that were actually dropped.\n\nAs with \`PUT /workflows/{workflowId}/state\`, this changes only the draft; deploy to publish it. ${WORKSPACE_API_KEY_DENIED}\n\nSet \`?dryRun=true\` to validate and lint without persisting: nothing is written, no audit entry is recorded, and collaborators are not notified. The response carries the same shape and the same validation and \`lint\` findings the committed write would, with \`dryRun: true\` — but \`needsRedeployment\` describes the state before the write, and warnings raised by persistence itself are necessarily absent.`, errors: RESOURCE_MUTATION_ERRORS, success: jsonSuccess('The batch was applied.'), }), diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index abe9070bcf1..931d89054bd 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -2639,6 +2639,229 @@ const WORKFLOW_OPERATION_PARAM_ENVELOPE = 'block id, `{ block, handle }`, or an array of either; `success` is accepted as an ' + 'alias for the `source` handle.' +const v2AgentToolUsageControlSchema = z + .enum(['auto', 'force', 'none']) + .describe( + 'When the Agent may call the tool: `auto` lets the model decide, `force` requires a call, and `none` disables it. Omitted means `auto`.' + ) + +const v2AgentToolParamsSchema = z + .record(z.string(), z.unknown().describe('One tool parameter value.')) + .describe( + 'Parameters fixed by the workflow author. Parameters left out remain available for the model to supply when the tool declares them.' + ) + +/** A catalog integration attached directly to an Agent block. */ +export const v2AgentIntegrationToolSchema = z + .object({ + type: z + .string() + .trim() + .min(1, 'Agent integration tool type cannot be empty') + .max(255, 'Agent integration tool type must be at most 255 characters') + .regex( + /^(?!(?:custom-tool|mcp)$).+$/, + 'Agent integration tool type must be a catalog block id, not `custom-tool` or `mcp`' + ) + .describe( + 'Catalog block id, such as `cloudwatch` or `slack`. Use the block id, never an underlying tool id.' + ), + operation: z + .string() + .trim() + .min(1, 'Agent integration tool operation cannot be empty') + .max(255, 'Agent integration tool operation must be at most 255 characters') + .optional() + .describe( + 'Operation id from `GET /api/v2/blocks/{blockId}`. Required when the block exposes multiple operations; it may differ from the underlying tool id.' + ), + usageControl: v2AgentToolUsageControlSchema.optional(), + params: v2AgentToolParamsSchema.optional(), + }) + .catchall( + z + .unknown() + .describe('Forward-compatible integration tool metadata preserved by the workflow editor.') + ) + .meta({ + id: 'AgentIntegrationTool', + title: 'Agent integration tool', + description: + 'A catalog integration operation the Agent may call. Resolve valid block and operation ids through the block catalog.', + examples: [ + { + type: 'cloudwatch', + operation: 'describe_alarm_history', + usageControl: 'auto', + params: {}, + }, + ], + }) + +const v2AgentCustomToolReferenceSchema = z + .object({ + type: z.literal('custom-tool').describe('Custom-tool discriminator.'), + customToolId: z + .string() + .trim() + .min(1, 'Agent customToolId cannot be empty') + .max(255, 'Agent customToolId must be at most 255 characters') + .describe('Custom tool id returned by `GET /api/v2/custom-tools`.'), + usageControl: v2AgentToolUsageControlSchema.optional(), + }) + .catchall( + z + .unknown() + .describe('Forward-compatible custom tool metadata preserved by the workflow editor.') + ) + +const v2AgentInlineCustomToolSchema = z + .object({ + type: z.literal('custom-tool').describe('Custom-tool discriminator.'), + schema: z + .object({ + type: z.literal('function').optional().describe('Function declaration discriminator.'), + function: z + .object({ + name: z + .string() + .trim() + .min(1, 'Inline custom tool function name cannot be empty') + .describe('Function name presented to the model.'), + description: z.string().optional().describe('What the inline custom tool does.'), + parameters: z + .record( + z.string(), + z.unknown().describe('One JSON Schema keyword on the function parameters.') + ) + .describe('JSON Schema describing the function arguments.'), + }) + .catchall(z.unknown().describe('Additional function declaration metadata.')) + .describe('OpenAI-style function definition.'), + }) + .catchall(z.unknown().describe('Additional custom tool declaration metadata.')) + .describe('Inline OpenAI-style function declaration.'), + code: z.string().describe('Inline tool implementation executed by the Function runtime.'), + usageControl: v2AgentToolUsageControlSchema.optional(), + }) + .catchall( + z + .unknown() + .describe('Forward-compatible custom tool metadata preserved by the workflow editor.') + ) + +/** A workspace custom tool attached directly to an Agent block. */ +export const v2AgentCustomToolSchema = z + .union([v2AgentCustomToolReferenceSchema, v2AgentInlineCustomToolSchema]) + .meta({ + id: 'AgentCustomTool', + title: 'Agent custom tool', + description: + 'A workspace custom tool. Reference `customToolId` is the preferred shape; the inline declaration is retained for legacy workflow round trips.', + examples: [ + { + type: 'custom-tool', + customToolId: 'cst_01J9X2ABCDEF', + usageControl: 'auto', + }, + ], + }) + +/** An MCP server tool attached directly to an Agent block. */ +export const v2AgentMcpToolSchema = z + .object({ + type: z.literal('mcp').describe('MCP-tool discriminator.'), + params: z + .intersection( + z + .object({ + serverId: z + .string() + .trim() + .min(1, 'Agent MCP serverId cannot be empty') + .describe('MCP server id returned by `GET /api/v2/mcp-servers`.'), + toolName: z + .string() + .trim() + .min(1, 'Agent MCP toolName cannot be empty') + .describe('Tool name returned by the MCP server’s tools endpoint.'), + }) + .catchall(z.unknown().describe('One parameter fixed by the workflow author.')), + z.record(z.string(), z.unknown().describe('One parameter fixed by the workflow author.')) + ) + .describe( + 'MCP server and tool identity plus any tool arguments fixed by the workflow author.' + ), + usageControl: v2AgentToolUsageControlSchema.optional(), + }) + .catchall( + z.unknown().describe('Forward-compatible MCP tool metadata preserved by the workflow editor.') + ) + .meta({ + id: 'AgentMcpTool', + title: 'Agent MCP tool', + description: 'One tool discovered from a workspace MCP server.', + examples: [ + { + type: 'mcp', + params: { serverId: 'mcp_01J9X2ABCDEF', toolName: 'search_docs' }, + usageControl: 'auto', + }, + ], + }) + +/** One callable tool attached directly to an Agent block. */ +export const v2AgentToolSchema = z + .xor([v2AgentIntegrationToolSchema, v2AgentCustomToolSchema, v2AgentMcpToolSchema]) + .meta({ + id: 'AgentTool', + title: 'Agent tool', + description: + 'A catalog integration operation, workspace custom tool, or MCP tool available to an Agent.', + }) +export type V2AgentTool = z.input + +/** The stored value of an Agent block's `tools` input. */ +export const v2AgentToolInputSchema = z + .array(v2AgentToolSchema) + .describe( + '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.' + ) + .meta({ + id: 'AgentToolInput', + title: 'Agent tools input', + description: 'The complete value stored in an Agent block’s `tools` input.', + }) +export type V2AgentToolInput = z.input + +const v2WorkflowOperationInputsSchema = z + .intersection( + z + .object({ + tools: v2AgentToolInputSchema + .optional() + .describe( + 'Agent tools configuration. Applies to a `tool-input` field; other block inputs remain catalog-defined.' + ), + }) + .catchall( + z + .unknown() + .describe( + 'One block-specific input whose accepted shape is published by the block catalog.' + ) + ), + z.record( + z.string(), + z + .unknown() + .describe( + 'One block-specific input whose accepted shape is published by the block catalog.' + ) + ) + ) + .describe('Block configuration keyed by sub-block id.') + /** * The keys `edit` accepts. Open because the per-block input set is defined by * the block registry rather than by this contract, but the envelope around it @@ -2646,9 +2869,18 @@ const WORKFLOW_OPERATION_PARAM_ENVELOPE = * rename a block and nothing else. */ const v2WorkflowOperationParamsSchema = z - .record( - z.string(), - z.unknown().describe('One operation parameter; see the description for the accepted keys.') + .intersection( + z + .object({ + inputs: v2WorkflowOperationInputsSchema.optional(), + }) + .catchall( + z.unknown().describe('One operation parameter; see the description for the accepted keys.') + ), + z.record( + z.string(), + z.unknown().describe('One operation parameter; see the description for the accepted keys.') + ) ) .describe( 'Fields to change on the target block. Send only what changes. Accepted keys: `inputs`, ' + @@ -2669,6 +2901,7 @@ const v2AddWorkflowBlockParamsSchema = z .string() .min(1, 'params.name is required to add a block') .describe('Block display name.'), + inputs: v2WorkflowOperationInputsSchema.optional(), }) .catchall(z.unknown().describe('One block-specific input or connection descriptor.')) .describe( @@ -2701,6 +2934,7 @@ const v2InsertIntoSubflowParamsSchema = z .string() .min(1, 'params.name is required to insert a block') .describe('Block display name.'), + inputs: v2WorkflowOperationInputsSchema.optional(), }) .catchall(z.unknown().describe('One block-specific input or connection descriptor.')) .describe( @@ -2821,7 +3055,24 @@ export const v2ApplyWorkflowOperationsBodySchema = z examples: [ { operations: [ - { operation_type: 'add', block_id: 'agent-1', params: { type: 'agent', name: 'Triage' } }, + { + operation_type: 'add', + block_id: 'agent-1', + params: { + type: 'agent', + name: 'Triage', + inputs: { + tools: [ + { + type: 'cloudwatch', + operation: 'describe_alarm_history', + usageControl: 'auto', + params: {}, + }, + ], + }, + }, + }, ], }, ], diff --git a/packages/sim-cli/src/generated/v2-api.ts b/packages/sim-cli/src/generated/v2-api.ts index d023a361c5b..2a5dd8f52b9 100644 --- a/packages/sim-cli/src/generated/v2-api.ts +++ b/packages/sim-cli/src/generated/v2-api.ts @@ -345,12 +345,19 @@ type ApplyWorkflowOperationsBodyRef0 = params: { type: string name: string + inputs?: { + tools?: ApplyWorkflowOperationsBodyRef1 + } & Record } } | { operation_type: 'edit' block_id: string - params: Record + params: { + inputs?: { + tools?: ApplyWorkflowOperationsBodyRef1 + } & Record + } & Record } | { operation_type: 'delete' @@ -363,6 +370,9 @@ type ApplyWorkflowOperationsBodyRef0 = subflowId: string type: string name: string + inputs?: { + tools?: ApplyWorkflowOperationsBodyRef1 + } & Record } } | { @@ -373,6 +383,49 @@ type ApplyWorkflowOperationsBodyRef0 = } } +type ApplyWorkflowOperationsBodyRef1 = Array + +type ApplyWorkflowOperationsBodyRef2 = + | ApplyWorkflowOperationsBodyRef3 + | ApplyWorkflowOperationsBodyRef4 + | ApplyWorkflowOperationsBodyRef5 + +type ApplyWorkflowOperationsBodyRef3 = { + type: string + operation?: string + usageControl?: 'auto' | 'force' | 'none' + params?: Record +} + +type ApplyWorkflowOperationsBodyRef4 = + | { + type: 'custom-tool' + customToolId: string + usageControl?: 'auto' | 'force' | 'none' + } + | { + type: 'custom-tool' + schema: { + type?: 'function' + function: { + name: string + description?: string + parameters: Record + } + } + code: string + usageControl?: 'auto' | 'force' | 'none' + } + +type ApplyWorkflowOperationsBodyRef5 = { + type: 'mcp' + params: { + serverId: string + toolName: string + } & Record + usageControl?: 'auto' | 'force' | 'none' +} + export type ApplyWorkflowOperationsBody = { operations: Array atomic?: boolean diff --git a/scripts/openapi/documents.test.ts b/scripts/openapi/documents.test.ts index f3185143099..06c4938dfe3 100644 --- a/scripts/openapi/documents.test.ts +++ b/scripts/openapi/documents.test.ts @@ -293,6 +293,46 @@ describe('generated OpenAPI documents', () => { ) }) + it('publishes Agent tools as named integration, custom, and MCP schemas', () => { + const workflowsSpec = generateOpenApiDocument(workflowsOpenApiDocument) + const schemas = (workflowsSpec.components as JsonObject).schemas as JsonObject + const agentTool = schemas.AgentTool as JsonObject + const agentToolVariants = agentTool.oneOf as JsonObject[] + const integrationTool = schemas.AgentIntegrationTool as JsonObject + const integrationProperties = integrationTool.properties as JsonObject + const customTool = schemas.AgentCustomTool as JsonObject + const mcpTool = schemas.AgentMcpTool as JsonObject + const mcpProperties = mcpTool.properties as JsonObject + const mcpParams = mcpProperties.params as JsonObject + const mcpParamIdentity = (mcpParams.allOf as JsonObject[])[0] + const mcpParamProperties = mcpParamIdentity.properties as JsonObject + + expect(agentToolVariants).toEqual([ + { $ref: '#/components/schemas/AgentIntegrationTool' }, + { $ref: '#/components/schemas/AgentCustomTool' }, + { $ref: '#/components/schemas/AgentMcpTool' }, + ]) + expect(integrationProperties).toEqual( + expect.objectContaining({ + type: expect.objectContaining({ type: 'string', pattern: expect.any(String) }), + operation: expect.objectContaining({ type: 'string' }), + usageControl: expect.objectContaining({ enum: ['auto', 'force', 'none'] }), + params: expect.objectContaining({ type: 'object' }), + }) + ) + expect(customTool).toHaveProperty('anyOf') + expect((mcpProperties.type as JsonObject).const).toBe('mcp') + expect(mcpParamProperties).toEqual( + expect.objectContaining({ + serverId: expect.objectContaining({ type: 'string' }), + toolName: expect.objectContaining({ type: 'string' }), + }) + ) + expect(JSON.stringify(schemas.WorkflowEditOperation)).toContain( + '#/components/schemas/AgentToolInput' + ) + }) + it('uses named schemas for top-level response objects and list items', () => { for (const document of DOCUMENTS) { expect(anonymousTopLevelResponseObjects(generateOpenApiDocument(document))).toEqual([]) From 7fe34b73b80670158c8409dae8181a72fa448485 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 26 Aug 2026 11:23:55 -0700 Subject: [PATCH 2/2] fix(api): bound agent tool inputs --- apps/docs/openapi-v2-workflows.json | 4 ++ .../v2/__tests__/workflow-agent-tools.test.ts | 54 +++++++++++++++++++ apps/sim/lib/api/contracts/v2/workflows.ts | 16 ++++++ scripts/openapi/documents.test.ts | 23 +++++++- 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/apps/docs/openapi-v2-workflows.json b/apps/docs/openapi-v2-workflows.json index 4160d3a7a05..13416eb4837 100644 --- a/apps/docs/openapi-v2-workflows.json +++ b/apps/docs/openapi-v2-workflows.json @@ -6197,6 +6197,7 @@ "description": "One semantic edit against a workflow graph." }, "AgentToolInput": { + "maxItems": 100, "type": "array", "items": { "$ref": "#/components/schemas/AgentTool" @@ -6315,6 +6316,7 @@ "name": { "type": "string", "minLength": 1, + "maxLength": 64, "description": "Function name presented to the model." }, "description": { @@ -6387,11 +6389,13 @@ "serverId": { "type": "string", "minLength": 1, + "maxLength": 128, "description": "MCP server id returned by `GET /api/v2/mcp-servers`." }, "toolName": { "type": "string", "minLength": 1, + "maxLength": 256, "description": "Tool name returned by the MCP server’s tools endpoint." } }, diff --git a/apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts b/apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts index 8cf44dc7921..dd5c2a1c115 100644 --- a/apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts +++ b/apps/sim/lib/api/contracts/v2/__tests__/workflow-agent-tools.test.ts @@ -2,10 +2,13 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' +import { MAX_ID_LENGTH } from '@/lib/api/contracts/primitives' import { + MAX_AGENT_TOOLS_PER_BLOCK, v2AgentToolInputSchema, v2ApplyWorkflowOperationsBodySchema, } from '@/lib/api/contracts/v2/workflows' +import { MAX_MCP_TOOL_NAME_BYTES } from '@/lib/mcp/constants' describe('v2AgentToolInputSchema', () => { it('accepts catalog integration, custom-tool reference, and MCP tool shapes', () => { @@ -57,6 +60,57 @@ describe('v2AgentToolInputSchema', () => { ])('rejects a malformed reserved tool shape', (tools) => { expect(v2AgentToolInputSchema.safeParse(tools).success).toBe(false) }) + + it('rejects a tool list above the workflow-operation ceiling', () => { + const tools = Array.from({ length: MAX_AGENT_TOOLS_PER_BLOCK + 1 }, (_, index) => ({ + type: `integration-${index}`, + })) + + expect(v2AgentToolInputSchema.safeParse(tools).success).toBe(false) + }) + + it.each([ + [ + 'inline function name', + { + type: 'custom-tool', + schema: { + type: 'function', + function: { name: 'a'.repeat(65), parameters: { type: 'object' } }, + }, + code: 'return null', + }, + ], + [ + 'MCP server id', + { + type: 'mcp', + params: { serverId: 'a'.repeat(MAX_ID_LENGTH + 1), toolName: 'search_docs' }, + }, + ], + [ + 'MCP tool name', + { + type: 'mcp', + params: { + serverId: 'mcp_123', + toolName: 'a'.repeat(MAX_MCP_TOOL_NAME_BYTES + 1), + }, + }, + ], + [ + 'MCP multibyte tool name', + { + type: 'mcp', + params: { + serverId: 'mcp_123', + toolName: '💡'.repeat(Math.floor(MAX_MCP_TOOL_NAME_BYTES / 4) + 1), + }, + }, + ], + ])('rejects an overlong %s', (_label, tool) => { + expect(v2AgentToolInputSchema.safeParse([tool]).success).toBe(false) + }) }) describe('workflow operation Agent tools contract', () => { diff --git a/apps/sim/lib/api/contracts/v2/workflows.ts b/apps/sim/lib/api/contracts/v2/workflows.ts index 931d89054bd..e46cfba6383 100644 --- a/apps/sim/lib/api/contracts/v2/workflows.ts +++ b/apps/sim/lib/api/contracts/v2/workflows.ts @@ -17,6 +17,7 @@ import { } from '@/lib/api/contracts/deployments' import { booleanQueryFlagSchema, + MAX_ID_LENGTH, missingFieldError, noInputSchema, runIdSchema, @@ -58,6 +59,7 @@ import { import { MAX_WORKFLOW_EXECUTION_TIMEOUT_SECONDS } from '@/lib/billing/execution-timeout-defaults' import { MAX_INLINE_MATERIALIZATION_BYTES } from '@/lib/execution/payloads/limits' import { PERSISTED_WORKFLOW_EXECUTION_STATUSES } from '@/lib/logs/types' +import { MAX_MCP_TOOL_NAME_BYTES } from '@/lib/mcp/constants' import { WORKFLOW_SKIPPED_ITEM_TYPES } from '@/lib/workflows/editing/types' export const V2_WORKFLOW_RUN_ID_HEADER = 'X-Run-Id' @@ -2053,6 +2055,8 @@ export const MAX_WORKFLOW_GRAPH_BLOCKS = 2000 export const MAX_WORKFLOW_GRAPH_EDGES = 10_000 /** Ceiling on one `POST /operations` batch. */ export const MAX_WORKFLOW_EDIT_OPERATIONS = 200 +/** Ceiling on the complete tool list assigned to one Agent block. */ +export const MAX_AGENT_TOOLS_PER_BLOCK = 100 /** Ceiling on one `PATCH /variables` batch; mirrors the application use case's own cap. */ export const MAX_WORKFLOW_VARIABLE_OPERATIONS = 100 /** Ceiling on one bulk move; mirrors the application use case's own cap. */ @@ -2727,6 +2731,7 @@ const v2AgentInlineCustomToolSchema = z .string() .trim() .min(1, 'Inline custom tool function name cannot be empty') + .max(64, 'Inline custom tool function name must be at most 64 characters') .describe('Function name presented to the model.'), description: z.string().optional().describe('What the inline custom tool does.'), parameters: z @@ -2779,11 +2784,21 @@ export const v2AgentMcpToolSchema = z .string() .trim() .min(1, 'Agent MCP serverId cannot be empty') + .max(MAX_ID_LENGTH, `Agent MCP serverId must be at most ${MAX_ID_LENGTH} characters`) .describe('MCP server id returned by `GET /api/v2/mcp-servers`.'), toolName: z .string() .trim() .min(1, 'Agent MCP toolName cannot be empty') + .max( + MAX_MCP_TOOL_NAME_BYTES, + `Agent MCP toolName must be at most ${MAX_MCP_TOOL_NAME_BYTES} characters` + ) + .refine( + (toolName) => + new TextEncoder().encode(toolName).byteLength <= MAX_MCP_TOOL_NAME_BYTES, + `Agent MCP toolName must be at most ${MAX_MCP_TOOL_NAME_BYTES} bytes` + ) .describe('Tool name returned by the MCP server’s tools endpoint.'), }) .catchall(z.unknown().describe('One parameter fixed by the workflow author.')), @@ -2824,6 +2839,7 @@ export type V2AgentTool = z.input /** The stored value of an Agent block's `tools` input. */ export const v2AgentToolInputSchema = z .array(v2AgentToolSchema) + .max(MAX_AGENT_TOOLS_PER_BLOCK, `Agent tools cannot exceed ${MAX_AGENT_TOOLS_PER_BLOCK} entries`) .describe( '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.' ) diff --git a/scripts/openapi/documents.test.ts b/scripts/openapi/documents.test.ts index 06c4938dfe3..00621d72977 100644 --- a/scripts/openapi/documents.test.ts +++ b/scripts/openapi/documents.test.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs' import path from 'node:path' import { describe, expect, it } from 'vitest' +import { MAX_ID_LENGTH } from '../../apps/sim/lib/api/contracts/primitives' import { billingOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/billing' import { filesAuditOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/files-audit' import { knowledgeOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/knowledge' @@ -14,6 +15,8 @@ import { } from '../../apps/sim/lib/api/contracts/v2/openapi/shared' import { tablesOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/tables' import { workflowsOpenApiDocument } from '../../apps/sim/lib/api/contracts/v2/openapi/workflows' +import { MAX_AGENT_TOOLS_PER_BLOCK } from '../../apps/sim/lib/api/contracts/v2/workflows' +import { MAX_MCP_TOOL_NAME_BYTES } from '../../apps/sim/lib/mcp/constants' import { generateOpenApiDocument, serializeOpenApiDocument } from './generator' type JsonObject = Record @@ -296,11 +299,18 @@ describe('generated OpenAPI documents', () => { it('publishes Agent tools as named integration, custom, and MCP schemas', () => { const workflowsSpec = generateOpenApiDocument(workflowsOpenApiDocument) const schemas = (workflowsSpec.components as JsonObject).schemas as JsonObject + const agentToolInput = schemas.AgentToolInput as JsonObject const agentTool = schemas.AgentTool as JsonObject const agentToolVariants = agentTool.oneOf as JsonObject[] const integrationTool = schemas.AgentIntegrationTool as JsonObject const integrationProperties = integrationTool.properties as JsonObject const customTool = schemas.AgentCustomTool as JsonObject + const customToolVariants = customTool.anyOf as JsonObject[] + const inlineCustomToolProperties = customToolVariants[1].properties as JsonObject + const inlineCustomToolSchema = inlineCustomToolProperties.schema as JsonObject + const inlineCustomToolSchemaProperties = inlineCustomToolSchema.properties as JsonObject + const inlineFunction = inlineCustomToolSchemaProperties.function as JsonObject + const inlineFunctionProperties = inlineFunction.properties as JsonObject const mcpTool = schemas.AgentMcpTool as JsonObject const mcpProperties = mcpTool.properties as JsonObject const mcpParams = mcpProperties.params as JsonObject @@ -312,6 +322,9 @@ describe('generated OpenAPI documents', () => { { $ref: '#/components/schemas/AgentCustomTool' }, { $ref: '#/components/schemas/AgentMcpTool' }, ]) + expect(agentToolInput).toEqual( + expect.objectContaining({ type: 'array', maxItems: MAX_AGENT_TOOLS_PER_BLOCK }) + ) expect(integrationProperties).toEqual( expect.objectContaining({ type: expect.objectContaining({ type: 'string', pattern: expect.any(String) }), @@ -321,11 +334,17 @@ describe('generated OpenAPI documents', () => { }) ) expect(customTool).toHaveProperty('anyOf') + expect(inlineFunctionProperties.name).toEqual( + expect.objectContaining({ type: 'string', maxLength: 64 }) + ) expect((mcpProperties.type as JsonObject).const).toBe('mcp') expect(mcpParamProperties).toEqual( expect.objectContaining({ - serverId: expect.objectContaining({ type: 'string' }), - toolName: expect.objectContaining({ type: 'string' }), + serverId: expect.objectContaining({ type: 'string', maxLength: MAX_ID_LENGTH }), + toolName: expect.objectContaining({ + type: 'string', + maxLength: MAX_MCP_TOOL_NAME_BYTES, + }), }) ) expect(JSON.stringify(schemas.WorkflowEditOperation)).toContain(