picatz / picatz/flowstate

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

Open
#1,502 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

design enhancement examples security
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/webhook ships with an example under examples/plugins/webhook/ and a flow test case, validated by the catalog gate.
  • A package test signs with the task and verifies with webhookverify.go for 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 body is signed as encoded protojson or as a caller-supplied string, and record it beside the inbound verifier's expectations.
  • Whether message_key/idempotency_key naming aligns with slack.post's message_key and #1493's proposed idempotency_key option; one spelling across plugins.

Adjacent: #96 (closed), #1412, #337 (identity in both directions), #1344, #239, #112.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.