DeepSeek Harness Plugin

kun2-5code/dsh-plugin-template

Stars ★ 1 Category Development & Runtime Added 2026-08-16

A starter template for dsh plugins covering config, tools, events, services, hooks, browser UI slots, and slash commands.

Install

# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)

dsh plugin --profile web add github:kun2-5code/dsh-plugin-template

Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time. Only install sources you trust, and pin a commit (github:owner/repo#sha).

README

English | 简体中文

A ready-to-run, ready-to-install starter template for DeepSeek Harness (dsh) plugins. It demonstrates the six most common plugin shapes in one minimal installable bundle:

  • ConfigConfig interface + Schemastery schema; validation and defaults apply at load time (docs)
  • Toolctx.tools.register(defineTool(...)) registers a model-callable tool (docs)
  • Eventsctx.on / ctx.emit with declaration merging for typed events (docs)
  • Service — a class-form plugin that provides a service to other plugins (docs)
  • Hook — a tools/pre-execute permission gate that denies tool calls by config (docs)
  • Browser half (client)src/client/ registers browser UI on fourteen surfaces (index: docs/ui-surfaces.md): a clickable config card under Settings → Plugins → Configurable (writes greeting / maxRetries / verbose into the settings document, taking effect live; on a stock harness the card renders a read-only "not exposed" explainer instead of vanishing), a sidebar footer action button, an input dock strip above the composer, a shell overlay pill, a header utility badge, input tool-row buttons (left/right), a custom command row for /dsh-demo, a General settings row, a Plugins tab, a settings header action, a session header action, a composer dock strip, and per-message actions on AI replies — plus a presentResult render intent on the greet tool. Only the config card's data path is gated by the harness allowlist; the other thirteen are pure slot registrations that work on any harness.

The template follows the official bundle distribution model: the package declares dsh.bundle plus cordis.patch.yml, and dsh plugin add activates it as a config layer.

Directory structure

dsh-plugin-template/
├── package.json        # npm manifest + dsh.bundle / dsh.client declarations + prepare build script
├── tsconfig.json       # strict type-check configuration (tsc --noEmit)
├── tsdown.config.ts    # build config: Node library (lib/) + client bundle (lib/client.js), self-contained for git-install prepare
├── cordis.patch.yml    # bundle config layer: inserts the plugin rows
├── dev/cordis.yml      # local dev overlay (points at source; use with dsh web --patch; host half only)
├── docs/
│   └── ui-surfaces.md  # where the plugin registers UI + index of every slot (bilingual: ui-surfaces.zh.md)
├── src/
│   ├── index.ts        # main plugin: Config + tool + events + effect, config wired through the settings namespace
│   ├── commands.ts     # host half: demo slash commands /hello (replies world) and /dsh-demo (custom row)
│   ├── service.ts      # optional example: Service provider (disabled by default)
│   ├── hook.ts         # optional example: hook permission gate (disabled by default)
│   └── client/         # browser half: one module per UI surface (see docs/ui-surfaces.md)
│       ├── index.ts        # client entry: inject + apply, assembles the registrations
│       ├── constants.ts    # shared NAMESPACE + DEMO_COMMAND_NAME (keep in sync with package.json name / cordis.patch.yml)
│       ├── types.ts        # minimal structural types for ctx services (no @deepseek-ai client imports)
│       ├── styles.ts       # one injected <style> with all dtpl-* classes (theme tokens only)
│       ├── config-card.ts  # settings.plugin.item: the clickable config card (staged form + status states)
│       ├── sidebar-action.ts # sidebar.footer.action: sidebar-footer button
│       ├── input-dock.ts   # conversation.input.dock: strip above the composer (session-scoped)
│       ├── shell-overlay.ts # shell.overlay: frame-wide floating pill
│       ├── header-utilities.ts # conversation.session.header.utilities: right-aligned header badge
│       ├── input-left.ts   # conversation.input.left: tool-row control at the left end
│       ├── input-right.ts  # conversation.input.right: tool-row control next to send
│       ├── commandview.ts  # conversation.chat.commandview: custom row for /dsh-demo
│       ├── general-item.ts # settings.general.item: one preference row in Settings → General
│       ├── plugins-tab.ts  # settings.plugins.tab: a new tab in the Plugins page
│       ├── settings-action.ts # settings.action: button in the settings header
│       ├── header-actions.ts # conversation.session.header.actions: session-title action button
│       ├── composer-dock.ts  # conversation.composer.dock: strip under the composer card
│       └── assistant-actions.ts # conversation.chat.assistant-actions: per-message button
└── test/smoke.mjs      # smoke test on the build output (incl. settings wiring unit test)

Quick start

Install as a bundle (for users)

From any directory, install this package (or your fork) into a dsh profile:

# local directory
dsh plugin --profile demo add /path/to/dsh-plugin-template

# or directly from GitHub (replace with your own repo after forking)
dsh plugin --profile demo add github:you/dsh-plugin-template

A GitHub install pulls source; pnpm runs prepare (i.e. tsdown) to build lib/. On pnpm ≥10 the first git-dependency prepare is refused; add the package name pnpm prints to the profile's pnpm-workspace.yaml and retry:

allowBuilds:
  dsh-plugin-template: true

This allowlist authorizes executing that package's code at install time — only allow source you trust, and prefer pinning a commit: github:you/dsh-plugin-template#<sha>.

Verify the config layer and boot:

dsh --profile demo --dump-config   # should show a "# == dsh-plugin-template" layer
dsh --profile demo

Note: a custom-named profile (e.g. demo) contains only dsh-base and is headless (no GUI). For the Web GUI and the config card below, use the web profile (= dsh-base + dsh-web-app) — see testing the config card.

Local development (modifying the plugin)

From the root of a deepseek-harness source checkout, load this repo's source directly via an overlay (no install, no build):

pnpm dsh web --patch /absolute/path/to/dsh-plugin-template/dev/cordis.yml

Set name in dev/cordis.yml to this repo's absolute path on your machine, open http://127.0.0.1:3080, and ask the model to call the greet tool.

⚠️ A --patch overlay only loads the plugin's host half (module resolution cannot reach package-level declarations). To test the browser-half config card you must install into a profile (resolved by name: dsh-plugin-template) — see the next section.

Run the checks yourself during development:

pnpm install
pnpm typecheck
pnpm build
node test/smoke.mjs

If this repo sits INSIDE a deepseek-harness checkout (nested, as in the harness repo root), pnpm install is captured by the parent workspace and installs nothing here — the template is not a workspace member. Use pnpm install --ignore-workspace (pnpm ≥9.5) so the template installs its own node_modules from its own lockfile; or clone the template standalone.

Testing the config card (in the GUI)

The config card renders in the browser and depends on dsh's client-modules discovering the dsh.client declaration by package name, so the package must be installed into a profile (a --patch source path won't do):

# 1. Build (produces lib/index.js + lib/client.js)
cd /path/to/dsh-plugin-template && pnpm build

# 2. Install into the web profile (= dsh-base + dsh-web-app, full GUI)
dsh plugin --profile web add /path/to/dsh-plugin-template

# 3. Boot the web GUI (`dsh web` is equivalent to `dsh --profile web`)
dsh web

Open http://127.0.0.1:3080:

  1. Bottom-left SettingsPluginsConfigurable tab: you should see a dsh-plugin-template card. On a stock harness it renders a read-only "not exposed" status card (see below); after the one-line harness change it renders the editable greeting / maxRetries / verbose fields;
  2. Change greeting, click Save — the status line should confirm it takes effect immediately;
  3. Back in a session, ask the model to call the greet tool — you should see the new greeting (the host half reads the resolved namespace value live, no restart);
  4. The change lands in the settings document (settings.yaml under $DSH_HOME) and survives restarts; to restore a default, edit the field back or clear it in the card.

After editing the client half (src/client/), rerun pnpm build and refresh the page (the client bundle's rev query cache-busts).

The config card on a stock harness (no source edits)

The card is a browser plugin (src/client/config-card.ts) that binds the settings namespace dsh-plugin-template through the settingsScope service. It always renders — but on a stock harness it shows a read-only "not exposed" status card instead of editable fields. Why: dsh's web gateway serves settings namespaces only from an explicit allowlist (WEB_SETTINGS_NAMESPACES in packages/host/apiproxy/src/api-proxy.ts), and a namespace absent from it answers settings-not-exposed even when its owner plugin registered it. This is a harness-side registration decision (the same source comment calls moving the declaration into settings.register() "deferred work"), not a template defect: the built-in cards render because their namespaces (shell, agent-loop, …) are allowlisted, and there is currently no plugin-side channel to add one — the gateway's RPC map is compile-time fixed and no registration flag exists yet.

What works on a stock harness with zero edits:

  • the entire host half — the greet tool, events, the service, the hook gate — including live config reads: writes are only gated at the web RPC, the plugin itself reads the resolved namespace value on every execution;
  • the card slot itself: the card appears under Settings → Plugins → Configurable and explains the exposure state instead of vanishing silently.

To make the card editable, pick one:

  1. add 'dsh-plugin-template' to WEB_SETTINGS_NAMESPACES in packages/host/apiproxy/src/api-proxy.ts (one line; rebuild/restart the harness; lost when you update the checkout):
const WEB_SETTINGS_NAMESPACES = [
  'agent-loop', 'shell', 'locale', 'permission', 'ui-conversation', 'ui-theme', 'web-search-deepseek',
  'dsh-plugin-template',   // ← add this line
] as const
  1. wait for the harness's deferred work — moving the exposure declaration into settings.register() — which this template already targets by registering the namespace the canonical way (installSettingsSection).

Making it your own plugin

  1. Rename the package: keep package.json name (npm name, e.g. dsh-my-plugin), src/index.ts name, and cordis.patch.yml id/name consistent; when renaming the ./service subpath, update exports/files too. Renaming also touches browser-half spots: the client bundle id in tsdown.config.ts (__ModuleLoader__.load({ id })), NAMESPACE in src/client/constants.ts, and dsh.client in package.json (if you need inject).
  2. Change the Config interface and Config schema: anything two deployments should be able to set differently must be a config field (design principles). The config is wired to the settings namespace — does the GUI card auto-render a form from your schema? No: the card in src/client/config-card.ts is hand-written; add a field row there for each new config field.
  3. Register your tool in apply: ctx.tools.register(defineTool({...})); execute returns the canonical value declared by output.schema, and output.render is the pure function for model-visible rendering (tool reference).
  4. To provide capabilities to other plugins, enable src/service.ts and uncomment its row in cordis.patch.yml.
  5. Remember to declare module '@deepseek-ai/cordis' to merge Context / Events types — that is what keeps cross-package boundaries type-safe.
  6. To intercept tool calls, act as a permission gate, or respond to system hooks, enable src/hook.ts (uncomment its cordis.patch.yml row): ctx.on('tools/pre-execute', ...) returns { kind: 'deny', reason } or calls next() to allow (extension cookbook).
  7. Config reads: every read in src/index.ts goes through configSource() (the resolved settings-namespace value, falling back to the composition entry). If you derive registration-level facts from config in apply (e.g. register different tools by config), rebuild them in installSettingsSection's onChange rather than reading only at execution points (see bash-local).

How the browser half works

  • package.json declares dsh.client: { platform: "web" } + exports["./client"] → dsh's client-modules discovers it and loads lib/client.js as a browser plugin;
  • the client entry (src/client/index.ts) assembles one registration per UI surface — the config card (settings.plugin.item), the sidebar footer action (sidebar.footer.action), and the input dock (conversation.input.dock) — see the UI surfaces index;
  • the config card binds the dsh-plugin-template namespace via the settingsScope service: reads snapshots, stages drafts, and writes field-by-field on save (revision-fenced);
  • the host half (src/index.ts) registers the same namespace with installSettingsSection (the cordis.yml config is the base layer) and reads the resolved value lazily in the tool → saving takes effect immediately;
  • at runtime the client half depends only on react (provided by the browser platform module table); everything else goes through ctx services and no @deepseek-ai client package is imported — keep that discipline when editing the template.

Publishing

  • npm: pnpm publish (files already includes the build output and the patch; no extra steps)
  • tarball: pnpm pack, then dsh plugin --profile demo add ./dsh-plugin-template-0.1.0.tgz
  • git: dsh plugin add github:you/dsh-plugin-template (combined with the allowBuilds step above)

Related docs

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →