This repository has no description
5.8 kB
133 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 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}