From cf82db29bd342a733f91efd4661faa8116d154f7 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Tue, 28 Jul 2026 15:11:26 +0000 Subject: [PATCH] fix(daemon): wire webhook HMAC signing secret flag (PILOT-90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pilot-protocol/webhook library (v0.2.0+) already supports HMAC-SHA256 signing of outbound event payloads via webhook.WithSecret, which sets the X-Pilot-Signature-256 header on every POST. However, the daemon's main.go never defined or passed a secret — the WithSecret option was unreachable from the command line or environment. This commit: - Adds a -webhook-secret CLI flag - Adds PILOT_WEBHOOK_SECRET env var fallback - Passes the value as webhook.WithSecret(...) to the service constructor When a secret is configured, receivers can verify authenticity and integrity of webhook payloads by recomputing the HMAC-SHA256 of the body and comparing it to the X-Pilot-Signature-256 header. When no secret is set (the default), behavior is identical to before — no signature header, fully backward-compatible. Closes PILOT-90 --- cmd/daemon/main.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 53c5b51e..fda84237 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -94,6 +94,7 @@ func main() { noEventStream := flag.Bool("no-eventstream", false, "disable built-in event stream service (port 1002)") noSkillinject := flag.Bool("no-skillinject", false, "disable built-in skill-injection service (agent context injection). Env: PILOT_NO_SKILLINJECT=1.") webhookURL := flag.String("webhook", "", "HTTP(S) endpoint for event notifications (empty = disabled)") + webhookSecret := flag.String("webhook-secret", "", "HMAC-SHA256 pre-shared secret for webhook payload signing (empty = no signature). Env: PILOT_WEBHOOK_SECRET.") adminToken := flag.String("admin-token", "", "admin token for network operations") networks := flag.String("networks", "", "comma-separated network IDs to auto-join at startup") trustAutoApprove := flag.Bool("trust-auto-approve", false, "automatically approve all incoming trust handshakes") @@ -348,7 +349,16 @@ func main() { // bus, the plugin subscribes and POSTs to the configured URL. URL // hot-swap (IPC's `set-webhook`) routes through SetURL on the // plugin via the daemon's WebhookManager interface. - webhookSvc := webhook.NewService(*webhookURL) + // Resolve webhook secret: explicit flag overrides env var + webhookSecretVal := *webhookSecret + if webhookSecretVal == "" { + webhookSecretVal = os.Getenv("PILOT_WEBHOOK_SECRET") + } + var webhookOpts []webhook.Option + if webhookSecretVal != "" { + webhookOpts = append(webhookOpts, webhook.WithSecret(webhookSecretVal)) + } + webhookSvc := webhook.NewService(*webhookURL, webhookOpts...) if err := rt.Register(webhookSvc); err != nil { log.Fatalf("register webhook: %v", err) }