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}