nixos/modules/nixos/openclaw.nix
2026-03-06 20:16:16 +01:00

199 lines
6.2 KiB
Nix

{ config, lib, pkgs, inputs, ... }:
with lib;
let
cfg = config.services.openclaw-service;
openclawPkg = inputs.nix-openclaw.packages.${pkgs.system}.default;
in
{
options.services.openclaw-service = {
enable = mkEnableOption "OpenClaw AI Agent Service";
user = mkOption {
type = types.str;
default = "kafka";
description = "User to run OpenClaw as";
};
group = mkOption {
type = types.str;
default = "kafka";
description = "Group to run OpenClaw as";
};
port = mkOption {
type = types.int;
default = 18789;
description = "Port to listen on";
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/openclaw";
description = "Directory for OpenClaw data and workspace";
};
};
config = mkIf cfg.enable {
users.users.${cfg.user} = {
isNormalUser = true;
linger = true;
uid = 1001;
group = cfg.group;
description = "OpenClaw Service User";
home = cfg.dataDir;
createHome = true;
};
users.groups.${cfg.group} = {
gid = 1001;
};
sops.secrets."openclaw/discord_token" = {
owner = cfg.user;
group = cfg.group;
key = "discord_bot_token";
};
sops.secrets."openclaw/glm_api_key" = {
owner = cfg.user;
group = cfg.group;
key = "glm_api_key";
};
sops.secrets."openclaw/brave_api_key" = {
owner = cfg.user;
group = cfg.group;
key = "searxng_brave_api_key";
};
# Ensure secrets exist in sops config, if not user needs to add them.
# We assume secrets.yaml has these keys or user will map them.
# The user had /run/secrets/openclaw-discord-token before.
systemd.services.openclaw = {
description = "OpenClaw AI Agent";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
StateDirectory = "openclaw"; # Creates /var/lib/openclaw
WorkingDirectory = cfg.dataDir;
Restart = "always";
RestartSec = "10s";
# Environment variables or config file generation
# OpenClaw seems to take config via a file or env vars.
# Based on previous flake, it used a config file.
# We can generate the config file in the ExecStartPre or rely on env vars if supported.
# The previous flake copied a config file.
# Let's verify how openclaw takes config.
# It used OPENCLAW_CONFIG_DIR, OPENCLAW_DATA_DIR, OPENCLAW_WORKSPACE_DIR env vars.
};
environment = {
OPENCLAW_CONFIG_PATH = "${cfg.dataDir}/config/openclaw.json";
OPENCLAW_HOME = "${cfg.dataDir}";
OPENCLAW_DATA_DIR = "${cfg.dataDir}/data";
OPENCLAW_WORKSPACE_DIR = "${cfg.dataDir}/workspace";
OPENCLAW_GATEWAY_TOKEN = "openclaw-local-token";
XDG_RUNTIME_DIR = "/run/user/1001";
DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/user/1001/bus";
# We need to ensure these directories exist.
};
preStart = ''
mkdir -p ${cfg.dataDir}/config
mkdir -p ${cfg.dataDir}/data
mkdir -p ${cfg.dataDir}/workspace
# Generate config.json
cat > ${cfg.dataDir}/config/openclaw.json <<EOF
{
"gateway": {
"mode": "local",
"port": ${toString cfg.port},
"bind": "loopback",
"trustedProxies": [ "::1", "127.0.0.1", "10.88.0.0/16", "10.89.0.0/16" ],
"controlUi": {
"dangerouslyAllowHostHeaderOriginFallback": true,
"allowedOrigins": [ "*" ]
}
},
"agents": {
"defaults": {
"workspace": "${cfg.dataDir}/workspace",
"model": { "primary": "zai/glm-4.7" },
"memorySearch": {
"enabled": true,
"provider": "local",
"remote": {
"baseUrl": "http://localhost:11434"
},
"model": "nomic-embed-text"
}
}
},
"channels": {
"discord": {
"enabled": true,
"token": "$(cat ${config.sops.secrets."openclaw/discord_token".path})",
"allowFrom": [ "1178286690750693419", "*" ],
"groupPolicy": "open",
"dmPolicy": "open"
}
},
"commands": {
"native": true,
"nativeSkills": "auto",
"restart": true,
"ownerDisplay": "raw"
},
"tools": {
"exec": { "security": "full", "ask": "off" }
},
"models": {
"mode": "merge",
"providers": {
"ollama": {
"baseUrl": "http://127.0.0.1:11434",
"models": [
{ "id": "nomic-embed-text", "name": "nomic-embed-text" },
{ "id": "mxbai-embed-large", "name": "mxbai-embed-large" },
{ "id": "llama3.2", "name": "llama3.2", "reasoning": true, "contextWindow": 128000, "maxTokens": 128000 },
{ "id": "llama3.1", "name": "llama3.1", "reasoning": true, "contextWindow": 128000, "maxTokens": 128000 }
]
},
"zai": {
"baseUrl": "https://api.z.ai/api/coding/paas/v4",
"apiKey": "$(cat ${config.sops.secrets."openclaw/glm_api_key".path})",
"models": [
{ "id": "glm-4.7", "name": "GLM 4.7", "reasoning": true, "contextWindow": 128000, "maxTokens": 128000 },
{ "id": "glm-5", "name": "GLM 5", "reasoning": true, "contextWindow": 128000, "maxTokens": 128000 }
]
}
}
},
"skills": {
"entries": {
"mcporter": { "enabled": true }
}
},
"env": {
"vars": {
"BRAVE_API_KEY": "$(cat ${config.sops.secrets."openclaw/brave_api_key".path})",
"OPENCLAW_BRAVE_API_KEY": "$(cat ${config.sops.secrets."openclaw/brave_api_key".path})"
}
}
}
EOF
'';
script = "${openclawPkg}/bin/openclaw gateway run --port ${toString cfg.port}";
};
};
}