hermes: match --events issue_comment, not pull_request_comment

A timeline comment on a PR never reached the route. Gitea reuses the same
strings in two namespaces and they collide:

  subscription name            wire name (X-GitHub-Event)   what it is
  pull_request_comment         issue_comment                comment on a PR
  issue_comment                issue_comment                comment on an issue
  pull_request_review_comment  pull_request_comment         review on a PR

The hook's `events` array takes the subscription name; Hermes matches
--events against X-GitHub-Event, the wire name, produced by
HookEventType.Event() in modules/webhook/type.go. So --events
pull_request_comment was selecting review submissions and could never match a
comment -- the exact inversion of what it reads like.

That also explains both observed failures. The review submission matched
(wire name pull_request_comment) and reached the filter, which correctly
dropped it on action=reviewed since a PullRequestPayload carries no comment
object. The timeline comment arrived as issue_comment, matched nothing, and
was dropped by the events filter before the script ever ran.

gitea.nix and hermes-agent.nix now deliberately name the same event
differently, so both carry the table and say the other is not a typo.

issue_comment on the wire also covers comments on plain issues. The hook does
not subscribe those, and the filter's is_pull check drops them regardless, so
widening the hook later cannot leak issue comments into the agent.

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:46:31 +02:00
co-authored by Claude Opus 5
parent 6116ec4e5a
commit e75f474726
3 changed files with 46 additions and 9 deletions
+10
View File
@@ -54,6 +54,16 @@ defaults to `external` and does NOT include tailnet addresses
gitea classifies it). `services/dev/gitea.nix` sets it accordingly; without
that, deliveries fail with `webhook can only call allowed HTTP servers`.
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
Hermes matches `--events` against, carries a lossy *wire* name from
`HookEventType.Event()`. A comment on a PR subscribes as
`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
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
+25 -6
View File
@@ -311,11 +311,30 @@ in
# exact format AND X-GitHub-Event, unconditionally, for every webhook type,
# which is precisely what Hermes validates and reads the event name from.
#
# --events pull_request_comment narrows the route to the one event the prompt
# handles; Gitea sends that value distinctly from issue_comment, so plain
# issue comments never reach the agent. A route carries exactly one prompt,
# so another event means either branching on {action} in the prompt or a
# second subscription plus a second Gitea hook at /webhooks/<name>.
# --events issue_comment, NOT pull_request_comment. Gitea uses the same
# strings in two different namespaces and they collide:
#
# subscription name wire name (X-GitHub-Event) what it is
# ----------------------- -------------------------- ----------------
# pull_request_comment issue_comment comment on a PR
# issue_comment issue_comment comment on an issue
# pull_request_review_comment pull_request_comment review on a PR
#
# The hook's `events` array (services/dev/gitea.nix) takes the SUBSCRIPTION
# name; Hermes matches --events against X-GitHub-Event, i.e. the WIRE name,
# which comes from HookEventType.Event() in modules/webhook/type.go. So
# "pull_request_comment" here would match review submissions and never a
# comment -- the exact inversion of what it reads like. X-GitHub-Event-Type
# carries the subscription name, but Hermes does not look at it.
#
# issue_comment on the wire covers comments on plain issues too; the hook
# does not subscribe those, and the filter's is_pull check drops them anyway
# if the hook is ever widened.
#
# A route carries exactly one prompt, so another event means either branching
# on {action} in the prompt or a second subscription plus a second Gitea hook
# at /webhooks/<name>. Review comments would need that: they arrive as a
# PullRequestPayload with action "reviewed" and no comment object at all.
#
# No --deliver: it defaults to `log`. The prompt tells her to answer in the
# pull request, so the PR comment IS the delivery.
@@ -380,7 +399,7 @@ in
hermes webhook subscribe gitea-pr-comments \
--secret "$GITEA_HERMES_WEBHOOK_SECRET" \
--description "Gitea PR comments -> L.U.N.A." \
--events pull_request_comment \
--events issue_comment \
--script gitea-pr-comment-filter.py \
--prompt "$prompt"
'
+11 -3
View File
@@ -21,9 +21,17 @@ let
# nothing she does lands without darman clicking merge.
lunaRepos = [ "darman/homelab" ];
# Only the event Hermes's gitea-pr-comments route actually handles. Gitea
# sends pull_request_comment distinctly from issue_comment, so this covers
# comments on PRs and nothing else — no issue comments, no pushes.
# Only the event Hermes's gitea-pr-comments route actually handles.
#
# This is a SUBSCRIPTION name, and gitea reuses these strings in a second,
# colliding namespace on the wire — see the long comment on --events in
# hosts/mars/hermes-agent.nix. "pull_request_comment" HERE means a timeline
# comment on a pull request; the same string in X-GitHub-Event means a
# review submission. The two files therefore name the same event
# differently on purpose, and neither is a typo:
#
# here (subscription): pull_request_comment
# there (--events): issue_comment
#
# Hermes would drop the rest anyway (its route filters on X-GitHub-Event
# before any LLM call), so this is defence in depth rather than the only