Skip to content

Commit c758f73

Browse files
committed
dsh-plugin-installer: GUI plugin installer for DeepSeek Harness
Install/remove profile plugins straight from the browser — no terminal. Wraps the exact operation of 'dsh plugin --profile <name> add/remove': - src/installer.ts (host): webServer route POST /dsh-plugin-installer/api, forwards to pnpm inside the current profile dir, reconciles dsh.profile.bundles against installed state (same logic as the dsh plugin CLI); polls for webServer activation so headless profiles disable gracefully instead of failing boot - src/installer-client.ts (browser): Settings → Plugins → 安装 tab (settings.plugins.tab slot) calling the host via same-origin fetch - cordis.patch.yml: package-name row (client-modules discovery carrier) + installer subpath row; dev/cordis.yml mirrors both for --patch dev - smoke tests, bilingual README
0 parents  commit c758f73

18 files changed

Lines changed: 3496 additions & 0 deletions

‎.github/workflows/ci.yml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: pnpm/action-setup@v4
14+
with:
15+
version: 11.7.0
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: 22
19+
cache: pnpm
20+
- run: pnpm install --frozen-lockfile
21+
- run: pnpm typecheck
22+
- run: pnpm build
23+
- run: node test/smoke.mjs

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
lib/
3+
*.log
4+
.DS_Store

‎LICENSE‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 dsh-plugin-template authors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md‎

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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

Comments
 (0)