diff --git a/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts b/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts index efbab115c0d..978dd5d2836 100644 --- a/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts +++ b/packages/@n8n/api-types/src/agents/__tests__/sanitize-agent-json-config.test.ts @@ -70,7 +70,6 @@ describe('sanitizeAgentJsonConfig', () => { config: { toolCallConcurrency: 2, webSearch: { enabled: true, provider: 'native' }, - nodeTools: { enabled: true }, }, }); expect(AgentJsonConfigSchema.safeParse(sanitized).success).toBe(true); diff --git a/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts b/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts index cf06da9b105..89d422e222d 100644 --- a/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts +++ b/packages/@n8n/api-types/src/agents/agent-json-config.schema.ts @@ -300,11 +300,6 @@ export const AgentJsonConfigSchema = z.object({ .describe( 'Maximum number of agent loop iterations per run. Do not set unless the user explicitly asks.', ), - nodeTools: z - .object({ - enabled: z.boolean(), - }) - .optional(), }) .optional(), }); @@ -358,7 +353,3 @@ export function formatZodErrors(error: ZodError): ConfigValidationError[] { received: 'received' in issue ? String(issue.received) : undefined, })); } - -export function isNodeToolsEnabled(config: AgentJsonConfig['config']): boolean { - return config?.nodeTools?.enabled === true; -} diff --git a/packages/@n8n/config/src/configs/agents.config.ts b/packages/@n8n/config/src/configs/agents.config.ts index ddd6a1e4015..dc4d7160cfb 100644 --- a/packages/@n8n/config/src/configs/agents.config.ts +++ b/packages/@n8n/config/src/configs/agents.config.ts @@ -8,7 +8,7 @@ import { Config, Env } from '../decorators'; * `N8N_AGENTS_MODULES`. The backend fails fast on unknown tokens so typos * surface at startup instead of silently disabling a feature. */ -export const AGENTS_MODULE_NAMES = ['node-tools-searcher'] as const; +export const AGENTS_MODULE_NAMES = [] as const; export type AgentsModuleName = (typeof AGENTS_MODULE_NAMES)[number]; @@ -17,9 +17,15 @@ class AgentsModuleArray extends CommaSeparatedStringArray { super(str); for (const name of this) { + const moduleName: string = name; if (!AGENTS_MODULE_NAMES.includes(name)) { + const validTokens = AGENTS_MODULE_NAMES.join(', '); throw new Error( - `Unknown agents module: "${name}". Valid tokens: ${AGENTS_MODULE_NAMES.join(', ')}.`, + `Unknown agents module: "${moduleName}". ${ + validTokens + ? `Valid tokens: ${validTokens}.` + : 'No agents modules are currently supported.' + }`, ); } } @@ -35,12 +41,7 @@ export class AgentsConfig { /** * Comma-separated list of agent sub-feature modules to enable. Each entry * gates a specific frontend/runtime capability inside the agents module. - * Currently known: - * - `node-tools-searcher` — surfaces the "Built-in node tools" toggle in - * the agent editor. - * - * Gates the UI surface only — existing agents persisted with a given - * capability turned on continue to run even if its token is removed here. + * Add supported module tokens to `AGENTS_MODULE_NAMES`. */ @Env('N8N_AGENTS_MODULES') modules: AgentsModuleArray = []; diff --git a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts index 726c22f32dd..055b90f1a94 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-config.service.test.ts @@ -1,6 +1,5 @@ import type { AgentJsonConfig } from '@n8n/api-types'; import { mockLogger } from '@n8n/backend-test-utils'; -import type { AgentsConfig } from '@n8n/config'; import { mock } from 'jest-mock-extended'; import type { CredentialsService } from '@/credentials/credentials.service'; @@ -37,13 +36,12 @@ function makeAgent(overrides: Partial = {}): Agent { } as unknown as Agent; } -function makeService(config: Partial = {}) { +function makeService() { const agentRepository = mock(); const agentTaskRepository = mock(); const agentSkillsService = mock(); const runtimeCacheService = mock(); const credentialsService = mock(); - const agentsConfig = { modules: [], ...config } as AgentsConfig; agentRepository.save.mockImplementation(async (agent) => agent as Agent); credentialsService.findAllCredentialIdsForProject.mockResolvedValue([]); @@ -61,7 +59,6 @@ function makeService(config: Partial = {}) { agentRepository, agentTaskRepository, agentSkillsService, - agentsConfig, runtimeCacheService, credentialsService, ); @@ -112,27 +109,6 @@ describe('AgentConfigService', () => { }); }); - it('gates config.nodeTools.enabled on the node-tools module', async () => { - await expect( - makeService().service.validateConfig({ - ...baseConfig, - config: { nodeTools: { enabled: true } }, - }), - ).resolves.toMatchObject({ - valid: false, - error: expect.stringContaining('node-tools-searcher'), - }); - - await expect( - makeService({ - modules: ['node-tools-searcher'] as AgentsConfig['modules'], - }).service.validateConfig({ - ...baseConfig, - config: { nodeTools: { enabled: true } }, - }), - ).resolves.toMatchObject({ valid: true }); - }); - it('accepts draft credentials that are not checked until update sanitization', async () => { const { service } = makeService(); diff --git a/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts b/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts index 409f735da74..9f0b494c35f 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-json-config.test.ts @@ -1,4 +1,4 @@ -import { AgentJsonConfigSchema, isNodeToolsEnabled, type AgentJsonConfig } from '@n8n/api-types'; +import { AgentJsonConfigSchema, type AgentJsonConfig } from '@n8n/api-types'; const baseConfig: AgentJsonConfig = { name: 'Test Agent', @@ -6,43 +6,6 @@ const baseConfig: AgentJsonConfig = { instructions: 'Be helpful', }; -describe('AgentJsonConfigSchema — config.nodeTools', () => { - it('accepts a config without nodeTools', () => { - expect(AgentJsonConfigSchema.safeParse({ ...baseConfig, config: {} }).success).toBe(true); - }); - - it('accepts nodeTools: { enabled: true }', () => { - const parsed = AgentJsonConfigSchema.safeParse({ - ...baseConfig, - config: { nodeTools: { enabled: true } }, - }); - expect(parsed.success).toBe(true); - }); - - it('accepts nodeTools: { enabled: false }', () => { - const parsed = AgentJsonConfigSchema.safeParse({ - ...baseConfig, - config: { nodeTools: { enabled: false } }, - }); - expect(parsed.success).toBe(true); - }); - - it('rejects nodeTools without enabled', () => { - expect( - AgentJsonConfigSchema.safeParse({ ...baseConfig, config: { nodeTools: {} } }).success, - ).toBe(false); - }); - - it('rejects nodeTools.enabled of the wrong type', () => { - expect( - AgentJsonConfigSchema.safeParse({ - ...baseConfig, - config: { nodeTools: { enabled: 'yes' } }, - }).success, - ).toBe(false); - }); -}); - describe('AgentJsonConfigSchema — skill refs', () => { it('accepts a skill ref with a valid id', () => { const parsed = AgentJsonConfigSchema.safeParse({ @@ -72,24 +35,6 @@ describe('AgentJsonConfigSchema — skill refs', () => { }); }); -describe('isNodeToolsEnabled', () => { - it('returns false when config is undefined', () => { - expect(isNodeToolsEnabled(undefined)).toBe(false); - }); - - it('returns false when config has no nodeTools field', () => { - expect(isNodeToolsEnabled({})).toBe(false); - }); - - it('returns false when nodeTools.enabled is false', () => { - expect(isNodeToolsEnabled({ nodeTools: { enabled: false } })).toBe(false); - }); - - it('returns true only when nodeTools.enabled is explicitly true', () => { - expect(isNodeToolsEnabled({ nodeTools: { enabled: true } })).toBe(true); - }); -}); - describe('AgentJsonConfigSchema — subAgents', () => { it('accepts saved agent references', () => { const parsed = AgentJsonConfigSchema.safeParse({ diff --git a/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts b/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts index ed6f26132ac..9041ee27401 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-runtime-reconstruction-integration-tools.test.ts @@ -43,7 +43,6 @@ import type { AgentTaskService } from '../agent-task.service'; import { AgentsService } from '../agents.service'; import { AgentTestChatService } from '../agent-test-chat.service'; import { AgentValidationService } from '../agent-validation.service'; -import type { AgentsToolsService } from '../agents-tools.service'; import type { AgentHistory } from '../entities/agent-history.entity'; import type { AgentTaskSnapshot } from '../entities/agent-task-snapshot.entity'; import type { Agent } from '../entities/agent.entity'; @@ -118,7 +117,6 @@ function makeRuntimeReconstructionService( mock(), mock(), mock(), - mock(), mock(), mock(), { modules } as unknown as AgentsConfig, @@ -182,7 +180,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => { let chatIntegrationService: jest.Mocked; let agentKnowledgeService: jest.Mocked; let publisher: jest.Mocked; - let agentsConfig: AgentsConfig; let globalConfig: jest.Mocked; let telemetry: jest.Mocked; let runtimeCacheService: AgentRuntimeCacheService; @@ -214,11 +211,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => { agentKnowledgeService = mock(); publisher = mock(); publisher.publishCommand.mockResolvedValue(); - agentsConfig = { - modules: [], - sandboxEnabled: false, - sandboxProvider: '', - } as unknown as AgentsConfig; globalConfig = mock({ multiMainSetup: { enabled: false }, } as Partial); @@ -245,7 +237,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => { agentRepository, agentTaskRepository, agentSkillsService, - agentsConfig, runtimeCacheService, credentialsService, ); @@ -349,7 +340,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => { userId: string; runtimeProfile: 'top-level'; config: AgentJsonConfig; - nodeToolsEnabled: boolean; subAgentDelegation: { sourcesById: Record; availableSubAgents: []; @@ -370,7 +360,6 @@ describe('AgentRuntimeReconstructionService integration tools', () => { model: 'anthropic/claude-sonnet-4-5', instructions: 'Be helpful', }, - nodeToolsEnabled: false, parentAgentIdForDelegation: agentId, subAgentDelegation: { sourcesById: {}, diff --git a/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts b/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts index 708c91f00e8..35df8ad454d 100644 --- a/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agent-telemetry.test.ts @@ -45,7 +45,6 @@ describe('agent telemetry', () => { }, config: { webSearch: { enabled: true }, - nodeTools: { enabled: true }, }, memory: { enabled: true, @@ -61,8 +60,8 @@ describe('agent telemetry', () => { ).toEqual({ model: 'anthropic/claude-sonnet-4-5', channels: ['linear', 'slack'], - tool_types: ['custom', 'mcp', 'node_tools', 'provider', 'subagent', 'web_search', 'workflow'], - tool_count: 7, + tool_types: ['custom', 'mcp', 'provider', 'subagent', 'web_search', 'workflow'], + tool_count: 6, num_skills: 1, memory_type: 'n8n_observational_episodic', }); diff --git a/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts b/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts index e852603b93e..26c9a6f1094 100644 --- a/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agents-service-reconstruct-gating.test.ts @@ -33,7 +33,6 @@ import type { WorkflowFinderService } from '@/workflows/workflow-finder.service' import { AgentRuntimeReconstructionService } from '../agent-runtime-reconstruction.service'; import type { AgentKnowledgeSandboxService } from '../agent-knowledge-sandbox.service'; -import type { AgentsToolsService } from '../agents-tools.service'; import type { Agent } from '../entities/agent.entity'; import { ChatIntegrationRegistry } from '../integrations/agent-chat-integration'; import { ChatIntegrationActionExecutor } from '../integrations/integration-action-executor'; @@ -85,7 +84,6 @@ function getInjectedToolNames(): string[] { } function makeReconstructionService( - agentsToolsService: AgentsToolsService, modules: string[] = [], overrides: { logger?: Logger; @@ -111,7 +109,6 @@ function makeReconstructionService( overrides.n8nCheckpointStorage ?? mock(), secureRuntime, mock(), - agentsToolsService, mock(), mock(), { @@ -144,79 +141,6 @@ function makeAgentEntity( } as unknown as Agent; } -describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — node tools gating', () => { - beforeEach(() => { - jest.clearAllMocks(); - builtAgent.hasCheckpointStorage.mockReturnValue(true); - }); - - function setup(options: { nodeToolsModuleEnabled?: boolean } = {}) { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); - const credentialProvider = mock(); - const service = makeReconstructionService( - agentsToolsService, - options.nodeToolsModuleEnabled ? ['node-tools-searcher'] : [], - ); - return { service, agentsToolsService, credentialProvider }; - } - - it.each([ - { - name: 'config.nodeTools is absent and the module is disabled', - nodeToolsModuleEnabled: false, - schemaConfig: undefined, - attaches: false, - }, - { - name: 'config.nodeTools is absent and the module is enabled', - nodeToolsModuleEnabled: true, - schemaConfig: undefined, - attaches: false, - }, - { - name: 'config.nodeTools.enabled is true but the module is disabled', - nodeToolsModuleEnabled: false, - schemaConfig: { nodeTools: { enabled: true } }, - attaches: false, - }, - { - name: 'config.nodeTools.enabled is true and the module is enabled', - nodeToolsModuleEnabled: true, - schemaConfig: { nodeTools: { enabled: true } }, - attaches: true, - }, - { - name: 'config.nodeTools.enabled is false and the module is disabled', - nodeToolsModuleEnabled: false, - schemaConfig: { nodeTools: { enabled: false } }, - attaches: false, - }, - { - name: 'config.nodeTools.enabled is false and the module is enabled', - nodeToolsModuleEnabled: true, - schemaConfig: { nodeTools: { enabled: false } }, - attaches: false, - }, - ])('$name', async ({ nodeToolsModuleEnabled, schemaConfig, attaches }) => { - const { service, agentsToolsService, credentialProvider } = setup({ - nodeToolsModuleEnabled, - }); - const entity = makeAgentEntity(schemaConfig); - - await service.reconstructFromAgentEntity(entity, credentialProvider, 'user-1'); - - if (attaches) { - expect(agentsToolsService.getRuntimeTools).toHaveBeenCalledWith( - credentialProvider, - 'project-1', - ); - } else { - expect(agentsToolsService.getRuntimeTools).not.toHaveBeenCalled(); - } - }); -}); - describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — MCP wiring', () => { beforeEach(() => { jest.clearAllMocks(); @@ -233,10 +157,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — MCP w }); function setup() { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService); + const service = makeReconstructionService(); return { service, credentialProvider }; } @@ -284,10 +206,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }); function setup() { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService); + const service = makeReconstructionService(); return { service, credentialProvider }; } @@ -356,10 +276,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }); it('uses the shared default maxChildren when config does not override it', async () => { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService, []); + const service = makeReconstructionService(); await service.reconstructFromAgentEntity(makeAgentEntity(), credentialProvider, 'user-1'); @@ -369,10 +287,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }); it('uses subAgents.maxChildren over the SDK default', async () => { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService, []); + const service = makeReconstructionService(); const entity = makeAgentEntity(undefined, { subAgents: { maxChildren: 2 } }); await service.reconstructFromAgentEntity(entity, credentialProvider, 'user-1'); @@ -383,8 +299,6 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }); it('resolves subAgents.modelsByDifficulty into delegate tool metadata', async () => { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); credentialProvider.resolve.mockImplementation(async (credentialId: string) => { if (credentialId === 'low-cred') { @@ -395,7 +309,7 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a } throw new Error(`unexpected credential ${credentialId}`); }); - const service = makeReconstructionService(agentsToolsService, []); + const service = makeReconstructionService(); const entity = makeAgentEntity(undefined, { subAgents: { modelsByDifficulty: { @@ -421,8 +335,6 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }); it('resolves inline child provider tools for the child model provider', async () => { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); credentialProvider.resolve.mockImplementation(async (credentialId: string) => { if (credentialId === 'high-cred') { @@ -430,7 +342,7 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a } throw new Error(`unexpected credential ${credentialId}`); }); - const service = makeReconstructionService(agentsToolsService, []); + const service = makeReconstructionService(); const entity = makeAgentEntity( { webSearch: { enabled: true } }, { @@ -457,10 +369,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — sub-a }); it('omits inlineSubAgentModelsByDifficulty when no difficulty mappings are configured', async () => { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService, []); + const service = makeReconstructionService(); await service.reconstructFromAgentEntity(makeAgentEntity(), credentialProvider, 'user-1'); @@ -485,10 +395,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — n8n c }); function setup() { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService); + const service = makeReconstructionService(); return { service, credentialProvider }; } @@ -547,10 +455,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromAgentEntity — check }; const n8nCheckpointStorage = mock(); n8nCheckpointStorage.getStorage.mockReturnValue(scopedStorage); - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService, [], { n8nCheckpointStorage }); + const service = makeReconstructionService([], { n8nCheckpointStorage }); await service.reconstructFromAgentEntity(makeAgentEntity(), credentialProvider, 'user-1'); @@ -567,10 +473,8 @@ describe('AgentRuntimeReconstructionService.reconstructFromResolvedSource — su }); it('does not inject top-level integration context/action tools', async () => { - const agentsToolsService = mock(); - agentsToolsService.getRuntimeTools.mockReturnValue([] as BuiltTool[]); const credentialProvider = mock(); - const service = makeReconstructionService(agentsToolsService); + const service = makeReconstructionService(); const config: AgentJsonConfig = { name: 'Child', diff --git a/packages/cli/src/modules/agents/__tests__/agents-tools.service.test.ts b/packages/cli/src/modules/agents/__tests__/agents-tools.service.test.ts index b8df630ac7f..6d1485c5ad9 100644 --- a/packages/cli/src/modules/agents/__tests__/agents-tools.service.test.ts +++ b/packages/cli/src/modules/agents/__tests__/agents-tools.service.test.ts @@ -4,22 +4,15 @@ import { AGENT_BUILDER_AVAILABLE_AI_UTILITY_TOOL_NODE_TYPES, AGENT_BUILDER_HIDDEN_AVAILABLE_TOOL_NODE_TYPES, } from '@n8n/api-types'; -import type { Logger } from '@n8n/backend-common'; -import { validateNodeConfig } from '@n8n/workflow-sdk'; -import type { EphemeralNodeExecutor } from '@/node-execution'; import type { NodeCatalogService } from '@/node-catalog'; import { - AgentsToolsService, isAgentToolNodeType, isExecutableNodeType, + AgentsToolsService, } from '../agents-tools.service'; -jest.mock('@n8n/workflow-sdk', () => ({ - validateNodeConfig: jest.fn().mockReturnValue({ valid: true, errors: [] }), -})); - const ctx = { resumeData: undefined, suspend: jest.fn().mockResolvedValue(undefined as never), @@ -34,13 +27,9 @@ function makeService() { }); nodeCatalogService.getNodeTypes.mockResolvedValue('node-types-string'); - const ephemeralNodeExecutor = mock(); + const service = new AgentsToolsService(nodeCatalogService); - const logger = mock(); - - const service = new AgentsToolsService(logger, nodeCatalogService, ephemeralNodeExecutor); - - return { service, nodeCatalogService, ephemeralNodeExecutor, logger }; + return { service, nodeCatalogService }; } function makeCredentialProvider( @@ -64,21 +53,6 @@ describe('AgentsToolsService', () => { }); }); - describe('getRuntimeTools()', () => { - it('returns all four tools including run_node_tool', () => { - const { service } = makeService(); - const names = service - .getRuntimeTools(makeCredentialProvider(), 'project-1') - .map((t) => t.name); - expect(names).toEqual([ - 'search_nodes', - 'get_node_types', - 'list_credentials', - 'run_node_tool', - ]); - }); - }); - describe('list_credentials handler', () => { function getListTool(service: AgentsToolsService, provider: CredentialProvider) { return service.getSharedTools(provider, 'hint').find((t) => t.name === 'list_credentials')!; @@ -137,7 +111,7 @@ describe('AgentsToolsService', () => { describe('search_nodes handler', () => { function getSearchTool(service: AgentsToolsService) { return service - .getRuntimeTools(makeCredentialProvider(), 'project-1') + .getSharedTools(makeCredentialProvider(), 'hint') .find((t) => t.name === 'search_nodes')!; } @@ -198,7 +172,7 @@ describe('AgentsToolsService', () => { describe('get_node_types handler', () => { function getTypesTool(service: AgentsToolsService) { return service - .getRuntimeTools(makeCredentialProvider(), 'project-1') + .getSharedTools(makeCredentialProvider(), 'hint') .find((t) => t.name === 'get_node_types')!; } @@ -223,132 +197,4 @@ describe('AgentsToolsService', () => { ]); }); }); - - describe('run_node_tool handler', () => { - function getRunTool(service: AgentsToolsService) { - return service - .getRuntimeTools(makeCredentialProvider(), 'project-1') - .find((t) => t.name === 'run_node_tool')!; - } - - beforeEach(() => { - jest.mocked(validateNodeConfig).mockReturnValue({ valid: true, errors: [] }); - }); - - it('refuses to run trigger nodes', async () => { - const { service, ephemeralNodeExecutor } = makeService(); - - const result = await getRunTool(service).handler!( - { nodeType: 'n8n-nodes-base.scheduleTrigger', nodeTypeVersion: 1 }, - ctx, - ); - - expect(ephemeralNodeExecutor.executeInline).not.toHaveBeenCalled(); - expect(result).toMatchObject({ - status: 'error', - message: expect.stringContaining('scheduleTrigger'), - }); - }); - - it('skips validation and calls executeInline when nodeParameters is absent', async () => { - const { service, ephemeralNodeExecutor } = makeService(); - ephemeralNodeExecutor.executeInline.mockResolvedValue({ status: 'success' } as never); - - await getRunTool(service).handler!( - { nodeType: 'n8n-nodes-base.httpRequest', nodeTypeVersion: 4 }, - ctx, - ); - - expect(validateNodeConfig).not.toHaveBeenCalled(); - expect(ephemeralNodeExecutor.executeInline).toHaveBeenCalled(); - }); - - it('validates nodeParameters via validateNodeConfig before executing', async () => { - const { service, ephemeralNodeExecutor } = makeService(); - ephemeralNodeExecutor.executeInline.mockResolvedValue({ status: 'success' } as never); - - await getRunTool(service).handler!( - { - nodeType: 'n8n-nodes-base.httpRequest', - nodeTypeVersion: 4, - nodeParameters: { url: 'https://example.com' }, - }, - ctx, - ); - - expect(validateNodeConfig).toHaveBeenCalledWith( - 'n8n-nodes-base.httpRequest', - 4, - { - parameters: { url: 'https://example.com' }, - }, - { isToolNode: true }, - ); - expect(ephemeralNodeExecutor.executeInline).toHaveBeenCalled(); - }); - - it('returns an error and skips executeInline when validation fails', async () => { - jest.mocked(validateNodeConfig).mockReturnValue({ - valid: false, - errors: [{ path: 'method', message: 'Field "method" has invalid value.' }], - }); - const { service, ephemeralNodeExecutor } = makeService(); - - const result = await getRunTool(service).handler!( - { - nodeType: 'n8n-nodes-base.httpRequest', - nodeTypeVersion: 4, - nodeParameters: { method: 'DELETE' }, - }, - ctx, - ); - - expect(ephemeralNodeExecutor.executeInline).not.toHaveBeenCalled(); - expect(result).toMatchObject({ - status: 'error', - message: expect.stringContaining('"method"'), - }); - }); - - it('returns a structured error when executeInline throws', async () => { - const { service, ephemeralNodeExecutor, logger } = makeService(); - ephemeralNodeExecutor.executeInline.mockRejectedValue(new Error('boom')); - - const result = await getRunTool(service).handler!( - { nodeType: 'n8n-nodes-base.httpRequest', nodeTypeVersion: 4 }, - ctx, - ); - - expect(result).toMatchObject({ - status: 'error', - message: expect.stringContaining('boom'), - }); - expect(logger.warn).toHaveBeenCalled(); - }); - - it('maps inputData, passes credentials and projectId through to executeInline', async () => { - const executionResult = { status: 'success', data: [{ json: { ok: true } }] }; - const { service, ephemeralNodeExecutor } = makeService(); - ephemeralNodeExecutor.executeInline.mockResolvedValue(executionResult as never); - - const result = await getRunTool(service).handler!( - { - nodeType: 'n8n-nodes-base.gmail', - nodeTypeVersion: 2, - credentials: { gmailOAuth2: { id: 'cred-1', name: 'My Gmail' } }, - inputData: { to: 'user@example.com' }, - }, - ctx, - ); - - expect(ephemeralNodeExecutor.executeInline).toHaveBeenCalledWith( - expect.objectContaining({ - credentialDetails: { gmailOAuth2: { id: 'cred-1', name: 'My Gmail' } }, - inputData: [{ json: { to: 'user@example.com' } }], - projectId: 'project-1', - }), - ); - expect(result).toEqual(executionResult); - }); - }); }); diff --git a/packages/cli/src/modules/agents/agent-config.service.ts b/packages/cli/src/modules/agents/agent-config.service.ts index ba59670c06f..f2af9f4af3a 100644 --- a/packages/cli/src/modules/agents/agent-config.service.ts +++ b/packages/cli/src/modules/agents/agent-config.service.ts @@ -1,13 +1,11 @@ import { extractFromAIParameters } from '@n8n/ai-utilities/fromai-helpers'; import { AgentJsonConfigSchema, - isNodeToolsEnabled, sanitizeAgentJsonConfig, type AgentJsonConfig, type AgentJsonToolConfig, } from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; -import { AgentsConfig } from '@n8n/config'; import { Service } from '@n8n/di'; import { UserError, type INodeParameters } from 'n8n-workflow'; @@ -34,15 +32,10 @@ export class AgentConfigService { private readonly agentRepository: AgentRepository, private readonly agentTaskRepository: AgentTaskRepository, private readonly agentSkillsService: AgentSkillsService, - private readonly agentsConfig: AgentsConfig, private readonly runtimeCacheService: AgentRuntimeCacheService, private readonly credentialsService: CredentialsService, ) {} - private isNodeToolsModuleEnabled(): boolean { - return this.agentsConfig.modules.includes('node-tools-searcher'); - } - /** * Get the JSON config for an agent. */ @@ -74,14 +67,6 @@ export class AgentConfigService { const config = parsed.data; - if (isNodeToolsEnabled(config.config) && !this.isNodeToolsModuleEnabled()) { - return { - valid: false, - error: - 'config.nodeTools.enabled requires the node-tools-searcher agents module to be enabled.', - }; - } - try { this.validateNodeToolExpressions(config); } catch (error) { diff --git a/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts b/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts index 6438e5c92e9..43aa7954a43 100644 --- a/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts +++ b/packages/cli/src/modules/agents/agent-runtime-reconstruction.service.ts @@ -7,7 +7,6 @@ import { ToolDescriptor, } from '@n8n/agents'; import { - isNodeToolsEnabled, N8N_CHAT_ACTION_TOOL_NAME, N8N_CHAT_CONTEXT_TOOL_NAME, N8N_CHAT_INTEGRATION_TYPE, @@ -38,7 +37,6 @@ import { createAiMcpFetch, createAiProxyFetch } from '@/utils/ai-proxy-fetch'; import { WorkflowRunner } from '@/workflow-runner'; import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; -import { AgentsToolsService } from './agents-tools.service'; import { Agent } from './entities/agent.entity'; import { ChatIntegrationRegistry } from './integrations/agent-chat-integration'; import { @@ -133,7 +131,6 @@ export class AgentRuntimeReconstructionService { private readonly n8nCheckpointStorage: N8NCheckpointStorage, private readonly secureRuntime: AgentSecureRuntime, private readonly ephemeralNodeExecutor: EphemeralNodeExecutor, - private readonly agentsToolsService: AgentsToolsService, private readonly n8nMemory: N8nMemory, private readonly oauthService: OauthService, private readonly agentsConfig: AgentsConfig, @@ -272,7 +269,6 @@ export class AgentRuntimeReconstructionService { userId, runtimeProfile, config, - nodeToolsEnabled: this.shouldAttachNodeTools(config.config), subAgentDelegation, parentAgentIdForDelegation: parentAgentIdForDelegation ?? memoryOwnerAgentId, integrationType, @@ -312,14 +308,6 @@ export class AgentRuntimeReconstructionService { return (_params: AgentJsonMemoryConfig) => this.n8nMemory.getImplementation(agentId); } - private shouldAttachNodeTools(config: AgentJsonConfig['config']): boolean { - return this.isNodeToolsModuleEnabled() && isNodeToolsEnabled(config); - } - - private isNodeToolsModuleEnabled(): boolean { - return this.agentsConfig.modules.includes('node-tools-searcher'); - } - private makeToolResolver(projectId: string, userId: string): ToolResolver { return async (ref: AgentJsonToolConfig) => { if (ref.type === 'workflow') { @@ -359,7 +347,6 @@ export class AgentRuntimeReconstructionService { userId: string; runtimeProfile: AgentRuntimeProfile; config: AgentJsonConfig; - nodeToolsEnabled: boolean; subAgentDelegation: SubAgentDelegationConfig; parentAgentIdForDelegation: string; integrationType?: string; @@ -373,7 +360,6 @@ export class AgentRuntimeReconstructionService { userId, runtimeProfile, config, - nodeToolsEnabled, subAgentDelegation, parentAgentIdForDelegation, integrationType, @@ -449,10 +435,6 @@ export class AgentRuntimeReconstructionService { } } - if (nodeToolsEnabled) { - agent.tool(this.agentsToolsService.getRuntimeTools(credentialProvider, projectId)); - } - if (runtimeProfile === 'top-level') { await this.attachSubAgentDelegationTool({ agent, diff --git a/packages/cli/src/modules/agents/agent-telemetry.ts b/packages/cli/src/modules/agents/agent-telemetry.ts index d7e4da9aa8a..cdb2e4920a3 100644 --- a/packages/cli/src/modules/agents/agent-telemetry.ts +++ b/packages/cli/src/modules/agents/agent-telemetry.ts @@ -23,7 +23,6 @@ export function buildAgentConfigurationTelemetryFromConfig( ]); const providerToolCount = Object.keys(config?.providerTools ?? {}).length; const hasWebSearch = config?.config?.webSearch?.enabled === true; - const hasNodeTools = config?.config?.nodeTools?.enabled === true; const mcpServerCount = config?.mcpServers?.length ?? 0; const subAgentCount = config?.subAgents?.agents?.length ?? 0; @@ -31,7 +30,6 @@ export function buildAgentConfigurationTelemetryFromConfig( if (mcpServerCount > 0) toolTypes.add('mcp'); if (providerToolCount > 0) toolTypes.add('provider'); if (hasWebSearch) toolTypes.add('web_search'); - if (hasNodeTools) toolTypes.add('node_tools'); if (subAgentCount > 0) toolTypes.add('subagent'); return { @@ -43,7 +41,6 @@ export function buildAgentConfigurationTelemetryFromConfig( mcpServerCount + providerToolCount + (hasWebSearch ? 1 : 0) + - (hasNodeTools ? 1 : 0) + subAgentCount, num_skills: config?.skills?.length ?? 0, memory_type: getMemoryType(config), diff --git a/packages/cli/src/modules/agents/agents-tools.service.ts b/packages/cli/src/modules/agents/agents-tools.service.ts index 3781f6f3d9c..2b4496a2814 100644 --- a/packages/cli/src/modules/agents/agents-tools.service.ts +++ b/packages/cli/src/modules/agents/agents-tools.service.ts @@ -4,17 +4,14 @@ import { AGENT_BUILDER_AVAILABLE_AI_UTILITY_TOOL_NODE_TYPES, AGENT_BUILDER_HIDDEN_AVAILABLE_TOOL_NODE_TYPES, } from '@n8n/api-types'; -import { Logger } from '@n8n/backend-common'; import { Service } from '@n8n/di'; -import { validateNodeConfig } from '@n8n/workflow-sdk'; import { isToolType, isTriggerNodeType } from 'n8n-workflow'; -import type { IDataObject, INodeParameters } from 'n8n-workflow'; import { z } from 'zod'; import { MCP_REGISTRY_PACKAGE_NAME } from '../mcp-registry/node-description-transform'; import { NodeCatalogService } from '@/node-catalog'; -import { EphemeralNodeExecutor, isAgentProviderNode } from '@/node-execution'; +import { isAgentProviderNode } from '@/node-execution'; type NodeRequest = | string @@ -104,39 +101,13 @@ const listCredentialsInputSchema = z.object({ ), }); -const runNodeInputSchema = z.object({ - nodeType: z.string().describe('Tool node type identifier from search_nodes'), - nodeTypeVersion: nodeVersionSchema, - nodeParameters: z - .record(z.unknown()) - .optional() - .describe( - 'Static node config. Use expressions like ={{ $json.url }} to reference inputData fields.', - ), - credentials: z - .record(z.object({ id: z.string(), name: z.string() })) - .optional() - .describe('Credential slot → { id, name }. Copy from list_credentials results.'), - inputData: z - .record(z.unknown()) - .optional() - .describe('Runtime input, available as $json inside nodeParameters expressions.'), -}); - @Service() export class AgentsToolsService { - constructor( - private readonly logger: Logger, - private readonly nodeCatalogService: NodeCatalogService, - private readonly ephemeralNodeExecutor: EphemeralNodeExecutor, - ) {} + constructor(private readonly nodeCatalogService: NodeCatalogService) {} /** - * Tools usable from both the builder and the agent runtime. - * - * `listCredentialsUsageHint` lets each caller tailor the `list_credentials` - * description to its flow — the runtime points at `run_node_tool`, while the - * builder points at code generation. + * Tools usable by the builder while configuring node-backed tools. + * `listCredentialsUsageHint` lets callers tailor the credential guidance. */ getSharedTools( credentialProvider: CredentialProvider, @@ -149,17 +120,6 @@ export class AgentsToolsService { ]; } - /** Shared tools plus the runtime-only `run_node_tool` which binds to a project. */ - getRuntimeTools(credentialProvider: CredentialProvider, projectId: string): BuiltTool[] { - return [ - ...this.getSharedTools( - credentialProvider, - 'Call this before run_node_tool to know which credential to pass.', - ), - this.buildRunNodeTool(projectId), - ]; - } - private buildSearchNodesTool(): BuiltTool { return new Tool('search_nodes') .description( @@ -216,70 +176,11 @@ export class AgentsToolsService { }) .build(); } - - private buildRunNodeTool(projectId: string): BuiltTool { - return new Tool('run_node_tool') - .description( - 'Execute an n8n node for the current request. ' + - 'Use the tool nodeType and nodeTypeVersion from search_nodes. ' + - 'Call get_node_types first to understand what nodeParameters the node accepts. ' + - 'nodeParameters holds static node config; use n8n expressions like ={{ $json.url }} to map inputData fields. ' + - 'credentials maps slot names to { id, name } — copy from the list_credentials results. ' + - 'inputData is the runtime payload available as $json inside expressions. ' + - 'Parameters are validated against the node schema before execution.', - ) - .input(runNodeInputSchema) - .handler(async ({ nodeType, nodeTypeVersion, nodeParameters, credentials, inputData }) => { - if (!isExecutableNodeType(nodeType)) { - return { - status: 'error', - message: `Node type "${nodeType}" cannot be executed directly — trigger nodes are not supported here.`, - }; - } - - if (nodeParameters) { - const { valid, errors } = validateNodeConfig( - nodeType, - nodeTypeVersion, - { - parameters: nodeParameters, - }, - { isToolNode: true }, - ); - if (!valid) { - return { - status: 'error', - message: `Invalid nodeParameters: ${errors.map((e) => e.message).join('; ')}`, - }; - } - } - - try { - return await this.ephemeralNodeExecutor.executeInline({ - nodeType, - nodeTypeVersion, - nodeParameters: (nodeParameters ?? {}) as INodeParameters, - credentialDetails: credentials, - inputData: [{ json: (inputData ?? {}) as IDataObject }], - projectId, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - this.logger.warn('run_node_tool execution failed', { nodeType, error }); - return { - status: 'error', - message: `Node execution failed: ${message}`, - }; - } - }) - .build(); - } } /** * The catalog's `getNodeTypes` signature expects `version` as a string (matching the - * code-builder tool's wire format). Our public schema uses `number` for consistency - * with `run_node_tool`; adapt at the boundary. + * builder tool's wire format); adapt at the boundary. */ function normalizeNodeRequestForCatalog(req: NodeRequest): | string diff --git a/packages/cli/src/node-execution/ephemeral-node-executor.ts b/packages/cli/src/node-execution/ephemeral-node-executor.ts index 5032d3adea2..95a26d48abd 100644 --- a/packages/cli/src/node-execution/ephemeral-node-executor.ts +++ b/packages/cli/src/node-execution/ephemeral-node-executor.ts @@ -331,9 +331,9 @@ export class EphemeralNodeExecutor { // Validation failures (unknown node type, trigger nodes, blacklisted // operations like send-and-wait) need to surface to the agent as a // tool error rather than crashing silently. Returning the standard - // `{ status: 'error', error }` shape lets `run_node_tool` translate - // it into a tool-result the LLM sees AND lets the ExecutionRecorder - // record it as a failed tool call in the session timeline. + // `{ status: 'error', error }` shape lets node tools surface the error + // to the LLM and lets the ExecutionRecorder record it as a failed tool + // call in the session timeline. try { this.validateNodeForExecution( request.nodeType, diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index eba2728f399..c2aae86c497 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -6748,8 +6748,6 @@ "agents.builder.tools.title": "Tools", "agents.builder.tools.count": "{count} tool configured | {count} tools configured", "agents.builder.tools.add": "Add tool", - "agents.builder.tools.builtIn.title": "Built-in node tools", - "agents.builder.tools.builtIn.hint": "Let the agent search the n8n node catalog and execute nodes on demand.", "agents.builder.tools.empty": "No tools added yet.", "agents.builder.tools.type.workflow": "Workflow", "agents.builder.tools.type.custom": "Custom tool", diff --git a/packages/frontend/editor-ui/src/app/stores/settings.store.ts b/packages/frontend/editor-ui/src/app/stores/settings.store.ts index 5be33a89ca8..149db76ba0a 100644 --- a/packages/frontend/editor-ui/src/app/stores/settings.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/settings.store.ts @@ -183,12 +183,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { return isOtelCustomSpanAttributesLicensed && isOtelModuleActive; }); - // Opt-in flag: the `node-tools-searcher` token must be listed in the backend - // `N8N_AGENTS_MODULES` env var for this to evaluate true. - const isAgentsNodeToolsFeatureEnabled = computed(() => - isAgentModuleActive('node-tools-searcher'), - ); - // Opt-in flag: requires `N8N_AGENTS_AI_SANDBOX_ENABLED=true` and // `N8N_AGENTS_AI_SANDBOX_PROVIDER=daytona` on the backend. const isAgentsKnowledgeBaseFeatureEnabled = computed( @@ -492,7 +486,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { isDataTableFeatureEnabled, isChatFeatureEnabled, isOtelCustomSpanAttributesEnabled, - isAgentsNodeToolsFeatureEnabled, isAgentsKnowledgeBaseFeatureEnabled, isPublicChatTriggerDisabled, }; diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentToolsListPanel.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentToolsListPanel.vue index f18be295f9a..e7bd7d74c4a 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentToolsListPanel.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentToolsListPanel.vue @@ -15,14 +15,12 @@ import { N8nIcon, N8nIconButton, N8nCard, - N8nSwitch2, N8nText, N8nTooltip, } from '@n8n/design-system'; import { useI18n } from '@n8n/i18n'; import { useNodeHelpers } from '@/app/composables/useNodeHelpers'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; -import { useSettingsStore } from '@/app/stores/settings.store'; import type { INode, INodeTypeDescription } from 'n8n-workflow'; import type { INodeUi } from '@/Interface'; @@ -49,22 +47,6 @@ const emit = defineEmits<{ 'remove-tool': [index: number]; }>(); -const settingsStore = useSettingsStore(); -const nodeToolsFeatureEnabled = computed(() => settingsStore.isAgentsNodeToolsFeatureEnabled); - -const nodeToolsEnabled = computed( - () => props.config?.config?.nodeTools?.enabled !== false, -); - -function setNodeToolsEnabled(enabled: boolean) { - emit('update:config', { - config: { - ...(props.config?.config ?? {}), - nodeTools: { enabled }, - }, - }); -} - const i18n = useI18n(); const nodeTypesStore = useNodeTypesStore(); const nodeHelpers = useNodeHelpers(); @@ -169,21 +151,6 @@ const totalCount = computed(() => props.tools.length); -
-
- {{ i18n.baseText('agents.builder.tools.builtIn.title') }} - - {{ i18n.baseText('agents.builder.tools.builtIn.hint') }} - -
- -
-
{{ i18n.baseText('agents.builder.tools.empty') @@ -337,21 +304,6 @@ const totalCount = computed(() => props.tools.length); scrollbar-color: var(--border-color) transparent; } -.toggleRow { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing--sm); - padding: var(--spacing--xs) 0; - border-bottom: var(--border); -} - -.toggleText { - display: flex; - flex-direction: column; - gap: var(--spacing--5xs); -} - .empty { padding: var(--spacing--lg); text-align: center;