Skip to content

shell 接缝

packages/shell/shell 是仓库中最典型的能力接缝(capability seam)。一个接缝把某项能力拆分为三种角色(见 docs/capability-seams.md):

  • 服务定义(Service Definition)——抽象类与词汇表,由接缝包持有,并通过 Cordis 的 declare module 增强注册为 ctx.<key> 服务。
  • Provider——以插件加载、实现抽象方法的具体子类。每个上下文恰好挂载一个;再挂一个会抛出标准的 Cordis「重复服务」错误。
  • 消费者(Consumer)——依赖该服务、与具体 Provider 无关的工具插件(以及 hooks 桥)。

shell 接缝就是参考范例:ShellExecutor 定义 ctx.shell,四个 Provider 实现它,tool-bashtool-pwsh 消费它。docs/capability-seams.md 记录的直接消费者有 tool-bashtool-pwshhooks-claude-codehooks-codex

text
            tool-bash ──┐        tool-pwsh ──┐
     hooks-claude-code ─┤   hooks-codex ────┤        (消费者)
                        ▼                    ▼
              ┌──────────────────────────────┐
              │   服务定义:ctx.shell        │
              │   ShellExecutor(抽象类)    │   @deepseek-ai/dsh-shell
              └──────────────────────────────┘

      ┌──────┬──────────┴──────┬─────────────┐
      ▼      ▼                 ▼             ▼
 bash-local │ bash-sandbox  pwsh-local ── pwsh-sandbox     (Provider)
   (LocalBashExecutor) │ (SandboxBashExecutor)
      └──┬───┘            │
         │ 经 ctx.subprocess 生成进程(subprocess 接缝)

ctx.shell 只负责进程句柄。后台任务的 id、会话、所有权、轮询与通知归属 ctx.jobs@deepseek-ai/dsh-jobs),因此执行器与会话解耦。

各包版本

该组内所有 workspace 包共享被钉住的根版本:

角色
@deepseek-ai/dsh-shell服务定义(ShellExecutorctx.shell
@deepseek-ai/dsh-bash-localProvider:本地 bash -c,基于 ctx.subprocess
@deepseek-ai/dsh-bash-sandboxProvider:经 ctx.sandbox 包裹的 bash
@deepseek-ai/dsh-pwsh-localProvider:本地 PowerShell,基于 ctx.subprocess
@deepseek-ai/dsh-pwsh-sandboxProvider:经 ctx.sandbox 包裹的 PowerShell
@deepseek-ai/dsh-shell-envCore:ctx.shellEnv,受管理的 DSH_* 事实
@deepseek-ai/dsh-tool-bash消费者:面向模型的 bash 工具
@deepseek-ai/dsh-tool-bash-persistent消费者:基于 PTY 接缝的持久 bash 工具
@deepseek-ai/dsh-tool-pwsh消费者:面向模型的 PowerShell 工具

执行契约

ShellExecutorpackages/shell/shell/src/index.ts)暴露三个抽象方法外加一个 sandboxMode getter:

ts
export abstract class ShellExecutor extends Service {
  constructor(ctx: Context) { super(ctx, 'shell') }
  get sandboxMode(): SandboxMode | undefined { return undefined }
  abstract resolve(request: ShellExecRequest): ShellExecSpec
  abstract run(spec: ShellExecSpec): Promise<ShellRunResult>
  abstract start(spec: ShellExecSpec): ShellProcess
}

流程是请求 → resolve → run/start。调用方传入原始 ShellExecRequest(command 以及可选的 workdirtimeoutMsstdoutMaxBytessignalstdinenvdshEnvsandboxPolicy);resolve() 填充默认值并给超时加上限。工具层总是在 runstart 之前调用 resolve,因此后面两个方法读到的都是显式取值。

前台:一次 shell 执行返回什么

ShellRunResultpackages/shell/shell/src/types.ts)是前台一次运行 resolve 出的结构:

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   // 未沙箱化执行器时为缺省
}

请注意这里的错误纪律(见 docs/defensive-patterns.md):run 只针对基础设施类故障 reject。非零退出、超时杀死、中止杀死都会resolve 出一个描述性的 ShellRunResulttimedOutaborted 互斥——由一个融合的截止时间(@deepseek-ai/dsh-timeout)驱动二者,最先触发的因胜出。

输出捕获与保留

CollectedOutput 从 subprocess 接缝再导出,而保留(retention)策略来自共享库 @deepseek-ai/dsh-output-retentionpackages/util/output-retention)。它明确不是 Cordis 服务或插件——不接收 ctx、不注册任何东西、不发任何事件。它提供两个有状态的保留器:

  • ItemRetainer<T>——为逻辑单元流(路径、grep 命中、搜索来源)设置上限。v1 只支持 head 策略(maxItems)。
  • TextRetainer——为面向字节的文本流设置上限,策略为 headtailheadTail,在内存中滚动保留有界后缀,并在每次截断处修剪不完整的 UTF-8 码点,使返回的文本不会因截断本身引入替换字符。

两者都上报精确的 Omitted 计数;formatRetentionNotice 把它拼成标准页脚。工具专属的「恢复提示」(收窄模式、读取 spill 文件)留在工具里——库只负责机制。

另外,bash-local 的配置带有每条流的 spill 上限(maxSpillBytes,默认 64 MiB):内存中最多保留 maxOutputBytes(默认 64_000)字节,超出 spill 上限后只保留内存尾部并上报截断。

后台进程

start(spec) 同步返回一个 ShellProcess 句柄:

ts
export interface ShellProcess {
  status: ShellProcessStatus      // 'running' | 'completed' | 'killed'
  exitCode: number | null
  signal: NodeJS.Signals | null
  readonly done: Promise<void>    // 永不 reject;spawn 失败以 'killed' 收场
  sandbox?: ShellSandboxInfo
  readOutput(): ShellProcessRead  // 增量、消耗性;lossy 读取会标记 spill 路径
  kill(): boolean
}

后台运行忽略 timeoutMs(执行器不设超时);调用方通过 kill() 或 spec 的 signal 停止。readOutput 增量且消耗性——连续读取不会重复投递输出;一次丢失了未读字节的读取会置 lossy 并指向完整流的 spill 文件。仍在运行的后台进程会在其所属 composition 拆除时被杀死并等待。

Provider:本地 vs 沙箱化

LocalBashExecutorpackages/shell/bash-local/src/index.ts)经 ctx.subprocess 生成 bash -c <command>,并负责命令默认值、截止时间、对模型友好的终端环境,以及 stdout/stderr 合并。要点:

  • 环境覆盖ENV_OVERRIDES):NO_COLOR=1TERM=dumbPAGER=catGIT_PAGER=cat——禁用会让工具输出变乱的分页/颜色。
  • 配置static Config):cwdtimeoutMs(默认 120_000)、maxTimeoutMs600_000)、maxOutputBytes64_000)、maxSpillBytes64 MiB)、graceMs3_000,SIGTERM→SIGKILL 宽限期)。
  • 环境分层:spawn 合并 {...ENV_OVERRIDES, ...spec.env, ...spec.dshEnv},所以受信任的受管理 DSH_* 快照既压过调用方 env 也压过终端覆盖。subprocess 服务会先应用它自己的环境凭据清理。
  • LocalBashExecutor.runArgvstartArgv 标记为 protected,便于子类在执行边界替换公开的 bash -c argv。

SandboxBashExecutor@deepseek-ai/dsh-bash-sandbox继承 LocalBashExecutor,并经 ctx.sandbox 包裹精确 argv。它原样继承本地配置;沙箱策略(默认模式 + workspace 根)位于 ctx.sandboxPolicy,而 runner 的选择是 ctx.sandbox Provider 的配置。resolve() 盖上完整的逐调用策略;run()start() 上报 result.sandbox = { mode, denied, enforcement?, runnerFailed? }。runner 启动失败会让前台调用抛出 SANDBOX_UNAVAILABLE,而后台进程则携带 runnerFailedpwsh-localpwsh-sandbox 这对在 Windows 层镜像 bash 版,替换掉 POSIX 行。

三种沙箱模式(packages/sandbox/sandbox/src/index.ts):read-onlyworkspace-writedanger-full-access。强制执行的机制(本地 Landlock/Seatbelt/bwrap runner,以及 E2B 远端底座)见 沙箱架构

面向模型的工具

@deepseek-ai/dsh-tool-bash 通过 defineTool 注册 bash 工具。其解析后的 BashToolArgs

ts
interface BashToolArgs {
  command: string
  description: string
  timeoutMs?: number
  workdir?: string
  run_in_background?: boolean
  sandbox_permissions?: string   // 仅在存在会约束的执行器时对外公布
  justification?: string
}

执行语义(编码在模型看到的工具描述里):

  • 每次调用都在全新 shell 中运行——cwd/变量/函数均不保留;请传 workdir 而不是用 cd
  • 非零退出渲染为 [exit code: N];正常退出无标记。
  • run_in_background: true 立即返回一个 job id;用 job_outputjob_killctx.jobs 管理。
  • 沙箱拒绝渲染为 [sandbox: file access denied under <mode> mode];升级(escalation)会用 sandbox_permissionsjustification 把同一条命令重跑一次,经 ctx.approvalapproveEscalation 走审批。
  • 长输出截断到尾部,完整输出保存到文件并上报路径([output truncated; full output: <spill>])。

渲染器会标记超时或信号:先是 [timed out after <ms>ms],然后是非零时 [killed by signal: X][exit code: N]renderResultrenderProcessRead 位于 packages/shell/tool-bash/src/render.ts。退出标记契约是共享的:parseExitStatus(在 @deepseek-ai/dsh-shell/render)从渲染字符串中恢复 { exitCode }{ signal },因此终端展示可以把回放的运行结果渲染成真正的退出标记胶囊。

@deepseek-ai/dsh-tool-pwsh 为 PowerShell 镜像 bash 工具。

shell-env:受管理的 DSH_* 事实

@deepseek-ai/dsh-shell-env 持有 ctx.shellEnv——一个由 shell 工具消费的受信任、按执行生效的 DSH_* 变量注册表。内置事实(如 DSH_HOME,经 DSH_HOME~/.dsh 解析)由注册表自身持有;插件可注册带 effect 作用域清理的额外可枚举事实。每个 shell 工具在每次执行时收集一个可信快照,执行器再重建命名空间(DSH_ENV_PREFIX 位于 subprocess 接缝)。subprocess 服务在合并显式层之前,会先清理凭据形态的环境名(SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i)以及环境里残留的 DSH_* 名。

终端接缝(持久化 PTY)

packages/terminal/terminal 定义 ctx.terminals——一个按所有者划分的持久 PTY 注册表。与一次性的 bash 工具不同,PTY 会话在多次调用之间保留。后端负责终端机制;注册表负责 id、发布、授权(每个会话都绑定到它的 owner: Agent)以及等待清理。

  • ctx.terminals 接缝的所有者是 terminal,Provider 是 terminal-bash,消费者是 tool-terminal(据 docs/capability-seams.md)。
  • 后端接口:TerminalBackend + TerminalBackendSession。后端经 ctx.subprocess.spawnTerminal 生成,并执行共享沙箱围栏。
  • 模型表面上允许的信号:SIGINT | SIGTERM | SIGKILL | SIGTSTP | SIGHUP
  • 错误分类:TerminalErrorCode——DUPLICATE_BACKENDDUPLICATE_NAMEFOREIGN_SESSIONNO_BACKENDNO_SESSIONOWNER_NOT_LIVESEND_ACTIVESERVICE_DISPOSING

tool-terminal@deepseek-ai/dsh-tool-terminal)暴露四个按所有者划分的工具:terminal_openterminal_sendterminal_readterminal_signal(外加一种后台发送形态)。发送是面向行的:TerminalSendRequest { text, submit, signal? };一次 settle 返回 viewport 外加 waitReasonstdin_read | inferred_idle | timeout | session_exit)。

@deepseek-ai/dsh-tool-bash-persistent 是 PTY 接缝的另一个模型侧消费者:它注册一个 bash 工具,用回显标记(__DSH_PERSISTENT_BASH_START_<nonce>____DSH_PERSISTENT_BASH_END_<nonce>:<status>)和提示符(__DSH_PERSISTENT_BASH_PROMPT__)包裹每条命令,因此对于一个 agent,状态(cwd、导出的变量)在多次调用间保留,而输出被定界并截断到 maxOutputChars

terminal 对比持久 bash

tool-bash(一次性)tool-terminal(PTY)bash 持久版
跨调用状态完整 PTY 画面cwd + 导出 env
底座ctx.shell(执行器接缝)ctx.terminals(PTY 注册表)ctx.terminals
模型工具bashterminal_open/send/read/signalbash(持久)

延伸阅读

  • 文件系统工具与策略 —— 兄弟 ctx.fs 接缝,采用同样的本地/沙箱 Provider 模式。
  • 代码运行时 —— 同样的接缝思路如何运行不可信程序。
  • 沙箱架构 —— bash-sandbox 背后的强制执行机制与三种模式。
  • 权限与审批 —— tool-bashctx.approval 驱动的升级流程。
  • docs/capability-seams.md —— 接缝分类以及完整的 ctx.shellctx.terminals 条目。
  • packages/shell/shell/src/index.ts —— 典范的 ShellExecutor 定义。