relay: remove it; gitea already speaks Hermes's protocol
The relay existed on the premise that Gitea sends no header Hermes can read
an event name from, so something had to copy X-Gitea-Event into
X-GitHub-Event. That premise was wrong. Gitea's addDefaultHeaders sets
req.Header["X-GitHub-Delivery"] = []string{t.UUID}
req.Header["X-GitHub-Event"] = []string{event}
req.Header["X-GitHub-Event-Type"] = []string{eventType}
unconditionally, for every webhook type, alongside X-Hub-Signature-256 in
GitHub's exact format. (Direct map assignment rather than .Add() specifically
to keep the "GitHub" casing that canonicalisation would destroy.) Hermes
validates that signature on any route without provider gating and reads the
event name from that header, so gitea and hermes already speak the same
protocol and the translation layer was translating nothing.
Gitea now posts straight at http://mars.orbit.sol:8644/webhooks/gitea-pr-comments.
The URL path is the Hermes route name, so a second subscription is a second
hook and nothing else -- the route-in-path indirection the relay grew was a
reimplementation of something Hermes already had.
Removes the module, the 200-line relay, its test, the mars import, the 8645
listener, and the stale gitea-hermes-webhook-relay.service entry left in the
secret's restartUnits. hermes-agent-webhook-route moves to
hosts/mars/hermes-agent.nix, next to the container and the read-only prompt
and filter mounts it depends on.
Also makes that unit refuse to subscribe when GITEA_HERMES_WEBHOOK_SECRET is
unset in the container, matching the existing empty-prompt check. An empty
secret silently fails every delivery signature check afterwards while the
unit still reports success -- the worst possible failure shape, and one this
setup can actually produce on a first deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa
This commit is contained in:
@@ -37,58 +37,27 @@ scripts/ # deploy, edit_secrets
|
||||
Hosts compose by importing `common.nix` + whichever `services/*` modules they
|
||||
run. Each service module opens its own firewall ports.
|
||||
|
||||
## Gitea event relay
|
||||
## Gitea events to Hermes
|
||||
|
||||
Mars includes a small HMAC-validating relay for Gitea webhooks. It forwards the
|
||||
authenticated request body and Gitea's own signature to Hermes over localhost
|
||||
completely unchanged, and copies `X-Gitea-Event` into `X-GitHub-Event`. It has
|
||||
no event, repository, action, payload, or prompt policy; Hermes owns
|
||||
interpretation and response behavior. Jupiter's Gitea provisioning service
|
||||
registers the webhook idempotently at
|
||||
`http://mars.orbit.sol:8645/gitea/gitea-pr-comments`.
|
||||
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
|
||||
`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.
|
||||
|
||||
The path after `/gitea/` names the Hermes route to forward into, so the relay
|
||||
is not tied to any one subscription: another Hermes route needs a
|
||||
`hermes webhook subscribe <name>` and a Gitea hook pointing at
|
||||
`/gitea/<name>`, and no relay change. Route names are validated against a
|
||||
strict charset before being used in the outbound URL.
|
||||
Gitea will only deliver to hosts in `[security] ALLOWED_HOST_LIST`, which
|
||||
defaults to `external` and does NOT include tailnet addresses
|
||||
(100.64.0.0/10 is RFC 6598 carrier-grade NAT, neither private nor external as
|
||||
gitea classifies it). `services/dev/gitea.nix` sets it accordingly; without
|
||||
that, deliveries fail with `webhook can only call allowed HTTP servers`.
|
||||
|
||||
Neither provisioning unit deletes anything: Jupiter's only creates or updates
|
||||
its own hook, and Mars's only removes the route it is about to re-subscribe.
|
||||
Retiring the pre-rename `gitea-events` route is therefore a one-off, done by
|
||||
hand after the first deploy of both hosts:
|
||||
|
||||
```
|
||||
# on mars — drop the old subscription (`hermes` is the alias in common.nix)
|
||||
hermes webhook remove gitea-events
|
||||
|
||||
# on jupiter — delete the old hook (it posts to the relay's bare /gitea path)
|
||||
api=http://127.0.0.1:3000/api/v1; repo=darman/homelab
|
||||
auth=(-H "Authorization: token $(sudo cat /run/secrets/gitea_provisioning_token)")
|
||||
for id in $(curl -fsS "${auth[@]}" "$api/repos/$repo/hooks" \
|
||||
| jq -r '.[] | select(.config.url == "http://mars.orbit.sol:8645/gitea") | .id'); do
|
||||
curl -fsS "${auth[@]}" -X DELETE "$api/repos/$repo/hooks/$id"
|
||||
done
|
||||
```
|
||||
|
||||
Or just delete it in the web UI: repo Settings -> Webhooks, the entry whose
|
||||
URL ends in `:8645/gitea` with no route after it.
|
||||
|
||||
Check `hermes webhook list` and the repo's webhook page afterwards; until the
|
||||
old hook is gone both it and the new one fire, so events arrive twice.
|
||||
|
||||
That one header copy is the entire reason the relay exists. Gitea signs every
|
||||
webhook with `X-Hub-Signature-256` in GitHub's exact format, which Hermes
|
||||
already accepts on any route — so authentication would work pointing Gitea
|
||||
straight at Hermes on 8644. But Hermes reads the event name only from
|
||||
`X-GitHub-Event`/`X-GitLab-Event` (then `event_type`/`type` in the payload,
|
||||
then the literal `"unknown"`), and Gitea sends none of those. Without the copy
|
||||
every delivery arrives as `unknown` and `hermes webhook subscribe --events ...`
|
||||
can never match anything.
|
||||
|
||||
Run `python3 services/dev/gitea-hermes-webhook-relay-test.py` to exercise the
|
||||
relay end to end (signature acceptance and rejection, byte-identical body
|
||||
forwarding, and the event-header copy).
|
||||
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.
|
||||
|
||||
Before deploying either host, add the same random
|
||||
`gitea_hermes_webhook_secret` value to both `secrets/mars.yaml` and
|
||||
|
||||
Reference in New Issue
Block a user