mars: add generic Gitea webhook relay #2
@@ -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
|
||||
|
||||
@@ -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" ];
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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" ];
|
||||
|
||||
Reference in New Issue
Block a user