ByteNoteByteNote
DeepSeek Harness Bash 执行器设计解析
字

字节笔记本

2026年10月6日 · 约 8 分钟读完

DeepSeek Harness Bash 执行器设计解析

API中转
¥120

DeepSeek Harness(简称 dsh)是 DeepSeek 开源的智能体框架,主打"一切皆插件",由 Cordis 框架驱动,目前处于开发者预览阶段。对 agent 来说,执行 shell 命令是最高频也最危险的动作:既要让模型跑得动构建、测试与检索,又要管住超时、输出体积、环境变量泄漏和文件系统副作用。dsh 把这条链路收敛成一条 bash 执行服务接缝,本文基于它的子系统文档,拆解分层结构与背后的设计取舍。

一条接缝,三个角色

bash 执行接缝由三部分组成。服务定义在 dsh-shell 包里,以抽象服务 ctx.shell 的形式暴露,一个上下文只允许加载一个实现,重复加载会按 Cordis 的惯例直接抛错。服务提供方有两个:dsh-bash-local 负责本地直接执行,dsh-bash-sandbox 负责沙箱执行。消费方是 dsh-tool-bash,也就是暴露给模型的 bash 工具,由它把模型意图翻译成对 ctx.shell 的调用。

职责边界切得很干净:后台任务的 job id、所有权与生命周期控制不在本接缝内,它们属于通用的任务运行时接缝,dsh-tool-bash 只负责把进程句柄适配过去;进程组管理、有界收集器、spill 文件、凭据清洗这些原始机制,则封装在更底层的子进程服务之后。每一层只做自己的事。

请求与规格分离

接缝把面向模型与插件的请求(ShellExecRequest)和执行器真正使用的规格(ShellExecSpec)拆成两个类型。请求里只有 command 必填,workdir、timeoutMs、stdoutMaxBytes 都可以省略,由实现自己的配置补全并封顶;而规格里这些字段全部必填。两者之间由 ctx.shell.resolve(request) 显式转换,对应仓库"包边界处显式优于隐式"的规则。解析一旦完成,ShellExecSpec 携带的就是确定的工作目录、确定的超时和确定的输出预算,工具层拿不到任何含糊的默认值。

有几个字段只对受信任的进程内插件开放,面向模型的 bash 工具刻意不暴露。stdin 允许插件向命令的标准输入写一段字节再关闭,钩子桥就是用它把 hook 命令的 JSON 载荷喂进去的;模型如果需要 stdin,应该用 heredoc 或管道自己解决。env 允许插件附加普通环境变量,钩子桥会用它设置 CLAUDE_PROJECT_DIR、CLAUDE_PLUGIN_ROOT 之类的路径事实。stdoutMaxBytes 让受信任的消费方在有界预算内请求完整 stdout,同时不影响 stderr、后台任务和模型侧的常规输出上限。

受管环境变量的最后写入权

DSH_ 开头的变量被定义为框架所有的子进程事实。执行器在合并环境前会先剥离继承来的 DSH_ 名称,再合并调用方显式提供的 env,最后合并本次执行的 dshEnv 快照。写入顺序决定了安全性:任何调用方条目都抢不走受管事实,任何陈旧的继承值都活不过一次执行,当前事实不可用时也不会从框架进程继承到过期值。

ctx.shellEnv 是这些变量的注册表:内置事实由注册表自己持有,插件可以按执行粒度注册额外的可枚举事实,并随插件卸载自动注销;collect 会在每次模型 shell 调用前重建整个命名空间。本地执行器还会在合并之前做一次凭据清洗,避免 API 密钥顺着环境渗进子进程。

前台结果:正交事实独立报告

一次前台运行的返回值是 ShellRunResult,核心取舍是各正交结果独立成字段、绝不合并。exitCode 为 null 表示进程死于信号;signal 记录终止信号;timedOut 与 aborted 分别标记执行器超时和调用方取消是不是第一个打断命令的原因。二者互斥,因为超时与取消共享同一个 fused deadline,竞争时只报先发生的那一个。这样调用方永远不会把一次被提前打断的运行误读为正常成功,哪怕它恰好以退出码 0 收场。

stdout 与 stderr 各是一个 CollectedOutput:可能被截断的文本加恢复信息。截断时 text 字段保留的是尾部,完整流溢出到一个私有 spill 文件,需要全景时可以按路径去读。

沙箱事实与故障关闭

沙箱化运行会附带 ShellSandboxInfo:实际生效的 mode、是否拒绝了文件操作的 denied、所选 runner 对该模式的强制执行完整度 enforcement,以及标记 runner 本身失败的 runnerFailed。这些事实独立于退出状态上报,调用方因此能区分三种情况:命令自己失败、策略拒绝、沙箱基础设施故障。

受限模式没有可用后端,或选定 runner 拒绝其 profile 时,前台执行会抛出 SANDBOX_UNAVAILABLE,这是典型的故障关闭设计:宁可拒绝执行,也不悄悄裸跑。模型会在结果里看到拒绝与 runner 事实,且只有在拒绝标记指出生效模式时才得知该模式;它可以请求 sandbox_permissions 加 justification 做一次性的、严格更宽松的重试,而这次确切的调用必须先通过 ctx.approval 的批准。沙箱模式只管辖文件效果,每个调用会话可持久覆盖,工作目录一经确定不可变。

后台进程:无身份的句柄

start() 立即返回一个 ShellProcess 句柄,句柄上没有 id 也没有 owner,任务标识与生命周期由 dsh-tool-bash 适配到通用任务运行时之后统一管理。done 这个 Promise 在进程关闭时完成且从不 reject,连 spawn 失败也会安静地落定为 killed 状态并把错误写进 stderr;进程结束后缓冲输出仍可读。

readOutput() 是消费式的增量读取,连续调用绝不重复交付;一旦发生截断丢数据,会标记 lossy 并给出 stdout 与 stderr 的 spill 文件路径。kill() 杀掉整个进程组,幂等,进程已结束时返回 false,调用方可以放心重试。归属组合被销毁时,仍在运行的后台进程会被停止并等待完全停稳;而只重载执行器本身不会打断它们,这条边界落在子进程服务的销毁点上。

一条共享的退出状态约定

bash 与 pwsh 两个模型工具的渲染文本末尾都会追加形如 [exit code: N] 或 [killed by signal: X] 的标记。dsh-shell 导出的 parseExitStatus 负责逆解析,把渲染文本拆成 terminal 卡的输出正文与退出状态胶囊。约定集中在一处,渲染层就不必各自解析退出状态,两个工具的表现也保持一致。

值得借鉴的三件事

即使不用 dsh,这套设计里至少有三点可以直接搬走。第一,请求与规格分离:把默认值与上限的补全收拢到一个显式的 resolve 边界,执行器永远面对完全确定的输入。第二,结果的正交事实独立报告:超时、取消、信号、退出码互不吞并,配合 spill 文件解决大输出的有界读取。第三,信任分级:stdin、env、stdoutMaxBytes 只对受信任插件开放,模型永远走最窄的口子,受管环境变量靠写入顺序保证不可被覆盖。命令执行是 agent 框架里最容易失控的一段,dsh 用分层与显式性把它管住了,源码可在其 GitHub 仓库的 packages/shell 目录下查阅。

相关文章

分享: