核心摘要
DeepSeek Harness 插件是通过 apply(ctx)、依赖声明、服务、事件和可逆副作用扩展 Cordis 的模块。一个正确插件不止要能编译:它应选择最窄的文档化接缝,在卸载时释放资源,保持授权边界,并针对锁定的 DSH 版本进行测试。本文讨论这些契约,而不是提供一份很快失效的插件清单。
目录
最小可用插件
DeepSeek Harness 插件是由 Cordis 挂载的 TypeScript 模块。官方“第一个插件”教程使用导出的 name 与 apply 函数;apply 接收运行时 Context,插件通过文档化服务或事件注册行为。
import type { Context } from '@deepseek-ai/cordis'
export const name = 'workspace-notice'
export function apply(ctx: Context) {
console.info('[workspace-notice] mounted')
}
这个例子故意很小。它只能证明加载器可导入并挂载模块,不代表提供了工具、服务、授权、持久化或清理策略。每次只添加一种能力,并为该能力增加可观察的测试。
官方教程通过绝对路径的 cordis.yml Overlay 加载本地插件:
- insert:
- id: workspace-notice
name: "/absolute/path/to/deepseek-harness/scratch-plugin/src/workspace-notice.ts"
使用 Overlay 启动 Web Profile:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
先只在一次性工作区使用这套流程。Patch 能改变激活的插件树,因此生产 Patch 应具备与可执行部署配置相同的审查和回滚要求。
服务、依赖与生命周期
Cordis 服务在 Context 上提供具名能力。需要某项服务的插件应在 inject 中声明,框架会等待依赖就绪,而不是依赖隐式的注册顺序。
import type { Context } from '@deepseek-ai/cordis'
export const name = 'guarded-tool-extension'
export const inject = ['tools']
export function apply(ctx: Context) {
// 因为已声明依赖,ctx.tools 在此处已经可用。
// 在这里注册一个边界明确的能力。
}
该模式有三个关键边界:
| 关注点 | 正确契约 | 不安全的捷径 |
|---|---|---|
| 依赖 | 为消费的服务声明 inject |
访问可选服务并假定它必定存在 |
| 生命周期 | 通过 ctx.on() 或 ctx.effect() 注册 |
保留未受管理的定时器、Socket 或监听器 |
| 能力 | 依赖 ctx.tools 等接口 |
导入并修改某个具体内部实现 |
Cordis 注册是可逆副作用。受支持的事件监听器和注册项会在插件卸载时移除。定时器、网络客户端等需要自定义回收的资源,应从 ctx.effect() 返回 disposer:
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.info('plugin heartbeat')
}, 30_000)
return () => clearInterval(timer)
})
}
不要把定时器当作持久化调度或恢复机制。进程重启后它会消失;需要定时执行的任务必须拥有持久化 Job 契约和明确恢复模型。
服务、事件和副作用如何选择
DSH 架构区分直接能力和生命周期接缝。插件需要直接调用能力时使用服务;需要观察、转换或治理运行时中的工作流动时使用事件。
官方 Extension Cookbook 给出如下分工:
tools/pre-execute:在派发前决定允许、拒绝或要求审批。tools/execute:包裹派发生命周期,实现超时、重试或计量。tools/post-execute:只有确实需要时才转换结果。tools/result:观察不可变最终结果,用于审计或指标。session/event:将日志事件投影到界面、遥测或外部协议。
这些接缝不能互换。事后记录结果无法阻止被禁止的操作;权限决定也不应藏在指标监听器中。
工具策略插件示例
下方草图遵循官方 tools/pre-execute 模式,只展示一个边界明确的策略决定,并不是完整授权系统。
import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
export const name = 'repository-write-policy'
export const inject = ['tools']
async function canWriteWorkspace(exec: ToolExecution): Promise<boolean> {
// 从应用拥有的策略服务解析用户身份、租户、工作区、分支与审批状态。
// 不能信任模型生成的参数。
return exec.tool.name !== 'write_file'
}
export function apply(ctx: Context) {
ctx.on(
'tools/pre-execute',
async (exec, next): Promise<PreToolDecision> => {
if (!(await canWriteWorkspace(exec))) {
return { kind: 'deny', reason: 'Workspace write is not approved.' }
}
return next()
},
)
}
核心细节是 next()。仅追加检查的 waterfall 监听器必须把控制权交给后续链路;真正拥有最终决定的监听器可以返回类型化拒绝来停止链路。事件的派发模式属于公共契约,不能把它假设为普通中间件。
该插件仍不能证明写入安全。生产策略必须将已认证的用户和租户绑定到精确资源、参数、时间窗口、策略版本和审批制品。更广泛的设计可参阅 人机协同 和 审批门禁。
配置覆盖与发布
Profile、Bundle 和 Patch 使 DSH 部署可组合,同时也使配置成为会改变行为的制品。建议采用如下发布契约:
- 锁定 DSH 版本及所有外部插件依赖。
- 将经过审查的 Patch 文件纳入版本控制,避免手工修改生产状态。
- 在预发布环境导出有效插件树,并与批准配置进行比较。
- 在隔离工作区执行能力、拒绝、取消和卸载测试。
- 以最小工具白名单和作用域凭证逐步发布。
- 启用写操作之前,保留回滚 Patch 或上一版 Profile。
不要根据插件名称承诺兼容性。DeepSeek Harness 仍处于开发者预览阶段,配置形态和扩展契约均可能改变;可重复的测试集比教程快照更可靠。
插件契约测试
应测试插件的可观察契约,而不是只测试成功路径。下表是实用的最小集:
| 测试 | 断言 |
|---|---|
| 挂载 | 声明的服务依赖在 apply() 运行前已经可用 |
| 卸载 | 监听器、定时器和客户端被释放 |
| 允许操作 | 被允许的工具进入执行并产生结果 |
| 拒绝操作 | 被禁止的工具不会进入执行 |
| 审批过期 | 过期审批不能授权已变化的参数 |
| 取消 | 取消不会留下未受管理的后台工作 |
| 升级 | 相同测试集可通过目标锁定的 DSH 版本 |
对于工具插件,应使用伪造或沙箱化的下游系统做集成测试。不能把共享仓库或真实客户数据当作写入策略的测试环境。Agent Harness 评测指南 中的故障注入和发布门禁可以补充框架层测试。
常见问题 (FAQ)
每个 DeepSeek Harness 功能都应该拆成插件吗?
不应该。只有具备独立生命周期、配置面、依赖边界或替换需求的能力才适合成为插件。把每个小辅助函数都拆成插件会掩盖控制流并增加兼容测试成本。
可以直接使用 DSH 仓库的私有 API 吗?
从源码检出中导入内部实现技术上可行,但不应把它视为稳定扩展契约。优先使用文档化的服务和事件接缝;若确实无法避免,必须锁定源码版本并增加升级测试。
插件状态应如何保存?
按状态语义选择。仅运行时缓存可以保存在内存;模型可见或与回放相关的事实需要持久化会话事件;外部副作用则需要应用拥有的操作记录与对账。
插件可以添加 MCP Server 吗?
DSH 的扩展映射将 MCP 描述为每个 Server 一个插件:发现工具后注册到 ctx.tools。发现不等于授权,仍要验证 Server、工具元数据、租户 Scope、参数、结果上限和下游副作用。
插件需要锁定哪些版本?
至少锁定插件包或提交、DSH 版本、配置 Patch、服务假设、模型路由、策略版本、测试集版本,以及所有会改变行为的外部协议或 Tool Schema。