diff --git a/packages/@n8n/instance-ai/src/agent/__tests__/system-prompt.python-code.test.ts b/packages/@n8n/instance-ai/src/agent/__tests__/system-prompt.python-code.test.ts index 940d54967d8..2855ad1f6a0 100644 --- a/packages/@n8n/instance-ai/src/agent/__tests__/system-prompt.python-code.test.ts +++ b/packages/@n8n/instance-ai/src/agent/__tests__/system-prompt.python-code.test.ts @@ -58,40 +58,38 @@ describe('getSystemPrompt — Python Code nodes', () => { // n8n's view can be wrong in the permissive direction here, so the safe assumption // has to lead — reporting the configured list as fact would invite a runtime break. + // This is the shape cloud runs in. it('assumes nothing is importable when the runner is configured separately', () => { const prompt = getSystemPrompt({ pythonImportPolicy: { stdlib: ['re'], external: [], authoritative: false }, }); - expect(prompt).toMatch(/external runner mode/i); expect(prompt).toMatch(/assume no imports are available/i); expect(prompt).not.toMatch(/allows \*\*only\*\*/); }); - // n8n's values are not the ones an external runner reads, so an invalid one says - // nothing about whether that runner starts. Claiming otherwise would push the - // builder off Python on an instance where it works fine. - it('never claims the runner will not start from a policy it cannot confirm', () => { - const prompt = getSystemPrompt({ - pythonImportPolicy: { - stdlib: [], - external: [], - authoritative: false, - misconfigured: true, - }, - }); - - expect(prompt).not.toMatch(/refuse to start/i); - expect(prompt).toMatch(/assume no imports are available/i); - }); - - it('warns that an invalid allowlist stops the runner starting at all', () => { + it('gives the same safe guidance for an allowlist the runner would reject', () => { const prompt = getSystemPrompt({ pythonImportPolicy: { stdlib: [], external: [], authoritative: true, misconfigured: true }, }); - expect(prompt).toMatch(/refuse to start/i); - expect(prompt).toMatch(/must be used alone/i); + expect(prompt).toMatch(/assume no imports are available/i); + }); + + // The agent must never point users at deployment configuration. On cloud they + // cannot change it, so naming a setting only generates a support request for + // something nobody will change for them. + it.each([ + ['nothing allowlisted', { stdlib: [], external: [], authoritative: true }], + ['modules allowlisted', { stdlib: ['re'], external: ['pandas'], authoritative: true }], + ['wildcard', { stdlib: ['*'], external: ['*'], authoritative: true }], + ['not authoritative', { stdlib: ['re'], external: [], authoritative: false }], + ['misconfigured', { stdlib: [], external: [], authoritative: true, misconfigured: true }], + ])('never names an environment variable or asks for a config change (%s)', (_name, policy) => { + const prompt = getSystemPrompt({ pythonImportPolicy: policy }); + + expect(prompt).not.toMatch(/N8N_RUNNERS_/); + expect(prompt).not.toMatch(/tell the user their .* setting/i); }); it('always states the globals the runner defines, whatever the import policy', () => { diff --git a/packages/@n8n/instance-ai/src/agent/system-prompt.ts b/packages/@n8n/instance-ai/src/agent/system-prompt.ts index f194813365f..681d8f9cf85 100644 --- a/packages/@n8n/instance-ai/src/agent/system-prompt.ts +++ b/packages/@n8n/instance-ai/src/agent/system-prompt.ts @@ -141,20 +141,17 @@ function getPythonCodeSection(policy?: PythonImportPolicy): string { let importRule: string; // `authoritative` is checked first on purpose: a policy n8n cannot confirm says // nothing about the runner, including whether its configuration is even valid. - if (!policy.authoritative) { - // Anything n8n reports here may be wrong in the permissive direction, so lead - // with the safe assumption. - const configured = - allowed.length > 0 - ? ` n8n itself is configured for ${allowed.join(', and ')}, but that may not be what the runner enforces.` - : ''; - importRule = `This instance runs task runners in **external runner mode**, so n8n cannot confirm what the runner allows. **Assume no imports are available** and write import-free Python.${configured} If an import fails at run time, rewrite the code without it rather than telling the user it should have worked.`; - } else if (policy.misconfigured) { + // + // None of these name the environment variables behind the allowlist, and none tell + // the user to change them. Whether they *can* is a deployment question the agent + // cannot see — on cloud they cannot, so naming a setting only sends them to support + // over something nobody will change for them. + if (!policy.authoritative || policy.misconfigured) { importRule = - 'This instance combines a wildcard with named modules in an allowlist, which the runner rejects — it will **refuse to start**, so a Python Code node cannot run here at all. Use JavaScript, and tell the user their `N8N_RUNNERS_STDLIB_ALLOW` / `N8N_RUNNERS_EXTERNAL_ALLOW` setting is invalid: a `*` must be used alone.'; + '**Assume no imports are available** and write import-free Python, using builtins and str/list/dict methods, or use JavaScript when the task genuinely needs a library. If an import does fail at run time, rewrite the code without it rather than telling the user it should have worked.'; } else if (allowed.length === 0) { importRule = - 'This instance **allows no imports at all** — the allowlists are empty, so `import re`, `import json`, `import datetime` and every package fail at run time with "Import of ... is disallowed". Write import-free Python using builtins and str/list/dict methods, or use JavaScript when the task genuinely needs a library.'; + 'This instance **allows no imports at all**, so `import re`, `import json`, `import datetime` and every package fail at run time with "Import of ... is disallowed". Write import-free Python using builtins and str/list/dict methods, or use JavaScript when the task genuinely needs a library.'; } else { importRule = `This instance allows **only** ${allowed.join(', and ')} to be imported. Every other import fails at run time, a module named in the wrong category is rejected too, and relative imports always fail.`; } diff --git a/packages/@n8n/instance-ai/src/tools/workflows/__tests__/detect-python-code-constraints.test.ts b/packages/@n8n/instance-ai/src/tools/workflows/__tests__/detect-python-code-constraints.test.ts index ebfa106f023..fddd6b73e1a 100644 --- a/packages/@n8n/instance-ai/src/tools/workflows/__tests__/detect-python-code-constraints.test.ts +++ b/packages/@n8n/instance-ai/src/tools/workflows/__tests__/detect-python-code-constraints.test.ts @@ -105,7 +105,9 @@ describe('detectPythonCodeConstraints', () => { const warnings = detectPythonCodeConstraints(workflow('import re\nreturn []'), policy); expect(warnings.map((w) => w.code)).toEqual(['CODE_NODE_PYTHON_IMPORT']); - expect(warnings[0].message).toMatch(/cannot see the configuration/i); + expect(warnings[0].message).toMatch(/cannot see the allowlist in force/i); + // The agent may repeat this to a user who cannot change the setting. + expect(warnings[0].message).not.toMatch(/N8N_RUNNERS_/); }); it('flags every import when the configured allowlist is one the runner rejects', () => { diff --git a/packages/@n8n/workflow-sdk/src/lint/code-node/code-node.test.ts b/packages/@n8n/workflow-sdk/src/lint/code-node/code-node.test.ts index 7077c1fafad..b3c06f3b5c0 100644 --- a/packages/@n8n/workflow-sdk/src/lint/code-node/code-node.test.ts +++ b/packages/@n8n/workflow-sdk/src/lint/code-node/code-node.test.ts @@ -220,6 +220,7 @@ describe('lintPythonCode — native runner constraints', () => { expect(issues.map((i) => i.code)).toEqual(['CODE_NODE_PYTHON_IMPORT']); expect(issues[0].message).toMatch(/refuse to start/i); + expect(issues[0].message).not.toMatch(/N8N_RUNNERS_/); }); it('reports the two allowlist categories separately', () => { @@ -251,10 +252,22 @@ describe('lintPythonCode — native runner constraints', () => { expect(issues[0].message).toContain('allowlists no imports'); }); - it('names both allowlists rather than assuming the module is stdlib', () => { + it('does not assume an unknown policy allowlists nothing on purpose', () => { const issues = lintPythonCode('import pandas\nreturn []'); - expect(issues[0].message).toContain('N8N_RUNNERS_STDLIB_ALLOW'); - expect(issues[0].message).toContain('N8N_RUNNERS_EXTERNAL_ALLOW'); + expect(issues[0].message).toMatch(/cannot see the allowlist in force/i); + }); + + // The agent reads these and may repeat them to the user. On cloud the allowlist is + // not theirs to change, so naming a setting only sends them to support. + it.each([ + ['unknown policy', undefined], + ['nothing allowlisted', { stdlib: [], external: [], authoritative: true }], + ['modules allowlisted', { stdlib: ['re'], external: ['pandas'], authoritative: true }], + ['misconfigured', { stdlib: [], external: [], authoritative: true, misconfigured: true }], + ])('never names an environment variable (%s)', (_name, importPolicy) => { + for (const issue of lintPythonCode('import math\nreturn []', { importPolicy })) { + expect(issue.message).not.toMatch(/N8N_RUNNERS_/); + } }); it('does not flag an unsupported global named only in a comment', () => { diff --git a/packages/@n8n/workflow-sdk/src/lint/code-node/python.ts b/packages/@n8n/workflow-sdk/src/lint/code-node/python.ts index 9eb91b1f188..851ad07d7b9 100644 --- a/packages/@n8n/workflow-sdk/src/lint/code-node/python.ts +++ b/packages/@n8n/workflow-sdk/src/lint/code-node/python.ts @@ -53,17 +53,21 @@ function policyIsUndecidable(policy: PythonImportPolicy): boolean { /** Renders the allowlist clause shared by the import message. */ function describePolicy(policy: PythonImportPolicy | undefined): string { + // These reach the agent, which may repeat them to the user. Deliberately no + // environment-variable names and no "ask an operator to change it": whether the + // allowlist can be changed at all is a deployment question neither the agent nor + // the user can act on — on cloud it cannot. if (!policy) { - return 'imports are allowlisted per deployment (N8N_RUNNERS_STDLIB_ALLOW for standard-library modules, N8N_RUNNERS_EXTERNAL_ALLOW for packages, both empty by default) and this check cannot see the configuration in force, so assume none are available'; + return 'imports are allowlisted per deployment, this check cannot see the allowlist in force, and the default allows nothing — so assume none are available'; } if (policy.misconfigured) { - return 'this deployment combines a wildcard with named modules, which the runner rejects — it will refuse to start, so no Python runs at all'; + return "this deployment's allowlist is one the runner rejects, so it will refuse to start and no Python runs at all"; } const parts: string[] = []; if (policy.stdlib.length > 0) parts.push(`standard-library modules ${policy.stdlib.join(', ')}`); if (policy.external.length > 0) parts.push(`packages ${policy.external.join(', ')}`); if (parts.length === 0) { - return 'this deployment allowlists no imports at all (N8N_RUNNERS_STDLIB_ALLOW and N8N_RUNNERS_EXTERNAL_ALLOW are both empty)'; + return 'this deployment allowlists no imports at all'; } return `this deployment allowlists only ${parts.join(', and ')}`; } diff --git a/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts index 709788500ef..3652a07fa4f 100644 --- a/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts +++ b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts @@ -14,7 +14,7 @@ const commonDescription: INodeProperties = { noDataExpression: true, builderHint: { propertyHint: - 'Runs in a locked-down native Python sandbox. IMPORTS ARE OPT-IN PER DEPLOYMENT: nothing is importable unless an operator allowlisted it (N8N_RUNNERS_STDLIB_ALLOW for standard-library modules, N8N_RUNNERS_EXTERNAL_ALLOW for packages), and both are empty by default, so on a stock instance `import re`, `import json`, `import math`, `import datetime`, `import pandas` and relative imports all FAIL at runtime with "Import of ... is disallowed". Assume nothing is importable unless you have been told this instance\'s allowlist — write import-free Python using builtins and str/list/dict methods, or set language to javaScript when the task genuinely needs a library. NO network access whatever the allowlist says: requests, urllib, httpx and other HTTP libraries are unavailable — use the HTTP Request node and process its output in this node instead. The ONLY globals are _items (runOnceForAllItems mode ONLY), _item (runOnceForEachItem mode ONLY) and print(); reading the accessor belonging to the other mode raises NameError. There are no cross-node helpers either — _("Node Name"), _input, _json, _today and _jmespath are all undefined, so read data from the connected upstream node only.', + 'Runs in a locked-down native Python sandbox. IMPORTS ARE OPT-IN PER DEPLOYMENT and off by default, so on a stock instance `import re`, `import json`, `import math`, `import datetime`, `import pandas` and relative imports all FAIL at runtime with "Import of ... is disallowed". Assume nothing is importable unless you have been told this instance\'s allowlist — write import-free Python using builtins and str/list/dict methods, or set language to javaScript when the task genuinely needs a library. Never tell the user to change the allowlist: on managed deployments they cannot. NO network access whatever the allowlist says: requests, urllib, httpx and other HTTP libraries are unavailable — use the HTTP Request node and process its output in this node instead. The ONLY globals are _items (runOnceForAllItems mode ONLY), _item (runOnceForEachItem mode ONLY) and print(); reading the accessor belonging to the other mode raises NameError. There are no cross-node helpers either — _("Node Name"), _input, _json, _today and _jmespath are all undefined, so read data from the connected upstream node only.', }, }; @@ -41,7 +41,7 @@ export const pythonCodeDescription: INodeProperties[] = [ }, }, { - displayName: `${PRINT_INSTRUCTION}

The Python option does not support _ syntax and helpers, except for _items in all-items mode and _item in per-item mode.

Imports are disabled unless your instance sets N8N_RUNNERS_STDLIB_ALLOW.`, + displayName: `${PRINT_INSTRUCTION}

The Python option does not support _ syntax and helpers, except for _items in all-items mode and _item in per-item mode.

Imports are disabled unless your instance allows them.`, name: 'notice', type: 'notice', displayOptions: {