|
| 1 | +# dsh-plugin-installer |
| 2 | + |
| 3 | +**English** | [简体中文](README.zh.md) |
| 4 | + |
| 5 | +A ready-to-run, ready-to-install starter template for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) plugins. It demonstrates the six most common plugin shapes in one minimal installable bundle: |
| 6 | + |
| 7 | +- **Config** — `Config` interface + Schemastery schema; validation and defaults apply at load time ([docs](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/config.md)) |
| 8 | +- **Tool** — `ctx.tools.register(defineTool(...))` registers a model-callable tool ([docs](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/tool.md)) |
| 9 | +- **Events** — `ctx.on` / `ctx.emit` with declaration merging for typed events ([docs](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/framework/events.md)) |
| 10 | +- **Service** — a class-form plugin that provides a service to other plugins ([docs](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/framework/service.md)) |
| 11 | +- **Hook** — a `tools/pre-execute` permission gate that denies tool calls by config ([docs](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/cookbook/extension-cookbook.md)) |
| 12 | +- **Browser half (client)** — `src/client.ts` registers a **clickable config card** under Settings → Plugins → Configurable; it writes `greeting` / `maxRetries` / `verbose` into the user settings document through the settings namespace, taking effect live. |
| 13 | + |
| 14 | +The template follows the official [bundle distribution model](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/publish.md): the package declares `dsh.bundle` plus `cordis.patch.yml`, and `dsh plugin add` activates it as a config layer. |
| 15 | + |
| 16 | +## Directory structure |
| 17 | + |
| 18 | +``` |
| 19 | +dsh-plugin-installer/ |
| 20 | +├── package.json # npm manifest + dsh.bundle / dsh.client declarations + prepare build script |
| 21 | +├── tsconfig.json # strict type-check configuration (tsc --noEmit) |
| 22 | +├── tsdown.config.ts # build config: Node library (lib/) + client bundle (lib/client.js), self-contained for git-install prepare |
| 23 | +├── cordis.patch.yml # bundle config layer: inserts the plugin rows |
| 24 | +├── dev/cordis.yml # local dev overlay (points at source; use with dsh web --patch; host half only) |
| 25 | +├── src/ |
| 26 | +│ ├── index.ts # main plugin: Config + tool + events + effect, config wired through the settings namespace |
| 27 | +│ ├── client.ts # browser half: clickable config card (settings.plugin.item slot) + installer tab registration |
| 28 | +│ ├── installer.ts # host half of the plugin installer: webServer route → pnpm add/remove + bundle reconcile |
| 29 | +│ ├── installer-client.ts # browser half of the installer: Settings → Plugins → 安装 tab (same-origin fetch) |
| 30 | +│ ├── service.ts # optional example: Service provider (disabled by default) |
| 31 | +│ └── hook.ts # optional example: hook permission gate (disabled by default) |
| 32 | +└── test/smoke.mjs # smoke test on the build output (incl. settings wiring unit test) |
| 33 | +``` |
| 34 | + |
| 35 | +## Quick start |
| 36 | + |
| 37 | +### Install as a bundle (for users) |
| 38 | + |
| 39 | +From any directory, install this package (or your fork) into a dsh profile: |
| 40 | + |
| 41 | +```sh |
| 42 | +# local directory |
| 43 | +dsh plugin --profile demo add /path/to/dsh-plugin-installer |
| 44 | + |
| 45 | +# or directly from GitHub (replace with your own repo after forking) |
| 46 | +dsh plugin --profile demo add github:you/dsh-plugin-installer |
| 47 | +``` |
| 48 | + |
| 49 | +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: |
| 50 | + |
| 51 | +```yaml |
| 52 | +allowBuilds: |
| 53 | + dsh-plugin-installer: true |
| 54 | +``` |
| 55 | +
|
| 56 | +> 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-installer#<sha>`. |
| 57 | + |
| 58 | +Verify the config layer and boot: |
| 59 | + |
| 60 | +```sh |
| 61 | +dsh --profile demo --dump-config # should show a "# == dsh-plugin-installer" layer |
| 62 | +dsh --profile demo |
| 63 | +``` |
| 64 | + |
| 65 | +> Note: a custom-named profile (e.g. `demo`) contains only `dsh-base` and is **headless** (no GUI). |
| 66 | +> For the Web GUI and the config card below, use the `web` profile (`= dsh-base` + `dsh-web-app`) — see [testing the config card](#testing-the-config-card-in-the-gui). |
| 67 | + |
| 68 | +### Local development (modifying the plugin) |
| 69 | + |
| 70 | +From the root of a [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) source checkout, load this repo's source directly via an overlay (no install, no build): |
| 71 | + |
| 72 | +```sh |
| 73 | +pnpm dsh web --patch /absolute/path/to/dsh-plugin-installer/dev/cordis.yml |
| 74 | +``` |
| 75 | + |
| 76 | +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. |
| 77 | + |
| 78 | +> ⚠️ A `--patch` overlay only loads the plugin's **host half** (module resolution cannot reach package-level declarations). |
| 79 | +> To test the browser-half config card you must install into a profile (resolved by `name: dsh-plugin-installer`) — see the next section. |
| 80 | + |
| 81 | +Run the checks yourself during development: |
| 82 | + |
| 83 | +```sh |
| 84 | +pnpm install |
| 85 | +pnpm typecheck |
| 86 | +pnpm build |
| 87 | +node test/smoke.mjs |
| 88 | +``` |
| 89 | + |
| 90 | +### Testing the config card (in the GUI) |
| 91 | + |
| 92 | +> The card is **disabled by default**: this bundle now ships the plugin installer only, and the demo plugin row (which hosts the card's settings namespace) is commented out in `cordis.patch.yml`. To try the card, re-enable the demo row there (rename its `id`, e.g. `dsh-plugin-installer-demo`, to avoid colliding with the installer row). |
| 93 | + |
| 94 | +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): |
| 95 | + |
| 96 | +```sh |
| 97 | +# 1. Build (produces lib/index.js + lib/client.js) |
| 98 | +cd /path/to/dsh-plugin-installer && pnpm build |
| 99 | +
|
| 100 | +# 2. Install into the web profile (= dsh-base + dsh-web-app, full GUI) |
| 101 | +dsh plugin --profile web add /path/to/dsh-plugin-installer |
| 102 | +
|
| 103 | +# 3. Boot the web GUI (`dsh web` is equivalent to `dsh --profile web`) |
| 104 | +dsh web |
| 105 | +``` |
| 106 | + |
| 107 | +Open `http://127.0.0.1:3080`: |
| 108 | + |
| 109 | +1. Bottom-left **Settings** → **Plugins** → **Configurable** tab: you should see a `dsh-plugin-installer` card with editable `greeting` / `maxRetries` / `verbose` fields; |
| 110 | +2. Change `greeting`, click **Save** — the status line should confirm it takes effect immediately; |
| 111 | +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); |
| 112 | +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. |
| 113 | + |
| 114 | +After editing `src/client.ts`, rerun `pnpm build` and refresh the page (the client bundle's rev query cache-busts). |
| 115 | + |
| 116 | +> ⚠️ **Known harness limitation (one-time setup, please read):** whether the card shows depends on the `WEB_SETTINGS_NAMESPACES` allowlist in harness's |
| 117 | +> `packages/host/apiproxy/src/api-proxy.ts` — a namespace absent from the list is treated as "not exposed" by `settings.describe` even when the plugin registered it, so the card does not render. Add one line to your harness checkout: |
| 118 | +
|
| 119 | +```ts |
| 120 | +const WEB_SETTINGS_NAMESPACES = [ |
| 121 | + 'agent-loop', 'shell', 'locale', 'permission', 'ui-conversation', 'ui-theme', 'web-search-deepseek', |
| 122 | + 'dsh-plugin-installer', // ← add this line |
| 123 | +] as const |
| 124 | +``` |
| 125 | + |
| 126 | +> This is the harness's current registration decision point (source comment: "adding a section to that page is a decision made here rather than by the registering plugin. Moving that declaration to `settings.register()` … is deferred work"). Once the harness moves the exposure declaration into `settings.register()`, the template no longer needs this line. The change is a local source edit: it is lost when you update/reinstall a fresh harness checkout and must be re-applied. |
| 127 | +
|
| 128 | +## Plugin installer (in the GUI) |
| 129 | + |
| 130 | +The bundle also ships a **plugin installer** — install/remove profile plugins straight from the browser, no terminal needed. It wraps the exact same operation as `dsh plugin --profile <name> add/remove`: |
| 131 | + |
| 132 | +- Host half (`src/installer.ts`) registers a `POST /dsh-plugin-installer/api` route on the web server (`ctx.webServer`), forwards each request to `pnpm` inside the current profile directory, and reconciles `dsh.profile.bundles` against the installed state (same logic as the `dsh plugin` CLI: a dependency declaring `dsh.bundle` joins the layer stack; a removed or declaration-less one leaves it). |
| 133 | +- Browser half (`src/installer-client.ts`, registered by `src/client.ts` into the `settings.plugins.tab` slot) renders the **安装 (Install) tab** under Settings → Plugins: an install input, the current bundle layers with run/needs-restart badges, and per-bundle uninstall buttons. |
| 134 | + |
| 135 | +```sh |
| 136 | +# After installing this bundle into the web profile and booting the GUI: |
| 137 | +# dsh plugin --profile web add /path/to/dsh-plugin-installer && dsh web |
| 138 | +# open http://127.0.0.1:3080 → Settings → Plugins → 安装 |
| 139 | +``` |
| 140 | + |
| 141 | +Install sources: npm package names (`dsh-my-plugin`), `github:user/repo`, `file:` links, and **absolute** paths to local directories/tarballs (relative paths are rejected — the browser has no working directory to anchor them to). |
| 142 | + |
| 143 | +Notes: |
| 144 | + |
| 145 | +- **Restart required to activate**: new bundles join the composition at boot. After an install, the row shows "需重启" until you restart `dsh`. |
| 146 | +- **pnpm must be on the harness process PATH.** Git installs run the package's `prepare` script; on pnpm ≥10 the first one is refused and the error surfaces the exact package key to add under `allowBuilds` in the profile's `pnpm-workspace.yaml` (the GUI shows the full pnpm output). |
| 147 | +- **No harness source edit needed** — unlike the config card above, the installer tab goes through the public `settings.plugins.tab` slot plus a plain web-server route, so it works on an unmodified harness checkout. |
| 148 | +- The host half only activates in profiles that mount `webServer` (the web profile). In headless profiles it disables itself instead of failing the boot. |
| 149 | +- The route is loopback-only (the web server binds `127.0.0.1`), and running it executes `pnpm` on your machine — same trust boundary as running `dsh plugin add` yourself. |
| 150 | + |
| 151 | +## Making it your own plugin |
| 152 | + |
| 153 | +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 three browser-half spots:** the client bundle `id` in `tsdown.config.ts` (`__ModuleLoader__.load({ id })`), `NAMESPACE` in `src/client.ts`, and `dsh.client` in `package.json` (if you need `inject`). |
| 154 | +2. Change the `Config` interface and `Config` schema: anything two deployments should be able to set differently must be a config field ([design principles](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/config.md#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.ts` is hand-written; add a field row there for each new config field. |
| 155 | +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](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/cookbook/adding-a-tool.md)). |
| 156 | +4. To provide capabilities to other plugins, enable `src/service.ts` and uncomment its row in `cordis.patch.yml`. |
| 157 | +5. Remember to `declare module '@deepseek-ai/cordis'` to merge `Context` / `Events` types — that is what keeps cross-package boundaries type-safe. |
| 158 | +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](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/cookbook/extension-cookbook.md)). |
| 159 | +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](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/shell/bash-local/src/index.ts)). |
| 160 | + |
| 161 | +## How the browser half works |
| 162 | + |
| 163 | +- `package.json` declares `dsh.client: { platform: "web" }` + `exports["./client"]` → dsh's client-modules discovers it and loads `lib/client.js` as a browser plugin; |
| 164 | +- `src/client.ts` registers a card in the `settings.plugin.item` slot and binds the `dsh-plugin-installer` namespace via the `settingsScope` service: reads snapshots, stages drafts, and writes field-by-field on save (revision-fenced); |
| 165 | +- `src/installer-client.ts` registers the installer tab in the `settings.plugins.tab` slot and calls the host half through a same-origin `fetch` to `/dsh-plugin-installer/api`; |
| 166 | +- 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; |
| 167 | +- 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. |
| 168 | + |
| 169 | +## Publishing |
| 170 | + |
| 171 | +- **npm**: `pnpm publish` (`files` already includes the build output and the patch; no extra steps) |
| 172 | +- **tarball**: `pnpm pack`, then `dsh plugin --profile demo add ./dsh-plugin-installer-0.1.0.tgz` |
| 173 | +- **git**: `dsh plugin add github:you/dsh-plugin-installer` (combined with the `allowBuilds` step above) |
| 174 | + |
| 175 | +## Related docs |
| 176 | + |
| 177 | +- Plugin development intro: [basic/index.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/index.md) |
| 178 | +- Plugin config: [basic/config.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/config.md) |
| 179 | +- Tool development: [basic/tool.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/tool.md) |
| 180 | +- Packaging & installation: [basic/publish.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/basic/publish.md) |
| 181 | +- Plugins & lifecycle: [framework/index.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/framework/index.md) |
| 182 | +- Services & dependencies: [framework/service.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/framework/service.md) |
| 183 | +- Event system: [framework/events.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/user/develop/framework/events.md) |
| 184 | +- Cordis tutorial: [cordis-tutorial](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/cordis-tutorial/index.md) |
0 commit comments