Skip to content
dsh.fish
Bundle

dsh-plugin-dev-manager

Manage isolated DeepSeek Harness instances for safe DSH plugin development.

Source
QuanQQQ
License
MIT
Updated
Updated 2 days ago

Readme

# dsh-plugin-dev-manager

在一个稳定的 DeepSeek Harness 中管理隔离的 Plugin 开发实例。

它解决这样的开发风险:目标 Plugin 的 Host 代码、配置或依赖发生错误时,开发 DSH 可以重启或启动失败,负责写代码的稳定 DSH 仍然保持运行。

```text
稳定 DSH
  └─ dsh-plugin-dev-manager
       ├─ Settings / Plugin Dev
       └─ 本地 supervisor
            ├─ Workspace A → Plugin A + Common + Auth → 127.0.0.1 / LAN:3081
            └─ Workspace B → Plugin B + Storage → 127.0.0.1 / LAN:3082
```

supervisor 作为独立 Node.js 进程运行。稳定 DSH 重启或重新加载 Manager Plugin 后,会通过带随机令牌的 localhost RPC 重新连接到原 supervisor。

从旧版本升级时,Manager 会把旧 supervisor 返回的单 Plugin 数据自动补成单成员 Workspace,因此原有项目可以继续展示和使用。多 Plugin API 需要 supervisor protocol v2,实例删除 API、可恢复删除记录和注销后保留的数据所有权需要 protocol/registry v3。若当前 supervisor 版本过旧,先保存开发实例中的工作并执行 `dsh_dev_shutdown({ confirm: true })`;下一次 Manager 调用会启动新版 supervisor 并迁移 registry。该操作会停止旧 supervisor 管理的开发实例。registry v3 是前向升级,旧版 supervisor 不再读取它。

## 当前能力

- 为每个开发 Workspace 创建独立 `DSH_HOME`、Web profile 和端口。
- 一个 Workspace 可以组合多个本地 Plugin、固定版本依赖和团队 Preset。
- 为开发实例设置独立 `DSH_AGENTS_HOME` 和 bundled-skill 目录,避免扫描稳定实例的 Skills。
- 执行依赖安装与初始构建。
- 通过 `link:<absolute-plugin-path>` 安装所有本地成员,并为每个成员独立运行 build/watch。
- 支持仓库内的 `.dsh-dev.yml`、`.dsh-dev.yaml` 或 `.dsh-dev.json` 声明。
- 在启动前运行 `dsh --profile web --dump-config`。
- 启动、停止、重启、检查一个开发 DSH。
- 安全注销单个实例,并可选择清理受管开发数据,同时保留已晋级 Release 和 Plugin 源码。
- 自动启动每个本地成员的 `dev:client` watcher(存在该 script 时)。
- localhost supervisor RPC 使用原生 HTTP,绕过 Devbox 的环境代理。
- supervisor 启动失败或竞争失败时主动回收刚创建的进程。
- 在 DSH 设置页提供 Plugin Dev 管理面板。
- 读取 Host、watcher、check、clean validation 和 supervisor 日志。
- 执行目标包的 `check`、`test` 或指定 package script。
- 将 pnpm tarball 安装到全新的验证环境,完成 config composition 和 Web 健康检查。
- 通过干净环境门禁后,复制原始 tarball、计算 SHA-256 并写入晋级元数据。
- 拒绝停止 supervisor 没有持有进程句柄的端口占用者。

## 安装

直接使用 GitHub package spec 安装固定版本:

```bash
dsh plugin --profile web add \
  "github:QuanQQQ/dsh-plugin-dev-manager#v0.4.1"
```

仓库提交包含构建后的 `lib/`,因此目标机器无需安装源码构建依赖。私有仓库需要提前配置 GitHub SSH 访问;也可以使用完整地址:

```bash
dsh plugin --profile web add \
  "git+ssh://git@github.com/QuanQQQ/dsh-plugin-dev-manager.git#v0.4.1"
```

需要 tarball 时,可以从 GitHub Release 下载已验证的安装包:

```bash
gh release download v0.4.1 \
  --repo QuanQQQ/dsh-plugin-dev-manager \
  --pattern 'dsh-plugin-dev-manager-*.tgz'

dsh plugin --profile web add ./dsh-plugin-dev-manager-0.4.1.tgz
```

私有仓库需要先执行 `gh auth login`。也可以在 GitHub Release 页面下载 tarball,再复制到目标机器。

首次部署 PDM 时,推荐把发布 tarball 安装到稳定 DSH,让控制面与正在编辑的源码完全分离:

```bash
pnpm install
pnpm run check
pnpm pack --pack-destination ..

dsh plugin --profile web add /absolute/path/to/dsh-plugin-dev-manager-0.4.1.tgz
```

首次安装尚无 PDM 队列可用,应先确认稳定 Host 没有活动会话或后台任务,再由操作者按宿主环境的正常方式重启以加载 bundle。PDM v0.4.0 及后续版本之间的更新不得重复这条直装/直启流程,必须使用 `dsh_dev_queue_update(confirm=true)`。从不具备更新队列的 v0.3.x 或更早版本升级到 v0.4.0 时,需先保存所有工作、确认稳定 Host 已空闲并停止旧开发 supervisor,再将这次升级作为显式的一次性 fallback;目标 Plugin 不要安装到稳定 profile。

只在调试 Manager 本身的早期阶段使用 link 安装:

```bash
dsh plugin --profile web add "link:/absolute/path/to/dsh-plugin-dev-manager"
```

开发 Manager 本身时,可在控制面安装上一版稳定 tarball,再把当前源码仓库作为目标 Plugin 注册到 Manager。

启动稳定 DSH 后,打开 `Settings → Plugin Dev`。输入目标 Plugin 的绝对路径即可创建隔离环境。Agent 工具与 Web 面板操作同一个 supervisor 和项目注册表。

## 推荐开发 SOP

1. 准备一个稳定 DSH,只安装 Manager 和日常开发所需的稳定插件。
2. 在 Plugin Dev 面板或通过 `dsh_dev_workspace_create` 注册 Workspace。Manager 会读取开发清单、安装依赖、初始构建、分配独立端口,并把所有成员组合到独立 `DSH_HOME`。
3. 调用启动。Client 代码由目标包的 `dev:client` watcher 持续构建,DSH Client HMR 会加载新的 `client.js`。
4. 修改 Host 代码后,先保存工作,再执行受控重启。Web 面板会弹出确认,Agent 工具要求 `confirm=true`。
5. 调用检查。准备交付时执行“验证并晋级”或 `dsh_dev_promote`。
6. 使用 `releases/latest.json` 指向的原始 tarball 进行安装或发布,保留其中的 SHA-256 供核对。

这套流程把开发实例的故障域限制在单个 Workspace。目标 Host 启动失败或自行退出时,稳定 DSH、Agent 对话和其他开发实例仍可继续工作。

## 多 Plugin Workspace

在 Workspace 根目录添加 `.dsh-dev.yml`:

```yaml
version: 1
name: campaign-dev
preset: team-web

plugins:
  - path: .
    role: primary
    watch: dev:client

  - path: ../dsh-plugin-common
    role: member

  - source: github:example/dsh-plugin-storage#v1.3.0
    role: dependency
```

然后在面板输入 Workspace 路径,或调用:

```text
dsh_dev_workspace_create({ workspacePath: "/work/campaign-plugin" })
```

`primary` 是执行 check、pack、promote 的主 Plugin;`member` 是一起构建和调试的本地 Plugin;`dependency` 是安装到同一 profile 的运行依赖。省略 `role` 时,第一个本地 Plugin 会成为 primary,其他本地 Plugin 使用 member,远程 source 使用 dependency。

安装顺序固定为 Preset、非 primary 成员、primary。这样 primary 的配置补丁最后参与组合。每个本地成员拥有独立 watcher PID 和日志。任意 Host 代码变化都需要重启整个 Workspace,Workspace 内的运行中任务会随之中断。

单 Plugin 场景可以继续使用 `dsh_dev_create`;它会创建只有一个 primary 成员的 Workspace。

Manager 以解析后的真实 `workspacePath` 作为共享实例边界:不同会话即使传入不同 `id`,只要 Workspace 与 Plugin 组成一致,就直接返回已有实例,不会重复安装、分配端口或启动 Host。若同一 Workspace 请求了不同组成,调用会提示刷新已有共享实例,而不是并行创建另一份。

## 配置

默认只允许管理稳定 DSH 启动目录下的 Plugin。需要管理其他目录时,在稳定 profile 的 `cordis.patch.yml` 覆盖配置:

```yaml
- id: dsh-plugin-dev-manager
  config:
    allowedRoots:
      - /absolute/path/to/plugin-workspaces
    portStart: 3081
    portEnd: 3180
    startupTimeoutMs: 30000
    shutdownTimeoutMs: 7000
    usePollingWatch: false
    allowRemoteWebApi: false
    discoverLanUrls: true
    defaultPreset: team-web
    presets:
      team-web:
        - github:example/dsh-plugin-auth#v2.1.0
        - github:example/dsh-plugin-observability#v1.4.2
```

可用字段:

| 字段 | 默认值 | 说明 |
|---|---:|---|
| `stateDir` | `$DSH_HOME/dev-manager` | supervisor、项目注册表、实例 home、日志和产物目录 |
| `allowedRoots` | 稳定 DSH 的启动目录 | 可管理 Plugin 的路径白名单 |
| `dshCommand` | `dsh` | 开发实例使用的 DSH 可执行文件 |
| `packageManager` | `pnpm` | 安装、构建、检查和打包命令 |
| `portStart` / `portEnd` | `3081` / `3180` | 自动分配端口范围 |
| `startupTimeoutMs` | `30000` | 等待开发 DSH 监听端口的时间 |
| `shutdownTimeoutMs` | `7000` | SIGTERM 后等待时间,超时升级到 SIGKILL |
| `operationTimeoutMs` | `600000` | install、build、check、pack 的最长时间 |
| `maxOutputBytes` | `131072` | 单次命令和日志返回上限 |
| `usePollingWatch` | `false` | 设置 `CHOKIDAR_USEPOLLING=1`;遇到 `EMFILE` 时启用 |
| `allowRemoteWebApi` | `false` | 允许来自非回环对端的可信 authority 访问 Web 管理 API;显式启用后会传递给受管开发 DSH,确保其中的 PDM 也能从同一受信 LAN 使用 |
| `discoverLanUrls` | `true` | 探测并展示实际可达的本机私网 IPv4 URL;不会修改开发 DSH 的监听配置 |
| `stableDshHome` | 当前 `$DSH_HOME` | 排队更新最终写入的稳定 DSH home |
| `stableUpdateIdleMs` | `5000` | 所有对话和后台任务停止后,安装与重启前各自需要保持的静默时间 |
| `stableUpdatePollMs` | `2500` | 待更新队列与稳定版空闲状态的检查间隔 |
| `stableUpdateRetryMs` | `30000` | 可重试安装/重启错误的退避时间 |
| `stableUpdateMaxAttempts` | `3` | 自动重试上限;达到后必须显式重试 |
| `autoRestartStable` | `true` | 安装后自动重启稳定版 systemd user service |
| `stableSystemdUnit` | 空 | 可选的明确稳定版 `.service` 单元;为空时只接受以 `dsh` 开头的 cgroup 叶子 service,不匹配祖先单元 |
| `presets` | `{}` | 团队公共 Plugin 集合;值会按顺序传给 `dsh plugin add` |
| `defaultPreset` | 空 | Workspace 未声明 preset 时使用的默认名称 |

## Agent 工具

| 工具 | 作用 |
|---|---|
| `dsh_dev_create` | 安装依赖、构建、创建隔离 profile、link Plugin、dump-config |
| `dsh_dev_workspace_create` | 从开发清单或显式成员列表创建多 Plugin Workspace |
| `dsh_dev_start` | 启动 Client watcher 和开发 DSH,等待健康 |
| `dsh_dev_stop` | 受控停止开发 DSH 和 watcher |
| `dsh_dev_remove` | 经确认后停止并注销单个实例;可选清理开发数据 |
| `dsh_dev_restart` | 只重启开发实例 |
| `dsh_dev_status` | 查看单个项目的状态、URL、PID 和错误 |
| `dsh_dev_list` | 查看全部项目 |
| `dsh_dev_logs` | 读取 Host、watch、check、validation 或 supervisor 日志 |
| `dsh_dev_check` | 执行 package.json 中的验证 script |
| `dsh_dev_pack` | 执行检查并输出 tarball |
| `dsh_dev_validate` | 在全新 DSH_HOME 中安装 tarball、dump-config、启动并检查健康 |
| `dsh_dev_promote` | 通过 clean validation 后保存带 SHA-256 的发布候选 |
| `dsh_dev_queue_update` | 经确认后验证、晋级并记录稳定版待更新版本,等待稳定版空闲后安装并重启 |
| `dsh_dev_retry_update` | 经确认后清除失败退避,重试持久化的安装或重启 |
| `dsh_dev_shutdown` | 经确认后停止 supervisor 及其管理的全部开发实例 |

示例对话:

```text
为 /work/dsh-foo 创建开发 Workspace。
启动 dsh-foo Workspace。
查看其中 dsh-plugin-common 的 Watcher 日志。
运行检查,成功后验证并晋级。
```

`dsh_dev_create` 的关键参数:

- `pluginPath`:目标 Plugin 路径,必填。
- `id`:稳定项目 ID;省略时从 package name 推导。
- `install`:默认 `true`。存在 `pnpm-lock.yaml` 时使用 `--frozen-lockfile`。
- `build`:默认 `true`。存在 `build` script 时自动执行。
- `watchScript`:默认探测 `dev:client`。
- `checkScript`:默认依次探测 `check`、`test`。

`dsh_dev_workspace_create` 的关键参数:

- `workspacePath`:Workspace 根目录,必填。
- `id`:稳定 Workspace ID;省略时依次使用清单 name 和 primary package name。
- `preset`:稳定控制面配置中的公共 Plugin 集合。
- `plugins`:显式成员列表;提供后会覆盖仓库开发清单。
- `install` / `build`:所有本地成员的默认行为;成员级配置可以覆盖。

`dsh_dev_remove` 必须传入 `confirm: true`。默认只停止并从活动列表注销实例,registry 会保留 ID、原 Workspace 与受管数据的所有权记录,原有 `DSH_HOME`、日志和产物仍留在磁盘;之后只有同一 Workspace 能用该 ID 重新接管,不同 Workspace 会被拒绝,避免继承旧 profile。对已注销 ID 再传 `purge: true` 也可清理保留数据。purge 会删除 Manager 管理的实例 home、当前 Workspace 的已知日志、顶层 tarball、依赖包和 validation 目录;`artifacts/<id>/releases/` 中的晋级 tarball、历史元数据及 `latest.json` 始终保留。该操作不会删除 Workspace 或 Plugin 源码。

Web 面板分别提供“注销”和“清理数据”:前者保留全部磁盘数据,后者使用 `purge: true`;两者使用不同确认文案,成功后会关闭该项目已打开的日志面板。

执行 purge 时,Manager 会先把注销和 cleanup tombstone 原子写入 registry,再清理磁盘,最后移除 tombstone。清理中断或部分失败时,`dsh_dev_list` 会以 `removing` 状态显示该 ID;只有再次传入 `purge: true` 才会继续幂等清理,默认的 `purge: false` 不会越过原始保留契约。cleanup 未完成前不能用同一 ID 重新创建。`remove` 遇到 RPC 传输错误不会自动重放,而会提示先调用 `dsh_dev_list` 判断注销是否已经提交。

## 状态与故障处理

Workspace 状态包含:

```text
stopped → initializing → starting → ready
                         ↘ failed
                         ↘ orphaned
removing → retry cleanup → removed
```

`orphaned` 表示端口有进程,但当前 supervisor 没有对应的子进程句柄。Manager 会拒绝停止或删除该实例,避免 PID 或端口复用时误杀其他程序,或在未知进程仍使用实例数据时清理磁盘。先核对端口和日志,再手动处理该进程。

开发 DSH 中的运行中任务在 Host 重启时仍会中断。Manager 保护的是稳定控制面和其他开发实例;调用 `dsh_dev_restart` 前仍应确认目标开发实例没有需要保留的任务。

`validate` 与 `promote` 每次都使用新的验证目录。primary 和本地 member 会分别打包为 `.tgz`,Preset 与固定 source 随后一起安装;验证过程不会复用 link 开发环境。晋级目录包含 primary 的不可变 tarball、时间戳元数据和 `latest.json`。单独调用 `promote` 不会修改稳定 DSH。

`dsh_dev_queue_update(confirm=true)` 会把晋级产物写入 `stateDir/stable-updates.json`。同一 npm package 只保留最新候选,不同 package 可合并;稳定 Host 同时检查所有 live agent 的 `idle/running`、agent maintenance 和 owner/unowned background jobs。第一次完整静默窗口后,supervisor 校验 tarball 的 releases 路径、SHA-256 和内部 `package.json` 名称,再逐个安装到稳定 Web profile;每次执行命令前先写 applying journal,每个 package 成功后立即 checkpoint,因此批次中途失败不会丢失已安装/未安装边界。profile 安装只修改磁盘,不改变当前进程;若最后一次空闲检查后有新工作开始,它仍继续使用当前已加载版本。随后重新等待一个完整静默窗口。重启交接前,Coordinator 使用 agent maintenance 栅栏锁住现有 Agent,并同步接管新创建的 Agent;此后到 Host teardown 之间的新消息只会排队,不会先开始再被重启打断。新 Host PID 启动后,Coordinator 优先要求它报告与安装后相同的 Web profile generation。若 generation 仅因其他合法 profile 更新而变化,则必须同时确认当前磁盘仍与 Host 启动快照一致、且本批次每个 bundle 的 manifest dependency 与 lockfile resolution 都仍精确引用原不可变 tarball,才把批次标记为已应用;目标 bundle 被移除、替换或在 Host 启动后再次漂移时仍会 fail-closed 并要求显式处理。

队列、applying journal 和“已安装、待重启”状态都通过临时文件 fsync、rename、目录 fsync 后再切换内存状态。安装/重启失败按配置有限退避,达到上限后使用 `dsh_dev_retry_update(confirm=true)` 显式恢复;supervisor 恢复时若发现结果未知的 applying journal,或旧版待重启记录缺少 install-time profile generation,则禁止自动处理,必须显式确认。更新类非幂等 RPC 在响应丢失时不会盲目重发。自动重启只接受显式 `stableSystemdUnit` 或 `/proc/self/cgroup` 的唯一叶子 `.service`,拒绝 scope 中的祖先 service。systemctl 使用阻塞式 restart:明确失败时释放栅栏并记录错误,成功路径由 Host teardown 和新 PID 确认。protocol v5 以前的 supervisor 不支持此能力时,先保存开发实例中的工作并执行 `dsh_dev_shutdown(confirm=true)`,让下一次调用启动新版 supervisor。稳定更新 Web API 只接受回环对端。

安装 PDM 后,它会自动向 DSH 的全局 system prompt 注册稳定更新弱护栏,不需要再安装独立 prompt Plugin。规则按行为而不是按某条宿主命令描述:稳定 profile 的修改、包替换以及 Host、supervisor、service、container、process 或 machine 的中断都必须走 PDM 队列和 idle gate;队列不可用时 Agent 必须停止并请求明确的 fallback 选择,不能自行换用 shell、文件系统、包管理器或平台 API 绕过。该机制会影响后续 prompt 组装,包括已有会话的后续 turn,但它是模型约束,不是 OS 权限隔离,不能替代强制执行。

## 安全边界

- 所有子进程通过参数数组启动,`shell` 固定为 `false`。
- `allowedRoots` 在真实路径解析后检查,可拦截越界路径和符号链接逃逸。
- supervisor 只监听 `127.0.0.1`,RPC 使用 256-bit 随机 bearer token。
- 控制面 RPC 通过 Node 原生 loopback HTTP 发出,不读取代理分发器。
- supervisor 元数据与配置以 `0600` 权限写入。
- Web 管理 API 默认同时要求 loopback 对端与可信 authority。只有显式启用 `allowRemoteWebApi` 后,非回环对端才可通过 `webRuntime.trustedHosts` authority、Fetch Metadata 与 Origin 篱笆访问;Origin 必须同时匹配当前 transport scheme 与 authority,远端伪造 `Host: 127.0.0.1` 不会被当成本地请求。写操作还要求同源 Client 使用的自定义请求头。
- `allowRemoteWebApi` 是可信网络 opt-in,不是用户认证;启用后,同一受信网络中的原生 HTTP 客户端仍可能伪造浏览器头并执行管理操作。必须配合防火墙、VPN 或其他网络访问控制,不能把端口直接暴露到不可信网络。该显式 opt-in 会通过受管环境变量传给开发 DSH,但默认 `false` 不会被自动放宽。
- `discoverLanUrls` 只展示经 TCP 探测实际可达的私网 URL,不绕过 DSH 对 `--host 0.0.0.0` 的安全限制。LAN 监听必须由开发 profile 显式启用(例如放入受信的 LAN-access Preset),并配合防火墙或 VPN;这不是用户认证。
- package script 名称经过白名单校验。
- 日志和命令输出有大小限制。
- 所有本地 Plugin 都必须声明 `dsh.bundle`。
- 端口冲突时启动失败,不会接管未知进程。
- 停止后会确认受管 Host/watcher 进程句柄对应的 PID 已退出且端口关闭;任一受管进程仍存活时拒绝注销或清理。目标脚本自行 daemonize 的后代进程不在句柄保证范围内,因此只管理受信任的 package scripts。子进程退出回调会立即清除受管句柄,降低 PID 复用导致的误信号风险。
- 删除要求传入与列表完全一致的规范化实例 ID;加载 registry 时也会重新验证项目和成员 ID。清理路径只由 `stateDir` 和这些 ID 推导,每次删除都校验受管根目录和 containment,不信任 registry 中的 `dshHome`,也不会跟随实例或 artifact 根目录的符号链接;日志按已知精确文件名清理,避免相似 ID 互相误删。
- `purge` 保留整个 `artifacts/<id>/releases/`,不会把晋级产物与临时验证数据一起删除。

安装依赖和执行 package script 会运行目标仓库中的代码。只管理受信任的 Plugin 仓库,并把 `allowedRoots` 缩小到实际开发目录。

## 开发与验证

```bash
pnpm install
pnpm run typecheck
pnpm test
pnpm run build
pnpm run test:e2e
pnpm pack --dry-run
```

`test:e2e` 会创建临时 Plugin 和临时 `DSH_HOME`,使用真实 `dsh` CLI 完成 link、dump-config、Web 启动、HTTP 健康检查、Manager 重连、受控停止、clean validation、晋级和实例清理,并在 purge 后重新校验晋级 tarball 的 SHA-256、历史元数据和 `latest.json`。单元测试还覆盖多 Plugin 组合、Preset、YAML 清单、多个 watcher、并发删除、cleanup 恢复、进程存活校验、路径防护和代理绕行。

环境覆盖:

```bash
DSH_COMMAND=/path/to/dsh \
DSH_DEV_MANAGER_E2E_PORT=43881 \
CHOKIDAR_USEPOLLING=1 \
pnpm run test:e2e
```

## 已知边界

- 当前管理 Web profile。
- Client HMR 依赖各本地成员持续重写 Client bundle;默认探测 `dev:client` script。
- Host 代码变化需要调用 `dsh_dev_restart`。
- supervisor 进程自身异常退出时,已启动的开发 DSH 可能成为 `orphaned`;安全策略会保留它并拒绝自动终止。
- 稳定 DSH 自身退出仍会中断它正在执行的任务;独立 supervisor 与开发实例会继续运行,稳定 DSH 恢复后可重新连接。

Install

dsh plugin --profile web add github:QuanQQQ/dsh-plugin-dev-manager#caf2f234c1a63b25c08b5a17e90571214ae0eb4c

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
Source