{ pkgs, lib, config, ... }: let cfg = config.mods.server.hermes; auth2apiCfg = config.mods.server.auth2api; # auth2api exposes an OpenAI-compatible endpoint; point Hermes at it. auth2apiUrl = "http://${auth2apiCfg.host}:${toString auth2apiCfg.port}/v1"; in with lib; { options.mods.server.hermes = { enable = mkEnableOption "Hermes AI agent (NousResearch)"; model = mkOption { type = types.str; default = "claude-sonnet-4-6"; description = '' Model identifier as exposed by auth2api. Available aliases: sonnet, haiku, opus, claude-sonnet-4-6, claude-haiku-4-5, claude-opus-4-6, etc. Run: curl http://127.0.0.1:8317/v1/models ''; }; container = { enable = mkOption { type = types.bool; default = true; description = "Run Hermes inside a persistent Ubuntu container."; }; hostUsers = mkOption { type = types.listOf types.str; default = [ config.mods.user.name ]; description = '' Users that get a ~/.hermes symlink to the service stateDir and are added to the hermes group for shared file access. ''; }; extraVolumes = mkOption { type = types.listOf types.str; default = [ ]; description = "Extra volume mounts (host:container:mode) for the container."; }; }; addToSystemPackages = mkOption { type = types.bool; default = true; description = '' Put the hermes CLI on the system PATH and set HERMES_HOME so the interactive CLI shares state with the gateway service. ''; }; extraPackages = mkOption { type = types.listOf types.package; default = [ ]; description = "Extra Nix packages available to the agent at runtime."; }; stateDir = mkOption { type = types.str; default = "/var/lib/hermes"; description = "State directory (HERMES_HOME parent)."; }; }; config = mkIf cfg.enable { # Write the nix-store overlay script into the stateDir so it's available # at /data/nix-store-overlay inside the container. Run it once with: # docker exec hermes-agent sudo /data/nix-store-overlay # This mounts a writable overlay over the read-only /nix/store so the Nix # daemon can build new derivations without touching the host store. # Upper/work dirs live on /data (persistent volume) so builds survive restarts. system.activationScripts.hermes-nix-overlay-script = { text = '' install -m 0755 -o root -g root /dev/stdin \ '${cfg.stateDir}/nix-store-overlay' << 'OVERLAY_EOF' #!/bin/sh set -e UPPER=/data/nix-store-upper WORK=/data/nix-store-work if ! touch /nix/store/.rw-test 2>/dev/null; then mkdir -p "$UPPER" "$WORK" mount -t overlay overlay \ -o lowerdir=/nix/store,upperdir="$UPPER",workdir="$WORK" \ /nix/store echo "nix-store overlay mounted (upper=$UPPER)" else rm -f /nix/store/.rw-test echo "nix-store already writable, skipping overlay" fi OVERLAY_EOF ''; deps = [ "users" "groups" ]; }; # Hermes needs auth2api running to have a backend to call. assertions = [ { assertion = auth2apiCfg.enable; message = "mods.server.hermes requires mods.server.auth2api.enable = true"; } ]; services.hermes-agent = { enable = true; inherit (cfg) stateDir addToSystemPackages extraPackages; # auth2api runs unauthenticated on localhost (api-keys: []), but Hermes's # provider resolver requires a non-empty key env var for any provider. # AUTH2API_KEY is a dummy — auth2api ignores Bearer tokens when api-keys: []. environment.AUTH2API_KEY = "auth2api"; # ── Model ────────────────────────────────────────────────────────────── # Declare auth2api as a named custom provider in the providers: section. # Hermes resolves it via resolve_user_provider, which accepts arbitrary # base_url + key_env without needing a built-in provider slug. settings = { providers.auth2api = { name = "auth2api (Claude via OAuth)"; api = auth2apiUrl; key_env = "AUTH2API_KEY"; transport = "openai_chat"; }; model = { default = cfg.model; provider = "auth2api"; }; toolsets = [ "all" ]; max_turns = 100; terminal = { backend = "local"; timeout = 180; }; memory = { memory_enabled = true; user_profile_enabled = true; }; display = { compact = false; show_reasoning = true; }; }; # ── Container ────────────────────────────────────────────────────────── container = mkIf cfg.container.enable { enable = true; backend = "docker"; inherit (cfg.container) hostUsers extraVolumes; # SYS_ADMIN is required for the overlay mount over /nix/store. # The nix-store-overlay script (written to stateDir by tmpfiles) mounts # a writable overlay so the Nix daemon can build inside the container # without touching the host store. extraOptions = [ "--cap-add=SYS_ADMIN" ]; }; }; }; }