✨ feat(webhook): author allow-list hard gate before worker spawn - #13
Conversation
PR webhook routes auto-acted on any author. A prompt-level authorship guard is only a SOFT gate — it relies on the agent obeying an instruction after a worker has already spawned on untrusted external input. Add an optional `authors` route field, evaluated alongside the event and action filters but BEFORE prompt render / skill load / agent spawn, so a delivery from a non-allow-listed author is ignored at the HTTP layer and never starts a run. Author is resolved per event type against real GitHub payloads: pull_request / pull_request_review -> pull_request.user.login; push -> sender.login (falling back to pusher.name); other events -> sender.login. Unlike the action filter (fail-open), this gate fails CLOSED: an allow-list with no resolvable author ignores the delivery. A security boundary must not spawn a worker on an unattributable event. The field is optional and backward-compatible — a route with no `authors` key behaves exactly as before.
cwest
left a comment
There was a problem hiding this comment.
Moving the identity check ahead of prompt render and worker spawn is the right place for it — this stops an untrusted author at the HTTP layer instead of relying on the prompt guard after a run has already started. The fail-closed choice (ignore when an allow-list is set but no author resolves) is correct for a trust boundary, and the contrast with the action filter's fail-open is worth the comment you wrote.
I ran the suite from a fresh clone at this head: the seven new TestAuthorFilter cases pass and the full test_webhook_adapter.py is 75 passed, matching the description. The no-spawn assertions via assert_not_awaited give real proof the gate runs before spawn rather than just returning the right status. I also exercised _resolve_author directly across the edge cases — null login, missing user node, push falling back through sender to pusher, empty payload — and every unresolvable shape returns the empty string, so the fail-closed path holds.
One non-blocking note inline on the membership check. Otherwise the change is sound; holding the event at comment because the PR is still a draft.
| allowed_authors = route_config.get("authors", []) | ||
| if allowed_authors: | ||
| author = self._resolve_author(payload, event_type) | ||
| if not author or author not in allowed_authors: |
There was a problem hiding this comment.
Membership is case-sensitive, but GitHub logins are case-insensitive, so Cwest would be rejected against ["cwest"]. In practice GitHub stamps the canonical casing into payloads, so this won't bite real deliveries — leaving it as a note rather than a change. If you ever want belt-and-suspenders, lower-casing both sides would close the gap without cost.
Summary
PR webhook routes auto-acted on any PR author. A prompt-level authorship guard is only a soft gate — it relies on the agent obeying an instruction after a worker has already spawned on untrusted external input. This adds a hard gate in the gateway that never starts a run for a non-allow-listed author.
Adds an optional
authorsfield to the webhook route schema, evaluated ingateway/platforms/webhook.pyalongside the existingevents/actionsfilters, but before prompt render / skill load / agent spawn. A delivery from a non-allow-listed author is ignored at the HTTP layer ({"status":"ignored","author":...}, 200) and never spawns a worker.Design decisions
authors— matches the existing plural-noun route schema (events,actions).pull_request/pull_request_review→pull_request.user.loginpush→sender.login, falling back topusher.namesender.loginauthorskey behaves exactly as before.Tests
New
TestAuthorFilterintests/gateway/test_webhook_adapter.py(7 tests), modeled on the existing event-filter harness. "No spawn" assertions are positive proof viahandle_message.assert_not_awaited():authorskey → unchanged (202)pull_request_reviewauthor resolutionpushauthor resolved fromsender.loginpushoutsider viasender.login→ ignored, no spawntests/gateway/test_webhook_adapter.py: 75 passed (68 baseline + 7 new). Fulltests/gateway/per-file CI harness: 6807 passed; the only failures are pre-existing baseline flakes in unrelated files (verified identical with this diff stashed) — none in the webhook adapter.Follow-up (separate card)
After this lands + deploys, set
authors: ["cwest"]on the three PR routes (github-prs,github-pr-review,github-pr-closed) as the hard gate; the soft prompt guard stays as belt-and-suspenders.Docs
website/docs/user-guide/messaging/webhooks.md— route-schema table + example updated.