dsh-webhook

August 31, 2026 · View on GitHub

dsh-webhook — verified HTTP events become durable Automation Runs

dsh-webhook

English | 中文

A durable inbound-webhook Trigger adapter for DSH Automation. It verifies HTTP events, persists a receipt, and submits an idempotent fresh-Session Run. It never executes an Agent turn itself.

The responsibility boundary is deliberate:

  • dsh-webhook owns HTTP serving, authentication, rate limits, receipt durability, source deduplication, replay, and outbound callbacks.
  • dsh-automation owns the Run queue, fresh canonical Sessions, concurrency, retries, cancellation, event history, retention, and worker recovery.
  • dsh-cron is the equivalent time-driven Trigger adapter.

Install

Install dsh-automation first, then this adapter:

dsh plugin --profile web add github:cofy-x/dsh-automation
dsh plugin --profile web add github:omdsh-dev/dsh-webhook

A Git install runs the package's self-contained prepare build. If pnpm asks, allow the exact package key it prints in the profile's pnpm-workspace.yaml, then repeat the add. Verify the composed rows with dsh --profile web --dump-config.

The plugin listens on 127.0.0.1:8788 by default. Only the process holding the listener lock accepts events and reconciles Automation results; other processes sharing the same Harness home remain management-only and can take over.

Lifecycle

HTTP request
  → authenticate and enforce limits
  → persist verified receipt (accepted)
  → submit Run with a stable idempotency key
  → persist Automation Run id (submitted)
  → consume durable Automation events after restart
  → project terminal outcome (settled)
  → dispatch matching callbacks

The receipt is committed before submission. If the process dies after Automation accepted the Run but before the receipt stored its Run id, startup resubmits the same key and receives the same Run. Unknown model or tool side effects are never retried by the adapter.

For a source event id, the key is v1:<hook-id>:<event-id>. When a sender provides no recognized id header, the persisted delivery id becomes the occurrence id. Manual replay deliberately creates a new receipt and a new occurrence.

Usage

Model tools:

  • webhook_add — register POST /hooks/<name> with a prompt template, auth profile, and optional absolute cwd.
  • webhook_list, webhook_remove, webhook_pause, webhook_resume — manage hooks.
  • webhook_deliveries — inspect receipt and linked Run projections.
  • webhook_replay — submit a stored, previously verified payload as a new occurrence.
  • webhook_callbacks — inspect outbound callback attempts.

Human command examples:

/webhook add github-ci "Review {{payload.repository.full_name}} event {{header.x-github-event}}" auth=hmac-sha256 secret=GITHUB_WEBHOOK_SECRET
/webhook deliveries github-ci
/webhook replay dl-2
/webhook pause github-ci
/webhook resume github-ci
/webhook remove github-ci

Hooks created by a command or tool capture the creating Session's absolute workspace as a fresh Automation target. API or static hooks must provide cwd or inherit defaultCwd. Migrated legacy hooks without either are paused with a migrationIssue; set a fresh target before resuming them.

Verification

Secrets are never stored in hook definitions. A secretRef is resolved through the Harness credentials service at request time.

AuthVerificationTypical sources
hmac-sha256HMAC-SHA256 of the raw body; configurable signature header; constant-time comparisonGitHub and compatible senders
bearerBearer token or configurable token headerGitLab, Grafana, CI and scripts
noneSource must be loopbacklocal scripts only

Wrong signature returns 401; disallowed source 403; unknown hook 404; rate exhaustion 429; oversized body 413. A public 0.0.0.0 bind refuses secret-less hooks at load and creation time.

Recognized source occurrence headers, in order, are X-GitHub-Delivery, X-GitLab-Delivery, and X-Request-Id. Repeating one within retained history records a rejected duplicate and does not create another Run.

Receipts and reconciliation

Each receipt contains bounded headers and payload, the stable idempotency key, linked automationRunId, Automation state, terminal outcome, result excerpt or error, and callback status. Receipt states are:

  • accepted: verified and persisted, but Run id is not yet known;
  • submitted: linked Run is non-terminal;
  • settled: linked Run is terminal (succeeded, failed, cancelled, or indeterminate);
  • rejected: source-level duplicate or another pre-submission rejection.

The adapter owns a durable Automation consumer checkpoint named webhook.adapter.v1. It advances its local cursor and the central checkpoint after projecting each scanned page. If retention has pruned an old cursor, it refreshes every linked Run by id, advances to the published prune watermark, and continues. A terminal callback is emitted only on the first non-terminal-to-terminal projection.

Legacy delivered and held receipts remain readable as migration audit records; new events never use those states.

Callbacks

Terminal receipts fan out to matching global rules and hook-local targets. HTTP targets receive JSON and may use a credential-backed bearer token. local://macos-notification is also supported. Failed callbacks use a persistent exponential-backoff queue (2 seconds doubling, five-minute cap) for callbackRetries total attempts; callback failure never changes Run settlement.

callbacks:
  - source: webhook
    outcomes: [error]
    target: https://hooks.example.com/alert
    secretRef: ALERT_TOKEN

Installing dsh-cron alongside this plugin also lets cron settlement events use the same optional callback service; cron does not depend on webhook for execution.

Configuration

KeyDefaultMeaning
bind127.0.0.1listener address
port8788listener port
maxPayloadBytes262144request body limit
rateLimitPerMinute60accepted requests per hook per minute
defaultCwdnoneabsolute fallback workspace for fresh Sessions
reconcilePollMs1000Automation event-feed poll interval
dataDir$DSH_HOME/webhookdurable store and lock directory
hooks[]static hooks (name, promptTemplate, auth fields, cwd, concurrencyLimit, paused, callbacks)
callbacks[]global callback rules
callbackRetries4total callback attempts, including the first

Each hook has a stable concurrency key webhook:<hook-id> and a configurable concurrencyLimit (default 1). The actual limit is enforced transactionally by dsh-automation across all workers and processes.

Operations and compatibility

store.json schema v3 migrates v2 on load. Writes are atomic and coordinated by short-lived locks; cross-process records and the Automation cursor are merged without moving the cursor backward. Corrupt files are quarantined. Active receipts are never trimmed merely to meet the bounded terminal history size.

Keep the listener behind a TLS reverse proxy or Cloudflare Tunnel for public use. Prefer loopback binding even when authentication is enabled.

The adapter requires the public dsh-automation >=0.2.0-alpha.0 <0.3.0 service contract. It does not import private dsh-automation source and does not require any deepseek-harness change.

Development

pnpm install
pnpm run verify:self-contained
pnpm run typecheck
pnpm test
pnpm run build
pnpm run prepare

See the plugin contract and source layout.

License

MIT