n8n
Version:
n8n Workflow Automation Tool
408 lines • 19.2 kB
JavaScript
;
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