This repository has no description
0

Configure Feed

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

coaxmetal: add Hermes Agent (Matrix gateway, MiniMax M2.5 via Nous Portal)

Run Nous Research's Hermes Agent as a native systemd gateway on coaxmetal,
reachable as @hermes:lassul.us over Matrix (E2EE). Wires upstream's
nixosModules.default via a new flake input.

Backend is Nous Portal (one key, 266 models) driving an agentic model
(MiniMax M2.5) through the generic OpenAI-compatible provider — hermes's
built-in nous provider is OAuth-only against a dead endpoint, and Nous does
not recommend the Hermes-4 chat models for the agent. Secrets (Matrix access
token, Nous API key) come from clan vars; a startup DNS-wait guards the
IPv6-only homeserver against a boot race.

Assisted-by: Claude Code (claude-opus-4-8[1m])

+349 -7
+212 -7
flake.lock
··· 212 212 "flake-parts_2": { 213 213 "inputs": { 214 214 "nixpkgs-lib": [ 215 + "hermes-agent", 216 + "nixpkgs" 217 + ] 218 + }, 219 + "locked": { 220 + "lastModified": 1772408722, 221 + "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", 222 + "owner": "hercules-ci", 223 + "repo": "flake-parts", 224 + "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", 225 + "type": "github" 226 + }, 227 + "original": { 228 + "owner": "hercules-ci", 229 + "repo": "flake-parts", 230 + "type": "github" 231 + } 232 + }, 233 + "flake-parts_3": { 234 + "inputs": { 235 + "nixpkgs-lib": [ 215 236 "le_menu", 216 237 "nixpkgs" 217 238 ] ··· 230 251 "type": "github" 231 252 } 232 253 }, 233 - "flake-parts_3": { 254 + "flake-parts_4": { 234 255 "inputs": { 235 256 "nixpkgs-lib": [ 236 257 "llm-agents", ··· 251 272 "type": "github" 252 273 } 253 274 }, 254 - "flake-parts_4": { 275 + "flake-parts_5": { 255 276 "inputs": { 256 277 "nixpkgs-lib": [ 257 278 "nether", ··· 272 293 "type": "github" 273 294 } 274 295 }, 275 - "flake-parts_5": { 296 + "flake-parts_6": { 276 297 "inputs": { 277 298 "nixpkgs-lib": [ 278 299 "nixvim", ··· 374 395 "type": "github" 375 396 } 376 397 }, 377 - "le_menu": { 398 + "hermes-agent": { 378 399 "inputs": { 379 400 "flake-parts": "flake-parts_2", 401 + "nixpkgs": [ 402 + "nixpkgs" 403 + ], 404 + "npm-lockfile-fix": "npm-lockfile-fix", 405 + "pyproject-build-systems": "pyproject-build-systems", 406 + "pyproject-nix": "pyproject-nix_2", 407 + "uv2nix": "uv2nix_2" 408 + }, 409 + "locked": { 410 + "lastModified": 1782384063, 411 + "narHash": "sha256-unMsFiF9qtgg5yJJBYvElCYtNsROjHoCRtR56Uqarrw=", 412 + "owner": "nousresearch", 413 + "repo": "hermes-agent", 414 + "rev": "d6269da7fdfe3a80eee60a4675b9e6ef55a71559", 415 + "type": "github" 416 + }, 417 + "original": { 418 + "owner": "nousresearch", 419 + "repo": "hermes-agent", 420 + "type": "github" 421 + } 422 + }, 423 + "le_menu": { 424 + "inputs": { 425 + "flake-parts": "flake-parts_3", 380 426 "nixpkgs": [ 381 427 "nixpkgs" 382 428 ], ··· 400 446 "inputs": { 401 447 "blueprint": "blueprint", 402 448 "bun2nix": "bun2nix", 403 - "flake-parts": "flake-parts_3", 449 + "flake-parts": "flake-parts_4", 404 450 "nixpkgs": [ 405 451 "nixpkgs" 406 452 ], ··· 427 473 "clan-core" 428 474 ], 429 475 "data-mesher": [], 430 - "flake-parts": "flake-parts_4", 476 + "flake-parts": "flake-parts_5", 431 477 "nixpkgs": [ 432 478 "nixpkgs" 433 479 ], ··· 650 696 }, 651 697 "nixvim": { 652 698 "inputs": { 653 - "flake-parts": "flake-parts_5", 699 + "flake-parts": "flake-parts_6", 654 700 "nixpkgs": [ 655 701 "nixpkgs" 656 702 ], ··· 667 713 "original": { 668 714 "owner": "nix-community", 669 715 "repo": "nixvim", 716 + "type": "github" 717 + } 718 + }, 719 + "npm-lockfile-fix": { 720 + "inputs": { 721 + "nixpkgs": [ 722 + "hermes-agent", 723 + "nixpkgs" 724 + ] 725 + }, 726 + "locked": { 727 + "lastModified": 1775903712, 728 + "narHash": "sha256-2GV79U6iVH4gKAPWYrxUReB0S41ty/Y3dBLquU8AlaA=", 729 + "owner": "jeslie0", 730 + "repo": "npm-lockfile-fix", 731 + "rev": "c6093acb0c0548e0f9b8b3d82918823721930fe8", 732 + "type": "github" 733 + }, 734 + "original": { 735 + "owner": "jeslie0", 736 + "repo": "npm-lockfile-fix", 670 737 "type": "github" 671 738 } 672 739 }, ··· 688 755 "original": { 689 756 "owner": "pinpox", 690 757 "repo": "opencrow", 758 + "type": "github" 759 + } 760 + }, 761 + "pyproject-build-systems": { 762 + "inputs": { 763 + "nixpkgs": [ 764 + "hermes-agent", 765 + "nixpkgs" 766 + ], 767 + "pyproject-nix": "pyproject-nix", 768 + "uv2nix": "uv2nix" 769 + }, 770 + "locked": { 771 + "lastModified": 1772555609, 772 + "narHash": "sha256-3BA3HnUvJSbHJAlJj6XSy0Jmu7RyP2gyB/0fL7XuEDo=", 773 + "owner": "pyproject-nix", 774 + "repo": "build-system-pkgs", 775 + "rev": "c37f66a953535c394244888598947679af231863", 776 + "type": "github" 777 + }, 778 + "original": { 779 + "owner": "pyproject-nix", 780 + "repo": "build-system-pkgs", 781 + "type": "github" 782 + } 783 + }, 784 + "pyproject-nix": { 785 + "inputs": { 786 + "nixpkgs": [ 787 + "hermes-agent", 788 + "pyproject-build-systems", 789 + "nixpkgs" 790 + ] 791 + }, 792 + "locked": { 793 + "lastModified": 1769936401, 794 + "narHash": "sha256-kwCOegKLZJM9v/e/7cqwg1p/YjjTAukKPqmxKnAZRgA=", 795 + "owner": "nix-community", 796 + "repo": "pyproject.nix", 797 + "rev": "b0d513eeeebed6d45b4f2e874f9afba2021f7812", 798 + "type": "github" 799 + }, 800 + "original": { 801 + "owner": "nix-community", 802 + "repo": "pyproject.nix", 803 + "type": "github" 804 + } 805 + }, 806 + "pyproject-nix_2": { 807 + "inputs": { 808 + "nixpkgs": [ 809 + "hermes-agent", 810 + "nixpkgs" 811 + ] 812 + }, 813 + "locked": { 814 + "lastModified": 1772865871, 815 + "narHash": "sha256-/ZTSg97aouL0SlPHaokA4r3iuH9QzHVuWPACD2CUCFY=", 816 + "owner": "pyproject-nix", 817 + "repo": "pyproject.nix", 818 + "rev": "e537db02e72d553cea470976b9733581bcf5b3ed", 819 + "type": "github" 820 + }, 821 + "original": { 822 + "owner": "pyproject-nix", 823 + "repo": "pyproject.nix", 824 + "type": "github" 825 + } 826 + }, 827 + "pyproject-nix_3": { 828 + "inputs": { 829 + "nixpkgs": [ 830 + "hermes-agent", 831 + "uv2nix", 832 + "nixpkgs" 833 + ] 834 + }, 835 + "locked": { 836 + "lastModified": 1771518446, 837 + "narHash": "sha256-nFJSfD89vWTu92KyuJWDoTQJuoDuddkJV3TlOl1cOic=", 838 + "owner": "pyproject-nix", 839 + "repo": "pyproject.nix", 840 + "rev": "eb204c6b3335698dec6c7fc1da0ebc3c6df05937", 841 + "type": "github" 842 + }, 843 + "original": { 844 + "owner": "pyproject-nix", 845 + "repo": "pyproject.nix", 691 846 "type": "github" 692 847 } 693 848 }, ··· 717 872 "clan-core": "clan-core", 718 873 "disko": "disko", 719 874 "extra-container": "extra-container", 875 + "hermes-agent": "hermes-agent", 720 876 "le_menu": "le_menu", 721 877 "llm-agents": "llm-agents", 722 878 "nether": "nether", ··· 1006 1162 "original": { 1007 1163 "owner": "numtide", 1008 1164 "repo": "treefmt-nix", 1165 + "type": "github" 1166 + } 1167 + }, 1168 + "uv2nix": { 1169 + "inputs": { 1170 + "nixpkgs": [ 1171 + "hermes-agent", 1172 + "pyproject-build-systems", 1173 + "nixpkgs" 1174 + ], 1175 + "pyproject-nix": [ 1176 + "hermes-agent", 1177 + "pyproject-build-systems", 1178 + "pyproject-nix" 1179 + ] 1180 + }, 1181 + "locked": { 1182 + "lastModified": 1770770348, 1183 + "narHash": "sha256-A2GzkmzdYvdgmMEu5yxW+xhossP+txrYb7RuzRaqhlg=", 1184 + "owner": "pyproject-nix", 1185 + "repo": "uv2nix", 1186 + "rev": "5d1b2cb4fe3158043fbafbbe2e46238abbc954b0", 1187 + "type": "github" 1188 + }, 1189 + "original": { 1190 + "owner": "pyproject-nix", 1191 + "repo": "uv2nix", 1192 + "type": "github" 1193 + } 1194 + }, 1195 + "uv2nix_2": { 1196 + "inputs": { 1197 + "nixpkgs": [ 1198 + "hermes-agent", 1199 + "nixpkgs" 1200 + ], 1201 + "pyproject-nix": "pyproject-nix_3" 1202 + }, 1203 + "locked": { 1204 + "lastModified": 1773039484, 1205 + "narHash": "sha256-+boo33KYkJDw9KItpeEXXv8+65f7hHv/earxpcyzQ0I=", 1206 + "owner": "pyproject-nix", 1207 + "repo": "uv2nix", 1208 + "rev": "b68be7cfeacbed9a3fa38a2b5adc0cfb81d9bb1f", 1209 + "type": "github" 1210 + }, 1211 + "original": { 1212 + "owner": "pyproject-nix", 1213 + "repo": "uv2nix", 1009 1214 "type": "github" 1010 1215 } 1011 1216 },
+3
flake.nix
··· 63 63 llm-agents.url = "github:numtide/llm-agents.nix"; 64 64 llm-agents.inputs.nixpkgs.follows = "nixpkgs"; 65 65 66 + hermes-agent.url = "github:nousresearch/hermes-agent"; 67 + hermes-agent.inputs.nixpkgs.follows = "nixpkgs"; 68 + 66 69 rogue-talk.url = "github:rogue-talk/rogue-talk"; 67 70 rogue-talk.inputs.nixpkgs.follows = "nixpkgs"; 68 71
+1
machines/coaxmetal/config.nix
··· 17 17 ../../2configs/wine.nix 18 18 ../../2configs/yellow-mounts/samba.nix 19 19 ../../2configs/review.nix 20 + ./hermes.nix 20 21 ]; 21 22 22 23 system.stateVersion = "24.05";
+133
machines/coaxmetal/hermes.nix
··· 1 + { 2 + self, 3 + config, 4 + pkgs, 5 + ... 6 + }: 7 + { 8 + imports = [ 9 + self.inputs.hermes-agent.nixosModules.default 10 + ]; 11 + 12 + # Hermes Agent (Nous Research) — autonomous agent run as a native systemd 13 + # gateway, reachable over Matrix (@hermes:lassul.us). Model backend is Nous 14 + # Portal (one key, 266 models) driving MiniMax M2.5 — an agentic model, per 15 + # Nous's own guidance that the Hermes-4 chat models are NOT for the agent. 16 + # Any Portal model is a one-line settings.model.default swap. 17 + services.hermes-agent = { 18 + enable = true; 19 + 20 + # Build the Matrix platform dependency group into the sealed venv (mautrix 21 + # SDK + liboqs for optional E2EE; Linux-only). 22 + extraDependencyGroups = [ "matrix" ]; 23 + 24 + settings.model = { 25 + # Nous does NOT recommend the Hermes-4 chat models for Hermes Agent — it 26 + # wants a dedicated *agentic* model. Community/Nous consensus agentic pick 27 + # is the MiniMax M2 family (Nous co-optimizes it for Hermes Agent); M2.5 28 + # is the cheapest tool-calling option ($0.12/$0.48 per 1M). All 266 Nous 29 + # Portal models share this key, so this is just a model-id swap. 30 + # Alternatives: minimax/minimax-m2.7, z-ai/glm-4.6, deepseek/deepseek-v3.2. 31 + default = "minimax/minimax-m2.5"; 32 + # hermes's built-in "nous" provider is OAuth-only and hardcodes the dead 33 + # host inference.nousresearch.com (NXDOMAIN) — true on both 0.17.0 and 34 + # main; "nous-api" exists only in docs, not code. So we drive Nous Portal 35 + # through the generic OpenAI-compatible "custom" provider pointed at the 36 + # live endpoint, authenticating with the Nous key as OPENAI_API_KEY. 37 + provider = "custom"; 38 + base_url = "https://inference-api.nousresearch.com/v1"; 39 + # The custom provider can't auto-detect limits, so it left context 40 + # uncompressed and defaulted max_tokens to 65536 — once history passed 41 + # ~65k tokens, input + max_tokens exceeded the 131072 window and the 42 + # endpoint 400'd ("check the model name and other parameters"). Pin both: 43 + # context_length so hermes compresses before the window fills, and a sane 44 + # output cap (covers thinking + reply) so input + max_tokens stays under. 45 + context_length = 204800; 46 + max_tokens = 16384; 47 + 48 + # Frontier-on-tap alternative (uncomment to default to Claude instead): 49 + # default = "anthropic/claude-sonnet-4-6"; 50 + }; 51 + 52 + # In rooms Hermes requires an @mention by default; DMs always respond. 53 + settings.matrix.session_scope = "room"; 54 + 55 + # Non-secret connection + SECURITY gate. Invites are auto-accepted and that 56 + # cannot be disabled, so the agent may join any room it's invited to. Joining 57 + # is harmless on its own — what matters is who can *trigger* the agent, which 58 + # on this host has terminal / filesystem / web tools (chat-driven RCE). 59 + # MATRIX_ALLOWED_USERS is the universal trigger gate (applies even in DMs); 60 + # only these MXIDs can make the agent act, in any room it's joined. 61 + environment = { 62 + MATRIX_HOMESERVER = "https://matrix.lassul.us"; 63 + MATRIX_USER_ID = "@hermes:lassul.us"; 64 + MATRIX_ALLOWED_USERS = "@lassulus:lassul.us"; 65 + # Element makes DMs end-to-end encrypted by default, so the bot must do 66 + # E2EE or it can't read messages. The `matrix` dep group already bundles 67 + # mautrix[encryption] (python-olm); "optional" initializes E2EE when those 68 + # deps are present (they are) and keeps the crypto store under 69 + # /var/lib/hermes/.hermes/platforms/matrix/store/. 70 + MATRIX_E2EE_MODE = "optional"; 71 + # Optional defense-in-depth (confine to specific rooms; DMs are exempt): 72 + # MATRIX_ALLOWED_ROOMS = "!yourRoomId:lassul.us"; 73 + }; 74 + 75 + # Secrets (MATRIX_ACCESS_TOKEN + NOUS_API_KEY) come from clan vars below, 76 + # merged into $HERMES_HOME/.env at activation. 77 + environmentFiles = [ 78 + config.clan.core.vars.generators.hermes-env.files."hermes.env".path 79 + ]; 80 + }; 81 + 82 + # Boot-race guard. matrix.lassul.us is IPv6-only; on this desktop 83 + # network-online.target can fire before v6 DNS is ready, so the gateway's 84 + # first Matrix connect fails ("Name or service not known") and it does not 85 + # retry — leaving the bot silent until a manual restart. Block startup until 86 + # the homeserver resolves (best-effort, ~60s cap) so reboots come up 87 + # connected. 88 + systemd.services.hermes-agent.serviceConfig.ExecStartPre = [ 89 + "${pkgs.writeShellScript "hermes-wait-matrix-dns" '' 90 + i=0 91 + while [ "$i" -lt 30 ]; do 92 + ${pkgs.getent}/bin/getent ahosts matrix.lassul.us >/dev/null 2>&1 && exit 0 93 + ${pkgs.coreutils}/bin/sleep 2 94 + i=$((i + 1)) 95 + done 96 + echo "matrix.lassul.us did not resolve within 60s; starting anyway" >&2 97 + exit 0 98 + ''}" 99 + ]; 100 + 101 + # Matrix bot access token for @hermes:lassul.us (minted against the neoprism 102 + # Synapse). persist=true so the prompt value is stored once. 103 + clan.core.vars.generators.hermes-matrix.prompts.matrix-access-token = { 104 + description = "Matrix access token for @hermes:lassul.us"; 105 + type = "hidden"; 106 + persist = true; 107 + }; 108 + 109 + # Nous Portal API key (portal.nousresearch.com, Plus subscription). 110 + clan.core.vars.generators.hermes-nous.prompts.nous-api-key = { 111 + description = "Nous Portal API key (NOUS_API_KEY) from portal.nousresearch.com"; 112 + type = "hidden"; 113 + persist = true; 114 + }; 115 + 116 + # Assemble the .env Hermes reads (mirrors the opencrow-env pattern). 117 + clan.core.vars.generators.hermes-env = { 118 + dependencies = [ 119 + "hermes-matrix" 120 + "hermes-nous" 121 + ]; 122 + files."hermes.env" = { }; 123 + runtimeInputs = [ pkgs.coreutils ]; 124 + script = '' 125 + key=$(cat "$in"/hermes-nous/nous-api-key) 126 + cat > "$out/hermes.env" <<EOF 127 + MATRIX_ACCESS_TOKEN=$(cat "$in"/hermes-matrix/matrix-access-token) 128 + NOUS_API_KEY=$key 129 + OPENAI_API_KEY=$key 130 + EOF 131 + ''; 132 + }; 133 + }