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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
This commit is contained in:
@@ -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=<output of `claude setup-token`>
|
||||
TELEGRAM_BOT_TOKEN=<from @BotFather>
|
||||
<IMAP/SMTP host, user, password or app-password — exact keys per Hermes' email integration doc>
|
||||
TELEGRAM_ALLOWED_USERS=<your numeric Telegram user id>
|
||||
EMAIL_ADDRESS=<mailbox address>
|
||||
EMAIL_PASSWORD=<password or app-password>
|
||||
EMAIL_IMAP_HOST=<imap host>
|
||||
EMAIL_SMTP_HOST=<smtp host>
|
||||
EMAIL_ALLOWED_USERS=<your own sending address>
|
||||
```
|
||||
(`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.
|
||||
|
||||
Reference in New Issue
Block a user