Skip to content

feat(triggers): external webhook trigger for agent dispatch - #10

Merged
suda merged 2 commits into
mainfrom
feature/external-webhook-trigger
Aug 2, 2026
Merged

feat(triggers): external webhook trigger for agent dispatch#10
suda merged 2 commits into
mainfrom
feature/external-webhook-trigger

Conversation

@suda

@suda suda commented Aug 2, 2026

Copy link
Copy Markdown
Member

Summary

New internal trigger internal:external-webhook: any third-party system can dispatch an agent by POSTing to the router:

POST {router}/external/webhook/:projectId/:agentType
Authorization: Bearer <password>

The POST body (≤64 KiB) reaches the agent as its work request — appended to the rendered task prompt as an ## External trigger request section (central, gated on the trigger type). Dispatch rides the existing manual-run dashboard-job path; runs appear with trigger external-webhook.

Configuration

  • Enable per agent: project → Agents → agent → Triggers tab (trigger declared on implementation, planning, splitting, backlog-manager, review, resolve-conflicts, alerting; defaultEnabled: false).
  • When enabled, the tab shows the unique webhook URL (copy button + curl example) and the password field.
  • Password = EXTERNAL_WEBHOOK_PASSWORD_<AGENT_TYPE> project credential — org-credential inheritance applies, so one org-level password can arm all projects. Server enforces ≥16 chars.

Security (adversarially reviewed, all confirmed findings fixed)

  • Fail closed: no stored password → every request rejected.
  • Anti-enumeration: unknown project / unset password / wrong token return an identical generic 401; distinct decision reasons only in webhook_logs (source external; Authorization header never logged).
  • Rate limiting: failed attempts IP-limited via the shared sliding-window limiter (429 + Retry-After); success resets.
  • Timing-safe password compare; Bearer scheme case-insensitive (RFC 7235).
  • Worker isolation invariant: EXTERNAL_WEBHOOK_PASSWORD_* keys are excluded from worker container env injection — agents can never read the inbound-auth verifier (prevents prompt-injection → self-re-dispatch, incl. cross-project via org-level passwords).
  • Body read incrementally with the cap; oversized bodies never fully buffer.

Notes

  • 200 = queued, not "will run" — in-worker validation can still fail the run (e.g. alerting agent without Sentry).
  • No dedup/rate cap on successful dispatches in v1 (mirrors manual-run semantics); follow-up candidate: 429 cap on queued/running external-webhook runs per (project, agent).
  • New trigger appears in existing projects after router restart (definition seeding).

Testing

11,254 unit tests green. New: endpoint matrix (fail-closed, anti-enumeration triple, rate limit + reset, case-insensitive Bearer, body cap, generic 500, log hygiene), worker-env exclusion, prompt-append gating, password policy, helper key mapping, YAML declaration guard.

🤖 Generated with Claude Code

suda and others added 2 commits August 2, 2026 18:33
New internal trigger internal:external-webhook: any third-party system can
dispatch an agent by POSTing to the router at
/external/webhook/:projectId/:agentType with Authorization: Bearer <password>.

- Password is the EXTERNAL_WEBHOOK_PASSWORD_<AGENT_TYPE> project credential
  (org-credential inheritance applies). Authentication fails closed: no
  stored password rejects every request with 403; comparison is timing-safe.
- POST body (capped at 64 KiB) reaches the agent as trigger context via the
  manual-run path's triggerCommentBody. Dispatch reuses createQueuedRun +
  submitDashboardJob({type:'manual-run'}) — no new job variant, adapter, or
  trigger handler. Runs carry triggerType 'external-webhook'.
- Enablement enforced end-to-end: agent enabled in project + trigger enabled
  (getResolvedTriggerConfig); unknown/undeclared/disabled all return 404
  (anti-probing) with distinct decision reasons in webhook_logs
  (source 'external'; Authorization header never logged).
- Declared on 7 agents (implementation, planning, splitting, backlog-manager,
  review, resolve-conflicts, alerting), defaultEnabled false.
- UI: Agents → agent → Triggers tab shows the webhook URL (copy button +
  curl example) and password field when the trigger is enabled, via a new
  renderTriggerExtra slot on DefinitionTriggerToggles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses all confirmed review findings plus one verified manually:

- CRITICAL: the POST body never reached the 7 declaring agents' prompts
  (only respond-to-* task prompts render commentBody). New
  appendExternalTriggerRequest appends the payload to the rendered task
  prompt centrally, gated on triggerType === 'external-webhook'.
- HIGH: EXTERNAL_WEBHOOK_PASSWORD_* credentials are inbound-auth verifiers
  and are now excluded from worker container env injection — a
  prompt-injected agent could otherwise read the password and gain a
  self-re-dispatch primitive (org-level passwords escalated cross-project).
- Brute-force resistance: failed attempts are rate-limited per client IP
  via the shared sliding-window limiter (429 + Retry-After; successful
  auth resets the counter), and webhook passwords now require >=16 chars
  at both credential write paths (project + org).
- Anti-enumeration: unknown project, unset password, and wrong token all
  return an identical generic 401; distinct decision reasons live only in
  webhook_logs.
- Bearer scheme matched case-insensitively (RFC 7235).
- Body read incrementally with the 64 KiB cap instead of buffering first.
- Enqueue failures return a generic message; detail stays in logs.
- internal:external-webhook added to TRIGGER_REGISTRY (definition editor).
- Webhook URL display no longer falls back to the dashboard API_URL (wrong
  service) — router URL comes from WEBHOOK_CALLBACK_BASE_URL, with a
  dev-only origin swap, else an explicit placeholder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@suda
suda merged commit 212c169 into main Aug 2, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant