relay: forward raw Gitea events unchanged

This commit is contained in:
2026-08-23 02:31:32 +00:00
parent 7b36d95293
commit 6a037d557c
4 changed files with 26 additions and 39 deletions
+7 -6
View File
@@ -37,13 +37,14 @@ scripts/ # deploy, edit_secrets
Hosts compose by importing `common.nix` + whichever `services/*` modules they
run. Each service module opens its own firewall ports.
## Gitea PR comment relay
## Gitea event relay
Mars includes a small HMAC-validating relay for Gitea webhooks. It normalizes
Gitea headers and forwards every authenticated JSON event to Hermes over
localhost; Hermes owns event selection, repository policy, and response
behavior. Jupiter's Gitea provisioning service registers the webhook
idempotently at `http://mars.orbit.sol:8645/gitea`.
Mars includes a small HMAC-validating relay for Gitea webhooks. It forwards the
authenticated request body unchanged, along with Gitea event and delivery
headers, to Hermes over localhost. The relay 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`.
Before deploying either host, add the same random
`gitea_hermes_webhook_secret` value to both `secrets/mars.yaml` and
+4 -5
View File
@@ -42,12 +42,11 @@ in
};
};
# The relay forwards into a named Hermes webhook subscription. Keep the
# subscription declaratively present without replacing the rest of Hermes's
# runtime-managed webhook state. The secret is already in the container's
# environment file, but never appears in this unit or the Nix store.
# The relay forwards into a generic Hermes webhook subscription. Keep the
# subscription declaratively present without putting event policy or prompt
# text in this transport unit. Hermes owns interpretation and response policy.
systemd.services.hermes-agent-webhook-route = {
description = "Configure Hermes Gitea PR comment webhook route";
description = "Configure Hermes Gitea event webhook route";
wantedBy = [ "multi-user.target" ];
after = [ "podman-hermes-agent.service" ];
requires = [ "podman-hermes-agent.service" ];
+14 -27
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Normalize authenticated Gitea PR webhooks for Hermes Agent."""
"""Relay authenticated Gitea webhook requests to Hermes Agent."""
from __future__ import annotations
import hashlib
@@ -40,13 +40,6 @@ def json_bytes(payload: dict) -> bytes:
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()
def normalized_event(headers, payload: dict) -> str:
event = headers.get("X-Gitea-Event-Type", "") or headers.get("X-Gitea-Event", "")
if event == "issue_comment" and payload.get("is_pull") is True:
return "pull_request_comment"
return event or "unknown"
class Handler(BaseHTTPRequestHandler):
server_version = "gitea-hermes-relay/1.0"
@@ -82,15 +75,6 @@ class Handler(BaseHTTPRequestHandler):
return
body = self.rfile.read(content_length)
try:
payload = json.loads(body)
except json.JSONDecodeError:
self.send_json(400, {"status": "invalid_json"})
return
if not isinstance(payload, dict):
self.send_json(400, {"status": "invalid_payload"})
return
try:
secret = load_secret()
except RuntimeError as exc:
@@ -107,22 +91,26 @@ class Handler(BaseHTTPRequestHandler):
self.send_json(401, {"status": "invalid_signature"})
return
event = normalized_event(self.headers, payload)
normalized = dict(payload)
normalized["event_type"] = event
normalized["relay_source"] = "gitea"
forwarded_body = json_bytes(normalized)
# Keep the incoming body unchanged. Event interpretation and policy
# belong to Hermes, not to this transport service.
forwarded_body = body
forwarded_signature = hmac.new(
secret, forwarded_body, hashlib.sha256
).hexdigest()
gitea_event = self.headers.get("X-Gitea-Event", "")
gitea_event_type = self.headers.get("X-Gitea-Event-Type", "")
delivery_id = self.headers.get("X-Gitea-Delivery", "")
forwarded_headers = {
"Content-Type": "application/json",
"X-GitHub-Event": event,
"X-Webhook-Signature": forwarded_signature,
}
if gitea_event:
forwarded_headers["X-Gitea-Event"] = gitea_event
if gitea_event_type:
forwarded_headers["X-Gitea-Event-Type"] = gitea_event_type
if delivery_id:
forwarded_headers["X-Request-ID"] = delivery_id
forwarded_headers["X-Gitea-Delivery"] = delivery_id
request = Request(
HERMES_URL,
@@ -148,12 +136,11 @@ class Handler(BaseHTTPRequestHandler):
return
LOG.info(
"forwarded %s action=%s delivery=%s",
event,
payload.get("action", ""),
"forwarded Gitea event=%s delivery=%s",
gitea_event or gitea_event_type or "unknown",
delivery_id or "none",
)
self.send_json(200, {"status": "forwarded", "event": event})
self.send_json(200, {"status": "forwarded"})
def main() -> None:
+1 -1
View File
@@ -300,7 +300,7 @@ in
# existing hook for the relay target or creates it when absent. Event policy
# belongs to Hermes, so the source sends the complete Gitea event set.
systemd.services.gitea-hermes-webhook-provision = {
description = "Provision Gitea webhook for Hermes PR comments";
description = "Provision Gitea webhook for Hermes events";
after = [ "gitea.service" ];
requires = [ "gitea.service" ];
wantedBy = [ "multi-user.target" ];