plugins: outbound signed webhooks — `webhook.send`, the half of #96 that closed unbuilt, signing with the same scheme vocabulary the inbound verifier speaks so the two prove each other
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 9
- Forks
- 0
- Avg merge
- 3h 3m
- Merged PRs (30d)
- 509
Description
Problem
#96 asked for webhooks in both directions and closed with PR #1412, which landed the inbound bridge: a verified delivery can now answer a wait_for_signal: gate. The outbound half — "a workflow sends a webhook with the secret store doing signing and auth" — did not land and is tracked nowhere; the September roadmap lists "no outbound webhook signing" as a thin edge (docs/plans/2026-09-roadmap.md, the ingress row). Today a workflow that must notify a receiver expecting a signature writes the HMAC by hand in CEL (which cannot, since the key is a secret reference and never a value in an expression) or sends unsigned.
The engine already has the vocabulary: webhookverify.go implements a generic WebhookSchemeHMACSHA256 (signature header plus timestamp) and WebhookSchemeStripe, with a test that holds the declarable set and the implemented set together. An outbound signer that speaks the same schemes gives a round-trip proof the tree can run against itself — sign with the task, verify with the engine — and makes Flowstate-to-Flowstate federation over webhooks a first-class path (examples/federation-flow-to-flow today calls the RPC directly).
Why a plugin task rather than a sign: on the built-in http task
#234 and #112 keep the built-in registry to log and http and keep protocol families out of core syntax; #239's guardrail says a task earns a process boundary by speaking a protocol http cannot or by needing credential handling http refuses. Signing needs the secret value at the point of use and a body canonicalisation the receiver agrees with, which is the second condition. Inbound verification lives in the engine because it is control-plane work on a trigger; outbound signing is an activity, so a plugin is the house shape and keeps the DSL unchanged.
Proposed shape
webhook.send: url, body (a Value, JSON-encoded canonically), signing_key (required_secret_inputs), scheme (an enum mirroring the engine's inbound schemes, so adding a scheme is one change in one table), idempotency_key (sent as Idempotency-Key and available to the receiver's dedupe), bounded headers. Sent through sdk.HTTPClient() with sdk.WithCredentials (a signature is what the receiver authenticates by, so the operator's credentials && rules must see it), outputs status and a bounded response. Outcome classification as http's: a lost response after the request was written is OutcomeUnknown.
Acceptance criteria
plugins/webhookships with an example underexamples/plugins/webhook/and aflow testcase, validated by the catalog gate.- A package test signs with the task and verifies with
webhookverify.gofor every scheme the engine declares, and the engine's "every declarable scheme is implemented" test gains the outbound direction, so a scheme added to one side without the other fails CI. - An example shows one Flowstate deployment starting a run on another through its webhook trigger with a signed delivery, replacing or complementing
examples/federation-flow-to-flow. - The signing key never appears in outputs, errors or history; the scrubber test covers the signature header's own bytes as well as the key.
Open questions
- Canonicalisation: the receiver verifies bytes on the wire, so the task must sign exactly what it sends; decide whether
bodyis signed as encoded protojson or as a caller-supplied string, and record it beside the inbound verifier's expectations. - Whether
message_key/idempotency_keynaming aligns withslack.post'smessage_keyand #1493's proposedidempotency_keyoption; one spelling across plugins.
Adjacent: #96 (closed), #1412, #337 (identity in both directions), #1344, #239, #112.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading webhookverify.go and the existing plugin and catalog-test patterns, then inspect examples/federation-flow-to-flow and the referenced inbound scheme test. Resolve the body canonicalisation and naming questions against the inbound verifier before implementing plugins/webhook. Done means every declared scheme signs and verifies round-trip, the example and flow test pass the catalog gate, and secret material is absent from outputs, errors, and history.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100