main
  1{
  2  config,
  3  lib,
  4  pkgs,
  5  ...
  6}:
  7let
  8  cfg = config.services.xmpp-research-bot;
  9
 10  pythonEnv = pkgs.python3.withPackages (
 11    ps: with ps; [
 12      slixmpp
 13      anthropic
 14      google-auth
 15      pyyaml
 16      google-genai
 17    ]
 18  );
 19
 20  botScript = pkgs.writeShellScript "xmpp-research-bot" ''
 21    export XMPP_JID="${cfg.jid}"
 22    export XMPP_PASSWORD="$(cat ${cfg.passwordFile})"
 23    export XMPP_OWNER_JID="${cfg.ownerJid}"
 24    export VERTEX_PROJECT_ID="${cfg.vertexProjectId}"
 25    export VERTEX_REGION="${cfg.vertexRegion}"
 26    export INBOX_PATH="${cfg.inboxPath}"
 27    ${lib.optionalString (cfg.commandsPath != null) ''
 28      export COMMANDS_PATH="${cfg.commandsPath}"
 29    ''}
 30    ${lib.optionalString (cfg.geminiApiKeyFile != null) ''
 31      export GEMINI_API_KEY="$(cat ${cfg.geminiApiKeyFile})"
 32    ''}
 33
 34    exec ${pythonEnv}/bin/python3 ${./bot.py}
 35  '';
 36in
 37{
 38  options.services.xmpp-research-bot = {
 39    enable = lib.mkEnableOption "XMPP Research Bot";
 40
 41    jid = lib.mkOption {
 42      type = lib.types.str;
 43      default = "researchbot@xmpp.sbr.pm";
 44      description = "XMPP JID (Jabber ID) for the bot";
 45    };
 46
 47    passwordFile = lib.mkOption {
 48      type = lib.types.path;
 49      description = "Path to file containing XMPP password";
 50    };
 51
 52    ownerJid = lib.mkOption {
 53      type = lib.types.str;
 54      default = "vincent@xmpp.sbr.pm";
 55      description = "XMPP JID of the bot owner (only responds to this user)";
 56    };
 57
 58    vertexProjectId = lib.mkOption {
 59      type = lib.types.str;
 60      description = "Google Cloud project ID for Vertex AI";
 61    };
 62
 63    vertexRegion = lib.mkOption {
 64      type = lib.types.str;
 65      default = "global";
 66      description = "Google Cloud region for Vertex AI";
 67    };
 68
 69    inboxPath = lib.mkOption {
 70      type = lib.types.path;
 71      default = "/home/vincent/desktop/org/inbox.org";
 72      description = "Path to inbox.org file for saving research results";
 73    };
 74
 75    user = lib.mkOption {
 76      type = lib.types.str;
 77      default = "vincent";
 78      description = "User to run the bot as";
 79    };
 80
 81    group = lib.mkOption {
 82      type = lib.types.str;
 83      default = "users";
 84      description = "Group to run the bot as";
 85    };
 86
 87    commandsPath = lib.mkOption {
 88      type = lib.types.nullOr lib.types.path;
 89      default = null;
 90      description = "Path to commands.yaml configuration file (optional)";
 91    };
 92
 93    geminiApiKeyFile = lib.mkOption {
 94      type = lib.types.nullOr lib.types.path;
 95      default = null;
 96      description = "Path to file containing Gemini API key (optional, for direct Gemini API)";
 97    };
 98  };
 99
100  config = lib.mkIf cfg.enable {
101    systemd.services.xmpp-research-bot = {
102      description = "XMPP Research Bot";
103      wantedBy = [ "multi-user.target" ];
104      after = [
105        "network-online.target"
106        "prosody.service"
107      ];
108      wants = [ "network-online.target" ];
109
110      serviceConfig = {
111        Type = "simple";
112        User = cfg.user;
113        Group = cfg.group;
114        ExecStart = "${botScript}";
115        Restart = "always";
116        RestartSec = "10s";
117
118        # Security hardening
119        PrivateTmp = true;
120        ProtectSystem = "strict";
121        ProtectHome = false; # Need access to inbox.org
122        ReadWritePaths = [ (builtins.dirOf cfg.inboxPath) ];
123        NoNewPrivileges = true;
124        ProtectKernelTunables = true;
125        ProtectKernelModules = true;
126        ProtectControlGroups = true;
127        RestrictAddressFamilies = [
128          "AF_INET"
129          "AF_INET6"
130        ];
131        RestrictNamespaces = true;
132        LockPersonality = true;
133        MemoryDenyWriteExecute = false; # Python needs this
134        RestrictRealtime = true;
135        RestrictSUIDSGID = true;
136        RemoveIPC = true;
137        PrivateMounts = true;
138      };
139    };
140  };
141}