Skip to main content

tai42-webhook-verifier-github

License: Apache 2.0

The GitHub webhook-signature verifier plugin for the TAI ecosystem — a per-provider WebhookVerifier that authenticates each inbound GitHub delivery before the platform parses or dispatches its payload.

A webhook door can bind a named verifier to a topic; this plugin supplies the github verifier.

GitHub signs every webhook delivery with an HMAC-SHA256 over the exact raw request body, keyed by the shared secret, and sends it in the X-Hub-Signature-256 header as "sha256=" + hexdigest. GitHubWebhookVerifier recomputes that HMAC over the raw bytes and compares it in constant time. It returns None on success and raises WebhookVerificationError on any failure.

Its only tai-* dependency is tai42-contract (the interface it registers through); the HMAC work is pure standard library (hmac / hashlib). It never imports the skeleton — the plugin is contract-facing.

The TAI ecosystem

TAI is an open-source runtime for MCP tools, agents, and workflows. A trigger fires a tool from an inbound event; a webhook verifier is the door's bouncer — the per-provider check that authenticates a delivery before any hook runs. This package is the GitHub verifier. The ecosystem is open-ended: any package can supply a verifier, so this repo is this verifier's own full doc home, and the documentation site covers the platform-level story:

Install

Requires Python 3.13+. Install from PyPI into the environment that runs the server:

uv add tai42-webhook-verifier-github

Or from source — clone this repo and add it as an editable dependency; the tai42-* dependencies resolve in-tree from the workspace.

git clone https://github.com/tai42ai/tai42   # next to your app checkout
cd /path/to/your/app
uv add --editable ../tai42/plugins/webhook-verifier-github

How it loads — lifecycle_modules

The platform loads this plugin through the manifest's lifecycle_modules field. That field is import-only: the host simply imports each listed module to run its registration side effect. Importing tai42_webhook_verifier_github calls tai42_app.webhook_verifiers.register("github", GitHubWebhookVerifier()), binding the github verifier on the app handle.

{
  // Import-only modules: loaded purely for their registration side effect.
  // `lifecycle_modules` are imported WITHOUT going through the extension
  // registry's validation — only `extensions_modules` run that. So this entry
  // is a plain registration, not an extension.
  "lifecycle_modules": ["tai42_webhook_verifier_github"]
}

lifecycle_modules entries are imported as-is, with no validation step — only extensions_modules go through the extension registry's validation. This entry is therefore a plain registration, not an extension.

Configuration — the secret never leaves the environment

A verifier is bound to a topic with {verifier, config}. This plugin's config holds only the name of the environment variable that carries the secret — never the secret itself:

{ "verifier": "github", "config": { "secret_env": "GITHUB_WEBHOOK_SECRET" } }

At verify time the secret is read from os.environ[config["secret_env"]]. A missing env var raises loudly (KeyError) and an empty one raises ValueError — verification fails CLOSED, so a misconfigured secret never becomes a silently-unauthenticated door.

Replay defense: after a valid signature, each delivery is deduped by its X-GitHub-Delivery id in a seen-set, so a replayed delivery draws an idempotent already_seen and re-fires no hook. The window defaults to 24h and is set with an optional replay_window_seconds (a positive int) in config; a delivery with no X-GitHub-Delivery header, or a non-positive/non-int replay_window_seconds, fails CLOSED.

Secret hygiene. The secret lives only in an environment variable. Never commit it to a file, a fixture, a manifest, or a URL. The example secret below (It's a Secret to Everybody) is GitHub's own published example — it is a placeholder for docs and tests, never a real secret.

End-to-end

  1. Create the GitHub webhook. In your repo → Settings → Webhooks → Add webhook, set the Payload URL to your platform's public door, e.g. https://<your-host>/universal_webhook/github-events, Content type application/json, and a Secret. Store that same secret in the environment as GITHUB_WEBHOOK_SECRET on the platform.

  2. List tai42_webhook_verifier_github in the manifest under lifecycle_modules (see above) so the github verifier is registered at boot.

  3. Bind the verifier to the topic. Through the skeleton's authenticated route:

    curl -X PUT https://<your-host>/api/hooks/topics/github-events/verifier \
      -H "Authorization: Bearer $TAI_API_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"verifier": "github", "config": {"secret_env": "GITHUB_WEBHOOK_SECRET"}}'
    
  4. Register a hook on the github-events topic that runs a demo tool (e.g. a tool that logs the event). Now push to the repo → GitHub signs and POSTs the delivery → the door verifies the X-Hub-Signature-256 HMAC → the hook fires and the tool runs. A delivery with a missing, malformed, or wrong signature is rejected before any hook runs.

Simulate a signed delivery locally

Compute the signature over the exact body with the same secret and POST it — this is what a genuine GitHub delivery looks like on the wire:

# The secret lives only in the environment — never hard-coded here.
export GITHUB_WEBHOOK_SECRET="It's a Secret to Everybody"   # GitHub's example placeholder

BODY='{"zen":"Keep it logically awesome."}'
SIG="sha256=$(printf '%s' "$BODY" \
  | openssl dgst -sha256 -hmac "$GITHUB_WEBHOOK_SECRET" -r \
  | cut -d ' ' -f1)"

curl -X POST http://127.0.0.1:8000/universal_webhook/github-events \
  -H "Content-Type: application/json" \
  -H "X-GitHub-Event: ping" \
  -H "X-Hub-Signature-256: $SIG" \
  --data-raw "$BODY"

printf '%s' (not echo) sends the body with no trailing newline, so the bytes signed match the bytes POSTed exactly — the HMAC is over the raw body.

Verification rules

verify(body, headers, config) is an async method — the caller awaits it. It raises WebhookVerificationError when:

  • the X-Hub-Signature-256 header is missing (lookup is case-insensitive),
  • the value is not prefixed exactly sha256= (e.g. a sha1= prefix),
  • the hex after the prefix is not exactly 64 characters (wrong/truncated),
  • the recomputed digest does not match (the final compare uses hmac.compare_digest — constant-time).

A missing secret_env env var raises KeyError and an empty one raises ValueError (fails closed), not WebhookVerificationError.

Development

uv venv --python 3.13
uv pip install --no-sources --group dev --editable .
uv run --no-sync pytest --cov --cov-report=term-missing
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync pyright

License

Apache-2.0. See LICENSE and NOTICE.

Metadata

Release files for tai42-webhook-verifier-github 1.3.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tai42-webhook-verifier-github 1.3.8
File Size Uploaded
tai42_webhook_verifier_github-1.3.8.tar.gz 16.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tai42-webhook-verifier-github 1.3.8
File Interpreter ABI Platform
tai42_webhook_verifier_github-1.3.8-py3-none-any.whl Python 3 none any Details

Total release size: 31.3 kB

Release files / tai42_webhook_verifier_github-1.3.8.tar.gz

Download URL tai42_webhook_verifier_github-1.3.8.tar.gz
Size 16.6 kB
Tags Source
SHA-256 checksum
How to use checksums
916e3a3895ebf0faced603bbd8c41a49cc5c86877ff851de5afbf56963080d0c
BLAKE2b-256 checksum
How to use checksums
02bed266f8177ad138e8323479623ffbf017a32f10f861390ffdbc2ae992de47
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / tai42_webhook_verifier_github-1.3.8-py3-none-any.whl

Download URL tai42_webhook_verifier_github-1.3.8-py3-none-any.whl
Size 14.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fe626ad1d8704932db27f376ab46bf8a08959021ee54866f1e1ee49fc4638bbb
BLAKE2b-256 checksum
How to use checksums
9d2c98786b4a2482f2c7e5c483a849e8b0db1f5ac2c7c649381c450003cfde70
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

1.3.12

2 release files

1.3.11

2 release files

1.3.10

2 release files

1.3.9

2 release files

This release

1.3.8 This release

2 release files

1.3.7

2 release files

1.3.6

2 release files

1.3.5

2 release files

1.3.4

2 release files

1.3.3

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.13

2 release files

1.2.12

2 release files

1.2.11

2 release files

1.2.10

2 release files

1.2.9

2 release files

1.2.8

2 release files

1.2.7

2 release files

1.2.6

2 release files

1.2.5

2 release files

1.2.4

2 release files

1.2.3

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.6

2 release files

1.1.5

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page