Skip to content

3.1 Shell 执行能力族

本章概览:DSH 如何把"执行一条 shell 命令"组织成一个可替换的能力缝(capability seam)——定义、实现、消费三分离,并在此基础上做显式默认化、托管环境注入与平台选择。读完可回答:一条 bash 调用从模型侧到进程侧经历了什么,如何换成沙箱化执行器,Windows 上为什么默认是 pwsh。

概述 / 定位

shell 能力族位于 packages/shell/ 下,全部是产品包(product package),覆盖一条执行缝的完整生命周期:

角色ctx 键
dsh-shellService Definition:抽象执行器契约 + 词汇表ctx.shell
dsh-bash-local / dsh-bash-sandboxService Provider:本地 / 沙箱化 bash 执行(注册 ctx.shell
dsh-pwsh-local / dsh-pwsh-sandboxService Provider:Windows 上的 PowerShell 执行(注册 ctx.shell
dsh-tool-bash / dsh-tool-pwshConsumer:模型侧 shell 工具(注册 ctx.tools
dsh-shell-env托管 DSH_* 环境注册表ctx.shellEnv

出处:packages/shell/README.mddeepseek-harness-src/packages/shell/README.md

职责边界很明确:ctx.shell 只做"前台运行一条命令 + 启动一个后台进程"两件事;job id、所有权、收集、取消与通知全部属于通用 ctx.jobs 运行时。执行器返回的是无任务概念的进程句柄(ShellProcess),由工具层适配进 ctx.jobs

核心机制

一次执行 = request → resolve() → spec

缝把"调用方想要的"与"执行器实际执行的"分开。ShellExecRequestworkdirtimeoutMsstdoutMaxBytes 等字段都是可选的,由 ctx.shell.resolve(request) 依据实现配置补默认值、按上限封顶,产出字段全部必填的 ShellExecSpecrun(spec) / start(spec) 只接受 spec,从不接收裸 request。

出处:packages/shell/shell/README.mddeepseek-harness-src/packages/shell/shell/README.md)与 docs/subsystems/shell.mddeepseek-harness-src/docs/subsystems/shell.md

这就是仓库的**"显式 > 隐式"(explicit > implicit at package boundaries)原则:默认化是一个显式的 resolve() 步骤,绝不在 run() 内部做隐藏的 ?? default。注意 workdir 的默认值在工具层**就定了:来自调用代理会话的 session.header.cwd(N 个会话共享一个执行器,所以不能由执行器决定),只有无会话 cwd 时才回退到执行器配置。

本地执行器 dsh-bash-local

LocalBashExecutor 每次调用经 ctx.subprocess 以进程组方式 spawn bash -c <command>:全新非登录 shell、无 rc 文件、无跨调用状态。配置默认值即执行语义:

yaml
- id: bash
  name: '@deepseek-ai/dsh-bash-local'
  config:
    cwd: /path/to/workspace   # 默认 process.cwd()
    timeoutMs: 120000         # 前台默认超时
    maxTimeoutMs: 600000      # 单次覆盖上限
    maxOutputBytes: 64000     # 每流内存上限,溢出落盘
    maxSpillBytes: 67108864   # 完整输出落盘上限
    graceMs: 3000             # kill 升级与退出后排空宽限

前台运行用"一个截止时间"融合配置超时与调用方中止信号:只有执行器自己的超时报告 timedOut,上游取消报告 aborted,二者互斥。执行器还注入模型友好终端环境 NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat,防止分页器与 ANSI 颜色污染结果。

出处:packages/shell/bash-local/README.mddeepseek-harness-src/packages/shell/bash-local/README.md

沙箱化执行器 dsh-bash-sandbox

SandboxBashExecutor 继承本地执行器的全部进程机制,唯一的差别是:把即将 spawn 的 ['bash', '-c', command] 原样交给 ctx.sandbox.confine(argv, policy),然后直接 spawn 返回的 argv。拒绝是结果事实(denial as result fact):一次被拒的运行在结果里上报 denied: true、实际生效的 mode 与 enforcement,从不协商权限;无可用后端时前台抛结构化 SANDBOX_UNAVAILABLE 错误,绝不静默裸跑(fail closed)。

出处:packages/shell/bash-sandbox/README.mddeepseek-harness-src/packages/shell/bash-sandbox/README.md

shell-env:每次执行一份 DSH_* 快照

ctx.shellEnv 是托管环境注册表。内建事实 DSH_HOMEDSH_SHELL=1DSH_SESSION_ID(有持久化产物时还有 DSH_SESSION_JSONL)由注册表自己持有;每次模型 shell 调用都重新收集一份快照(collect(execution)),经 ShellExecRequest.dshEnv 专用通道传入。执行器先丢弃继承自父进程的全部 DSH_*,再合并这份快照,所以嵌套 harness 与并发父子代理不会泄漏陈旧身份,process.env 也从不被修改。

插件可注册额外变量(如 DSH_DEPLOYMENT_REGION),注册通过 ctx.effect() 生效、随插件 fiber 释放——这就是"效果作用域"(effect scope);键名重复归属或运行时出现未声明键都会 fail loud。

出处:packages/shell/shell-env/README.mddeepseek-harness-src/packages/shell/shell-env/README.md

模型侧 bash 工具

dsh-tool-bash 是缝的 Consumer,工具参数只暴露模型需要的东西:

参数说明
command(必填)bash -c 运行;无状态,用 workdir 而非 cd
description(必填)一句话命令摘要,仅 UI/日志展示
timeoutMs / workdir覆盖执行器默认;workdir 默认会话 cwd
run_in_background立即返回 job id,无超时
sandbox_permissions / justification仅当挂载的执行器沙箱化时才出现(能力位 ctx.shell.sandboxMode 探测)

结果文本按序包含 stdout、可选 [stderr] 段、以及各条件标记:[exit code: N][killed by signal: X][timed out after <ms>ms][output truncated; full output: <path>][sandbox: file access denied under <mode> mode]。工具还注册了 tool:bash 提示词段(order 105):Check the [exit code: N] marker on every bash result; investigate failures before moving on. 后台调用返回精确的 started background job <id>,随后由 job_output / job_list / job_kill 控制。

出处:packages/shell/tool-bash/README.mddeepseek-harness-src/packages/shell/tool-bash/README.md

bash vs pwsh:base 组合包的平台表达式

base 组合包(dsh-base)的补丁层同时列出两套执行栈,用每行自带的 disabled 表达式按平台互斥挂载:

yaml
- id: bash-sandbox
  name: '@deepseek-ai/dsh-bash-sandbox'
  disabled: !!js process.platform === 'win32'
- id: pwsh-sandbox
  name: '@deepseek-ai/dsh-pwsh-sandbox'
  disabled: !!js process.platform !== 'win32'
# tool-bash / tool-pwsh 同样成对出现

POSIX 主机挂 bash 栈,win32 主机默认挂 pwsh 栈(bash 在 Windows 上无可用 runner);pwsh 栈同样跑在沙箱 runner 之上。想覆盖默认只能走组合配置:在 profile 或 home cordis.patch.yml 里按 id 覆盖,且 bash-restore 配方必须完整(同时禁用 pwsh 两行、重启用 bash 两行,否则两个执行器族注册同一 bash 服务会 fail loud)。

出处:packages/bundle/base/cordis.patch.ymldeepseek-harness-src/packages/bundle/base/cordis.patch.yml)与 Windows 默认 pwsh 设计笔记deepseek-harness-src/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md

关键设计决策

  • 三角色拆分dsh-shell 只依赖 cordis,不依赖任何后端;Consumer 通过能力位探测(sandboxMode)而非 import 提供方,所以换执行器(本地→沙箱→容器/远程)对工具层透明。
  • 显式 > 隐式resolve() 是唯一的默认化入口,run() 拿到的 spec 字段全部显式,执行器语义可预测、可测。
  • 每次执行重收集 DSH_* 快照:托管变量是"本次调用的托管事实",不是进程环境的镜像;陈旧继承值被丢弃,process.env 永不被修改。
  • deny-only 沙箱:执行器只上报拒绝事实,不碰审批;"要不要允许"的问题留在工具层,经 ctx.approvaltools/pre-execute 表达。
  • 后台进程无执行器超时:生命周期、所有权、取消归 ctx.jobs,执行器只保证进程句柄的增量读取与 kill。

扩展点

  • 换执行器:子类化 ShellExecutor 实现 resolve / run / start 并注册 ctx.shell——容器化、远程执行器按同一个缝接入;也可选 dsh-tool-bash-persistent(基于 ctx.terminals 的持久 shell 工具)替代一次性 bash。
  • 加托管环境变量ctx.shellEnv.register({ name, variables, resolve }),随 fiber 卸载。
  • 加沙箱后端 / 提权策略:见下一章沙箱能力族;审批问题在工具层与 ctx.approval
  • 拦截执行tools/pre-execute waterfall 承载每调用 allow/deny/ask 策略(docs/architecture.md)。

参考资料

本文档为学习用途的原创讲解,基于 MIT 许可的开源项目 deepseek-ai/deepseek-harness 编写;所有引用均注明出处。