Branch · Step 15
分岔点:能力缝与注入
主干 B 讲完了「工具如何执行」,但没讲「工具从哪来」。本页是旁支的入口:先看一个 capability seam 的三件套在代码里长什么样,再看 shell 这个 seam 的四个参与者如何通过 super(ctx,…) + static inject 连接。之后 16/17 两页分别深入 Provider 与 Consumer 的代码。
示例本次示例:一次 bash 调用跨过两个 seam 的完整路径
模型调用 bash("ls -la")
→ 14 页流水线 → tool-bash 的 execute(17 页 329 行)
→ ctx.shellEnv.collect(exec) ← DSH_* 快照
→ ctx.shell.resolve(request) ← ShellExecutor 抽象(15 页 84 行)
→ LocalBashExecutor(16 页):默认值/上限/env 合并 → ShellExecSpec
→ ctx.shell.run(spec)(17 页 379 行)
→ LocalBashExecutor.runArgv(16 页 225 行):
deadline(signal, timeoutMs, 'BASH_TIMEOUT')
ctx.subprocess.spawn(spawnSpec(spec, ['bash','-c','ls -la'], ...))
→ LocalSubprocessRuntime(16 页 §4):detached 进程树 + 输出采集
→ 结果上行:ShellRunResult → renderResult(退出码 marker)→ 14 页 post → tool/result
# 把 bash-local 换成 bash-sandbox(cordis.patch.yml): # - id: bash-sandbox # name: '@deepseek-ai/dsh-bash-sandbox' # 挂载后:ctx.shell 解析到沙箱化 executor,tool-bash 一行不改 # (17 页的 consumer 只依赖 'shell' 服务,不依赖具体实现) # 沙箱 executor 的 sandboxMode 非 undefined → tool-bash 启用升级审批(17 页 191 行)
§1三件套在代码里的形态
一个 capability seam 是「Service Definition(接口)/ Service Provider(实现)/ Consumer(工具)」三件套。在 dsh 代码里它们长这样(以 shell 为例,与 10 页的 LlmRuntime 同构):
64export abstract class ShellExecutor extends Service {
65 constructor(ctx: Context) {
66 super(ctx, 'shell') // ← ctx.shell 由此而来
67 }
74 get sandboxMode(): SandboxMode | undefined { return undefined }
84 abstract resolve(request: ShellExecRequest): ShellExecSpec
93 abstract execute(spec: ShellExecSpec): Promise<ShellExecution>0.1.7:run+start 合并
Service Definition 的注册形态:extends Service + super(ctx, 'shell')。declare module 里的 Context.shell 是类型层,这里才是运行时注册点。
sandboxMode 是可选能力声明:默认实现返回 undefined(不沙箱)。沙箱化 provider(bash-sandbox)override 它——tool-bash 靠它判断是否需要升级审批。
resolve 拆分是 seam 的核心心智模型:Request(可选字段)→ resolve → Spec(全字段)。Consumer 永远先 resolve 再 execute,绝不直接传 request——默认值归属实现方(16 页看具体落点)。
0.1.7 破坏性变更:run + start 合并为 execute。旧版是两个方法——run(spec): Promise<ShellRunResult>(前台,等结果)与 start(spec): Promise<ShellProcess>(后台,拿句柄)。现在只有一个 execute(spec): Promise<ShellExecution>:前景还是背景,由调用方 await 什么决定——await 整个 ShellExecution 拿句柄,await 它的 .result 才是等结果。
方法文档点明了 onExpiry 的两个取值语义:'none' 不设截止时间;'kill' 到期杀进程。准备期间超时会返回一个「已结算的 timed-out 句柄、无输出」——而不是抛错。@throws 只留给「准备失败或发布前被取消」。
§2shell seam 的四个参与者
连接方式的三个代码形态:
- Provider 侧拿下层:
static inject = ['subprocess'](bash-local)→ 构造器里ctx.subprocess可用(16 页)。 - Provider 侧注册自己:
super(ctx, 'shell')——同一个 seam 只能挂一个 provider,挂两个(如 bash-local + pwsh-local 同时)会因重复 service 注册失败。 - Consumer 侧拿能力:
inject = ['tools','shell','systemPrompt','shellEnv'](tool-bash,17 页)——tools 用来注册工具,shell 是能力本体,systemPrompt 是工具 schema 进提示词的通道,shellEnv 是环境事实。
§3Service 型 vs 事件 gate 型
读完本页,你拿到了分辨任意 seam 类型的罗盘——看 provider 与 policy 之间是靠 ctx.get() 还是靠事件名:
| Service 型 seam | 事件 gate 型 seam | |
|---|---|---|
| 代表 | shell / subprocess / llm | fs |
| 连接方式 | Cordis Service 注入(super + inject) | 事件 gate:provider 发 fs/write-intent 等,policy 监听并注入检查 |
| invariant | 各包 invariant.ts 声明 No runtime invariant / 无独立事件序列 | 事件 gate 即运行时关系校验点 |
| 判定口诀 | provider 与 policy 之间靠 ctx.get() 连接 = 注入型;靠事件名连接 = 拦截型。 | |
16 页(Provider):从 LocalBashExecutor 的 resolve/run/start 出发,追踪 ['bash','-c',command] 如何进 subprocess、env 四段合并、超时分类。
17 页(Consumer):从 tool-bash 的 defineTool('bash') 出发,看 execute 的三个路径(前台/后台/沙箱升级)与 inject 数组的每个成员。