From cb3b5bb64c458ba3c8effbf408ec26e9595b8b3c Mon Sep 17 00:00:00 2001 From: HwangJohn Date: Wed, 17 Jun 2026 17:31:01 +0900 Subject: [PATCH] feat(webhook): accept standard webhook signatures --- gateway/platforms/webhook.py | 11 +++++++---- website/docs/user-guide/messaging/webhooks.md | 5 +++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index 9c928a474c..7eb63cd505 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -548,8 +548,8 @@ class WebhookAdapter(BasePlatformAdapter): prompt = self._render_prompt(route_config.get("prompt", ""), payload, event_type, route_name) if skills := route_config.get("skills", []): prompt = self._apply_skills(prompt, skills) - delivery_id = headers.get("X-GitHub-Delivery", headers.get( - "svix-id", headers.get("X-Request-ID", str(int(time.time() * 1000))))) + delivery_id = headers.get("X-GitHub-Delivery", headers.get("svix-id", headers.get( + "webhook-id", headers.get("X-Request-ID", str(int(time.time() * 1000)))))) now = time.time() # idempotency: skip duplicate deliveries (webhook retries) if not self._record_delivery_id(delivery_id, now): logger.info("[webhook] Skipping duplicate delivery %s", delivery_id) @@ -617,14 +617,17 @@ class WebhookAdapter(BasePlatformAdapter): # --- Signature validation --- def _validate_signature(self, request: "web.Request", body: bytes, secret: str) -> bool: - """Validate webhook signature (GitHub, GitLab, Svix, Linear, generic HMAC-SHA256).""" + """Validate webhook signature (GitHub, GitLab, Svix, Standard Webhooks, Linear, generic HMAC-SHA256).""" headers = request.headers def _header(name: str) -> str: return headers.get(name, "") or headers.get(name.lower(), "") or headers.get(name.upper(), "") - # Svix / AgentMail: signed content is "{id}.{timestamp}.{raw_body}". + # Svix / AgentMail: signed content is "{id}.{timestamp}.{raw_body}". Standard Webhooks + # (webhook-*; GitLab signing tokens) is the same scheme under other header names. svix = [_header(name) for name in ("svix-id", "svix-timestamp", "svix-signature")] + if not any(svix): + svix = [_header(name) for name in ("webhook-id", "webhook-timestamp", "webhook-signature")] if any(svix): return _validate_svix_signature(body, secret, *svix) # Linear (any header case): hex HMAC of the body. GitHub: sha256=. GitLab: plain token. diff --git a/website/docs/user-guide/messaging/webhooks.md b/website/docs/user-guide/messaging/webhooks.md index dd1148b0fa..d9acdb8918 100644 --- a/website/docs/user-guide/messaging/webhooks.md +++ b/website/docs/user-guide/messaging/webhooks.md @@ -498,6 +498,7 @@ The adapter validates incoming webhook signatures using the appropriate method f - **GitHub**: `X-Hub-Signature-256` header — HMAC-SHA256 hex digest prefixed with `sha256=` - **GitLab**: `X-Gitlab-Token` header — plain secret string match +- **Standard Webhooks**: `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers — signed content is `{id}.{timestamp}.{raw_body}` with a `v1,` signature - **Generic (V2, recommended)**: `X-Webhook-Signature-V2` + `X-Webhook-Timestamp` headers — HMAC-SHA256 hex digest of `.`. The timestamp (Unix seconds) must be within ±300 seconds of the server clock, which prevents captured requests from being replayed later. - **Generic (V1, legacy)**: `X-Webhook-Signature` header — raw HMAC-SHA256 hex digest of the body only. Still accepted for backward compatibility, but it has no replay protection (a captured request replays indefinitely); the gateway logs a deprecation warning once per route. Switch senders to V2. @@ -529,7 +530,7 @@ Requests exceeding the limit receive a `429 Too Many Requests` response. ### Idempotency -Delivery IDs (from `X-GitHub-Delivery`, `X-Request-ID`, or a timestamp fallback) are cached for **1 hour**. Duplicate deliveries (e.g. webhook retries) are silently skipped with a `200` response, preventing duplicate agent runs. +Delivery IDs (from `X-GitHub-Delivery`, `svix-id`, `webhook-id`, `X-Request-ID`, or a timestamp fallback) are cached for **1 hour**. Duplicate deliveries (e.g. webhook retries) are silently skipped with a `200` response, preventing duplicate agent runs. ### Body size limits @@ -588,7 +589,7 @@ This is the same trust model that applies to everything the agent reads: web pag ### Duplicate responses -- The idempotency cache should prevent this — check that the webhook source is sending a delivery ID header (`X-GitHub-Delivery` or `X-Request-ID`) +- The idempotency cache should prevent this — check that the webhook source is sending a delivery ID header (`X-GitHub-Delivery`, `svix-id`, `webhook-id`, or `X-Request-ID`) - Delivery IDs are cached for 1 hour ### `gh` CLI errors (GitHub comment delivery)