DSH 设计哲学与本插件的对齐(Design notes)

August 14, 2026 · View on GitHub

本文是社区观察,基于对 DSH(deepseek-harness)源码与文档的阅读整理, 非官方文档。目的是解释"DSH 为什么这样设计"以及"本插件为什么是现在 这个样子",帮助读者理解取舍,而不是抱怨边界。

1. 一切皆插件:两个平面

DSH 的组装(composition)分成两个平面:

平面内容生命周期
宿主组装(host composition)注册表(tools/agents/skills)、沙箱与审批栈、持久化、模型路由、子代理注册表进程级,一份
agent 预设(agent preset)一个会话的能力面:工具行、persona、提示词段落、技能随会话挂载/卸载

工具注册表按 scope 三层分层:agent → preset → global近者遮蔽远者。 这是"项目 > 预设 > 宿主"可见性优先级的机制来源——本插件把 MCP 工具注册进 agent 层,正是尊重这一分层:项目配置只影响它自己的会话,不污染全局。

2. 信任模型:能力 = 信任

  • preset 就是组装:一个 user 预设的权限恰好等于它引用的插件——与 shell 访问同级。trust: system(随附只读)与 trust: user(可写)的 区分用于呈现,不用于隔离。
  • 项目目录是"内容区",不是"信任区":DSH 允许项目放 .dsh/skills.agents/skillsAGENTS.md——全是提示词文本(模型可斟酌、可拒)。 项目目录没有可执行配置的官方入口。
  • 推论:项目级 MCP(command/env 会 spawn 进程)是"可执行配置", 与 package.json scripts 同级信任——官方不内置它,最可能的理由正是 不替用户做这个信任陈述。预设是官方给出的"项目能力面"答案:能力绑定 显式选择,而不是绑定"打开目录"这个无意识动作。
  • 本插件的对齐.dsh/mcp.jsonopt-in 文件(存在才加载); 子进程用官方 scrubbedParentEnv() 降权(凭据形态变量不继承); 每个动作写可审计日志;README 明确声明"与 package.json scripts 同级信任"——缩小爆炸半径,但不假装能防供应链攻击。

3. 热加载边界:什么热,什么冷,为什么

变化热?机制
settings.yaml用户设置base 组装的 hot-reload 文档
用户补丁层(cordis.patch.yml加/改/删行✅ ~4swatchUserPatcheshmr.registerConfig → include entry.update
动态插件定义/激活/卸载✅ 全热cordis_run/cordis_stop(进程内存)
.dsh/mcp.json项目配置✅ ~1s本插件的 fs.watchFile + 世代替换
bundle 列表dsh.profile.bundlesdsh plugin add/remove重启启动时 composeProfile 组合,无 watcher

哲学解释:DSH 热的是已装配内容的变化,冷的是装配本身的变更。 装配变更(装一个带 94 个新依赖的包)如果热应用,中途失败的回滚与诊断 复杂度远高于启动时 fail-loud 审计(例如 must be a top-level YAML array ——启动即点名)。按"高频操作热、低频操作冷"分配,是工程取舍,不是理念 背叛;"一切皆插件"承诺的是能力边界的插件化,不是装配本身的热。

行业常态:VS Code 装扩展要"重新加载窗口",浏览器装扩展要重启, Claude Code 装 MCP server 要重开会话——"新包的安装要重载"是全行业常态; "已装内容的启停"才是热插拔的承诺对象(DSH 通过动态插件完整兑现)。

4. 为什么项目级 MCP 要由插件实现(而不是官方内置)

  1. 信任边界:可执行配置不该落在"打开即执行"的位置(见第 2 节)。
  2. 官方扩展点就是插件:DSH 的一切能力都是插件行——实现一个"项目级 MCP 加载器"插件,是官方认可的做法,且不破坏任何分层。
  3. 风险自担的诚实:插件作者替用户做了信任陈述,所以必须把降权、 审计、文档声明做齐(本插件的三个对齐措施)。

5. 本插件的已知边界

  • bundle 安装需重启一次dsh plugin add 改的是 bundle 列表(冷层), 这是 DSH 机制,不是插件缺陷;本机迭代可用"用户补丁行 + 包名"热安装 (见 README「免重启开发路径」)。
  • 只桥接工具:MCP 的 resources/prompts 没有消费接口。
  • 不支持任务式执行:仅普通 call(与官方 dsh-mcp-client 一致)。
  • 每 agent 独立连接(v4):每个 agent 对每个服务器各建各的连接——N 个 会话调用同一服务器 = N 个进程。隔离优先于共享;空闲超时兜底成本 (不用的连接关闭并释放进程)。

6. v3 连接 supervisor(设计记录)

连接会死是常态(休眠唤醒、浏览器重启、手动杀进程、服务器崩溃), 而"死得无声"最坑——工具还挂在列表里,调用永远失败,没有任何日志。 v3 的目标是把"静默死亡"变成"数秒自动复活"。

设计决策

  • 事件驱动(SDK client.onclose)而非轮询:PoC 验证过 Windows 强杀 stdio 子进程时 onclose 可靠触发(子进程 close → transport.onclose → protocol._onclose → client.onclose)。零开销、第一时间感知。
  • 不做退避重试循环:重建失败就停,等下一次触发(配置变化、新会话、 再次死亡)。官方桥的指数退避适合"一个实例管一条连接";项目级桥要防 多会话并发重建风暴,事件驱动 + 失败即停更简单可靠。
  • 重建失败不污染池:connectServer 失败时池条目删除,下次触发重新 尝试,不会缓存坏连接。

竞态教训(0.1.10 修复)poolPromises.has(key) ≠ "我的连接活着"。 onclose 删掉池条目后,第一个完成 reload 的会话会重建连接并用同一个 poolKey 填充池;后续会话的 reload 看到池条目存在(同指纹同 key)就 跳过——但它们的工具定义仍绑定死 client。症状:旧会话 Not connected、 新会话正常。修复:onclose 时把该项目所有会话的 record 标记 dead, 跳过条件要求 !record.dead,使每个会话都强制 teardown + 重建(重建时 复用池中已恢复的连接,不重复连接)。

7. v4:每 agent 独立连接(设计记录)

v3 的竞态修复都是给"共享池"设计打补丁:池、广播、指纹、record.dead 标记——全都只为了服务"每个(项目, 服务器)一个进程"。v4 直接把池删了:

  • 每 agent 独占连接:每个 agent 自己持有 serverName -> { client, idleTimer }。没有共享就没有竞态——§6 的竞态教训在 v4 里结构上不可能 出现。
  • 懒连接:连接只存在于"第一次调用"到"空闲超时"之间。会话创建时通过 一次性 schema 同步注册工具(连接 + 列工具 + 注册 + 关闭)——schema 只 存在于服务器上,注册永远不能懒,但注册之后的连接可以懒。
  • 死亡处理onclose 只丢本 agent 的连接;下次调用重连。没有 supervisor、没有广播、没有重建风暴。
  • 成本:N 个会话调用同一服务器 = N 个进程。这是设计接受的(隔离), 由空闲超时兜底。池的经济学(1 个进程、共享状态)正是 v4 拒绝的东西。

权衡要诚实:对有状态服务器(浏览器自动化),每 agent 连接严格更优 (每个会话拿到自己的状态);对无状态服务器则多花进程。idleTimeoutMs (默认 5 分钟,0 = 永不)是两者之间的旋钮。