Skip to content

Add foundational webhook package (Phase 1 of dynamic webhook middleware)#3840

Merged
JAORMX merged 8 commits intostacklok:mainfrom
Sanskarzz:dynamicwebhook1
Mar 23, 2026
Merged

Add foundational webhook package (Phase 1 of dynamic webhook middleware)#3840
JAORMX merged 8 commits intostacklok:mainfrom
Sanskarzz:dynamicwebhook1

Conversation

@Sanskarzz
Copy link
Copy Markdown
Contributor

@Sanskarzz Sanskarzz commented Feb 16, 2026

Fix: #3396

Summary

Implements Phase 1 of RFC THV-0017, creating the core pkg/webhook/ package with types, HTTP client, HMAC signing, and error handling.

ToolHive currently has no mechanism for external policy enforcement or request transformation. This PR lays the foundation for the dynamic webhook middleware system that will allow operators to configure validating and mutating webhooks for MCP request interception.

Changes

File Description
types.go Core types: Type, FailurePolicy, Config (with validation), Request/Response/MutatingResponse, TLSConfig, Principal, RequestContext
errors.go Error hierarchy: WebhookError base type, TimeoutError, NetworkError, InvalidResponseError — all with Unwrap() for errors.Is/errors.As
signing.go HMAC-SHA256 payload signing (SignPayload) and constant-time verification (VerifySignature)
client.go HTTP client with TLS/mTLS support, configurable timeouts, HMAC header injection, response size limiting (1MB), and error classification
types_test.go Config validation tests (12 cases)
errors_test.go Error type, message, and unwrap chain tests
signing_test.go Signing round-trip, tamper detection, malformed signature, and determinism tests
client_test.go HTTP client tests using httptest: validating/mutating responses, HMAC headers, timeouts, response size limits, content type

Design decisions

  • No stutter naming: Types are webhook.Config, webhook.Type, etc. per project lint rules (not webhook.WebhookConfig)
  • Follows existing patterns: Error types follow pkg/config/errors.go (struct with Unwrap()), HTTP client follows pkg/networking/http_client.go (transport setup, TLS config, timeouts)
  • Uses errors.As for error type inspection (not type assertions), matching project convention
  • Signature format: sha256=hex(HMAC-SHA256(secret, "timestamp.payload")) with X-ToolHive-Signature and X-ToolHive-Timestamp headers per RFC spec

Testing

  • 22 tests pass with -race flag
  • 0 lint issues from golangci-lint

Large PR Justification

New foundational package (pkg/webhook/) — the 4 source files are tightly coupled (client depends on types, errors, signing) and form the minimum viable unit for Phase 1. Over 65% of lines are tests.

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
@Sanskarzz Sanskarzz requested a review from JAORMX as a code owner February 16, 2026 19:35
@github-actions github-actions bot added the size/XL Extra large PR: 1000+ lines changed label Feb 16, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Large PR Detected

This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.

How to unblock this PR:

Add a section to your PR description with the following format:

## Large PR Justification

[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformation

Alternative:

Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.

See our Contributing Guidelines for more details.


This review will be automatically dismissed once you add the justification section.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 92.55319% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.14%. Comparing base (0108b11) to head (c219ecb).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
pkg/webhook/client.go 91.22% 6 Missing and 4 partials ⚠️
pkg/webhook/errors.go 93.54% 1 Missing and 1 partial ⚠️
pkg/webhook/types.go 92.30% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3840      +/-   ##
==========================================
+ Coverage   68.77%   69.14%   +0.36%     
==========================================
  Files         473      477       +4     
  Lines       47919    48074     +155     
==========================================
+ Hits        32955    33239     +284     
+ Misses      12299    12253      -46     
+ Partials     2665     2582      -83     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Feb 17, 2026
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Feb 17, 2026
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Feb 17, 2026
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Feb 18, 2026
Copy link
Copy Markdown
Collaborator

@JAORMX JAORMX left a comment

Choose a reason for hiding this comment

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

Hey @Sanskarzz, thanks for putting this together! The foundation looks solid -- clean package structure, good error hierarchy with proper Unwrap() chains, and the HMAC signing implementation is well done. Let's dig into a few things though.

The main blocker is the ValidatingTransport not being applied when TLS config is nil. That leaves the SSRF protections only active in the TLS branch, which doesn't match the #nosec comment's claim. The fix is straightforward (see inline comment), and we should also think about how to handle in-cluster HTTP webhooks (e.g., http://service.namespace.svc.cluster.local) where TLS isn't practical -- probably a configurable opt-in.

Other things to address:

  • t.Context() instead of context.Background() in tests (project convention)
  • classifyError missing context.DeadlineExceeded handling
  • webhookType stored but never read

Nice-to-haves / things to think about for follow-ups:

  • time.Duration JSON serialization (will matter when config integration lands)
  • Principal.Claims as map[string]any for real-world JWT compatibility
  • HTTP 422 distinction per RFC failure mode table
  • Replay protection docs on VerifySignature

The scope here is a solid subset of Phase 1 -- types, client, signing, errors. Looking forward to seeing the middleware integration and config wiring in follow-up PRs!

@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Mar 18, 2026
Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Mar 22, 2026
@github-actions github-actions bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Mar 22, 2026
Copy link
Copy Markdown
Collaborator

@JAORMX JAORMX left a comment

Choose a reason for hiding this comment

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

All feedback addressed. time.Duration serialization deferred to config integration PR as agreed. LGTM.

@JAORMX JAORMX dismissed github-actions[bot]’s stale review March 23, 2026 07:12

justification granted.

@JAORMX JAORMX merged commit 949ae42 into stacklok:main Mar 23, 2026
39 checks passed
JAORMX added a commit that referenced this pull request Mar 23, 2026
Address review feedback on the webhook wire format: auth.Identity
carries Token, TokenType, and Metadata fields that have no business
in external webhook payloads — even redacted.

Extract the credential-free subset (Subject, Name, Email, Groups,
Claims) into auth.PrincipalInfo and embed it in Identity. This gives
structural safety: webhook.Request.Principal is now *auth.PrincipalInfo,
so tokens can never enter the webhook payload. The JSON wire format
uses "sub" with omitempty, matching the original webhook.Principal
contract from #3840.

Field access on Identity (e.g., identity.Subject) continues to work
transparently via Go embedding — only struct literals needed updating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JAORMX added a commit that referenced this pull request Mar 23, 2026
* Replace webhook.Principal with auth.Identity

The webhook package defined its own Principal struct, but auth.Identity
already represents the same concept with a superset of fields. Maintaining
two parallel identity types creates sync burden and will require conversion
logic in the middleware layer. Since the webhook package has no external
consumers yet (v0.1.0), consolidate now before the API solidifies.

Closes #4315

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Introduce auth.PrincipalInfo and embed in Identity

Address review feedback on the webhook wire format: auth.Identity
carries Token, TokenType, and Metadata fields that have no business
in external webhook payloads — even redacted.

Extract the credential-free subset (Subject, Name, Email, Groups,
Claims) into auth.PrincipalInfo and embed it in Identity. This gives
structural safety: webhook.Request.Principal is now *auth.PrincipalInfo,
so tokens can never enter the webhook payload. The JSON wire format
uses "sub" with omitempty, matching the original webhook.Principal
contract from #3840.

Field access on Identity (e.g., identity.Subject) continues to work
transparently via Go embedding — only struct literals needed updating.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Webhook Middleware Phase 1: Core webhook package (types, HTTP client, HMAC signing)

2 participants