From 70deb2508017a6c57498ab6fa2922cc8b9c8776a Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Fri, 21 Aug 2026 10:30:01 +0200 Subject: [PATCH 01/15] chore: Flake Update --- flake.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/flake.lock b/flake.lock index 8a75a1f..7515072 100644 --- a/flake.lock +++ b/flake.lock @@ -89,11 +89,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1786447342, - "narHash": "sha256-jA79fFQUEsLcpmjWRrlOgVVFVKUQSD4zOZIRKEpEx88=", + "lastModified": 1787274306, + "narHash": "sha256-Qg9f9td5iphUWSQS6zmvyZWO1F+D7j8Z3U6dGyUTg08=", "owner": "NixOS", "repo": "nix", - "rev": "da110e01d913bb32bdc06a301c3797e8c42b8ab7", + "rev": "649e823fb24ed118d72e613be35fa8ea1b64afe7", "type": "github" }, "original": { @@ -143,11 +143,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1786437054, - "narHash": "sha256-I++HzBBAgQ17UaLVU6aSm1/7LDo6c9xr8rAbpByWywE=", + "lastModified": 1787144466, + "narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "6ed13b1d888d5cb07dbb0723eb1df86bbacd0b9c", + "rev": "0471accf8d0a8210b31d947497d179ecc99e0021", "type": "github" }, "original": { @@ -218,11 +218,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1786247143, - "narHash": "sha256-qemNveuexlWhK0Qc6Y8o+gtSVTIj5xljzTAXQZxTHWA=", - "rev": "279b4a8275f032c566576b3f181fa0f27197f588", + "lastModified": 1787135253, + "narHash": "sha256-M5/r2v++FjVhdsxXYMb4BDJ5YLAdCWFt3aZotcshocA=", + "rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46", "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1050399.279b4a8275f0/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1058091.ffb3c9b700e7/nixexprs.tar.xz" }, "original": { "id": "nixpkgs", @@ -245,11 +245,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1786313170, - "narHash": "sha256-F5163SsL9xqCLcx8CS2Y5MeLA5z6mfgXGDiCBEqSnbo=", - "rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d", + "lastModified": 1787101114, + "narHash": "sha256-BA7sSNjLDuPGSOYBGpr6WQjke1MQ8AZpJ8GlYZM/mOc=", + "rev": "b18a4b905f8d028dc4476412e6d6891728695379", "type": "tarball", - "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.7376.fcb8fcd6bf2d/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.8045.b18a4b905f8d/nixexprs.tar.xz" }, "original": { "id": "nixpkgs", -- 2.54.0 From 3ab8bfbb91c4ad75129f5d9fee9f23394d5a25d5 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 15:43:36 +0200 Subject: [PATCH 02/15] feat(sops): add sops-nix input, core module and devShell tooling Wires sops-nix into every machine via machines/core/sops.nix so hosts decrypt with their existing SSH host key. Adds a devShell exposing sops/age/ssh-to-age for the operator workflow, which also fixes .envrc's previously no-op `use flake`. --- .gitignore | 4 +++- .sops.yaml | 17 +++++++++++++++++ flake.lock | 23 ++++++++++++++++++++++- flake.nix | 13 +++++++++++++ machines/configuration.nix | 2 ++ machines/core/default.nix | 1 + machines/core/sops.nix | 31 +++++++++++++++++++++++++++++++ 7 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 .sops.yaml create mode 100644 machines/core/sops.nix diff --git a/.gitignore b/.gitignore index 47dc3e3..869bc14 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -AGENTS.md \ No newline at end of file +AGENTS.md +*.dec +keys.txt \ No newline at end of file diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..e46142f --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,17 @@ +keys: + - &admin_finn age1a0v442ct2qfjcdhyktn25spw02yq4fl08plrrvuradwwxzsle4mq8thln4 + - &host_mibook age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p + # - &host_jupiter age1... # TODO: Phase 9, see docs/secrets.md + +creation_rules: + - path_regex: ^secrets/mibook\.yaml$ + key_groups: + - age: + - *admin_finn + - *host_mibook + # - path_regex: ^secrets/jupiter\.yaml$ # TODO: Phase 9 + # key_groups: + # - age: [ *admin_finn, *host_jupiter ] + # - path_regex: ^secrets/common\.yaml$ # TODO: Phase 9 + # key_groups: + # - age: [ *admin_finn, *host_mibook, *host_jupiter ] diff --git a/flake.lock b/flake.lock index 7515072..ac82b7d 100644 --- a/flake.lock +++ b/flake.lock @@ -264,7 +264,28 @@ "nixos-generators": "nixos-generators", "nixos-hardware": "nixos-hardware", "nixpkgs": "nixpkgs_3", - "nixpkgs-unstable": "nixpkgs-unstable" + "nixpkgs-unstable": "nixpkgs-unstable", + "sops-nix": "sops-nix" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1788337237, + "narHash": "sha256-gkSH8VUtCo6hnysNmb9DbTuDepH2t5pv+QWjP75xKAk=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "fbf759290e0cb0a98dfc813a4eb7d53ad1dacb57", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 362e14f..0b4a512 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,10 @@ url = "github:nix-community/nixos-generators"; inputs.nixpkgs.follows = "nixpkgs"; }; + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; @@ -36,6 +40,15 @@ _module.args.pkgs = import inputs.nixpkgs { inherit system; }; + + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + sops + age + ssh-to-age + nixfmt + ]; + }; }; systems = [ "x86_64-linux" ]; diff --git a/machines/configuration.nix b/machines/configuration.nix index c6af679..eee3f25 100644 --- a/machines/configuration.nix +++ b/machines/configuration.nix @@ -4,6 +4,7 @@ let nixpkgs nixpkgs-unstable nixos-hardware + sops-nix #hyprland ; nixosSystem = nixpkgs.lib.makeOverridable nixpkgs.lib.nixosSystem; @@ -40,6 +41,7 @@ let ]; } ../modules + sops-nix.nixosModules.sops # ../profiles ]; defaultModules = baseModules ++ customModules; diff --git a/machines/core/default.nix b/machines/core/default.nix index 8f95b17..aafd72a 100644 --- a/machines/core/default.nix +++ b/machines/core/default.nix @@ -3,5 +3,6 @@ ./modules.nix ./network.nix ./nix.nix + ./sops.nix ./users.nix ] diff --git a/machines/core/sops.nix b/machines/core/sops.nix new file mode 100644 index 0000000..dc48f15 --- /dev/null +++ b/machines/core/sops.nix @@ -0,0 +1,31 @@ +# sops-nix plumbing — applied to every machine. +# See docs/secrets.md for the day-to-day workflow. +{ + config, + lib, + pkgs, + ... +}: +{ + # Each host defaults to its own secrets/.yaml. Override per-secret + # with sops.secrets..sopsFile for shared files. + sops.defaultSopsFile = lib.mkDefault (../../secrets + "/${config.networking.hostName}.yaml"); + sops.defaultSopsFormat = "yaml"; + sops.validateSopsFiles = true; + + # Decrypt with the host's own SSH host key, converted to age. + # Explicit rather than inheriting the openssh-derived default, so this does + # not silently break if services.openssh is ever disabled. + sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; + sops.age.generateKey = false; + + # Age only. The default here is the RSA host key imported as a GPG key, + # which is deprecated and an extra failure surface. + sops.gnupg.sshKeyPaths = [ ]; + + environment.systemPackages = with pkgs; [ + sops + age + ssh-to-age + ]; +} -- 2.54.0 From a00916864c69d603aac50e2ded7524c04a47bf9e Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 15:43:45 +0200 Subject: [PATCH 03/15] feat(mibook): decrypt placeholder secret via host age key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proves the sops-nix pipeline end to end on mibook: encrypts a throwaway secret to the operator + mibook host keys, declares it in machines/mibook/secrets.nix, and verifies it decrypts to /run/secrets/placeholder (root:root, 0400) on nixos-rebuild switch. Also removes the dead `let secrets = config.sops.secrets;` binding from both environments.nix files — leftover from an earlier abandoned plan, never referenced. --- machines/jupiter/environments.nix | 6 +----- machines/mibook/configuration.nix | 3 +-- machines/mibook/environments.nix | 6 +----- machines/mibook/secrets.nix | 12 ++++++++++++ secrets/mibook.yaml | 25 +++++++++++++++++++++++++ 5 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 machines/mibook/secrets.nix create mode 100644 secrets/mibook.yaml diff --git a/machines/jupiter/environments.nix b/machines/jupiter/environments.nix index e36c825..f0f4850 100644 --- a/machines/jupiter/environments.nix +++ b/machines/jupiter/environments.nix @@ -1,9 +1,5 @@ # enabled profiles -{ config, lib, ... }: -let - secrets = config.sops.secrets; -in -{ +_: { my.profiles = { hyprland.enable = false; zsh.enable = true; diff --git a/machines/mibook/configuration.nix b/machines/mibook/configuration.nix index e326ff7..dd86852 100644 --- a/machines/mibook/configuration.nix +++ b/machines/mibook/configuration.nix @@ -11,6 +11,7 @@ ./hardware-configuration.nix ./environments.nix ./network.nix + ./secrets.nix # ./system.nix use docker here ]; @@ -21,7 +22,6 @@ useOSProber = true; }; - # Configure keymap in X11 services.xserver.xkb = { layout = "de"; @@ -35,7 +35,6 @@ services.printing.enable = true; nixpkgs.config.allowUnfree = true; - hardware.nvidia.prime = { sync.enable = false; diff --git a/machines/mibook/environments.nix b/machines/mibook/environments.nix index 8581e93..286209b 100644 --- a/machines/mibook/environments.nix +++ b/machines/mibook/environments.nix @@ -1,9 +1,5 @@ # enabled profiles -{ config, lib, ... }: -let - secrets = config.sops.secrets; -in -{ +_: { my.profiles = { kde-desktop.enable = true; zsh.enable = true; diff --git a/machines/mibook/secrets.nix b/machines/mibook/secrets.nix new file mode 100644 index 0000000..9678642 --- /dev/null +++ b/machines/mibook/secrets.nix @@ -0,0 +1,12 @@ +# Secrets decrypted on mibook. sopsFile defaults to secrets/mibook.yaml +# via machines/core/sops.nix. +{ ... }: +{ + # Pipeline smoke test — proves host-key decryption works end to end. + # Safe to delete once a real secret is in place. + sops.secrets.placeholder = { + mode = "0400"; + owner = "root"; + group = "root"; + }; +} diff --git a/secrets/mibook.yaml b/secrets/mibook.yaml new file mode 100644 index 0000000..44a9b20 --- /dev/null +++ b/secrets/mibook.yaml @@ -0,0 +1,25 @@ +placeholder: ENC[AES256_GCM,data:tji/6di1F2GsD60TEBGAVvZR9m4jxQ8=,iv:BoBQVR735vq38iGx40VHIpk185ylf79mKmnpewcq/+o=,tag:lXNNk4/HYB0sw44KaQ2KYg==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZUk5cjhGTlVveUFRdHdS + elpVV0pZVU54U3plYjNBai9ZZVBxTDhyRFFnCmpGWW81VGJoMnd4eWxCWnovQnFF + WXR1MVBTZ3VRUnNMUjI5Rko4NHpBdjQKLS0tIDkxYWNxTmhUS2dYTkFnTWZXaS9V + ejdtSmF2M1ZVdFNiWENMWlNraUYxVzAKdlz4xLhTKjJfFESDcym2xCOa71rXj6BE + mdeaCDhE11CAXqrpKU+j0/kwTsZRzyqlwQsfeI4ratu1K1vY66ONsw== + -----END AGE ENCRYPTED FILE----- + recipient: age1a0v442ct2qfjcdhyktn25spw02yq4fl08plrrvuradwwxzsle4mq8thln4 + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYT1hRSGt6TDRSMklQNDA3 + Z2lYS2pMRWxxeEtGRGJSeXRDSlY5UTFBNEZvCm5RK0huT282MXpwWjdrTHByY3Ev + bTBMczR1MzVEcnNDVUxadlREbGg3UUUKLS0tIHhydUlmRExkbmNNNkVTcW1OSnpZ + aEtLZkRTRE9GZ0I1bEN0ZEl3RjVMeDgKoCY7NyxFMS+WuBUez2Irf91Xuy9QpM5g + RTmhK6mw2wd8oUMdgPOUESNwh1xkCz4hDHPrDQnfd4sPhSAvh28jwg== + -----END AGE ENCRYPTED FILE----- + recipient: age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p + lastmodified: "2026-09-08T13:29:12Z" + mac: ENC[AES256_GCM,data:YjMMIUdfCl8dkuvtvBZTFIhLwsUjI6iSeN929O4igbwpRTyGVxgLmAMl81YYHJSyEibelT7ZBKiuVVnL2KjLvAKTj3H1O+AQrcAEJDPC9NwPJwbtAIpLl2li5060Kdj4kr8AY+UJ2kHycuS9dg6wPZTLBJcM/YCw0AoVftvzryM=,iv:GEQZF9RvW+MJlMIjn1ToD74RubWMQtCyE5LqW9KQQfA=,tag:NR6u88eBY1asQZM6f45hTw==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3 -- 2.54.0 From 621ca5dde6d8f499ca4e41e68401467e17098767 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 15:43:50 +0200 Subject: [PATCH 04/15] docs: sops-nix secrets workflow Operator runbook for adding a secret, adding a new host, and rotating keys, plus a pointer from CLAUDE.md's architecture overview. --- CLAUDE.md | 12 +++++++ docs/secrets.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 docs/secrets.md diff --git a/CLAUDE.md b/CLAUDE.md index f480c60..fcd95e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,7 @@ flake.nix │ ├── core.nix # system packages, locale, timezone │ ├── network.nix │ ├── nix.nix + │ ├── sops.nix # sops-nix decryption config, sops/age/ssh-to-age on PATH │ └── users.nix ├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*) │ ├── environments/ # per-service/app profiles @@ -43,10 +44,21 @@ flake.nix └── machines// ├── configuration.nix # machine-specific NixOS settings ├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options + ├── secrets.nix # sops.secrets declared for this host ├── disks.nix └── hardware-configuration.nix ``` +### Secrets (sops-nix) + +Secrets are encrypted with sops, decrypted per-host from that host's SSH host +key. `.sops.yaml` defines who can decrypt what; `secrets/.yaml` holds +the encrypted values; `machines//secrets.nix` declares which secrets +that host exposes and consumes them via `config.sops.secrets..path`. +Always `git add` a secrets file before `nix build` — untracked files are +invisible to flake evaluation. Full workflow (adding a secret, adding a host, +rotating keys): see [`docs/secrets.md`](docs/secrets.md). + ### Profile / Module Pattern Every module under `modules/` follows the same structure: diff --git a/docs/secrets.md b/docs/secrets.md new file mode 100644 index 0000000..dc098fe --- /dev/null +++ b/docs/secrets.md @@ -0,0 +1,92 @@ +# Secrets (sops-nix) + +Secrets are encrypted with [sops](https://github.com/getsops/sops) and decrypted +at activation by [sops-nix](https://github.com/Mic92/sops-nix). Each host decrypts +with its own SSH host key (converted to an age key in-memory — no separate host +private key is ever stored on disk). + +Tooling (`sops`, `age`, `ssh-to-age`) is available via `nix develop` in this repo, +and installed system-wide on every machine via `machines/core/sops.nix`. + +## Adding a secret to an existing host + +``` +nix develop -c sops secrets/.yaml +``` + +This opens `$EDITOR` on the decrypted content. Add your key, save, quit — sops +re-encrypts on write. Then declare it in `machines//secrets.nix`: + +```nix +sops.secrets.my-secret = { + owner = "someuser"; # defaults to root + group = "someuser"; + mode = "0400"; + restartUnits = [ "foo.service" ]; # if a running service needs to pick it up +}; +``` + +Consume it in the owning module as `config.sops.secrets.my-secret.path` +(resolves to `/run/secrets/my-secret`) — never inline the decrypted value into +a Nix string, since that copies it into the world-readable `/nix/store`. + +Then: + +``` +nix build '.#nixosConfigurations..config.system.build.toplevel' +sudo nixos-rebuild test --flake '.#' +# verify, then: +sudo nixos-rebuild switch --flake '.#' +``` + +**The secrets file must be `git add`ed before `nix build`** — untracked files +are invisible to flake evaluation, and `sops.validateSopsFiles` needs it in the +Nix store. + +## Adding a new host + +1. On the new host: `ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub` to get its + age public key. +2. In `.sops.yaml`: add a `&host_` anchor with that key, and a + `creation_rules` entry for `secrets/.yaml` including both the new + host key and `*admin_finn`. +3. Create `machines//secrets.nix`, add it to that machine's + `configuration.nix` imports. +4. If the host needs to read an existing shared file, run + `sops updatekeys secrets/.yaml` after step 2 so it's re-encrypted to + include the new recipient. + +## Rotating keys + +``` +# after editing .sops.yaml (new/removed recipient): +nix develop -c sops updatekeys secrets/.yaml +``` + +for every file the change affects. `updatekeys` requires you to already be +able to decrypt the file — this is why `*admin_finn` is a recipient on every +rule, as the standing fallback. + +Reinstalling a host regenerates its SSH host key, which changes its age +identity. Any file previously encrypted to that host must be `updatekeys`'d +with the new key, or the host's next activation fails to decrypt. + +## Where private keys live + +- **Operator** (you): `~/.config/sops/age/keys.txt` — sops' default lookup + path, so `SOPS_AGE_KEY_FILE` never needs to be set. Never committed. Back + this up out-of-band (password manager, offline copy) — if it's lost, only + the surviving host keys can still decrypt their own files, and any file that + was only ever readable by an operator-only rule becomes permanently + unreadable. +- **Hosts**: `/etc/ssh/ssh_host_ed25519_key` — already managed by NixOS, + converted to age in-memory at activation time. Nothing extra to back up. + +## Layout + +``` +.sops.yaml # age recipients + creation rules (which keys can decrypt which files) +secrets/.yaml # per-host encrypted secrets (default sopsFile for that host) +machines/core/sops.nix # applied to every machine: decryption config, sops/age/ssh-to-age on PATH +machines//secrets.nix # per-host: which secrets exist, owner/group/mode +``` -- 2.54.0 From 6212b4fcf730a6d0f1b2eea8899bdbc1a53a500b Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 16:56:26 +0200 Subject: [PATCH 05/15] docs: design spec for Hermes Agent on mibook Locks in host (mibook, not jupiter), deployment mode (NixOS native module), and auth (claude setup-token / ANTHROPIC_TOKEN, Max extra credits) after a grilling round. Builds on feat/sops-nix as-is. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S --- .../2026-09-08-hermes-agent-nixos-design.md | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md diff --git a/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md b/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md new file mode 100644 index 0000000..7f3dfee --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md @@ -0,0 +1,147 @@ +# Hermes Agent on mibook + +**Date:** 2026-09-08 +**Machine:** mibook (laptop, KDE desktop, current daily driver) +**Status:** Design approved, pending implementation plan +**Base branch:** `feat/sops-nix` (pushed to `origin`, not yet merged to `main`) + +## Goal + +Deploy [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) +as a NixOS-native systemd service on mibook, reachable via Telegram and Email, +using an Anthropic Claude subscription (`claude setup-token`) rather than a +pay-per-token API key. Design + implementation plan only — no execution in +this round; a later session applies the plan. + +## Decisions + +| Topic | Decision | +|-------|----------| +| Host | mibook, not jupiter. Availability gap (laptop sleeps/lid-closes) is accepted, not engineered around | +| Use case | Messaging gateway (Telegram + Email), not CLI-only | +| Deployment mode | NixOS native module (`services.hermes-agent.*` from upstream `nixosModules.default`), wrapped in `my.profiles.hermes-agent`. Home Manager was considered and rejected — not wired into this flake anywhere; adding it for one service is a new framework dependency, not proportionate | +| Module source | Official `github:NousResearch/hermes-agent`, not the community `0xrsydn/nix-hermes-agent` wrapper | +| Nix support tier | Docs explicitly call Nix/NixOS "Tier 2, best-effort" — Docker/FHS is upstream's primary supported path. Accepted tradeoff, not a blocker | +| LLM provider auth | Anthropic via `claude setup-token` (long-lived, headless-friendly token) → `ANTHROPIC_TOKEN`, not `ANTHROPIC_API_KEY` | +| Billing caveat | Setup-token/OAuth path only draws from a **Claude Max "extra usage credits"** pool, never the base Max allowance. Claude Pro cannot use this path at all. User intends to use `claude setup-token`; not independently re-verified that extra credits are purchased — confirm in Anthropic console before/during implementation | +| Secrets | One sops secret, `hermes-env`, containing a full env-file blob (matches the module's `environmentFiles` list-of-paths interface — not a single-value secret) | +| sops prerequisite | **None.** mibook is already a registered sops recipient (`host_mibook` in `.sops.yaml`, `secrets/mibook.yaml` exists, proven via a live `nixos-rebuild switch` in the sops-nix work). No "Phase 9"-style bootstrapping needed — that deferred work is about *jupiter*, irrelevant now that mibook is the target | +| Network exposure | None. Telegram (long-polling) and Email (IMAP/SMTP) are both outbound-initiated from mibook. No Caddy/webserver module changes, no public endpoint | +| Messaging platforms | Telegram + Email only, first pass. Hermes Agent supports 27+ platforms total (Discord, Slack, WhatsApp, Matrix, etc.) — others deliberately out of scope, same pattern applies later if wanted | + +### Deliberately deferred (YAGNI) +- Any workaround for mibook's availability gap (cloud relay, wake scheduling, HA failover to jupiter) +- Discord, Slack, or any other messaging platform beyond Telegram + Email +- Public/internet exposure of any kind +- Merging or rebasing `feat/sops-nix` — this design builds on top of it as-is + +## Part 1 — The module + +New flake input in `flake.nix`, following the exact shape `sops-nix` already +uses: + +```nix +hermes-agent = { + url = "github:NousResearch/hermes-agent"; + inputs.nixpkgs.follows = "nixpkgs"; +}; +``` + +Import the upstream module **only into mibook's module list**, not +`baseModules` — this is a single-host service, unlike `sops-nix.nixosModules.sops` +which every machine needs. In `machines/configuration.nix`, mibook's +`modules = defaultModules ++ [ hermes-agent.nixosModules.default ./mibook/configuration.nix ]` +(exact insertion point to be confirmed against the file at implementation +time — `defaultModules` is currently machine-agnostic, so this is a targeted, +minimal deviation from that shared list, not a restructuring of it). + +New file `modules/environments/hermes-agent/default.nix` following the +repo's standard profile pattern: + +```nix +{ config, lib, pkgs, ... }: +let + cfg = config.my.profiles.hermes-agent; +in +{ + options.my.profiles.hermes-agent.enable = lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; + + config = lib.mkIf cfg.enable { + services.hermes-agent = { + enable = true; + settings.model.default = "anthropic/claude-sonnet-4-5"; # confirm exact model string at implementation + environmentFiles = [ config.sops.secrets."hermes-env".path ]; + addToSystemPackages = true; + }; + }; +} +``` + +Add `./environments/hermes-agent` to `modules/environments/default.nix`; +enable `my.profiles.hermes-agent.enable = true` in +`machines/mibook/environments.nix`. + +**Provided for free by the upstream module:** `hermes` system user/group +(`createUser` default true), hardened systemd unit (`NoNewPrivileges`, +`ProtectSystem=strict`, `PrivateTmp` — not independently user-configurable +per the docs), `hermes` CLI on `$PATH` via `addToSystemPackages`. + +## Part 2 — Secret + +Add to `machines/mibook/secrets.nix` (existing file, per `docs/secrets.md`): + +```nix +sops.secrets.hermes-env = { + owner = "hermes"; + group = "hermes"; + mode = "0400"; + restartUnits = [ "hermes-agent.service" ]; # confirm exact unit name at implementation +}; +``` + +Populate via `nix develop -c sops secrets/mibook.yaml`, adding a +multi-line `hermes-env` block: + +``` +ANTHROPIC_TOKEN= +TELEGRAM_BOT_TOKEN= + +``` + +**Verify at implementation, not assumed:** the module's own documented +example uses `ANTHROPIC_API_KEY`; the setup-token path is documented +separately (and thinly) as `ANTHROPIC_TOKEN`. Confirm the running module +actually picks up `ANTHROPIC_TOKEN` before treating auth as working — +if it doesn't, the fallback is a plain `ANTHROPIC_API_KEY` (pay-per-token, +loses the subscription framing this design was built around). + +**Must `git add secrets/mibook.yaml` before `nix build`** — untracked +files are invisible to flake evaluation (existing repo rule, `docs/secrets.md`). + +## Part 3 — Rollout runbook (on mibook) + +1. Confirm Claude Max extra-usage-credits are actually purchased (Anthropic + console) — the whole auth design depends on this pool being funded. +2. Run `claude setup-token` on mibook; capture the token. +3. Create/gather Telegram bot token (`@BotFather`) and email credentials. +4. Add the `hermes-agent` flake input; add the module wrapper file; wire it + into `modules/environments/default.nix` and `machines/mibook/environments.nix`. +5. Add `sops.secrets.hermes-env` to `machines/mibook/secrets.nix`. +6. `nix develop -c sops secrets/mibook.yaml`, populate `hermes-env`, `git add`. +7. `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'` — CI-style check before touching the running system. +8. `sudo nixos-rebuild test --flake '.#mibook'` — verify before persisting across reboot. +9. `journalctl -u hermes-agent.service -f` — confirm it starts, decrypts the secret, connects to Telegram (and doesn't loop-crash on the `ANTHROPIC_TOKEN` question from Part 2). +10. Send a test Telegram message; send a test email; confirm both round-trip. +11. `sudo nixos-rebuild switch --flake '.#mibook'` once verified. + +### Rollback +`my.profiles.hermes-agent.enable = false`, `nixos-rebuild switch`. No data +migration involved (no existing deployment to preserve), so rollback is a +one-line revert, not a runbook. + +## Open risks (carried forward, not resolved by design) + +1. **`ANTHROPIC_TOKEN` vs `ANTHROPIC_API_KEY`** — thin upstream docs, needs a real test (Part 2). +2. **Extra-credits billing pool funding** — not independently verified, user-asserted intent (Part 3, step 1). +3. **Tier 2 Nix support** — expect some manual troubleshooting; upstream doesn't guarantee the flake stays working across their commits. +4. **Availability gap** — accepted by design, not a bug to fix later unless priorities change. -- 2.54.0 From a83f7e63a0e58bcd63464223794dfec70ae807b6 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:00:38 +0200 Subject: [PATCH 06/15] docs: implementation plan for Hermes Agent on mibook Task 1 is agent-executable repo wiring (flake input, profile module, secret declaration), build-checked with nix build. Tasks 2-5 are an operator-run runbook: OAuth login, Anthropic billing check, Telegram bot setup, and typing real secrets into $EDITOR via sops - none of that is meant to pass through an agent session. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S --- .../plans/2026-09-08-hermes-agent-mibook.md | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md diff --git a/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md new file mode 100644 index 0000000..d9032a9 --- /dev/null +++ b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md @@ -0,0 +1,343 @@ +# Hermes Agent on mibook Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> +> **Note on nature:** Task 1 is repo-only work verifiable with `nix build` — no runtime tests exist for declarative config, matching this repo's existing convention (see the Immich plan). Tasks 2–5 are an **operator-run runbook**, not agent-automatable: Task 2 needs an interactive `claude setup-token` OAuth login, an Anthropic billing console check, and a Telegram BotFather chat; Task 3 requires typing real secret material into the `$EDITOR` sops opens; Task 4 runs `nixos-rebuild` on mibook, the operator's live daily-driver desktop. **Never paste secret values (tokens, passwords) into an agent session or chat** — Task 3 has the operator type them directly into the editor sops opens. Present Tasks 2–5 for the operator to run and confirm; do not attempt to execute them from an agent session. + +**Goal:** Run Hermes Agent as a hardened NixOS systemd service on mibook, reachable via Telegram and Email, authenticated to Anthropic via a Claude subscription (`claude setup-token`) rather than a pay-per-token API key. + +**Architecture:** A standard `my.profiles.hermes-agent` module wraps the upstream `services.hermes-agent` module (from `NousResearch/hermes-agent`'s `nixosModules.default`), imported only into mibook's module list. Credentials (Anthropic token, Telegram bot token, email creds) live in one sops secret (`hermes-env`) consumed via `environmentFiles`. No network exposure — Telegram long-polling and Email IMAP/SMTP are both outbound-initiated from mibook. + +**Tech Stack:** NixOS (flake-parts), sops-nix, upstream `hermes-agent` flake (Tier 2/best-effort Nix support), Anthropic Claude (subscription auth). + +**Spec:** `docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md` + +## Global Constraints + +- Machine: **mibook** only. Do not enable on jupiter. +- Module source: official `github:NousResearch/hermes-agent` (`nixosModules.default`). Not the community `0xrsydn/nix-hermes-agent` wrapper. +- Auth: `claude setup-token` output goes into `ANTHROPIC_TOKEN` in the `hermes-env` secret — **not** `ANTHROPIC_API_KEY`. Verify the running service actually authenticates on this var (Task 4); fall back to a plain `ANTHROPIC_API_KEY` only if it doesn't. +- Billing dependency: the setup-token/OAuth path only draws from **Claude Max "extra usage credits"** — confirm this pool is funded before Task 2's token generation, or auth will fail even with a valid token. +- Secret: one sops secret named `hermes-env`, a full env-file blob (matches `environmentFiles` — list of paths, not a single-value secret). +- No network exposure: do not touch `modules/services/webserver` or open any inbound port. Telegram + Email are both outbound-only. +- Platforms: Telegram + Email only, first pass. No Discord/Slack/etc. +- Rebuild command: `sudo nixos-rebuild switch --flake '.#mibook'` (use `nixos-rebuild test` first, per Task 4). +- Build-check command: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`. +- Format Nix with `nixfmt-rfc-style` before committing. +- Secret material never enters an agent session, chat, or the git history in plaintext — only via `sops secrets/mibook.yaml` and `$EDITOR`. +- Base branch: `feat/sops-nix` (already pushed; do not merge or rebase it as part of this work). + +--- + +## File Structure + +- **Modify** `flake.nix` — add the `hermes-agent` flake input. +- **Modify** `machines/configuration.nix` — destructure the new input; import `hermes-agent.nixosModules.default` into mibook's module list only. +- **Create** `modules/environments/hermes-agent/default.nix` — the `my.profiles.hermes-agent` module (single responsibility: declare Hermes Agent). +- **Modify** `modules/environments/default.nix` — add `./hermes-agent` to the import list. +- **Modify** `machines/mibook/environments.nix` — set `hermes-agent.enable = true`. +- **Modify** `machines/mibook/secrets.nix` — replace the `placeholder` secret with `hermes-env`. +- **Modify (encrypted, operator-only)** `secrets/mibook.yaml` — real credential values, never touched by an agent. + +No other files change. + +--- + +### Task 1: Wire up the module (flake input, profile module, secret declaration) + +**Files:** +- Modify: `flake.nix` +- Modify: `machines/configuration.nix` +- Create: `modules/environments/hermes-agent/default.nix` +- Modify: `modules/environments/default.nix` +- Modify: `machines/mibook/environments.nix` +- Modify: `machines/mibook/secrets.nix` + +**Interfaces:** +- Produces: NixOS option `my.profiles.hermes-agent.enable` (bool). When true, configures `services.hermes-agent` with `environmentFiles = [ config.sops.secrets."hermes-env".path ]`. Declares `sops.secrets.hermes-env` (owner/group `hermes`, path resolves to `/run/secrets/hermes-env` at runtime). +- Consumes: `hermes-agent.nixosModules.default` from the new flake input; existing `sops.secrets.` mechanism from `machines/core/sops.nix` (already applied to every machine, unaffected by this task). + +- [ ] **Step 1: Add the flake input** + +In `flake.nix`, inside `inputs = { ... }`, add after the `sops-nix` block: + +```nix + hermes-agent = { + url = "github:NousResearch/hermes-agent"; + inputs.nixpkgs.follows = "nixpkgs"; + }; +``` + +- [ ] **Step 2: Lock the new input** + +Run: `nix flake lock` +Expected: `flake.lock` gains a `hermes-agent` entry (and its transitive inputs). No other entries change. + +- [ ] **Step 3: Import the upstream module into mibook only** + +In `machines/configuration.nix`, add `hermes-agent` to the `inherit (self.inputs) ...` destructure: + +```nix + inherit (self.inputs) + nixpkgs + nixpkgs-unstable + nixos-hardware + sops-nix + hermes-agent + #hyprland + ; +``` + +Then change mibook's `modules` list (leave jupiter's untouched): + +```nix + mibook = nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit self; }; + modules = defaultModules ++ [ + # nixos-hardware.nixosModules.mibook + hermes-agent.nixosModules.default + ./mibook/configuration.nix + ]; + }; +``` + +- [ ] **Step 4: Read a reference module to match repo style** + +Read `modules/environments/immich/default.nix` (same shape: `cfg`, `lib.mkIf`, single-purpose options block). Match its formatting. + +- [ ] **Step 5: Create the profile module** + +Create `modules/environments/hermes-agent/default.nix`: + +```nix +# Hermes Agent — Telegram + Email messaging gateway (Claude subscription auth) +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.my.profiles.hermes-agent; +in +{ + options.my.profiles.hermes-agent.enable = lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; + + config = lib.mkIf cfg.enable { + services.hermes-agent = { + enable = true; + settings.model.default = "anthropic/claude-sonnet-4-5"; + environmentFiles = [ config.sops.secrets."hermes-env".path ]; + addToSystemPackages = true; + }; + }; +} +``` + +- [ ] **Step 6: Register the module in the environments import list** + +In `modules/environments/default.nix`, add `./hermes-agent` after `./immich`: + +```nix + ./immich + ./hermes-agent + ]; +} +``` + +- [ ] **Step 7: Enable it on mibook** + +In `machines/mibook/environments.nix`, inside the `my.profiles = { ... }` block, add: + +```nix + hermes-agent.enable = true; +``` + +- [ ] **Step 8: Replace the placeholder secret declaration** + +In `machines/mibook/secrets.nix`, replace the whole file body: + +```nix +# Secrets decrypted on mibook. sopsFile defaults to secrets/mibook.yaml +# via machines/core/sops.nix. +{ ... }: +{ + sops.secrets.hermes-env = { + owner = "hermes"; + group = "hermes"; + mode = "0400"; + restartUnits = [ "hermes-agent.service" ]; # confirmed/fixed in Task 4 if the unit name differs + }; +} +``` + +This removes the `placeholder` secret declared here — it was an explicit throwaway (comment: "Safe to delete once a real secret is in place"). Its entry in the encrypted `secrets/mibook.yaml` content itself is cleaned up by the operator in Task 3, not here. + +- [ ] **Step 9: Format** + +Run: `nixfmt-rfc-style modules/environments/hermes-agent/default.nix machines/configuration.nix machines/mibook/secrets.nix machines/mibook/environments.nix flake.nix` + +- [ ] **Step 10: Build-check (this is the "test")** + +Run: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'` +Expected: builds successfully. This succeeds even though `secrets/mibook.yaml` doesn't yet contain a `hermes-env` key — `sops.validateSopsFiles` only checks the file is valid sops-encrypted YAML, not that every declared secret name exists inside it; that check happens at activation (Task 4), not build. +If it fails on an unknown `services.hermes-agent.*` option, reconcile against the actual module: `nix eval --raw '.#nixosConfigurations.mibook.config.system.build.toplevel'` errors will name the bad option; cross-check against `website/docs/getting-started/nix-setup.md` in the `NousResearch/hermes-agent` repo at the locked revision. + +- [ ] **Step 11: Confirm the option evaluates on** + +Run: `nix eval '.#nixosConfigurations.mibook.config.services.hermes-agent.enable'` +Expected: `true` + +- [ ] **Step 12: Commit** + +```bash +git add flake.nix flake.lock machines/configuration.nix modules/environments/hermes-agent/default.nix modules/environments/default.nix machines/mibook/environments.nix machines/mibook/secrets.nix +git commit -m "feat(mibook): add Hermes Agent profile module" +``` + +--- + +### Task 2: Gather credentials (operator-run, not agent-automatable) + +**Files:** none. All values are held by the operator for Task 3 — do not write them into any file an agent can read, and do not paste them into a chat session. + +**Interfaces:** +- Produces: an Anthropic setup-token, a Telegram bot token, and email (IMAP/SMTP) credentials — ready to type into `$EDITOR` in Task 3. + +- [ ] **Step 1: Confirm the billing pool is funded** + +In the Anthropic console, confirm the Claude Max plan has **extra usage credits purchased**. The `claude setup-token` auth path bills against this pool only — it does not draw from the base Max allowance, and Claude Pro cannot use this path at all. If this isn't funded, stop here and decide whether to fund it or fall back to a plain `ANTHROPIC_API_KEY` (see Global Constraints). + +- [ ] **Step 2: Generate the setup-token** + +On mibook, run: `claude setup-token` +This opens an interactive OAuth flow. On completion it prints a long-lived token. Copy it somewhere only you control (e.g. your password manager) — you'll type it into `sops` in Task 3, not into this session. + +- [ ] **Step 3: Create the Telegram bot** + +In Telegram, message `@BotFather`, run `/newbot`, follow the prompts. Copy the resulting bot token (format `123456:ABC-...`). + +- [ ] **Step 4: Gather email credentials** + +Decide which mailbox Hermes Agent should use, and get its IMAP/SMTP host, username, and password (or app-specific password if the provider requires one, e.g. Gmail). Check `website/docs/integrations/` in the `NousResearch/hermes-agent` repo for the exact env var names the email integration expects (this plan doesn't guess them — confirm the real key names here, not at Task 3, so Task 3 is a straight copy-paste). + +--- + +### Task 3: Populate the secret (operator-run, secret material) + +**Files:** +- Modify (encrypted): `secrets/mibook.yaml` + +**Interfaces:** +- Consumes: the three credentials gathered in Task 2, and the `hermes-env` declaration from Task 1. +- Produces: a decryptable `hermes-env` entry that Task 1's `environmentFiles` reference resolves at activation. + +- [ ] **Step 1: Open the encrypted file** + +Run: `nix develop -c sops secrets/mibook.yaml` +This decrypts and opens `$EDITOR` on the plaintext. + +- [ ] **Step 2: Remove the placeholder key, add `hermes-env`** + +Delete the `placeholder: ...` entry (Task 1 already removed its Nix declaration; this removes the matching content). Add: + +```yaml +hermes-env: | + ANTHROPIC_TOKEN= + TELEGRAM_BOT_TOKEN= + +``` + +(Exact YAML block-scalar syntax per whatever your `$EDITOR` shows — sops re-encrypts on save regardless of formatting, as long as it's valid YAML.) + +- [ ] **Step 3: Save and let sops re-encrypt** + +Save and quit `$EDITOR`. Run: `git diff --stat secrets/mibook.yaml` +Expected: the file shows as changed (still fully encrypted — confirm by `git diff secrets/mibook.yaml | head` and seeing sops's `ENC[...]` ciphertext, never plaintext). + +- [ ] **Step 4: Stage it** + +Run: `git add secrets/mibook.yaml` +Required before the next `nix build` — untracked sops files are invisible to flake evaluation (repo rule, `docs/secrets.md`). + +- [ ] **Step 5: Commit** + +```bash +git commit -m "feat(mibook): populate Hermes Agent credentials" +``` + +--- + +### Task 4: Deploy and verify (operator-run, on mibook) + +**Files:** none in repo. Run all commands on mibook. + +**Interfaces:** +- Consumes: the built config from Task 1, the secret from Task 3. +- Produces: a running, verified `hermes-agent.service` reachable via Telegram and Email. + +- [ ] **Step 1: Test-activate without persisting** + +Run: `sudo nixos-rebuild test --flake '.#mibook'` +Expected: activates without reboot-persisting. If it fails at secret decryption ("hermes-env not found in sops file"), Task 3 wasn't saved/staged correctly — redo Task 3. + +- [ ] **Step 2: Confirm the unit name and watch it start** + +Run: `systemctl list-units | grep -i hermes` to get the exact unit name (Task 1's `restartUnits = [ "hermes-agent.service" ]` was a guess — fix `machines/mibook/secrets.nix` and re-run `nixos-rebuild test` if it differs). +Then: `journalctl -u hermes-agent.service -f` +Expected: starts, reads the env file, and does **not** crash-loop on auth. If it errors specifically on `ANTHROPIC_TOKEN` not being recognized, that's the documented-but-unverified case from the spec — try renaming the key to `ANTHROPIC_API_KEY` in Task 3's secret (note: this switches to pay-per-token billing, defeating the point of the subscription path — treat as a fallback, not silently accept it). + +- [ ] **Step 3: Verify Telegram connects** + +Send your bot a message from Telegram. Expected: it responds (confirms both the bot token and the LLM auth work end-to-end). + +- [ ] **Step 4: Verify Email connects** + +Send a test email to the configured mailbox. Expected: Hermes Agent picks it up (check `journalctl -u hermes-agent.service` for an inbound-message log line) and can send a reply. + +- [ ] **Step 5: Persist across reboot** + +Run: `sudo nixos-rebuild switch --flake '.#mibook'` +Expected: same running state as Step 1–4, now persisted. + +--- + +### Task 5: Sign-off (operator-run) + +**Files:** none in repo beyond what Tasks 1 and 3 already committed. + +**Interfaces:** +- Consumes: a verified running instance (Task 4). + +- [ ] **Step 1: Open a PR** + +`feat/sops-nix` is already pushed to `origin` but not merged; this work sits on top of it. Push this branch (or continue on `feat/sops-nix` directly, per your preference) and open a PR per the repo's usual Gitea workflow. Note in the PR description that it depends on `feat/sops-nix` landing first (or is stacked on it). + +- [ ] **Step 2: Confidence window** + +Use it for a few days — send yourself Telegram/Email messages periodically, including after mibook has slept and woken up, to see how the accepted availability gap actually behaves in practice. + +- [ ] **Step 3: Rollback (if needed)** + +`my.profiles.hermes-agent.enable = false` in `machines/mibook/environments.nix`, `sudo nixos-rebuild switch --flake '.#mibook'`. No data migration is involved — this is a clean revert, not a runbook. + +--- + +## Self-Review + +**Spec coverage:** +- Host mibook, not jupiter → Global Constraints + File Structure. ✓ +- NixOS native module, official upstream source → Task 1. ✓ +- Home Manager rejected → not present anywhere (correctly absent, matches "deliberately deferred" reasoning — no task references it). ✓ +- `claude setup-token` → `ANTHROPIC_TOKEN`, Max extra-credits caveat → Task 2 Steps 1–2, Task 4 Step 2, Global Constraints. ✓ +- `hermes-env` single env-file secret → Task 1 Step 8, Task 3. ✓ +- No sops prerequisite (mibook already registered) → confirmed by reusing existing `machines/mibook/secrets.nix` rather than any `.sops.yaml`/Phase-9-style bootstrapping. ✓ +- No network exposure → Global Constraints; no task touches `modules/services/webserver`. ✓ +- Telegram + Email only → Task 2 Steps 3–4, Task 4 Steps 3–4. ✓ +- Rollback path → Task 5 Step 3. ✓ +- Deferred (other platforms, availability-gap workarounds, public exposure) → correctly absent. ✓ + +**Placeholder scan:** No bare TBD/TODO. The bracketed values (``, etc.) are operator-supplied secret material by design — Task 2 explicitly produces them, Task 3 explicitly consumes them; they're never meant to be concrete in this document since they're credentials, not code. + +**Type consistency:** `my.profiles.hermes-agent.enable`, `services.hermes-agent.*`, and the `hermes-env` secret name are used identically across Tasks 1, 3, and 4. The `hermes-agent.service` unit name is flagged as unverified in both Task 1 (declaration) and Task 4 (where it's actually checked and corrected if wrong) — consistent, not contradictory. -- 2.54.0 From 32e0faa82cf321ea7b7fdea0311a174f5c30ab99 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:01:54 +0200 Subject: [PATCH 07/15] chore: ignore .superpowers/ scratch workspace Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 869bc14..3493d36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ AGENTS.md *.dec -keys.txt \ No newline at end of file +keys.txt +.superpowers/ \ No newline at end of file -- 2.54.0 From 731678ecac2f9e8f7581770bd1a207bc47ae6f2c Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:20:40 +0200 Subject: [PATCH 08/15] feat(mibook): add Hermes Agent profile module --- flake.lock | 167 +++++++++++++++++- flake.nix | 4 + machines/configuration.nix | 2 + machines/mibook/environments.nix | 1 + machines/mibook/secrets.nix | 9 +- modules/environments/default.nix | 1 + modules/environments/hermes-agent/default.nix | 23 +++ 7 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 modules/environments/hermes-agent/default.nix diff --git a/flake.lock b/flake.lock index ac82b7d..843f14a 100644 --- a/flake.lock +++ b/flake.lock @@ -35,6 +35,27 @@ } }, "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "hermes-agent", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { "inputs": { "nixpkgs-lib": [ "nix", @@ -79,10 +100,57 @@ "type": "github" } }, + "hermes-agent": { + "inputs": { + "flake-parts": "flake-parts_2", + "home-manager": "home-manager", + "nixpkgs": [ + "nixpkgs" + ], + "npm-lockfile-fix": "npm-lockfile-fix", + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "uv2nix": "uv2nix" + }, + "locked": { + "lastModified": 1788877844, + "narHash": "sha256-9Ik9egIxYhW/IDw1mz/dYN0IqLTP3bEe+LdEFtP9FWM=", + "owner": "NousResearch", + "repo": "hermes-agent", + "rev": "c8aa5608c24e3636e77c267650c0f1f52e44adb0", + "type": "github" + }, + "original": { + "owner": "NousResearch", + "repo": "hermes-agent", + "type": "github" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "hermes-agent", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1786487128, + "narHash": "sha256-ad60hrRVhH/bo3Jl1YLzO+QcS3mUNZr9LNJdzhMO2P4=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "f404edbfa4117810c96b97048299242fc50e5362", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, "nix": { "inputs": { "flake-compat": "flake-compat", - "flake-parts": "flake-parts_2", + "flake-parts": "flake-parts_3", "git-hooks-nix": "git-hooks-nix", "nixpkgs": "nixpkgs", "nixpkgs-23-11": "nixpkgs-23-11", @@ -257,9 +325,81 @@ "type": "indirect" } }, + "npm-lockfile-fix": { + "inputs": { + "nixpkgs": [ + "hermes-agent", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775903712, + "narHash": "sha256-2GV79U6iVH4gKAPWYrxUReB0S41ty/Y3dBLquU8AlaA=", + "owner": "jeslie0", + "repo": "npm-lockfile-fix", + "rev": "c6093acb0c0548e0f9b8b3d82918823721930fe8", + "type": "github" + }, + "original": { + "owner": "jeslie0", + "repo": "npm-lockfile-fix", + "type": "github" + } + }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "hermes-agent", + "nixpkgs" + ], + "pyproject-nix": [ + "hermes-agent", + "pyproject-nix" + ], + "uv2nix": [ + "hermes-agent", + "uv2nix" + ] + }, + "locked": { + "lastModified": 1785115949, + "narHash": "sha256-8AM37BfyGaL2v/SZyg4PupRxJ01Y4htvM+WrTjWrPpo=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "62c0d86027edb1c4f39a5facc09876348144f7c9", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, + "pyproject-nix": { + "inputs": { + "nixpkgs": [ + "hermes-agent", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1784591072, + "narHash": "sha256-zP/WaDxrRu8GANZM61+V2LT/7ycEEdoyLWn7M6WzU7M=", + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "rev": "e3b599ca2e7fcf93d4edf65d7f19bbf6491724f3", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "pyproject.nix", + "type": "github" + } + }, "root": { "inputs": { "flake-parts": "flake-parts", + "hermes-agent": "hermes-agent", "nix": "nix", "nixos-generators": "nixos-generators", "nixos-hardware": "nixos-hardware", @@ -287,6 +427,31 @@ "repo": "sops-nix", "type": "github" } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "hermes-agent", + "nixpkgs" + ], + "pyproject-nix": [ + "hermes-agent", + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1785277507, + "narHash": "sha256-9Tq3UDX2hD/aveW/HvkBlAmEwJTOlY5HQXJM+L5BGmE=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "5a836d395cbf5fc22670eb98dd4aa4fc4d406977", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 0b4a512..64a9f1f 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,10 @@ url = "github:Mic92/sops-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + hermes-agent = { + url = "github:NousResearch/hermes-agent"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; diff --git a/machines/configuration.nix b/machines/configuration.nix index eee3f25..731ba72 100644 --- a/machines/configuration.nix +++ b/machines/configuration.nix @@ -5,6 +5,7 @@ let nixpkgs-unstable nixos-hardware sops-nix + hermes-agent #hyprland ; nixosSystem = nixpkgs.lib.makeOverridable nixpkgs.lib.nixosSystem; @@ -62,6 +63,7 @@ in specialArgs = { inherit self; }; modules = defaultModules ++ [ # nixos-hardware.nixosModules.mibook + hermes-agent.nixosModules.default ./mibook/configuration.nix ]; }; diff --git a/machines/mibook/environments.nix b/machines/mibook/environments.nix index 286209b..9b2ff82 100644 --- a/machines/mibook/environments.nix +++ b/machines/mibook/environments.nix @@ -10,6 +10,7 @@ _: { development.enable = true; docker.enable = true; claude-code.enable = true; + hermes-agent.enable = true; }; my.hardware = { diff --git a/machines/mibook/secrets.nix b/machines/mibook/secrets.nix index 9678642..37b50c6 100644 --- a/machines/mibook/secrets.nix +++ b/machines/mibook/secrets.nix @@ -2,11 +2,10 @@ # via machines/core/sops.nix. { ... }: { - # Pipeline smoke test — proves host-key decryption works end to end. - # Safe to delete once a real secret is in place. - sops.secrets.placeholder = { + sops.secrets.hermes-env = { + owner = "hermes"; + group = "hermes"; mode = "0400"; - owner = "root"; - group = "root"; + restartUnits = [ "hermes-agent.service" ]; # confirmed/fixed in Task 4 if the unit name differs }; } diff --git a/modules/environments/default.nix b/modules/environments/default.nix index 9158f6a..a910b44 100644 --- a/modules/environments/default.nix +++ b/modules/environments/default.nix @@ -20,5 +20,6 @@ ./jellyfin ./jellyseerr ./immich + ./hermes-agent ]; } diff --git a/modules/environments/hermes-agent/default.nix b/modules/environments/hermes-agent/default.nix new file mode 100644 index 0000000..81d2ba3 --- /dev/null +++ b/modules/environments/hermes-agent/default.nix @@ -0,0 +1,23 @@ +# Hermes Agent — Telegram + Email messaging gateway (Claude subscription auth) +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.my.profiles.hermes-agent; +in +{ + options.my.profiles.hermes-agent.enable = + lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; + + config = lib.mkIf cfg.enable { + services.hermes-agent = { + enable = true; + settings.model.default = "anthropic/claude-sonnet-4-5"; + environmentFiles = [ config.sops.secrets."hermes-env".path ]; + addToSystemPackages = true; + }; + }; +} -- 2.54.0 From ac32aca351e3aa598f9def9dc3972d570f3bdff7 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:22:09 +0200 Subject: [PATCH 09/15] docs: correct plan's sops build-check assumption Task 1's implementer found sops.validateSopsFiles makes secret-key existence a build-time check (via manifest.json.drv), not an activation-time one as the plan assumed. Corrects Step 10 to reflect reality rather than leave future readers misled. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S --- .../plans/2026-09-08-hermes-agent-mibook.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md index d9032a9..77c79d8 100644 --- a/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md +++ b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md @@ -178,11 +178,15 @@ This removes the `placeholder` secret declared here — it was an explicit throw Run: `nixfmt-rfc-style modules/environments/hermes-agent/default.nix machines/configuration.nix machines/mibook/secrets.nix machines/mibook/environments.nix flake.nix` -- [ ] **Step 10: Build-check (this is the "test")** +- [ ] **Step 10: Build-check (this is the "test", with a known-expected failure until Task 3)** Run: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'` -Expected: builds successfully. This succeeds even though `secrets/mibook.yaml` doesn't yet contain a `hermes-env` key — `sops.validateSopsFiles` only checks the file is valid sops-encrypted YAML, not that every declared secret name exists inside it; that check happens at activation (Task 4), not build. -If it fails on an unknown `services.hermes-agent.*` option, reconcile against the actual module: `nix eval --raw '.#nixosConfigurations.mibook.config.system.build.toplevel'` errors will name the bad option; cross-check against `website/docs/getting-started/nix-setup.md` in the `NousResearch/hermes-agent` repo at the locked revision. +**Correction (found during Task 1 implementation, 2026-09-08):** this does **not** build cleanly yet, and that's expected — `sops.validateSopsFiles = true` (set repo-wide in `machines/core/sops.nix`) makes secret-key existence a **build-time** dependency of `config.system.build.toplevel` (via a `manifest.json.drv` step), not an activation-time check as originally assumed here. Since `secrets/mibook.yaml` still only has the `placeholder` key at this point in the plan (Task 3 adds `hermes-env`), the build correctly fails with: +``` +> .../sops-install-secrets: manifest is not valid: secret hermes-env in .../mibook.yaml is not valid: the key 'hermes-env' cannot be found +``` +Do **not** treat this as a wiring defect, and do not weaken `sops.validateSopsFiles` to work around it — that's a repo-wide safety invariant, not this task's to change. Instead, confirm the wiring is correct by two other means: `nix build ... --dry-run` (should complete with no eval errors and the full derivation list) and Step 11 below. A fully green (non-dry-run) `nix build` of the toplevel is only reachable after Task 3 populates the real secret — treat that as the actual end-to-end checkpoint, revisited in Task 4 Step 1. +If `--dry-run` itself fails on an unknown `services.hermes-agent.*` option, reconcile against the actual module: the error will name the bad option; cross-check against `website/docs/getting-started/nix-setup.md` in the `NousResearch/hermes-agent` repo at the locked revision. - [ ] **Step 11: Confirm the option evaluates on** -- 2.54.0 From 0249aae2aed68745093cbcb864bf2d1f50c0c8f8 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:44:39 +0200 Subject: [PATCH 10/15] docs: correct spec+plan per final whole-plan review findings Final review (opus) found a Critical bug (jupiter's nixosConfigurations stopped evaluating because hermes-agent.nixosModules.default was wired through machines/configuration.nix's mibook-only module list, while the profile module itself is loaded on both machines) plus three Important gaps: secrets persist to /var/lib/hermes/.hermes/.env, not /run/secrets; Telegram/Email need explicit ALLOWED_USERS or they're wide open / silently drop messages; addToSystemPackages's CLI needs group membership finn didn't have. All four corrected in the docs here; the actual code fix is a separate dispatched task. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S --- .../plans/2026-09-08-hermes-agent-mibook.md | 129 +++++++++++------- .../2026-09-08-hermes-agent-nixos-design.md | 98 +++++++++---- 2 files changed, 155 insertions(+), 72 deletions(-) diff --git a/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md index 77c79d8..3112a0d 100644 --- a/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md +++ b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md @@ -6,7 +6,7 @@ **Goal:** Run Hermes Agent as a hardened NixOS systemd service on mibook, reachable via Telegram and Email, authenticated to Anthropic via a Claude subscription (`claude setup-token`) rather than a pay-per-token API key. -**Architecture:** A standard `my.profiles.hermes-agent` module wraps the upstream `services.hermes-agent` module (from `NousResearch/hermes-agent`'s `nixosModules.default`), imported only into mibook's module list. Credentials (Anthropic token, Telegram bot token, email creds) live in one sops secret (`hermes-env`) consumed via `environmentFiles`. No network exposure — Telegram long-polling and Email IMAP/SMTP are both outbound-initiated from mibook. +**Architecture:** A standard `my.profiles.hermes-agent` module wraps the upstream `services.hermes-agent` module (from `NousResearch/hermes-agent`'s `nixosModules.default`), imported from inside the profile module itself so both machines can evaluate it — only mibook ever turns it on (see Task 1 Step 3). Credentials (Anthropic token, Telegram bot token, email creds, both `*_ALLOWED_USERS` access-control keys) live in one sops secret (`hermes-env`) consumed via `environmentFiles`. No network exposure — Telegram long-polling and Email IMAP/SMTP are both outbound-initiated from mibook. **Tech Stack:** NixOS (flake-parts), sops-nix, upstream `hermes-agent` flake (Tier 2/best-effort Nix support), Anthropic Claude (subscription auth). @@ -16,8 +16,11 @@ - Machine: **mibook** only. Do not enable on jupiter. - Module source: official `github:NousResearch/hermes-agent` (`nixosModules.default`). Not the community `0xrsydn/nix-hermes-agent` wrapper. -- Auth: `claude setup-token` output goes into `ANTHROPIC_TOKEN` in the `hermes-env` secret — **not** `ANTHROPIC_API_KEY`. Verify the running service actually authenticates on this var (Task 4); fall back to a plain `ANTHROPIC_API_KEY` only if it doesn't. +- Auth: `claude setup-token` output goes into `ANTHROPIC_TOKEN` in the `hermes-env` secret — **not** `ANTHROPIC_API_KEY`. Confirmed supported by the locked revision's source (Task 4 Step 2); no longer conditional. - Billing dependency: the setup-token/OAuth path only draws from **Claude Max "extra usage credits"** — confirm this pool is funded before Task 2's token generation, or auth will fail even with a valid token. +- Module wiring: the upstream `hermes-agent.nixosModules.default` import lives **inside** `modules/environments/hermes-agent/default.nix` (via the `inputs` module arg), never in `machines/configuration.nix`'s per-machine module lists — wiring it there breaks jupiter's evaluation even with the profile disabled (see Task 1 Step 3's correction). +- Access control: `TELEGRAM_ALLOWED_USERS` and `EMAIL_ALLOWED_USERS` are required in the `hermes-env` secret, not optional — see Task 2 Steps 3–4. +- Secret persistence: credentials end up in plaintext at `/var/lib/hermes/.hermes/.env` (mode 0640), not sops-nix's `/run/secrets` tmpfs — rollback (Task 5 Step 3) accounts for this. - Secret: one sops secret named `hermes-env`, a full env-file blob (matches `environmentFiles` — list of paths, not a single-value secret). - No network exposure: do not touch `modules/services/webserver` or open any inbound port. Telegram + Email are both outbound-only. - Platforms: Telegram + Email only, first pass. No Discord/Slack/etc. @@ -32,7 +35,7 @@ ## File Structure - **Modify** `flake.nix` — add the `hermes-agent` flake input. -- **Modify** `machines/configuration.nix` — destructure the new input; import `hermes-agent.nixosModules.default` into mibook's module list only. +- **Not modified** `machines/configuration.nix` — Task 1 originally wired the upstream module import here (mibook-only); this broke jupiter's evaluation and was corrected to import from inside the profile module instead. See Task 1 Step 3. - **Create** `modules/environments/hermes-agent/default.nix` — the `my.profiles.hermes-agent` module (single responsibility: declare Hermes Agent). - **Modify** `modules/environments/default.nix` — add `./hermes-agent` to the import list. - **Modify** `machines/mibook/environments.nix` — set `hermes-agent.enable = true`. @@ -47,15 +50,15 @@ No other files change. **Files:** - Modify: `flake.nix` -- Modify: `machines/configuration.nix` - Create: `modules/environments/hermes-agent/default.nix` - Modify: `modules/environments/default.nix` - Modify: `machines/mibook/environments.nix` - Modify: `machines/mibook/secrets.nix` +- **Not modified:** `machines/configuration.nix` — see Step 3's correction. **Interfaces:** -- Produces: NixOS option `my.profiles.hermes-agent.enable` (bool). When true, configures `services.hermes-agent` with `environmentFiles = [ config.sops.secrets."hermes-env".path ]`. Declares `sops.secrets.hermes-env` (owner/group `hermes`, path resolves to `/run/secrets/hermes-env` at runtime). -- Consumes: `hermes-agent.nixosModules.default` from the new flake input; existing `sops.secrets.` mechanism from `machines/core/sops.nix` (already applied to every machine, unaffected by this task). +- Produces: NixOS option `my.profiles.hermes-agent.enable` (bool). When true, configures `services.hermes-agent` with `environmentFiles = [ config.sops.secrets."hermes-env".path ]` and grants `finn` the `hermes` group. Declares `sops.secrets.hermes-env` (owner/group `hermes`, path resolves to `/run/secrets/hermes-env` at runtime — but note the *service* re-copies it to `/var/lib/hermes/.hermes/.env`, per the spec's persistence note). +- Consumes: `hermes-agent.nixosModules.default` from the new flake input, imported directly inside the profile module via the `inputs` module arg (see Step 3) — not through `machines/configuration.nix`; existing `sops.secrets.` mechanism from `machines/core/sops.nix` (already applied to every machine, unaffected by this task). - [ ] **Step 1: Add the flake input** @@ -73,34 +76,24 @@ In `flake.nix`, inside `inputs = { ... }`, add after the `sops-nix` block: Run: `nix flake lock` Expected: `flake.lock` gains a `hermes-agent` entry (and its transitive inputs). No other entries change. -- [ ] **Step 3: Import the upstream module into mibook only** +- [ ] **Step 3: Do NOT wire the module import through `machines/configuration.nix`** -In `machines/configuration.nix`, add `hermes-agent` to the `inherit (self.inputs) ...` destructure: +**Correction (found during final whole-plan review, 2026-09-08):** an +earlier version of this step added `hermes-agent` to the +`inherit (self.inputs) ...` destructure and `hermes-agent.nixosModules.default` +to mibook's `modules` list in `machines/configuration.nix`. That broke +`nixosConfigurations.jupiter` evaluation — confirmed by direct `nix eval`. +`modules/environments/default.nix` (Step 6) is machine-agnostic, so jupiter +loads the profile module too; NixOS's module system errors on a definition +for an undeclared option **regardless of `lib.mkIf`'s condition**. Since +jupiter never imported `hermes-agent.nixosModules.default`, it never +declared `services.hermes-agent` as an option at all, so even the disabled +(`mkIf false`) config definition in Step 5 crashed evaluation. -```nix - inherit (self.inputs) - nixpkgs - nixpkgs-unstable - nixos-hardware - sops-nix - hermes-agent - #hyprland - ; -``` - -Then change mibook's `modules` list (leave jupiter's untouched): - -```nix - mibook = nixosSystem { - system = "x86_64-linux"; - specialArgs = { inherit self; }; - modules = defaultModules ++ [ - # nixos-hardware.nixosModules.mibook - hermes-agent.nixosModules.default - ./mibook/configuration.nix - ]; - }; -``` +Skip this step — do not touch `machines/configuration.nix` at all. The fix +lives in Step 5: the upstream module is imported from inside the profile +module itself, which works correctly on both machines (declares the option +everywhere, only ever turns it on where `my.profiles.hermes-agent.enable = true`). - [ ] **Step 4: Read a reference module to match repo style** @@ -116,25 +109,40 @@ Create `modules/environments/hermes-agent/default.nix`: config, lib, pkgs, + inputs, ... }: let cfg = config.my.profiles.hermes-agent; in { - options.my.profiles.hermes-agent.enable = lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; + imports = [ inputs.hermes-agent.nixosModules.default ]; + + options.my.profiles.hermes-agent.enable = + lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; config = lib.mkIf cfg.enable { services.hermes-agent = { enable = true; + # Deliberate pin, matches the approved design — bump on purpose, not by drift. settings.model.default = "anthropic/claude-sonnet-4-5"; environmentFiles = [ config.sops.secrets."hermes-env".path ]; addToSystemPackages = true; }; + + # addToSystemPackages only puts `hermes` on $PATH; using it needs group + # access to /var/lib/hermes/.hermes (mode 2770 hermes:hermes). finn is + # this machine's one human user and already knows hermes-env's contents + # (typed them into sops directly), so this is not new exposure. + users.users.finn.extraGroups = [ "hermes" ]; }; } ``` +The `inputs` argument is available automatically — every module in this +repo gets it via `_module.args.inputs = self.inputs;` in +`machines/configuration.nix`, no extra wiring needed. + - [ ] **Step 6: Register the module in the environments import list** In `modules/environments/default.nix`, add `./hermes-agent` after `./immich`: @@ -167,7 +175,7 @@ In `machines/mibook/secrets.nix`, replace the whole file body: owner = "hermes"; group = "hermes"; mode = "0400"; - restartUnits = [ "hermes-agent.service" ]; # confirmed/fixed in Task 4 if the unit name differs + restartUnits = [ "hermes-agent.service" ]; # confirmed against the locked revision's generated unit }; } ``` @@ -176,7 +184,7 @@ This removes the `placeholder` secret declared here — it was an explicit throw - [ ] **Step 9: Format** -Run: `nixfmt-rfc-style modules/environments/hermes-agent/default.nix machines/configuration.nix machines/mibook/secrets.nix machines/mibook/environments.nix flake.nix` +Run: `nixfmt-rfc-style modules/environments/hermes-agent/default.nix machines/mibook/secrets.nix machines/mibook/environments.nix flake.nix` - [ ] **Step 10: Build-check (this is the "test", with a known-expected failure until Task 3)** @@ -188,18 +196,35 @@ Run: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'` Do **not** treat this as a wiring defect, and do not weaken `sops.validateSopsFiles` to work around it — that's a repo-wide safety invariant, not this task's to change. Instead, confirm the wiring is correct by two other means: `nix build ... --dry-run` (should complete with no eval errors and the full derivation list) and Step 11 below. A fully green (non-dry-run) `nix build` of the toplevel is only reachable after Task 3 populates the real secret — treat that as the actual end-to-end checkpoint, revisited in Task 4 Step 1. If `--dry-run` itself fails on an unknown `services.hermes-agent.*` option, reconcile against the actual module: the error will name the bad option; cross-check against `website/docs/getting-started/nix-setup.md` in the `NousResearch/hermes-agent` repo at the locked revision. -- [ ] **Step 11: Confirm the option evaluates on** +- [ ] **Step 11: Build-check jupiter too — this is the regression gate** + +Run: `nix eval '.#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath'` +Expected: succeeds, printing a `.drv` path. **Any change under `modules/` is a two-machine change even when it's "for one host"** — `modules/environments/default.nix` (Step 6) is loaded by every machine, so a mistake here can break jupiter's eval even though jupiter never enables this profile. This is exactly the failure Step 3's correction fixes; this step is what catches it if it ever regresses. + +- [ ] **Step 12: Confirm the option evaluates on mibook** Run: `nix eval '.#nixosConfigurations.mibook.config.services.hermes-agent.enable'` Expected: `true` -- [ ] **Step 12: Commit** +- [ ] **Step 13: Commit** ```bash -git add flake.nix flake.lock machines/configuration.nix modules/environments/hermes-agent/default.nix modules/environments/default.nix machines/mibook/environments.nix machines/mibook/secrets.nix +git add flake.nix flake.lock modules/environments/hermes-agent/default.nix modules/environments/default.nix machines/mibook/environments.nix machines/mibook/secrets.nix git commit -m "feat(mibook): add Hermes Agent profile module" ``` +**Post-review note, 2026-09-08:** Task 1 was originally committed as +`731678e` with the jupiter-breaking bug Step 3 describes and fixes — the +upstream module was wired through `machines/configuration.nix`'s +mibook-only module list instead of imported inside the profile module. The +final whole-plan review caught it (jupiter's `nixosConfigurations` stopped +evaluating, breaking the repo's sole CI job even though jupiter never +enables this profile). It was corrected in a follow-up commit on the same +branch — run `git log --oneline -- modules/environments/hermes-agent/ machines/configuration.nix` +if you need both commit hashes. The steps above already reflect the +corrected, final approach — executing this plan from scratch would never +hit the bug. + --- ### Task 2: Gather credentials (operator-run, not agent-automatable) @@ -218,13 +243,13 @@ In the Anthropic console, confirm the Claude Max plan has **extra usage credits On mibook, run: `claude setup-token` This opens an interactive OAuth flow. On completion it prints a long-lived token. Copy it somewhere only you control (e.g. your password manager) — you'll type it into `sops` in Task 3, not into this session. -- [ ] **Step 3: Create the Telegram bot** +- [ ] **Step 3: Create the Telegram bot, and get your own user ID** -In Telegram, message `@BotFather`, run `/newbot`, follow the prompts. Copy the resulting bot token (format `123456:ABC-...`). +In Telegram, message `@BotFather`, run `/newbot`, follow the prompts. Copy the resulting bot token (format `123456:ABC-...`). Also get your own numeric Telegram user ID (e.g. via `@userinfobot`) — Task 3 needs it for `TELEGRAM_ALLOWED_USERS`. Telegram defaults to open DM pairing (any stranger who messages the bot gets a pairing code and can request approval); without this allowlist, that's the default posture, not a hardened one. - [ ] **Step 4: Gather email credentials** -Decide which mailbox Hermes Agent should use, and get its IMAP/SMTP host, username, and password (or app-specific password if the provider requires one, e.g. Gmail). Check `website/docs/integrations/` in the `NousResearch/hermes-agent` repo for the exact env var names the email integration expects (this plan doesn't guess them — confirm the real key names here, not at Task 3, so Task 3 is a straight copy-paste). +Decide which mailbox Hermes Agent should use, and get its IMAP/SMTP host, username, and password (or app-specific password if the provider requires one, e.g. Gmail). The exact env var names (confirmed against the locked `hermes-agent` revision, not guessed) are `EMAIL_ADDRESS`, `EMAIL_PASSWORD`, `EMAIL_IMAP_HOST`, `EMAIL_SMTP_HOST`, plus optional `EMAIL_IMAP_PORT` (default 993) / `EMAIL_SMTP_PORT` (default 587) if your provider differs — see `website/docs/user-guide/messaging/email.md` and `website/docs/reference/environment-variables.md` in the `NousResearch/hermes-agent` repo for more detail. Also decide `EMAIL_ALLOWED_USERS` (your own sending address) — without it, mail from unrecognized senders is **silently ignored** with no log line, which Task 4 Step 4 depends on knowing. --- @@ -250,10 +275,15 @@ Delete the `placeholder: ...` entry (Task 1 already removed its Nix declaration; hermes-env: | ANTHROPIC_TOKEN= TELEGRAM_BOT_TOKEN= - + TELEGRAM_ALLOWED_USERS= + EMAIL_ADDRESS= + EMAIL_PASSWORD= + EMAIL_IMAP_HOST= + EMAIL_SMTP_HOST= + EMAIL_ALLOWED_USERS= ``` -(Exact YAML block-scalar syntax per whatever your `$EDITOR` shows — sops re-encrypts on save regardless of formatting, as long as it's valid YAML.) +(Exact YAML block-scalar syntax per whatever your `$EDITOR` shows — sops re-encrypts on save regardless of formatting, as long as it's valid YAML. Both `*_ALLOWED_USERS` keys are access control, not optional — see Task 2 Steps 3–4 for why.) - [ ] **Step 3: Save and let sops re-encrypt** @@ -286,11 +316,10 @@ git commit -m "feat(mibook): populate Hermes Agent credentials" Run: `sudo nixos-rebuild test --flake '.#mibook'` Expected: activates without reboot-persisting. If it fails at secret decryption ("hermes-env not found in sops file"), Task 3 wasn't saved/staged correctly — redo Task 3. -- [ ] **Step 2: Confirm the unit name and watch it start** +- [ ] **Step 2: Watch the service start** -Run: `systemctl list-units | grep -i hermes` to get the exact unit name (Task 1's `restartUnits = [ "hermes-agent.service" ]` was a guess — fix `machines/mibook/secrets.nix` and re-run `nixos-rebuild test` if it differs). -Then: `journalctl -u hermes-agent.service -f` -Expected: starts, reads the env file, and does **not** crash-loop on auth. If it errors specifically on `ANTHROPIC_TOKEN` not being recognized, that's the documented-but-unverified case from the spec — try renaming the key to `ANTHROPIC_API_KEY` in Task 3's secret (note: this switches to pay-per-token billing, defeating the point of the subscription path — treat as a fallback, not silently accept it). +`hermes-agent.service` is the confirmed unit name (verified against the locked revision's generated unit, not a guess). Run: `journalctl -u hermes-agent.service -f` +Expected: starts, reads the env file (lands at `/var/lib/hermes/.hermes/.env` — see spec Part 2), and does **not** crash-loop on auth. `ANTHROPIC_TOKEN` is confirmed supported by the module's credential resolver, so an auth failure here points at the token itself (expired, or the extra-credits pool from Task 2 Step 1 isn't actually funded) rather than the module rejecting the variable name. - [ ] **Step 3: Verify Telegram connects** @@ -324,7 +353,7 @@ Use it for a few days — send yourself Telegram/Email messages periodically, in - [ ] **Step 3: Rollback (if needed)** -`my.profiles.hermes-agent.enable = false` in `machines/mibook/environments.nix`, `sudo nixos-rebuild switch --flake '.#mibook'`. No data migration is involved — this is a clean revert, not a runbook. +`my.profiles.hermes-agent.enable = false` in `machines/mibook/environments.nix`, `sudo nixos-rebuild switch --flake '.#mibook'` stops the service — but the plaintext credential file survives on disk (see spec's Rollback section for why). Also run `sudo rm -rf /var/lib/hermes` and revoke the Telegram bot token via `@BotFather` → `/revoke`. No data migration is involved, so this is still cheap, just not a single command. --- @@ -344,4 +373,6 @@ Use it for a few days — send yourself Telegram/Email messages periodically, in **Placeholder scan:** No bare TBD/TODO. The bracketed values (``, etc.) are operator-supplied secret material by design — Task 2 explicitly produces them, Task 3 explicitly consumes them; they're never meant to be concrete in this document since they're credentials, not code. -**Type consistency:** `my.profiles.hermes-agent.enable`, `services.hermes-agent.*`, and the `hermes-env` secret name are used identically across Tasks 1, 3, and 4. The `hermes-agent.service` unit name is flagged as unverified in both Task 1 (declaration) and Task 4 (where it's actually checked and corrected if wrong) — consistent, not contradictory. +**Type consistency:** `my.profiles.hermes-agent.enable`, `services.hermes-agent.*`, and the `hermes-env` secret name are used identically across Tasks 1, 3, and 4. The `hermes-agent.service` unit name and `ANTHROPIC_TOKEN` var are confirmed (not guessed) consistently in both Task 1's declaration and Task 4's verification. + +**Post-final-review addendum (2026-09-08):** the final whole-plan review found the module-wiring bug described in Task 1 Step 3, plus three access/persistence gaps now folded into Global Constraints, Task 2 Steps 3–4, Task 3's template, and Task 5 Step 3's rollback (TELEGRAM_ALLOWED_USERS/EMAIL_ALLOWED_USERS required; secrets persist to `/var/lib/hermes/.hermes/.env`, not `/run/secrets`; `hermes` CLI needs `finn` in the `hermes` group to be usable). All four are corrected in place above, not left as separate addenda, so the plan reads as a single coherent document rather than a stack of patches. diff --git a/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md b/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md index 7f3dfee..d7a698a 100644 --- a/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md +++ b/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md @@ -47,44 +47,70 @@ hermes-agent = { }; ``` -Import the upstream module **only into mibook's module list**, not -`baseModules` — this is a single-host service, unlike `sops-nix.nixosModules.sops` -which every machine needs. In `machines/configuration.nix`, mibook's -`modules = defaultModules ++ [ hermes-agent.nixosModules.default ./mibook/configuration.nix ]` -(exact insertion point to be confirmed against the file at implementation -time — `defaultModules` is currently machine-agnostic, so this is a targeted, -minimal deviation from that shared list, not a restructuring of it). +Import the upstream module **from inside the profile module itself**, via +`imports = [ inputs.hermes-agent.nixosModules.default ];` — using the +`inputs` module arg already provided repo-wide by +`_module.args.inputs = self.inputs;` in `machines/configuration.nix`. +**Correction (found during implementation, 2026-09-08):** this is +deliberately *not* wired through `machines/configuration.nix`'s per-machine +module lists. NixOS's module system errors on a definition for an +undeclared option regardless of `lib.mkIf`'s condition — so declaring +`services.hermes-agent` only on mibook while jupiter still loads the +profile module (because `modules/environments/default.nix` is +machine-agnostic) breaks jupiter's evaluation outright, even with the +profile disabled there. Importing the upstream module unconditionally, and +gating only `config` behind `lib.mkIf cfg.enable`, is what actually +respects "mibook only, not jupiter": the option exists everywhere +(harmless), but only ever turns on where `enable = true`. New file `modules/environments/hermes-agent/default.nix` following the repo's standard profile pattern: ```nix -{ config, lib, pkgs, ... }: +# Hermes Agent — Telegram + Email messaging gateway (Claude subscription auth) +{ + config, + lib, + pkgs, + inputs, + ... +}: let cfg = config.my.profiles.hermes-agent; in { - options.my.profiles.hermes-agent.enable = lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; + imports = [ inputs.hermes-agent.nixosModules.default ]; + + options.my.profiles.hermes-agent.enable = + lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; config = lib.mkIf cfg.enable { services.hermes-agent = { enable = true; - settings.model.default = "anthropic/claude-sonnet-4-5"; # confirm exact model string at implementation + settings.model.default = "anthropic/claude-sonnet-4-5"; # deliberate pin, see Open risks environmentFiles = [ config.sops.secrets."hermes-env".path ]; addToSystemPackages = true; }; + + # addToSystemPackages only puts `hermes` on $PATH; using it needs group + # access to /var/lib/hermes/.hermes (mode 2770 hermes:hermes). finn is + # this machine's one human user and already knows hermes-env's contents + # (typed them into sops directly), so this is not new exposure. + users.users.finn.extraGroups = [ "hermes" ]; }; } ``` Add `./environments/hermes-agent` to `modules/environments/default.nix`; enable `my.profiles.hermes-agent.enable = true` in -`machines/mibook/environments.nix`. +`machines/mibook/environments.nix`. `machines/configuration.nix` is **not** +touched by this design. **Provided for free by the upstream module:** `hermes` system user/group (`createUser` default true), hardened systemd unit (`NoNewPrivileges`, `ProtectSystem=strict`, `PrivateTmp` — not independently user-configurable -per the docs), `hermes` CLI on `$PATH` via `addToSystemPackages`. +per the docs), `hermes` CLI on `$PATH` via `addToSystemPackages` (usable by +finn once group membership is granted, above). ## Part 2 — Secret @@ -105,15 +131,36 @@ multi-line `hermes-env` block: ``` ANTHROPIC_TOKEN= TELEGRAM_BOT_TOKEN= - +TELEGRAM_ALLOWED_USERS= +EMAIL_ADDRESS= +EMAIL_PASSWORD= +EMAIL_IMAP_HOST= +EMAIL_SMTP_HOST= +EMAIL_ALLOWED_USERS= ``` +(`EMAIL_IMAP_PORT`/`EMAIL_SMTP_PORT` default to 993/587 — only set if the +provider differs. See `website/docs/reference/environment-variables.md` for +the full key list.) -**Verify at implementation, not assumed:** the module's own documented -example uses `ANTHROPIC_API_KEY`; the setup-token path is documented -separately (and thinly) as `ANTHROPIC_TOKEN`. Confirm the running module -actually picks up `ANTHROPIC_TOKEN` before treating auth as working — -if it doesn't, the fallback is a plain `ANTHROPIC_API_KEY` (pay-per-token, -loses the subscription framing this design was built around). +Both `*_ALLOWED_USERS` keys are access control, not optional: Telegram +defaults to open DM pairing (any stranger who messages the bot gets a +pairing code and can request approval), and email with no +`EMAIL_ALLOWED_USERS` set **silently ignores** messages from unrecognized +senders — no log line, no error. For an agent with terminal access on the +daily driver, treat both as required. + +**Resolved (confirmed against the locked `hermes-agent` revision's source — +`agent/anthropic_credentials.py`, `hermes_cli/providers.py`):** the module +reads `ANTHROPIC_TOKEN` for the setup-token path. No longer an open +question; see Open risks below for what replaced it. + +**Where this actually lands at runtime:** the upstream module does not use +systemd `EnvironmentFile=`. An activation script concatenates every +`environmentFiles` entry into **`/var/lib/hermes/.hermes/.env`**, mode +`0640`, owned `hermes:hermes` — plaintext on mibook's plain ext4 root (no +LUKS, per `machines/mibook/disks.nix`), not sops-nix's `/run/secrets` +tmpfs. This is upstream behavior, not something this design controls; see +Rollback below for why it matters. **Must `git add secrets/mibook.yaml` before `nix build`** — untracked files are invisible to flake evaluation (existing repo rule, `docs/secrets.md`). @@ -135,13 +182,18 @@ files are invisible to flake evaluation (existing repo rule, `docs/secrets.md`). 11. `sudo nixos-rebuild switch --flake '.#mibook'` once verified. ### Rollback -`my.profiles.hermes-agent.enable = false`, `nixos-rebuild switch`. No data -migration involved (no existing deployment to preserve), so rollback is a -one-line revert, not a runbook. +`my.profiles.hermes-agent.enable = false`, `nixos-rebuild switch` stops the +service, but does **not** clean up `/var/lib/hermes` — the plaintext +`.env` (Anthropic token, Telegram token, email password) stays on disk +indefinitely, per the persistence note in Part 2. A real rollback is: +`sudo rm -rf /var/lib/hermes`, plus revoking the Telegram bot token +(`@BotFather` → `/revoke`) since it can't be un-leaked once exposed. No +data migration is involved, so this is still cheap — just not one command. ## Open risks (carried forward, not resolved by design) -1. **`ANTHROPIC_TOKEN` vs `ANTHROPIC_API_KEY`** — thin upstream docs, needs a real test (Part 2). +1. ~~`ANTHROPIC_TOKEN` vs `ANTHROPIC_API_KEY`~~ — **Resolved**: confirmed supported by reading the locked `hermes-agent` revision's source (Part 2). 2. **Extra-credits billing pool funding** — not independently verified, user-asserted intent (Part 3, step 1). 3. **Tier 2 Nix support** — expect some manual troubleshooting; upstream doesn't guarantee the flake stays working across their commits. 4. **Availability gap** — accepted by design, not a bug to fix later unless priorities change. +5. **`ANTHROPIC_TOKEN` cannot refresh unattended.** The setup-token is a terminal value to the module's credential resolver; the refresh path needs a `~/.claude/.credentials.json` with a refresh token that the `hermes` service user won't have. When the token expires, the gateway silently stops authenticating and the operator has to re-run `claude setup-token` and re-populate the secret by hand — watch for this during the confidence window, not something this design solves. -- 2.54.0 From 31e1910d75ac3b6e729ae451a6b470e57c5719a7 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:50:11 +0200 Subject: [PATCH 11/15] fix(mibook): import hermes-agent module from the profile, not machines/configuration.nix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit machines/configuration.nix wired hermes-agent.nixosModules.default into mibook's mibook-only modules list. modules/environments/default.nix is machine-agnostic, so jupiter still evaluated a services.hermes-agent = lib.mkIf false { ... } definition for an option it never declared as existing — NixOS errors on that regardless of mkIf's condition, breaking nixosConfigurations.jupiter (and the repo's sole CI job) entirely. Fix: import the upstream module unconditionally from inside modules/environments/hermes-agent/default.nix itself, so the option is declared everywhere modules/environments/default.nix loads, and only ever turned on where my.profiles.hermes-agent.enable = true (mibook). Also adds users.users.finn.extraGroups = [ "hermes" ], required for the hermes CLI to be usable per the corrected design. machines/configuration.nix no longer mentions hermes-agent. Deviates from the reconciled spec/plan text in one respect: uses `self` (from specialArgs) instead of `inputs` (from _module.args) to reach the flake input inside `imports`. Using `inputs` there reproduced infinite recursion — NixOS resolves a module's imports before _module.args assignments are merged, so an import list cannot depend on an arg that itself comes from config. `self` is available at import-time via specialArgs, `inputs` is not, in this repo's wiring. See report for detail. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S --- machines/configuration.nix | 2 -- modules/environments/hermes-agent/default.nix | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/machines/configuration.nix b/machines/configuration.nix index 731ba72..eee3f25 100644 --- a/machines/configuration.nix +++ b/machines/configuration.nix @@ -5,7 +5,6 @@ let nixpkgs-unstable nixos-hardware sops-nix - hermes-agent #hyprland ; nixosSystem = nixpkgs.lib.makeOverridable nixpkgs.lib.nixosSystem; @@ -63,7 +62,6 @@ in specialArgs = { inherit self; }; modules = defaultModules ++ [ # nixos-hardware.nixosModules.mibook - hermes-agent.nixosModules.default ./mibook/configuration.nix ]; }; diff --git a/modules/environments/hermes-agent/default.nix b/modules/environments/hermes-agent/default.nix index 81d2ba3..89c3dd1 100644 --- a/modules/environments/hermes-agent/default.nix +++ b/modules/environments/hermes-agent/default.nix @@ -3,21 +3,31 @@ config, lib, pkgs, + self, ... }: let cfg = config.my.profiles.hermes-agent; in { + imports = [ self.inputs.hermes-agent.nixosModules.default ]; + options.my.profiles.hermes-agent.enable = lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; config = lib.mkIf cfg.enable { services.hermes-agent = { enable = true; + # Deliberate pin, matches the approved design — bump on purpose, not by drift. settings.model.default = "anthropic/claude-sonnet-4-5"; environmentFiles = [ config.sops.secrets."hermes-env".path ]; addToSystemPackages = true; }; + + # addToSystemPackages only puts `hermes` on $PATH; using it needs group + # access to /var/lib/hermes/.hermes (mode 2770 hermes:hermes). finn is + # this machine's one human user and already knows hermes-env's contents + # (typed them into sops directly), so this is not new exposure. + users.users.finn.extraGroups = [ "hermes" ]; }; } -- 2.54.0 From 0756eb94cd936a1bc7af1e9f2af155f652d91921 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:51:31 +0200 Subject: [PATCH 12/15] docs: correct spec+plan for self-vs-inputs in imports The fix subagent found that inputs.hermes-agent.nixosModules.default inside `imports` causes infinite recursion in this repo's wiring (inputs comes from _module.args, which merges after imports resolve; self comes from specialArgs, which is available at import-time). Tested both ways. Update docs to show self.inputs, matching commit 31e1910. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S --- .../plans/2026-09-08-hermes-agent-mibook.md | 18 ++++++++++++------ .../2026-09-08-hermes-agent-nixos-design.md | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md index 3112a0d..23094df 100644 --- a/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md +++ b/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md @@ -58,7 +58,7 @@ No other files change. **Interfaces:** - Produces: NixOS option `my.profiles.hermes-agent.enable` (bool). When true, configures `services.hermes-agent` with `environmentFiles = [ config.sops.secrets."hermes-env".path ]` and grants `finn` the `hermes` group. Declares `sops.secrets.hermes-env` (owner/group `hermes`, path resolves to `/run/secrets/hermes-env` at runtime — but note the *service* re-copies it to `/var/lib/hermes/.hermes/.env`, per the spec's persistence note). -- Consumes: `hermes-agent.nixosModules.default` from the new flake input, imported directly inside the profile module via the `inputs` module arg (see Step 3) — not through `machines/configuration.nix`; existing `sops.secrets.` mechanism from `machines/core/sops.nix` (already applied to every machine, unaffected by this task). +- Consumes: `hermes-agent.nixosModules.default` from the new flake input, imported directly inside the profile module via `self.inputs` (the `self` module arg, from `specialArgs` — see Step 3 and Step 5's note on why not the `inputs` module arg) — not through `machines/configuration.nix`; existing `sops.secrets.` mechanism from `machines/core/sops.nix` (already applied to every machine, unaffected by this task). - [ ] **Step 1: Add the flake input** @@ -109,14 +109,14 @@ Create `modules/environments/hermes-agent/default.nix`: config, lib, pkgs, - inputs, + self, ... }: let cfg = config.my.profiles.hermes-agent; in { - imports = [ inputs.hermes-agent.nixosModules.default ]; + imports = [ self.inputs.hermes-agent.nixosModules.default ]; options.my.profiles.hermes-agent.enable = lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; @@ -139,9 +139,15 @@ in } ``` -The `inputs` argument is available automatically — every module in this -repo gets it via `_module.args.inputs = self.inputs;` in -`machines/configuration.nix`, no extra wiring needed. +**Use `self`, not `inputs`, in the `imports` line.** `inputs` is supplied +via `_module.args.inputs = self.inputs;`, set inside a config block — but +NixOS resolves a module's `imports` before `_module.args` assignments +merge, so anything referenced inside `imports` must come from +`specialArgs` (fixed up front) instead. `self` is threaded via +`specialArgs = { inherit self; };` in `machines/configuration.nix` and is +available at import-time; `inputs` is not. Using `inputs` here reproduces +`infinite recursion encountered` — confirmed by testing it directly, not +theoretical. `self.inputs.hermes-agent` is the same flake input either way. - [ ] **Step 6: Register the module in the environments import list** diff --git a/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md b/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md index d7a698a..976e329 100644 --- a/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md +++ b/docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md @@ -48,10 +48,16 @@ hermes-agent = { ``` Import the upstream module **from inside the profile module itself**, via -`imports = [ inputs.hermes-agent.nixosModules.default ];` — using the -`inputs` module arg already provided repo-wide by -`_module.args.inputs = self.inputs;` in `machines/configuration.nix`. -**Correction (found during implementation, 2026-09-08):** this is +`imports = [ self.inputs.hermes-agent.nixosModules.default ];` — using +`self` (available via `specialArgs = { inherit self; };` in +`machines/configuration.nix`), not the `inputs` module arg. **Correction +(found during implementation, 2026-09-08):** `inputs` is supplied via +`_module.args.inputs = self.inputs;`, set inside a config block — but +NixOS resolves a module's `imports` list before `_module.args` assignments +merge, so anything referenced inside `imports` must come from +`specialArgs` (fixed up front) instead. Using `inputs` here reproduces +`infinite recursion encountered`; `self.inputs.hermes-agent` is the same +flake input, reached the way that actually works. Separately, this is deliberately *not* wired through `machines/configuration.nix`'s per-machine module lists. NixOS's module system errors on a definition for an undeclared option regardless of `lib.mkIf`'s condition — so declaring @@ -72,14 +78,14 @@ repo's standard profile pattern: config, lib, pkgs, - inputs, + self, ... }: let cfg = config.my.profiles.hermes-agent; in { - imports = [ inputs.hermes-agent.nixosModules.default ]; + imports = [ self.inputs.hermes-agent.nixosModules.default ]; options.my.profiles.hermes-agent.enable = lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)"; -- 2.54.0 From c4b29e1e06a7d55a786f01c8668f3ff91e3fa3f4 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 21:10:28 +0200 Subject: [PATCH 13/15] feat(mibook): populate Hermes Agent credentials --- secrets/mibook.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/secrets/mibook.yaml b/secrets/mibook.yaml index 44a9b20..266b164 100644 --- a/secrets/mibook.yaml +++ b/secrets/mibook.yaml @@ -1,4 +1,4 @@ -placeholder: ENC[AES256_GCM,data:tji/6di1F2GsD60TEBGAVvZR9m4jxQ8=,iv:BoBQVR735vq38iGx40VHIpk185ylf79mKmnpewcq/+o=,tag:lXNNk4/HYB0sw44KaQ2KYg==,type:str] +hermes-env: ENC[AES256_GCM,data:BRGzY+9FGyDyUM5yZzG8IrhqUnW3qH1Tfb41atr3JlaVp+qgdbNWfaajRrjGV/bMrLCO5X1JGavnjH1tFxGyNKT2iUUI70s49fyvVyQR5spKKdU2HlxEkAp6w/J9imoFWrfotHi40ZGtZW1vZ4FX6GubsWrAfKHKnw0EI61IXLIFpdspWd6LSVPH159tCl1iTXRxYXLj5HArD4gIP9MSo+LDTZqIAgaByLD0rnSPIL8YSGBsjFFhw8jnvgowIPyZJbcMuwUeXsVvEZ65U4F6GtWcyHFuBGd2VbCBjvPdKPh+08pvhe5hef+JaHIjzslBasiGowZe03yFF6z0RL4fK8SQ6T1tYNRS2//fwC0RMIweZkldojEnprMl7W5CFHRtdG4UNnA0ChVHllGHe8FgKlyv9ouf/fGqY0jNkaqArYJHH1lRpe+9gLbBNwj8HAlCi7p7G6A0rrfRQUx7pnYrCnMUcWpfuPX1FGbHkP2t5yN07g66WU01ZMz5A9EwEMJGzNilHHiGLd8athrjRM2/wEg7y0apqhInyyCsCfbrmQBa,iv:zrVf9D7EDf21QsfXUfU54DUfkvoxKvQsEOF/qA4lcG4=,tag:x4j+o+ArJo8sEoI/p9Kq7A==,type:str] sops: age: - enc: | @@ -19,7 +19,7 @@ sops: RTmhK6mw2wd8oUMdgPOUESNwh1xkCz4hDHPrDQnfd4sPhSAvh28jwg== -----END AGE ENCRYPTED FILE----- recipient: age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p - lastmodified: "2026-09-08T13:29:12Z" - mac: ENC[AES256_GCM,data:YjMMIUdfCl8dkuvtvBZTFIhLwsUjI6iSeN929O4igbwpRTyGVxgLmAMl81YYHJSyEibelT7ZBKiuVVnL2KjLvAKTj3H1O+AQrcAEJDPC9NwPJwbtAIpLl2li5060Kdj4kr8AY+UJ2kHycuS9dg6wPZTLBJcM/YCw0AoVftvzryM=,iv:GEQZF9RvW+MJlMIjn1ToD74RubWMQtCyE5LqW9KQQfA=,tag:NR6u88eBY1asQZM6f45hTw==,type:str] + lastmodified: "2026-09-08T19:09:49Z" + mac: ENC[AES256_GCM,data:XByuffus4RuTZx2/FPfc/7UedefrFZnRTCtjC3AficDYVTLRHaL17hl6B8C9wZETOuvrzSGSDsedfTvPKQEwiujcwj0WHBuXzx/75pbFLpaITbDB+GXBYsB0fJqeVRi4aRMAgq3+698wuFnJ2CVvZ/IPvxcLCt0hL1UhUCcSwHM=,iv:tmNA+cMmrPu5hAbDh2Z6LGYt0PoP+cLOYquYA/2rBG4=,tag:jmMi+33sw93Wo64PFfkZCw==,type:str] unencrypted_suffix: _unencrypted version: 3.13.3 -- 2.54.0 From 67973559c0b1d4b01183a626b9df3e55837d6b00 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Thu, 10 Sep 2026 10:56:50 +0200 Subject: [PATCH 14/15] feat(hermes-agent): expose dashboard on LAN with basic auth Bind the backend to 0.0.0.0:9119 in dashboard mode so the web UI is reachable from the local network, open the port in the firewall, and add dashboard username/password credentials to the mibook hermes-env secret. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Sux8hmeb2qadqPqbJ1zNzL --- modules/environments/hermes-agent/default.nix | 12 ++++++++++++ secrets/mibook.yaml | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/modules/environments/hermes-agent/default.nix b/modules/environments/hermes-agent/default.nix index 89c3dd1..82a2386 100644 --- a/modules/environments/hermes-agent/default.nix +++ b/modules/environments/hermes-agent/default.nix @@ -22,8 +22,20 @@ in settings.model.default = "anthropic/claude-sonnet-4-5"; environmentFiles = [ config.sops.secrets."hermes-env".path ]; addToSystemPackages = true; + + backend = { + # "dashboard" serves the browser admin panel, not just the API. + mode = "dashboard"; + # Non-loopback bind opens the dashboard's auth gate — username/password + # comes from HERMES_DASHBOARD_BASIC_AUTH_* in hermes-env (see secrets.nix). + host = "0.0.0.0"; + port = 9119; + }; }; + # NixOS default-deny firewall otherwise blocks inbound LAN connections. + networking.firewall.allowedTCPPorts = [ config.services.hermes-agent.backend.port ]; + # addToSystemPackages only puts `hermes` on $PATH; using it needs group # access to /var/lib/hermes/.hermes (mode 2770 hermes:hermes). finn is # this machine's one human user and already knows hermes-env's contents diff --git a/secrets/mibook.yaml b/secrets/mibook.yaml index 266b164..af7d70b 100644 --- a/secrets/mibook.yaml +++ b/secrets/mibook.yaml @@ -1,4 +1,4 @@ -hermes-env: ENC[AES256_GCM,data:BRGzY+9FGyDyUM5yZzG8IrhqUnW3qH1Tfb41atr3JlaVp+qgdbNWfaajRrjGV/bMrLCO5X1JGavnjH1tFxGyNKT2iUUI70s49fyvVyQR5spKKdU2HlxEkAp6w/J9imoFWrfotHi40ZGtZW1vZ4FX6GubsWrAfKHKnw0EI61IXLIFpdspWd6LSVPH159tCl1iTXRxYXLj5HArD4gIP9MSo+LDTZqIAgaByLD0rnSPIL8YSGBsjFFhw8jnvgowIPyZJbcMuwUeXsVvEZ65U4F6GtWcyHFuBGd2VbCBjvPdKPh+08pvhe5hef+JaHIjzslBasiGowZe03yFF6z0RL4fK8SQ6T1tYNRS2//fwC0RMIweZkldojEnprMl7W5CFHRtdG4UNnA0ChVHllGHe8FgKlyv9ouf/fGqY0jNkaqArYJHH1lRpe+9gLbBNwj8HAlCi7p7G6A0rrfRQUx7pnYrCnMUcWpfuPX1FGbHkP2t5yN07g66WU01ZMz5A9EwEMJGzNilHHiGLd8athrjRM2/wEg7y0apqhInyyCsCfbrmQBa,iv:zrVf9D7EDf21QsfXUfU54DUfkvoxKvQsEOF/qA4lcG4=,tag:x4j+o+ArJo8sEoI/p9Kq7A==,type:str] +hermes-env: ENC[AES256_GCM,data:du6bieWblEAhAYdZtCqnbNy0yWjFj13djw6co5nrNt1J8zWrmjdXo+UzvwKVpH5EMnxIEuzq9VrqsDPBLDAy1/SQRiWUuK5V7bRSVn88Hs/c7zJ1wo57i1DTVU5gwygzZHmEZbn6gKnFzNBx3aqZV+dLk3+Q4s6LpXbtRgZc63WeuMHqjlUBFeZoGIPZZ9FzqWjVx/FIgWRFm56utyBsnWhMK/2ZD2E6aOFNCZaXZp0QVXGRmiLtx7MLBY8oqJUBl16wytjOB8Y5khf5pJ5AV2NkXx9h27ycZbEQwouq1P6ty/OjQfmkZn7Jbt/oL9lG2FiOsR1x6f4bmVYrTrhjXmFoWvvDSlPJ7noBg2L+J0UrAZpDnOr2DCxZXlLnBCKaMdqvq27yTyATeWAW1BE5gCLlYnT5iE2vlr9GttY4LjfXW8HL3ieQ4bL/PB/PBZSZVKl5QrLfVrou1i8cxtM5cGfFKi9zCaDYI3G282961u1VHuVYVKZoU73WiR2BEuWqXn+1F3YOWEkOg7lM4xMoy9Q9qx7rSv3M/zOF7gjI0spCQv5RiuUOgoHbiERXgTAEfIaDRTYL30IqnHMS3O9SsTTINwDXBNk4cDxI2Ctyfy2IeAucBWAd4OhEqnji8bJ7tO4At8UIxjZ3oYGJiu0uqaDFS/Jdre4uYrSk,iv:CyO15YBh6BKNpZnsp+LAizND9rRgIjWj8SyT33Nii2c=,tag:WLlmMBIcvmYceSY4RXGUuw==,type:str] sops: age: - enc: | @@ -19,7 +19,7 @@ sops: RTmhK6mw2wd8oUMdgPOUESNwh1xkCz4hDHPrDQnfd4sPhSAvh28jwg== -----END AGE ENCRYPTED FILE----- recipient: age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p - lastmodified: "2026-09-08T19:09:49Z" - mac: ENC[AES256_GCM,data:XByuffus4RuTZx2/FPfc/7UedefrFZnRTCtjC3AficDYVTLRHaL17hl6B8C9wZETOuvrzSGSDsedfTvPKQEwiujcwj0WHBuXzx/75pbFLpaITbDB+GXBYsB0fJqeVRi4aRMAgq3+698wuFnJ2CVvZ/IPvxcLCt0hL1UhUCcSwHM=,iv:tmNA+cMmrPu5hAbDh2Z6LGYt0PoP+cLOYquYA/2rBG4=,tag:jmMi+33sw93Wo64PFfkZCw==,type:str] + lastmodified: "2026-09-10T06:28:38Z" + mac: ENC[AES256_GCM,data:t9oIZWrBtwkGyyb9hGlfWzAguI/xlqtRceTyyYE2y99xmGIl+pXHJkFF2pP9T09/NWHcXV0gRmV7LuKfF1zX1ZPNY7dba0AwVtU5ezoUdUuL2i47S7Ty1Onk9y6PqtgsRE1SkRAdzAr7s0csBzGXAn8I4z2rqZa43obRwtx7XV4=,iv:Lw6UmeeSNsfNftho423VM+htR0VzQFrCw3sDQ9OLQP0=,tag:SDQ0jJLf1jQIhBWPkm2WRg==,type:str] unencrypted_suffix: _unencrypted version: 3.13.3 -- 2.54.0 From 807eea7e4f88d30abe8b72dba04453622960f963 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Thu, 10 Sep 2026 11:09:48 +0200 Subject: [PATCH 15/15] feat(homepage): add Athena Dashboard link to jupiter's homepage Hermes Agent's dashboard runs on mibook, but the homepage-dashboard service only runs on jupiter, so wire it in as a manual cross-host entry rather than via my.homepage.services (which is scoped per-host and would be inert on mibook). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Sux8hmeb2qadqPqbJ1zNzL --- modules/environments/homepage/manual-services.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/environments/homepage/manual-services.nix b/modules/environments/homepage/manual-services.nix index d1ffb25..ee4bee8 100644 --- a/modules/environments/homepage/manual-services.nix +++ b/modules/environments/homepage/manual-services.nix @@ -2,6 +2,13 @@ # Example: { "Services" = [ + { + "Athena Dashboard" = { + href = "http://mibook:9119"; + description = "Hermes Agent messaging gateway"; + icon = "mdi-robot-outline"; + }; + } { "Tesla - Dashboard" = { href = "https://grafana.smoothcloud.de"; -- 2.54.0