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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa
This commit is contained in:
@@ -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/<route>`), 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 <name>` 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user