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: {