Webhook triggers
A trigger is a secret URL on this machine. Each event that an outside service sends to it either sends a message to one existing conversation or starts a new conversation in a project. Events reach the machine directly through Tailscale Funnel, so overdeck.ai never sees them and you need no overdeck.ai account.Available from the next release. Manage triggers with
pan triggers or
Settings → Anywhere → Triggers.What a trigger does
Every trigger has one target:- Conversation target. Each event is sent as one message to an existing conversation,
the same way the composer sends one. The conversation must still be open; an ended or
archived conversation answers
410. - Project target. Each event starts one new conversation in a registered project. The conversation’s first message is your prompt template followed by the event. A project target needs a prompt template. You can also set the model, harness and effort for the conversations it starts; unset values follow the normal defaults.
ping event. It has
no repository or event-type filter, because the URL is the authorization and GitHub and
Stripe already let you choose which events to send. A trigger can only send a message or
start a conversation. It never answers a question, picks a menu option or approves a
permission (see Limits and refusals).
Create a trigger
From the terminal:--conversation takes a conversation name or its numeric id; the trigger stores the name.
See pan triggers for every flag.
From the dashboard: open Settings → Anywhere, then Add trigger in the Triggers panel.
Either way you get the trigger’s URL once:
.ts.net host is saved (Settings → Anywhere), Overdeck fills in the real host;
otherwise it prints the <machine>.<tailnet> placeholder for you to replace.
pan triggers add also prints a local test URL, http://127.0.0.1:<dashboard port>/hooks/…,
which works on this machine without Funnel:
Signing secrets
A trigger’s verification kind isnone, github or stripe. With github or stripe,
the dashboard checks the event’s signature over the exact request bytes before it does
anything else with the body. A trigger with github or stripe and no signing secret
refuses every event with 503.
GitHub. When you create a github trigger without --signing-secret, Overdeck generates
a secret and shows it once with the URL. In the repository’s Settings → Webhooks → Add
webhook form, paste the URL into Payload URL and the secret into Secret. Either
content type works. GitHub signs each event with X-Hub-Signature-256.
Stripe. Stripe makes the secret, so create the trigger first, create the endpoint in
Stripe with the trigger’s URL, then copy the endpoint’s whsec_… secret into the trigger:
set-secret reads the secret from one line of standard input (or pass --secret). Stripe
signs each event with Stripe-Signature. Overdeck accepts any matching v1 signature, so a
rolled secret keeps working during Stripe’s overlap window, and it refuses an event whose
timestamp is more than 300 seconds before or after this machine’s clock.
No route or command shows a signing secret after you set it. To change one, set it again.
Publish with Tailscale Funnel
Tailscale Funnel publishes one local address on the public internet through Tailscale’s relays. Overdeck publishes only the/hooks path, on port 8443.
Prerequisites:
- Tailscale is installed on this machine and signed in to your tailnet.
- HTTPS certificates are on for the tailnet (admin console → DNS → HTTPS Certificates).
-
The tailnet policy gives this machine the
funnelnode attribute. -
Your user may run Tailscale serve commands. Run this once:
3011 if your dashboard API port is
different; pan triggers add and the Settings panel print the command with your port):
/hooks because Funnel strips the mount path before it proxies the
request. Port 8443 keeps Funnel off port 443, where Tailscale Serve may publish your
dashboard to the tailnet; Funnel is allowed per host and port, so funneling 443 would also
publish that dashboard. The public URL form is:
tailscale command itself.
What the agent sees
The event never reaches the agent as raw text. Overdeck wraps it in a fixed envelope:- The message always starts with fixed text, so a body that starts with
/handoffor any other command never runs as one. - The nonce is random for each event, so the body cannot fake the end marker.
sourceisgithuborstripeonly when that signature was verified, elseunsigned.- Control characters in the body (carriage returns, Ctrl-C, escape sequences) become
�. - At most 44 KiB of the body is included. A longer body is cut, marked
truncated=yes, and ends with[truncated: <n> more bytes not shown]. The cap keeps the whole message under the 50,000-character limit that every conversation message has. The signature is still checked over the full body, up to 1 MiB.
Limits and refusals
- Rate limits. A project trigger starts at most 6 conversations per rolling hour; a conversation trigger sends at most 60 messages per rolling hour. The count lives in the dashboard’s memory and resets when the dashboard restarts.
- Pending input. While the target conversation has an open question, plan approval,
permission prompt or menu, the trigger answers
503instead of sending, so an event can never answer it. - CPU pressure. While the host’s CPU is saturated, every trigger answers
503. - Nothing is queued. A refused event is not stored. GitHub does not retry failed deliveries (use Redeliver in the webhook’s Recent Deliveries); Stripe retries non-2xx deliveries for up to three days.
- “Started” means the conversation was created and its launch requested; the agent becoming ready is not part of the HTTP answer.
Security
- The URL is a bearer secret. Anyone who has it can send events. Treat it like a password, and remove the trigger if it leaks.
- Funnel requests reach only
POST /hooks/*. Tailscale marks every funneled request with theTailscale-Funnel-Requestheader and removes any copy a client sends. The dashboard answers such a request with404on every other method and path (even with a credential), never treats it as a local caller, refuses it a dashboard session, and refuses its WebSocket upgrades. - overdeck.ai never sees Funnel events. They travel from the sender through Tailscale’s relays to this machine.
- Unknown URLs cost nothing. The dashboard checks the trigger id and URL secret before
it reads any of the body, and it stops reading a body as soon as it passes 1 MiB, even
when the sender gives no
Content-Length. - Logs name the trigger id and the outcome only, never the body or a secret.
- The trigger store is
~/.overdeck/triggers.json, mode0600. It holds the URL secret’s hash and the signing secrets in plain text, because the machine must compute the HMAC. - A directory inbox that holds events while this machine is offline is planned separately (PAN-2356).