This repository has no description
6.2 kB
141 lines
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 # 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 # Nous does NOT recommend the Hermes-4 chat models for Hermes Agent — it
34 # wants a dedicated *agentic* model. Community/Nous consensus agentic pick
35 # is the MiniMax M2 family (Nous co-optimizes it for Hermes Agent); M2.5
36 # is the cheapest tool-calling option ($0.12/$0.48 per 1M). All 266 Nous
37 # Portal models share this key, so this is just a model-id swap.
38 # Alternatives: minimax/minimax-m2.7, z-ai/glm-4.6, deepseek/deepseek-v3.2.
39 default = "minimax/minimax-m2.5";
40 # hermes's built-in "nous" provider is OAuth-only and hardcodes the dead
41 # host inference.nousresearch.com (NXDOMAIN) — true on both 0.17.0 and
42 # main; "nous-api" exists only in docs, not code. So we drive Nous Portal
43 # through the generic OpenAI-compatible "custom" provider pointed at the
44 # live endpoint, authenticating with the Nous key as OPENAI_API_KEY.
45 provider = "custom";
46 base_url = "https://inference-api.nousresearch.com/v1";
47 # The custom provider can't auto-detect limits, so it left context
48 # uncompressed and defaulted max_tokens to 65536 — once history passed
49 # ~65k tokens, input + max_tokens exceeded the 131072 window and the
50 # endpoint 400'd ("check the model name and other parameters"). Pin both:
51 # context_length so hermes compresses before the window fills, and a sane
52 # output cap (covers thinking + reply) so input + max_tokens stays under.
53 context_length = 204800;
54 max_tokens = 16384;
55
56 # Frontier-on-tap alternative (uncomment to default to Claude instead):
57 # default = "anthropic/claude-sonnet-4-6";
58 };
59
60 # In rooms Hermes requires an @mention by default; DMs always respond.
61 settings.matrix.session_scope = "room";
62
63 # Non-secret connection + SECURITY gate. Invites are auto-accepted and that
64 # cannot be disabled, so the agent may join any room it's invited to. Joining
65 # is harmless on its own — what matters is who can *trigger* the agent, which
66 # on this host has terminal / filesystem / web tools (chat-driven RCE).
67 # MATRIX_ALLOWED_USERS is the universal trigger gate (applies even in DMs);
68 # only these MXIDs can make the agent act, in any room it's joined.
69 environment = {
70 MATRIX_HOMESERVER = "https://matrix.lassul.us";
71 MATRIX_USER_ID = "@hermes:lassul.us";
72 MATRIX_ALLOWED_USERS = "@lassulus:lassul.us";
73 # Element makes DMs end-to-end encrypted by default, so the bot must do
74 # E2EE or it can't read messages. The `matrix` dep group already bundles
75 # mautrix[encryption] (python-olm); "optional" initializes E2EE when those
76 # deps are present (they are) and keeps the crypto store under
77 # /var/lib/hermes/.hermes/platforms/matrix/store/.
78 MATRIX_E2EE_MODE = "optional";
79 # Optional defense-in-depth (confine to specific rooms; DMs are exempt):
80 # MATRIX_ALLOWED_ROOMS = "!yourRoomId:lassul.us";
81 };
82
83 # Secrets (MATRIX_ACCESS_TOKEN + NOUS_API_KEY) come from clan vars below,
84 # merged into $HERMES_HOME/.env at activation.
85 environmentFiles = [
86 config.clan.core.vars.generators.hermes-env.files."hermes.env".path
87 ];
88 };
89
90 # Boot-race guard. matrix.lassul.us is IPv6-only; on this desktop
91 # network-online.target can fire before v6 DNS is ready, so the gateway's
92 # first Matrix connect fails ("Name or service not known") and it does not
93 # retry — leaving the bot silent until a manual restart. Block startup until
94 # the homeserver resolves (best-effort, ~60s cap) so reboots come up
95 # connected.
96 systemd.services.hermes-agent.serviceConfig.ExecStartPre = [
97 "${pkgs.writeShellScript "hermes-wait-matrix-dns" ''
98 i=0
99 while [ "$i" -lt 30 ]; do
100 ${pkgs.getent}/bin/getent ahosts matrix.lassul.us >/dev/null 2>&1 && exit 0
101 ${pkgs.coreutils}/bin/sleep 2
102 i=$((i + 1))
103 done
104 echo "matrix.lassul.us did not resolve within 60s; starting anyway" >&2
105 exit 0
106 ''}"
107 ];
108
109 # Matrix bot access token for @hermes:lassul.us (minted against the neoprism
110 # Synapse). persist=true so the prompt value is stored once.
111 clan.core.vars.generators.hermes-matrix.prompts.matrix-access-token = {
112 description = "Matrix access token for @hermes:lassul.us";
113 type = "hidden";
114 persist = true;
115 };
116
117 # Nous Portal API key (portal.nousresearch.com, Plus subscription).
118 clan.core.vars.generators.hermes-nous.prompts.nous-api-key = {
119 description = "Nous Portal API key (NOUS_API_KEY) from portal.nousresearch.com";
120 type = "hidden";
121 persist = true;
122 };
123
124 # Assemble the .env Hermes reads (mirrors the opencrow-env pattern).
125 clan.core.vars.generators.hermes-env = {
126 dependencies = [
127 "hermes-matrix"
128 "hermes-nous"
129 ];
130 files."hermes.env" = { };
131 runtimeInputs = [ pkgs.coreutils ];
132 script = ''
133 key=$(cat "$in"/hermes-nous/nous-api-key)
134 cat > "$out/hermes.env" <<EOF
135 MATRIX_ACCESS_TOKEN=$(cat "$in"/hermes-matrix/matrix-access-token)
136 NOUS_API_KEY=$key
137 OPENAI_API_KEY=$key
138 EOF
139 '';
140 };
141}