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}