避开五条硬契约,别让插件写坏他人会话历史
29.9/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/1 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add imtokenxinluo/dsh-plugin-guide该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:需留意
是什么
dsh 原生插件 · dev
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 32 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
⚠︎ 实装验证未通过(unknown · 2026/10/1) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0⚪ 扫描:无法判定尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0已尝试扫描但取不到可分析的源码:仓库或包不存在、已删除,或为非 JS/TS 实现、未随包发布源码。无法判定,不代表安全。
- · 包内无可扫 JS
- · TS 源码(可能为非 JS 实现或未发布源码),无法判定
扫描于 2026/11/1(0 天前) · 范围 全包扫描(包内未声明入口) · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 避开五条硬契约
- 防止写坏会话历史
README
由 DeepSeek 最新模型翻译生成DSH 插件开发者守则
一句话:写 DSH 插件,你的一次疏忽,代价由别人的记忆承担。
状态:draft v0.1 · 2026-09-13 · 本地草稿(未发布)对象:任何给 DeepSeek Harness 写插件(cordis 插件 / bundle / 工具)的人依据:DSH 0.1.0-rc.6 安装版内核实读 + 我们亲历的三次事故 + 社区公开报告
0. 为什么要有这份文档
不是洁癖。以下代价都是真实发生过的:
| 事故 | 现象 | 谁受损 |
| dsh-ssh-ops 的 sftp_/tunnel_ 工具 render() 返回裸字符串 | 多个会话报 history unavailable … must contain one tool-result block,历史永久打不开 | 使用者的记忆 |
| 我们自己的 dsh-session-doctor@0.2.0 打包漏了 cordis.patch.yml | 安装后 DSH 整个插件树拒绝启动(bundle 机制 ENOENT) | 所有装它的人 |
| 第三方插件写入词汇表外的自定义事件类型 | 新版加载时 整条日志被拒绝解释 | 使用者的记忆 |
共同点:插件作者看不出问题(本地跑得好好的),用户看不出原因(GUI 只报一句含糊的错),伤害落在会话历史上,且当时不可逆。
展开完整文档(README 剩余 1 节)
DSH 插件开发者守则
一句话:写 DSH 插件,你的一次疏忽,代价由别人的记忆承担。
状态:draft v0.1 · 2026-09-13 · 本地草稿(未发布)对象:任何给 DeepSeek Harness 写插件(cordis 插件 / bundle / 工具)的人依据:DSH 0.1.0-rc.6 安装版内核实读 + 我们亲历的三次事故 + 社区公开报告
0. 为什么要有这份文档
不是洁癖。以下代价都是真实发生过的:
| 事故 | 现象 | 谁受损 |
| dsh-ssh-ops 的 sftp_/tunnel_ 工具 render() 返回裸字符串 | 多个会话报 history unavailable … must contain one tool-result block,历史永久打不开 | 使用者的记忆 |
| 我们自己的 dsh-session-doctor@0.2.0 打包漏了 cordis.patch.yml | 安装后 DSH 整个插件树拒绝启动(bundle 机制 ENOENT) | 所有装它的人 |
| 第三方插件写入词汇表外的自定义事件类型 | 新版加载时 整条日志被拒绝解释 | 使用者的记忆 |
共同点:插件作者看不出问题(本地跑得好好的),用户看不出原因(GUI 只报一句含糊的错),伤害落在会话历史上,且当时不可逆。
DSH 的架构决定了这个不对称:写入路径宽松,读取路径严格,失败模式是“整条会话”而不是“这一条记录”。
1. 五条硬契约(违反 = 损害他人数据)
C1 · 工具的 render() 必须返回 ContentBlock[]
症状:会话某天突然报 SessionPersistenceCorruptionError: session event at seq N message must contain one tool-result block → 整条历史不可加载。
根因(已核验):@deepseek-ai/dsh-tools 的 createSuccessResult() 里
rendered = tool.output.render(...) 的结果被原样写入会话日志,运行时从不校验它是不是数组。
而加载器要求 tool-result 块的 content 必须是数组。类型契约 render(): ContentBlock[] 只存在于 TypeScript 里——内核自己的工具都遵守,所以内部永远测不出问题;第一次暴露必然来自第三方插件。
规矩: // ✗ 会写坏别人的会话
output: { render: (args, value) => 已写入 ${value.bytes} 字节 }
// ✓
output: { render: (args, value) => [{ type: "text", text: 已写入 ${value.bytes} 字节 }] }
每一个 render() 返回路径都要检查——包括提前 return、错误分支、map/filter 之后。
(dsh-ssh-ops 就是 9 个工具、15 处返回点里漏了几处。)
自检:grep -n "render:" src/ 逐个看返回值,或跑一份静态审计(见 §5)。
C2 · 消息事件的形状必须与加载器一致
根因(已核验):加载路径 assertMessageEventShape 对消息类事件逐项校验;不匹配即拒载整条日志。
(写路径在未打补丁的内核上不校验——所以你本地怎么试都正常。)
| 事件 | 必须满足 |
| user/message | data.content 是数组;data.role === "user";data.source.kind 是非空字符串;data.id 非空 |
| assistant/message | 同上但 role === "assistant";source.kind === "model" 且带非空 provider / model |
| tool/result | role === "user";source.kind === "tool" 且 source.callId 非空;message.content 长度恰为 1;content[0].type === "tool-result";content[0].content 必须是数组;content[0].toolCallId === source.callId |
| request/header | data.header.config 带非空 provider / model;reasoningEffort 若存在须为非空字符串 |
规矩:不要手搓消息事件。要产出工具结果,就让 render() 干(C1);要发消息,让框架干。
C3 · 只写词汇表内的事件类型——绝不要自己发明事件名
这是最容易被忽略、后果最重的一条。
根因(已核验的内核事实):
- 加载器只认识 KNOWN_SESSION_EVENT_TYPES 词汇表里的事件类型;遇到表外类型直接拒绝解释整条日志: session "…" contains event type "…" (seq N) unknown to this harness and not marked ignorable; refusing to interpret the log — it was likely written by a newer harness
判定就一行:if (KNOWN_SESSION_EVENT_TYPES.has(event.type) || event.ignorable === true) continue;
(dsh-session-persistence/lib/index.js,0.1.0-rc.6 第 1119 行)
- 唯一的豁免是在事件信封上标 ignorable: true。
- 但 Session.append(type, data, ...opts) 的 opts 只接受 sourceEventSeqs 和 surfaceOp——事件信封由内核构造,只有 type/seq/time/data + surface 元数据,插件没有设置 ignorable 的入口。
- 更硬的一条(已核验):整个内核里不存在任何把 ignorable 置为 true 的赋值。这个概念只有读取方在用,没有写入方。也就是说在 0.1.0-rc.6 上,没有任何事件会被标成 ignorable——表外类型 = 必然拒载。
结论:ctx.session.append("my-plugin/whatever", …) 会让使用者的会话在新版 DSH 上无法加载,而且你无法自救。
规矩:
1. 优先不写自定义事件——把状态存在自己的 storageDomain / 自己的文件里。
2. 确实需要写会话事件时,只用词汇表内的类型(本版本共 44 个,含 todo/write、goal/change、session/title 等)。
3. 不要指望“以后内核会加 ignorable 支持”——在你亲手验证它存在之前,就当它不存在。
词汇表全量(0.1.0-rc.6,用于核对)
agent-preset/selected agent/inbox/spliced approval/asked approval/decided
approval/policy assistant/chunk assistant/message command/done command/run
compaction/end compaction/prune compaction/start compaction/summary
feedback/record goal/change hook/invoked hook/result llm/retry
llm/retry-started permission/preset plan/mode request/context request/header
sandbox/mode schedule/change session/end-seed session/title
session/title-llm-request step/end step/start subagent/descriptor todo/write
tool-workflow/agent-end tool-workflow/agent-start tool-workflow/run-end
tool-workflow/run-start tool/call tool/code-dispatch tool/code-dispatch-start
tool/result turn/end turn/start user/message web/deepseek-search-llm-request
自检:grep -rn "\.append(" src/ 看有没有非词汇表的第一参数。
C4 · npm 打包白名单必须覆盖 package.json 声明的一切
症状:安装后 DSH 整个插件树拒绝启动(不是你这一个插件失效,是全部插件都起不来)。
根因(我们自己的事故):package.json 声明了
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
但 files 白名单是 ["src","bin","README.md"]——漏了 cordis.patch.yml。
npm 按白名单过滤打包 → 包里没这个文件 → bundle 机制启动时必须读它 → ENOENT → 插件树整体拒启。
规矩:files 是白名单,声明了什么就必须打包什么。逐一核对:
- [ ] main / exports 指向的文件
- [ ] bin 指向的文件
- [ ] dsh.bundle.patch 指向的文件 ← 最容易漏
- [ ] dsh.client 的入口(若为 client 插件)
- [ ] 运行期真的会 import/readFile 的文件(含运行时模板、cordis.patch.yml、skills/ 目录)
自检(发布前必跑):
npm pack --dry-run # 看清单里有没有上面每一项
再狠一点:解包后按 package.json 的声明逐个 existsSync。
C5 · 不要杀宿主进程,也不要自杀式重启
症状:用户的会话中断、GUI 掉线,最坏情况是运行中的会话被腰斩(日志被截断)。
我们亲历的真实案例:一个会话为了“重启 harness 加载新代码”,自己拼了一条 Get-CimInstance … | Where-Object { $_.CommandLine -match 'dsh' } | Stop-Process -Force 再 Start-Process——它把自己所在的进程杀了,会话死了 2 分 20 秒,turn/end 记的是 interrupted。
规矩:
- 插件绝不杀/重启宿主进程。要重启就提供明确的、用户触发的通道(按钮/命令),并且先起新进程、再退旧进程。
- 进程匹配不要用宽泛关键词(dsh、node)——那会连自己和无关进程一起杀。
- 工具作者注意:给 agent 一个安全的重启工具,比让它自己发挥要好得多(它不会总是选对)。没有安全通道时,agent 会退化成手搓 Stop-Process。
2. 其它已知坑
| 坑 | 症状 | 规矩 |
| 消息配对 | 会话报 400 INVALID_REQUEST 崩溃 | 改写/注入消息时,tool_calls 与对应 tool 结果必须成对;不要只删一半(社区报告:Leeminjing/dsh-messages-sanitizer) |
| typert 命名导出 | typert-loader: … exports "./typert" but its module has no TYPERT manifest object | 必须 export { TYPERT }(命名导出),不是 export default TYPERT |
| profile patch 与 lockfile 不一致 | ERR_PNPM_LOCKFILE_CONFIG_MISMATCH | 删/改 patchedDependencies 时必须同步改 pnpm-lock.yaml(配置里是路径,lockfile 里是 hash) |
| 供应链延迟 | 刚发布的版本装不上 | pnpm 有最小发布龄保护;新版本需在 minimumReleaseAgeExclude 里放行 |
| !!js 配置求值 | 配置里带 !!js 会在加载时执行代码 | 你不是配置的唯一读者时,别在配置里塞可执行表达式 |
| 工具改名/删除 | 会话中断后恢复时,对已不存在的工具会报 unknown tool "…"(我们实测过)| 改名/卸载要当破坏性变更:正在执行的工具调用会失去落点;变更日志里写清楚 |
3. 信任姿态(写之前先想清楚)
cordis 插件是进程内代码。 DSH 自带的"创造模式"讲得很直白:
cordis_mount evaluates model-written JavaScript against the live runtime … Treat a session on this preset as shell access.
也就是说:装一个插件 ≈ 给作者一个 shell。所以:
- 默认只读。写文件、发网络请求、起子进程,都要在 README 里明说。
- 零依赖优先。每个依赖都是你替用户引入的新信任面。(我们的 doctor/backup 都是零运行时依赖。)
- 不要偷偷扩权。新增一个网络端点、一个 shell 调用、一个后台定时器,都算能力扩张,要写在变更日志里。
- 你的插件会读到的内容,都是用户的隐私(会话历史、路径、密钥所在的环境变量)。读之前先问"我真的需要吗"。
4. 发布前自检清单
1. 打包内容是否覆盖全部声明(C4)
npm pack --dry-run
2. render 返回值审计(C1)——对照下面这份脚本思路 grep -rn "render:" src/
3. 是否写了词汇表外的事件类型(C3) grep -rn "\.append(" src/
4. 是否有杀进程 / 宽泛匹配(C5) grep -rniE "taskkill|Stop-Process|pkill|killall|Stop-Service" src/
5. 是否有意外的网络/子进程(§3) grep -rniE "fetch\(|https?://|child_process|spawn\(|exec\(" src/
- [ ] files 覆盖 main/exports/bin/dsh.bundle.patch/dsh.client(C4)
- [ ] 每个 render() 返回路径都是 ContentBlock[](C1)
- [ ] 没有手搓消息事件(C2)
- [ ] 没有词汇表外的事件类型(C3)
- [ ] 没有杀宿主进程、没有宽泛进程匹配(C5)
- [ ] 测试能跑(npm test),且测的是契约,不只是 happy path
- [ ] README 写清:碰什么(文件/网络/进程)、不碰什么、边界在哪
- [ ] 装进一个干净 profile 验一遍:能启动、能卸载、卸载后不留垃圾
5. 范例:什么叫"做对了"
我们自己在维护的两个插件可以当参照(不是让你抄功能,是抄姿态):
- dsh-session-doctor —— 零运行时依赖;默认只读;repair 只修一种已确认的损坏,其它一律报告不修;改文件前必备份、重写先写临时文件、产物复验通过才落盘;每条边界(比如"watch 不阻止写入")都如实写在 README 里。
- dsh-session-backup —— 只做备份/还原,不做分析;manifest 带 sha256;还原前校验。
共同点:能力小、边界清、可验证、不越权。
6. 出处与状态(哪些是核验过的)
| 结论 | 来源 |
| render 契约不校验、写路径宽松读路径严格 | 内核实读(dsh-tools / dsh-session lib)+ 我们应用过的 P0-1/P0-3 补丁 |
| 消息事件形状各项要求 | 内核 assertMessageEventShape 的镜像实现(dsh-session-doctor/src/validate.js) |
| 事件类型词汇表 + 表外类型拒载 + append 无 ignorable | 内核实读(KNOWN_SESSION_EVENT_TYPES、persistence 报错点、Session.append 签名) |
| bundle patch 漏文件导致插件树拒启 | 我们自己的 0.2.0 事故 + 0.2.1 修复 |
| 自杀式重启 | 真实会话日志(2026-09-06,scheduled-run-3fd818d1) |
| 消息配对 / typert / 供应链延迟 | 社区公开报告与本机踩坑记录,标注为社区经验,非内核核验 |
版本敏感性:以上针对 0.1.0-rc.6。升级 DSH 后请重新核验——词汇表、校验函数、append 签名都可能变。这份文档的价值在于"去哪查、怎么查",而不只是结论。
附:为什么这份文档自己不推广
它不带你装任何东西、不指向任何第三方链接、不需要你关注谁。
如果它有用,它会因为"能挡住一次事故"被引用;如果没用,推它也没用。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
imtokenxinluo/dsh-plugin-guide
避开五条硬契约,别让插件写坏他人会话历史
相关插件 · 开发 / 工程
隐私简历生成器reactive-resume/reactive-resume
Reactive Resume 已迁至 GitHub 上的…
★ 45,125已汉化MCP
71.7
AI 架构文档生成器sopaco/deepwiki-rs
🚀 Litho 已进化为 Terrain —— 给你的 AI 智能体一张会呼吸的代码库地图。
★ 3,157已汉化DSH
70.8
长时任务执行闭环AMAP-ML/LongHorizon-Harness
面向 Computer-Use Agent 的 Loop Engineering
★ 1,764已汉化DSH
70.5
AI 编程纪律方法包GanyuanRan/Aegis
让 AI 编程 agent 变得可信:少返工、更安全、说“完成”前先给证据。
★ 1,384已汉化DSH
69.6
多代理编排运行时Enderfga/claw-orchestrator
面向编程智能体的运行时。把 Claude Code、Codex、Antigravity、Grok…
★ 586已汉化MCP
69.4
记忆进化插件csyangwen/dsh-memory-evolve
dsh-memory-evolve 使用场景指南
★ 461已汉化DSH
67.9