{ config, ... }: # Hermes Agent — jupiter's own instance (terra no longer runs one; see # 713d91d). Locked down given jupiter's much bigger blast radius # (irreplaceable immich photos on an unredundant RAID0, gitea/CI tokens, the # whole media stack): # # - Own dedicated "hermes" system user (module default: user/group "hermes", # createUser = true) — NOT darman. darman is in jupiter's "docker" group # (services/containers.nix: rootful podman with dockerCompat), which is # root-equivalent (`docker run -v /:/host --privileged ...`). Handing an # LLM-driven agent that identity would mean a container escape = root on # the whole NAS. # - container.enable = true, backend = "podman": the ENTIRE gateway process # runs inside a container (reusing jupiter's existing rootful podman # instead of also standing up a second Docker daemon), not just the shell # tool. Per upstream's own SECURITY.md this is "whole-process wrapping" — # shell, file tools, MCP subprocesses, and the code-exec tool are all # confined, unlike the lighter "terminal-backend"-only isolation. # - Its own Telegram bot (own token, in secrets.nix) with an EXPLICIT # TELEGRAM_ALLOWED_USERS rather than relying solely on the adapter's # fail-closed default. Sharing terra's bot token would 409-conflict two # long-pollers on the same token. # - No container.extraVolumes into /mnt/data or the homelab repo — nothing # valuable is in reach if a command goes wrong or gets injected via # Telegram/tool output. stateDir/workingDirectory live on the array # (below) purely because coding-task state (repo clones, npm/pip caches # inside the container's writable layer) belongs off the 29G eMMC, same # reasoning as postgres/containers.storage in configuration.nix — NOT # because anything else on /mnt/data is exposed to the agent. { services.hermes-agent = { enable = true; addToSystemPackages = true; # `hermes` on darman's PATH for interactive # debugging over ssh — routes through to the # container, does not grant darman any group. # Off the eMMC: stateDir bind-mounts into the container as /data, so this # is where any future scoped repo clone (container.extraVolumes) and the # container's own writable layer (npm/pip installs during coding tasks) # actually land. RequiresMountsFor below (mirrors podman/sabnzbd/gitea-runner # in configuration.nix) keeps the service from starting — and bind-mounting # the wrong, empty eMMC path — before the nofail array is up. stateDir = "/mnt/data/AppData/hermes"; workingDirectory = "/mnt/data/AppData/hermes/workspaces"; # HERMES_TIMEZONE is the highest-priority source hermes_time.py checks # (ahead of config.yaml's `timezone` key) — the container has no host # /etc/localtime bind-mount, so it defaults to UTC otherwise. Not a # secret, so `environment` (plain .env) rather than sops. environment.HERMES_TIMEZONE = "Europe/Berlin"; container = { enable = true; backend = "podman"; # jupiter already runs podman (services/containers.nix); # default "docker" would stand up a second daemon. }; # OpenCode Go provider account (same key originally used for terra's now- # removed instance, copied into secrets/jupiter.yaml — just an API key, # not a stateful identity like the Telegram bot token). settings.model = { provider = "opencode-go"; base_url = "https://opencode.ai/zen/go/v1"; default = "gpt-5.6-luna"; api_mode = "codex_responses"; }; settings.platforms.telegram = { enabled = true; home_channel = { platform = "telegram"; chat_id = "15151223"; name = "Erik Simon"; user_id = "15151223"; }; }; environmentFiles = [ config.sops.templates."hermes-agent.env".path ]; }; systemd.services.hermes-agent.unitConfig.RequiresMountsFor = [ "/mnt/data" ]; }