From dfe7c02f03412a7dfd6824a70f42e75e62656a1f Mon Sep 17 00:00:00 2001 From: Predrag Ristic Date: Wed, 19 Aug 2026 16:22:04 +0000 Subject: [PATCH] fix(core): Hide get-json from Instance AI (#36146) --- .../src/tools/__tests__/workflows.tool.test.ts | 8 ++++++-- packages/@n8n/instance-ai/src/tools/index.ts | 2 +- .../__tests__/eval-setup-agent.tools.test.ts | 17 +++++++++++++++++ .../instance-ai/src/tools/workflows.tool.ts | 2 +- .../__tests__/build-workflow.tool.test.ts | 8 ++++++++ .../src/tools/workflows/build-workflow.tool.ts | 4 ++-- .../src/tools/workflows/summarize-workflow.ts | 2 +- 7 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/@n8n/instance-ai/src/tools/__tests__/workflows.tool.test.ts b/packages/@n8n/instance-ai/src/tools/__tests__/workflows.tool.test.ts index c0469fdde40..916f745ba35 100644 --- a/packages/@n8n/instance-ai/src/tools/__tests__/workflows.tool.test.ts +++ b/packages/@n8n/instance-ai/src/tools/__tests__/workflows.tool.test.ts @@ -213,12 +213,12 @@ describe('workflows tool', () => { expect(context.workflowService.publish).not.toHaveBeenCalled(); }); - it('should allow code inspection but reject raw update on orchestrator surface', () => { + it('should allow code inspection but reject raw workflow actions on orchestrator surface', () => { const context = createMockContext(); const tool = createWorkflowsTool(context, 'orchestrator'); const schema = getInputSchema(tool); - expect(schema.safeParse({ action: 'get-json', workflowId: 'w1' }).success).toBe(true); + expect(schema.safeParse({ action: 'get-json', workflowId: 'w1' }).success).toBe(false); expect(schema.safeParse({ action: 'get-as-code', workflowId: 'w1' }).success).toBe(true); expect( schema.safeParse({ @@ -227,6 +227,8 @@ describe('workflows tool', () => { workflow: { name: 'WF', nodes: [], connections: {} }, }).success, ).toBe(false); + expect(getDescription(tool)).toContain('TypeScript SDK code'); + expect(getDescription(tool)).not.toContain('WorkflowJSON'); }); }); @@ -645,6 +647,8 @@ describe('workflows tool', () => { structure: '// generated code', note: STRUCTURE_ONLY_NOTE, }); + expect(STRUCTURE_ONLY_NOTE).toContain('get-as-code'); + expect(STRUCTURE_ONLY_NOTE).not.toContain('get-json'); const codegenInput = vi.mocked(generateWorkflowCode).mock.calls[0][0]; expect(codegenInput).toMatchObject({ name: 'Test WF' }); expect(JSON.stringify(codegenInput)).not.toContain('conditions'); diff --git a/packages/@n8n/instance-ai/src/tools/index.ts b/packages/@n8n/instance-ai/src/tools/index.ts index 4e347d73341..f76de57e692 100644 --- a/packages/@n8n/instance-ai/src/tools/index.ts +++ b/packages/@n8n/instance-ai/src/tools/index.ts @@ -127,7 +127,7 @@ export function createAllTools(context: InstanceAiContext): InstanceAiToolRegist /** * Creates orchestrator domain tools. Skills run in the orchestrator now, so * domain tools keep their workflow-building surface while hiding raw full - * WorkflowJSON update actions. + * WorkflowJSON read and update actions. */ export function createOrchestratorDomainTools(context: InstanceAiContext): InstanceAiToolRegistry { const tools: Array<[string, BuiltTool]> = [ diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/eval-setup-agent.tools.test.ts b/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/eval-setup-agent.tools.test.ts index 00f03272109..2fab69971ba 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/eval-setup-agent.tools.test.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/eval-setup-agent.tools.test.ts @@ -20,6 +20,9 @@ function makeContext( domainContext.workflowService.get = vi .fn() .mockResolvedValue({ id: 'w1', name: 'Test', nodes: [], connections: {}, active: false }); + domainContext.workflowService.getAsWorkflowJSON = vi + .fn() + .mockResolvedValue({ name: 'Eval setup', nodes: [], connections: {} }); domainContext.workflowService.updateFromWorkflowJSON = vi .fn() .mockResolvedValue({ id: 'w1', name: 'Test', versionId: 'v1', nodes: [], connections: {} }); @@ -79,6 +82,7 @@ describe('buildEvalSetupTools', () => { expect(schema.safeParse({ action: 'get-json', workflowId: 'w1' }).success).toBe(true); expect(schema.safeParse({ action: 'get', workflowId: 'w1' }).success).toBe(false); + expect(schema.safeParse({ action: 'get-as-code', workflowId: 'w1' }).success).toBe(false); expect( schema.safeParse({ action: 'update', @@ -96,6 +100,19 @@ describe('buildEvalSetupTools', () => { ).toBe(false); }); + it('reads raw workflow JSON through the eval setup workflow tool', async () => { + const ctx = makeContext('require_approval'); + const workflows = buildEvalSetupTools(ctx).get('workflows'); + + const result = await workflows?.handler!({ action: 'get-json', workflowId: 'w1' }, {} as never); + + expect(ctx.domainContext!.workflowService.getAsWorkflowJSON).toHaveBeenCalledWith( + 'w1', + undefined, + ); + expect(result).toEqual({ name: 'Eval setup', nodes: [], connections: {} }); + }); + it('leaves the original workflows tool in place when admin has blocked updates', () => { const ctx = makeContext('blocked'); const tools = buildEvalSetupTools(ctx); diff --git a/packages/@n8n/instance-ai/src/tools/workflows.tool.ts b/packages/@n8n/instance-ai/src/tools/workflows.tool.ts index 041ceaa2fff..5e2a10a15ff 100644 --- a/packages/@n8n/instance-ai/src/tools/workflows.tool.ts +++ b/packages/@n8n/instance-ai/src/tools/workflows.tool.ts @@ -383,7 +383,7 @@ function getSupportedWorkflowActionSchemas( return { list: listAction, get: getAction, - 'get-json': getJsonAction, + ...(surface !== 'orchestrator' ? { 'get-json': getJsonAction } : {}), 'get-as-code': getAsCodeAction, delete: deleteAction, unarchive: unarchiveAction, diff --git a/packages/@n8n/instance-ai/src/tools/workflows/__tests__/build-workflow.tool.test.ts b/packages/@n8n/instance-ai/src/tools/workflows/__tests__/build-workflow.tool.test.ts index 85878513d3c..0fba80a0dd2 100644 --- a/packages/@n8n/instance-ai/src/tools/workflows/__tests__/build-workflow.tool.test.ts +++ b/packages/@n8n/instance-ai/src/tools/workflows/__tests__/build-workflow.tool.test.ts @@ -218,6 +218,14 @@ describe('createBuildWorkflowTool', () => { expect(tool.description).toContain('workflow-builder'); expect(tool.description).toContain('data-table-manager'); expect(tool.description).toContain('load_skill'); + expect(tool.description).toContain( + 'Use TypeScript SDK .workflow.ts source for new and existing workflows.', + ); + expect(tool.description).not.toContain('WorkflowJSON .json source for existing workflow edits'); + expect(buildWorkflowInputSchema.shape.filePath.description).toContain( + 'Workspace-relative path to the TypeScript SDK workflow source file', + ); + expect(buildWorkflowInputSchema.shape.filePath.description).not.toContain('WorkflowJSON'); }); it('builds a new workflow from a workspace source file', async () => { diff --git a/packages/@n8n/instance-ai/src/tools/workflows/build-workflow.tool.ts b/packages/@n8n/instance-ai/src/tools/workflows/build-workflow.tool.ts index 207a0c942a9..05f5eb71cdf 100644 --- a/packages/@n8n/instance-ai/src/tools/workflows/build-workflow.tool.ts +++ b/packages/@n8n/instance-ai/src/tools/workflows/build-workflow.tool.ts @@ -141,7 +141,7 @@ export const buildWorkflowInputSchema = z 'Pass a workspace-relative path like src/workflows/my-workflow.workflow.ts.', }) .describe( - 'Workspace-relative path to the workflow source file to build, e.g. src/workflows/my-workflow.workflow.ts. Supports TypeScript SDK files and WorkflowJSON .json files.', + 'Workspace-relative path to the TypeScript SDK workflow source file to build, e.g. src/workflows/my-workflow.workflow.ts.', ), sourceCode: z .string() @@ -351,7 +351,7 @@ export function createBuildWorkflowTool(context: InstanceAiContext) { 'Build and save a workflow from workflow source. ' + 'Load `workflow-builder` via `load_skill` before calling this tool. ' + 'When the workflow creates or writes Data Tables, also load `data-table-manager` first. ' + - 'Use TypeScript SDK source for new workflows, or WorkflowJSON .json source for existing workflow edits. ' + + 'Use TypeScript SDK .workflow.ts source for new and existing workflows. ' + 'Prefer writing the file with `workspace_write_file` / `workspace_str_replace_file` so `workflow-sdk validate` can run on it, then call this tool with filePath. ' + 'For a one-shot create/rewrite you may pass `sourceCode` instead (the tool writes filePath and builds).', ) diff --git a/packages/@n8n/instance-ai/src/tools/workflows/summarize-workflow.ts b/packages/@n8n/instance-ai/src/tools/workflows/summarize-workflow.ts index 673be422989..df1a4b52574 100644 --- a/packages/@n8n/instance-ai/src/tools/workflows/summarize-workflow.ts +++ b/packages/@n8n/instance-ai/src/tools/workflows/summarize-workflow.ts @@ -5,7 +5,7 @@ import { NodeConnectionTypes } from 'n8n-workflow'; import type { WorkflowNode } from '../../types'; export const STRUCTURE_ONLY_NOTE = - 'Node parameters omitted to keep context small. Pass full: true to include them in one call, or use get-json / get-as-code (optionally with versionId) for parameter-level detail.'; + 'Node parameters omitted to keep context small. Pass full: true to include them in one call, or use get-as-code (optionally with versionId) for parameter-level detail.'; // Below this, summarizing saves too little to be worth a possible second full fetch. export const PARAMETERS_INLINE_LIMIT_BYTES = 4096;