Skip to content

✨ feat(webhook): author allow-list hard gate before worker spawn - #13

Merged
cwest merged 1 commit into
cwest/integrationfrom
wt/webhook-author-allowlist
Jun 27, 2026
Merged

✨ feat(webhook): author allow-list hard gate before worker spawn#13
cwest merged 1 commit into
cwest/integrationfrom
wt/webhook-author-allowlist

Conversation

@cwest

@cwest cwest commented Jun 27, 2026

Copy link
Copy Markdown
Owner

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 authors field to the webhook route schema, evaluated in gateway/platforms/webhook.py alongside the existing events / actions filters, 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

  • Field name authors — matches the existing plural-noun route schema (events, actions).
  • Author resolution per event type (verified against real GitHub payloads):
    • pull_request / pull_request_reviewpull_request.user.login
    • pushsender.login, falling back to pusher.name
    • other events → sender.login
  • Fail-CLOSED — deliberately the opposite of the action filter's fail-open. When an allow-list is configured but no author can be resolved, the delivery is ignored. A security boundary must not spawn a worker on an unattributable event. Documented in a code comment + a dedicated test.
  • Optional / backward-compatible — a route with no authors key behaves exactly as before.

Tests

New TestAuthorFilter in tests/gateway/test_webhook_adapter.py (7 tests), modeled on the existing event-filter harness. "No spawn" assertions are positive proof via handle_message.assert_not_awaited():

  • allow-listed author → 202, spawn reached
  • outsider → ignored, no spawn
  • no authors key → unchanged (202)
  • missing/unparseable author → fail-closed (ignored)
  • pull_request_review author resolution
  • push author resolved from sender.login
  • push outsider via sender.login → ignored, no spawn

tests/gateway/test_webhook_adapter.py: 75 passed (68 baseline + 7 new). Full tests/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.

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 cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cwest
cwest marked this pull request as ready for review June 27, 2026 20:33
@cwest
cwest merged commit 6289531 into cwest/integration Jun 27, 2026
8 checks passed
@cwest
cwest deleted the wt/webhook-author-allowlist branch June 27, 2026 20:33
cwest added a commit that referenced this pull request Jul 1, 2026
Optional per-route action allow-list filter for webhook routes, plus an
author allow-list hard gate enforced BEFORE any worker is spawned. A local
feature for the fork's webhook deployment; kept indefinitely as a deliberate
local divergence (fork PR #2, #13).
cwest added a commit that referenced this pull request Jul 26, 2026
Optional per-route action allow-list filter for webhook routes, plus an
author allow-list hard gate enforced BEFORE any worker is spawned. A local
feature for the fork's webhook deployment; kept indefinitely as a deliberate
local divergence (fork PR #2, #13).

(cherry picked from commit 54ee32d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant