Repository navigation
Conversation
Add scalekit.verify_trigger_event(body, /, *, headers, secret) and client.actions.triggers.verify_event(...). Both check the webhook signature first (whsec_ secret, v1 HMAC-SHA256, five-minute window, case-insensitive header names, strict UTF-8), then parse the body into an immutable TriggerEvent model with DeliveryScope, DetectionMode and PayloadState enums that pass unknown values through as str. A correctly signed but malformed event raises the new ScalekitTriggerEventParseException, a WebhookVerificationError subclass. The existing signature logic moves into a private shared module; verify_webhook_payload and verify_interceptor_payload keep their behaviour, covered by new regression tests. Shared JSON test vectors live in tests/fixtures/trigger_events/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify_trigger_event and actions.triggers.verify_event now type `headers` as HeadersLike, a small protocol (get + items) that dict and the header objects of Flask/Werkzeug, Django, Starlette/FastAPI and aiohttp satisfy, so the documented Flask example type-checks. TriggerEvent.payload keeps the value json.loads produced without re-validating it, so payloads of any nesting depth parse; nesting too deep for the JSON decoder raises ScalekitTriggerEventParseException instead of RecursionError. correlation_id is documented as linking follow-up work back to the event. Tests cover real Werkzeug, Starlette and Django header objects, a mypy check of the documented snippet (skipped when mypy is not installed), 1000-level payloads, and that patching scalekit.client.webhook_tolerance_in_seconds still changes the window of the existing verifiers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the trigger event path, a webhook-signature candidate with no comma,
another version, invalid base64 or the wrong decoded length is now
skipped, so a later valid candidate still verifies; a header with no
matching candidate raises WebhookVerificationError("Invalid
signature"). verify_webhook_payload and verify_interceptor_payload keep
their current behaviour, covered by regression tests.
TriggerEvent.payload is typed Annotated[JsonValue, SkipValidation]:
precise for type checkers, still not re-validated at runtime. The
deep-payload tests use 500 levels, which the stdlib JSON decoder
accepts on every supported Python; the docstring notes that Python
3.10/3.11 reject about 1000 levels. The documented example picks the
payload with one conditional expression so it type-checks whatever the
fetch helper returns. HeadersLike docs list only tested frameworks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
base64.b64decode(validate=True) raises a plain ValueError (not binascii.Error) for non-ASCII text, so a candidate such as "v1,éééé" rejected the whole header instead of being skipped. The trigger path now skips any candidate that fails to decode; the existing webhook verifiers are unchanged. The documented-snippet type check now sets MYPYPATH to the checkout and asks mypy for absolute paths, so it checks this checkout and keeps its errors whatever the working directory or installed package. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the trigger path only (verify_webhook_payload and verify_interceptor_payload are unchanged): - Collect every matching header value, case-insensitively. All webhook-signature values are signature candidates. A repeated webhook-id or webhook-timestamp must carry the same value (values joined with "," are split first); different values raise "Multiple <name> headers with different values". - Require webhook-timestamp to be ASCII digits only. Any other value, including an empty one, raises "Invalid Signature Headers". Header values are not trimmed. - Reject a body that starts with a UTF-8 byte order mark as a parse error. - occurred_at: reject offset minutes of 60 or more and years outside 0001-9999, also after normalising to UTC. Document that fractional seconds are truncated to microseconds. - Document the positional-only body in the verify_event signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the trigger path only (verify_webhook_payload and verify_interceptor_payload are unchanged): - The key after the first "_" of the secret must be non-empty, padded standard base64 that decodes to at least one byte; anything else raises "Invalid secret". Before, secrets such as "whsec_", "whsec_!!!!" or "whsec_====" decoded to an empty HMAC key, so an event signed with an empty key verified. A suffix after a second "_" is no longer ignored. The secret is checked after the required headers and before the timestamp format. - verify_trigger_event raises TypeError when headers has no callable items() and get(), instead of an AttributeError. - REFERENCE.md example branches on both delivery scopes and on unknown scopes. - Tests pin the ",[ \t]*" separator for repeated header values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the trigger path, check the argument types and read the signature headers (which raise TypeError for non-str values) before decoding the body, so a wrong argument type raises TypeError even when the body is not valid UTF-8. The body is still decoded before the signature check. Document that TypeError is raised for arguments of the wrong type, and that WebhookVerificationError covers verification failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ravibits
requested review from
AkshayParihar33,
anujsharmask,
dhawani,
saif-at-scalekit and
srinivaskarre-sk
as code owners
October 9, 2026 19:00
saif-at-scalekit
left a comment
Contributor
There was a problem hiding this comment.
Tried verifying a signed AgentKit trigger in Python and branching on who it is for (one connected account vs the whole connection).
After this ships, a Flask (or FastAPI) handler can trust the signature and get a typed TriggerEvent without standing up a client.
Requests
- In the REFERENCE usage sample, label the account-scope branch “connected account” — not bare “account” — so it matches
connected_account_idand the AGENTKIT walk. (REFERENCE.md Trigger Events usage)
Sent by pocket agent saif-waiting-review
…ger sample Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
@saif-at-scalekit Done in 42b5b72. The account-scope branch in the REFERENCE.md usage sample now prints "one connected account:", which matches Generated by Claude Code |
saif-at-scalekit
left a comment
Contributor
There was a problem hiding this comment.
Checked your update against the earlier requests.
- Label the account-scope branch “connected account” — not bare “account” — in the REFERENCE usage sample — done
All 1 look done.
Sent by pocket agent saif-waiting-review
This branch has not been deployed
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.
Description
AgentKit triggers deliver events to your endpoint as signed HTTP requests (
webhook-id,webhook-timestamp,webhook-signatureheaders, the same scheme as other Scalekit webhooks). This PR adds a helper that verifies a delivery and returns it as a typed event:What it checks, in order:
TypeError.webhook-signaturevalue is a candidate.webhook-idandwebhook-timestampmay repeat only with the same value. A missing header raises "Missing required headers".whsec_must be non-empty, padded standard base64. An empty or malformed key raises "Invalid secret", so a misconfigured secret can't verify a forged event.id.timestamp.body, compared in constant time. Malformed candidates are skipped.TriggerEvent.Errors:
WebhookVerificationError.ScalekitTriggerEventParseException, so existingexcept WebhookVerificationErrorhandlers catch it.TriggerEvent:dedupe_key,delivery_scope,occurred_at(timezone-aware UTC, fractional seconds truncated to microseconds),resource_id, the connected account and connection, andpayload.DeliveryScope,DetectionModeandPayloadStateare open enums: unknown values from newer servers are kept, not rejected.Shared code: signature checking moved into
scalekit/common/_webhook_signature.py.verify_webhook_payloadandverify_interceptor_payloadbehave exactly as before: the stricter rules apply only to trigger events, and regression tests pin the old behaviour.Usage
Testing
make lint: passes.mypy --strictreports no errors in the new modules, andruff check/ruff format --checkare clean at line length 100.python -m unittest tests.test_trigger_events: 91 tests pass. They cover:tests/fixtures/trigger_events/)occurred_atedge casesrelease.py checkandrelease.py verify --labels skip-release: pass.Release
No release (skip-release). The notes wait in
.changes/unreleased:Scope
verifyTriggerEvent. Both SDKs behave the same on the same inputs.🤖 Generated with Claude Code
Generated by Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit