feature/pi-refactor
  1{
  2  config,
  3  lib,
  4  pkgs,
  5  ...
  6}:
  7let
  8  cfg = config.services.daneel;
  9in
 10{
 11  options.services.daneel = {
 12    enable = lib.mkEnableOption "Daneel XMPP research bot";
 13
 14    package = lib.mkOption {
 15      type = lib.types.package;
 16      default = pkgs.callPackage ./package.nix { };
 17      description = "The daneel package to use";
 18    };
 19
 20    xmppJid = lib.mkOption {
 21      type = lib.types.str;
 22      example = "daneel@xmpp.example.com";
 23      description = "XMPP JID for the bot";
 24    };
 25
 26    xmppPasswordFile = lib.mkOption {
 27      type = lib.types.path;
 28      description = "Path to file containing XMPP password";
 29    };
 30
 31    ownerJid = lib.mkOption {
 32      type = lib.types.str;
 33      example = "user@xmpp.example.com";
 34      description = "XMPP JID of the bot owner (only this JID can interact)";
 35    };
 36
 37    dataDir = lib.mkOption {
 38      type = lib.types.path;
 39      default = "/var/lib/daneel";
 40      description = "Directory for session data";
 41    };
 42
 43    inboxPath = lib.mkOption {
 44      type = lib.types.path;
 45      default = "/var/lib/daneel/inbox.org";
 46      description = "Path to org-mode inbox file";
 47    };
 48
 49    geminiApiKeyFile = lib.mkOption {
 50      type = lib.types.nullOr lib.types.path;
 51      default = null;
 52      description = "Path to file containing Google Gemini API key";
 53    };
 54
 55    anthropicApiKeyFile = lib.mkOption {
 56      type = lib.types.nullOr lib.types.path;
 57      default = null;
 58      description = "Path to file containing Anthropic API key";
 59    };
 60
 61    openaiApiKeyFile = lib.mkOption {
 62      type = lib.types.nullOr lib.types.path;
 63      default = null;
 64      description = "Path to file containing OpenAI API key";
 65    };
 66
 67    searxngUrl = lib.mkOption {
 68      type = lib.types.nullOr lib.types.str;
 69      default = null;
 70      example = "https://search.sbr.pm";
 71      description = "URL of SearXNG instance for web search";
 72    };
 73
 74    debug = lib.mkOption {
 75      type = lib.types.bool;
 76      default = false;
 77      description = "Enable debug logging";
 78    };
 79
 80    user = lib.mkOption {
 81      type = lib.types.str;
 82      default = "daneel";
 83      description = "User to run daneel as";
 84    };
 85
 86    group = lib.mkOption {
 87      type = lib.types.str;
 88      default = "daneel";
 89      description = "Group to run daneel as";
 90    };
 91  };
 92
 93  config = lib.mkIf cfg.enable {
 94    assertions = [
 95      {
 96        assertion =
 97          cfg.geminiApiKeyFile != null
 98          || cfg.anthropicApiKeyFile != null
 99          || cfg.openaiApiKeyFile != null;
100        message = "At least one API key file must be configured for Daneel (geminiApiKeyFile, anthropicApiKeyFile, or openaiApiKeyFile)";
101      }
102    ];
103
104    # Only create user/group when using dedicated daneel user
105    # (skip when running as an existing user like "vincent")
106    users.users = lib.mkIf (cfg.user == "daneel") {
107      daneel = {
108        isSystemUser = true;
109        group = cfg.group;
110        home = cfg.dataDir;
111        createHome = true;
112      };
113    };
114
115    users.groups = lib.mkIf (cfg.group == "daneel") {
116      daneel = { };
117    };
118
119    systemd.services.daneel = {
120      description = "Daneel XMPP Research Bot";
121      wantedBy = [ "multi-user.target" ];
122      after = [ "network.target" ];
123
124      serviceConfig = {
125        Type = "simple";
126        User = cfg.user;
127        Group = cfg.group;
128        WorkingDirectory = cfg.dataDir;
129        Restart = "always";
130        RestartSec = "10s";
131
132        # Hardening
133        NoNewPrivileges = true;
134        PrivateTmp = true;
135        ProtectSystem = "strict";
136        ProtectHome = cfg.user == "daneel";
137        ReadWritePaths = [ cfg.dataDir (builtins.dirOf cfg.inboxPath) ];
138        ProtectKernelTunables = true;
139        ProtectKernelModules = true;
140        ProtectControlGroups = true;
141        RestrictAddressFamilies = [
142          "AF_INET"
143          "AF_INET6"
144          "AF_UNIX"
145        ];
146        RestrictNamespaces = true;
147        LockPersonality = true;
148        RestrictRealtime = true;
149        RestrictSUIDSGID = true;
150        PrivateDevices = true;
151      };
152
153      script = ''
154        export DANEEL_XMPP_JID="${cfg.xmppJid}"
155        export DANEEL_XMPP_PASSWORD="$(cat ${cfg.xmppPasswordFile})"
156        export DANEEL_OWNER_JID="${cfg.ownerJid}"
157        export DANEEL_DATA_DIR="${cfg.dataDir}"
158        export DANEEL_INBOX_PATH="${cfg.inboxPath}"
159        ${lib.optionalString cfg.debug ''export DANEEL_DEBUG="true"''}
160
161        ${lib.optionalString (cfg.geminiApiKeyFile != null) ''
162          export GEMINI_API_KEY="$(cat ${cfg.geminiApiKeyFile})"
163        ''}
164        ${lib.optionalString (cfg.anthropicApiKeyFile != null) ''
165          export ANTHROPIC_API_KEY="$(cat ${cfg.anthropicApiKeyFile})"
166        ''}
167        ${lib.optionalString (cfg.openaiApiKeyFile != null) ''
168          export OPENAI_API_KEY="$(cat ${cfg.openaiApiKeyFile})"
169        ''}
170        ${lib.optionalString (cfg.searxngUrl != null) ''
171          export SEARXNG_URL="${cfg.searxngUrl}"
172        ''}
173
174        exec ${cfg.package}/bin/daneel
175      '';
176    };
177  };
178}