DeepSeek Harness 的扩展体系:packages/extensions 里到底装了什么

2026-08-17

翻 deepseek-harness 这个仓库时,packages/extensions/ 是最容易被误读的一块。看名字像是「放第三方插件的地方」,实际不是。这一组包解决的是另一个问题:模型在对话中间自己写一段插件代码,要不要、以及怎么装进它此刻正在运行的那个进程里。

先把一个数数的坑说在前面。这个仓库的 packages/ 下没有任何一个直接的 package.json——我们用 ls -d packages/*/package.json 数出来是 0 个,而 packages/*/*/package.json 数出来是 219 个。也就是说 packages/ 下面那一层全是分组目录,真正的包在第二层。extensions 就是这样一个分组,它下面是 4 个包:tool-cordis/cordis-host-runner/cordis-client-runner/ui-cordis/。谁要是按第一层目录数报「这个仓库有 40 多个包」,那是把组当成包了。

一次 define 是怎么走完的

顺着一条具体路径走。模型调 cordis_define,这个工具注册在 packages/extensions/tool-cordis/src/index.ts 里;它自己不做任何事,转手交给 packages/extensions/cordis-host-runner/src/index.ts 里的 DynamicCordisRunnerService.define()

define() 干的第一件事是拒绝:namepurposetrim(),空的直接抛;code.hostcode.client 一个都没给也抛(错误文案原文是 cordis_define needs \code.host`, `code.client`, or both);两半代码都要先过 precheckCode 做语法预检——只编译,不执行。README(packages/extensions/cordis-host-runner/README.md`)对这一步的自述是:因为 define 阶段没有任何副作用可回滚,所以不可解析的代码必须在 id 被铸出来之前就被挡掉。

接着是铸 id。这里有个具体到能背下来的约束:新建 Plugin 时模型要自己提一个语义前缀,源码里的校验是 /^[a-z]{3,6}$/.test(prefix),不合规就抛 cordis_define \plugin.idPrefix` must contain 3–6 lowercase English letters。前缀过关后由 packages/extensions/cordis-host-runner/src/registry.tsmintPluginId(prefix)拼上一个进程内自增号,得到-。同一个文件里还有另外三个 mint 方法,格式是写死的:pkg-run-approval-`,分别对应不可变的 Package 版本、一次具体的激活、一次待批准的请求。

这里就是本篇第一处「文档对不上源码」的地方。cordis-host-runnertool-cordis 两个 README 都写 define 会铸出 dyn-<n> 形式的 id,而我们实读 registry.tsmintPluginId,拼出来的是模型提交的那个 3–6 位前缀加自增号,代码里没有 dyn 这个常量。两处不一致,以源码为准。

工具是五个还是七个

第二处不一致更直接。packages/extensions/tool-cordis/README.md 开篇写的是「五个面向模型的工具」,并把只读那个叫 cordis_inspect。我们在 src/index.ts 里数 ctx.tools.register(defineTool({ 的调用,是 7 次,工具名依次为:cordis_inspect_listcordis_inspect_querycordis_inspect_selfcordis_definecordis_runcordis_stopcordis_undefine。源码里根本没有叫 cordis_inspect 的工具,只读那一路被拆成了三个。

这三个的边界在工具描述原文里划得很清楚,值得单说:cordis_inspect_list 列出 Host 已知的全部 Inspect Provider(含从 Client 同步来的 manifest),cordis_inspect_query 才是真正发查询,且 platformprovidermethod 三个参数必须来自前一个工具的返回,描述里明写「不要猜名字」;cordis_inspect_self 则是看当前 Session 自己定义的那些动态 Plugin,参数上有个硬规矩——packageId 不能单独给,必须配 pluginId

apply() 里除了注册工具,还挂了一节系统提示:ctx.systemPrompt.section({ name: 'tool:cordis', order: 115, ... })。这一节在合成 prompt 里的位置是写死的 115。

vmTimeoutMs 5000 到底管住了什么

Host 半边跑在 node:vm 里。配置只有一个字段,源码在 cordis-host-runner/src/index.tsstatic Config

vmTimeoutMs: z.number().min(1).default(5000),

默认 5000 毫秒。但它的语义比字面窄得多packages/extensions/cordis-host-runner/src/sandbox.tsevaluateHostCode 的 JSDoc 自己写明了这一点,README 的「Known Limitations」也复述了一遍:这个值只约束宿主半边的同步求值部分,一个 async 的 host-half 函数体会直接逃出这个界。所以别把它当成「动态包最多跑 5 秒」的保护——它管的是求值那一瞬。

同一节里还有个更要紧的口径:README 的 Config 表下面一句是「一个字段就是全部」,理由是自述的——运行请求要等人,所以这趟往返没有自己的截止时间。但 tool-cordis/README.md 第 27 行又写道 vm 求值边界 vmTimeoutMs 与「浏览器确认窗口 ackTimeoutMs」都属于 runner 服务。我们在整个仓库 grep ackTimeoutMs,命中的只有 tool-cordis README 的中英两个语言版本,.ts 源码里一处都没有。两处不一致,我们只陈述到这里。

顺带第三处:tool-cordis/README.md 把 runner 的服务键写作 ctx.dynamic,而 packages/extensions/README.md 的表格、docs/subsystems/extensions.md 的 API 章节、以及 DynamicCordisRunnerService 构造函数里 super(ctx, 'dynamicCordisRunner') 这一行,写的都是 dynamicCordisRunner

沙箱不是安全边界——这话是它自己说的

这一点必须原样转述,不能润色。sandbox.ts 的模块 JSDoc 里写的是:这层隔离让协作性的包保持可检查、可释放,但不是 containment,host realm 的辅助函数仍然是一条逃逸路径。tool-cordiscordis-host-runner 两个 README 也各写了一遍同样意思的「不是安全边界」,并且都给了同一句处置建议:把加载一个动态包当成给出 bash 权限那样慎重。

具体到沙箱里有什么,sandbox.ts 的做法是把被禁掉的 Node API 换成会抛异常的「教学式陷阱」,NODE_API_REDIRECTS 表里列了 7 个:requiresetTimeoutsetIntervalsetImmediateclearTimeoutclearIntervalfetch。抛出的文案不是干巴巴的报错,而是直接指路——文件走 ctx.fs、网络走 ctx.web、进程走 ctx.bash、定时器走 Cordis 的 timer 服务。

这里有个反直觉的细节,注释里专门解释了:只有函数值的全局才做陷阱,像 process 这种数据值的全局就让它保持 undefined。原因是自述的——如果给它装一个会抛的访问器,那么最常见的 typeof process 特性探测会在取值那一刻就炸掉。

沙箱里被留下的东西也有明确清单,HOST_BUILTIN_INSPECTION 常量把它们连签名一起列了出来:受限的 ctxget/on/provide/effect)、harnesshandle/defineTool/registerTool)、带包 id 标签的 console,以及 btoa/atob/TextEncoder/TextDecoder 这几个裸 vm 上下文本来没有的编码原语。

浏览器那一半,参数表就是它的全部世界

带 UI 的动态包还有个浏览器半边,由 packages/extensions/cordis-client-runner/ 负责。它的求值方式在 src/client/evaluator.ts 里,直白到有点粗暴:把源码塞进 new Function(...parameters, ...) 包出来的 async IIFE,而 parameters 这个数组本身就是这段代码能看见的全部符号——Reactconsolestyleshostharness,加上那批陷阱名,再加 processBuffer。README 把这条边界写得很硬:没有 JSX,没有 TypeScript,没有模块导入;React 元素只能用 React.createElement 造,require 的陷阱文案里也是这么写的。

harness 这个座位在浏览器侧只是个陷阱:碰它会抛出一句解释宿主/浏览器分工的话,真正的宿主方法要通过 host.call(method, args) 走 RPC,对面是宿主半边用 harness.handle(method, fn) 注册的处理器。两个方向都只走 JSON,README 里还点了一个易踩的点——省略参数会以 null 送达,所以 host.call('listServices') 是合法的,处理器收到的是 null

阻塞不阻塞,这里也有两种说法

cordis-host-runner/README.md 描述 run 时说:带浏览器半边的包必须由某个页面来执行,于是 run 变成一次可应答的往返——发出 cordis/request-run 事件后挂起,由人批准或拒绝来收束;没有定时器,唯一的另一条出路是调用方的 AbortSignal(提问的那一轮被取消了)。ui-cordis/README.md 也是按「模型的 cordis_run 在宿主侧阻塞」来解释它为什么把审批面板做成 frame 级的 shell.overlay 座位——因为要批准的那个包可能属于一个当下没人在看的 Session。

tool-cordis/src/index.tscordis_run 的工具描述原文写的是:未授权的 Client Package 会创建审批请求并返回 awaiting-approval,已授权的返回 starting 并在浏览器侧异步继续,两种结果都不在工具内部等待最终结局;异步的成功、拒绝或技术失败通过 state 与 steering 回报。两处描述不一致,我们只指出这一点。

不管按哪种读法,有一条边界是两边都写明的、并且直接影响你能不能用它:带浏览器半边的动态包,在没有页面连接的部署里会一直挂着。README 的「Known Limitations」原文说 headless 与 ACP 部署会把这次 run 一直持有到提问的那一轮被取消,因为转发事件不会告诉宿主谁收到了它;而且这个挂起的请求没有超时。结论也是它自己下的:无人值守的自动化用不了带浏览器半边的包。纯宿主半边的包不受此影响。

这些东西一次都不落盘

最后一条容易被忽略、但会直接改变你对它的预期:cordis-host-runner/README.md 的「Storage stance」写明注册表就是进程内存,且是唯一的真相来源;没有任何东西写到磁盘,进程重启后合法地一个定义都不剩。Session 日志里只留 define 调用的元数据,不留代码。tool-cordis/README.md 把后果说得更完整:动态包不会生成 Plugin 文件、不安装任何包、不改 cordis.yml 或个人/项目配置、重启不存活、也不能自动升格;想留住一次实验,得按常规开发流程去实现一个正经的本地/项目/仓库 Plugin。

所以把 packages/extensions/ 理解成「插件市场」是彻底反了。它是一次性的、进程内的、会话作用域的自我改造能力tool-cordis 出工具面,cordis-host-runner 出注册表与 node:vmcordis-client-runner 出浏览器闭包与 guard,ui-cordis 出那个 frame 级的操作座位。上面这些命令名、字段名和默认值都能在文中给出的相对路径里逐行找到;同时也要记住,该项目 README 自述处于开发者预览阶段、并明确说明未来会有破坏兼容性的变更,vmTimeoutMs 的默认值、idPrefix 的正则、工具名的拆法都随时可能变,看仓库最新内容为准。


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。