feat(core): Enable 'instance-ai' module by default and remove startup warning (#35670)

This commit is contained in:
Jaakko Husso
2026-08-10 10:14:01 +03:00
committed by GitHub
parent ddcc94f896
commit fcc4ef59d6
7 changed files with 37 additions and 20 deletions
@@ -30,7 +30,16 @@ describe('getModuleEntryUrl', () => {
describe('eligibleModules', () => {
it('should not include opt-in modules by default', () => {
const eligible = Container.get(ModuleRegistry).eligibleModules;
expect(eligible).not.toContain('instance-ai');
expect(eligible).not.toContain('agents');
});
it('should include instance-ai by default', () => {
expect(Container.get(ModuleRegistry).eligibleModules).toContain('instance-ai');
});
it('should allow opting out of a default module via env var', () => {
process.env.N8N_DISABLED_MODULES = 'instance-ai';
expect(Container.get(ModuleRegistry).eligibleModules).not.toContain('instance-ai');
});
it('should consider a module ineligible if it was disabled via env var', () => {
@@ -64,11 +73,12 @@ describe('eligibleModules', () => {
'runtime-credentials',
'mcp-registry',
'workflow-reviews',
'instance-ai',
]);
});
it('should consider a module eligible if it was enabled via env var', () => {
process.env.N8N_ENABLED_MODULES = 'instance-ai';
process.env.N8N_ENABLED_MODULES = 'agents';
expect(Container.get(ModuleRegistry).eligibleModules).toEqual([
'insights',
'external-secrets',
@@ -100,6 +110,7 @@ describe('eligibleModules', () => {
'mcp-registry',
'workflow-reviews',
'instance-ai',
'agents',
]);
});
@@ -72,6 +72,7 @@ export class ModuleRegistry {
'runtime-credentials',
'mcp-registry',
'workflow-reviews',
'instance-ai',
];
private readonly activeModules: string[] = [];
@@ -3,19 +3,9 @@ import type { ModuleInterface } from '@n8n/decorators';
import { BackendModule, OnShutdown } from '@n8n/decorators';
import { Container } from '@n8n/di';
const YELLOW = '\x1b[33m';
const CLEAR = '\x1b[0m';
const WARNING_MESSAGE =
"[Instance AI] 'instance-ai' module is experimental, undocumented and subject to change. " +
'Before its official release any features may become inaccessible at any point, ' +
'and using the module could compromise the stability of your system. Use at your own risk!';
@BackendModule({ name: 'instance-ai', instanceTypes: ['main'] })
export class InstanceAiModule implements ModuleInterface {
async init() {
const logger = Container.get(Logger).scoped('instance-ai');
logger.warn(`${YELLOW}${WARNING_MESSAGE}${CLEAR}`);
const { InstanceCredentialBroker } = await import(
'@/credentials/instance-credential-broker.js'
);
@@ -48,6 +38,7 @@ export class InstanceAiModule implements ModuleInterface {
if (Container.get(GlobalConfig).instanceAi.durableLog) {
const { InterruptedRunSweeper } = await import('./event-bus/interrupted-run-sweeper.js');
const { InstanceAiService } = await import('./instance-ai.service.js');
const logger = Container.get(Logger).scoped('instance-ai');
const sweeper = Container.get(InterruptedRunSweeper);
sweeper.setResumeHost(Container.get(InstanceAiService));
void sweeper.sweep().catch((error: unknown) => {
@@ -239,7 +239,19 @@ export class n8nPage {
this.clipboard = new ClipboardHelper(page);
}
/**
* Navigate to the workflow overview. Goes there directly rather than via `/`,
* because the root route lands users on the AI Assistant when the `instance-ai`
* module is active. Use {@link goToRoot} to exercise that root routing itself.
*/
async goHome() {
await this.page.goto('/home/workflows');
}
/**
* Navigate to the app root and let it decide where the user lands.
*/
async goToRoot() {
await this.page.goto('/');
}
}
@@ -6,17 +6,19 @@ test.describe(
annotation: [{ type: 'owner', description: 'Identity & Access' }],
},
() => {
// Every signed-in role holds `instanceAi:message`, so the root route lands
// them on the AI Assistant while the `instance-ai` module is active.
const testCases = [
{ role: 'default', expectedUrl: /\/workflow/, auth: '' },
{ role: 'owner', expectedUrl: /\/workflow/, auth: '@auth:owner' },
{ role: 'admin', expectedUrl: /\/workflow/, auth: '@auth:admin' },
{ role: 'member', expectedUrl: /\/workflow/, auth: '@auth:member' },
{ role: 'default', expectedUrl: /\/assistant/, auth: '' },
{ role: 'owner', expectedUrl: /\/assistant/, auth: '@auth:owner' },
{ role: 'admin', expectedUrl: /\/assistant/, auth: '@auth:admin' },
{ role: 'member', expectedUrl: /\/assistant/, auth: '@auth:member' },
{ role: 'none', expectedUrl: /\/signin/, auth: '@auth:none' },
];
for (const { role, expectedUrl, auth } of testCases) {
test(`${role} authentication ${auth}`, async ({ n8n }) => {
await n8n.goHome();
await n8n.goToRoot();
await expect(n8n.page).toHaveURL(expectedUrl);
});
}
@@ -8,7 +8,7 @@ test.describe(
},
() => {
test('should login and logout @auth:none', async ({ n8n }) => {
await n8n.goHome();
await n8n.goToRoot();
await n8n.signIn.goto();
@@ -22,8 +22,8 @@ test.describe(
},
});
await n8n.goHome();
await n8n.page.waitForURL('/home/chat'); // home is chat UI for chat users
await n8n.goToRoot();
await n8n.page.waitForURL('/home/chat'); // root is the chat UI for chat users
// Verify global credential is available and pre-selected
await expect(n8n.chatHubChat.getModelSelectorButton()).toContainText(/claude/i); // pre-selected