From 152c38b56bb47117109f74e2d617a351c058c18e Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Mon, 24 Aug 2026 03:10:43 +0200 Subject: [PATCH] readme: document both hermes routes and the toolset grant Fills in the subscription/wire name table for all five mappings rather than the two prose examples, and records why the routes are written as config instead of subscribed -- including that the toolset grant is deliberate but not enforced, since the file it lives in is inside HERMES_WRITE_SAFE_ROOT. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa --- README.md | 72 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 6bc7cf6..7a5964a 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,21 @@ run. Each service module opens its own firewall ports. ## Gitea events to Hermes -Jupiter's Gitea registers a webhook straight at Hermes on mars, -`http://mars.orbit.sol:8644/webhooks/gitea-pr-comments`, with no relay in -between. Gitea's `addDefaultHeaders` signs every webhook type with +Jupiter's Gitea registers one webhook per Hermes route, straight at Hermes on +mars (`http://mars.orbit.sol:8644/webhooks/`), with no relay in between: + +| route | subscribed gitea events | wakes luna on | +| --- | --- | --- | +| `gitea-pr-comments` | `pull_request_comment` | a timeline comment on a PR | +| `gitea-pr-reviews` | `pull_request_review_comment`, `pull_request_review_rejected` | a review with a body, or changes requested | + +Approvals are deliberately not subscribed: an approval is darman signing off, +not asking for work. Gitea's `addDefaultHeaders` signs every webhook type with `X-Hub-Signature-256` in GitHub's exact format and sends `X-GitHub-Event` -unconditionally — which is exactly what Hermes validates against the -subscription secret and reads the event name from, so the two speak the same -protocol without translation. The URL path is the Hermes route name, so -another subscription is just another hook. +unconditionally — which is exactly what Hermes validates against the route +secret and reads the event name from, so the two speak the same protocol +without translation. The URL path is the Hermes route name, so another route +is just another hook. Gitea will only deliver to hosts in `[security] ALLOWED_HOST_LIST`, which defaults to `external` and does NOT include tailnet addresses @@ -56,25 +63,48 @@ that, deliveries fail with `webhook can only call allowed HTTP servers`. Gitea names webhook events twice, and the two namespaces collide. The hook's `events` array takes the *subscription* name; `X-GitHub-Event`, which is what -Hermes matches `--events` against, carries a lossy *wire* name from -`HookEventType.Event()`. A comment on a PR subscribes as -`pull_request_comment` but arrives as `issue_comment`, while -`pull_request_comment` on the wire means a review submission. So -`services/dev/gitea.nix` and `hosts/mars/hermes-agent.nix` deliberately name -the same event differently; `X-GitHub-Event-Type` carries the subscription -name, but Hermes does not read it. +each Hermes route matches its `events` against, carries a lossy *wire* name +from `HookEventType.Event()`: -The route's prompt and its filter script live in `hosts/mars/`, bind-mounted -read-only from the nix store so the agent cannot edit its own loop guard out, -and are re-subscribed by `hermes-agent-webhook-route` on every start. Run -`python3 hosts/mars/gitea-pr-comment-filter-test.py` after editing the filter. +| subscription | wire | what it is | +| --- | --- | --- | +| `pull_request_comment` | `issue_comment` | comment on a PR | +| `pull_request_review_comment` | `pull_request_comment` | review with a body | +| `pull_request_review_rejected` | `pull_request_rejected` | changes requested | +| `pull_request_review_approved` | `pull_request_approved` | approval | + +So `services/dev/gitea.nix` and `hosts/mars/hermes-agent.nix` deliberately name +the same event differently, and neither is a typo. `X-GitHub-Event-Type` +carries the subscription name, but Hermes does not read it. + +Each route's prompt and filter script live in `hosts/mars/`. The filters are +bind-mounted read-only from the nix store so the agent cannot edit her own +loop guard out; run +`python3 hosts/mars/gitea-pr-comment-filter-test.py` and +`python3 hosts/mars/gitea-pr-review-filter-test.py` after editing either. + +`hermes-agent-webhook-routes` writes the routes into +`~/.hermes/webhook_subscriptions.json` directly, host-side, rather than +calling `hermes webhook subscribe`. That CLI has no `--toolsets` flag, and +without a toolset override a webhook run gets Hermes's constrained default +(`web_search`, `web_extract`, `vision_analyze`, `clarify`) — no shell, no file +access, so neither prompt can actually be carried out. Upstream's documented +answer is to add the `toolsets` key to that file by hand, which does not +survive a re-provision, so the whole route definition lives in nix instead. +The grant (`terminal`, `file`, `web`) is therefore deliberate and restored on +every start — but note it is not *enforced*: that file sits inside +`HERMES_WRITE_SAFE_ROOT`, so luna can widen her own toolset until the unit +next runs. The real backstop is gitea's branch protection on `master`. + +Routes the unit does not name are left untouched, so retiring one is a manual +`sudo podman exec hermes-agent hermes webhook remove ` on mars — and +likewise its hook in the repo's Settings → Webhooks. Before deploying either host, add the same random `gitea_hermes_webhook_secret` value to both `secrets/mars.yaml` and `secrets/jupiter.yaml` using `scripts/edit_secrets`, with no trailing newline -— the value reaches Hermes through an env-file template, where a newline both -corrupts the file and changes the key the HMAC is computed with. The value is -intentionally not included in the repository. +— a newline would change the key the HMAC is computed with, and the two ends +would disagree. The value is intentionally not included in the repository. ## Test in VirtualBox (no hardware needed)