Branch · Step 17
tool-bash:Consumer 的注入面与三条路径
packages/shell/tool-bash/src/index.ts(394 行)。这是旁支的最后一页,也是主链 14 页「工具从哪来」的最终答案:一个 394 行的插件,用 defineTool('bash') 把 shell 能力变成模型可见的工具。
示例本次示例:bash 工具的三个路径实况
# 模型调用 bash({ command:'ls -la' })
# → execute(330 行)→ ctx.shellEnv.collect(exec)(341 行)
# → ctx.shell.run(ctx.shell.resolve({...}))(380 行)
# → ShellRunResult { exitCode:0, stdout:'total 8\ndrwxr-xr-x ...', timedOut:false }
# → renderResult(render.ts)→ 模型看到的文本:
# "total 8
# drwxr-xr-x ..."
# (退出码 0 无 marker;非零会附加 [exit code: N])
# → 14 页 post-execute → tool/result 落盘(seq 9)
# 后台:模型请求 background:true
# 354 行 ctx.get('jobs') 有值 → 370 行 ctx.shell.start(resolve(request))
# → ShellProcess → background.ts 的 processOutcome → jobs 任务
# → 结果变成「任务已启动,id=...」;模型稍后用 tool-jobs 查看(25 页)
# 沙箱升级:挂载了 bash-sandbox 时(15 页示例 15-2)
# 模型传 sandbox_permissions:['network'] 请求升级
# → 223-227 行 approveEscalation → approver.request
# → approval/request waterfall → 用户批准/拒绝(29 页)
# → 批准后按升级的权限跑;拒绝则按原权限或失败
§1inject 数组:依赖面声明
30export const name = 'tool-bash'
31export const inject = ['tools', 'shell', 'systemPrompt', 'shellEnv']
inject 就是依赖面:这四个服务全部就绪,本插件才激活(03 页的 PENDING 审计靠它诊断)。逐一看:tools(注册 bash 工具的注册表)、shell(能力本体,16 页的 provider)、systemPrompt(工具 schema 进提示词的通道,12 页)、shellEnv(环境事实注册表,16 页的 dshEnv 来源)。
§2apply:注册工具与沙箱升级
190export function apply(ctx: Context, config: Config = {}): void {
192 const defaultMode = ctx.shell.sandboxMode
194 const sandboxPolicy = defaultMode === undefined
195 ? undefined
196 : ctx.get('sandboxPolicy')
200 sandboxPolicy?.resolve(exec.agent === undefined ? {} : { session: exec.agent.session })
223 return approveEscalation(
226 { approver: ctx.get('approval'), ... },
227 )
242 ctx.tools.register(defineTool({
243 name: 'bash',
330 async execute(args: BashToolArgs, exec) { ... },
插件入口是 apply——Cordis 插件协议(03 页的 Inject/Plugin)。树挂载时 Loader 调它,返回 void(注册是副作用,卸载时由 fiber 逆转)。
能力探针:读 ctx.shell.sandboxMode(15 页接口里的可选能力声明)。本地 executor 返回 undefined(不沙箱);沙箱化 executor 返回具体 mode——tool-bash 由此决定是否启用升级审批路径。
沙箱升级走审批:approveEscalation(来自 dsh-sandbox)最终调 approver.request 的 approval/request waterfall——这是 shell seam 唯一相交的 Cordis 事件(15 页图里标过)。
ctx.tools.register(defineTool({name:'bash',...}))——「工具从哪来」的最终答案:一个插件用 defineTool 定义,register 进 registry(14 页),schema 经 systemPrompt 进提示词(12 页),模型就能调它。
execute 是工具的 body——14 页流水线的 dispatchToolBody 最终调到这里。
§3execute:三条路径
330 async execute(args: BashToolArgs, exec) {
341 const dshEnv = ctx.shellEnv.collect(exec)
354 const jobs = ctx.get('jobs')
370 const proc = ctx.shell.start(ctx.shell.resolve(request))
380 const result = await ctx.shell.run(ctx.shell.resolve({ ... }))
能力事实采集:ctx.shellEnv.collect(exec) 把 DSH_* 环境快照收进 request 的 dshEnv——16 页环境合并第④段的来源。
后台路径探针:ctx.get('jobs')——jobs 服务挂载时后台命令走它(ShellProcess → jobs 任务 outcome),否则只能前台。
后台路径:ctx.shell.start(ctx.shell.resolve(request))——注意「先 resolve 再 start」的字面形态,15 页的心智模型在这里落点。
前台路径:await ctx.shell.run(ctx.shell.resolve({...}))——resolve → run 一段式。run 返回的 ShellRunResult 经 renderResult(render.ts)变成模型看到的文本 + 退出码 marker([exit code: N] / [killed by signal: X],正反解析共享 parseExitStatus)。
§4旁支结束:回望与后续
旁支走完了。回望这段树:
主链 14 页的工具结果来自 registry → registry 里的工具来自插件(15 的分岔)→ 工具 execute 调ctx.shell服务(17)→ 服务由 Provider 实现(16)→ Provider 调下层ctx.subprocess→ 进程树。
这套「Definition / Provider / Consumer + inject」的读法就是读其他能力的模板——fs、lsp、web、terminal、subagent 都同构。读新 seam 时的检查清单:
- Service Definition 在哪(
super(ctx,…)与抽象方法); - resolve 拆分有没有(Request → Spec);
- 默认值归谁(Provider 的 config);
- Provider 的 teardown 责任(
ctx.effect挂什么); - Consumer 的 inject 数组(依赖面);
- 相交事件是哪些(Service 型往往只有 approval/request;事件 gate 型看 fs/*)。
整站到这里为止。回到树状总览,或继续扩展新的旁支:持久化(session-persistence-jsonl / session-query-sqlite)、压缩(compaction + surface replace)、子代理(subagent seam)、审批权限(approval/request + permission)、沙箱(sandbox seam + fs-observation-policy)、Cordis 运行时(vendor/ Loader/Include/Context 内部)、Web 层(api/remotes BFF + ACP)。