From 75bedd8eaf5ff2a04504c9973d019b787cb6c149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Repe=C4=87?= Date: Tue, 18 Aug 2026 14:54:47 +0200 Subject: [PATCH] feat(core): Make the vm expression engine the default on 1.x (#36498) Co-authored-by: Claude Fable 5 --- .../expression-engine.config.test.ts | 19 +++++++++++++++++++ .../src/configs/expression-engine.config.ts | 8 +++----- packages/@n8n/config/test/config.test.ts | 2 +- packages/@n8n/expression-runtime/README.md | 2 +- .../commands/__tests__/execute-batch.test.ts | 2 ++ .../src/commands/__tests__/execute.test.ts | 2 ++ .../__tests__/deprecation.service.test.ts | 2 ++ .../src/deprecation/deprecation.service.ts | 6 ++++++ packages/workflow/src/expression.ts | 4 ++-- packages/workflow/vitest.config.ts | 7 ++++--- 10 files changed, 42 insertions(+), 12 deletions(-) diff --git a/packages/@n8n/config/src/configs/__tests__/expression-engine.config.test.ts b/packages/@n8n/config/src/configs/__tests__/expression-engine.config.test.ts index 9c9c11ae6e8..c83c0ce525e 100644 --- a/packages/@n8n/config/src/configs/__tests__/expression-engine.config.test.ts +++ b/packages/@n8n/config/src/configs/__tests__/expression-engine.config.test.ts @@ -16,6 +16,10 @@ describe('ExpressionEngineConfig', () => { }); describe('defaults', () => { + test('engine defaults to vm', () => { + expect(Container.get(ExpressionEngineConfig).engine).toBe('vm'); + }); + test('bridgeTimeout defaults to 5000', () => { expect(Container.get(ExpressionEngineConfig).bridgeTimeout).toBe(5000); }); @@ -25,6 +29,21 @@ describe('ExpressionEngineConfig', () => { }); }); + describe('N8N_EXPRESSION_ENGINE', () => { + test('overrides engine to legacy', () => { + process.env.N8N_EXPRESSION_ENGINE = 'legacy'; + expect(Container.get(ExpressionEngineConfig).engine).toBe('legacy'); + }); + + test('falls back to default on invalid value', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + process.env.N8N_EXPRESSION_ENGINE = 'not-an-engine'; + expect(Container.get(ExpressionEngineConfig).engine).toBe('vm'); + expect(consoleWarnSpy).toHaveBeenCalled(); + }); + }); + describe('N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT', () => { test('overrides idleTimeout', () => { process.env.N8N_EXPRESSION_ENGINE_IDLE_TIMEOUT = '60'; diff --git a/packages/@n8n/config/src/configs/expression-engine.config.ts b/packages/@n8n/config/src/configs/expression-engine.config.ts index 6a92b5b6eff..f51030edb9e 100644 --- a/packages/@n8n/config/src/configs/expression-engine.config.ts +++ b/packages/@n8n/config/src/configs/expression-engine.config.ts @@ -8,13 +8,11 @@ const expressionEngineSchema = z.enum(['legacy', 'vm']); export class ExpressionEngineConfig { /** * Which expression engine to use. - * - `legacy` runs expressions without isolation. - * - `vm` runs expressions in a V8 isolate. - * - * `vm` is currently **experimental**. Use at your own risk. + * - `vm` (default) runs expressions in a V8 isolate. + * - `legacy` runs expressions without isolation. Less secure and soon to be deprecated. */ @Env('N8N_EXPRESSION_ENGINE', expressionEngineSchema) - engine: 'legacy' | 'vm' = 'legacy'; + engine: 'legacy' | 'vm' = 'vm'; /** Number of V8 isolates ready in the pool. */ @Env('N8N_EXPRESSION_ENGINE_POOL_SIZE') diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index d8653d9b4cf..5dfc488368a 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -153,7 +153,7 @@ describe('GlobalConfig', () => { }, }, expressionEngine: { - engine: 'legacy', + engine: 'vm', poolSize: 1, maxCodeCacheSize: 1024, bridgeTimeout: 5000, diff --git a/packages/@n8n/expression-runtime/README.md b/packages/@n8n/expression-runtime/README.md index 7aab8d49f29..56cc5a82539 100644 --- a/packages/@n8n/expression-runtime/README.md +++ b/packages/@n8n/expression-runtime/README.md @@ -15,7 +15,7 @@ Implemented so far: - ✅ Integration tests (PR 4) Coming in later PRs: -- 🚧 Workflow integration behind `N8N_EXPRESSION_ENGINE=vm` flag (PR 5) +- ✅ Workflow integration via `N8N_EXPRESSION_ENGINE` (default: `vm`; set `legacy` to opt out) (PR 5) - 🚧 Web Worker support (Phase 2+) - 🚧 Performance optimizations (Phase 3) diff --git a/packages/cli/src/commands/__tests__/execute-batch.test.ts b/packages/cli/src/commands/__tests__/execute-batch.test.ts index 86626f448dd..cd5db0f52c2 100644 --- a/packages/cli/src/commands/__tests__/execute-batch.test.ts +++ b/packages/cli/src/commands/__tests__/execute-batch.test.ts @@ -11,6 +11,7 @@ import { ActiveExecutions } from '@/active-executions'; import { DeprecationService } from '@/deprecation/deprecation.service'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay'; +import { ExpressionObservabilityProvider } from '@/expression-observability/expression-observability.provider'; import { ExternalHooks } from '@/external-hooks'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { CommunityPackagesService } from '@/modules/community-packages/community-packages.service'; @@ -31,6 +32,7 @@ const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials); const shutdownService = mockInstance(ShutdownService); const deprecationService = mockInstance(DeprecationService); mockInstance(MessageEventBus); +mockInstance(ExpressionObservabilityProvider); const posthogClient = mockInstance(PostHogClient); const telemetryEventRelay = mockInstance(TelemetryEventRelay); const externalHooks = mockInstance(ExternalHooks); diff --git a/packages/cli/src/commands/__tests__/execute.test.ts b/packages/cli/src/commands/__tests__/execute.test.ts index abc50ab8980..7405ee55965 100644 --- a/packages/cli/src/commands/__tests__/execute.test.ts +++ b/packages/cli/src/commands/__tests__/execute.test.ts @@ -12,6 +12,7 @@ import { ActiveExecutions } from '@/active-executions'; import { DeprecationService } from '@/deprecation/deprecation.service'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { TelemetryEventRelay } from '@/events/relays/telemetry.event-relay'; +import { ExpressionObservabilityProvider } from '@/expression-observability/expression-observability.provider'; import { ExternalHooks } from '@/external-hooks'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { CommunityPackagesService } from '@/modules/community-packages/community-packages.service'; @@ -30,6 +31,7 @@ const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials); const shutdownService = mockInstance(ShutdownService); const deprecationService = mockInstance(DeprecationService); mockInstance(MessageEventBus); +mockInstance(ExpressionObservabilityProvider); const posthogClient = mockInstance(PostHogClient); const telemetryEventRelay = mockInstance(TelemetryEventRelay); const externalHooks = mockInstance(ExternalHooks); diff --git a/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts b/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts index a6919391a9d..fa79f33a89d 100644 --- a/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts +++ b/packages/cli/src/deprecation/__tests__/deprecation.service.test.ts @@ -63,6 +63,8 @@ describe('DeprecationService', () => { ['EXECUTIONS_DATA_PRUNE_TIMEOUT', '1', true], ['N8N_CONFIG_FILES', '1', true], ['N8N_SKIP_WEBHOOK_DEREGISTRATION_SHUTDOWN', '1', true], + ['N8N_EXPRESSION_ENGINE', 'legacy', true], + ['N8N_EXPRESSION_ENGINE', 'vm', false], ])('should detect when %s is `%s`', (envVar, value, mustWarn) => { toTest(envVar, value, mustWarn); }); diff --git a/packages/cli/src/deprecation/deprecation.service.ts b/packages/cli/src/deprecation/deprecation.service.ts index 55c650d8eee..5a29d270c16 100644 --- a/packages/cli/src/deprecation/deprecation.service.ts +++ b/packages/cli/src/deprecation/deprecation.service.ts @@ -91,6 +91,12 @@ export class DeprecationService { 'n8n does not support `own` mode since May 2023. Please remove this environment variable to allow n8n to start. If you need the isolation and performance gains, please consider queue mode: https://docs.n8n.io/hosting/scaling/queue-mode/', checkValue: (value: string) => value === 'own', }, + { + envVar: 'N8N_EXPRESSION_ENGINE', + message: + 'The `legacy` expression engine runs expressions without isolation, is no longer considered secure, and will be removed in a future version. Remove this environment variable to use the default `vm` engine.', + checkValue: (value?: string) => value === 'legacy', + }, { envVar: 'N8N_BLOCK_ENV_ACCESS_IN_NODE', message: diff --git a/packages/workflow/src/expression.ts b/packages/workflow/src/expression.ts index 41063ad5401..7df1c1dfb99 100644 --- a/packages/workflow/src/expression.ts +++ b/packages/workflow/src/expression.ts @@ -252,7 +252,7 @@ export class Expression { } /** - * Initialize the VM evaluator (if feature flag is enabled). + * Initialize the VM evaluator (no-op when the legacy engine is selected). * Should be called once during application startup. * Only available in Node.js environments (not in browser). */ @@ -660,7 +660,7 @@ export class Expression { if (Expression.expressionEngine === 'vm' && !IS_FRONTEND) { if (!Expression.vmEvaluator) { throw new ApplicationError( - 'N8N_EXPRESSION_ENGINE=vm is enabled but VM evaluator is not initialized. Call Expression.initExpressionEngine() during application startup.', + 'The VM expression engine has not been initialized. Call Expression.initExpressionEngine() during application startup.', ); } diff --git a/packages/workflow/vitest.config.ts b/packages/workflow/vitest.config.ts index 82de49c01c7..621a04a294b 100644 --- a/packages/workflow/vitest.config.ts +++ b/packages/workflow/vitest.config.ts @@ -15,14 +15,15 @@ export default defineConfig({ { test: { ...sharedTestConfig, - name: 'legacy-engine', + name: 'vm-engine', + env: { N8N_EXPRESSION_ENGINE: 'vm' }, }, }, { test: { ...sharedTestConfig, - name: 'vm-engine', - env: { N8N_EXPRESSION_ENGINE: 'vm' }, + name: 'legacy-engine', + env: { N8N_EXPRESSION_ENGINE: 'legacy' }, }, }, ],