feat(automations): add webhook trigger type#3426
Conversation
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsSuggested: Minor ( React with an emoji to override the release type:
Current version:
|
There was a problem hiding this comment.
2 issues found across 21 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/tools/automations/trigger-rotate-token.ts">
<violation number="1" location="apps/mesh/src/tools/automations/trigger-rotate-token.ts:76">
P1: Old token can stay valid after rotate. Delete failure is swallowed, and webhook auth does not enforce current `trigger.api_key_id`. Reject non-current key or fail rotation when revoke fails.</violation>
</file>
<file name="apps/mesh/migrations/082-automation-webhook-triggers.ts">
<violation number="1" location="apps/mesh/migrations/082-automation-webhook-triggers.ts:41">
P1: Down migration can fail if webhook triggers exist. The `ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))` will fail when existing rows have `type = 'webhook'`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if (oldKeyId) { | ||
| try { | ||
| await ctx.boundAuth.apiKey.delete(oldKeyId); | ||
| } catch (err) { |
There was a problem hiding this comment.
P1: Old token can stay valid after rotate. Delete failure is swallowed, and webhook auth does not enforce current trigger.api_key_id. Reject non-current key or fail rotation when revoke fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/tools/automations/trigger-rotate-token.ts, line 76:
<comment>Old token can stay valid after rotate. Delete failure is swallowed, and webhook auth does not enforce current `trigger.api_key_id`. Reject non-current key or fail rotation when revoke fails.</comment>
<file context>
@@ -0,0 +1,90 @@
+ if (oldKeyId) {
+ try {
+ await ctx.boundAuth.apiKey.delete(oldKeyId);
+ } catch (err) {
+ console.warn(
+ `[trigger-rotate] failed to delete old key ${oldKeyId} for trigger ${trigger.id}:`,
</file context>
There was a problem hiding this comment.
Fixed by enforcing current key id in the webhook auth route. automation-webhooks.ts now rejects with 401 "Stale token" when verified.apiKeyId !== trigger.api_key_id, so even if the best-effort delete in rotate fails (or if any other path leaves a stale key with the FIRE permission), the old token stops working the moment the swap row update commits. Left the rotate delete as best-effort with a log since the auth-side check makes it harmless.
There was a problem hiding this comment.
Thanks for the feedback! I've saved this as a new learning to improve future reviews.
| await sql`ALTER TABLE automation_triggers DROP CONSTRAINT chk_trigger_type`.execute( | ||
| db, | ||
| ); | ||
| await sql`ALTER TABLE automation_triggers ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))`.execute( |
There was a problem hiding this comment.
P1: Down migration can fail if webhook triggers exist. The ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event')) will fail when existing rows have type = 'webhook'.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/migrations/082-automation-webhook-triggers.ts, line 41:
<comment>Down migration can fail if webhook triggers exist. The `ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))` will fail when existing rows have `type = 'webhook'`.</comment>
<file context>
@@ -0,0 +1,44 @@
+ await sql`ALTER TABLE automation_triggers DROP CONSTRAINT chk_trigger_type`.execute(
+ db,
+ );
+ await sql`ALTER TABLE automation_triggers ADD CONSTRAINT chk_trigger_type CHECK (type IN ('cron', 'event'))`.execute(
+ db,
+ );
</file context>
There was a problem hiding this comment.
Fixed. Followed the convention from migration 078 (chk_automation_kind): delete rows that violate the narrower CHECK before re-applying it in the down migration.
Webhook triggers are a third option alongside cron and event, letting external systems fire an automation via HTTP POST. Authentication uses a Better Auth API key scoped to the trigger; the token is shown once at creation and can be rotated. The callback route bypasses the buggy event-bus dispatch path and enqueues directly through the same DBOS gates as cron/event fires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Reject stale API keys in webhook auth by comparing against trigger.api_key_id - Delete webhook rows in down migration before re-applying narrower CHECK
cce28fa to
bb6b28e
Compare
- Add curl/fetch snippet variants and a "Token in URL" checkbox that rewrites the URL and removes the Authorization header in lockstep. - Render the snippet in a readonly Monaco editor for syntax highlighting (shell for curl, typescript for fetch). - Fix dialog content overflowing the modal box (missing min-w-0 on flex/grid children with long URLs/tokens). - Rename "Example" label to "Snippet". Extends MonacoCodeEditor with a "shell" language option and a disableDiagnostics prop for clean readonly display.
…-trigger-automation # Conflicts: # apps/mesh/migrations/index.ts
…fail When `streamText.onError` fires (model API error, credential decrypt failure, permission denial, etc.), the run was force-finished as `failed` but no assistant message was persisted. The UI's user/assistant pairing then drops the orphan and renders "No response was generated". This is especially painful for background runs (cron, webhook, event-bus automations) where the user has no console to read the swallowed `console.error` from. Now the sanitized error is written as a synthetic assistant message so it shows up directly in the thread and in the DB.
Background automation fires (cron, webhook, event-bus triggers) flow
through dispatch-run with the default `DispatchTarget` (`local`/`default`),
which sets `ctx.sandboxPreference = "default"` but never populates
`ctx.linkForCurrentRun`. In local dev `STUDIO_SANDBOX_RUNNER` is unset,
so `resolveSandboxProviderKindFromEnv()` returns "desktop" — and the
"default" branch was passing that straight to `getSandboxProviderByKind`,
which routes to `instantiate("desktop")` and throws:
desktop runner cannot be instantiated without a per-run LinkEntry —
call resolveSandboxProvider, which binds the link before constructing
the provider.
The error was being swallowed by `streamText.onError`, surfacing in the
UI only as "No response was generated" with 0 tokens.
Route the "default" path through `bindProviderForKind` instead, which
already handles the desktop-needs-a-link case by looking up the user's
link via `ctx.linkRegistry`. Non-desktop env kinds (docker, agent-sandbox)
still fall through to `getSandboxProviderByKind` unchanged.
…automation detail Spreads the AgentModelTrigger-style collapse fix to two more spots: - Tools button (chat input) and SimpleModeTierDropdown: add `gap-0 @[496px]/chat-bottom:gap-1.5` to the outer button so the icon doesn't carry a phantom `gap-2` (Button default) when the label is collapsed to max-w-0. - Automation detail bottom bar: add `@container/chat-bottom` so the SimpleModeTierDropdown's `@[496px]/chat-bottom:` container queries actually match — the label was permanently hidden there before because no parent named the container.
What is this contribution about?
Adds a third automation trigger type,
webhook, alongsidecronandevent. Creating one mints a Better Auth API key scoped via{ webhook_<trigger_id>: ["FIRE"] }and returns a one-time URL + token; senders POST JSON to/api/:org/webhooks/:triggerId(or/:triggerId/:token) with eitherAuthorization: Bearer <token>or the path-embedded form. A newAUTOMATION_TRIGGER_ROTATE_TOKENtool rotates the secret (mint new, swap, revoke old), andAUTOMATION_TRIGGER_REMOVEnow also deletes the underlying key. The callback route bypasses the buggy event-bus dispatch path and goes straight throughenqueueAutomationFireso webhook fires still flow through the same DBOS org/global concurrency gates. UI gains a Webhook option in the starter popover, a one-time secret reveal dialog (URL/token/curl), and a Rotate action on the trigger card.How to Test
bun run --cwd=apps/mesh migrateto apply082-automation-webhook-triggers.curl -X POST '<URL>' -H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' -d '{\"hello\":\"world\"}'→ expect202 { ok: true, trigger_id }and the automation to run with the payload in its context.curl -X POST '<URL>/<token>'— same result.Migration Notes
Migration
082-automation-webhook-triggersrelaxes thechk_trigger_typeconstraint to include'webhook'and adds a nullableapi_key_idcolumn toautomation_triggers. Safe to roll forward; the down migration restores the old constraint after clearing the new column.Review Checklist
🤖 Generated with Claude Code
Summary by cubic
Adds a new
webhooktrigger so external systems can fire automations via HTTP POST with scoped Better Auth tokens; includes DB migration090-automation-webhook-triggersto allowtype: "webhook"and storeapi_key_id. Also improves background runs by persisting stream errors as assistant messages, fixing desktop sandbox binding for default targets in local dev, and small chat UI polish.New Features
POST /api/:org/webhooks/:triggerId(Bearer) orPOST /api/:org/webhooks/:triggerId/:token; accepts JSON up to 1MB, forwards as untrusted context, returns 202; enqueues directly through DBOS gates.{ webhook_<triggerId>: ["FIRE"] }, cross-checks org/trigger, and rejects stale tokens by matchingapi_key_id; 401/403 on invalid/mismatch.AUTOMATION_TRIGGER_ADDsupportstype: "webhook"and returns a one‑time URL + token;AUTOMATION_TRIGGER_ROTATE_TOKENissues a new token and revokes the old;AUTOMATION_TRIGGER_REMOVErevokes the key. Starter adds “Webhook” with a one‑time secret dialog (curl/fetch snippets + “Token in URL”), and Trigger card adds a Rotate action.Bug Fixes
sandboxPreference="default"resolves to desktop, bind the user’s link instead of instantiating directly; clearer error if no link is available.Written for commit 54c09a7. Summary will update on new commits. Review in cubic