核心摘要
DeepSeek Harness 插件是通过 apply(ctx)、依赖声明、服务、事件和可逆副作用扩展 Cordis 的模块。在本文核验的 dsh-v0.2.1-alpha.1 源码中,可部署扩展还应属于一个 Bundle,而可运行安装则属于由有序 Patch Layer 组成的 Profile。因此,正确插件必须具备生命周期测试、明确兼容目标、可检查的有效插件树和回滚路径。
目录
最小可用插件
DeepSeek Harness 插件是由 Cordis 挂载的 TypeScript 模块。官方入门教程使用导出的 name 与 apply(ctx) 函数。这个形态只能证明 Loader 能导入并挂载模块,不代表已经提供工具、授权、持久化、分发元数据或清理策略。
应把本地开发与持久安装拆成四个阶段:
| 阶段 | 制品 | 能证明什么 |
|---|---|---|
| 临时开发 | TypeScript 模块与 --patch Overlay |
模块可在一次性检出中挂载 |
| 分发 | 声明 dsh.bundle.patch 的 npm Package |
Package 提供具名配置层 |
| 安装 | 包含 Bundle 列表与 Profile Patch 的具名 Profile | 目标运行时可解析并组合 Package |
| 运行验证 | 有效插件树、激活状态与契约测试 | 目标能力已激活且行为符合预期 |
官方教程通过绝对路径的 cordis.yml Overlay 加载本地插件:
- insert:
- id: workspace-notice
name: "/absolute/path/to/deepseek-harness/scratch-plugin/src/workspace-notice.ts"
使用 Overlay 启动 Web Profile,并检查组合后的有效插件树:
pnpm dsh --profile web --patch ./scratch-plugin/cordis.yml
pnpm dsh --profile web --patch ./scratch-plugin/cordis.yml --dump-config
先只在一次性工作区使用这套流程。Patch 能改变激活的插件树,而且模块直接在 Host Process 中执行;文件 Sandbox Mode 不会把不可信插件代码自动变成受隔离依赖。
服务、依赖与生命周期
Cordis 服务在 Context 上提供具名能力。例如,需要 ctx.tools 的插件应在 inject 中声明 tools,Cordis 会让插件保持 Pending,直到 Provider 可用。如果所需服务随后消失,Cordis 会卸载依赖插件,只有服务恢复后才重新挂载。因此,依赖注入是一份实时生命周期契约,而不只是启动时检查。
该模式有三个关键边界:
| 关注点 | 正确契约 | 不安全的捷径 |
|---|---|---|
| 依赖 | 为消费的服务声明 inject |
访问可选服务并假定它必定存在 |
| 生命周期 | 通过 ctx.on() 或 ctx.effect() 注册 |
保留未受管理的定时器、Socket 或监听器 |
| 能力 | 依赖 ctx.tools 等接口 |
导入并修改某个具体内部实现 |
Cordis 注册是可逆副作用。受支持的事件监听器和注册项会在插件卸载时移除;定时器、网络 Client、Watcher 等非托管资源必须在 ctx.effect() 内创建,并由 Disposer 释放。如果清理存在顺序依赖,应把相关步骤放在同一个 Effect 中,因为多个异步 Disposer 可能并发执行。
不要把定时器当作持久化调度或恢复机制。进程重启后它会消失;需要定时执行的任务必须拥有持久化 Job 契约和明确恢复模型。
服务、事件和副作用如何选择
DSH 架构区分直接能力和生命周期接缝。插件需要直接调用能力时使用服务;需要观察、转换或治理运行时中的工作流动时使用事件。
当前 Extension Cookbook 与 Tool Subsystem 给出的执行顺序更严格:
tools/pre-execute:派发前执行可扩展的允许、拒绝或审批策略。ctx.tools.guard():执行后续 Listener 不能撤销的单调拒绝不变量。tools/execute:包裹派发生命周期,实现取消、超时、重试或计量。tools/post-execute:在最终物化前检查、阻止或替换结果。tools/result:只观察冻结后的权威结果,不能再修改。session/event:将日志事件投影到界面、遥测或外部协议。
这些接缝不能互换。可重排的 Pre-execute Listener 不是最终安全边界;Monotonic Guard 不能强制允许其他 Guard 已拒绝的调用;替换展示文本不会自动隐藏程序化 Value;tools/result Observer 也不可能阻止已经发生的副作用。
把策略放在正确的工具接缝
应根据需要执行的不变量选择接缝。追加协作式审批策略的插件属于 tools/pre-execute,不拥有最终决定时必须通过 next() 委托。不能受 Listener 重排影响的租户边界属于 Monotonic Guard;超时包装属于 tools/execute;最终指标则属于 tools/result。
这些框架接缝都不能自行证明写入安全。生产策略必须把已认证用户与租户绑定到精确资源、参数、Schema Digest、时间窗口、策略版本和审批制品,下游服务也必须独立执行授权。更广泛的设计可参阅 人机协同 和 审批门禁。
Profile、Bundle、Patch 与安装
当前 DSH 把开发者分发的 Package 与用户实际运行的应用组合明确分开:
| 概念 | 当前职责 | 运维含义 |
|---|---|---|
| Bundle | 声明一个或多个有序 Patch 文件的 npm Package | 按可执行供应链输入进行版本锁定与审查 |
| Profile | Harness Home 下的一份可运行组合 | 把用户和环境选择留在 Bundle 之外 |
| Profile Patch | 在 Bundle Layer 之后应用的用户层 | 后应用的 Row 胜出,且 config 整体替换而非深度合并 |
| Home Patch | 多个 Profile 共享的机器级偏好 | 评审其对每个 Profile 的影响 |
--patch Overlay |
优先级最高的单次启动层 | 用于受控实验,不能形成未记录的生产漂移 |
持久安装使用 dsh plugin --profile <name> add <spec>。Manager 会先检查 Bundle Metadata 与运行时 Peer Compatibility;依赖构建脚本需要单独批准,因为脚本以 Host User 权限执行。版本豁免只对精确 Plugin/Runtime 组合生效,升级不会继承,应把它视为一次明确接受崩溃或数据丢失风险的决定。
发布前应执行:
- 锁定 DSH Revision 与所有外部插件依赖。
- 检查源码、依赖脚本、Host Capability 与 Bundle Patch。
- 将审核后的 Patch 和预期有效插件树纳入版本控制。
- 安装到一次性 Profile,确认激活状态是
applied、restart-required还是失败。 - 执行允许、拒绝、取消、卸载、重启和数据兼容测试。
- 使用最小能力集、作用域凭证和经过验证的移除路径逐步发布。
不要根据 Package 名称承诺兼容性。DSH 公共 API 仍处于 Pre-stable 状态,而 Session 数据承担比普通运行时接口更严格的兼容义务。
插件契约测试
应测试插件的可观察契约,而不是只测试成功路径。下表是实用的最小集:
| 测试 | 断言 |
|---|---|
| 挂载 | 声明的服务依赖在 apply() 运行前已经可用 |
| 卸载 | 监听器、定时器和客户端被释放 |
| 允许操作 | 被允许的工具进入执行并产生结果 |
| 拒绝操作 | 被禁止的工具不会进入执行 |
| 审批过期 | 过期审批不能授权已变化的参数 |
| 取消 | 取消不会留下未受管理的后台工作 |
| 有效插件树 | 导出配置与审核通过的 Bundle、Profile Layer 一致 |
| 持久化 | 旧 Fixture 要么忠实迁移,要么失败关闭 |
| 升级 | 相同测试集可通过目标锁定的 DSH 版本 |
对于工具插件,应使用伪造或沙箱化的下游系统做集成测试。不能把共享仓库或真实客户数据当作写入策略的测试环境。Agent Harness 评测指南 中的故障注入和发布门禁可以补充框架层测试。
下面的 Go 门禁在插件发布前校验一次已捕获的 Rollout Manifest。它不会加载插件,而是避免运维人员把“已经安装”误判成“兼容且已激活”。
package main
import (
"errors"
"fmt"
)
type Manifest struct {
RuntimeVersion string
PluginVersion string
PeerCompatible bool
BundleDeclared bool
ApplicationState string
TestsPassed bool
RollbackTested bool
}
func validate(manifest Manifest) error {
if manifest.RuntimeVersion == "" || manifest.PluginVersion == "" {
return errors.New("runtime and plugin versions must be pinned")
}
if !manifest.PeerCompatible {
return errors.New("plugin peer range rejects this runtime")
}
if !manifest.BundleDeclared {
return errors.New("package has no dsh bundle patch")
}
if manifest.ApplicationState != "applied" {
return fmt.Errorf("plugin is not active: %s", manifest.ApplicationState)
}
if !manifest.TestsPassed || !manifest.RollbackTested {
return errors.New("contract tests and rollback rehearsal are required")
}
return nil
}
func main() {
ready := Manifest{
RuntimeVersion: "dsh-v0.2.1-alpha.1",
PluginVersion: "plugin-7",
PeerCompatible: true,
BundleDeclared: true,
ApplicationState: "applied",
TestsPassed: true,
RollbackTested: true,
}
restartRequired := ready
restartRequired.ApplicationState = "restart-required"
fmt.Printf("ready=%t\n", validate(ready) == nil)
fmt.Printf("restart_required_rejected=%t\n", validate(restartRequired) != nil)
}
常见问题 (FAQ)
每个 DeepSeek Harness 功能都应该拆成插件吗?
不应该。只有具备独立生命周期、配置面、依赖边界或替换需求的能力才适合成为插件。把每个小辅助函数都拆成插件会掩盖控制流并增加兼容测试成本。
可以直接使用 DSH 仓库的私有 API 吗?
从源码检出中导入内部实现技术上可行,但不应把它视为稳定扩展契约。优先使用生成的 Subsystem Reference 和文档化 Service/Event 接缝;若确实无法避免,必须锁定源码版本并增加升级测试。
插件状态应如何保存?
按状态语义选择。仅运行时缓存可以保存在内存;模型可见或与回放相关的事实需要持久化会话事件;外部副作用则需要应用拥有的操作记录与对账。
插件可以添加 MCP Server 吗?
DSH 的扩展映射将 MCP 描述为每个 Server 一个插件:发现工具后注册到 ctx.tools。发现不等于授权,仍要验证 Server、工具元数据、租户 Scope、参数、结果上限和下游副作用。
插件需要锁定哪些版本?
至少锁定插件包或提交、DSH Revision、Profile Template、Bundle 顺序、配置 Patch、Service 假设、模型路由、策略版本、测试集、Session Fixture,以及所有会改变行为的外部协议或 Tool Schema。