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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
This commit is contained in:
2026-09-08 17:00:38 +02:00
co-authored by Claude Sonnet 5
parent 6212b4fcf7
commit a83f7e63a0
@@ -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.<name>` 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=<paste the claude setup-token output>
TELEGRAM_BOT_TOKEN=<paste the BotFather token>
<paste the email IMAP/SMTP keys gathered in Task 2, Step 4>
```
(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 (`<paste the claude setup-token output>`, 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.