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:
2026-08-23 08:09:01 +02:00
co-authored by Claude Opus 5
parent 503623551a
commit 2a1a1628e1
8 changed files with 117 additions and 645 deletions
+12 -8
View File
@@ -21,8 +21,10 @@ let
# nothing she does lands without darman clicking merge.
lunaRepos = [ "darman/homelab" ];
# Forward every Gitea event to the generic Mars relay. Hermes owns the
# decision about which events matter and what to do with them.
# Send every Gitea event to Hermes on mars. Hermes owns the decision about
# which events matter and what to do with them: its route filters on
# X-GitHub-Event and drops the rest before any LLM call, so narrowing this
# list would only move that policy to the wrong side of the wire.
giteaWebhookEvents = [
"create"
"delete"
@@ -83,7 +85,7 @@ in
# Tailscale addresses are 100.64.0.0/10 (RFC 6598 carrier-grade NAT),
# which is neither RFC1918 private nor, as far as gitea's matcher is
# concerned, external — so the hermes relay on mars was refused with
# deny 'mars.orbit.sol(100.64.0.6:8645)'
# deny 'mars.orbit.sol(100.64.0.6:8644)'
# even though nothing here is private in the RFC1918 sense. Adding
# the tailnet CIDR is what makes tailnet-internal webhook targets
# deliverable at all; `external` is kept so a future webhook to a
@@ -367,11 +369,13 @@ in
admin_token="$(cat "$TOKEN_FILE")"
secret="$(cat "$SECRET_FILE")"
auth=(-H "Authorization: token $admin_token")
# The path carries the Hermes route the relay should forward into, so
# each Hermes subscription gets its own hook here and the relay itself
# stays generic. Adding one is a new subscribe + a new hook URL.
relay="http://mars.orbit.sol:8645"
target="$relay/gitea/gitea-pr-comments"
# Straight at Hermes's own webhook listener on mars, no relay in
# between: gitea 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 and reads the event name from. The
# path is the Hermes route name, so a second subscription is just a
# second hook here.
target="http://mars.orbit.sol:8644/webhooks/gitea-pr-comments"
# This unit only ever creates or updates $target. It deliberately does
# NOT delete anything, including the pre-rename hook on the relay's bare