n8n
Version:
n8n Workflow Automation Tool
988 lines • 49.3 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);
};
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