读 dsh 源码最大的一个疑问:为什么 packages/shell/ 这一个「能力域」下面有九个包,一个跑 bash 的活儿至于拆这么细吗?看到 docs/glossary.md 里的一个词才反应过来:seam(接缝)。
一条接缝 = 三个角色
词汇表里 seam 的定义很硬:
a swappable capability with three roles: a Service Definition … one or more Service Providers, and one or more Consumers.
三个角色各管一摊:Service Definition 拥有这个能力的接口(注意,是一个 Cordis Service,占住 ctx.<key>,「never a TypeScript interface」);Provider 是实现;Consumer 是使用者。根目录规约补了一句:一条接缝必须三角色齐全,「It is complete, never one role」,只有当角色要独立演化时才允许拆包。
官方钦定的样板就是 shell。把 packages/shell/ 展开看:
| 角色 | 包 | 说明 |
|---|---|---|
| Service Definition | dsh-shell | 定义执行器服务 ctx.shell,仓库里每个 shell 执行器都实现这同一份契约 |
| Provider | dsh-bash-local、dsh-bash-sandbox、dsh-pwsh-local、dsh-pwsh-sandbox | 本地 bash / 沙箱内 bash / 本地 pwsh / 沙箱内 pwsh |
| Consumer | dsh-tool-bash、dsh-tool-bash-persistent、dsh-tool-pwsh、dsh-tool-pwsh-persistent | 给模型用的 shell 工具 |
文件系统的接缝同款:dsh-fs 定义 ctx.fs,README 里直说「deliberately leaves storage mechanics to the backends」——后端有 fs-local、fs-sandbox、fs-e2b 三种,消费侧是 tool-fs、tool-fs-search、tool-str-replace-editor 这些文件工具,另有一个 fs-observation-policy 管访问策略。仓库里还有脚本生成整张接缝图谱(docs/capability-seams.md),每条接缝的 SD、实现、消费者一览无余。
这个设计带来的实际效果:换后端只动 Provider,消费者一行不碰。换掉 shell 执行器,模型端的 bash 工具毫无感知。
三角色的两条约束
接缝不是随便画条线就行,仓库里有两条明确的反模式约束:
- 接口要为所有现存消费者设计。工具 schema、UI、传输、provider 特有的行为都留在消费者或 provider 侧,不许让某一个消费者反向决定服务契约。反模式也点名了:一个公开方法只有一个内部调用方,说明该传私有闭包,不该扩公开接口。
- Provider 切换改变全局。接缝的完整含义是:一旦把 FS 和 subprocess 的 provider 指向远程沙箱,上层的 bash 工具、文件工具全部跟着迁移,不用逐个改。接缝画对了,迁移是配置问题,不是代码问题。
沙箱:三层防线
危险能力的接缝背后是沙箱,dsh 的做法是叠三层。
第一层:内核级,一段 300 行的 C
native/landlock-run/ 里有个叫 landlock-run 的工具——「self-restrict-then-exec」的 Landlock 启动器,大约 300 行 C11,直接对着内核 UAPI 写,musl 静态链接。设计原则是 fail-closed:内核没法强制执行政策时,直接退出,不跑命令。
第二层:策略抽象
packages/sandbox/ 下的 dsh-sandbox 把「同世界子进程」限制在三档文件效果策略里:read-only、workspace-write、danger-full-access。请求的模式强制不了时,调用以 SANDBOX_UNAVAILABLE 失败——同样是 fail closed,宁可拒绝不放行。具体实现有两个:sandbox-local(Linux Landlock)和 sandbox-windows-acl(Windows ACL)。
第三层:远程沙箱(POC)
packages/e2b/ 把 agent 的文件和命令操作整体搬进远程 Linux 沙箱,内含共享沙箱生命周期管理加 fs-e2b、subprocess-e2b 两个适配器。README 明确标注是实验性 POC,没有任何出厂组合默认启用它。这正是接缝威力的展示位:换个组合配置,整套文件与命令能力就搬到远端去了。
审批:工具执行前的最后一道闸
沙箱管「能碰到什么」,审批管「这次让不让」。工具执行管线本身就带策略:每个模型发起的工具调用都过一条受保护的管线——allow/deny/ask 策略、单调性守卫、around-dispatch 包装、结果检查。
人机交互的部分收在 packages/interaction/,五个包:
| 包 | 职责 |
|---|---|
| user-approval | 一次性的 allow/reject 审批,ctx.approval |
| permission-presets | 把沙箱模式 + 审批策略打包成可选的 Permissions 预设 |
| user-questions | agent 暂停、等人回答的问答接缝 |
| tool-ask-user | ask_user_question 模型工具 |
| commands | 斜杠命令 |
审批的结果只有四种:allowed-once、rejected、cancelled、unavailable。注意 allowed 是「one-time」的,一次一授权,不发长期通行证。缺应答方、应答方不归属、应答方抛异常,一律落回 unavailable——还是 fail closed。per-session 策略只有两档:ask(默认)或 never。出厂的两个预设正好对应两种姿态:workspace-write = 工作区可写 + 每次询问;danger-full-access = 全权限 + 不询问。
值得抄走的东西
三角色模式本身不新鲜,接口和实现分离谁都会说。dsh 做得扎实的地方在两点:一是接缝必须三角色齐全才算数,杜绝「只有接口没有消费者」的纸面抽象;二是每条接缝的图谱由脚本生成并纳入校验,接缝不是文档修辞,是持续被检查的结构事实。
下一篇是这个系列的收尾:一个 alpha 阶段的项目,凭什么守住这些设计——质量门禁篇。