Branch · Step 16
bash-local:Provider 的职责清单
packages/shell/bash-local/src/index.ts(404 行)。LocalBashExecutor 是 shell seam 的本地 Provider:默认值与上限、超时分类、环境合并、把 command 包成 argv 交给下层 subprocess。它是「resolve 拆分」的完整范本。
示例本次示例:ls -la 的 env 四段合并
# 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 事实」的环境里
# 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 页的结果呈现可区分
§1resolve:Request → Spec
15 页的接口里 resolve 是抽象的——bash-local 的实现把它变成「默认值 + 钳制」的确定性过程。它的产出物 spawnSpec(151-174 行)里最典型的一行是环境合并(bash-local:172):
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 }
四段合并的第三段(§3 有全表)。spec.env(调用方)覆盖 ENV_OVERRIDES,spec.dshEnv(DSH_* 快照)最后合并——普通 env 无法覆盖 harness 事实。
注释说明剩余一段在哪:subprocess 服务在它的 ambient scrub(去凭证)之后合并整张 map。Provider 只负责它自己这一层。
§2execute:超时分类与 deadline
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') { ... }
argv 从不由 subprocess 解释:bash-local 把 command 包成 ['bash','-c',command](pwsh-local 对应 ['pwsh','-NoProfile','-Command',...]);subprocess 只认 argv[0] 为程序。shell 语法归 provider,进程生成归 subprocess。
deadline(spec.signal, spec.timeoutMs, 'BASH_TIMEOUT'):一个 deadline 融合两个取消源(上游取消信号 + 自身超时)。using 保证 deadline 的定时器随函数结束释放。
spawn 下层进程。这里发生 seam 的分层:bash-local 只关心「bash 语义」,进程树管理全在下层。
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_OVERRIDES | NO_COLOR=1 · TERM=dumb · PAGER=cat · GIT_PAGER=cat(工具友好输出;bash-local/src/index.ts:27-32) |
| ③ | spec.env | 调用方环境 |
| ④ | spec.dshEnv | DSH_* 快照(来自 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 只影响新命令,不追杀旧进程。