Loading nixos/modules/services/misc/n8n.nix +259 −4 Original line number Diff line number Diff line Loading @@ -30,8 +30,35 @@ let path = "${pkg}/lib/node_modules/${pkg.pname}"; }) cfg.customNodes ); # Runners runnersCfg = cfg.taskRunners; runnersEnv = partitionEnv runnersCfg.environment; commonAllowedEnv = lib.attrNames runnersEnv.regular; enabledRunners = lib.filterAttrs (_name: runnerCfg: runnerCfg.enable) runnersCfg.runners; anyRunnerEnabled = runnersCfg.enable && (enabledRunners != { }); runnerTypes = lib.attrNames enabledRunners; runnersStateDir = "n8n-task-runners"; taskRunnerConfigs = lib.mapAttrsToList (runnerType: runnerCfg: { runner-type = runnerType; workdir = "/var/lib/${runnersStateDir}"; command = runnerCfg.command; args = runnerCfg.args; allowed-env = commonAllowedEnv; env-overrides = runnerCfg.environment; health-check-server-port = toString runnerCfg.healthCheckPort; }) enabledRunners; launcherConfigFile = pkgs.writeText "n8n-task-runners.json" ( builtins.toJSON { task-runners = taskRunnerConfigs; } ); in { meta.maintainers = with lib.maintainers; [ sweenu gepbird ]; imports = [ (lib.mkRemovedOptionModule [ "services" "n8n" "settings" ] "Use services.n8n.environment instead.") (lib.mkRemovedOptionModule [ Loading Loading @@ -128,13 +155,188 @@ in When enabled, n8n sends notifications of new versions and security updates. ''; }; N8N_CUSTOM_EXTENSIONS = lib.mkOption { internal = true; type = with lib.types; nullOr path; default = if cfg.customNodes != [ ] then toString customNodesDir else null; description = '' Specify the path to directories containing your custom nodes. ''; }; N8N_RUNNERS_MODE = lib.mkOption { internal = true; type = with lib.types; enum [ "internal" "external" ]; default = if runnersCfg.enable then "external" else "internal"; description = '' How to launch and run the task runner. `internal` means n8n will launch a task runner as child process. `external` means an external orchestrator will launch the task runner. ''; }; N8N_RUNNERS_BROKER_PORT = lib.mkOption { type = with lib.types; coercedTo port toString str; default = 5679; description = '' Port the task broker listens on for task runner connections. ''; }; N8N_RUNNERS_BROKER_LISTEN_ADDRESS = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = '' Address the task broker listens on. ''; }; N8N_RUNNERS_AUTH_TOKEN_FILE = lib.mkOption { type = with lib.types; nullOr path; default = null; description = '' Path to a file containing the shared authentication token used between the n8n server (task broker) and the task runners. This option is required when {option}`services.n8n.taskRunners.enable` is true. The file should be readable by the service and not stored in the Nix store. Use tools like `agenix` or `sops-nix` to manage this secret. ''; }; }; }; default = { }; }; taskRunners = { enable = lib.mkEnableOption "n8n task runners for sandboxed Code node execution"; launcherPackage = lib.mkPackageOption pkgs "n8n-task-runner-launcher" { }; environment = lib.mkOption { description = '' Environment variables for the task runner launcher and runners. These are common to all runners and passed via `allowed-env` in the launcher config. See <https://docs.n8n.io/hosting/configuration/environment-variables/task-runners/> for available options. Environment variables ending with `_FILE` are automatically handled as secrets: they are loaded via systemd credentials for secure access with `DynamicUser=true`. Note: The authentication token should be set via {option}`services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE`. ''; example = lib.literalExpression '' { N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT = 15; N8N_RUNNERS_MAX_CONCURRENCY = 10; } ''; type = lib.types.submodule { freeformType = with lib.types; attrsOf (oneOf [ str (coercedTo int toString str) (coercedTo bool builtins.toJSON str) ]); options = { N8N_RUNNERS_CONFIG_PATH = lib.mkOption { internal = true; type = with lib.types; nullOr path; default = launcherConfigFile; description = '' Path to the configuration file for the task runner launcher. ''; }; N8N_RUNNERS_AUTH_TOKEN_FILE = lib.mkOption { type = with lib.types; nullOr path; default = cfg.environment.N8N_RUNNERS_AUTH_TOKEN_FILE; defaultText = lib.literalExpression "config.services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE"; description = '' Path to the authentication token file for the task runner. ''; }; N8N_RUNNERS_TASK_BROKER_URI = lib.mkOption { type = lib.types.str; default = "http://${cfg.environment.N8N_RUNNERS_BROKER_LISTEN_ADDRESS}:${cfg.environment.N8N_RUNNERS_BROKER_PORT}"; defaultText = lib.literalExpression ''"http://''${config.services.n8n.environment.N8N_RUNNERS_BROKER_LISTEN_ADDRESS}:''${config.services.n8n.environment.N8N_RUNNERS_BROKER_PORT}"''; description = '' URI of the n8n task broker that the runner connects to. ''; }; }; }; default = { }; }; runners = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule ( { name, ... }: { options = { enable = lib.mkOption { type = lib.types.bool; default = true; description = '' Whether to enable the ${name} task runner. Only takes effect when {option}`services.n8n.taskRunners.enable` is true. ''; }; command = lib.mkOption { type = lib.types.str; description = "Command to execute for this runner."; }; healthCheckPort = lib.mkOption { type = lib.types.port; description = "Port for the runner's health check server."; }; args = lib.mkOption { type = with lib.types; listOf str; default = [ ]; description = "Additional command-line arguments to pass to the task runner."; }; environment = lib.mkOption { type = with lib.types; attrsOf str; default = { }; description = "Environment variables specific to this task runner."; }; }; } ) ); default = { }; defaultText = lib.literalExpression '' { javascript = { enable = true; command = lib.getExe' config.services.n8n.package "n8n-task-runner"; healthCheckPort = 5681; }; python = { enable = true; command = lib.getExe' config.services.n8n.package "n8n-task-runner-python"; healthCheckPort = 5682; }; } ''; description = "Configuration for individual task runners."; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = anyRunnerEnabled -> cfg.environment.N8N_RUNNERS_AUTH_TOKEN_FILE != null; message = "services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE must be set when task runners are enabled."; } ]; systemd.services.n8n = { description = "n8n service"; after = [ "network.target" ]; Loading @@ -144,15 +346,12 @@ in // { HOME = cfg.environment.N8N_USER_FOLDER; } // lib.optionalAttrs (cfg.customNodes != [ ]) { N8N_CUSTOM_EXTENSIONS = toString customNodesDir; } // n8nEnv.fileBasedTransformed; serviceConfig = { Type = "simple"; ExecStart = lib.getExe cfg.package; Restart = "on-failure"; StateDirectory = "n8n"; StateDirectory = baseNameOf cfg.environment.N8N_USER_FOLDER; LoadCredential = lib.mapAttrsToList ( varName: secretPath: "${envVarToCredName varName}:${secretPath}" Loading @@ -178,6 +377,62 @@ in }; }; warnings = lib.optional (runnersCfg.enable && !anyRunnerEnabled) '' services.n8n.taskRunners.enable is true, but both JavaScript and Python runners are disabled. Enable at least one runner or disable taskRunners. ''; # We set the defaults here to ease adding attributes services.n8n.taskRunners.runners = { javascript = lib.mapAttrs (_: lib.mkDefault) { enable = true; command = lib.getExe' cfg.package "n8n-task-runner"; healthCheckPort = 5681; }; python = lib.mapAttrs (_: lib.mkDefault) { enable = true; command = lib.getExe' cfg.package "n8n-task-runner-python"; healthCheckPort = 5682; }; }; systemd.services.n8n-task-runner = lib.mkIf anyRunnerEnabled { description = "n8n task runner"; after = [ "n8n.service" ]; requires = [ "n8n.service" ]; wantedBy = [ "multi-user.target" ]; environment = runnersEnv.regular // runnersEnv.fileBasedTransformed; serviceConfig = { Type = "simple"; ExecStart = "${lib.getExe runnersCfg.launcherPackage} ${lib.concatStringsSep " " runnerTypes}"; Restart = "on-failure"; StateDirectory = runnersStateDir; LoadCredential = lib.mapAttrsToList ( varName: secretPath: "${envVarToCredName varName}:${secretPath}" ) runnersEnv.fileBased; # Hardening DynamicUser = "true"; NoNewPrivileges = "yes"; PrivateTmp = "yes"; PrivateDevices = "yes"; DevicePolicy = "closed"; ProtectSystem = "strict"; ProtectHome = "read-only"; ProtectControlGroups = "yes"; ProtectKernelModules = "yes"; ProtectKernelTunables = "yes"; RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; RestrictNamespaces = "yes"; RestrictRealtime = "yes"; RestrictSUIDSGID = "yes"; MemoryDenyWriteExecute = "no"; # v8 JIT requires memory segments to be Writable-Executable. LockPersonality = "yes"; }; }; networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ (lib.toInt cfg.environment.N8N_PORT) ]; }; Loading nixos/tests/n8n.nix +56 −2 Original line number Diff line number Diff line { lib, pkgs, ... }: let port = 5678; brokerPort = 5679; webhookUrl = "http://example.com"; secretFile = toString (pkgs.writeText "n8n-encryption-key" "test-encryption-key-12345"); authTokenFile = toString (pkgs.writeText "n8n-runner-auth-token" "test-runner-auth-token-12345"); in { name = "n8n"; meta.maintainers = with lib.maintainers; [ k900 ]; meta.maintainers = with lib.maintainers; [ k900 sweenu gepbird ]; node.pkgsReadOnly = false; Loading @@ -20,8 +26,26 @@ in WEBHOOK_URL = webhookUrl; N8N_TEMPLATES_ENABLED = false; DB_PING_INTERVAL_SECONDS = 2; # !!! Don't do this with real keys. The /nix store is world-readable! # !!! Don't do this with real keys/tokens. The /nix store is world-readable! N8N_ENCRYPTION_KEY_FILE = secretFile; N8N_RUNNERS_AUTH_TOKEN_FILE = authTokenFile; }; taskRunners = { enable = true; environment = { # Common env var for all runners N8N_RUNNERS_MAX_CONCURRENCY = 10; }; runners = { javascript = { args = [ "--disallow-code-generation-from-strings" ]; environment.NODE_FUNCTION_ALLOW_BUILTIN = "*"; }; python = { args = [ "--test-arg" ]; environment.N8N_RUNNERS_STDLIB_ALLOW = "*"; }; }; }; }; }; Loading @@ -48,5 +72,35 @@ in custom_extensions_dir = machine.succeed("grep -oP 'N8N_CUSTOM_EXTENSIONS=\\K[^\"]+' /etc/systemd/system/n8n.service").strip() machine.succeed(f"test -L {custom_extensions_dir}/n8n-nodes-carbonejs") machine.succeed(f"test -f {custom_extensions_dir}/n8n-nodes-carbonejs/package.json") # Test task runner integration on n8n service machine.succeed("grep -qF 'N8N_RUNNERS_MODE=external' /etc/systemd/system/n8n.service") machine.succeed("grep -qF 'N8N_RUNNERS_BROKER_PORT=${toString brokerPort}' /etc/systemd/system/n8n.service") machine.succeed("grep -qF 'LoadCredential=n8n_runners_auth_token_file:${authTokenFile}' /etc/systemd/system/n8n.service") # Test task runner service machine.wait_for_unit("n8n-task-runner.service") machine.succeed("systemctl is-active n8n-task-runner.service") # Test that both runner types are enabled machine.succeed("grep -qF 'javascript python' /etc/systemd/system/n8n-task-runner.service") # Test common environment variables are passed to launcher machine.succeed("grep -qF 'N8N_RUNNERS_MAX_CONCURRENCY=10' /etc/systemd/system/n8n-task-runner.service") machine.succeed("grep -qF 'N8N_RUNNERS_TASK_BROKER_URI=http://127.0.0.1:${toString brokerPort}' /etc/systemd/system/n8n-task-runner.service") # Test auth token is loaded via credentials machine.succeed("grep -qF 'LoadCredential=n8n_runners_auth_token_file:${authTokenFile}' /etc/systemd/system/n8n-task-runner.service") # Test launcher config file config_path = machine.succeed("grep -oP 'N8N_RUNNERS_CONFIG_PATH=\\K[^[:space:]\"]+' /etc/systemd/system/n8n-task-runner.service").strip() config = machine.succeed(f"cat {config_path}") assert "NODE_FUNCTION_ALLOW_BUILTIN" in config, "JavaScript env-override not in config" assert "N8N_RUNNERS_STDLIB_ALLOW" in config, "Python env-override not in config" assert "N8N_RUNNERS_MAX_CONCURRENCY" in config, "Common allowed-env not in config" assert "--disallow-code-generation-from-strings" in config, "JavaScript args not in config" assert "--test-arg" in config, "Python args not in config" assert '"health-check-server-port":"5681"' in config, "JavaScript health check port not in config" assert '"health-check-server-port":"5682"' in config, "Python health check port not in config" ''; } Loading
nixos/modules/services/misc/n8n.nix +259 −4 Original line number Diff line number Diff line Loading @@ -30,8 +30,35 @@ let path = "${pkg}/lib/node_modules/${pkg.pname}"; }) cfg.customNodes ); # Runners runnersCfg = cfg.taskRunners; runnersEnv = partitionEnv runnersCfg.environment; commonAllowedEnv = lib.attrNames runnersEnv.regular; enabledRunners = lib.filterAttrs (_name: runnerCfg: runnerCfg.enable) runnersCfg.runners; anyRunnerEnabled = runnersCfg.enable && (enabledRunners != { }); runnerTypes = lib.attrNames enabledRunners; runnersStateDir = "n8n-task-runners"; taskRunnerConfigs = lib.mapAttrsToList (runnerType: runnerCfg: { runner-type = runnerType; workdir = "/var/lib/${runnersStateDir}"; command = runnerCfg.command; args = runnerCfg.args; allowed-env = commonAllowedEnv; env-overrides = runnerCfg.environment; health-check-server-port = toString runnerCfg.healthCheckPort; }) enabledRunners; launcherConfigFile = pkgs.writeText "n8n-task-runners.json" ( builtins.toJSON { task-runners = taskRunnerConfigs; } ); in { meta.maintainers = with lib.maintainers; [ sweenu gepbird ]; imports = [ (lib.mkRemovedOptionModule [ "services" "n8n" "settings" ] "Use services.n8n.environment instead.") (lib.mkRemovedOptionModule [ Loading Loading @@ -128,13 +155,188 @@ in When enabled, n8n sends notifications of new versions and security updates. ''; }; N8N_CUSTOM_EXTENSIONS = lib.mkOption { internal = true; type = with lib.types; nullOr path; default = if cfg.customNodes != [ ] then toString customNodesDir else null; description = '' Specify the path to directories containing your custom nodes. ''; }; N8N_RUNNERS_MODE = lib.mkOption { internal = true; type = with lib.types; enum [ "internal" "external" ]; default = if runnersCfg.enable then "external" else "internal"; description = '' How to launch and run the task runner. `internal` means n8n will launch a task runner as child process. `external` means an external orchestrator will launch the task runner. ''; }; N8N_RUNNERS_BROKER_PORT = lib.mkOption { type = with lib.types; coercedTo port toString str; default = 5679; description = '' Port the task broker listens on for task runner connections. ''; }; N8N_RUNNERS_BROKER_LISTEN_ADDRESS = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = '' Address the task broker listens on. ''; }; N8N_RUNNERS_AUTH_TOKEN_FILE = lib.mkOption { type = with lib.types; nullOr path; default = null; description = '' Path to a file containing the shared authentication token used between the n8n server (task broker) and the task runners. This option is required when {option}`services.n8n.taskRunners.enable` is true. The file should be readable by the service and not stored in the Nix store. Use tools like `agenix` or `sops-nix` to manage this secret. ''; }; }; }; default = { }; }; taskRunners = { enable = lib.mkEnableOption "n8n task runners for sandboxed Code node execution"; launcherPackage = lib.mkPackageOption pkgs "n8n-task-runner-launcher" { }; environment = lib.mkOption { description = '' Environment variables for the task runner launcher and runners. These are common to all runners and passed via `allowed-env` in the launcher config. See <https://docs.n8n.io/hosting/configuration/environment-variables/task-runners/> for available options. Environment variables ending with `_FILE` are automatically handled as secrets: they are loaded via systemd credentials for secure access with `DynamicUser=true`. Note: The authentication token should be set via {option}`services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE`. ''; example = lib.literalExpression '' { N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT = 15; N8N_RUNNERS_MAX_CONCURRENCY = 10; } ''; type = lib.types.submodule { freeformType = with lib.types; attrsOf (oneOf [ str (coercedTo int toString str) (coercedTo bool builtins.toJSON str) ]); options = { N8N_RUNNERS_CONFIG_PATH = lib.mkOption { internal = true; type = with lib.types; nullOr path; default = launcherConfigFile; description = '' Path to the configuration file for the task runner launcher. ''; }; N8N_RUNNERS_AUTH_TOKEN_FILE = lib.mkOption { type = with lib.types; nullOr path; default = cfg.environment.N8N_RUNNERS_AUTH_TOKEN_FILE; defaultText = lib.literalExpression "config.services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE"; description = '' Path to the authentication token file for the task runner. ''; }; N8N_RUNNERS_TASK_BROKER_URI = lib.mkOption { type = lib.types.str; default = "http://${cfg.environment.N8N_RUNNERS_BROKER_LISTEN_ADDRESS}:${cfg.environment.N8N_RUNNERS_BROKER_PORT}"; defaultText = lib.literalExpression ''"http://''${config.services.n8n.environment.N8N_RUNNERS_BROKER_LISTEN_ADDRESS}:''${config.services.n8n.environment.N8N_RUNNERS_BROKER_PORT}"''; description = '' URI of the n8n task broker that the runner connects to. ''; }; }; }; default = { }; }; runners = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule ( { name, ... }: { options = { enable = lib.mkOption { type = lib.types.bool; default = true; description = '' Whether to enable the ${name} task runner. Only takes effect when {option}`services.n8n.taskRunners.enable` is true. ''; }; command = lib.mkOption { type = lib.types.str; description = "Command to execute for this runner."; }; healthCheckPort = lib.mkOption { type = lib.types.port; description = "Port for the runner's health check server."; }; args = lib.mkOption { type = with lib.types; listOf str; default = [ ]; description = "Additional command-line arguments to pass to the task runner."; }; environment = lib.mkOption { type = with lib.types; attrsOf str; default = { }; description = "Environment variables specific to this task runner."; }; }; } ) ); default = { }; defaultText = lib.literalExpression '' { javascript = { enable = true; command = lib.getExe' config.services.n8n.package "n8n-task-runner"; healthCheckPort = 5681; }; python = { enable = true; command = lib.getExe' config.services.n8n.package "n8n-task-runner-python"; healthCheckPort = 5682; }; } ''; description = "Configuration for individual task runners."; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = anyRunnerEnabled -> cfg.environment.N8N_RUNNERS_AUTH_TOKEN_FILE != null; message = "services.n8n.environment.N8N_RUNNERS_AUTH_TOKEN_FILE must be set when task runners are enabled."; } ]; systemd.services.n8n = { description = "n8n service"; after = [ "network.target" ]; Loading @@ -144,15 +346,12 @@ in // { HOME = cfg.environment.N8N_USER_FOLDER; } // lib.optionalAttrs (cfg.customNodes != [ ]) { N8N_CUSTOM_EXTENSIONS = toString customNodesDir; } // n8nEnv.fileBasedTransformed; serviceConfig = { Type = "simple"; ExecStart = lib.getExe cfg.package; Restart = "on-failure"; StateDirectory = "n8n"; StateDirectory = baseNameOf cfg.environment.N8N_USER_FOLDER; LoadCredential = lib.mapAttrsToList ( varName: secretPath: "${envVarToCredName varName}:${secretPath}" Loading @@ -178,6 +377,62 @@ in }; }; warnings = lib.optional (runnersCfg.enable && !anyRunnerEnabled) '' services.n8n.taskRunners.enable is true, but both JavaScript and Python runners are disabled. Enable at least one runner or disable taskRunners. ''; # We set the defaults here to ease adding attributes services.n8n.taskRunners.runners = { javascript = lib.mapAttrs (_: lib.mkDefault) { enable = true; command = lib.getExe' cfg.package "n8n-task-runner"; healthCheckPort = 5681; }; python = lib.mapAttrs (_: lib.mkDefault) { enable = true; command = lib.getExe' cfg.package "n8n-task-runner-python"; healthCheckPort = 5682; }; }; systemd.services.n8n-task-runner = lib.mkIf anyRunnerEnabled { description = "n8n task runner"; after = [ "n8n.service" ]; requires = [ "n8n.service" ]; wantedBy = [ "multi-user.target" ]; environment = runnersEnv.regular // runnersEnv.fileBasedTransformed; serviceConfig = { Type = "simple"; ExecStart = "${lib.getExe runnersCfg.launcherPackage} ${lib.concatStringsSep " " runnerTypes}"; Restart = "on-failure"; StateDirectory = runnersStateDir; LoadCredential = lib.mapAttrsToList ( varName: secretPath: "${envVarToCredName varName}:${secretPath}" ) runnersEnv.fileBased; # Hardening DynamicUser = "true"; NoNewPrivileges = "yes"; PrivateTmp = "yes"; PrivateDevices = "yes"; DevicePolicy = "closed"; ProtectSystem = "strict"; ProtectHome = "read-only"; ProtectControlGroups = "yes"; ProtectKernelModules = "yes"; ProtectKernelTunables = "yes"; RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; RestrictNamespaces = "yes"; RestrictRealtime = "yes"; RestrictSUIDSGID = "yes"; MemoryDenyWriteExecute = "no"; # v8 JIT requires memory segments to be Writable-Executable. LockPersonality = "yes"; }; }; networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ (lib.toInt cfg.environment.N8N_PORT) ]; }; Loading
nixos/tests/n8n.nix +56 −2 Original line number Diff line number Diff line { lib, pkgs, ... }: let port = 5678; brokerPort = 5679; webhookUrl = "http://example.com"; secretFile = toString (pkgs.writeText "n8n-encryption-key" "test-encryption-key-12345"); authTokenFile = toString (pkgs.writeText "n8n-runner-auth-token" "test-runner-auth-token-12345"); in { name = "n8n"; meta.maintainers = with lib.maintainers; [ k900 ]; meta.maintainers = with lib.maintainers; [ k900 sweenu gepbird ]; node.pkgsReadOnly = false; Loading @@ -20,8 +26,26 @@ in WEBHOOK_URL = webhookUrl; N8N_TEMPLATES_ENABLED = false; DB_PING_INTERVAL_SECONDS = 2; # !!! Don't do this with real keys. The /nix store is world-readable! # !!! Don't do this with real keys/tokens. The /nix store is world-readable! N8N_ENCRYPTION_KEY_FILE = secretFile; N8N_RUNNERS_AUTH_TOKEN_FILE = authTokenFile; }; taskRunners = { enable = true; environment = { # Common env var for all runners N8N_RUNNERS_MAX_CONCURRENCY = 10; }; runners = { javascript = { args = [ "--disallow-code-generation-from-strings" ]; environment.NODE_FUNCTION_ALLOW_BUILTIN = "*"; }; python = { args = [ "--test-arg" ]; environment.N8N_RUNNERS_STDLIB_ALLOW = "*"; }; }; }; }; }; Loading @@ -48,5 +72,35 @@ in custom_extensions_dir = machine.succeed("grep -oP 'N8N_CUSTOM_EXTENSIONS=\\K[^\"]+' /etc/systemd/system/n8n.service").strip() machine.succeed(f"test -L {custom_extensions_dir}/n8n-nodes-carbonejs") machine.succeed(f"test -f {custom_extensions_dir}/n8n-nodes-carbonejs/package.json") # Test task runner integration on n8n service machine.succeed("grep -qF 'N8N_RUNNERS_MODE=external' /etc/systemd/system/n8n.service") machine.succeed("grep -qF 'N8N_RUNNERS_BROKER_PORT=${toString brokerPort}' /etc/systemd/system/n8n.service") machine.succeed("grep -qF 'LoadCredential=n8n_runners_auth_token_file:${authTokenFile}' /etc/systemd/system/n8n.service") # Test task runner service machine.wait_for_unit("n8n-task-runner.service") machine.succeed("systemctl is-active n8n-task-runner.service") # Test that both runner types are enabled machine.succeed("grep -qF 'javascript python' /etc/systemd/system/n8n-task-runner.service") # Test common environment variables are passed to launcher machine.succeed("grep -qF 'N8N_RUNNERS_MAX_CONCURRENCY=10' /etc/systemd/system/n8n-task-runner.service") machine.succeed("grep -qF 'N8N_RUNNERS_TASK_BROKER_URI=http://127.0.0.1:${toString brokerPort}' /etc/systemd/system/n8n-task-runner.service") # Test auth token is loaded via credentials machine.succeed("grep -qF 'LoadCredential=n8n_runners_auth_token_file:${authTokenFile}' /etc/systemd/system/n8n-task-runner.service") # Test launcher config file config_path = machine.succeed("grep -oP 'N8N_RUNNERS_CONFIG_PATH=\\K[^[:space:]\"]+' /etc/systemd/system/n8n-task-runner.service").strip() config = machine.succeed(f"cat {config_path}") assert "NODE_FUNCTION_ALLOW_BUILTIN" in config, "JavaScript env-override not in config" assert "N8N_RUNNERS_STDLIB_ALLOW" in config, "Python env-override not in config" assert "N8N_RUNNERS_MAX_CONCURRENCY" in config, "Common allowed-env not in config" assert "--disallow-code-generation-from-strings" in config, "JavaScript args not in config" assert "--test-arg" in config, "Python args not in config" assert '"health-check-server-port":"5681"' in config, "JavaScript health check port not in config" assert '"health-check-server-port":"5682"' in config, "Python health check port not in config" ''; }