跳到主要内容
路径文档

Shell 执行能力缝(ctx.shell)

一句话版ctx.shell 是 DSH 的 bash 能力层服务缝:它定义「运行一条前台命令、启动一个后台进程」要做什么,但把怎么实现留给下层 provider。run() 只因基础设施失败而 reject、其余情况都以描述性 ShellRunResult 求解;start() 立即返回 ShellProcess 句柄、不套用前台超时;readOutput() 增量读取、丢失标 lossy 并指向 spill 文件。子进程(怎么跑)与 bash(跑 bash 的语义)由此分居两层。

bash 工具、hooks 桥接、沙箱执行器都建立在这个能力缝上。这一篇讲清「DSH 怎么跑 bash」。

子进程 的关系:ctx.subprocess进程原语——可执行查找、受管 spawn、收集 stdio、树级终止;ctx.shellbash 能力层缝——命令默认语义、deadline、因果分类、模型友好输出叠加。bash 建在 subprocess 之上,二者不互相替代。

一、是什么

ctx.shellShellExecutor 服务定义缝:抽象类只声明 run()start()resolve() 与能力 getter sandboxMode,自己不碰进程实现。job id、所有权、收集、取消与通知归通用的 ctx.jobs 运行时。

ctx key角色
shellctx.shell服务定义:抽象 ShellExecutor + 词汇类型(ShellExecRequest / ShellExecSpec / ShellRunResult / ShellProcess),导出 SHELL_SETTINGS_NAMESPACEparseExitStatus
bash-local—(ctx.shell 本地 provider)本地子进程执行器:bash -c、命令默认、deadline、因果分类、模型友好环境
bash-sandbox—(ctx.shell 沙箱 provider)沿用 bash-local 机制,但把 argv 经 ctx.sandbox 包进 confine,并把拒绝/runner 失败报成结果事实
tool-bash—(模型面工具 bash基于 ctx.shell、面向模型的工具 schema 与渲染约定

bash-sandbox 是与 bash-local 落在同一服务缝之后的沙箱执行器;tool-bash 检测它的 sandboxMode 能力来公告升权字段,无需导入提供方。这是标准的能力缝(capability seam)拆法,容器化/远程执行器可以同样接入。

二、服务 API(ctx.shell

成员语义
resolve(request)由请求填充/封顶为完全指定的 ShellExecSpecworkdirtimeoutMsstdoutMaxBytes…),run/start 只收到已解析 spec
run(spec)前台执行,完成时 resolve。只因基础设施失败而 reject(工作目录不可用、shell 缺失、signal 调用前已中止);非零退出、超时终止、中止都以描述性 ShellRunResult 求解
start(spec)后台执行,立即返回 ShellProcess 句柄;不应用超时。调用方可用 kill()/readOutput()/done 把它适配到 ctx.jobs
get sandboxMode()能力事实:沙箱执行器的默认模式(基类返回 undefined=不使用沙箱);tool-bash 据此仅在支持升权时公布 sandbox_permissions/justification 字段
ShellProcess.kill()终止进程组;进程已结束则返回 false
ShellProcess.readOutput()增量读取:连续读取绝不重复交付;因缓冲区容量丢数据的读取标 lossy,并指向完整流 spill 文件

实现继承 ShellExecutor 并实现抽象方法;dispose 必须终止并等待仍在运行的进程。

前台超时始终由执行器负责start() 明确忽略 timeoutMs:后台进程只经 kill() 或 spec 的 AbortSignal 停止。

三、run 的求解语义(不 reject)

// packages/shell/shell/src/index.ts
/**
* Run a command in the foreground; resolves when it finishes.
* @returns the outcome; nonzero exits, timeout kills, and abort kills
* resolve with a descriptive result rather than reject.
*/
abstract run(spec: ShellExecSpec): Promise<ShellRunResult>

ShellRunResult 把退出事实与首个中止原因合并成一个字段对:

// packages/shell/shell/src/types.ts
export interface ShellRunResult {
exitCode: number | null // 进程被信号杀死时为 null
signal: NodeJS.Signals | null
timedOut: boolean // 执行器自身超时是首个中断原因
aborted: boolean // 调用方 AbortSignal 是首个中断原因
timeoutMs: number // 本跑实际生效的超时(默认/封顶后)
stdout: CollectedOutput
stderr: CollectedOutput
sandbox?: ShellSandboxInfo // 仅沙箱执行器会出现
}

timedOutaborted 互斥:单一 deadline 同时驱动超时与取消,结束时只报 first-abort 原因。bash-local 里裁判逻辑为:

// packages/shell/bash-local/src/index.ts
using d = deadline(spec.signal, spec.timeoutMs, 'BASH_TIMEOUT')
const handle = this.ctx.subprocess.spawn(this.spawnSpec(spec, argv, spec.stdoutMaxBytes, d.signal))
const outcome = await handle.done
const timedOut = timeoutOf(d.signal, 'BASH_TIMEOUT') !== undefined
const aborted = d.signal.aborted && !timedOut

四、start:后台句柄

start()run() 的后台对偶:立即返回 ShellProcessdone 在进程关闭时 settle 且从不 reject(spawn 失败 settle 为 killed,并把错误放在 stderr)。后台进程在宿主组合 teardown 时被终止并 join,因此即使执行器热重载,进程仍受 subprocess 缝管理。

readOutput() 每次返回自上次读取以来的增量(stderr 用 [stderr] 段标记),并把 stdout/stderr 的尾截断合并成一个 lossy 标志,stdoutSpillPath/stderrSpillPath 指向完整流 spill 文件:

// packages/shell/shell/src/types.ts
export interface ShellProcessRead {
delta: string // 自上次读取以来新增的输出
lossy: boolean // 截断丢弃了未被 delta 覆盖的字节
stdoutSpillPath?: string
stderrSpillPath?: string
}

五、工具层能力公告(sandboxMode)

sandboxMode 抽象 getter 是组合能力事实。基类默认返回 undefined(=此执行器不使用沙箱);bash-sandbox 覆盖它并返回 ctx.sandboxPolicy.defaultMode

// packages/shell/bash-sandbox/src/index.ts
override get sandboxMode(): SandboxMode {
return this.mode // 构造时取 ctx.sandboxPolicy.defaultMode
}

tool-bash 在注册时读取它:

// packages/shell/tool-bash/src/index.ts
const defaultMode = ctx.shell.sandboxMode
const escalationModes: readonly SandboxMode[] = defaultMode === undefined ? [] : ESCALATION_TARGETS
const sandboxPolicy = defaultMode === undefined ? undefined : ctx.get('sandboxPolicy')
  • defaultMode === undefined不公布 sandbox_permissions/justification 参数(本地无沙箱执行器);
  • defaultMode !== undefined → 公布升权字段 = ESCALATION_TARGETS,并要求 ctx.sandboxPolicy 已挂载(缺则加载时抛错);
  • 升权走 approveEscalation,通过 ctx.approval任何执行前完成 fail-closed 序列。

六、设置命名空间与环境 overlay

SHELL_SETTINGS_NAMESPACEbash 由服务定义导出,而非某个 provider——因为它命名的是能力而非实现。一个宿主只组装一个 ctx.shell provider:

// packages/shell/shell/src/index.ts
export const SHELL_SETTINGS_NAMESPACE = settingsNamespace('shell')

win32 层把 POSIX 行换成 pwsh 行(pwsh-localpwsh -NoLogo -NoProfile -NonInteractive -Command … 调用),同时挂载两者会因服务重复注册而在加载期快速失败;所以各平台 provider 用同一 schema 注册这同一个命名空间,永不相撞,跨平台携带的 settings.yaml 在两边都能继续解析。

受类型限制的信任环境 overlay。执行环境有三条输入通道(两条普通 + 一条受管):

通道来源语义
普通 envhooks 桥接、原生插件(CLAUDE_PROJECT_DIRCLAUDE_PLUGIN_ROOT凭据清除之后 merge;模型面工具不暴露为参数
dshEnvctx.shellEnv.collect() 收集的受管快照类型为 DshEnvironmentKey`${'DSH_'}${string}`;在 env 之后 merge,受管 DSH_* 不可被顶掉
ENV_OVERRIDESbash-local 常量模型友好条目 NO_COLOR=1TERM=dumbPAGER=cat 等,最先 merge,信任调用方自身条目仍胜出

最终 spawn 的显式 env 是分层合并的,随后才由 subprocess 服务再做一次凭据清除:

// packages/shell/bash-local/src/index.ts
env: { ...ENV_OVERRIDES, ...spec.env, ...spec.dshEnv },

因为受管 key 在最后 merge,所以省略的当前事实不会回退到陈旧环境值env 条目也无法顶掉 DSH_*dshEnv 在已解析 spec 上仍可选;缺失表示没有 overlay。

七、parseExitStatus:marker 逆解析

parseExitStatus(连同 ParsedExitStatus)是 shell 工具共享渲染约定的另一半:dsh-tool-bashrenderResultdsh-tool-pwshrenderPwshResult 都追加 [exit code: N][killed by signal: X] marker。把解析放进服务定义,两个工具就永远不会在 marker 约定上漂移

// packages/shell/shell/src/render.ts
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
if (signal?.[1] !== undefined) return { body: text.slice(0, signal.index), signal: signal[1] }
const exit = /\n\[exit code: (\d+)\]$/.exec(text)
if (exit?.[1] !== undefined) return { body: text.slice(0, exit.index), exitCode: Number(exit[1]) }
return { body: text, exitCode: 0 }

规则:带 [killed by signal: X] → 返回 signal;非零 [exit code: N] → 返回 exitCode;两者皆无 → 干净退出 0。已消费的 marker 从 body 移除,因为终端卡把退出状态渲染成独立 pill,留在正文会重复。要求前导换行 + 字符串结尾,避免普通输出恰好以 marker 样文本结尾被误匹配。

八、已知限制与暂缓事项

  • 没有交互式输入词汇stdin 只会在 spawn 时写入一次并关闭;缝不提供对运行中进程继续输入的通道,也没有 PTY 会话概念。
  • 前台超时始终由执行器负责:缝上由调用方负责 deadline 的模式已由工具调用超时策略 Agent Note 明确暂缓。

九、验证

# 看 bash 执行器/工具是否装载
dsh web --dump-config | grep -iE "shell|tool-bash"
# 会话里看 bash 子进程(透传)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E '"bash' | head
# 在会话里跑一条命令、观察 exit marker 与 job 语义
# 前台:非零退出以描述性结果返回,不 reject
bash -c "exit 3" # → [exit code: 3]
# 后台:立即返回 job id,不应用前台超时
bash -c "sleep 30" # run_in_background: true → 得到 jobId,job_output / job_kill

下一步