Branch · Step 16

bash-local:Provider 的职责清单

packages/shell/bash-local/src/index.ts(404 行)。LocalBashExecutor 是 shell seam 的本地 Provider:默认值与上限、超时分类、环境合并、把 command 包成 argv 交给下层 subprocess。它是「resolve 拆分」的完整范本。

15 seam 定义:ShellExecutor 抽象类 → bash-local/src/index.ts LocalBashExecutor(inject=['subprocess']) → (下层) ctx.subprocess.spawn → subprocess-local 进程树

示例本次示例:ls -la 的 env 四段合并

示例轨迹 16-1 · 子进程最终看到的环境变量
# bash-local spawnSpec(172 行):env: { ...ENV_OVERRIDES, ...spec.env, ...spec.dshEnv }
# ① scrubbedParentEnv(subprocess-local):
#    宿主 env 去掉 DEEPSEEK_API_KEY 等凭证 + 去掉 DSH_* → 基线
# ② ENV_OVERRIDES(bash-local 27-32 行):
#    NO_COLOR=1, TERM=dumb, PAGER=cat, GIT_PAGER=cat
#    → ls 的输出没有 ANSI 颜色,分页器不会挂起
# ③ spec.env:本次调用方没传 → 无
# ④ spec.dshEnv(来自 17 页 shellEnv.collect):
#    DSH_SESSION_ID=..., DSH_WORKSPACE=... 等 harness 事实
#    → 最后合并,普通 env 无法覆盖
# 结果:ls -la 跑在「干净基线 + 工具友好覆盖 + harness 事实」的环境里
来源:16 页 §3 的四段表 + bash-local/src/index.ts:27-32, 194-196(真实行号)
示例轨迹 16-2 · 超时分类:谁算 timedOut
# spec.timeoutMs=120000(bash-local 默认)
# 120 秒到 → deadline 的 BASH_TIMEOUT 触发
# bash-local:219:timeoutOf(d.signal,'BASH_TIMEOUT') → timedOut=true
# 用户在第 30 秒取消 → d.signal.aborted 但非 BASH_TIMEOUT
# bash-local:220:aborted=true(外层 deadline 算 abort,不算 timedOut)
# 两个 cause 分类清楚 → 14 页 post-execute 与 11 页的结果呈现可区分
来源:16 页 201-389 行(executeArgv,真实行号)

§1resolve:Request → Spec

15 页的接口里 resolve 是抽象的——bash-local 的实现把它变成「默认值 + 钳制」的确定性过程。它的产出物 spawnSpec(151-174 行)里最典型的一行是环境合并(bash-local:172):

packages/shell/bash-local/src/index.tsspawnSpec 里的环境合并151-174
157    const collect = (maxBytes: number): SubprocessCollect =>
159    return {
160      argv,
161      cwd: spec.workdir,
162      stdio: { ... },
167      graceMs: this.config.graceMs.get(),   // 0.1.7:volatile schema 访问器配置来源变了
168      signal,
169      // One explicit env map for the seam, layered so the trusted dshEnv
170      // snapshot beats both the caller's env and the terminal overrides; the
171      // subprocess service merges the whole map after its ambient scrub.
172      env: { ...ENV_OVERRIDES, ...spec.env, ...spec.dshEnv },三层合并,dshEnv 最优先
173    }
174  }
198

四段合并的第三段(§3 有全表)。spec.env(调用方)覆盖 ENV_OVERRIDES,spec.dshEnv(DSH_* 快照)最后合并——普通 env 无法覆盖 harness 事实。

169-171

注释说明剩余一段在哪:subprocess 服务在它的 ambient scrub(去凭证)之后合并整张 map。Provider 只负责它自己这一层。

§2execute:超时分类与 deadline

packages/shell/bash-local/src/index.tsexecute → executeArgv187-189, 201-231
187  async execute(spec: ShellExecSpec): Promise<ShellExecution> {   // 0.1.7:取代 run + start
188    return this.executeArgv(spec, ['bash', '-c', spec.command])
189  }
201  protected async executeArgv(0.1.7:runArgv 改名
202    spec: ShellExecSpec,
203    argvOrPrepare: readonly string[] | ((signal: AbortSignal) => Promise<readonly string[]>),
204    onStarted?: (process: ShellExecution) => void,   // 0.1.7 新增钩子
205  ): Promise<ShellExecution> {
211-230    // 按 onExpiry 分两臂:每臂给出 spawn 信号、首因分类闭包、以及结算时的 disarm0.1.7:分类逻辑归入闭包
213    if (spec.onExpiry === 'kill') {
216      const d = deadline(spec.signal, spec.timeoutMs, 'BASH_TIMEOUT')
218      classify = () => {
219        const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined
220        return { timedOut, aborted: d.signal.aborted && !timedOut }
221      }
222      disarm = () => { d[Symbol.dispose]() }
223    } else {   // onExpiry: 'none' —— 无截止时间
225      spawnSignal = spec.signal
226      classify = () => ({ timedOut: false, aborted: spec.signal?.aborted === true })
227    }
229    let argv: readonly string[] = []
230    let preparationTimedOut = false   // 准备阶段也能超时
231    if (typeof argvOrPrepare === 'function') { ... }
213-215

argv 从不由 subprocess 解释:bash-local 把 command 包成 ['bash','-c',command](pwsh-local 对应 ['pwsh','-NoProfile','-Command',...]);subprocess 只认 argv[0] 为程序。shell 语法归 provider,进程生成归 subprocess。

227

deadline(spec.signal, spec.timeoutMs, 'BASH_TIMEOUT'):一个 deadline 融合两个取消源(上游取消信号 + 自身超时)。using 保证 deadline 的定时器随函数结束释放。

228

spawn 下层进程。这里发生 seam 的分层:bash-local 只关心「bash 语义」,进程树管理全在下层。

232-234

cause 分类归 bash-local:只有 executor 自己的 BASH_TIMEOUT 算 timedOut,外层 deadline(上游取消)算 aborted。subprocess seam 对取消原因无感——它只对 AbortSignal 反应。这就是 15 页说的「默认值与分类归实现方」。

§3env 四段合并顺序

完整顺序(subprocess-local 的 childEnv + bash-local 的覆盖):

顺序来源说明
①scrubbedParentEnv去凭证 + 去 DSH_* 的环境基线(scrub 在 subprocess-local,唯一实现)
②ENV_OVERRIDESNO_COLOR=1 · TERM=dumb · PAGER=cat · GIT_PAGER=cat(工具友好输出;bash-local/src/index.ts:27-32)
③spec.env调用方环境
④spec.dshEnvDSH_* 快照(来自 ctx.shellEnv 注册表,17 页)最后合并、无法被普通 env 覆盖

为什么 DSH_* 必须最后?它携带 harness 的事实(会话 id、执行世界标识),普通 env 不应有权遮蔽——这是「能力事实随执行对象流动」的载体。

§4下层:subprocess-local 的职责

packages/subprocess/subprocess-local/src/index.ts(195 行)是下层 seam 的 Provider:

  • 存活管理:用 ctx.effect 挂 process exit 监听(host 退出时对存活进程树/终端同步强杀),live handle 存 Set 供 disposal 终止。
  • 进程管线(spawn.ts,543 行):spawnSubprocess 组装 detached 进程树、OutputCollector 实现 tail-keep + spill 文件、树级 SIGTERM→graceMs→SIGKILL 升级与 waitForExit——观察整棵树,TERM-trapping 的后代不会逃逸。
  • 输出契约:CollectedOutput.text 截断时是「尾部」(tail-keep),spillPath 才是全量。

分层存活边界(重要):bash-local 依赖 ctx.subprocess,后台进程的 teardown 边界是 subprocess disposal——因此 executor-only 重载后后台进程仍然存活(shell/src/index.ts 的类注释明确写了这一点)。换 provider 只影响新命令,不追杀旧进程。