Expand description
Webhook ingress — the swappable public-reachability seam (P6a of the
unified-tool-gateway epic, #479). Extracted from apps/core into its own
crate (program §3/§4 W3); the kernel couplings are inverted through
WebhookIngressHost, so this crate has ZERO dependency on apps/core.
Composio triggers are webhook-delivered: there is no event-pull API, so a
local Core bound to 127.0.0.1 never receives them. This crate is the
swappable seam that gives Core a publicly-reachable URL pointed at its existing
handler (POST /api/composio/webhook, reached via the host), so a trigger
fires unchanged. Public webhook routes stay kernel-ingress in Core (program
§5) and forward into this engine.
Core vs Gateway (CLAUDE.md §1): exposing a tunnel + deciding which backend runs is what runs → Core. There is no policy here.
“Nothing hardcoded” (CLAUDE.md §1): the backend is a swappable Ingress
enum selected by the webhook.ingress.backend pref, with an
RYU_WEBHOOK_INGRESS_URL env override for the BYO (OwnRelay) case. The default
is the managed IngressKind::RyuRelay.
Backend dispatch uses native async fn trait methods (not object-safe) + a
closed Ingress enum match-dispatched — no async-trait, no dyn. See
[tunnels]. (The host seam is the one dyn/async-trait boundary.)
Structs§
- Cloudflared
Source - Cloudflared — adopt-or-spawn a
cloudflaredquick tunnel pointed at Core’s local port.start()spawnscloudflared tunnel --url http://localhost:<port>, parses the assignedhttps://<sub>.trycloudflare.combase from its output, and holds the child alive for the process lifetime (dropping the child tears the tunnel down).public_url()returns that base joined withWEBHOOK_PATH. No account/login is needed — quick tunnels are anonymous and ephemeral, which is exactly the BYO-public-URL contract this seam needs. Requires thecloudflaredbinary on PATH; spawn failure errors clearly. - OwnRelay
Source - OwnRelay — the BYO ingress: the user already exposes Core (or a reverse
proxy) at a public URL and configures it here. The base comes from the env
RYU_WEBHOOK_INGRESS_URL(preferred) or the value handed at construction (e.g. a pref).public_url()appends the webhook path. - RyuRelay
Source - RyuRelay — the managed push relay (the default). Core opens an outbound
SSE subscription to
apps/server; Composio POSTs to a public ingress URL and the server fans the payload out over that stream, which Core dispatches in-process. The register + SSE-client loop live in [super::ryu_relay]; this source delegates to them. - Tailscale
Funnel Source - TailscaleFunnel — exposes Core’s bind port to the public internet via the
P5 mesh’s Tailscale Funnel. Consumes the host’s
ensure_funnel/funnel_url(Core forwards tocrate::mesh). When the mesh is not enabled/available — or no host is installed — it stub-errors with a clear “Phase 5” message so this runs standalone.
Enums§
- Inbound
Outcome - The outcome of routing one inbound webhook by path.
- Ingress
- The closed set of ingress backends, match-dispatched (no
async-trait/dyn). - Ingress
Kind - The four ingress backends. Serializes kebab-case so the wire form and the pref
value round-trip (
ryu-relay/tailscale-funnel/cloudflared/own-relay). - Workflow
Webhook Outcome - The outcome of delivering a per-workflow webhook. Rich enough that both the
axum
workflow_webhookhandler (→ HTTP status) and the relay dispatcher (→ log line) map from the same decision, so their auth can never drift. - Workflow
Webhook Secret - The raw result of looking up a workflow’s webhook trigger secret. The crate
(not the host) owns the empty-secret →
NoSecretdecision, so this returns the trigger’ssecretfield verbatim (Secret(None)when the trigger exists but carries no secret at all).
Constants§
- INGRESS_
BACKEND_ PREF - The pref key selecting the active ingress backend (
webhook.ingress.backend). - INGRESS_
URL_ PREF - The pref key holding the BYO public base URL (the OwnRelay fallback when the
RYU_WEBHOOK_INGRESS_URLenv override is absent). - OWN_
RELAY_ URL_ ENV - The env var a BYO operator sets to declare Core’s public base URL.
- WEBHOOK_
PATH - The path Composio is pointed at. Every tunnel/relay appends this to its public base so an inbound webhook lands on Core’s existing handler.
Traits§
- Webhook
Ingress - The webhook-ingress trait every backend implements. Native
async fn(not object-safe) → stored via the closedIngressenum, neverdyn. - Webhook
Ingress Host - Every kernel coupling the webhook-ingress engine needs, inverted.
dyn-stored (→async_trait), installed once at boot. Implemented by Core; the crate’s own tests install a mock.
Functions§
- configured_
kind - The configured backend kind, resolved from (1) the
RYU_WEBHOOK_INGRESS_URLenv override ⇒IngressKind::OwnRelay, else (2) thebackend_pref(webhook.ingress.backend), else (3) theIngressKind::DEFAULT(RyuRelay). Shared byfrom_prefs, the backend selector, and the status handler so they never disagree. - deliver_
inbound - Route an inbound webhook to the correct in-process handler by
path. - deliver_
workflow_ webhook - Verify and (on success) fire a per-workflow webhook trigger. This is the single source of truth for the workflow-webhook auth + run path — the HTTP handler and the relay dispatcher both call it, guaranteeing identical fail-closed semantics.
- ensure_
relay_ started - Idempotently ensure the RyuRelay subscription is live (register + SSE loop).
- first_
http_ delivery - Process-global dedup set for DIRECT-HTTP deliveries. The relay transport
keeps its own per-subscription set (ryu_relay.rs); this one covers the
public HTTP handlers, which face the same at-least-once retry semantics.
Returns true when
idis new (dispatch) — false when already seen (skip). An empty id is always “new”: deliveries without a delivery-id header are not dedupable and pass through unchanged. - from_
prefs - Build the configured
Ingressfrom the resolved pref values + the local server URL. - last_
delivery - The unix-seconds timestamp of the last accepted delivery for
path, if any. - public_
base_ url - The resolved public origin base URL (no webhook path) — but ONLY when the
active ingress is a true reverse-proxy origin that forwards every path to
Core.
Noneotherwise. - public_
url - The current public ingress URL, if one has been resolved.
- record_
delivery - Record that
pathjust received (and accepted) a delivery, stamping “now”. Called from both the relay dispatcher and the direct HTTP handlers so the registry reflects every source. - relay_
inbound_ url - The reachable public URL a third party POSTs to so an inbound webhook at
pathreaches THIS node over the RyuRelay ingress:<relay_base>/api/composio-relay/inbound/<relay_token>/<path>.Noneuntil the node has registered with the relay (no persisted token yet). This is how a per-workflow webhook becomes discoverable under the default (RyuRelay) ingress, where there is no path-forwarding origin base. - set_
global_ host - Install the host implementation. Called once from
apps/coreat startup (unconditionally — Core consumes this crate as a non-optional dependency and the public webhook routes reach it in every build). Idempotent: a second call is ignored. - set_
public_ url - Publish the resolved public ingress URL for
GET /api/webhook-ingress/status. - timestamp_
fresh - Whether an inbound delivery is fresh enough to accept, given the value of a
timestamp header (e.g. Svix/Composio
webhook-timestamp) if present. - workflow_
webhook_ path - Build the canonical per-workflow webhook path for
id. The registry (GET /api/webhooks) and the delivery recorder use this so the stored last-delivery key and the advertised URL never drift.