|
| 1 | +{ |
| 2 | + config, |
| 3 | + lib, |
| 4 | + pkgs, |
| 5 | + ... |
| 6 | +}: |
| 7 | + |
| 8 | +let |
| 9 | + cfg = config.services.openclaw-gateway; |
| 10 | + |
| 11 | + deepConfigType = lib.types.mkOptionType { |
| 12 | + name = "openclaw-config-attrs"; |
| 13 | + description = "OpenClaw JSON config (attrset), merged deeply via lib.recursiveUpdate."; |
| 14 | + check = builtins.isAttrs; |
| 15 | + merge = _loc: defs: lib.foldl' lib.recursiveUpdate { } (map (d: d.value) defs); |
| 16 | + }; |
| 17 | + |
| 18 | + configJson = builtins.toJSON cfg.config; |
| 19 | + generatedConfigFile = pkgs.writeText "openclaw.json" configJson; |
| 20 | + configFile = if cfg.configFile != null then cfg.configFile else generatedConfigFile; |
| 21 | + |
| 22 | + # `environment.etc` takes a relative path. |
| 23 | + etcRelPath = lib.removePrefix "/etc/" cfg.configPath; |
| 24 | + |
| 25 | + execStartCmd = |
| 26 | + if cfg.execStart != null then |
| 27 | + cfg.execStart |
| 28 | + else |
| 29 | + "${cfg.package}/bin/openclaw gateway --port ${toString cfg.port}"; |
| 30 | + |
| 31 | +in |
| 32 | +{ |
| 33 | + options.services.openclaw-gateway = with lib; { |
| 34 | + enable = mkEnableOption "OpenClaw gateway (openclaw gateway as a systemd service)"; |
| 35 | + |
| 36 | + unitName = mkOption { |
| 37 | + type = types.str; |
| 38 | + default = "openclaw-gateway"; |
| 39 | + description = "systemd unit name (service will be <unitName>.service)."; |
| 40 | + }; |
| 41 | + |
| 42 | + package = mkOption { |
| 43 | + type = types.package; |
| 44 | + default = if pkgs ? openclaw-gateway then pkgs.openclaw-gateway else pkgs.openclaw; |
| 45 | + description = "OpenClaw gateway package."; |
| 46 | + }; |
| 47 | + |
| 48 | + port = mkOption { |
| 49 | + type = types.port; |
| 50 | + default = 18789; |
| 51 | + description = "Gateway listen port."; |
| 52 | + }; |
| 53 | + |
| 54 | + user = mkOption { |
| 55 | + type = types.str; |
| 56 | + default = "openclaw"; |
| 57 | + description = "System user running the gateway."; |
| 58 | + }; |
| 59 | + |
| 60 | + group = mkOption { |
| 61 | + type = types.str; |
| 62 | + default = "openclaw"; |
| 63 | + description = "System group running the gateway."; |
| 64 | + }; |
| 65 | + |
| 66 | + createUser = mkOption { |
| 67 | + type = types.bool; |
| 68 | + default = true; |
| 69 | + description = "Create the user/group automatically."; |
| 70 | + }; |
| 71 | + |
| 72 | + stateDir = mkOption { |
| 73 | + type = types.str; |
| 74 | + default = "/var/lib/openclaw"; |
| 75 | + description = "State dir (OPENCLAW_STATE_DIR)."; |
| 76 | + }; |
| 77 | + |
| 78 | + workingDirectory = mkOption { |
| 79 | + type = types.str; |
| 80 | + default = cfg.stateDir; |
| 81 | + description = "Working directory for the systemd service."; |
| 82 | + }; |
| 83 | + |
| 84 | + configPath = mkOption { |
| 85 | + type = types.str; |
| 86 | + default = "/etc/openclaw/openclaw.json"; |
| 87 | + description = "Path to the OpenClaw JSON config file (OPENCLAW_CONFIG_PATH). Must be under /etc."; |
| 88 | + }; |
| 89 | + |
| 90 | + configFile = mkOption { |
| 91 | + type = types.nullOr types.path; |
| 92 | + default = null; |
| 93 | + description = "Optional path to an existing config file. If set, it is copied to configPath (under /etc)."; |
| 94 | + }; |
| 95 | + |
| 96 | + config = mkOption { |
| 97 | + type = deepConfigType; |
| 98 | + default = { }; |
| 99 | + description = "OpenClaw JSON config (attrset), deep-merged across definitions."; |
| 100 | + }; |
| 101 | + |
| 102 | + logPath = mkOption { |
| 103 | + type = types.str; |
| 104 | + default = "${cfg.stateDir}/logs/gateway.log"; |
| 105 | + description = "Log file path (systemd StandardOutput/StandardError append)."; |
| 106 | + }; |
| 107 | + |
| 108 | + environment = mkOption { |
| 109 | + type = types.attrsOf types.str; |
| 110 | + default = { }; |
| 111 | + description = "Additional environment variables for the gateway process."; |
| 112 | + }; |
| 113 | + |
| 114 | + environmentFiles = mkOption { |
| 115 | + type = types.listOf types.str; |
| 116 | + default = [ ]; |
| 117 | + description = "systemd EnvironmentFile= entries (use leading '-' to ignore missing)."; |
| 118 | + }; |
| 119 | + |
| 120 | + execStart = mkOption { |
| 121 | + type = types.nullOr types.str; |
| 122 | + default = null; |
| 123 | + description = "Override ExecStart command. If unset, runs: openclaw gateway --port <port>."; |
| 124 | + }; |
| 125 | + |
| 126 | + execStartPre = mkOption { |
| 127 | + type = types.listOf types.str; |
| 128 | + default = [ ]; |
| 129 | + description = "List of ExecStartPre= commands."; |
| 130 | + }; |
| 131 | + |
| 132 | + servicePath = mkOption { |
| 133 | + type = types.listOf types.package; |
| 134 | + default = [ ]; |
| 135 | + description = "Extra packages added to systemd service PATH."; |
| 136 | + }; |
| 137 | + |
| 138 | + restart = mkOption { |
| 139 | + type = types.str; |
| 140 | + default = "always"; |
| 141 | + description = "systemd Restart=."; |
| 142 | + }; |
| 143 | + |
| 144 | + restartSec = mkOption { |
| 145 | + type = types.int; |
| 146 | + default = 2; |
| 147 | + description = "systemd RestartSec=."; |
| 148 | + }; |
| 149 | + }; |
| 150 | + |
| 151 | + config = lib.mkIf cfg.enable { |
| 152 | + assertions = [ |
| 153 | + { |
| 154 | + assertion = lib.hasPrefix "/etc/" cfg.configPath; |
| 155 | + message = "services.openclaw-gateway.configPath must be under /etc (got: ${cfg.configPath})."; |
| 156 | + } |
| 157 | + ]; |
| 158 | + |
| 159 | + users.groups.${cfg.group} = lib.mkIf cfg.createUser { }; |
| 160 | + users.users.${cfg.user} = lib.mkIf cfg.createUser { |
| 161 | + isSystemUser = true; |
| 162 | + group = cfg.group; |
| 163 | + home = cfg.stateDir; |
| 164 | + createHome = true; |
| 165 | + shell = pkgs.bashInteractive; |
| 166 | + }; |
| 167 | + |
| 168 | + systemd.tmpfiles.rules = [ |
| 169 | + "d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -" |
| 170 | + "d ${builtins.dirOf cfg.logPath} 0750 ${cfg.user} ${cfg.group} - -" |
| 171 | + "d ${builtins.dirOf cfg.configPath} 0755 root root - -" |
| 172 | + ]; |
| 173 | + |
| 174 | + environment.etc.${etcRelPath} = { |
| 175 | + mode = "0644"; |
| 176 | + source = configFile; |
| 177 | + }; |
| 178 | + |
| 179 | + systemd.services.${cfg.unitName} = { |
| 180 | + description = "OpenClaw gateway"; |
| 181 | + wantedBy = [ "multi-user.target" ]; |
| 182 | + after = [ "network.target" ]; |
| 183 | + |
| 184 | + environment = { |
| 185 | + OPENCLAW_CONFIG_PATH = cfg.configPath; |
| 186 | + OPENCLAW_STATE_DIR = cfg.stateDir; |
| 187 | + |
| 188 | + # Backward-compatible env names. |
| 189 | + CLAWDBOT_CONFIG_PATH = cfg.configPath; |
| 190 | + CLAWDBOT_STATE_DIR = cfg.stateDir; |
| 191 | + } |
| 192 | + // cfg.environment; |
| 193 | + |
| 194 | + serviceConfig = { |
| 195 | + User = cfg.user; |
| 196 | + Group = cfg.group; |
| 197 | + WorkingDirectory = cfg.workingDirectory; |
| 198 | + |
| 199 | + EnvironmentFile = cfg.environmentFiles; |
| 200 | + ExecStartPre = cfg.execStartPre; |
| 201 | + ExecStart = execStartCmd; |
| 202 | + |
| 203 | + Restart = cfg.restart; |
| 204 | + RestartSec = cfg.restartSec; |
| 205 | + |
| 206 | + StandardOutput = "append:${cfg.logPath}"; |
| 207 | + StandardError = "append:${cfg.logPath}"; |
| 208 | + }; |
| 209 | + |
| 210 | + path = [ |
| 211 | + pkgs.bash |
| 212 | + pkgs.coreutils |
| 213 | + ] |
| 214 | + ++ cfg.servicePath; |
| 215 | + }; |
| 216 | + }; |
| 217 | +} |
0 commit comments