Branch · Step 15

分岔点:能力缝与注入

主干 B 讲完了「工具如何执行」,但没讲「工具从哪来」。本页是旁支的入口:先看一个 capability seam 的三件套在代码里长什么样,再看 shell 这个 seam 的四个参与者如何通过 super(ctx,…) + static inject 连接。之后 16/17 两页分别深入 Provider 与 Consumer 的代码。

(来自主干 14) 工具结果来自 registry 里注册的某个工具 → 分岔点:工具(Consumer)↔ 能力(Service)↔ 实现(Provider) → (旁支子页) 16 bash-local · 17 tool-bash

示例本次示例:一次 bash 调用跨过两个 seam 的完整路径

示例轨迹 15-1 · 从模型工具到进程的调用链
模型调用 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
来源:15 页全景图 + 16/17 页行级解读。整条链上没有 shell/* 事件——纯 Service 注入(15 页 §3 的判定)
示例轨迹 15-2 · 换 provider 只改一行配置
# 把 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 行)
来源:15 页三件套 + 17 页能力探针

§1三件套在代码里的形态

一个 capability seam 是「Service Definition(接口)/ Service Provider(实现)/ Consumer(工具)」三件套。在 dsh 代码里它们长这样(以 shell 为例,与 10 页的 LlmRuntime 同构):

packages/shell/shell/src/index.tsService Definition:抽象类 + super 注册64-93
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 合并
64-67

Service Definition 的注册形态:extends Service + super(ctx, 'shell')。declare module 里的 Context.shell 是类型层,这里才是运行时注册点。

74

sandboxMode 是可选能力声明:默认实现返回 undefined(不沙箱)。沙箱化 provider(bash-sandbox)override 它——tool-bash 靠它判断是否需要升级审批。

84, 93

resolve 拆分是 seam 的核心心智模型:Request(可选字段)→ resolve → Spec(全字段)。Consumer 永远先 resolve 再 execute,绝不直接传 request——默认值归属实现方(16 页看具体落点)。

93

0.1.7 破坏性变更:run + start 合并为 execute。旧版是两个方法——run(spec): Promise<ShellRunResult>(前台,等结果)与 start(spec): Promise<ShellProcess>(后台,拿句柄)。现在只有一个 execute(spec): Promise<ShellExecution>:前景还是背景,由调用方 await 什么决定——await 整个 ShellExecution 拿句柄,await 它的 .result 才是等结果。

86-92

方法文档点明了 onExpiry 的两个取值语义:'none' 不设截止时间;'kill' 到期杀进程。准备期间超时会返回一个「已结算的 timed-out 句柄、无输出」——而不是抛错。@throws 只留给「准备失败或发布前被取消」。

§2shell seam 的四个参与者

shell seam(上层)+ subprocess seam(下层):四个参与者 Consumer:tool-bash(17 页) defineTool('bash') inject = ['tools','shell','systemPrompt','shellEnv'] ctx.shell.resolve() → run()/start() Service Definition ShellExecutor · super(ctx,'shell') shell/src/index.ts extends + inject=['subprocess'] Provider:LocalBashExecutor(16 页) 默认值 · 上限 · 超时分类 · env 覆盖 bash-local/src/index.ts ctx.subprocess.spawn(['bash','-c',command]) 下层 seam:ctx.subprocess SubprocessRuntime · LocalSubprocessRuntime detached 进程树 · 输出采集 · 树级终止 ctx.shellEnv(能力事实) DSH_* 环境快照注册表 tool-bash 经 dshEnv 注入(17 页) ctx.jobs(后台路径) ShellProcess → jobs 任务 tool-bash 后台命令的归宿 关键事实:shell/subprocess 没有 shell/* 或 subprocess/* 事件——连接方式是 Service 注入 + dshEnv 数据流 唯一相交的 Cordis 事件是 approval/request:沙箱化 executor 上模型用 sandbox_permissions 升级时,tool-bash 经 approveEscalation 走审批 waterfall(17 页) 对照:fs seam 用 fs/write-intent、fs/edit-intent、fs/observed 事件 gate 让 policy 注入检查——那是「事件 gate 型 seam」(§3)
图 15-1 · shell seam 全景:两个 seam 叠放(shell 在 subprocess 之上),四个参与者。

连接方式的三个代码形态:

  • 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 / llmfs
连接方式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 数组的每个成员。