tai42-webhook-verifier-github
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
githubverifier.
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:
- Triggers & webhooks concept: https://tai42.ai/concepts/triggers-and-webhooks
- Build a webhook verifier (author guide): https://tai42.ai/guides/authors/webhook-verifier
- Ecosystem catalog: https://tai42.ai/reference/catalog
The current release line tracks the 10.x contract (tai42-contract>=10.0,<11).
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
-
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 typeapplication/json, and a Secret. Store that same secret in the environment asGITHUB_WEBHOOK_SECRETon the platform. -
List
tai42_webhook_verifier_githubin the manifest underlifecycle_modules(see above) so thegithubverifier is registered at boot. -
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"}}'
-
Register a hook on the
github-eventstopic 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 theX-Hub-Signature-256HMAC → 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-256header is missing (lookup is case-insensitive), - the value is not prefixed exactly
sha256=(e.g. asha1=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.2.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tai42_webhook_verifier_github-1.2.5.tar.gz | 16.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tai42_webhook_verifier_github-1.2.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.8 kB
Release files / tai42_webhook_verifier_github-1.2.5.tar.gz
| Download URL | tai42_webhook_verifier_github-1.2.5.tar.gz |
|---|---|
| Size | 16.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6ef0b885b993a36633f121abfbbf4810e8d40a6e15b82d15b3a9c8ff787b4786
|
|
BLAKE2b-256 checksum How to use checksums |
9752d32c38e6aeb21223c19c603f5f7806a8518ab9cbab1105b6f81767c90b9e
|
| 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.2.5-py3-none-any.whl
| Download URL | tai42_webhook_verifier_github-1.2.5-py3-none-any.whl |
|---|---|
| Size | 14.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fc3df9ae29c1aaf4e7d2177303bd8d2497ceb9421b1199770b339be16c312a6b
|
|
BLAKE2b-256 checksum How to use checksums |
f289fe5ae6d176bf5e3bdcaa4b65d147503ee8e4a359035357b1244ab04e4b0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|