Skip to content

feat(webhooks): schedule webhook config UI + optional HMAC signature auth (trinity-enterprise#77) - #1469

Merged
vybe merged 4 commits into
devfrom
feature/77-webhook-config-ui-auth
Jul 6, 2026
Merged

feat(webhooks): schedule webhook config UI + optional HMAC signature auth (trinity-enterprise#77)#1469
vybe merged 4 commits into
devfrom
feature/77-webhook-config-ui-auth

Conversation

@dolho

@dolho dolho commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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 in X-Trinity-Signature: sha256=….
  • Per-schedule secret: minted whsec_…, returned exactly once, stored only as an AES-256-GCM envelope (Invariant fix: Add ARM64 Alpine compatibility for frontend Docker build #12). New agent_schedules.webhook_secret_encrypted + webhook_auth_enableddual-track migration (SQLite agent_schedules_webhook_auth + Alembic 0014; schema.py + tables.py).
  • POST/DELETE /api/agents/{name}/schedules/{id}/webhook/secret (enable-rotate / disable). GET reports auth_enabled/has_secret, never the secret. Rotating the URL or revoking clears the secret.
  • Public trigger verifies the signature after body read+cap — fail-closed (401 missing/invalid; 500 on an unreadable stored secret; never a silent bypass). Off by default → existing token-in-URL webhooks unchanged.

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

  • Per-schedule Webhook control, disabled by default (no token until enabled)
  • Masked/revealable token, Copy URL, copyable example curl (with optional context body)
  • Rotate + Revoke with immediate-invalidation warnings
  • Signature auth — HMAC secret scheme, off by default, 401 on unauthenticated/invalid when on; secret stored AES-256-GCM, surfaced once
  • Access control decided + documented: mint/rotate/revoke = AuthorizedAgent, aligning with schedule management (per the AC)
  • Audit-logged (unchanged); dark-mode + accessible
  • User-doc snippet (configure + secure a webhook, openssl signing example)
  • MCP tool (explicit stretch) — deferred; noted for a follow-up

Verification

  • Unit: test_77_webhook_signature.py (12) + test_77_webhook_secret_db.py (8, on db_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.
  • SFC compiles (@vue/compiler-sfc).
  • Live against real Redis + SQLite + AES-256-GCM: migration ran, mint → encrypt → store → trigger-path decrypt → signature verify round-trips; valid accepted, tampered/wrong-secret rejected; URL-rotate + revoke clear the secret. PASS.

Notes

  • Dual-track migration guarded by the schema-parity CI job; both migrations.py (SQLite) + Alembic 0014 land together.
  • The three companion type-bug hardening issues referenced in the issue (dedup, soft-delete guard, pre-auth rate limit) are already merged on dev (webhooks.py); this builds on them.

Related to trinity-enterprise#77

🤖 Generated with Claude Code

dolho and others added 2 commits July 6, 2026 13:16
…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
@dolho
dolho requested a review from vybe July 6, 2026 10:34
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

Eugene Vyborov and others added 2 commits July 6, 2026 13:37
…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 vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@vybe
vybe merged commit 13961fa into dev Jul 6, 2026
20 checks passed
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.

2 participants