乐于分享
好东西不私藏

拆解 dsh(五):AI 给自己写插件

拆解 dsh(五):AI 给自己写插件

摘要:“AI 能给自己装插件”——翻完源码发现仓库里根本没有市场。真实机制更炸:模型现场写一段插件代码,登记、审批、沙箱里跑,跑挂了错误信息自动喂回,自己修自己升级。

“AI 能给自己装插件。”

这句话听起来,像是有个插件市场,模型在里头挑挑拣拣,然后点一下安装。我翻完源码后第一反应是:仓库里根本没有市场。没有 marketplace,没有 npm install,没有能力货架。

真实机制比“装”更炸:AI 自己写一段插件代码,现场登记、审批、沙箱里跑起来,跑挂了还能自己修。不是逛市场,是现场手搓一个。

这篇是 dsh 系列第五篇,拆的就是这个“自举插件”系统。

一、没有市场:AI 手写插件的三步流程

1.1 先摸清自己家:cordis_inspect_*

模型想给自己加能力,第一步不是去外面找,而是先读自己家底。

源码里提供了一对接口:cordis_inspect_list 和 cordis_inspect_query。前者列出当前运行时里有哪些 Service、Event、Slot,后者按名字查详情。

这不像逛应用商店,更像是翻自家工具箱。模型得先知道:我现在能注入什么、能监听什么、能往哪个 Slot 插东西。

探测完,模型才会决定:我要写一个什么样的插件,需要注入哪些服务。

1.2 define:只登记,不执行

第二步是 cordis_define

这个名字容易误解成“定义并启用”。其实不是。define 只做登记,代码一次都没跑。它先把代码字符串收进来,做一次语法预检:new Script(code)(来自 node:vm)只编译不执行,语法错误直接挡在登记阶段。

更妙的是错误信息不是裸堆栈,而是会识别模型常见错误:TS 注解没去掉、括号不配、顶层 await 乱用之类。报错里带着教学提示,告诉模型怎么改。

这对应了《拆解 dsh(四):多智能体委派光谱》里说的“报错里带着出路”。不过那次是运行时错误,这里是登记阶段就把错误变成教学材料

一旦 define 成功,这个 Package 就被冻结。源码注释写得很死:Package 不可变。你不能改老版本代码,只能追加新版本。

1.3 run 与审批:单勾 / 双勾

第三步是 cordis_plugin_run,这才是真正激活。

但激活前有一道审批分叉:

  • 纯 Host 插件,只操作后端服务,直接跑
  • 含浏览器 UI 的插件,必须弹窗让用户批准

审批界面有两个勾。源码里叫单勾和双勾:

  • 单勾
    :只授权当前这个版本。
  • 双勾
    :授权当前版本,也授权这个 Package 的未来版本。

这是审批粒度的分层。用户不是信“这个 AI”,而是信“这段代码”或“这个代码系列”。双勾相当于给了一个持续授权,后续自修升级时不用再弹窗。

1.4 版本双指针:current 与 next

每个 Package 有两个版本指针:current 和 next

current 是正在运行的版本。next 是登记成功、等待激活的版本。

cordis_plugin_update 会把 next 切到 current。如果激活失败,current 保持不动,旧版本继续跑

外部引用插件时用 @pluginId。系统注入的是当前 current 指针,不是固定版本号。所以模型只要更新指针,所有引用方自动跟随。

二、场景走一遍:做一个实时倒计时小组件

光说流程抽象,我举一个源码注释里反复出现的例子:“帮我做一个实时倒计时小组件”

完整走一遍是六步:

  1. 探测
    :模型调用 cordis_inspect_list,发现有一个 timer 服务和一个 UI Slot。
  2. 写代码
    :模型生成一段插件代码,暴露一个倒计时组件,注入 timer 和 ui
  3. define
    :调用 cordis_plugin_define,语法预检通过,Package 登记为 v1。
  4. 审批
    :因为涉及浏览器 UI,弹窗让用户勾选。用户单勾通过 v1。
  5. run
    :沙箱里激活,UI 上出现倒计时。
  6. 失败自修
    :如果倒计时渲染炸了,模型收到 steer 指令,用 cordis_inspect_self 读诊断,在同一 Plugin 上修,追加 v2。如果用户之前双勾授权,v2 直接替换;单勾则再弹一次窗。

重启进程后,这个插件会消失。它不是永久安装,只活在当前进程里

这个“六步流程”本身就是 dsh 世界观的一个缩影:AI 不是用户,不是管理员,它是运行时的另一个开发者,在自己的上下文里写代码。

三、沙箱不是安全边界

3.1 node:vm + guard façade 长什么样

代码要在哪跑?源码用的是 node:vm

但 dsh 没有天真地认为 node:vm 就是安全隔离。它在上头套了一层 guard façade

传给插件的全局对象被大幅裁剪:

  • console
     是打过 tag 的,日志会进入宿主审计流。
  • 几个 harness 助手,比如编码/解码工具。
  • 其他常见全局?大部分被替换成重定向陷阱

3.2 重定向陷阱:require / setTimeout / fetch 调用即报错

这是我最喜欢的细节。

插件代码里如果写 require('fs'),不会静默失败,也不会真的加载 fs。它会立刻抛错,并告诉你该用哪个 Cordis 服务——require 被拒绝时提示改用 inject: ['fs']fetch 指向 ctx.websetTimeout 指向 Cordis 的 timer 服务。

不是封死,而是把错误变成 API 教学。模型写错一次,下次就知道该 inject 什么。

另外,插件执行有超时:vmTimeoutMs 默认 5000 毫秒。死循环会被直接掐掉。

3.3 Proxy façade:只放行 CTX_VERBS 与 inject

上下文对象 ctx 也不是原样给的。它包在一个 Proxy 里。

Proxy 的白名单只有两类:

  • CTX_VERBS
    :Cordis 上下文里的合法动词(effect/on/provide/timer 等);
  • inject
     声明过的服务:插件 manifest 里没写的服务,ctx 上根本访问不到。

两个专门的防御点:ctx.tools.get 只返回 schema 视图不给 execute 函数(防绕过工具执行管线直接调内部实现);某个服务的返回值里如果包含 Cordis Context,会被 denyContext 拒绝(防插件拿到未防护的宿主句柄层层突围)。

3.4 诚实声明:可审查、可回收,但不是隔离

但源码注释非常诚实:

这不是安全隔离,而是可审查、可回收的协作约束。

意思是:node:vm 拦不住 host realm 的闭包逃逸。如果插件真的想搞事,同进程内总有办法。所以 dsh 的信任模型不是“沙箱万能”,而是前置审批 + 运行时可审计 + 失败可回滚

这让我想起《拆解 dsh(三):guard 包,两种权力形态》里那句判断:诚实的缺席比表演性的控制值钱。这里同理:诚实的沙箱比假装的安全值钱

承认“我不可能绝对安全”,反而让设计更可信。

3.5 fiber 挂载:一次 dispose,全部回收

插件注册的所有 effect,最终都挂在一个 fiber 上。

fiber.dispose() 一调,插件注册的 service、event、slot、timer 全部回收。这个设计呼应了《拆解 DeepSeek Harness:热插拔的数学》里讲的热插拔的数学:插件不是零散的资源,而是一个有生命周期的整体,可以整体创建、整体销毁。

四、失败自修:把报错喂回模型

4.1 steerRunOutcome 给模型递条子

插件跑起来之后,成功或失败都会通过 agent.steer() 把结果写回模型上下文。

这就是《拆解 dsh(三)》(就是权限姊妹篇那篇)里说的“递条子”。宿主给模型递条子,告诉它刚才那步怎么样了。失败时的条子大意是:用 cordis_inspect_self 读诊断信息,在同一个 Plugin 上修复,用 update 追加新版本自主重试。

不是简单返回一个布尔值,而是一段带修复指引的结构化提示

4.2 四类失败,四类 steer

源码把失败分了四类,每类有专门 steer:

  • 渲染失败
    :UI 组件抛错,给组件堆栈 + 建议检查 manifest 的 slot 声明。
  • Host handler 抛错
    :后端 handler 异常,给 handler 名 + 输入参数 + 建议加日志。
  • guard 拒绝
    :触发了 Proxy 或 guard 的禁止项,给禁止原因 + 对应合法 API。
  • 运行失败
    :vm 里跑挂了,给堆栈 + 建议用 cordis_inspect_self 读运行时状态。

这四类 steer 都是机器写给机器看的,但格式很克制,信息密度高,没有废话

4.3 去重:同一错误只 steer 一次

还有个 claimRuntimeFailure 函数,按错误内容去重——同一类失败只在第一次发生时递条子,后面不再重复。

否则模型可能陷入“报错—重试—同样报错”的循环,条子反而成了噪声。

4.4 闭环:写 → 挂 → 修 → 追加版本

整个自修循环是这样的:

  1. 模型写 v1。
  2. v1 跑挂。
  3. 宿主 steer 回具体诊断。
  4. 模型读 cordis_inspect_self,定位问题。
  5. 模型在同一 Plugin 上修,生成 v2。
  6. cordis_plugin_update
     把 v2 提到 current。

如果用户之前双勾授权,v2 直接替换。单勾则再审批一次。

这就是 self-modification 的灵魂:AI 修自己写的代码,修的还是给自己用的插件

五、为什么“写”比“装”更好

5.1 审批的是代码,不是包

传统插件模型里,用户安装的是一个包。包里有什么、会不会变、能不能联网,都很难一眼看清。

dsh 里用户审批的是一段具体代码。代码就躺在那儿,用户可以审。沙箱里的行为也被 Proxy 和 guard 约束。

攻击面从“一个未知包 + 任意权限”缩小成“一段可见代码 + 声明过的 inject + 超时回收”。

5.2 不可变 + 双指针 = 审计 + 回滚

Package 不可变,意味着每次修改都留下一个版本。配合 current/next 双指针,你天然有一条审计轨迹。

出问题了?把 current 指回上一个版本就行。这种回滚不需要 git,不需要 npm,就是指针一动。

机制
带来的属性
Package 不可变
完整审计轨迹
current/next 双指针
原子切换 + 秒级回滚
单勾 / 双勾
用户控制授权粒度
fiber.dispose()
一键回收所有副作用

5.3 报错里带着出路

从 define 的语法预检,到 guard 的“请改用 inject”,再到 steer 的“请用 cordis_inspect_self 诊断”,整个系统都在把错误变成下一步指令

《拆解 dsh(四)》里说过“报错里带着出路”。这里不是一句口号,而是一套方法论:每次模型写错,系统都给出可执行的修正方向

5.4 局限也要诚实说

设计虽好,源码注释没有吹。

它列出的局限包括:

  • 进程内即失
    :重启后插件消失,没有积累,下次要重写。
  • vm 只护同步段
    :异步逃逸仍有可能。
  • 沙箱不是安全边界
    :信任模型仍是前提。

这种诚实很重要。说穿了,这不是一个“AI 想干嘛就干嘛”的系统,而是一个“AI 在人的审批和可审计约束下,给自己写临时工具”的系统。

六、结语

dsh 的“万物皆插件”在系列第一篇《拆解 DeepSeek Harness:万物皆插件》里是一个静态架构。走到第五篇,它变成了动态终局:

插件本身,也成了 AI 的工具。

AI 不再只是调用别人写好的能力。它在运行时给自己手搓能力,写砸了再修,修好了再跑,跑完了还能一键回收。

“不是逛市场,是现场手搓一个。”这就是我看到的最像“自举”的插件系统。

对了,上一篇结尾预告的 compaction + spill 后勤篇还在路上,下周补。这篇先插播一个更炸的。

你在自己的项目里,会让 AI 在运行时给自己写扩展吗?如果会,你最担心哪一环?


觉得有用?点个关注,持续看我把 dsh 源码拆完。