From 6328c99f1da3ee8417f33b51dfe4d5585ac237e0 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 19 Mar 2026 11:19:57 -0700 Subject: [PATCH 1/3] fix(tool): Fix issue with custom tools spreading out string output --- apps/sim/executor/utils/output-filter.ts | 3 + apps/sim/tools/index.test.ts | 180 +++++++++++++++++++++++ apps/sim/tools/index.ts | 11 +- 3 files changed, 192 insertions(+), 2 deletions(-) diff --git a/apps/sim/executor/utils/output-filter.ts b/apps/sim/executor/utils/output-filter.ts index be28f48a59a..5da00faba53 100644 --- a/apps/sim/executor/utils/output-filter.ts +++ b/apps/sim/executor/utils/output-filter.ts @@ -24,6 +24,9 @@ export function filterOutputForLog( additionalHiddenKeys?: string[] } ): NormalizedBlockOutput { + if (typeof output !== 'object' || output === null || Array.isArray(output)) { + return output as NormalizedBlockOutput + } const blockConfig = blockType ? getBlock(blockType) : undefined const filtered: NormalizedBlockOutput = {} const additionalHiddenKeys = options?.additionalHiddenKeys ?? [] diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index 18fdbd1fb2a..c20681b5d27 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -1830,6 +1830,186 @@ describe('Rate Limiting and Retry Logic', () => { }) }) +describe('stripInternalFields Safety', () => { + let cleanupEnvVars: () => void + + beforeEach(() => { + process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000' + cleanupEnvVars = setupEnvVars({ NEXT_PUBLIC_APP_URL: 'http://localhost:3000' }) + }) + + afterEach(() => { + vi.resetAllMocks() + cleanupEnvVars() + }) + + it('should preserve string output from tools without character-indexing', async () => { + const stringOutput = '{"type":"button","phone":"917899658001"}' + + const mockTool = { + id: 'test_string_output', + name: 'Test String Output', + description: 'A tool that returns a string as output', + version: '1.0.0', + params: {}, + request: { + url: '/api/test/string-output', + method: 'POST' as const, + headers: () => ({ 'Content-Type': 'application/json' }), + }, + transformResponse: vi.fn().mockResolvedValue({ + success: true, + output: stringOutput, + }), + } + + const originalTools = { ...tools } + ;(tools as any).test_string_output = mockTool + + global.fetch = Object.assign( + vi.fn().mockImplementation(async () => ({ + ok: true, + status: 200, + headers: new Headers(), + json: () => Promise.resolve({ success: true }), + })), + { preconnect: vi.fn() } + ) as typeof fetch + + const result = await executeTool('test_string_output', {}, true) + + expect(result.success).toBe(true) + expect(result.output).toBe(stringOutput) + expect(typeof result.output).toBe('string') + + Object.assign(tools, originalTools) + }) + + it('should preserve array output from tools', async () => { + const arrayOutput = [{ id: 1 }, { id: 2 }] + + const mockTool = { + id: 'test_array_output', + name: 'Test Array Output', + description: 'A tool that returns an array as output', + version: '1.0.0', + params: {}, + request: { + url: '/api/test/array-output', + method: 'POST' as const, + headers: () => ({ 'Content-Type': 'application/json' }), + }, + transformResponse: vi.fn().mockResolvedValue({ + success: true, + output: arrayOutput, + }), + } + + const originalTools = { ...tools } + ;(tools as any).test_array_output = mockTool + + global.fetch = Object.assign( + vi.fn().mockImplementation(async () => ({ + ok: true, + status: 200, + headers: new Headers(), + json: () => Promise.resolve({ success: true }), + })), + { preconnect: vi.fn() } + ) as typeof fetch + + const result = await executeTool('test_array_output', {}, true) + + expect(result.success).toBe(true) + expect(Array.isArray(result.output)).toBe(true) + expect(result.output).toEqual(arrayOutput) + + Object.assign(tools, originalTools) + }) + + it('should still strip __-prefixed fields from object output', async () => { + const mockTool = { + id: 'test_strip_internal', + name: 'Test Strip Internal', + description: 'A tool with __internal fields in output', + version: '1.0.0', + params: {}, + request: { + url: '/api/test/strip-internal', + method: 'POST' as const, + headers: () => ({ 'Content-Type': 'application/json' }), + }, + transformResponse: vi.fn().mockResolvedValue({ + success: true, + output: { result: 'ok', __costDollars: 0.05, _id: 'keep-this' }, + }), + } + + const originalTools = { ...tools } + ;(tools as any).test_strip_internal = mockTool + + global.fetch = Object.assign( + vi.fn().mockImplementation(async () => ({ + ok: true, + status: 200, + headers: new Headers(), + json: () => Promise.resolve({ success: true }), + })), + { preconnect: vi.fn() } + ) as typeof fetch + + const result = await executeTool('test_strip_internal', {}, true) + + expect(result.success).toBe(true) + expect(result.output.result).toBe('ok') + expect(result.output.__costDollars).toBeUndefined() + expect(result.output._id).toBe('keep-this') + + Object.assign(tools, originalTools) + }) + + it('should preserve __-prefixed fields in custom tool output', async () => { + const mockTool = { + id: 'custom_test-preserve-dunder', + name: 'Custom Preserve Dunder', + description: 'A custom tool whose output has __ fields', + version: '1.0.0', + params: {}, + request: { + url: '/api/function/execute', + method: 'POST' as const, + headers: () => ({ 'Content-Type': 'application/json' }), + }, + transformResponse: vi.fn().mockResolvedValue({ + success: true, + output: { result: 'ok', __metadata: { source: 'user' }, __tag: 'important' }, + }), + } + + const originalTools = { ...tools } + ;(tools as any)['custom_test-preserve-dunder'] = mockTool + + global.fetch = Object.assign( + vi.fn().mockImplementation(async () => ({ + ok: true, + status: 200, + headers: new Headers(), + json: () => Promise.resolve({ success: true }), + })), + { preconnect: vi.fn() } + ) as typeof fetch + + const result = await executeTool('custom_test-preserve-dunder', {}, true) + + expect(result.success).toBe(true) + expect(result.output.result).toBe('ok') + expect(result.output.__metadata).toEqual({ source: 'user' }) + expect(result.output.__tag).toBe('important') + + Object.assign(tools, originalTools) + }) +}) + describe('Cost Field Handling', () => { let cleanupEnvVars: () => void diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 60f710ed08e..76ceb96f1d7 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -363,6 +363,9 @@ async function reportCustomDimensionUsage( * fields like `_id`. */ function stripInternalFields(output: Record): Record { + if (typeof output !== 'object' || output === null || Array.isArray(output)) { + return output + } const result: Record = {} for (const [key, value] of Object.entries(output)) { if (!key.startsWith('__')) { @@ -825,7 +828,9 @@ export async function executeTool( ) } - const strippedOutput = stripInternalFields(finalResult.output || {}) + const strippedOutput = isCustomTool(normalizedToolId) + ? (finalResult.output || {}) + : stripInternalFields(finalResult.output || {}) return { ...finalResult, @@ -880,7 +885,9 @@ export async function executeTool( ) } - const strippedOutput = stripInternalFields(finalResult.output || {}) + const strippedOutput = isCustomTool(normalizedToolId) + ? (finalResult.output || {}) + : stripInternalFields(finalResult.output || {}) return { ...finalResult, From 27414134eed008fbd9160a563100d2bf6ef5d24b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 19 Mar 2026 11:29:54 -0700 Subject: [PATCH 2/3] Fix lint --- apps/sim/tools/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 76ceb96f1d7..f5198f78b0b 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -829,7 +829,7 @@ export async function executeTool( } const strippedOutput = isCustomTool(normalizedToolId) - ? (finalResult.output || {}) + ? finalResult.output || {} : stripInternalFields(finalResult.output || {}) return { @@ -886,7 +886,7 @@ export async function executeTool( } const strippedOutput = isCustomTool(normalizedToolId) - ? (finalResult.output || {}) + ? finalResult.output || {} : stripInternalFields(finalResult.output || {}) return { From eb8b17e0add7203ba80350b1b99e0edeeb98d626 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 19 Mar 2026 11:38:09 -0700 Subject: [PATCH 3/3] Avoid any transformation on custom tool outputs --- apps/sim/tools/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index f5198f78b0b..0dbf1753f6b 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -829,8 +829,8 @@ export async function executeTool( } const strippedOutput = isCustomTool(normalizedToolId) - ? finalResult.output || {} - : stripInternalFields(finalResult.output || {}) + ? finalResult.output + : stripInternalFields(finalResult.output ?? {}) return { ...finalResult, @@ -886,8 +886,8 @@ export async function executeTool( } const strippedOutput = isCustomTool(normalizedToolId) - ? finalResult.output || {} - : stripInternalFields(finalResult.output || {}) + ? finalResult.output + : stripInternalFields(finalResult.output ?? {}) return { ...finalResult,