main
  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    defaultModel = lib.mkOption {
 50      type = lib.types.str;
 51      default = "sonnet";
 52      description = "Default model alias (e.g., sonnet, opus, gemini)";
 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    googleApiKeyFile = lib.mkOption {
 62      type = lib.types.nullOr lib.types.path;
 63      default = null;
 64      description = "Path to file containing Google API key";
 65    };
 66
 67    openaiApiKeyFile = lib.mkOption {
 68      type = lib.types.nullOr lib.types.path;
 69      default = null;
 70      description = "Path to file containing OpenAI API key";
 71    };
 72
 73    ollamaBaseUrl = lib.mkOption {
 74      type = lib.types.nullOr lib.types.str;
 75      default = null;
 76      example = "http://localhost:11434";
 77      description = "Base URL for Ollama API";
 78    };
 79
 80    debug = lib.mkOption {
 81      type = lib.types.bool;
 82      default = false;
 83      description = "Enable debug logging";
 84    };
 85
 86    user = lib.mkOption {
 87      type = lib.types.str;
 88      default = "daneel";
 89      description = "User to run daneel as";
 90    };
 91
 92    group = lib.mkOption {
 93      type = lib.types.str;
 94      default = "daneel";
 95      description = "Group to run daneel as";
 96    };
 97  };
 98
 99  config = lib.mkIf cfg.enable {
100    users.users.${cfg.user} = {
101      isSystemUser = true;
102      group = cfg.group;
103      home = cfg.dataDir;
104      createHome = true;
105    };
106
107    users.groups.${cfg.group} = { };
108
109    systemd.services.daneel = {
110      description = "Daneel XMPP Research Bot";
111      wantedBy = [ "multi-user.target" ];
112      after = [ "network.target" ];
113
114      serviceConfig = {
115        Type = "simple";
116        User = cfg.user;
117        Group = cfg.group;
118        WorkingDirectory = cfg.dataDir;
119        ExecStart = "${cfg.package}/bin/daneel";
120        Restart = "always";
121        RestartSec = "10s";
122
123        # Hardening
124        NoNewPrivileges = true;
125        PrivateTmp = true;
126        ProtectSystem = "strict";
127        ProtectHome = true;
128        ReadWritePaths = [ cfg.dataDir (builtins.dirOf cfg.inboxPath) ];
129        ProtectKernelTunables = true;
130        ProtectKernelModules = true;
131        ProtectControlGroups = true;
132        RestrictAddressFamilies = [
133          "AF_INET"
134          "AF_INET6"
135          "AF_UNIX"
136        ];
137        RestrictNamespaces = true;
138        LockPersonality = true;
139        MemoryDenyWriteExecute = true;
140        RestrictRealtime = true;
141        RestrictSUIDSGID = true;
142        PrivateDevices = true;
143      };
144
145      script = ''
146        export DANEEL_XMPP_JID="${cfg.xmppJid}"
147        export DANEEL_XMPP_PASSWORD="$(cat ${cfg.xmppPasswordFile})"
148        export DANEEL_OWNER_JID="${cfg.ownerJid}"
149        export DANEEL_DATA_DIR="${cfg.dataDir}"
150        export DANEEL_INBOX_PATH="${cfg.inboxPath}"
151        export DANEEL_DEFAULT_MODEL="${cfg.defaultModel}"
152        ${lib.optionalString cfg.debug ''export DANEEL_DEBUG="true"''}
153
154        ${lib.optionalString (cfg.anthropicApiKeyFile != null) ''
155          export ANTHROPIC_API_KEY="$(cat ${cfg.anthropicApiKeyFile})"
156        ''}
157        ${lib.optionalString (cfg.googleApiKeyFile != null) ''
158          export GOOGLE_API_KEY="$(cat ${cfg.googleApiKeyFile})"
159        ''}
160        ${lib.optionalString (cfg.openaiApiKeyFile != null) ''
161          export OPENAI_API_KEY="$(cat ${cfg.openaiApiKeyFile})"
162        ''}
163        ${lib.optionalString (cfg.ollamaBaseUrl != null) ''
164          export OLLAMA_BASE_URL="${cfg.ollamaBaseUrl}"
165        ''}
166
167        exec ${cfg.package}/bin/daneel
168      '';
169    };
170  };
171}