feat: add generic bidirectional webhook channel#626
feat: add generic bidirectional webhook channel#626vrknetha wants to merge 2 commits intosipeed:mainfrom
Conversation
8d64b07 to
c5140cf
Compare
c5140cf to
d99cfc1
Compare
|
hi @vrknetha, we are currently undergoing a refactoring of the channel system. I have already opened the refactoring branch, which implements a generic WebSocket channel. Would you be interested in implementing your webhook channel within the new architecture as well? |
nikolasdehor
left a comment
There was a problem hiding this comment.
Thorough implementation with good test coverage (536 lines of tests for 426 lines of code). The security defaults are sensible (loopback-only binding, optional token auth, allowlist). A few observations:
Security concern — token comparison is not constant-time:
In validateJSONRequest, the token comparison token != c.config.Token uses standard string comparison, which is vulnerable to timing attacks. Since this is an auth token check, use subtle.ConstantTimeCompare:
import "crypto/subtle"
if subtle.ConstantTimeCompare([]byte(token), []byte(c.config.Token)) != 1 {
http.Error(w, "Invalid token", http.StatusForbidden)
return false
}Missing auth on outbound route:
The handleOutbound route goes through validateJSONRequest which checks the token, but there is no IsAllowed check for the outbound sender. If the webhook is exposed beyond loopback (user sets webhook_host to 0.0.0.0), anyone could forward messages through the connector. Consider either:
- Also checking
IsAllowedon the outbound route, or - Documenting that outbound auth relies solely on the shared token.
Body read after close:
In handleInbound, defer r.Body.Close() is placed after io.ReadAll. The defer will still execute, but the ordering is misleading — the read has already completed. Move the defer before the read call for clarity and consistency (other channels in this repo follow that pattern).
setRunning(true) before goroutine confirms server is listening:
In Start(), setRunning(true) is called before the go func() that calls server.Serve(). If Serve fails immediately (e.g., TLS config error), the channel reports as running even though it is not. This is a minor race since most failures are caught by net.Listen above, but worth noting.
Outbound route re-forwards to connector — clarify purpose:
The /v1/outbound HTTP handler receives a payload and then forwards it to connectorURL. This means an external caller POSTs to the local server, which then re-POSTs to the connector. The PR description says this is for "outbound send requests -> forwarded to local connector," but it is not immediately obvious why the extra hop is needed vs. the caller posting directly to the connector. A code comment explaining the relay architecture would help future maintainers.
Overall this is well-structured and the test coverage is solid. The constant-time token comparison is the most important fix.
Summary
This PR adds a generic bidirectional webhook channel for PicoClaw.
The channel runs one local HTTP server with two routes:
POST /v1/inboundfor inbound relay deliveries -> PicoClaw inbound message bus.POST /v1/outboundfor outbound send requests -> forwarded to local connector.Default connector forward target:
http://127.0.0.1:19400/v1/outboundWhat is Clawdentity?
Clawdentity is an open identity and messaging protocol for AI agents.
In this PR, Clawdentity is one consumer of the generic webhook channel. The same channel design also supports:
What Changed
WebhookChannel(pkg/channels/webhook.go) as bidirectional:/v1/inbound)/v1/outbound)Send()now forwards outbound bus messages to connectorchannels.webhookconfig (pkg/config/config.go):webhook_pathsend_pathconnector_urlconnector_timeoutpkg/config/defaults.go,config/config.example.json)pkg/channels/webhook_test.go,pkg/config/config_test.go)Request Validation / Auth
POSTContent-Type: application/jsonchannels.webhook.tokenis set):x-webhook-token: <token>Authorization: Bearer <token>Payload Contracts
/v1/inbound): accepts any JSON payload./v1/outbound): expects JSON:to(required)content(required)peer(optional)Security
127.0.0.1by default (loopback only)allow_fromValidation
go test ./pkg/channels ./pkg/config(passing)