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-bash/tool-pwsh 消费它。docs/capability-seams.md 记录的直接消费者有 tool-bash、tool-pwsh、hooks-claude-code、hooks-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服务定义(ShellExecutor、ctx.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 工具
@deepseek-ai/dsh-tool-pwsh-persistent消费者:基于 PTY 接缝、所有者隔离的持久 PowerShell 工具

执行契约 ​

ShellExecutor(packages/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 以及可选的 workdir/timeoutMs/stdoutMaxBytes/signal/stdin/env/dshEnv/sandboxPolicy);resolve() 填充默认值并给超时加上限。工具层总是在 run/start 之前调用 resolve,因此后面两个方法读到的都是显式取值。

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

ShellRunResult(packages/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 出一个描述性的 ShellRunResult。timedOut 与 aborted 互斥——由一个融合的截止时间(@deepseek-ai/dsh-timeout)驱动二者,最先触发的因胜出。

输出捕获与保留 ​

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

  • ItemRetainer<T>——为逻辑单元流(路径、grep 命中、搜索来源)设置上限。v1 只支持 head 策略(maxItems)。
  • TextRetainer——为面向字节的文本流设置上限,策略为 head/tail/headTail,在内存中滚动保留有界后缀,并在每次截断处修剪不完整的 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 沙箱化 ​

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

  • 环境覆盖(ENV_OVERRIDES):NO_COLOR=1、TERM=dumb、PAGER=cat、GIT_PAGER=cat——禁用会让工具输出变乱的分页/颜色。
  • 配置(static Config):cwd、timeoutMs(默认 120_000)、maxTimeoutMs(600_000)、maxOutputBytes(64_000)、maxSpillBytes(64 MiB)、graceMs(3_000,SIGTERM→SIGKILL 宽限期)。
  • 环境分层:spawn 合并 {...ENV_OVERRIDES, ...spec.env, ...spec.dshEnv},所以受信任的受管理 DSH_* 快照既压过调用方 env 也压过终端覆盖。subprocess 服务会先应用它自己的环境凭据清理。
  • LocalBashExecutor.runArgv/startArgv 标记为 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,而后台进程则携带 runnerFailed。pwsh-local/pwsh-sandbox 这对在 Windows 层镜像 bash 版,替换掉 POSIX 行。

三种沙箱模式(packages/sandbox/sandbox/src/index.ts):read-only、workspace-write、danger-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_output/job_kill 经 ctx.jobs 管理。
  • 沙箱拒绝渲染为 [sandbox: file access denied under <mode> mode];升级(escalation)会用 sandbox_permissions 加 justification 把同一条命令重跑一次,经 ctx.approval 的 approveEscalation 走审批。
  • 长输出截断到尾部,完整输出保存到文件并上报路径([output truncated; full output: <spill>])。

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

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

@deepseek-ai/dsh-tool-pwsh-persistent(packages/shell/tool-pwsh-persistent)是 tool-bash-persistent 的 PowerShell 对应体:它注册一个持久 pwsh 工具,其 PowerShell 状态(cwd、$env: 变量、函数、后台任务)在多次调用间对所属 agent 保留。每个 agent 有自己的 shell,由带 pwsh 方言后端的所有者隔离 PTY 会话支撑(默认 shell 后端通过配置了 shellDialect: pwsh 的 dsh-terminal-bash 启动 PowerShell;部署也可注册并点名另一个 pwsh 方言 PTY 后端),同一 agent 的命令一次只跑一条。超时或显式 exit 会关闭 shell,下次调用重新开始;读取 stdin 的交互式前台子进程会阻塞到命令超时,因此交互式工作属于终端工具。

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_BACKEND、DUPLICATE_NAME、FOREIGN_SESSION、NO_BACKEND、NO_SESSION、OWNER_NOT_LIVE、SEND_ACTIVE、SERVICE_DISPOSING。

tool-terminal(@deepseek-ai/dsh-tool-terminal)暴露六个按所有者划分的工具:terminal_open、terminal_send、terminal_read、terminal_signal、terminal_close(等待被捕获的属主进程树真正消失;面向 shell 的 SIGKILL 会被拒绝,改用本工具)、terminal_list(当前 agent 拥有的会话,见 packages/terminal/tool-terminal/src/index.ts)。发送是面向行的:TerminalSendRequest { text, submit, signal? };一次 settle 返回 viewport 外加 waitReason(stdin_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>)包裹每条命令,因此对于一个 agent,状态(cwd、导出的变量)在多次调用间保留,而标记之间的输出被捕获、剥离标记并截断到 maxOutputChars。提示符保持为后端自有——terminal-bash 设置受控 PS1,其 PROMPT_COMMAND 在每次提示符前重新断言它,使就绪检测保持有效——当 shell 在未打印结束标记的情况下再次读取 stdin(exec、中断、交互式前台子进程)时,调用返回已捕获的部分输出,该输出可能以后端自己的提示符文本结尾。

terminal 对比持久 bash ​

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

延伸阅读 ​

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