filter: make drops visible in the gateway log

Every drop so far has been silent. The script printed its reason to stderr
and exited 0 with "[SILENT]", but Hermes only logs stderr on the nonzero
path, as

  script ignored webhook path=... code=... stderr=...

so from outside, a deliberate drop, a crash, a timeout and a missing file all
looked identical: {"status":"ignored","reason":"script"} and nothing else.
Finding out which one it was meant re-running the payload through the script
by hand.

Drops now exit 3 with an empty stdout. Both still mean "ignored" to Hermes,
but the reason lands in the log. Exit 3 rather than 1 keeps a deliberate drop
distinguishable from an unhandled exception, which exits 1, so the code alone
says which happened.

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:55:17 +02:00
co-authored by Claude Opus 5
parent e75f474726
commit b516a800bf
2 changed files with 34 additions and 8 deletions
+14 -5
View File
@@ -89,12 +89,21 @@ for path in [("comment","id"), ("comment","body"), ("comment","user","login"),
print(f"{'PASS' if ok else 'FAIL'} {'prompt path survives: {' + label + '}':<52} {cur if ok else 'MISSING'}")
if not ok: fails.append(f"path-{label}")
# --- stdout discipline: an ignore must emit EXACTLY [SILENT] ---
# --- drop contract: nonzero exit + empty stdout + reason on stderr ---
# Nonzero is what gets the reason into the gateway log (Hermes logs
# "script ignored webhook path=... code=... stderr=..." only on that path).
rc, out, err = run(payload(author="luna"))
print(f"{'PASS' if out == chr(91)+'SILENT'+chr(93)+chr(10) else 'FAIL'} {'ignore emits exactly [SILENT] on stdout':<52} {out!r}")
if out != "[SILENT]\n": fails.append("silent-exact")
print(f"{'PASS' if err.strip() else 'FAIL'} {'ignore explains itself on stderr':<52} {err.strip()[:40]!r}")
if not err.strip(): fails.append("stderr-reason")
print(f"{'PASS' if rc == 3 else 'FAIL'} {'drop exits 3 (not 0, so Hermes logs it)':<52} rc={rc}")
if rc != 3: fails.append("drop-exit-code")
print(f"{'PASS' if out == '' else 'FAIL'} {'drop writes nothing to stdout':<52} {out!r}")
if out != "": fails.append("drop-stdout-empty")
print(f"{'PASS' if 'luna' in err else 'FAIL'} {'drop names the rule on stderr':<52} {err.strip()[-44:]!r}")
if "luna" not in err: fails.append("stderr-reason")
# a crash must stay distinguishable from a deliberate drop
rc, out, err = run("not-a-dict")
print(f"{'PASS' if rc == 3 else 'FAIL'} {'malformed payload is a drop (3), not a crash':<52} rc={rc}")
if rc != 3: fails.append("malformed-exit-code")
print()
print("ALL PASSED" if not fails else "FAILURES: " + ", ".join(fails))
+20 -3
View File
@@ -12,7 +12,19 @@ STDOUT IS A PROTOCOL CHANNEL, not a log:
That last case is why every diagnostic here goes to stderr. A stray print()
would not drop an event, it would let one through.
Empty stdout, a nonzero exit, a missing script, or a timeout also count as
Drops exit with DROP_EXIT_CODE and an empty stdout rather than printing
"[SILENT]" and exiting 0. Both mean "ignored" to Hermes, but only the nonzero
path is logged, as
script ignored webhook path=... code=3 stderr=...
which puts the reason in the gateway log. On the exit-0 path the reason goes
to stderr and is never surfaced anywhere, so a drop is indistinguishable from
a crash from a missing file -- which cost a long debugging detour once
already. code=3 is what separates a deliberate drop from a real crash: a
traceback exits 1.
Empty stdout, a nonzero exit, a missing script, or a timeout all count as
"ignored", so this script fails CLOSED: if it breaks, nothing reaches the
agent rather than everything. That is the right direction for a loop guard,
but it does mean a syntax error silently disables the whole integration --
@@ -35,6 +47,11 @@ import sys
# and you do not want her reacting to build output.
IGNORED_AUTHORS = {"luna"}
# Exit code for a deliberate drop. Anything nonzero makes Hermes ignore the
# delivery AND log the reason; 3 distinguishes "a rule fired" from an
# unhandled exception, which exits 1.
DROP_EXIT_CODE = 3
# Gitea's HookIssueCommentAction values are created / edited / deleted.
# "deleted" is dropped: the payload still carries the comment body, so letting
# it through would have her act on a request that was explicitly withdrawn.
@@ -42,9 +59,9 @@ ALLOWED_ACTIONS = {"created", "edited"}
def ignore(reason: str) -> None:
"""Drop the delivery, loudly enough to find in the gateway log."""
print(f"gitea-pr-comment-filter: ignoring delivery: {reason}", file=sys.stderr)
print("[SILENT]")
raise SystemExit(0)
raise SystemExit(DROP_EXIT_CODE)
def main() -> None: