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 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 our 14 # shared llama cluster: one vLLM instance behind an OpenAI-compatible 15 # endpoint, so the model is whatever the cluster currently serves and a swap 16 # is a one-line settings.model.default change. 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 # Put the Claude Code CLI on the agent's PATH so its bundled "claude-code" 25 # skill can delegate coding via `terminal(command="claude -p '...'")`. 26 # extraPackages lands in both the systemd service PATH and the hermes 27 # user's profile. Auth is separate (run `claude` once as the hermes user). 28 extraPackages = [ 29 self.legacyPackages.${pkgs.system}.llm.claude-code 30 ]; 31 32 settings.model = { 33 # The cluster serves Qwen3.6-27B-FP8 (also aliased as "default" in 34 # /v1/models — both resolve to the same weights); pin the explicit id so 35 # logs and 400s name the actual model. Verified against the live endpoint 36 # that it emits native tool_calls, which Hermes Agent requires. 37 default = "Qwen3.6-27B-FP8"; 38 # hermes's built-in "nous" provider is OAuth-only and hardcodes the dead 39 # host inference.nousresearch.com (NXDOMAIN) — true on both 0.17.0 and 40 # main, so we drive the cluster's OpenAI-compatible endpoint instead. 41 # 42 # It MUST be a *named* custom provider ("custom:<name>" + a 43 # custom_providers entry), not bare "custom". Bare custom resolves its 44 # credential from a host-gated candidate list 45 # (hermes_cli/runtime_provider.py:1244) that only forwards 46 # OPENAI_API_KEY when base_url's host is openai.com — a deliberate 47 # anti-credential-leak measure (upstream #28660 / GHSA-76xc-57q6-vm5m). 48 # For any other host it silently substitutes the literal placeholder 49 # "no-key-required" and the endpoint 401s. A named entry instead reads 50 # the key from its declared key_env (runtime_provider.py:692), which is 51 # not host-gated. Named entries also ignore config.yaml's model.api_key, 52 # so a stray `hermes model` run can no longer shadow this with a stale 53 # key (that is exactly how the Nous→cluster switch broke). 54 provider = "custom:llama"; 55 base_url = "https://inference.p0.contact/v1"; 56 # The custom provider can't auto-detect limits, so unset it leaves 57 # context uncompressed and defaults max_tokens to 65536 — once history 58 # outgrows the window, input + max_tokens exceeds it and the endpoint 59 # 400s ("check the model name and other parameters"). vLLM reports 60 # max_model_len 262144 for this model: pin that as the window so hermes 61 # compresses in time, and keep a sane output cap (covers thinking + 62 # reply) so input + max_tokens always stays under it. 63 context_length = 262144; 64 max_tokens = 16384; 65 }; 66 67 # The endpoint hermes actually authenticates against. key_env names the 68 # env var carrying the token; it comes from the hermes-env generator below 69 # via environmentFiles, so the secret never enters the nix store. 70 # api_mode is pinned because auto-detection only runs as a fallback. 71 settings.custom_providers = [ 72 { 73 name = "llama"; 74 base_url = "https://inference.p0.contact/v1"; 75 key_env = "LLAMA_API_TOKEN"; 76 api_mode = "chat_completions"; 77 } 78 ]; 79 80 # In rooms Hermes requires an @mention by default; DMs always respond. 81 settings.matrix.session_scope = "room"; 82 83 # Matrix is a Tier-2 platform whose default tool-progress preview caps 84 # commands at 40 chars (so a `terminal` call shows only `curl -X POST -H 85 # "Content-Type: applic...`). "verbose" makes the gateway render the FULL 86 # command — multi-line — as a fenced code block instead of the truncated 87 # one-liner. Trade-off: verbose also prints full JSON args for every other 88 # tool call, so the channel is chattier. 89 settings.display.platforms.matrix.tool_progress = "verbose"; 90 91 # Non-secret connection + SECURITY gate. Invites are auto-accepted and that 92 # cannot be disabled, so the agent may join any room it's invited to. Joining 93 # is harmless on its own — what matters is who can *trigger* the agent, which 94 # on this host has terminal / filesystem / web tools (chat-driven RCE). 95 # MATRIX_ALLOWED_USERS is the universal trigger gate (applies even in DMs); 96 # only these MXIDs can make the agent act, in any room it's joined. 97 environment = { 98 MATRIX_HOMESERVER = "https://matrix.lassul.us"; 99 MATRIX_USER_ID = "@hermes:lassul.us"; 100 MATRIX_ALLOWED_USERS = "@lassulus:lassul.us"; 101 # Element makes DMs end-to-end encrypted by default, so the bot must do 102 # E2EE or it can't read messages. The `matrix` dep group already bundles 103 # mautrix[encryption] (python-olm); "optional" initializes E2EE when those 104 # deps are present (they are) and keeps the crypto store under 105 # /var/lib/hermes/.hermes/platforms/matrix/store/. 106 MATRIX_E2EE_MODE = "optional"; 107 # Optional defense-in-depth (confine to specific rooms; DMs are exempt): 108 # MATRIX_ALLOWED_ROOMS = "!yourRoomId:lassul.us"; 109 }; 110 111 # Secrets (MATRIX_ACCESS_TOKEN + LLAMA_API_TOKEN) come from clan vars below, 112 # merged into $HERMES_HOME/.env at activation. 113 environmentFiles = [ 114 config.clan.core.vars.generators.hermes-env.files."hermes.env".path 115 ]; 116 }; 117 118 # Boot-race guard. matrix.lassul.us is IPv6-only; on this desktop 119 # network-online.target can fire before v6 DNS is ready, so the gateway's 120 # first Matrix connect fails ("Name or service not known") and it does not 121 # retry — leaving the bot silent until a manual restart. Block startup until 122 # the homeserver resolves (best-effort, ~60s cap) so reboots come up 123 # connected. 124 systemd.services.hermes-agent.serviceConfig.ExecStartPre = [ 125 "${pkgs.writeShellScript "hermes-wait-matrix-dns" '' 126 i=0 127 while [ "$i" -lt 30 ]; do 128 ${pkgs.getent}/bin/getent ahosts matrix.lassul.us >/dev/null 2>&1 && exit 0 129 ${pkgs.coreutils}/bin/sleep 2 130 i=$((i + 1)) 131 done 132 echo "matrix.lassul.us did not resolve within 60s; starting anyway" >&2 133 exit 0 134 ''}" 135 ]; 136 137 # Matrix bot access token for @hermes:lassul.us (minted against the neoprism 138 # Synapse). persist=true so the prompt value is stored once. 139 clan.core.vars.generators.hermes-matrix.prompts.matrix-access-token = { 140 description = "Matrix access token for @hermes:lassul.us"; 141 type = "hidden"; 142 persist = true; 143 }; 144 145 # API token for the shared llama cluster, surfaced to hermes as 146 # LLAMA_API_TOKEN (the custom_providers key_env above). 147 # persist=true so the prompt value is stored once. 148 clan.core.vars.generators.hermes-llama.prompts.llama-api-token = { 149 description = "API token for the shared llama cluster (inference.p0.contact)"; 150 type = "hidden"; 151 persist = true; 152 }; 153 154 # Assemble the .env Hermes reads (mirrors the opencrow-env pattern). 155 clan.core.vars.generators.hermes-env = { 156 dependencies = [ 157 "hermes-matrix" 158 "hermes-llama" 159 ]; 160 files."hermes.env" = { }; 161 runtimeInputs = [ pkgs.coreutils ]; 162 script = '' 163 cat > "$out/hermes.env" <<EOF 164 MATRIX_ACCESS_TOKEN=$(cat "$in"/hermes-matrix/matrix-access-token) 165 LLAMA_API_TOKEN=$(cat "$in"/hermes-llama/llama-api-token) 166 EOF 167 ''; 168 }; 169}