← Back to home@xuan25

dsh-webhook-dispatcher

dsh-webhook-dispatcher gives a dsh deployment inbound webhook dispatch: when an HTTP caller reaches one of the plugin's endpoints, a fresh, isolated agent session handles the delivered content.

Stars
0
Language
JavaScript
Created
Oct 10, 2026
Updated
Oct 15, 2026

Introduction

dsh-webhook-dispatcher

dsh-webhook-dispatcher gives a dsh deployment inbound webhook dispatch: when an HTTP caller reaches one of the plugin's endpoints, a fresh, isolated agent session handles the delivered content. External systems such as CI pipelines, issue trackers, or monitoring tools can drive dsh sessions directly - no per-service bridge code.

  • Endpoints are exact routes on the profile's webServer service (the Web GUI server in the web profile; other profiles compose @deepseek-ai/dsh-host-webserver - see Cross-profile deployment); external reachability is the operator's reverse-proxy decision
  • Per-endpoint configuration: identity (id / path / name), token auth, policy text, working directory, sandbox, model, reasoning effort, and resource limits
  • Each admitted delivery spawns a fresh agent session; per-endpoint concurrency and queue caps, with 429 + Retry-After backpressure
  • A single webhook status tool verb reports live routes, queue depth, and recent delivery history; the plugin creates no config files
  • Session offload: a per-minute sweep archives settled trigger sessions that outlived the endpoint's sessionRetention window, keeping the framework session store bounded

Installation

dsh-webhook-dispatcher is an out-of-tree plugin for a dsh profile. Install it with the dsh plugin command:

dsh plugin --profile <name> add dsh-webhook-dispatcher

Configure endpoints through the DSH_WEBHOOK_* env block and/or the cordis config layer (see Configuration).

Cross-profile deployment

Endpoints register as exact routes on the profile's webServer service. The web profile provides it through the Web GUI server; every other profile (sdk, acp, a custom bundle) must compose a webServer provider into its own composition - the framework's route server @deepseek-ai/dsh-host-webserver (see its own docs). One-shot surfaces such as headless cannot host a resident listener, so they cannot host this plugin:

# $DSH_HOME/profiles/<name>/cordis.patch.yml (example)
- insert:
    - id: webserver
      name: '@deepseek-ai/dsh-host-webserver'
      config:
        host: <host>
        port: <port>  # a free fixed port; port 0 re-resolves the bound address on every restart

Configuration

Endpoints are configured through two equal channels that fold onto one contract: the DSH_WEBHOOK_* env block (read once at process boot) and the cordis config layer (the profile/user cordis.patch.yml entry targeting the plugin id dsh-webhook-dispatcher; hot - a patch change re-constructs the plugin entry, which re-reads both channels). Precedence per field: config layer > env > built-in defaults.

Config layer

- id: dsh-webhook-dispatcher
  config:
    endpoints:
      - id: <name>
        path: /<path>
        policy: "Treat the content as a task description and act on it."
        # name: <name>           (default: the id)
        # cwd: /abs/path         (default: $DSH_HOME/dsh-webhook-dispatcher)
        # auth: <TOKEN_ENV>           (env var NAME holding the token value; empty = open endpoint)
        # sandbox: workspace-write    (read-only | workspace-write | danger-full-access)
        # model: some/model           (default: the default selection ladder)
        # reasoningEffort: low        (closed set off|minimal|low|medium|high|xhigh|max; absent: the framework provider default)
        # maxConcurrent: 1            (default)
        # maxQueue: 100               (default)
        # maxBodyBytes: 262144        (default)
        # sessionRetention: 24h       (session offload window; absent/0 = retain in the store)

Unknown keys are rejected (a typo guard); path must not collide with a framework-owned route.

Env contract

The same endpoint shape as DSH_WEBHOOK_<ENDPOINT_ID>_<FIELD> variables:

DSH_WEBHOOK_<NAME>_PATH=/<path>
DSH_WEBHOOK_<NAME>_POLICY="Treat the content as a task description and act on it."
DSH_WEBHOOK_<NAME>_AUTH=<TOKEN_ENV>   # the NAME of the env holding the token value
DSH_WEBHOOK_<NAME>_MODEL=some/model
DSH_WEBHOOK_<NAME>_REASONING_EFFORT=low
<TOKEN_ENV>=...
fieldenv suffixbuilt-in default
path (required)PATH-
nameNAMEthe id
policyPOLICYempty (free-policy fallback)
cwdCWD$DSH_HOME/dsh-webhook-dispatcher
authAUTHempty (open endpoint)
sandboxSANDBOXthe session default
modelMODELthe default selection ladder
reasoningEffortREASONING_EFFORTthe framework provider default (closed set off/minimal/low/medium/high/xhigh/max, validated at boot)
maxConcurrentMAX_CONCURRENT1
maxQueueMAX_QUEUE100
maxBodyBytesMAX_BODY_BYTES262144
sessionRetentionSESSION_RETENTIONabsent (retain in the store)
  • The id segment is the endpoint id uppercased with hyphens written as underscores (ci-run -> DSH_WEBHOOK_CI_RUN_*)
  • Both channels share one validator: a declaration without PATH, an out-of-bounds value, or a declared AUTH name that is not set fails the plugin at boot (fail-closed, the config layer's posture)
  • A declared token env produces no warning; any other mistyped DSH_WEBHOOK_ variable warns at boot

Tool

webhook { verb: "status" } - the live endpoint state, the recent-delivery ring, and the effective (non-secret) endpoint config. See the dsh-webhook-dispatcher skill for the full reference.

Session offload

Webhook dispatches leave on-disk session logs in the framework session store; at delivery volume (frequent or chatty endpoints) the store grows without bound. The per-minute offload sweep archives settled trigger sessions once their endpoint's sessionRetention window elapses, so the store stays bounded: the store is never deleted, only moved, and a session that is still running is kept until it settles.

  • Windows: sessionRetention is per-endpoint (the config layer's sessionRetention field or the env DSH_WEBHOOK_<ID>_SESSION_RETENTION): a non-negative integer number of milliseconds, or a duration string of 1-2 digits+unit segments (units d/h/m/s/ms) in descending order (e.g. 24h, 4h30m, 90s, 5000ms; a bare numeric string is invalid). 0 / absent = retain (no offload); an invalid value warns and is treated as 0 (never a boot failure).
  • Budget: DSH_WEBHOOK_OFFLOAD_BATCH (env, default 50) bounds the sessions one sweep may process; an invalid value warns and falls back to 50, never disabling the sweep. When consecutive sweeps observe new log lines arriving faster than the budget, the sweep logs a trend warning (raise the knob or lower the retention windows; the warning takes no action on its own).
  • Failure semantics: an unreachable persistence service, a missing store root, or an archive on a different mount disables the sweep for that round (a visible warning; log lines are never lost); a failed individual move is retried next round.

The offload state (retention log retention-log.jsonl, archive archived-sessions/, and the default trigger-session workspace) lives under the plugin home $DSH_HOME/dsh-webhook-dispatcher/. The selftest covers the offload plane as well (window parsing, retention-log mechanics, the sweep's window/liveness/budget/trend semantics, the dispatch write gate, the archive move, and the layout pin).

Development

pnpm install     # peers resolve from the registry (onboarding .npmrc / pnpm-workspace.yaml)
pnpm run build   # tsc -> lib/
pnpm run typecheck
pnpm run lint    # eslint over src/ and test/
pnpm run selftest

Peer resolution is a deliberate dev/staged pair: the dev tree keeps autoInstallPeers: true so the peer closure resolves from the package's own node_modules (needed for the in-tree typecheck); the staged deployment tree ships autoInstallPeers: false plus .npmrc legacy-peer-deps=true, so peers resolve from the enclosing plugin tree of the deployment, like every neighbor plugin.