{ config, lib, pkgs, ... }: with lib; let cfg = config.services.nextcloud-claude-bot; pythonEnv = pkgs.python3.withPackages (ps: with ps; [ fastapi uvicorn httpx ]); botModule = pkgs.runCommand "nextcloud-claude-bot-module" {} '' mkdir -p $out cp ${./bot.py} $out/nextcloud_claude_bot.py ''; in { options.services.nextcloud-claude-bot = { enable = mkEnableOption "Nextcloud Talk Claude Bot"; port = mkOption { type = types.port; default = 8085; description = "Port for the webhook listener"; }; host = mkOption { type = types.str; default = "127.0.0.1"; description = "Host to bind to"; }; nextcloudUrl = mkOption { type = types.str; example = "https://cloud.example.com"; description = "Base URL of your Nextcloud instance"; }; botSecretFile = mkOption { type = types.path; description = "Path to file containing the bot secret (shared with Nextcloud)"; }; claudePath = mkOption { type = types.path; default = "${pkgs.claude-code}/bin/claude"; description = "Path to claude CLI binary"; }; allowedUsers = mkOption { type = types.listOf types.str; default = []; example = [ "harald" "admin" ]; description = "Nextcloud usernames allowed to talk to the bot (empty = all)"; }; contextMessages = mkOption { type = types.int; default = 6; description = "Number of recent messages to fetch from chat for context"; }; timeout = mkOption { type = types.int; default = 120; description = "Timeout in seconds for Claude CLI"; }; systemPrompt = mkOption { type = types.nullOr types.str; default = null; example = "Du bist ein hilfreicher Assistent."; description = "Optional system prompt for Claude"; }; }; config = mkIf cfg.enable { systemd.services.nextcloud-claude-bot = { description = "Nextcloud Talk Claude Bot"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment = { HOME = "/var/lib/nextcloud-claude-bot"; BOT_HOST = cfg.host; BOT_PORT = toString cfg.port; NEXTCLOUD_URL = cfg.nextcloudUrl; CLAUDE_PATH = cfg.claudePath; ALLOWED_USERS = concatStringsSep "," cfg.allowedUsers; CONTEXT_MESSAGES = toString cfg.contextMessages; TIMEOUT = toString cfg.timeout; SYSTEM_PROMPT = cfg.systemPrompt or ""; PYTHONPATH = botModule; }; serviceConfig = { Type = "simple"; ExecStart = "${pythonEnv}/bin/uvicorn nextcloud_claude_bot:app --host ${cfg.host} --port ${toString cfg.port}"; Restart = "always"; RestartSec = 5; User = "claude-bot"; Group = "claude-bot"; # Security hardening NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = "read-only"; PrivateTmp = true; PrivateDevices = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; MemoryDenyWriteExecute = false; # Python needs this LockPersonality = true; # Bot secret LoadCredential = "bot-secret:${cfg.botSecretFile}"; # Claude CLI needs home for config StateDirectory = "nextcloud-claude-bot"; }; }; users.users.claude-bot = { isSystemUser = true; group = "claude-bot"; home = "/var/lib/nextcloud-claude-bot"; }; users.groups.claude-bot = {}; # Nginx reverse proxy config (optional, if you want external access) # services.nginx.virtualHosts."cloud.example.com".locations."/claude-bot/" = { # proxyPass = "http://${cfg.host}:${toString cfg.port}/"; # }; }; }