This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

1{ 2 self, 3 config, 4 lib, 5 pkgs, 6 ... 7}: 8let 9 # Pinned Piper voice for the bot's spoken replies. Single-speaker on purpose: 10 # the radio's libritts-high (configs/services/radio/tts.nix) is multi-speaker 11 # and needs an -s speaker id per call. 12 piperVoice = pkgs.runCommand "piper-voice-en_US-amy-medium" { } '' 13 mkdir -p $out 14 ln -s ${ 15 pkgs.fetchurl { 16 url = "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/amy/medium/en_US-amy-medium.onnx?download=true"; 17 hash = "sha256-s6bke1e4x/vmoM4lGBYaUPWanN2KUINcAssCvdYgbBg="; 18 } 19 } $out/model.onnx 20 ln -s ${ 21 pkgs.fetchurl { 22 url = "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/amy/medium/en_US-amy-medium.onnx.json?download=true"; 23 hash = "sha256-laI+tNQpCdON9zu5rH9F9Zfb/N4tG/lSb96vVGaXfXc="; 24 } 25 } $out/model.onnx.json 26 ''; 27 28 # TTS backend for the gateway, wired as a `type: command` provider below. 29 # 30 # Hermes's built-in `tts.provider = "piper"` cannot work on this deployment: 31 # it imports the `piper` Python module, which is in neither the sealed uv2nix 32 # venv nor tools/lazy_deps.py's install allowlist, and the venv is a 33 # read-only store path — so its lazy `pip install` has nowhere to land. Same 34 # for the upstream default "edge" (lazy edge-tts). A command provider calls 35 # nixpkgs' piper instead: no runtime pip, no runtime voice download. 36 # 37 # Contract: hermes writes the reply text to $1 and expects audio at $2. 38 hermes-tts = pkgs.writeShellApplication { 39 name = "hermes-tts"; 40 runtimeInputs = [ pkgs.piper-tts ]; 41 text = '' 42 piper --model ${piperVoice}/model.onnx --output-file "$2" < "$1" 43 ''; 44 }; 45 46 # herdr keeps its API socket under the session owner's 47 # $HOME/.config/herdr/herdr.sock (verified on this host: the running server 48 # has HOME=/home/lass and listens on /home/lass/.config/herdr/herdr.sock), 49 # and the herd runs as lass — so the hermes user cannot see any pane without 50 # crossing the user boundary. sudo's env_reset leaves HOME pointing at the 51 # *calling* user, which made the CLI look for a socket under 52 # /var/lib/hermes/.config and report "no herdr server is running", so set it 53 # here. Keeping the env setup inside a store script also means sudoers 54 # matches one fixed path instead of an argv template. 55 herdr-as-lass = pkgs.writeShellApplication { 56 name = "herdr-as-lass"; 57 text = '' 58 export HOME=${config.users.users.mainUser.home} 59 exec ${self.inputs.llm-agents.packages.${pkgs.system}.herdr}/bin/herdr "$@" 60 ''; 61 }; 62 63 # What the agent actually calls: `herdr agent list`, `herdr agent read <id>`, 64 # `herdr agent prompt <id> '...'`. Named plainly because the gateway's PATH 65 # is hermes + bash + coreutils + git + extraPackages — /run/current-system/sw 66 # is absent, so there is no real `herdr` to shadow. 67 herdr = pkgs.writeShellApplication { 68 name = "herdr"; 69 text = '' 70 exec /run/wrappers/bin/sudo -u ${config.users.users.mainUser.name} \ 71 ${herdr-as-lass}/bin/herdr-as-lass "$@" 72 ''; 73 }; 74in 75{ 76 imports = [ 77 self.inputs.hermes-agent.nixosModules.default 78 ]; 79 80 # Hermes Agent (Nous Research) — autonomous agent run as a native systemd 81 # gateway, reachable over Matrix (@hermes:lassul.us). Model backend is our 82 # shared llama cluster: one vLLM instance behind an OpenAI-compatible 83 # endpoint, so the model is whatever the cluster currently serves and a swap 84 # is a one-line settings.model.default change. 85 services.hermes-agent = { 86 enable = true; 87 88 # Build the Matrix platform dependency group into the sealed venv (mautrix 89 # SDK + liboqs for optional E2EE; Linux-only). "voice" adds faster-whisper, 90 # which is what `stt.provider = "local"` below runs — that group exists 91 # precisely because local STT pulls wheel-only transitive deps 92 # (ctranslate2, onnxruntime) that uv2nix has to resolve at build time. 93 # "anthropic" is the SDK the anthropic model provider imports; the venv is 94 # sealed (lazy installs disabled), so without it every turn fails with 95 # "The 'anthropic' package is required for the Anthropic provider". 96 extraDependencyGroups = [ 97 "matrix" 98 "voice" 99 "anthropic" 100 ]; 101 102 # Put the Claude Code CLI on the agent's PATH so its bundled "claude-code" 103 # skill can delegate coding via `terminal(command="claude -p '...'")`. 104 # extraPackages lands in both the systemd service PATH and the hermes 105 # user's profile. Auth is separate (run `claude` once as the hermes user). 106 extraPackages = [ 107 self.legacyPackages.${pkgs.system}.llm.claude-code 108 # Voice both ways: the Matrix adapter shells out to ffmpeg to transcode 109 # piper's WAV to Ogg/Opus (native MSC3245 voice bubbles in Element) and 110 # to ffprobe for the duration + waveform metadata those bubbles carry. 111 pkgs.ffmpeg 112 hermes-tts 113 # Drive the herd from a voice note: "read me what the agent on herdr is 114 # stuck on" -> herdr agent list / read / prompt, as lass. 115 herdr 116 # CalDAV (calendar skill below) is plain HTTP; curl is not otherwise on 117 # the gateway PATH. 118 pkgs.curl 119 ]; 120 121 settings.model = { 122 # Claude Opus 5.5 on lass's Claude subscription: the anthropic provider 123 # takes the `claude setup-token` OAuth token (sk-ant-oat…) from 124 # ANTHROPIC_TOKEN (hermes-anthropic generator below) and sends it as a 125 # Bearer token. Anthropic bills third-party apps on subscription tokens 126 # as extra usage; hermes renames some tools on the OAuth wire to avoid 127 # that classification (agent/anthropic_adapter.py), which goes against 128 # Anthropic's usage policies and risks the account. Chosen knowingly. 129 # 130 # Context (1M) and output (128k) limits come from hermes's own model 131 # tables, so nothing is pinned here. 132 provider = "anthropic"; 133 default = "claude-opus-5-5"; 134 }; 135 136 # The shared llama cluster stays configured as a named provider, so 137 # `/model Qwen3.6-27B-FP8 --provider custom:llama` switches back. Its key 138 # comes from hermes-env (key_env), never the nix store. It MUST be a 139 # *named* custom provider ("custom:<name>" + this entry), not bare 140 # "custom": bare custom resolves its credential from a host-gated list 141 # (hermes_cli/runtime_provider.py:1244) that only forwards OPENAI_API_KEY 142 # to openai.com and otherwise sends "no-key-required" (upstream #28660 / 143 # GHSA-76xc-57q6-vm5m). api_mode is pinned because auto-detection only 144 # runs as a fallback. 145 settings.custom_providers = [ 146 { 147 name = "llama"; 148 base_url = "https://inference.p0.contact/v1"; 149 key_env = "LLAMA_API_TOKEN"; 150 api_mode = "chat_completions"; 151 } 152 ]; 153 154 # Voice in. Element voice notes arrive flagged MSC3245; the Matrix adapter 155 # recognizes that flag and routes the audio to STT before the agent sees 156 # the turn, so a held-down mic button is just another prompt. 157 # 158 # "local" = faster-whisper in-process (from the "voice" dep group). An 159 # explicit provider is honored strictly: if it cannot run, transcription 160 # errors out loud instead of silently falling back to a cloud key. 161 settings.stt = { 162 provider = "local"; 163 # "" = auto-detect per clip, instead of upstream's "en" hint — commands 164 # here switch between English and German mid-conversation. 165 language = ""; 166 # Vocabulary bias (faster-whisper initial_prompt). Measured on this host: 167 # "the agent on herdr is blocked" transcribes as "the agent on Herter" 168 # without it and verbatim with it. Every name here is one the agent has 169 # to route on — a misheard host or tool name is a failed command, not a 170 # typo. 171 prompt = "Jargon: herdr, omp, nix, clan, retiolum, coaxmetal, neoprism, ignavia, lassulus."; 172 local = { 173 # small is the first size whose transcripts survive our jargon; a 4s 174 # clip takes ~3s warm on this CPU (int8), ~6s from cold weights. 175 model = "small"; 176 # Release the weights after five idle minutes; this is a laptop, and 177 # the next voice note pays the reload instead of holding the RAM. 178 unload_after_idle_seconds = 300; 179 }; 180 }; 181 182 # Voice out. `voice_compatible` is the opt-in that makes the gateway treat 183 # this provider's output as a voice message (Opus-transcoding it on the 184 # way) rather than a file attachment, so replies land as playable bubbles. 185 settings.tts = { 186 provider = "piper-local"; 187 providers.piper-local = { 188 type = "command"; 189 command = "${hermes-tts}/bin/hermes-tts {input_path} {output_path}"; 190 output_format = "wav"; 191 voice_compatible = true; 192 }; 193 }; 194 195 # Speak every gateway reply, not just ones requested with /voice tts. The 196 # toggle also takes a lease on the engine, so the first spoken reply after 197 # startup does not pay model load as silence. Turn it off per chat with 198 # /voice off. 199 settings.voice.auto_tts = true; 200 201 # In rooms Hermes requires an @mention by default; DMs always respond. 202 settings.matrix.session_scope = "room"; 203 204 # Matrix is a Tier-2 platform whose default tool-progress preview caps 205 # commands at 40 chars (so a `terminal` call shows only `curl -X POST -H 206 # "Content-Type: applic...`). "verbose" makes the gateway render the FULL 207 # command — multi-line — as a fenced code block instead of the truncated 208 # one-liner. Trade-off: verbose also prints full JSON args for every other 209 # tool call, so the channel is chattier. 210 settings.display.platforms.matrix.tool_progress = "verbose"; 211 212 # Teach the agent the herd. Without this the `herdr` wrapper is just an 213 # unadvertised binary on PATH: nothing in the model's context would make it 214 # reach for the herd when a voice note says "what is the agent stuck on". 215 # Skills are loaded from $HERMES_HOME/skills/<name>/SKILL.md. 216 hermesHomeFiles."skills/herdr/SKILL.md" = '' 217 --- 218 name: herdr 219 description: Inspect and steer the coding agents running in lass's herdr session on this machine. 220 version: 1.0.0 221 license: MIT 222 platforms: [linux] 223 metadata: 224 hermes: 225 tags: [herdr, agents, terminal, coding, inspect] 226 --- 227 228 # herdr 229 230 `herdr` on this host is a wrapper that talks to lass's running herdr 231 server (the agent multiplexer holding the coding-agent panes). Every 232 invocation prints JSON. 233 234 Use it whenever the user asks about "the agents", "the herd", "the 235 session on herdr", what something is "stuck on", or wants a prompt or 236 answer delivered to a running agent. 237 238 | Command | Use | 239 | --- | --- | 240 | `herdr agent list` | Every agent with its state: working, blocked, idle, done. Start here. | 241 | `herdr agent get <agent>` | One agent's detail (workspace, tab, pane, cwd). | 242 | `herdr agent read <agent>` | Recent terminal output. This is how you answer "what is it doing/asking". | 243 | `herdr agent prompt <agent> '<text>'` | Submit a prompt to that agent, e.g. to unblock an approval. | 244 | `herdr agent wait <agent> --state idle` | Block until it reaches a state. | 245 | `herdr workspace list` / `herdr tab list` / `herdr pane list` | Structure, when agent-level views are not enough. | 246 | `herdr --skill` | herdr's own, fuller usage reference — read it before improvising flags. | 247 248 Answering by voice: the reply is spoken, so summarize what `agent read` 249 shows in a sentence or two instead of reciting scrollback, and say the 250 agent's name and state first. Read a diff or a stack trace aloud only if 251 asked. 252 253 Blocked agents are the interesting case: report what the agent is waiting 254 for, and only run `herdr agent prompt` when the user actually decides. 255 Never close panes or kill agents on your own. 256 ''; 257 258 # The one shared calendar (configs/radicale.nix): lass's own events, the 259 # book.lassul.us requests and bookings, and anything Hermes adds. Hermes 260 # authenticates as its own radicale user, which may only touch this 261 # collection. Booking events are read back by book, so deleting, moving 262 # or confirming them in CalDAV manages the booking (tools/book). 263 hermesHomeFiles."skills/calendar/SKILL.md" = '' 264 --- 265 name: calendar 266 description: Read and manage lass's calendar (CalDAV) and approve, decline or cancel book.lassul.us booking requests. 267 version: 1.0.0 268 license: MIT 269 platforms: [linux] 270 required_environment_variables: 271 - name: CALDAV_PASSWORD 272 prompt: Radicale password of the hermes user 273 help: Provisioned by clan vars (radicale-hermes) on coaxmetal. 274 required_for: any calendar access 275 prerequisites: 276 commands: [curl] 277 metadata: 278 hermes: 279 tags: [calendar, caldav, schedule, booking, appointments] 280 requires_toolsets: [terminal] 281 --- 282 283 # calendar 284 285 lass has exactly one calendar, at `$CALDAV_CALENDAR_URL` 286 (CalDAV on radicale). It holds his own events, booking requests and 287 bookings from https://book.lassul.us, and events you add. The booking 288 page treats every event in it as busy time. 289 290 Use it when the user asks what is on his schedule, whether he is free, 291 to add, move or remove an appointment, or about booking requests. 292 293 Authenticate every request with 294 `curl -sS -u "$CALDAV_USER:$CALDAV_PASSWORD"`. Never print, echo or 295 repeat the password. Times are Europe/Berlin unless the user says 296 otherwise; write DTSTART/DTEND with `;TZID=Europe/Berlin` or in UTC (`Z`). 297 298 ## Reading 299 300 Events overlapping a range (UTC, `YYYYMMDDTHHMMSSZ`): 301 302 ```sh 303 curl -sS -u "$CALDAV_USER:$CALDAV_PASSWORD" -X REPORT -H 'Depth: 1' \ 304 -H 'Content-Type: application/xml; charset=utf-8' "$CALDAV_CALENDAR_URL" --data ' 305 <C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"> 306 <D:prop><D:getetag/><C:calendar-data/></D:prop> 307 <C:filter><C:comp-filter name="VCALENDAR"><C:comp-filter name="VEVENT"> 308 <C:time-range start="20261010T000000Z" end="20261017T000000Z"/> 309 </C:comp-filter></C:comp-filter></C:filter> 310 </C:calendar-query>' 311 ``` 312 313 Each `<D:response>` has the event's `href` (its URL path), `getetag` and 314 the iCalendar text. Recurring events (RRULE) come back once with their 315 rule; expand them yourself when answering "what is on Tuesday". 316 317 ## Adding, changing, removing your own events 318 319 - Add: pick a UID (`uuidgen`-style, suffix `@hermes`), then 320 `PUT "$CALDAV_CALENDAR_URL<uid>.ics"` with 321 `-H 'Content-Type: text/calendar; charset=utf-8' -H 'If-None-Match: *'` 322 and a body of one `VCALENDAR` (VERSION:2.0, PRODID) holding one 323 `VEVENT` with UID, DTSTAMP, DTSTART, DTEND (or DURATION) and SUMMARY. 324 Add a `VALARM` (ACTION:DISPLAY, TRIGGER:-PT15M) when the user wants a 325 reminder. 326 - Change: GET the event, edit it, PUT it back to the same href with 327 `-H 'If-Match: <etag>'` so you never overwrite a concurrent edit. 328 - Remove: `DELETE` the href with `-H 'If-Match: <etag>'`. 329 - Success is HTTP 201/204. 412 means it changed meanwhile: re-read and 330 retry. Confirm by reading the range again. 331 332 Before adding, check the range for overlaps and tell the user about 333 any. Do not change or delete events the user did not ask about. 334 335 ## Booking requests and bookings (book.lassul.us) 336 337 Events whose UID ends in `@book.lassul.us` come from the booking page. 338 It reads them back every minute (for bookings that have not started 339 yet) and mails the guest about what changed, so editing them is how 340 bookings are managed: 341 342 - Pending request: `STATUS:TENTATIVE`, SUMMARY starts with `? `. 343 - Booked: `STATUS:CONFIRMED`. 344 - The DESCRIPTION holds the guest (`Name <email>`), their note, and as 345 its last line the admin link `https://book.lassul.us/a/<token>`. 346 347 | Calendar edit | Effect, guest mail | 348 | --- | --- | 349 | DELETE a booked event (or set `STATUS:CANCELLED`) | cancelled, guest gets a cancellation | 350 | DELETE a pending request | declined, guest gets a decline | 351 | Change DTSTART/DTEND | rescheduled; a booked guest gets the updated invitation | 352 | Set a pending request to `STATUS:CONFIRMED` | approved, guest gets the invitation | 353 354 Use If-Match as for your own events. The booking page rewrites SUMMARY 355 and DESCRIPTION itself; edits to them are not kept. Act on a booking 356 only when the user decided to. 357 358 To send the guest a personal message with the decision, use the admin 359 link instead (an optional `message` is mailed along): 360 361 ```sh 362 curl -sS -o /dev/null -w '%{http_code}\n' -X POST \ 363 --data-urlencode 'message=Looking forward to it' \ 364 'https://book.lassul.us/a/<token>/approve' # or /decline, /cancel 365 ``` 366 367 A 303 means it worked. `GET` on the admin link shows the request's 368 current state. Treat admin links as secrets: never paste one into a 369 chat unless the user asks for it. 370 ''; 371 372 # Non-secret connection + SECURITY gate. Invites are auto-accepted and that 373 # cannot be disabled, so the agent may join any room it's invited to. Joining 374 # is harmless on its own — what matters is who can *trigger* the agent, which 375 # on this host has terminal / filesystem / web tools (chat-driven RCE). 376 # MATRIX_ALLOWED_USERS is the universal trigger gate (applies even in DMs); 377 # only these MXIDs can make the agent act, in any room it's joined. 378 environment = { 379 MATRIX_HOMESERVER = "https://matrix.lassul.us"; 380 MATRIX_USER_ID = "@hermes:lassul.us"; 381 MATRIX_ALLOWED_USERS = "@lassulus:lassul.us"; 382 383 # The shared calendar (calendar skill above); CALDAV_PASSWORD comes from 384 # hermes-env. 385 CALDAV_USER = "hermes"; 386 CALDAV_CALENDAR_URL = "https://cal.lassul.us/lass/calendar/"; 387 388 # Element makes DMs end-to-end encrypted by default, so the bot must do 389 # E2EE or it can't read messages. The `matrix` dep group already bundles 390 # mautrix[encryption] (python-olm); "optional" initializes E2EE when those 391 # deps are present (they are) and keeps the crypto store under 392 # /var/lib/hermes/.hermes/platforms/matrix/store/. 393 MATRIX_E2EE_MODE = "optional"; 394 # Optional defense-in-depth (confine to specific rooms; DMs are exempt): 395 # MATRIX_ALLOWED_ROOMS = "!yourRoomId:lassul.us"; 396 397 # The OpenAI-compatible API server: the assistant-button lane, reached 398 # both directly from massulus.r and through the public 399 # hermes.lassul.us proxy on neoprism. API_SERVER_KEY comes from clan 400 # vars via environmentFiles; API_SERVER_HOST is written at start by the 401 # ExecStartPre below (the retiolum address). Unlike Matrix there is no 402 # per-user allowlist here — the bearer token is the whole gate. 403 API_SERVER_ENABLED = "true"; 404 API_SERVER_PORT = "8642"; 405 }; 406 407 # Secrets (MATRIX_ACCESS_TOKEN + LLAMA_API_TOKEN) come from clan vars below, 408 # merged into $HERMES_HOME/.env at activation. 409 environmentFiles = [ 410 config.clan.core.vars.generators.hermes-env.files."hermes.env".path 411 ]; 412 }; 413 414 # The herd/hermes user boundary. herdr keeps its API socket in the session 415 # owner's runtime dir, so reaching lass's panes means running as lass. 416 # 417 # Be clear about what this grants: `herdr pane run` / `agent prompt` execute 418 # commands inside lass's panes, so this is lass-level code execution for 419 # anything that can drive the agent — it does not narrow the existing threat 420 # model (the gateway already has terminal tools as hermes), but it widens the 421 # blast radius from the hermes user to lass. The gate stays 422 # MATRIX_ALLOWED_USERS above. Restricting the command to one store path only 423 # buys auditability, not containment. 424 security.sudo.extraRules = [ 425 { 426 users = [ config.services.hermes-agent.user ]; 427 runAs = config.users.users.mainUser.name; 428 commands = [ 429 { 430 command = "${herdr-as-lass}/bin/herdr-as-lass"; 431 options = [ 432 "NOPASSWD" 433 "SETENV" 434 ]; 435 } 436 ]; 437 } 438 ]; 439 440 # sudo is setuid, and the upstream unit sets NoNewPrivileges=yes, which makes 441 # every setuid exec fail — so the wrapper above returned "sudo: The "no new 442 # privileges" flag is set, which prevents sudo from running as root" and the 443 # agent reported the herd as unreachable. Turning it off for this unit is the 444 # price of the bridge; it only re-enables what the sudoers rule already 445 # allows (one store path, runas lass), since sudo itself is the only setuid 446 # binary the agent is pointed at. 447 # The module hardens the unit with NoNewPrivileges = true, hence mkForce. 448 systemd.services.hermes-agent.serviceConfig.NoNewPrivileges = lib.mkForce false; 449 450 # Boot-race guard. matrix.lassul.us is IPv6-only; on this desktop 451 # network-online.target can fire before v6 DNS is ready, so the gateway's 452 # first Matrix connect fails ("Name or service not known") and it does not 453 # retry — leaving the bot silent until a manual restart. Block startup until 454 # the homeserver resolves (best-effort, ~60s cap) so reboots come up 455 # connected. 456 # 457 # The second entry resolves the API server's bind address (see the 458 # API_SERVER_* environment above). Retiolum, because both consumers live 459 # there: massulus.r talks to it directly, and neoprism reverse-proxies it 460 # to hermes.lassul.us. This host joins untrusted networks, so binding the 461 # mesh address rather than "::" keeps the listener off café wifi even 462 # before the packet filter sees it; the public door is nginx on neoprism, 463 # which we control. 464 # 465 # It is resolved at start rather than hardcoded because the address derives 466 # from the retiolum key in clan vars, so a regenerated key would otherwise 467 # leave the unit bound to a stale literal. It waits because the interface 468 # gets its address after network-online, and aiohttp cannot bind an address 469 # that does not exist yet. "+" runs it as root: /run is not writable by 470 # hermes. 471 systemd.services.hermes-agent.serviceConfig.ExecStartPre = [ 472 "${pkgs.writeShellScript "hermes-wait-matrix-dns" '' 473 i=0 474 while [ "$i" -lt 30 ]; do 475 ${pkgs.getent}/bin/getent ahosts matrix.lassul.us >/dev/null 2>&1 && exit 0 476 ${pkgs.coreutils}/bin/sleep 2 477 i=$((i + 1)) 478 done 479 echo "matrix.lassul.us did not resolve within 60s; starting anyway" >&2 480 exit 0 481 ''}" 482 "+${pkgs.writeShellScript "hermes-api-bind-retiolum" '' 483 set -eu 484 i=0 485 while [ "$i" -lt 30 ]; do 486 addr=$(${pkgs.iproute2}/bin/ip -6 -o addr show dev retiolum scope global \ 487 | ${pkgs.gawk}/bin/awk '{print $4}' | ${pkgs.coreutils}/bin/cut -d/ -f1 \ 488 | ${pkgs.coreutils}/bin/head -n1) 489 if [ -n "$addr" ]; then 490 ${pkgs.coreutils}/bin/mkdir -p /run/hermes-api 491 echo "API_SERVER_HOST=$addr" > /run/hermes-api/host.env 492 ${pkgs.coreutils}/bin/chmod 0444 /run/hermes-api/host.env 493 exit 0 494 fi 495 ${pkgs.coreutils}/bin/sleep 2 496 i=$((i + 1)) 497 done 498 # No retiolum address: leave the file absent so the API server falls 499 # back to its 127.0.0.1 default rather than silently listening on every 500 # interface. The Matrix lane keeps working either way. 501 ${pkgs.coreutils}/bin/rm -f /run/hermes-api/host.env 502 echo "retiolum has no global address after 60s; API server stays on localhost" >&2 503 exit 0 504 ''}" 505 ]; 506 507 # The packet filter drops INPUT by default here (configs/default.nix sets 508 # filter.INPUT.policy = "DROP"), so the API server's port has to be opened 509 # for the interface it binds. Without this neoprism's proxy_pass times out 510 # and hermes.lassul.us answers 504, while a local curl to the same 511 # address:port returns 200 — traffic to the host's own address goes through 512 # lo, which is accepted. 513 networking.firewall.interfaces.retiolum.allowedTCPPorts = [ 8642 ]; 514 515 # "-" because the file is absent until the ExecStartPre above finds an 516 # address, and a missing bind override must not fail the unit. 517 systemd.services.hermes-agent.serviceConfig.EnvironmentFile = [ 518 "-/run/hermes-api/host.env" 519 ]; 520 521 # Matrix bot access token for @hermes:lassul.us (minted against the neoprism 522 # Synapse). persist=true so the prompt value is stored once. 523 clan.core.vars.generators.hermes-matrix.prompts.matrix-access-token = { 524 description = "Matrix access token for @hermes:lassul.us"; 525 type = "hidden"; 526 persist = true; 527 }; 528 529 # API token for the shared llama cluster, surfaced to hermes as 530 # LLAMA_API_TOKEN (the custom_providers key_env above). 531 # persist=true so the prompt value is stored once. 532 clan.core.vars.generators.hermes-llama.prompts.llama-api-token = { 533 description = "API token for the shared llama cluster (inference.p0.contact)"; 534 type = "hidden"; 535 persist = true; 536 }; 537 538 # Bearer token for the API server. Generated, not prompted, and kept in a 539 # separate file as well so it can be read back out for the phone: 540 # clan vars get coaxmetal hermes-api/api_key 541 # (same shape as the covibe api_key generator). 542 clan.core.vars.generators.hermes-api = { 543 files."api_key" = { }; 544 runtimeInputs = [ 545 pkgs.coreutils 546 pkgs.openssl 547 ]; 548 script = '' 549 openssl rand -hex 32 | tr -d '\n' > "$out/api_key" 550 ''; 551 }; 552 553 # Hermes's radicale login (configs/radicale.nix). Shared with neoprism, where 554 # radicale runs; a shared generator has to be declared identically on every 555 # machine that uses it, hence the common definition. 556 clan.core.vars.generators.radicale-hermes = 557 import ../../configs/radicale-user-generator.nix pkgs "hermes" 558 // { 559 share = true; 560 }; 561 562 # Claude subscription OAuth token for the anthropic provider: run 563 # `claude setup-token` (long-lived, sk-ant-oat…) and paste it at the 564 # prompt. persist=true so it is asked once. 565 clan.core.vars.generators.hermes-anthropic.prompts.oauth-token = { 566 description = "Claude subscription OAuth token from `claude setup-token` (sk-ant-oat…)"; 567 type = "hidden"; 568 persist = true; 569 }; 570 571 # Assemble the .env Hermes reads (mirrors the opencrow-env pattern). 572 clan.core.vars.generators.hermes-env = { 573 dependencies = [ 574 "hermes-matrix" 575 "hermes-llama" 576 "hermes-api" 577 "radicale-hermes" 578 "hermes-anthropic" 579 ]; 580 files."hermes.env" = { }; 581 runtimeInputs = [ pkgs.coreutils ]; 582 script = '' 583 cat > "$out/hermes.env" <<EOF 584 MATRIX_ACCESS_TOKEN=$(cat "$in"/hermes-matrix/matrix-access-token) 585 LLAMA_API_TOKEN=$(cat "$in"/hermes-llama/llama-api-token) 586 API_SERVER_KEY=$(cat "$in"/hermes-api/api_key) 587 CALDAV_PASSWORD=$(cat "$in"/radicale-hermes/password) 588 ANTHROPIC_TOKEN=$(cat "$in"/hermes-anthropic/oauth-token) 589 EOF 590 ''; 591 }; 592}