返回博客

DeepSeek Harness 能自己改自己,也能被别人改

作者 约 8 分钟读完

实测安装 DeepSeek Harness:343MB、255 个包、490 行插件树。cordis 创造模式让模型自己写插件热装进正在跑的主进程;但装插件就是 pnpm install 一份进程内全权限代码——最漂亮的部分和最没锁的部分,是同一处。


DeepSeek Harness 能自己改自己,也能被别人改

8 月 13 日,DeepSeek 建了 deepseek-ai/deepseek-harness 这个仓库。四天后 14 W 个 Star,1.4 W 个 fork,但 0 个 open issue——issue 那一栏是关着的。

当前已经有不少写得很好的架构拆解了,机制层面我不重复。我想干的是把它装到自己机器上,一步步走完,看这套"一切皆插件"在真实的安装-启动-装插件流程里长什么样。

走完之后我发现,这套架构最漂亮的部分和最没锁的部分,是同一处。


2 分钟,343M,255 个包

我这台是 Node v22.21.1(官方要求 ^22.19.0 || >=24.0.0),pnpm 10.32.1。

mkdir dsh-test && cd dsh-test
npm install @deepseek-ai/dsh

实测下来耗时 1 分 42 秒,node_modules 343 MB,顶层 255 个包,其中 @deepseek-ai/ 作用域下 195 个。

195 个官方包按前缀数一下,能看出这个产品的重心在哪:

前缀包数
client-*(前端)40
tool-*(工具)19
session-*(会话)16
host-*8
cordis-*(vendor 进来的框架)8
agent-*6
sandbox-* / llm-* / fs-* / subagent-*各 4

40 个前端包,6 个 agent 包。界面被切得比 agent 循环还碎。

当前版本 0.1.0-rc.6。

启动之后只有一行输出

./node_modules/.bin/dsh web

等了大概 40 秒,终端吐出一行:

dsh web: http://127.0.0.1:3080

就这一行。没有 banner,没有 "listening on",没有提示你下一步该干什么,没告诉你要去哪儿填 API key。这个克制和它那份 1700 字符、不带截图的 README 是同一个风格:它假设你已经知道自己在干什么。

curl 一下首页,HTTP 200,12,076 字节。有意思的是 HTML 的第一个 <script>:

<script>window.__DSH_BOOT__ = {"rev":"dcccb8324e44","entries":[
  {"id":"@deepseek-ai/dsh-typert-registry","url":"/plugins/@deepseek-ai/dsh-typert-registry/client.js?rev=f41d56e0b747","inject":[],"immediately":true},
  {"id":"@deepseek-ai/dsh-api-gateway","url":"...","inject":["@deepseek-ai/dsh-typert-registry","@deepseek-ai/dsh-client-connection"]},
  ...

我把这段 JSON 解出来数了数:38 个前端插件条目,9 个标了 immediately: true 立即加载,另外 29 个按 inject 声明的依赖关系加载。每一个都带自己的 rev 哈希。

前端插件依赖树:9 个立即加载,其余按 inject 声明等待

所以前端自己就是一棵插件树,浏览器按 inject 顺序把它拼起来,每个插件一个独立 URL、一个独立版本号。文档里说"webui 也是插件、可以在服务不中断的情况下换掉",不是修辞——rev 变了,浏览器就去拉新的那一份。

后端这棵树可以直接打出来:

dsh --profile web --dump-config

web profile 490 行,headless profile 333 行。每一行是一个插件条目,长这样:

- id: agent-default-model
  name: '@deepseek-ai/dsh-agent-default-model'
  config:
    provider: deepseek-official
    model: deepseek-v4-flash

注释还会告诉你这一行是谁贴的、被谁改过:

# == @deepseek-ai/dsh-base, patched by @deepseek-ai/dsh-web-app
- id: hmr
  name: '@deepseek-ai/cordis-plugin-hmr'
  disabled: true

dsh-base 铺了 hmr 这一行,dsh-web-app 把它 patch 成 disabled: true。配置的每一层来源都是可追溯的,这一点做得确实干净——你想改任何一行,往自己的 cordis.patch.yml 里写一条同 id 的覆盖就行。

顺带记两个数:主进程稳定后 RSS 137 MB;~/.dsh 目录本身只有 32 KB(依赖全在 pnpm store 里,profile 目录下是符号链接,不占额外空间——这点比我预期的克制)。

四种模式,第四种是让它改自己

装完之后 config/agent-presets/ 下躺着四个预设,中文名是官方自己写的:

preset名字配置行数干什么
minimal极简模式62只有持久 bash + str_replace_editor 两个工具
standard标准模式251完整编码 agent:文件、Shell、检索、Skills、计划、子代理、工作流
codePTC 模式262标准模式 + Code Mode SDK,让模型写一个 TypeScript 程序编排多步操作
cordis创造模式262标准模式 + 运行时检查 + 插件实验 + preset 创作指导

前三个是常规操作。第四个我想多说两句。

cordis 这个 preset 目录里带了两份 SKILL.md,一份 154 行,一份 420 行。420 行那份叫 cordis-plugin-development,它教 agent 的事情是这个:

1. cordis_inspect_list   —— 看当前运行时注册了哪些服务、方法、schema
2. cordis_inspect_query  —— 精确查它要用到的 Service / Event / Slot / 主题 token
3. cordis_inspect_self   —— 读某个插件现在的源码和运行诊断
4. 在 code.host / code.client 里写 JavaScript,调 cordis_define
5. cordis_run            —— 把这个 package 激活
6. cordis_stop           —— 临时停掉
7. cordis_undefine       —— 永久删除

也就是模型自己写一份插件代码,塞进正在运行的这个进程里,装上、跑起来、坏了再滚回去。不重启,不发版本,不退出会话。

skill 里有一句写得很直白:

Both code.host and code.client are plain JavaScript function bodies that return a Cordis Plugin. They are not compiled by TypeScript, JSX, or a bundler.

不过 TS、不过 bundler,模型写的 JavaScript 函数体直接是插件本体。

它的版本模型也设计过:每次 cordis_define 追加一个不可变的 package,不改旧的;run 指向哪个 packageId,当前跑的就是哪一版;回滚就是 run 回旧的那个 id。这套 append-only 加版本指针和它的会话日志是同一个思路。

append-only 版本链:指针指向当前版本,回滚就是把指针指回去

授权这一层有个细节值得单独拎出来:

A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin.

单勾只授权眼下这一个 package。双勾授权这个插件以后的所有版本。

你点第二个勾的那一刻,是把"这个插件将来自己改自己的每一版都不用再问我"这件事永久批掉了。一个只需要点一下、语义极重的开关。

还有两处诚实的注脚,都在官方 skill 里:

A failed update does not automatically restore the old physical Run; explicitly run current when recovery is required.

更新失败不会自动恢复旧的运行,得你(或者模型)显式地 run 回去。这给"可逆"打了个折扣:框架保证效果被撤销,不保证服务自动回到可用状态。

另一份 154 行的 skill 里还有一句更有画面感的:

Never edit, delete, or overwrite a preset that ships with the deployment... corrupting cordis disables preset authoring itself.

别改自带的预设——把 cordis 这个预设搞坏了,你就再也没法创作预设了。自进化系统怎么自救,官方用一条禁令绕过去了。

到这儿我对这套架构的评价还是正面的。

装一个插件,然后我看到了那条警告

dsh 自己的插件管理是这么设计的:

dsh plugin --profile web add <package>

我先试了 dsh plugin --profile web --help,想看看有哪些子命令。打出来的是这个:

Version 10.32.1
Usage: pnpm [command] [flags]
...
Manage your dependencies:
      add                  Installs a package and any packages that it depends on...

pnpm 自己的帮助。dsh plugin 是一层薄薄的转发器,--help 也一起转过去了。你问 dsh 插件怎么用,它给你 pnpm 的手册。

然后我装了个官方的工具包试流程:

dsh plugin --profile web add @deepseek-ai/dsh-tool-web

1.7 秒,7 个包。最后一行是这个警告:

dsh: warning: @deepseek-ai/dsh-tool-web declares no dsh.bundle — installed as
a plain dependency, not a profile layer (a later update that gains one
activates it automatically)

前半句没问题:这个包没声明 dsh.bundle,所以只当普通依赖装着,不进插件树。

关键在括号里那半句:a later update that gains one activates it automatically.

一个包今天装进来是惰性的,什么都不做。它的作者明天发一个新版本,在 package.json 里加上 dsh.bundle 字段,它就自动变成一个挂载在你 harness 进程里的插件层。不需要你重新 add,不需要你确认,你的 package.json 里那行依赖不用改一个字符。

惰性依赖在一次版本更新后自动跨进边界,成为挂载中的插件

这条路径不是我推测出来的,是官方的警告文案自己讲的。

于是我去数了一下这条路上有什么。装完之后 ~/.dsh/profiles/ 下:

包总数547
带安装期生命周期脚本的包68

68 个包在 install / postinstall / preinstall / prepare 里挂着命令。比如 node-pty:

"install": "node scripts/prebuild.js || node-gyp rebuild",
"postinstall": "node scripts/post-install.js"

这些脚本在 pnpm install 的时候就在你机器上跑了,跟 dsh 有没有启动无关。

把这几件事放在一起看。装插件就是 pnpm install,没有权限清单,manifest 里没有能力声明,没有沙箱。插件代码在 harness 主进程里跑,跟 dsh-tool-bash、跟你的凭据、跟你的会话日志同一个进程。一个惰性依赖可以在某次版本更新之后自动升级成挂载中的插件。依赖树里还有 68 个包带安装期脚本。

架构把可替换性做到了头,代价是信任边界也一起被拆掉。Cordis 那套可逆副作用保证的是卸载时不留垃圾,它从来没打算保证装进来的东西不干坏事。这两个是不同的问题,很容易被"可逆"这个词混过去。

社区已经在替官方补这一层了

我去 npm 上按关键字数了一下:dsh-plugin 这个 keyword 下 1098 个包。GitHub 上 dsh-plugin 这个 topic 下 5964 个仓库。

翻这个列表的时候,我看到一个包的描述停住了:

dsh-plugin-vetting:装插件前,先体检:第三方插件 = 进程内全权限代码,这个工具让"盲装"变成"知情安装"。

我把它的 tarball 解出来读了 README(只解包,没装、没跑)。它的"威胁模型"章节写得比官方文档清楚:

DSH 插件在 harness 进程内执行,拥有完整权限。因此本插件不是安全边界——它是启发式绊线(类似杀毒软件):静态扫描插件源码,命中可疑模式就报告,从不执行插件代码,不拦截(拦截会误伤正常插件)。

它做的事:15 条恶意规则(网络外传、凭据访问、eval、持久化、读会话日志、生命周期脚本)、3 条误伤规则、传递依赖统计、官方包内容哈希基线(防的正是"信任名字"被投毒)、以及一个默认关闭的工具门禁。

然后是这句:

真正的根治方案应由 harness 提供"挂载前扫描钩子"——本插件是该思路的独立原型。

一个社区开发者,在官方开源四天之内,写了一个插件来扫别的插件,并且在 README 里说:这事本来该 harness 干。

这里有一个绕不过去的圈。dsh-plugin-vetting 自己也是一个插件,你要用它来审查第三方插件,得先 pnpm install 它——一份进程内全权限代码。它自己的 README 里还专门留了个 allowlist 配置项,理由是"安全插件规则里引用密钥路径,启发式必然高分",也就是说它扫自己都会报警。

你需要信任一个插件,来帮你判断该不该信任插件。这不是这位作者的设计缺陷,当挂载层没有锁的时候,任何补救方案都只能长成这个形状。

和 Claude Code 摆在一起看

我上一篇正好写了 Claude Code 的 auto mode,那边的整个工程投入是往相反方向去的:

Claude Codedsh
每个工具调用过一遍分类器(hard_deny / soft_deny / allow / 用户意图四级)由插件自己实现的 tools/pre-execute 决定
扩展代码的位置大部分能力在核心里,扩展受权限模型约束插件在主进程内,全权限
装第三方扩展有明确的信任决策界面pnpm install
不可逆动作permissions.deny 在分类器之前硬拦靠插件自觉 / 社区插件启发式扫描
出厂默认8 月 14 日起 auto mode 默认开无

Claude Code 花大力气量化"人类审核只拦住 13.6%",然后用分类器把每个工具调用重新审一遍。dsh 把架构做到连 agent loop 都能换掉,然后让你用 pnpm 装进程内代码。

不矛盾,它们在解不同的题。Claude Code 那道题是 agent 在你机器上干活怎么不出事,dsh 那道题是 agent 的运行时怎么在不重启的前提下改自己。

门槛清单(照实说)

想上手,先过这几道:

  • Node ^22.19.0 || >=24.0.0。我的 22.21.1 刚好够。
  • pnpm。插件管理就是 pnpm,你得会读它的报错。我这台 10.32.1,仓库自己钉的是 [email protected]。
  • 磁盘:一份 343 MB 起。每个 profile 一套 workspace(好在走 pnpm store 链接,增量不大)。
  • 内存:主进程稳定 137 MB。
  • 版本:0.1.0-rc.6,README 原话 "THERE WILL BE COMPATIBILITY-BREAKING CHANGES"。三天一个 rc。
  • 反馈渠道:issue 关着。有问题你只能去 fork、去社区仓库、去 npm 包作者那儿。

我的判据

如果你的目的是学"一个运行时怎么改自己而不重启",dsh 是今天最值得读的一份实现。去读 cordis preset 那 420 行 SKILL.md,比读论文快。

要让它替你干活就是另一回事了。在挂载前扫描钩子进官方之前,把它当成一个你愿意给 root 的进程:独立环境、假凭据、只装官方包。

对我自己,结论是现在不迁。我需要的那个能力(让助手自己长出新技能、不重启就生效)dsh 确实给了,而且给得比任何现成产品都彻底。但我这台机器上有 App Store 的证书、有几个 SaaS 的生产凭据。让一个"插件即进程内全权限代码"的运行时跑在这台机器上,换来的自进化能力配不上这个风险。

我在等的信号只有一个:dsh 什么时候提供挂载前的权限声明和扫描钩子。