Appearance
3.1 Shell 执行能力族
本章概览:DSH 如何把"执行一条 shell 命令"组织成一个可替换的能力缝(capability seam)——定义、实现、消费三分离,并在此基础上做显式默认化、托管环境注入与平台选择。读完可回答:一条 bash 调用从模型侧到进程侧经历了什么,如何换成沙箱化执行器,Windows 上为什么默认是 pwsh。
概述 / 定位
shell 能力族位于 packages/shell/ 下,全部是产品包(product package),覆盖一条执行缝的完整生命周期:
| 包 | 角色 | ctx 键 |
|---|---|---|
dsh-shell | Service Definition:抽象执行器契约 + 词汇表 | ctx.shell |
dsh-bash-local / dsh-bash-sandbox | Service Provider:本地 / 沙箱化 bash 执行 | (注册 ctx.shell) |
dsh-pwsh-local / dsh-pwsh-sandbox | Service Provider:Windows 上的 PowerShell 执行 | (注册 ctx.shell) |
dsh-tool-bash / dsh-tool-pwsh | Consumer:模型侧 shell 工具 | (注册 ctx.tools) |
dsh-shell-env | 托管 DSH_* 环境注册表 | ctx.shellEnv |
出处:
packages/shell/README.md(deepseek-harness-src/packages/shell/README.md)
职责边界很明确:ctx.shell 只做"前台运行一条命令 + 启动一个后台进程"两件事;job id、所有权、收集、取消与通知全部属于通用 ctx.jobs 运行时。执行器返回的是无任务概念的进程句柄(ShellProcess),由工具层适配进 ctx.jobs。
核心机制
一次执行 = request → resolve() → spec
缝把"调用方想要的"与"执行器实际执行的"分开。ShellExecRequest 中 workdir、timeoutMs、stdoutMaxBytes 等字段都是可选的,由 ctx.shell.resolve(request) 依据实现配置补默认值、按上限封顶,产出字段全部必填的 ShellExecSpec;run(spec) / start(spec) 只接受 spec,从不接收裸 request。
出处:
packages/shell/shell/README.md(deepseek-harness-src/packages/shell/shell/README.md)与docs/subsystems/shell.md(deepseek-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.md(deepseek-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.md(deepseek-harness-src/packages/shell/bash-sandbox/README.md)
shell-env:每次执行一份 DSH_* 快照
ctx.shellEnv 是托管环境注册表。内建事实 DSH_HOME、DSH_SHELL=1、DSH_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.md(deepseek-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.md(deepseek-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.yml(deepseek-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.approval与tools/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-executewaterfall 承载每调用 allow/deny/ask 策略(docs/architecture.md)。
参考资料
- 族总览:
packages/shell/README.md(deepseek-harness-src/packages/shell/README.md) - 子系统页:
docs/subsystems/shell.md(deepseek-harness-src/docs/subsystems/shell.md) - Definition:
packages/shell/shell/README.md;Provider:packages/shell/bash-local/README.md、packages/shell/bash-sandbox/README.md;Consumer:packages/shell/tool-bash/README.md(均在deepseek-harness-src/packages/shell/下) - 托管环境:
packages/shell/shell-env/README.md(deepseek-harness-src/packages/shell/shell-env/README.md) - 设计笔记:能力缝、stdin/env 可信插件 API、会话身份与日志位置、Windows 默认 pwsh(均在
deepseek-harness-src/.agents/notes/implemented/下)