Files
nixos/docs/superpowers/plans/2026-09-08-hermes-agent-mibook.md
T
finn.markwitzandClaude Sonnet 5 ac32aca351 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 17:22:09 +02:00

18 KiB
Raw Blame History

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:

    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:

  inherit (self.inputs)
    nixpkgs
    nixpkgs-unstable
    nixos-hardware
    sops-nix
    hermes-agent
    #hyprland
    ;

Then change mibook's modules list (leave jupiter's untouched):

    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:

# 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:

    ./immich
    ./hermes-agent
  ];
}
  • Step 7: Enable it on mibook

In machines/mibook/environments.nix, inside the my.profiles = { ... } block, add:

    hermes-agent.enable = true;
  • Step 8: Replace the placeholder secret declaration

In machines/mibook/secrets.nix, replace the whole file body:

# 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", with a known-expected failure until Task 3)

Run: nix build '.#nixosConfigurations.mibook.config.system.build.toplevel' 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

Run: nix eval '.#nixosConfigurations.mibook.config.services.hermes-agent.enable' Expected: true

  • Step 12: Commit
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:

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
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.