前置:第 08、09 课 | 预计时长:45–60 分钟 | 动手环节:必须完成 | 与第 12 课可互换顺序
你将学会
- skill 系统如何让「目录 + 按需加载」成为模型可用的能力;
- extensions(动态 Cordis 包)如何让模型在运行时定义、挂载新插件;
- 这套自扩展的安全边界在哪——为什么说它是「可检视的遏制」而不是隔离。
问题引入
「一切皆插件」再往前一步:让模型在运行时自己写插件。dsh 靠两套机制做到——skill(内容注入)与 extensions(代码求值 + 插件挂载)。这一课讲清两套机制的边界,顺带精读四篇 postmortem,看这个系统怎么被现实教育。
正文
11.1 skill:注册表 + 模型侧工作流
对应 packages/skill/skill/src/index.ts 与 packages/skill/tool-skill/:
// provider 接口(skill/src/index.ts:248-260)
interface SkillProvider {
name
list(options) → SkillCandidate[] // 列目录
get(candidate, options) → SkillDefinition
}
registerProvider(create) // :391 注册是 effect,fiber 拆除即反注册 + 目录缓存失效
register(skill) // :440 任何插件可在运行时注入技能(保留名 'runtime')
模型侧工作流(tool-skill)三步:
agent/pre-step注入<available_skills>目录——内容寻址:目录摘要按 sha256 比对,变化时才发布替换(:213-251),避免每步重发全文;- 模型调用
skill工具 → 校验 modelInvocable →ctx.skills.get()→ 渲染<skill_content>; - 用户消息里的
/skill-name手势触发确定性注入——只认source.kind === 'user',外部文本无法伪造用户手势。
SkillRegistry 是 scoped 分层注册表:全局层 + 每个 agent preset 的 scope 层,近层同名遮蔽。本地磁盘 provider skill-filesystem 按根分级:项目 .dsh(100) / .agents(200) / 自定义(300) / 用户(400/500) / bundled(600),chokidar 监视 + 失效。
11.2 extensions:模型写 JS,运行即插件
四包:tool-cordis(七个模型工具)、cordis-host-runner(宿主半边)、cordis-client-runner(浏览器半边)、ui-cordis(面板)。七个工具 = 三个只读检视(cordis_inspect_list/query/self)+ 四个写操作(cordis_define/run/stop/undefine)。
// 宿主半边:node:vm 新 realm 中作为 async 函数体求值(cordis-host-runner/src/sandbox.ts:254)
evaluateHostCode(code) {
// require/fetch/setTimeout 被"教学陷阱"拦截,指向 ctx 服务(:96-108)
vm.runInNewRealm(`(async () => { ${code} })()`)
}
// 产物作为 'cordis-dynamic' group 的子 fiber 挂载,启动失败即 dispose
// 浏览器半边:闭包求值,参数即符号面(cordis-client-runner/src/client/evaluator.ts:173-180)
parameters = ['React', 'console', 'styles', 'host', 'harness', ...traps]
factory = new Function(...parameters, `return (async () => { ${clientCode} })()`)
关键约束:
cordis_define只做语法预检(不执行、不审批),cordis_run才激活;定义只存在于进程内存,重启即清空,不写仓库文件;- ctx 门面白名单:只放行
effect/on/once/provide等,拒绝框架内部(root/fiber/registry);注入服务若返回Context一律拒绝;ctx.tools.get只给 schema 视图,防止直接调用别家execute; - 会话属主隔离:插件按
sessionId/agent 隔离;预置文件头警告「Treat a session on this preset as shell access」; - 浏览器半边需人类审批:client 侧动态包返回
awaiting-approval,由面板决断,被拒后不得再次请求。
边界声明(自己读原文):“host-realm helper functions remain an escape route”——沙箱是可检视、可丢弃的遏制(containment),不是安全边界。
勘误背景:根
AGENTS.md仍列self-modification/,但磁盘上该职责由packages/extensions/承接(组 README 自称 “Agent runtime self-modification”)。读文档遇到对不上时以磁盘为准。
11.3 postmortem:四篇必须精读的事故复盘
docs/postmortem/ 收录「已触达真实用户」的事故,写作三条件:subtle(机制非显然)、systemic(逃逸原因是测试/工具/规范缺口)、costly to rediscover。
| # | 事故 | 教训 → 固化 |
|---|---|---|
| 0001 | ACP 连接即崩:export default apply 使 Loader 丢弃命名空间插件的 inject;178 个单测全绿(都手工挂载) | 命名空间插件禁加 default export;可选服务用 ctx.get;必须有走真实 Loader 的无键 e2e |
| 0002 | !!js 表达式写在条目元数据里恒真 → 文件系统工具被永久禁用;快照刷新把 UNKNOWN_TOOL 接受为新期望 | !!js 仅限 config/disabled;催生 verify-cordis-config 门禁 |
| 0003 | 模型把裸 Vite HTTP 200(白屏)当成功,验证了替身服务器 | GUI 规范 URL 必须 model-visible;验收要在确切 origin 上外部观察 |
| 0004 | landlock 良性 partial enforcement 与致命行共享前缀被误判 | 致命证据须精确行匹配且与 exit 125 联结;正交结果独立报告 |
配套 docs/defensive-patterns.md 七条防御规则(正交结果独立上报、公共契约两侧都守、异步状态不是同步状态、Dispose 必须抵达静止、派发器包含回调异常、不把环境交给不可信输出、链接形态路径用 unlink 删)——生命周期/并发/子进程/拆除工作的常备必读。
动手环节
- 解剖一个真实 skill:打开
deepseek-harness/.agents/skills/里任意一个(如dsh-pre-push-checks),找出它的名字、描述、触发约定;再确认本课程第 10 课让你写的 Agent Note 骨架与.agents/notes/README.md的制度一致。 - 精读一篇 postmortem:完整读 0001,回答:事故的机制根因是什么?为什么 178 个单测全都没拦住?最终固化了哪三层防线?
- 推演题:为什么目录注入用 sha256 比对而不是每步重发?为什么
/skill-name手势必须校验source.kind === 'user'?各举一个没有这条约束时会发生的事故场景。
自检清单
- skill 注入发生在哪个事件点?为什么它是内容寻址的?
- extensions 的定义与运行分在哪两个工具?为什么分开?
- CTX 门面白名单挡住了什么?
ctx.tools.get为什么只给 schema 视图? - 为什么说 extensions 的沙箱是遏制不是安全边界?浏览器半边多了什么约束?
常见误解
- 「skill 是微调或提示词模板」——skill 是运行时可注册、内容寻址注入的目录 + 定义,provider 可以来自磁盘、插件或运行时注入。
- 「动态包会写进仓库」——不会,只存在于进程内存,重启即清空。
- 「vm realm = 安全隔离」——不是,官方明言存在 escape route;真正的防线是门面白名单、属主隔离与人工审批。
延伸阅读
- 维基:《self-extension》
- 仓库:
docs/postmortem/README.md与 0001–0004、docs/defensive-patterns.md - 下一课:第 12 课 · Web 客户端:Slots 与模块图