UNPKG

n8n

Version:

n8n Workflow Automation Tool

988 lines • 49.3 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); }; var InstanceAiSettingsService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.InstanceAiSettingsService = exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY = exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY = exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY = exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY = void 0; const node_util_1 = require("node:util"); const api_types_1 = require("@n8n/api-types"); const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const permissions_1 = require("@n8n/permissions"); const ensure_error_1 = require("@n8n/utils/errors/ensure-error"); const n8n_workflow_1 = require("n8n-workflow"); const credentials_finder_service_1 = require("../../credentials/credentials-finder.service"); const credentials_service_1 = require("../../credentials/credentials.service"); const instance_credential_broker_1 = require("../../credentials/instance-credential-broker"); const conflict_error_1 = require("../../errors/response-errors/conflict.error"); const forbidden_error_1 = require("../../errors/response-errors/forbidden.error"); const unprocessable_error_1 = require("../../errors/response-errors/unprocessable.error"); const event_service_1 = require("../../events/event.service"); const ai_service_1 = require("../../services/ai.service"); const user_service_1 = require("../../services/user.service"); const sandbox_provider_1 = require("./sandbox-provider"); const ADMIN_SETTINGS_KEY = 'instanceAi.settings'; const N8N_SANDBOX_HEADER_NAME = 'x-api-key'; const MODEL_PROVIDER_API_KEY_ENV = new Map([ ['anthropic', 'ANTHROPIC_API_KEY'], ['cohere', 'COHERE_API_KEY'], ['deepseek', 'DEEPSEEK_API_KEY'], ['google', 'GOOGLE_GENERATIVE_AI_API_KEY'], ['groq', 'GROQ_API_KEY'], ['mistral', 'MISTRAL_API_KEY'], ['openai', 'OPENAI_API_KEY'], ['openrouter', 'OPENROUTER_API_KEY'], ['xai', 'XAI_API_KEY'], ]); const CREDENTIAL_TO_MODEL_PROVIDER = { openAiApi: 'openai', anthropicApi: 'anthropic', googlePalmApi: 'google', groqApi: 'groq', deepSeekApi: 'deepseek', mistralCloudApi: 'mistral', xAiApi: 'xai', openRouterApi: 'openrouter', cohereApi: 'cohere', }; const URL_FIELD_MAP = { openAiApi: 'url', anthropicApi: 'url', googlePalmApi: 'host', }; function requireConnectionValue(type, data, field) { const value = data[field]; if (typeof value !== 'string' || value.trim().length === 0) { throw new unprocessable_error_1.UnprocessableRequestError(`The field "${field}" is required for provider connection type "${type}"`); } return value.trim(); } function requireHttpUrl(type, data, field) { const value = requireConnectionValue(type, data, field); try { const url = new URL(value); if (url.protocol === 'http:' || url.protocol === 'https:') return; } catch { } throw new unprocessable_error_1.UnprocessableRequestError(`The field "${field}" must be a valid HTTP URL for provider connection type "${type}"`); } function validateModelCredential({ type, data, }) { const apiKey = data.apiKey; if (typeof apiKey === 'string' && apiKey.trim().length > 0) return; const urlField = URL_FIELD_MAP[type]; const url = urlField === undefined ? undefined : data[urlField]; if (typeof url === 'string' && url.trim().length > 0) return; throw new unprocessable_error_1.UnprocessableRequestError(urlField === undefined ? `The field "apiKey" is required for provider connection type "${type}"` : `The field "apiKey" or "${urlField}" is required for provider connection type "${type}"`); } function modelCredentialHeaders(credentialType, data) { const headers = {}; if (credentialType === 'openAiApi' && typeof data.organizationId === 'string') { const organizationId = data.organizationId.trim(); if (organizationId) headers['OpenAI-Organization'] = organizationId; } if ((credentialType === 'openAiApi' || credentialType === 'anthropicApi') && data.header === true && typeof data.headerName === 'string' && typeof data.headerValue === 'string') { const headerName = data.headerName.trim(); if (headerName) headers[headerName] = data.headerValue; } return Object.keys(headers).length ? headers : undefined; } function validateSandboxServiceCredential({ type, data, }) { const headerName = requireConnectionValue(type, data, 'name').toLowerCase(); if (headerName !== N8N_SANDBOX_HEADER_NAME) { throw new unprocessable_error_1.UnprocessableRequestError(`The credential's header name must be "${N8N_SANDBOX_HEADER_NAME}" but is "${headerName}"`); } requireConnectionValue(type, data, 'value'); } function validateDaytonaCredential({ type, data, }) { requireHttpUrl(type, data, 'apiUrl'); requireConnectionValue(type, data, 'apiKey'); } function validateSearchCredential({ type, data, }) { if (type === 'searXngApi') requireHttpUrl(type, data, 'apiUrl'); else requireConnectionValue(type, data, 'apiKey'); } exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY = { id: 'instance-ai:model', credentialTypes: api_types_1.INSTANCE_AI_MODEL_CREDENTIAL_TYPES, validate: validateModelCredential, }; exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY = { id: 'instance-ai:sandbox:daytona', credentialTypes: ['daytonaApi'], validate: validateDaytonaCredential, }; exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY = { id: 'instance-ai:sandbox:n8n', credentialTypes: ['httpHeaderAuth'], validate: validateSandboxServiceCredential, }; exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY = { id: 'instance-ai:search', credentialTypes: api_types_1.INSTANCE_AI_SEARCH_CREDENTIAL_TYPES, validate: validateSearchCredential, }; function validateInstanceAiCredential(policy, credential) { if (policy === exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY) { validateModelCredential(credential); } else if (policy === exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY) { validateDaytonaCredential(credential); } else if (policy === exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY) { validateSandboxServiceCredential(credential); } else if (policy === exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY) { validateSearchCredential(credential); } else { throw new n8n_workflow_1.UnexpectedError(`Unknown instance AI credential policy "${policy.id}"`); } } let InstanceAiSettingsService = InstanceAiSettingsService_1 = class InstanceAiSettingsService { constructor(globalConfig, dbLockService, settingsRepository, userRepository, userService, aiService, credentialsService, credentialsFinderService, instanceCredentialBroker, eventService) { this.dbLockService = dbLockService; this.settingsRepository = settingsRepository; this.userRepository = userRepository; this.userService = userService; this.aiService = aiService; this.credentialsService = credentialsService; this.credentialsFinderService = credentialsFinderService; this.instanceCredentialBroker = instanceCredentialBroker; this.eventService = eventService; this.enabled = true; this.mcpAccessEnabled = true; this.permissions = { ...api_types_1.DEFAULT_INSTANCE_AI_PERMISSIONS }; this.adminModelName = null; this.config = globalConfig.instanceAi; this.deploymentConfig = globalConfig.deployment; this.environmentSandboxProvider = (0, sandbox_provider_1.normalizeSandboxProvider)(this.config.sandboxProvider); this.config.sandboxProvider = this.environmentSandboxProvider; } get isCloud() { return this.deploymentConfig.type === 'cloud'; } isProxyEnabled() { return this.aiService.isProxyEnabled(); } async loadFromDb() { this.config.sandboxProvider = (0, sandbox_provider_1.normalizeSandboxProvider)(this.config.sandboxProvider); const envSnapshot = { sandboxEnabled: this.config.sandboxEnabled, sandboxProvider: this.config.sandboxProvider, }; await this.reloadFromDb(); const c = this.config; const overridden = c.sandboxEnabled !== envSnapshot.sandboxEnabled || c.sandboxProvider !== envSnapshot.sandboxProvider; const logger = di_1.Container.get(backend_common_1.Logger).scoped('instance-ai'); logger.info(`Sandbox: enabled=${c.sandboxEnabled} provider=${c.sandboxProvider}` + (overridden ? ` (DB override; env was enabled=${envSnapshot.sandboxEnabled} provider=${envSnapshot.sandboxProvider})` : ' (from env)')); const sandboxStatus = this.getSandboxStatus(); if (sandboxStatus.unavailableReason) { logger.warn(`Sandbox unavailable: ${sandboxStatus.unavailableReason}`); } } async getAdminSettings() { if (this.isCloud) { return this.buildAdminSettingsResponse({ modelCredentialId: null, modelName: null, daytonaCredentialId: null, n8nSandboxCredentialId: null, searchCredentialId: null, }); } if (this.aiService.isProxyEnabled()) { const n8nSandboxCredentialId = await this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY); return this.buildAdminSettingsResponse({ modelCredentialId: null, modelName: null, daytonaCredentialId: null, n8nSandboxCredentialId, searchCredentialId: null, }); } const [modelSelection, daytonaCredentialId, n8nSandboxCredentialId, searchCredentialId] = await Promise.all([ this.readAdminModelSelection(), this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY), this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY), this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY), ]); return this.buildAdminSettingsResponse({ ...modelSelection, daytonaCredentialId, n8nSandboxCredentialId, searchCredentialId, }); } buildAdminSettingsResponse(credentialSelection) { const c = this.config; const modelProviderApiKeyEnv = MODEL_PROVIDER_API_KEY_ENV.get(c.model.split('/', 1)[0] ?? ''); const isProxyEnabled = this.aiService.isProxyEnabled(); const isManaged = this.isCloud || isProxyEnabled; const sandboxProvider = (0, sandbox_provider_1.normalizeSandboxProvider)(c.sandboxProvider); return { enabled: this.enabled, permissions: { ...this.permissions }, mcpAccessEnabled: this.mcpAccessEnabled, sandboxEnabled: c.sandboxEnabled, sandboxProvider, daytonaCredentialId: isManaged ? null : credentialSelection.daytonaCredentialId, n8nSandboxCredentialId: this.isCloud ? null : credentialSelection.n8nSandboxCredentialId, searchCredentialId: isManaged ? null : credentialSelection.searchCredentialId, modelCredentialId: isManaged ? null : credentialSelection.modelCredentialId, modelName: isManaged ? null : credentialSelection.modelName, modelEnvConfigured: Boolean(c.modelApiKey.trim() || c.modelUrl.trim() || (modelProviderApiKeyEnv && process.env[modelProviderApiKeyEnv]?.trim())), sandboxEnvConfigured: sandboxProvider === 'daytona' ? isProxyEnabled || Boolean(c.daytonaApiKey.trim()) : Boolean(c.n8nSandboxServiceUrl.trim()), searchEnvConfigured: Boolean(c.braveSearchApiKey.trim() || c.searxngUrl.trim()), localGatewayDisabled: this.isLocalGatewayDisabled(), browserUseEnabled: this.isBrowserUseEnabled(), }; } async updateAdminSettings(update, user) { this.rejectManagedFields(update, InstanceAiSettingsService_1.MANAGED_ADMIN_FIELDS, this.deploymentLabel()); if (this.isCloud) { this.rejectManagedFields(update, InstanceAiSettingsService_1.INSTANCE_CREDENTIAL_FIELDS, this.deploymentLabel()); this.rejectManagedFields(update, ['modelName', 'sandboxProvider'], this.deploymentLabel()); } else if (this.aiService.isProxyEnabled()) { this.rejectManagedFields(update, ['modelCredentialId', 'searchCredentialId', 'modelConnection', 'searchConnection'], this.deploymentLabel()); this.rejectManagedFields(update, ['modelName'], this.deploymentLabel()); if (update.daytonaCredentialId !== null) { this.rejectManagedFields(update, ['daytonaCredentialId'], this.deploymentLabel()); } if (update.sandboxConnection?.type === 'daytonaApi') { this.rejectManagedFields(update, ['sandboxConnection'], this.deploymentLabel()); } } const { modelCredentialId: initialModelCredentialId, daytonaCredentialId: initialDaytonaCredentialId, n8nSandboxCredentialId: initialN8nSandboxCredentialId, searchCredentialId: initialSearchCredentialId, modelConnection, sandboxConnection, searchConnection, ...settingsUpdate } = update; let modelCredentialId = initialModelCredentialId; let daytonaCredentialId = initialDaytonaCredentialId; let n8nSandboxCredentialId = initialN8nSandboxCredentialId; let searchCredentialId = initialSearchCredentialId; this.rejectConnectionConflicts(update); if (modelConnection !== undefined || sandboxConnection !== undefined || searchConnection !== undefined) { if (!user || !(0, permissions_1.hasGlobalScope)(user, 'credential:manageInstance')) { throw new forbidden_error_1.ForbiddenError('You do not have permission to manage provider connections'); } } if (modelConnection) { const modelName = settingsUpdate.modelName === undefined ? (await this.readAdminModelSelection()).modelName : settingsUpdate.modelName; if (modelName === null) { throw new unprocessable_error_1.UnprocessableRequestError('modelName must be set together with modelCredentialId'); } } const [modelPrepared, searchPrepared, sandboxPrepared] = user ? await Promise.all([ this.prepareConnection(exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY, 'AI Assistant model', modelConnection), this.prepareConnection(exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY, 'AI Assistant web search', searchConnection), this.prepareSandboxConnection(sandboxConnection), ]) : [undefined, undefined, undefined]; this.validateAdminSettingsUpdate(update, this.snapshotAdminSettings(), sandboxConnection === null ? this.environmentSandboxProvider : sandboxConnection?.type === 'daytonaApi' ? 'daytona' : sandboxConnection?.type === 'httpHeaderAuth' ? 'n8n-sandbox' : settingsUpdate.sandboxProvider); await this.runConnectionHooks([modelPrepared, searchPrepared, sandboxPrepared]); const { previous, next, credentialSelection } = await this.dbLockService.withLockContext(1007, async (ctx) => { if (user && modelConnection !== undefined) { modelCredentialId = await this.upsertConnection(user, exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY, 'AI Assistant model', modelConnection, ctx, modelPrepared); } if (user && searchConnection !== undefined) { searchCredentialId = await this.upsertConnection(user, exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY, 'AI Assistant web search', searchConnection, ctx, searchPrepared); } if (user && sandboxConnection !== undefined) { const sandbox = await this.upsertSandboxConnection(user, sandboxConnection, ctx, sandboxPrepared); daytonaCredentialId = sandbox.daytonaCredentialId; n8nSandboxCredentialId = sandbox.n8nSandboxCredentialId; if (sandbox.sandboxProvider) settingsUpdate.sandboxProvider = sandbox.sandboxProvider; } const updateCredentialAssignment = async (credentialUse, credentialId) => { if (credentialId === undefined) return; if (credentialId === null) { await this.instanceCredentialBroker.clearForUse(credentialUse, ctx); } else { await this.instanceCredentialBroker.assignForUse(credentialUse, credentialId, ctx); } }; const persisted = await this.settingsRepository.findByKeyInContext(ADMIN_SETTINGS_KEY, ctx); const current = this.mergeAdminSettings(this.snapshotAdminSettings(), this.parsePersistedAdminSettings(persisted?.value)); const [currentModelCredentialId, currentDaytonaCredentialId, currentN8nCredentialId, currentSearchCredentialId,] = await Promise.all([ this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY, ctx), this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, ctx), this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, ctx), this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY, ctx), ]); const nextDaytonaCredentialId = daytonaCredentialId === undefined ? currentDaytonaCredentialId : daytonaCredentialId; const nextN8nCredentialId = n8nSandboxCredentialId === undefined ? currentN8nCredentialId : n8nSandboxCredentialId; const nextSearchCredentialId = searchCredentialId === undefined ? currentSearchCredentialId : searchCredentialId; const clearsSandboxConnection = (sandboxConnection !== undefined || daytonaCredentialId !== undefined || n8nSandboxCredentialId !== undefined) && nextDaytonaCredentialId === null && nextN8nCredentialId === null; this.validateAdminSettingsUpdate(update, current, clearsSandboxConnection ? this.environmentSandboxProvider : settingsUpdate.sandboxProvider); const previous = this.snapshotAdminSettings(); const next = this.mergeAdminSettings(current, settingsUpdate); if (clearsSandboxConnection) delete next.sandboxProvider; const nextModelCredentialId = modelCredentialId === undefined ? currentModelCredentialId : modelCredentialId; const nextModelName = settingsUpdate.modelName !== undefined ? settingsUpdate.modelName : modelCredentialId === null ? null : current.modelName; if (modelCredentialId !== undefined || settingsUpdate.modelName !== undefined) { const hasCredential = nextModelCredentialId !== null && nextModelCredentialId !== undefined; const hasModelName = nextModelName !== null && nextModelName !== undefined; if (hasCredential && !hasModelName) { throw new unprocessable_error_1.UnprocessableRequestError('modelName must be set together with modelCredentialId'); } if (hasModelName && !hasCredential) { throw new unprocessable_error_1.UnprocessableRequestError('modelName requires modelCredentialId'); } } next.modelName = nextModelName ?? null; await updateCredentialAssignment(exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY, modelCredentialId); await updateCredentialAssignment(exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, daytonaCredentialId); await updateCredentialAssignment(exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, n8nSandboxCredentialId); await updateCredentialAssignment(exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY, searchCredentialId); if (typeof modelCredentialId === 'string') { await this.validateAssignedServiceCredential(exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY, ctx); } if (typeof daytonaCredentialId === 'string') { await this.validateAssignedServiceCredential(exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, ctx); } if (typeof n8nSandboxCredentialId === 'string') { await this.validateAssignedServiceCredential(exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, ctx); } if (typeof searchCredentialId === 'string') { await this.validateAssignedServiceCredential(exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY, ctx); } await this.settingsRepository.upsertByKey(ADMIN_SETTINGS_KEY, JSON.stringify(next), true, ctx); return { previous, next, credentialSelection: { modelCredentialId: nextModelCredentialId ?? null, modelName: next.modelName ?? null, daytonaCredentialId: nextDaytonaCredentialId, n8nSandboxCredentialId: nextN8nCredentialId, searchCredentialId: nextSearchCredentialId, }, }; }); this.applyAdminSettings(next); this.emitSettingsUpdated(previous, next); return this.buildAdminSettingsResponse(credentialSelection); } rejectConnectionConflicts(update) { const conflicts = [ ['modelConnection', 'modelCredentialId', update.modelCredentialId !== undefined], ['sandboxConnection', 'daytonaCredentialId', update.daytonaCredentialId !== undefined], ['sandboxConnection', 'n8nSandboxCredentialId', update.n8nSandboxCredentialId !== undefined], ['searchConnection', 'searchCredentialId', update.searchCredentialId !== undefined], ]; for (const [connectionField, idField, idPresent] of conflicts) { const connectionPresent = update[connectionField] !== undefined; if (connectionPresent && idPresent) { throw new unprocessable_error_1.UnprocessableRequestError(`Cannot combine ${connectionField} with ${idField} in one update`); } } } async upsertConnection(user, policy, name, connection, ctx, prepared) { if (connection === null) return null; if (!prepared?.encryptedData) { throw new n8n_workflow_1.UnexpectedError('Prepared provider connection is missing encrypted data'); } let current; try { current = await this.instanceCredentialBroker.resolveForUse(policy, ctx); } catch (error) { if (!(error instanceof unprocessable_error_1.UnprocessableRequestError)) throw error; current = null; } if ((current?.id ?? null) !== prepared.expectedCredentialId || current?.name !== prepared.expectedCredentialName || current?.type !== prepared.expectedCredentialType || (current !== null && current !== undefined && !(0, node_util_1.isDeepStrictEqual)(current.data, prepared.expectedCredentialData))) { throw new conflict_error_1.ConflictError('Provider connection changed; retry'); } const data = connection.data; if (current && current.type === connection.type) { await this.credentialsService.updateInstanceCredential(user, current.id, { name: current.name, type: current.type, data }, ctx, { skipExternalHooks: true, encryptedData: prepared.encryptedData }); return current.id; } const dto = { name, type: connection.type, data: connection.data, usageScope: 'instance', }; const created = await this.credentialsService.createInstanceCredential(dto, user, ctx, { skipExternalHooks: true, encryptedData: prepared.encryptedData, }); return created.id; } async prepareConnection(policy, name, connection) { if (!connection) return undefined; if (!policy.credentialTypes.includes(connection.type)) { throw new unprocessable_error_1.UnprocessableRequestError(`Connection type "${connection.type}" is not supported for "${policy.id}"`); } let current; try { current = await this.instanceCredentialBroker.resolveForUse(policy); } catch (error) { if (!(error instanceof unprocessable_error_1.UnprocessableRequestError)) throw error; current = null; } const data = current?.type === connection.type ? this.credentialsService.unredact(connection.data, current.data, this.credentialsService.getCredentialTypeProperties(connection.type)) : connection.data; validateInstanceAiCredential(policy, { type: connection.type, data }); const existing = current?.type === connection.type ? { id: current.id, name: current.name } : null; return { event: existing ? 'update' : 'create', expectedCredentialId: current?.id ?? null, expectedCredentialName: current?.name, expectedCredentialType: current?.type, expectedCredentialData: current?.data, credential: { id: existing?.id ?? null, name: existing?.name ?? name, type: connection.type, data, }, }; } async prepareSandboxConnection(connection) { if (!connection) return undefined; const policy = connection.type === 'daytonaApi' ? exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY : connection.type === 'httpHeaderAuth' ? exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY : undefined; if (!policy) { throw new unprocessable_error_1.UnprocessableRequestError(`Connection type "${connection.type}" is not supported for the sandbox`); } return await this.prepareConnection(policy, 'AI Assistant sandbox', connection); } async runConnectionHooks(preparedConnections) { for (const prepared of preparedConnections) { if (prepared) { prepared.encryptedData = await this.credentialsService.runInstanceCredentialHooks(prepared.event, prepared.credential); } } } async upsertSandboxConnection(user, connection, ctx, prepared) { const name = 'AI Assistant sandbox'; if (connection === null) { return { daytonaCredentialId: await this.upsertConnection(user, exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, name, null, ctx), n8nSandboxCredentialId: await this.upsertConnection(user, exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, name, null, ctx), }; } if (connection.type === 'daytonaApi') { return { daytonaCredentialId: await this.upsertConnection(user, exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, name, connection, ctx, prepared), n8nSandboxCredentialId: await this.upsertConnection(user, exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, name, null, ctx), sandboxProvider: 'daytona', }; } if (connection.type === 'httpHeaderAuth') { return { n8nSandboxCredentialId: await this.upsertConnection(user, exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, name, connection, ctx, prepared), daytonaCredentialId: await this.upsertConnection(user, exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, name, null, ctx), sandboxProvider: 'n8n-sandbox', }; } throw new unprocessable_error_1.UnprocessableRequestError(`Connection type "${connection.type}" is not supported for the sandbox`); } async reloadFromDb() { const previous = this.snapshotAdminSettings(); const persisted = await this.readPersistedAdminSettings(); this.applyAdminSettings(persisted); this.emitSettingsUpdated(previous, this.snapshotAdminSettings()); } async getUserPreferences(user) { const prefs = this.readUserPreferences(user); const credentialId = prefs.credentialId ?? null; let credentialType = null; let credentialName = null; if (credentialId) { const cred = await this.credentialsFinderService.findCredentialForUser(credentialId, user, [ 'credential:read', ]); if (cred) { credentialType = cred.type; credentialName = cred.name; } } return { credentialId, credentialType, credentialName, modelName: prefs.modelName || this.extractModelName(this.config.model), localGatewayDisabled: prefs.localGatewayDisabled ?? false, }; } async updateUserPreferences(user, update) { this.rejectManagedFields(update, InstanceAiSettingsService_1.MANAGED_PREFERENCE_FIELDS, this.deploymentLabel()); const prefs = { ...this.readUserPreferences(user) }; if (update.credentialId !== undefined) prefs.credentialId = update.credentialId; if (update.modelName !== undefined) prefs.modelName = update.modelName; if (update.localGatewayDisabled !== undefined) prefs.localGatewayDisabled = update.localGatewayDisabled; await this.userService.updateSettings(user.id, { instanceAi: prefs }); user.settings = { ...(user.settings ?? {}), instanceAi: prefs }; return await this.getUserPreferences(user); } async listInstanceModelCredentials() { if (this.isCloud || this.aiService.isProxyEnabled()) return []; const instanceCredentials = await this.instanceCredentialBroker.listForUse(exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY); return instanceCredentials.map((c) => ({ id: c.id, name: c.name, type: c.type, })); } async listInstanceServiceCredentials() { if (this.isCloud) return []; const policies = this.aiService.isProxyEnabled() ? [exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY] : [ exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY, ]; const credentials = await Promise.all(policies.map(async (policy) => await this.instanceCredentialBroker.listForUse(policy))); return credentials.flat().map((c) => ({ id: c.id, name: c.name, type: c.type, })); } async resolveDaytonaConfig() { const { daytonaApiUrl, daytonaApiKey } = this.config; const envConfig = { apiUrl: daytonaApiUrl || undefined, apiKey: daytonaApiKey || undefined, }; const resolved = await this.resolveServiceCredential(exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY, 'Daytona sandbox'); if (!resolved) return envConfig; const { data } = resolved; const apiUrl = typeof data.apiUrl === 'string' ? data.apiUrl : undefined; const apiKey = typeof data.apiKey === 'string' ? data.apiKey : undefined; if (!apiUrl || !apiKey) { this.warnCredentialFallback('Daytona sandbox', exports.INSTANCE_AI_DAYTONA_CREDENTIAL_POLICY.id, 'Credential data is incomplete'); return envConfig; } return { apiUrl, apiKey }; } async resolveN8nSandboxConfig() { const { n8nSandboxServiceUrl, n8nSandboxServiceApiKey } = this.config; const envConfig = { serviceUrl: n8nSandboxServiceUrl || undefined, apiKey: n8nSandboxServiceApiKey || undefined, }; const resolved = await this.resolveServiceCredential(exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY, 'n8n Sandbox'); if (!resolved) return envConfig; const { data } = resolved; const headerName = typeof data.name === 'string' ? data.name.trim().toLowerCase() : ''; const apiKey = typeof data.value === 'string' ? data.value : undefined; if (headerName !== N8N_SANDBOX_HEADER_NAME) { this.warnCredentialFallback('n8n Sandbox', exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY.id, `Credential header must be "${N8N_SANDBOX_HEADER_NAME}" but is "${headerName || '(empty)'}"`); return envConfig; } if (!apiKey) { this.warnCredentialFallback('n8n Sandbox', exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY.id, 'Credential data is incomplete'); return envConfig; } return { serviceUrl: n8nSandboxServiceUrl || undefined, apiKey }; } async resolveSearchConfig() { const { braveSearchApiKey, searxngUrl } = this.config; const envConfig = { braveApiKey: braveSearchApiKey || undefined, searxngUrl: searxngUrl || undefined, }; const resolved = await this.resolveServiceCredential(exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY, 'search'); if (!resolved) return envConfig; const { type, data } = resolved; if (type === 'braveSearchApi' && typeof data.apiKey === 'string' && data.apiKey) { return { braveApiKey: data.apiKey }; } if (type === 'searXngApi' && typeof data.apiUrl === 'string' && data.apiUrl) { return { searxngUrl: data.apiUrl }; } this.warnCredentialFallback('search', exports.INSTANCE_AI_SEARCH_CREDENTIAL_POLICY.id, 'Credential data is incomplete'); return envConfig; } async validateAssignedServiceCredential(policy, ctx) { const resolved = await this.instanceCredentialBroker.resolveForUse(policy, ctx); if (!resolved) return; validateInstanceAiCredential(policy, { type: resolved.type, data: resolved.data }); } async resolveServiceCredential(policy, service, ctx) { if (this.isCloud || (this.aiService.isProxyEnabled() && policy.id !== exports.INSTANCE_AI_N8N_SANDBOX_CREDENTIAL_POLICY.id)) return null; const resolved = ctx ? this.instanceCredentialBroker.resolveForUse(policy, ctx) : this.instanceCredentialBroker.resolveForUse(policy); return await resolved.catch((error) => { this.warnCredentialFallback(service, policy.id, (0, ensure_error_1.ensureError)(error).message); return null; }); } warnCredentialFallback(service, credentialUseId, reason) { di_1.Container.get(backend_common_1.Logger) .scoped('instance-ai') .warn(`Could not resolve the configured ${service} credential; using environment fallback`, { credentialUseId, error: reason, }); } getPermissions() { return { ...this.permissions }; } isMcpAccessEnabled() { return this.mcpAccessEnabled; } async isLocalGatewayDisabledForUser(userId) { if (!this.enabled) return true; if (this.config.localGatewayDisabled) return true; const user = await this.userRepository.findOneBy({ id: userId }); if (!user) return true; return this.readUserPreferences(user).localGatewayDisabled ?? false; } isAgentEnabled() { return this.enabled; } isLocalGatewayDisabled() { return this.config.localGatewayDisabled; } isBrowserUseEnabled() { return this.config.browserUseEnabled; } getSandboxStatus() { const provider = (0, sandbox_provider_1.normalizeSandboxProvider)(this.config.sandboxProvider); const unavailableReason = this.getSandboxUnavailableReason(this.config.sandboxEnabled, provider); return { enabled: this.config.sandboxEnabled, provider, workflowBuilderAvailable: this.config.sandboxEnabled && unavailableReason === null, unavailableReason, }; } isInstanceAiEnabled() { return this.enabled; } resolveModelName(user) { const prefs = this.readUserPreferences(user); const adminModelName = this.isCloud || this.aiService.isProxyEnabled() ? null : this.adminModelName; return adminModelName ?? prefs.modelName ?? this.extractModelName(this.config.model); } async resolveModelConfig(user) { const prefs = this.readUserPreferences(user); const fallbackModelName = prefs.modelName ?? this.extractModelName(this.config.model); const adminModelConfig = await this.resolveAdminModelConfig(); if (adminModelConfig) { return adminModelConfig; } const credentialId = prefs.credentialId ?? null; if (!credentialId) { return this.envVarModelConfig(); } const credential = await this.credentialsFinderService.findCredentialForUser(credentialId, user, ['credential:read']); if (!credential) { return this.envVarModelConfig(); } return ((await this.buildModelConfigFromCredential(credential, fallbackModelName)) ?? this.envVarModelConfig()); } async resolveAdminModelConfig() { if (this.isCloud || this.aiService.isProxyEnabled()) return null; return await this.withPersistedAdminSettings(async (ctx, persisted) => { const modelName = persisted.modelName ?? null; if (!modelName) return null; const resolved = await this.resolveServiceCredential(exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY, 'model', ctx); if (!resolved) return null; const config = this.buildModelConfig(resolved.type, resolved.data, modelName); if (!config) { this.warnCredentialFallback('model', exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY.id, 'Credential data is incomplete'); } return config; }); } async buildModelConfigFromCredential(credential, modelName) { const data = await this.credentialsService.decrypt(credential, true); return this.buildModelConfig(credential.type, data, modelName); } buildModelConfig(credentialType, data, modelName) { const provider = CREDENTIAL_TO_MODEL_PROVIDER[credentialType]; if (!provider) { return null; } const apiKey = typeof data.apiKey === 'string' ? data.apiKey : ''; const urlField = URL_FIELD_MAP[credentialType]; const rawUrl = urlField ? data[urlField] : undefined; const baseUrl = typeof rawUrl === 'string' ? rawUrl : ''; const id = `${provider}/${modelName}`; if (!baseUrl && !apiKey) return null; const headers = modelCredentialHeaders(credentialType, data); return { id, url: baseUrl, ...(apiKey ? { apiKey } : {}), ...(headers ? { headers } : {}) }; } deploymentLabel() { if (this.isCloud) return 'cloud'; if (this.aiService.isProxyEnabled()) return 'proxy'; return 'instance'; } rejectManagedFields(update, managedFields, label) { const record = update; const present = managedFields.filter((key) => key in record && record[key] !== undefined); if (present.length > 0) { throw new unprocessable_error_1.UnprocessableRequestError(`Cannot update ${label}-managed fields: ${present.join(', ')}`); } } validateAdminSettingsUpdate(update, current, sandboxProviderOverride) { const touchesSandboxSettings = update.sandboxEnabled !== undefined || update.sandboxProvider !== undefined || update.sandboxImage !== undefined || update.sandboxTimeout !== undefined || update.daytonaCredentialId !== undefined || update.n8nSandboxCredentialId !== undefined || update.sandboxConnection !== undefined; if (!touchesSandboxSettings) return; const sandboxProvider = (0, sandbox_provider_1.normalizeSandboxProvider)(sandboxProviderOverride ?? update.sandboxProvider ?? current.sandboxProvider ?? this.environmentSandboxProvider); const sandboxEnabled = update.sandboxEnabled ?? current.sandboxEnabled ?? false; const unavailableReason = this.getSandboxUnavailableReason(sandboxEnabled, sandboxProvider); if (unavailableReason) throw new unprocessable_error_1.UnprocessableRequestError(unavailableReason); } getSandboxUnavailableReason(sandboxEnabled, sandboxProvider) { if (sandboxEnabled && sandboxProvider === 'n8n-sandbox' && this.config.n8nSandboxServiceUrl.trim().length === 0) { return sandbox_provider_1.N8N_SANDBOX_SERVICE_URL_REQUIRED_MESSAGE; } return null; } envVarModelConfig() { return this.envVarModelConfigForModel(this.config.model); } envVarModelConfigForModel(model) { const { modelUrl, modelApiKey } = this.config; const id = model.includes('/') ? model : `custom/${model}`; if (modelUrl) { return { id, url: modelUrl, ...(modelApiKey ? { apiKey: modelApiKey } : {}) }; } if (modelApiKey) { return { id, url: '', apiKey: modelApiKey }; } return model; } extractModelName(model) { const slash = model.indexOf('/'); return slash >= 0 ? model.slice(slash + 1) : model; } applyAdminSettings(persisted) { const c = this.config; if (persisted.enabled !== undefined) this.enabled = persisted.enabled; if (persisted.permissions) { this.permissions = { ...api_types_1.DEFAULT_INSTANCE_AI_PERMISSIONS, ...persisted.permissions, }; } if (persisted.mcpServers !== undefined) c.mcpServers = persisted.mcpServers; if (persisted.mcpAccessEnabled !== undefined) this.mcpAccessEnabled = persisted.mcpAccessEnabled; if (persisted.sandboxEnabled !== undefined) c.sandboxEnabled = persisted.sandboxEnabled; this.sandboxProviderOverride = this.isCloud || !persisted.sandboxProvider ? undefined : (0, sandbox_provider_1.normalizeSandboxProvider)(persisted.sandboxProvider); c.sandboxProvider = this.sandboxProviderOverride ?? this.environmentSandboxProvider; if (persisted.sandboxImage !== undefined) c.sandboxImage = persisted.sandboxImage; if (persisted.sandboxTimeout !== undefined) c.sandboxTimeout = persisted.sandboxTimeout; if (persisted.modelName !== undefined) this.adminModelName = persisted.modelName; if (persisted.localGatewayDisabled !== undefined) c.localGatewayDisabled = persisted.localGatewayDisabled; if (persisted.browserUseEnabled !== undefined) c.browserUseEnabled = persisted.browserUseEnabled; } readUserPreferences(user) { return user.settings?.instanceAi ?? {}; } snapshotAdminSettings() { const c = this.config; return { enabled: this.enabled, permissions: this.permissions, mcpServers: c.mcpServers, mcpAccessEnabled: this.mcpAccessEnabled, sandboxEnabled: c.sandboxEnabled, ...(this.sandboxProviderOverride ? { sandboxProvider: this.sandboxProviderOverride } : {}), sandboxImage: c.sandboxImage, sandboxTimeout: c.sandboxTimeout, modelName: this.adminModelName, localGatewayDisabled: c.localGatewayDisabled, browserUseEnabled: c.browserUseEnabled, }; } mergeAdminSettings(base, update) { return { ...base, ...update, permissions: update.permissions ? { ...(base.permissions ?? api_types_1.DEFAULT_INSTANCE_AI_PERMISSIONS), ...update.permissions } : base.permissions, }; } async readPersistedAdminSettings() { const row = await this.settingsRepository.findByKey(ADMIN_SETTINGS_KEY); return this.parsePersistedAdminSettings(row?.value); } async withPersistedAdminSettings(read) { return await this.dbLockService.withLockContext(1007, async (ctx) => { const row = await this.settingsRepository.findByKeyInContext(ADMIN_SETTINGS_KEY, ctx); return await read(ctx, this.parsePersistedAdminSettings(row?.value)); }); } async readAdminModelSelection() { return await this.withPersistedAdminSettings(async (ctx, persisted) => ({ modelCredentialId: await this.instanceCredentialBroker.getAssignedCredentialId(exports.INSTANCE_AI_MODEL_CREDENTIAL_POLICY, ctx), modelName: persisted.modelName ?? null, })); } parsePersistedAdminSettings(value) { if (value === undefined) return {}; const settings = (0, n8n_workflow_1.jsonParse)(value, { fallbackValue: {} }); delete settings.modelCredentialId; delete settings.daytonaCredentialId; delete settings.n8nSandboxCredentialId; delete settings.searchCredentialId; return settings; } emitSettingsUpdated(previous, current) { try { this.eventService.emit('instance-ai-settings-updated', { mcpSettingsChanged: current.mcpServers !== previous.mcpServers || current.mcpAccessEnabled !== previous.mcpAccessEnabled, }); } catch (error) { di_1.Container.get(backend_common_1.Logger) .scoped('instance-ai') .warn('Failed to apply local settings event', { error: (0, ensure_error_1.ensureError)(error).message, }); } } }; exports.InstanceAiSettingsService = InstanceAiSettingsService; InstanceAiSettingsService.MANAGED_ADMIN_FIELDS = [ 'mcpServers', 'sandboxEnabled', 'sandboxImage', 'sandboxTimeout', ]; InstanceAiSettingsService.INSTANCE_CREDENTIAL_FIELDS = [ 'modelCredentialId', 'daytonaCredentialId', 'n8nSandboxCredentialId', 'searchCredentialId', 'modelConnection', 'sandboxConnection', 'searchConnection', ]; InstanceAiSettingsService.MANAGED_PREFERENCE_FIELDS = [ 'credentialId', 'modelName', ]; exports.InstanceAiSettingsService = InstanceAiSettingsService = InstanceAiSettingsService_1 = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [config_1.GlobalConfig, db_1.DbLockService, db_1.SettingsRepository, db_1.UserRepository, user_service_1.UserService, ai_service_1.AiService, credentials_service_1.CredentialsService, credentials_finder_service_1.CredentialsFinderService, instance_credential_broker_1.InstanceCredentialBroker, event_service_1.EventService]) ], InstanceAiSettingsService); //# sourceMappingURL=instance-ai-settings.service.js.map