From 0249aae2aed68745093cbcb864bf2d1f50c0c8f8 Mon Sep 17 00:00:00 2001 From: "Finn@MiBook" Date: Tue, 8 Sep 2026 17:44:39 +0200 Subject: [PATCH] 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.