feat(webhooks): schedule webhook config UI + optional HMAC signature auth (trinity-enterprise#77) - #1469
Merged
Merged
Conversation
…auth (trinity-enterprise#77) Public schedule webhooks (WEBHOOK-001) were backend-only — mint/rotate/revoke was REST-API-only and a leaked URL token was the entire credential. This adds a first-class Schedules-tab configuration surface and an optional signature layer. Backend (the net-new work is the auth layer): - `services/webhook_signature.py` — HMAC-SHA256 over the raw body, verified constant-time against a per-webhook secret in `X-Trinity-Signature: sha256=…`. - Per-schedule signing secret: minted `whsec_…`, returned to the operator exactly once, persisted only as an AES-256-GCM envelope (Invariant #12). New columns `agent_schedules.webhook_secret_encrypted` + `webhook_auth_enabled` (dual-track migration: SQLite `agent_schedules_webhook_auth` + Alembic 0014; schema.py + tables.py updated). - `POST/DELETE /api/agents/{name}/schedules/{id}/webhook/secret` enable-rotate / disable signing (AuthorizedAgent — aligns with schedule management, the access-control decision settled per the AC). GET reports auth_enabled/has_secret (never the secret). Rotating the URL or revoking clears the secret. - Public trigger (`routers/webhooks.py`): when auth is on, verify the signature after the body is read + size-capped — FAIL-CLOSED (401 missing/invalid, 500 on an unreadable stored secret, never a silent bypass). Off by default → existing token-in-URL webhooks unchanged. Frontend (`SchedulesPanel.vue`): per-schedule Webhook panel — enable, masked/ revealable URL, one-click Copy URL, ready-to-paste example curl, rotate/revoke (with warnings), enable/rotate/disable signature, secret shown exactly once. Dark-mode aware. Docs: user-doc snippet (configure + secure a webhook, incl. an openssl signing example) + architecture (endpoints, DDL, signature-auth behavior). Tests: `test_77_webhook_signature.py` (helper: valid/tampered/wrong-secret/ missing-header/empty-body, prefix-optional) + `test_77_webhook_secret_db.py` (secret lifecycle on db_harness: mint-once, ciphertext at rest, status, trigger- path decrypt, URL-rotate/revoke clear, secret rotation). Updated the #1424 hardening test double for the new model fields. 24 passed. Also verified live against real Redis + SQLite + AES-256-GCM (mint→encrypt→trigger-path decrypt→ signature verify round-trip). MCP tool (stretch) deferred. Related to trinity-enterprise#77 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fig-ui-auth # Conflicts: # tests/registry.json
|
Resolve by running |
…dedup change Two semantic conflicts with #1437 (merged after this branch was cut) that git merges cleanly but pytest doesn't: - test_1422_webhook_idempotency._FakeSchedule gains the ent#77 webhook_auth_enabled/webhook_secret_encrypted attributes, matching the real Schedule model, so the new signature gate doesn't AttributeError. - test_1424 small-body test now sends an explicit Idempotency-Key — since #1422 the idempotency short-circuit (its benign 409 probe) engages only for keyed calls. This failure pre-existed on dev; repaired here since this PR owns the file. All 4 webhook unit files: 28 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vybe
approved these changes
Jul 6, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
Validated via /validate-pr: dual-track schema migration (SQLite + Alembic 0014 + schema.py + tables.py), AES-256-GCM secret storage (Invariant #12), fail-closed HMAC verify with constant-time compare, secret shown exactly once, architecture + user docs updated, unit + DB lifecycle tests registered. Reconciled two semantic conflicts with the just-merged #1437 (fake-schedule attrs + keyed idempotency probe); all 4 webhook unit files pass (28/28). Approving.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements trinity-enterprise#77 (type-feature; lands in the public repo — WEBHOOK-001 is core OSS, the issue points only at public
src/backend/src/frontend).Problem
Public schedule webhooks were backend-only: mint/rotate/revoke via REST only, no UI, and the opaque URL token was the entire credential — a leaked URL could trigger the schedule.
What's added
Signature auth (the net-new security work):
services/webhook_signature.py— HMAC-SHA256 over the raw body, constant-time verify against a per-webhook secret inX-Trinity-Signature: sha256=….whsec_…, returned exactly once, stored only as an AES-256-GCM envelope (Invariant fix: Add ARM64 Alpine compatibility for frontend Docker build #12). Newagent_schedules.webhook_secret_encrypted+webhook_auth_enabled— dual-track migration (SQLiteagent_schedules_webhook_auth+ Alembic0014;schema.py+tables.py).POST/DELETE /api/agents/{name}/schedules/{id}/webhook/secret(enable-rotate / disable). GET reportsauth_enabled/has_secret, never the secret. Rotating the URL or revoking clears the secret.UI (
SchedulesPanel.vue): per-schedule Webhook panel — enable, masked/revealable URL, one-click Copy URL, ready-to-paste example curl, Rotate URL/Revoke (with warnings), enable/rotate/disable signature, and the signing secret shown exactly once. Dark-mode aware.Acceptance criteria
curl(with optional context body)AuthorizedAgent, aligning with schedule management (per the AC)Verification
test_77_webhook_signature.py(12) +test_77_webhook_secret_db.py(8, ondb_harness→ real schema + AES-256-GCM). Updated the bug(webhooks): public webhook endpoint has no pre-auth/per-IP rate limit or request-body cap #1424 hardening test double for the new model fields. 24 passed.@vue/compiler-sfc).mint → encrypt → store → trigger-path decrypt → signature verifyround-trips; valid accepted, tampered/wrong-secret rejected; URL-rotate + revoke clear the secret. PASS.Notes
schema-parityCI job; bothmigrations.py(SQLite) + Alembic0014land together.type-bughardening issues referenced in the issue (dedup, soft-delete guard, pre-auth rate limit) are already merged ondev(webhooks.py); this builds on them.Related to trinity-enterprise#77
🤖 Generated with Claude Code