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
|
## Gitea events to Hermes
|
||||||
|
|
||||||
Jupiter's Gitea registers a webhook straight at Hermes on mars,
|
Jupiter's Gitea registers one webhook per Hermes route, straight at Hermes on
|
||||||
`http://mars.orbit.sol:8644/webhooks/gitea-pr-comments`, with no relay in
|
mars (`http://mars.orbit.sol:8644/webhooks/<route>`), with no relay in between:
|
||||||
between. Gitea's `addDefaultHeaders` signs every webhook type with
|
|
||||||
|
| 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`
|
`X-Hub-Signature-256` in GitHub's exact format and sends `X-GitHub-Event`
|
||||||
unconditionally — which is exactly what Hermes validates against the
|
unconditionally — which is exactly what Hermes validates against the route
|
||||||
subscription secret and reads the event name from, so the two speak the same
|
secret and reads the event name from, so the two speak the same protocol
|
||||||
protocol without translation. The URL path is the Hermes route name, so
|
without translation. The URL path is the Hermes route name, so another route
|
||||||
another subscription is just another hook.
|
is just another hook.
|
||||||
|
|
||||||
Gitea will only deliver to hosts in `[security] ALLOWED_HOST_LIST`, which
|
Gitea will only deliver to hosts in `[security] ALLOWED_HOST_LIST`, which
|
||||||
defaults to `external` and does NOT include tailnet addresses
|
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
|
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
|
`events` array takes the *subscription* name; `X-GitHub-Event`, which is what
|
||||||
Hermes matches `--events` against, carries a lossy *wire* name from
|
each Hermes route matches its `events` against, carries a lossy *wire* name
|
||||||
`HookEventType.Event()`. A comment on a PR subscribes as
|
from `HookEventType.Event()`:
|
||||||
`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.
|
|
||||||
|
|
||||||
The route's prompt and its filter script live in `hosts/mars/`, bind-mounted
|
| subscription | wire | what it is |
|
||||||
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
|
| `pull_request_comment` | `issue_comment` | comment on a PR |
|
||||||
`python3 hosts/mars/gitea-pr-comment-filter-test.py` after editing the filter.
|
| `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
|
Before deploying either host, add the same random
|
||||||
`gitea_hermes_webhook_secret` value to both `secrets/mars.yaml` and
|
`gitea_hermes_webhook_secret` value to both `secrets/mars.yaml` and
|
||||||
`secrets/jupiter.yaml` using `scripts/edit_secrets`, with no trailing newline
|
`secrets/jupiter.yaml` using `scripts/edit_secrets`, with no trailing newline
|
||||||
— the value reaches Hermes through an env-file template, where a newline both
|
— a newline would change the key the HMAC is computed with, and the two ends
|
||||||
corrupts the file and changes the key the HMAC is computed with. The value is
|
would disagree. The value is intentionally not included in the repository.
|
||||||
intentionally not included in the repository.
|
|
||||||
|
|
||||||
## Test in VirtualBox (no hardware needed)
|
## Test in VirtualBox (no hardware needed)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user