UNPKG

n8n

Version:

n8n Workflow Automation Tool

408 lines • 19.2 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AgentValidationService = void 0; const agent_config_1 = require("@n8n/ai-utilities/agent-config"); const node_catalog_1 = require("@n8n/ai-utilities/node-catalog"); const api_types_1 = require("@n8n/api-types"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const n8n_workflow_1 = require("n8n-workflow"); const agent_missing_skill_ids_1 = require("../../modules/agents/utils/agent-missing-skill-ids"); const node_types_1 = require("../../node-types"); const llm_provider_defaults_1 = require("./llm-provider-defaults"); const agent_chat_integration_1 = require("./integrations/agent-chat-integration"); const cron_validation_1 = require("./integrations/cron-validation"); const agent_task_snapshot_repository_1 = require("./repositories/agent-task-snapshot.repository"); const agent_task_repository_1 = require("./repositories/agent-task.repository"); const agent_repository_1 = require("./repositories/agent.repository"); const workflow_tool_factory_1 = require("./tools/workflow-tool-factory"); const workflow_tool_workflow_resolver_1 = require("./tools/workflow-tool-workflow-resolver"); function issue(code, path, capability) { return { code, path, capability }; } function agentIssue(code, path) { return issue(code, path, { kind: 'agent' }); } let AgentValidationService = class AgentValidationService { constructor(agentRepository, agentTaskRepository, agentTaskSnapshotRepository, nodeTypes, workflowRepository, chatIntegrationRegistry) { this.agentRepository = agentRepository; this.agentTaskRepository = agentTaskRepository; this.agentTaskSnapshotRepository = agentTaskSnapshotRepository; this.nodeTypes = nodeTypes; this.workflowRepository = workflowRepository; this.chatIntegrationRegistry = chatIntegrationRegistry; } async validateAgentIsRunnable(agentId, projectId, credentialProvider) { const { issues } = await this.validateAgentConfiguration(agentId, projectId, credentialProvider, 'runtime'); return { missing: issues.map((i) => i.path) }; } async validateAgentConfiguration(agentId, projectId, credentialProvider, scope = 'publish') { const agentEntity = await this.agentRepository.findByIdAndProjectId(agentId, projectId); if (!agentEntity) { return { status: 'invalid', issues: [agentIssue('missing_required', 'agent')] }; } return await this.validateLoadedAgentConfiguration(agentEntity, projectId, credentialProvider, scope); } async validateLoadedAgentConfiguration(agent, projectId, credentialProvider, scope = 'publish') { const tasks = scope === 'publish' ? new Map((await this.agentTaskRepository.findByAgentId(agent.id)).map((task) => [task.id, task])) : new Map(); return await this.validateAgentEntityConfiguration(agent, projectId, tasks, credentialProvider, scope); } async validateAgentEntityConfiguration(agent, projectId, tasks, credentialProvider, scope = 'publish', integrationsOverride) { return await this.runValidation({ agentId: agent.id, projectId, config: agent.schema, skills: agent.skills ?? {}, customTools: agent.tools ?? {}, integrations: integrationsOverride ?? agent.integrations ?? [], tasks, credentialProvider, }, scope); } async validateAgentHistoryConfiguration(agentId, projectId, history, currentIntegrations, credentialProvider) { const tasks = new Map((await this.agentTaskSnapshotRepository.findByVersionId(history.versionId)).map((snapshot) => [snapshot.taskId, snapshot])); return await this.runValidation({ agentId, projectId, config: history.schema, skills: history.skills ?? {}, customTools: history.tools ?? {}, integrations: currentIntegrations, tasks, credentialProvider, }, 'publish'); } async runValidation(ctx, scope = 'publish') { if (!ctx.config) { return { status: 'invalid', issues: this.missingConfigIssues() }; } const issues = await this.collectIssues({ ...ctx, config: ctx.config }, scope); return { status: issues.length === 0 ? 'valid' : 'invalid', issues }; } missingConfigIssues() { return [ agentIssue('missing_required', 'instructions'), agentIssue('missing_required', 'model'), agentIssue('missing_credential', 'credential'), ]; } async collectIssues(ctx, scope) { const { config } = ctx; const issues = []; let credentialList; const findCredential = async (credentialId) => { credentialList ??= await ctx.credentialProvider.list(); return credentialList.find((credential) => credential.id === credentialId); }; const { agentsById, workflowsByName } = await this.prefetchReferenceLookups(ctx); this.collectCoreIssues(config, issues); this.collectVectorStoreIssues(config, issues); await this.collectMainCredentialIssues(config, findCredential, issues); this.collectSubAgentRefIssues(ctx, agentsById, issues); this.collectSkillIssues(config, ctx.skills, issues); if (scope === 'publish') { this.collectTaskIssues(config, ctx.tasks, issues); await this.collectChannelIssues(ctx.integrations, findCredential, issues); } await this.collectToolIssues(ctx, findCredential, workflowsByName, issues); await this.collectMcpServerIssues(config, findCredential, issues); return this.dedupe(issues); } async prefetchReferenceLookups(ctx) { const subAgentIds = new Set(); const workflowNames = new Set(); for (const ref of ctx.config.subAgents?.agents ?? []) { if (ref.agentId && ref.agentId !== ctx.agentId) { subAgentIds.add(ref.agentId); } } for (const tool of ctx.config.tools ?? []) { if (tool.type === 'workflow' && tool.workflow) { workflowNames.add(tool.workflow); } } const [agents, workflowsByName] = await Promise.all([ this.agentRepository.findByIdsAndProjectId([...subAgentIds], ctx.projectId), (0, workflow_tool_workflow_resolver_1.findWorkflowToolWorkflows)(this.workflowRepository, [...workflowNames], ctx.projectId), ]); return { agentsById: new Map(agents.map((agent) => [agent.id, agent])), workflowsByName, }; } dedupe(issues) { const seen = new Set(); const result = []; for (const item of issues) { const key = `${item.code}:${item.path}`; if (seen.has(key)) continue; seen.add(key); result.push(item); } return result; } collectCoreIssues(config, issues) { if (!config.instructions?.trim()) { issues.push(agentIssue('missing_required', 'instructions')); } if ((0, api_types_1.isDraftAgentConfig)(config)) { issues.push(agentIssue('missing_required', 'model')); } else if (!api_types_1.AgentModelSchema.safeParse(config.model).success) { issues.push(agentIssue('invalid_value', 'model')); } } async collectMainCredentialIssues(config, findCredential, issues) { if (!config.credential?.trim()) { issues.push(agentIssue('missing_credential', 'credential')); return; } const credentialId = config.credential.trim(); const credential = await this.findCredentialSafe(findCredential, credentialId); if (!credential) { issues.push(agentIssue('invalid_credential', 'credential')); return; } const model = config.model?.trim(); if (model && api_types_1.AgentModelSchema.safeParse(model).success && !this.credentialSupportsModel(credential.type, model)) { issues.push(agentIssue('incompatible_credential', 'credential')); } } collectSubAgentRefIssues(ctx, agentsById, issues) { const refs = ctx.config.subAgents?.agents ?? []; for (let index = 0; index < refs.length; index++) { const ref = refs[index]; const path = `subAgents.agents.${index}.agentId`; const capability = { kind: 'subAgent', id: ref.agentId, index, }; if (ref.agentId === ctx.agentId) { issues.push(issue('incompatible_reference', path, capability)); continue; } const target = agentsById.get(ref.agentId); if (!target) { issues.push(issue('missing_reference', path, capability)); continue; } if (!target.activeVersionId) { issues.push(issue('incompatible_reference', path, capability)); } } } collectSkillIssues(config, skills, issues) { for (const skillId of (0, agent_missing_skill_ids_1.getMissingSkillIds)(config, skills)) { issues.push(issue('missing_reference', `skill:${skillId}`, { kind: 'skill', id: skillId })); } } collectTaskIssues(config, tasks, issues) { const refs = config.tasks ?? []; for (let index = 0; index < refs.length; index++) { const ref = refs[index]; const task = tasks.get(ref.id); if (!task) { issues.push(issue('missing_reference', `tasks.${index}.id`, { kind: 'task', id: ref.id, index })); continue; } if (!ref.enabled) continue; if (!api_types_1.agentTaskSchema.safeParse(task).success || !(0, cron_validation_1.isValidCronExpression)(task.cronExpression)) { issues.push(issue('invalid_value', `tasks.${index}`, { kind: 'task', id: ref.id, index })); } } } collectVectorStoreIssues(config, issues) { const collisions = new Set((0, api_types_1.findVectorStoreToolNameCollisions)(config)); const stores = config.vectorStores ?? []; for (let index = 0; index < stores.length; index++) { const store = stores[index]; if (!collisions.has(`search_${store.name.replace(/-/g, '_')}`)) continue; issues.push(issue('invalid_value', `vectorStores.${index}.name`, { kind: 'vectorStore', id: store.name, index, })); } } async collectChannelIssues(integrations, findCredential, issues) { for (let index = 0; index < integrations.length; index++) { const integration = integrations[index]; const path = `integrations.${index}.credentialId`; const capability = { kind: 'channel', id: integration.type, index, }; if ((0, api_types_1.isDraftIntegration)(integration)) { issues.push(issue('missing_credential', path, capability)); continue; } const credentialId = integration.credentialId.trim(); const credential = await this.findCredentialSafe(findCredential, credentialId); if (!credential) { issues.push(issue('invalid_credential', path, capability)); continue; } const integrationImpl = this.chatIntegrationRegistry.get(integration.type); if (integrationImpl && !integrationImpl.credentialTypes.includes(credential.type)) { issues.push(issue('incompatible_credential', path, capability)); } } } async collectToolIssues(ctx, findCredential, workflowsByName, issues) { const tools = ctx.config.tools ?? []; for (let index = 0; index < tools.length; index++) { const tool = tools[index]; if (tool.type === 'custom') { if (!ctx.customTools[tool.id]) { issues.push(issue('missing_reference', `tools.${index}.id`, { kind: 'tool', id: tool.id, index, toolType: 'custom', })); } continue; } if (tool.type === 'workflow') { this.collectWorkflowToolIssues(tool, index, workflowsByName, issues); continue; } if (tool.type === 'node') { await this.collectNodeToolIssues(tool, index, findCredential, issues); } } } collectWorkflowToolIssues(tool, index, workflowsByName, issues) { const path = `tools.${index}.workflow`; const capability = { kind: 'tool', id: tool.name ?? tool.workflow, index, toolType: 'workflow', }; const workflow = workflowsByName.get(tool.workflow); if (!workflow) { issues.push(issue('missing_reference', path, capability)); return; } try { (0, workflow_tool_factory_1.validateCompatibility)(workflow); (0, workflow_tool_factory_1.detectTriggerNode)(workflow); } catch { issues.push(issue('incompatible_reference', path, capability)); } } async collectNodeToolIssues(tool, index, findCredential, issues) { const capabilityBase = { kind: 'tool', id: tool.name, index, toolType: 'node', }; let nodeType; try { nodeType = this.nodeTypes.getByNameAndVersion(tool.node.nodeType, tool.node.nodeTypeVersion); } catch { issues.push(issue('missing_reference', `tools.${index}.node.nodeType`, capabilityBase)); return; } const nodeParameters = n8n_workflow_1.NodeHelpers.getNodeParameters(nodeType.description.properties, (tool.node.nodeParameters ?? {}), true, false, { typeVersion: tool.node.nodeTypeVersion }, nodeType.description) ?? {}; const requiredSlots = (0, node_catalog_1.getRequiredNodeCredentialSlots)(nodeType.description); for (const slot of requiredSlots) { const credentialDefinition = nodeType.description.credentials?.find((credential) => credential.name === slot.credentialType); if (credentialDefinition && !n8n_workflow_1.NodeHelpers.displayParameter(nodeParameters, credentialDefinition, { typeVersion: tool.node.nodeTypeVersion }, nodeType.description)) { continue; } const path = `tools.${index}.node.credentials.${slot.credentialType}`; const credentialRef = tool.node.credentials?.[slot.credentialType]; const credentialId = credentialRef?.id?.trim(); if (!credentialId) { issues.push(issue('missing_credential', path, capabilityBase)); continue; } const credential = await this.findCredentialSafe(findCredential, credentialId); if (!credential || credential.type !== slot.credentialType) { issues.push(issue('invalid_credential', path, capabilityBase)); } } } async collectMcpServerIssues(config, findCredential, issues) { const servers = config.mcpServers ?? []; for (let index = 0; index < servers.length; index++) { const server = servers[index]; const capability = { kind: 'mcpServer', id: server.name, index, }; if (!server.url?.trim()) { issues.push(issue('missing_required', `mcpServers.${index}.url`, capability)); continue; } if (server.authentication === 'none') continue; const credentialId = server.credential?.trim(); if (!credentialId) { issues.push(issue('missing_credential', `mcpServers.${index}.credential`, capability)); continue; } const credential = await this.findCredentialSafe(findCredential, credentialId); if (!credential) { issues.push(issue('invalid_credential', `mcpServers.${index}.credential`, capability)); } else if (!this.mcpCredentialTypeMatches(server.authentication, credential.type)) { issues.push(issue('incompatible_credential', `mcpServers.${index}.credential`, capability)); } } } mcpCredentialTypeMatches(authentication, credentialType) { switch (authentication) { case 'bearerAuth': return credentialType === 'httpBearerAuth'; case 'headerAuth': return credentialType === 'httpHeaderAuth'; case 'multipleHeadersAuth': return credentialType === 'httpMultipleHeadersAuth'; default: return (0, n8n_workflow_1.isMcpOAuth2Authentication)(authentication) ? credentialType === authentication : true; } } credentialSupportsModel(credentialType, model) { return llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credentialType]?.provider === (0, agent_config_1.getProviderPrefix)(model); } async findCredentialSafe(findCredential, credentialId) { try { return await findCredential(credentialId); } catch { return undefined; } } }; exports.AgentValidationService = AgentValidationService; exports.AgentValidationService = AgentValidationService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [agent_repository_1.AgentRepository, agent_task_repository_1.AgentTaskRepository, agent_task_snapshot_repository_1.AgentTaskSnapshotRepository, node_types_1.NodeTypes, db_1.WorkflowRepository, agent_chat_integration_1.ChatIntegrationRegistry]) ], AgentValidationService); //# sourceMappingURL=agent-validation.service.js.map