DSH plugin 的 shell 与子进程 seam:ShellExecRequest 与 DSH_* 环境

插件开发发布于 2026-10-17作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH pluginShellExecRequestDSH_*子进程沙箱
DSH plugin 的执行 seam 怎么接:请求经 resolve() 变成必填规格、DSH_* 受管环境为何不能继承、沙箱事实的 denied 与 runnerFailed 如何区分,以及子进程 stdio 全部显式。

DSH plugin 的执行能力分两层:shell seam 面向「跑一条命令」,子进程 seam 面向「起一个进程并接管它的流」。 前者给你 ctx.shell 与 resolve(),把可选字段补全成必填规格;后者给你完全显式的 stdio 处置与受管的 DSH_* 环境。两者共享一套词汇——DSH_* 命名空间、凭据清除与 CollectedOutput 形状都由子进程 seam 拥有,shell 只是重导出。

理解这条分工,才能解释为什么 stdin、env 与 stdoutMaxBytes 只对进程内插件开放,而面向模型的 bash 工具不暴露它们。

DSH plugin 的 ShellExecRequest:请求、规格与前台结果

一句话结论:请求是可选的、规格是必填的;resolve() 是两者之间唯一的转换点,也是上限的唯一来源。

  1. 请求形态:ShellExecRequest 里只有 command 必填,workdir、timeoutMs、onExpiry、stdoutMaxBytes 都可选。
  2. 规格形态:ShellExecSpec 的这些字段全部必填,由 resolve() 按实现配置补全并设上限。
  3. 超时策略:onExpiry 默认 kill;设为 none 时不启动计时器,timeoutMs 只回显进结果。
  4. 前台结果正交:exitCode、signal、timedOut、aborted 各自独立;timedOut 与 aborted 互斥,按「谁先中断」判定,不会同时为真。
  5. 输出形状:stdout/stderr 都是 CollectedOutput——截断时保留尾部,完整流可落在 spill 文件里。
  6. 进程句柄不等于后台任务:shell seam 返回进程句柄,后台任务的 id、所有权与控制归 jobs 子系统。

DSH plugin 的 DSH_* 受管环境:为什么不能继承

一句话结论:DSH_* 是 Harness 的事实,不是调用方的输入;实现先清除继承值,再合并本次快照。

  1. 收集入口:面向模型的 bash 工具通过 ctx.shellEnv 收集,再经 ShellExecRequest.dshEnv 传入。
  2. 清除顺序:子进程服务在合并当前快照之前移除继承而来的 DSH_* 名称。
  3. 为什么:不可用的当前事实不能继承旧值;调用方 env 也不能顶掉受管值。
  4. 普通环境:env 在凭据清除之后合并;显式的 undefined tombstone 可以删除普通环境里已有的值。
  5. 验证:在命令里打印一个 DSH_* 变量,父进程设过旧值时它也应反映本次快照而不是旧值。

DSH plugin 的沙箱事实:denied 与 runnerFailed 要分开看

一句话结论:denied 是策略拒绝,runnerFailed 是执行器没起来——两者的处置完全不同。

字段含义处置
mode命令实际运行的模式判断是否比你预期更宽松
denied沙箱拒绝了某次文件操作属于策略拒绝
enforcementrunner 对模式的执行完整度判断是否降级
runnerFailed命令运行前 runner 已失败前台抛 SANDBOX_UNAVAILABLE
  1. 前台与后台不同:前台在 runnerFailed 时抛错;已结束的后台进程只能通过事实通道报告。
  2. 模式只在拒绝时可见:模型只有在拒绝事实指出生效模式时才知道它。
  3. 一次性放宽要审批:可用 sandbox_permissions 加 justification 请求严格更宽松的重试,执行前必须经 ctx.approval 批准该次确切调用。
  4. 模式只管文件效果:进程约束与文件效果由沙箱 seam 定义,别把模式当成全部隔离。
  5. 常见误判:命令退出码非零不一定是沙箱拒绝,先看 denied。

DSH plugin 的子进程 seam:stdio 全部显式、argv 不经 shell

一句话结论:没有默认值——每条流的处置、上限与目录都写在 spec 上;argv 绝不经过 shell 解释。

ts
interface SubprocessStdio {
  stdin: 'ignore' | 'pipe' | { data: string }
  stdout: 'pipe' | 'inherit' | SubprocessCollect
  stderr: 'pipe' | 'inherit' | SubprocessCollect
  control?: 'pipe'
}
  1. stdin 三态:ignore 挂到 /dev/null,pipe 暴露给调用方持续写入,{ data } 一次写入并关闭。
  2. 输出三态:pipe 给原始 Readable 供协议解码,inherit 直通父描述符,SubprocessCollect 有界收集。
  3. 收集与 spill:maxBytes 是内存上限,溢出保留尾部;带 spill.maxBytes 时完整流可恢复,超出则丢弃不完整的 spill。
  4. 控制通道:需要独立的字节模式双工时声明 control: 'pipe',不声明就没有。
  5. 可执行文件查找:resolveExecutable(command, env?, signal?) 校验绝对路径,或经提供方清理后的 PATH 解析裸名称。
  6. 结果只承载退出事实:分类与语义由消费方自己判断,seam 不替你下结论。

DSH plugin 的四个消费方:同一 seam 的不同流处置

一句话结论:collect 给批量输出、pipe 给协议分帧、inherit 给诊断直通、终端原语给 PTY。

  1. bash 执行器家族:收集模式,有界批量输出。
  2. LSP:原始协议管道,JSON-RPC 分帧。
  3. PTY 后端:终端进程原语。
  4. ACP subagent:管道传 ndjson,stderr 用 inherit。
  5. 选型判据:要结构化解析用 pipe;要给人看诊断用 inherit;要展示结果用 collect。
  6. 诊断尾部可以不留文件:不需要完整流时省略 spill,只在内存里保留尾部。

DSH plugin 排查注意事项

一句话结论:执行问题先分「规格没补全」「环境不对」「沙箱拒绝」「流处置选错」四类。

  1. 字段缺失报错 → 没走 resolve(),直接把请求当规格用了。
  2. DSH_* 是旧值 → 别从父进程继承,走 dshEnv 快照。
  3. 命令被拒 → 看 denied 与 mode,不是先怀疑命令写错。
  4. 前台抛 SANDBOX_UNAVAILABLE → runnerFailed,runner 没起来。
  5. 输出丢头 → 截断保留的是尾部,需要完整内容就开 spill。
  6. 修复状态如实:本文的请求/规格拆分、DSH_* 规则、沙箱事实与 stdio 处置均取自官方「Shell 执行器」「子进程」「沙箱」页面;接口签名以对应 Cordis API 与你所用版本为准。

来源:Shell 执行器、子进程、沙箱。

常见问题

DSH plugin 调用 shell 时,为什么还要先调一次 resolve()?

DSH plugin 的 shell seam 把面向模型的请求与执行器使用的规格分开:请求里的 workdir、timeoutMs 可选,resolve 会按实现配置补全并设上限。工具层必须先 resolve,拿到的必填规格才是真正执行的值。

DSH plugin 里,DSH_* 环境变量为什么不能从父进程继承?

DSH plugin 的 DSH_* 是 Harness 拥有的子进程事实,实现会先丢弃环境中已有的 DSH_* 再合并本次快照。否则一个已不可用的事实会继承到旧值,调用方也无法用 env 覆盖受管值。

DSH plugin 里,沙箱拒绝和沙箱 runner 失败有什么区别?

DSH plugin 的 denied 表示沙箱拒绝了某次文件操作,runnerFailed 表示命令还没跑 runner 就失败了。前台执行在 runnerFailed 时抛 SANDBOX_UNAVAILABLE,后台进程只能通过事实通道报告。

DSH plugin 里,子进程的 stdout 被截断后,完整输出去哪了?

DSH plugin 的 CollectedOutput 在截断时 text 保留流的尾部,完整流写入私有文件并通过 spillPath 给出路径。是否保留 spill 文件由消费方选择,诊断尾部可以只在内存里缓冲。

DSH plugin 里,子进程 seam 会替我补默认的 stdio 配置吗?

不会,DSH plugin 的子进程 seam 不应用任何默认值:stdin、stdout、stderr 的处置方式、上限与目录全部显式给出。argv 也绝不经过 shell 解释,避免隐式解析带来的注入面。

相关术语

ShellExecRequest(执行请求)
ShellExecRequest 是面向模型或插件的执行请求,command 必填,workdir、timeoutMs、onExpiry、stdoutMaxBytes 等可选。它必须先经 resolve 补全为 ShellExecSpec 才能执行。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/shell
CollectedOutput(收集输出)
CollectedOutput 是一条被收集的输出流:文本在被截断时保留尾部,truncated 标记是否丢字节,spillPath 指向保存完整流的文件。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess
DshEnvironment(受管环境)
DshEnvironment 是一次子进程执行中由 Harness 拥有的 DSH_* 变量集合,键受命名空间约束。实现先清除继承的 DSH_* 再合并这份快照。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess
SubprocessCollect(有界收集)
SubprocessCollect 是子进程输出的一种处置方式:在内存里按 maxBytes 有界收集,溢出保留尾部,并可选择把完整流写入带上限的 spill 文件。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess

来源