DeepSeek Harness 源码精读 07:执行世界——手脚与耳目

第 07 章 执行世界:手脚与耳目

代码基线:commit 8f86b979a1(2026-10-01)| 0.2.0-rc.1 | 本系列讲次:M6

本章导读

这一章讲"手脚与耳目":模型伸出去的那五只手。

前面讲的都是机制,这一章开始一个个看能力。主角是五个执行类接缝、19 个包:bash 执行、持久终端、进程沙箱、语言服务器、文件系统。

先给一张体检表。每个接缝的 Provider 数量决定它是否真的可替换——ctx.shell 有多个 Provider,ctx.lsp 只有一个。前者是真正可替换的接缝,后者不是。这个数字比接缝的定义本身更能说明问题。

然后是三段值得学的设计。第一,ShellExecRequest → ShellExecSpec 两段式:默认值不藏在 run() 里写成 ?? default,而是在一个可单独测试、单独复用的 resolve(request): Spec 步骤里完成。做成类型上的两段式,这条规则就没法被违反。

第二,env 与 dshEnv 的三重保险:执行器先丢弃环境里的全部 DSH_*;dshEnv 最后合并,所以调用方顶不掉受管条目;键名类型化,拼错编译不过。设计意图写在注释里——"an unavailable current fact cannot inherit a stale value",取不到当前事实时宁可没有,也不能是错的。

第三,沙箱按平台先定候选链、探测只在候选多于一个时才仲裁;而 SandboxEnforcement 有两类来源——链长为一的 runner 读静态档案事实,经探测选中的读运行时报备。

学习要点

  • 数 Provider 的个数,比读接缝的定义更有价值
  • resolve(request): Spec 是"显式优于隐式"的标准模板
  • 模型能设什么、只有代码能设什么,在类型与注释里分层
  • 接缝之间会互相消费,Provider/Consumer 结构是有向图不是树
  • 沙箱的强制力是被报告的事实,而且要能回答"这个事实从哪来"

本章目标

  1. 拿到五个执行接缝的实测体检表:ctx 键、Definition、Provider 数、Consumer 数
  2. 读懂 ShellExecRequest → ShellExecSpec 这对拆分——AGENTS.md 说的"显式优于隐式"模板
  3. 掌握 env / dshEnv 的三重保险:受管事实不可能被陈旧值或调用方顶掉
  4. 理解 SandboxEnforcement 的两类来源:静态档案事实 vs 运行时报备

一、五个接缝的实测体检表

脚本第 1–5 项的输出(自己跑可复现):

接缝 ctx 键 Definition Provider Consumer
shell ctx.shell dsh-shell 4 4
subprocess ctx.subprocess dsh-subprocess 2 7 ⚠️ 原写 1/0,见 E-26
terminal ctx.terminals dsh-terminal 1 1
sandbox ctx.sandbox dsh-sandbox 2(含 sandbox-ssh) 2 ⚠️ 原写 0,见 E-26
lsp ctx.lsp dsh-lsp 1 1

五个 ctx 键书稿全部写对了(含 ctx.terminals 这个复数形式,容易记错但书稿没错)。


⚠️ 本节已作废(2026-10-03 M9 交叉验证)

M9 拿 docs/capability-seams.md(由 scripts/gen-doc-graphs.ts 生成的 94 行接缝权威表)回来交叉验证,本节的分类错了三处:

  1. tool-bash-persistent / tool-pwsh-persistent 不是 ctx.shell 的消费者 —— 实测 export const inject = ['tools', 'terminals'],它们消费的是 ctx.terminals。
  2. pwsh-sandbox 是替换者而非并列实现 —— 它自述 "Registers as ctx.shell in place of the local pwsh executor",生成表也只列三个实现(bash-local / bash-sandbox / pwsh-local)。
  3. ctx.subprocess 与 ctx.sandbox 的实现/消费者数全错 —— 见本章 §一表格与勘误附录 E-26。

本节保留原文是为了留痕。正确版本见 M9 讲义第五节与勘误附录 E-26。

二、★ shell 接缝:真实结构是"1 + 4 + 4 + 1"

书稿用 shell 举能力接缝的例子时,提的是 dsh-shell(定义)+ dsh-bash-local / dsh-bash-sandbox(两个 Provider)+ dsh-tool-bash(Consumer)。实测是 10 个包:

Service Definition : dsh-shell          Abstract bash executor seam (ctx.shell)  
Service Providers  : 4 个  
    · dsh-bash-local        Local-subprocess implementation  
    · dsh-bash-sandbox      Sandbox-consuming implementation  
    · dsh-pwsh-local        Local PowerShell implementation  
    · dsh-pwsh-sandbox      Sandbox-consuming PowerShell implementation  
Consumers          : 4 个  
    · dsh-tool-bash                 Model-facing bash tool  
    · dsh-tool-bash-persistent      owner-scoped persistent Bash tool (PTY)  
    · dsh-tool-pwsh                 Model-facing pwsh tool  
    · dsh-tool-pwsh-persistent      owner-scoped persistent PowerShell tool (PTY)  
横切              : dsh-shell-env      Tool-independent managed DSH_* shell environment registry  

两个维度相乘:{bash, pwsh} × {一次性, persistent} × {local, sandbox}。书稿 §6.2 提到 pwsh 兄弟实现是对的,但没展开这个乘法结构。

注意 persistent 变体走的是 PTY("backed by the Harness PTY"),也就是 ctx.terminals 那个接缝。接缝会互相消费:tool-bash-persistent 同时站在 shell 和 terminals 两个接缝上。


三、★ ShellExecRequest → ShellExecSpec:显式优于隐式的模板

AGENTS.md 有一条规则:"Explicit > implicit at package boundaries: defaulting is an explicit resolve(request): Spec step in the owning implementation, never a hidden ?? default inside run() (the dsh-shell request/spec split is the template)."

packages/shell/shell/src/types.ts:56-135 就是那个模板:

字段 Request Spec 说明
command 必填 必填
workdir ? 必填 由实现的 config 填
timeoutMs ? 必填 实现会封顶("implementations cap it")
onExpiry ? 必填 默认 'kill'
stdoutMaxBytes ? 必填 执行器的输出上限

Request 的文档注释一句话说清了定位:

A caller's execution REQUEST ... This is the model-/plugin-facing shape; pass it to resolve() to obtain a fully-resolved ShellExecSpec.

默认值不在 run() 里偷偷 ??,而是在一个可被单独测试、单独复用的 resolve 步骤里完成。 这条规则如果只在文档里,没人会遵守;把它做成一个类型上的两段式,就没法违反。


四、★ env 与 dshEnv:防止"陈旧受管值泄漏"的三重保险

这是本章最值得学的一段设计。两个环境变量入口,合并顺序被严格规定:

/** Ordinary environment entries for the command, merged after the credential  
 *  scrub. Managed facts belong in {@link dshEnv}, which merges after this map,  
 *  so an entry here can never displace one. */  
env?: Record<string, string>  
  
/** Harness-owned `DSH_*` variables for this execution ... Executors discard  
 *  ambient `DSH_*` entries before merging this snapshot last, so an unavailable  
 *  current fact cannot inherit a stale value from the harness process and a  
 *  caller {@link env} entry cannot displace a managed one. */  
dshEnv?: DshEnvironment  

三道保险:

  1. 执行器先丢弃环境中的全部 DSH_* —— 防止 harness 进程里的陈旧值渗进来
  2. dshEnv 最后合并 —— 调用方的 env 条目无法顶掉受管条目
  3. dshEnv 类型化到受管键(DshEnvironmentKey)—— 拼错键名编译不过

注释最后那两句把设计意图写死了:"an unavailable current fact cannot inherit a stale value" —— "取不到当前事实"时,宁可没有,也不能是错的。

这和你那份 开发大坑_settings.yaml一个空格炸全部模型路由 是同一个世界观:配置出错必须立刻炸,不许悄悄继承一个看起来合理的默认值。


五、★ 类型分层:模型能设什么,只有代码能设什么

ShellExecRequest 的三个字段注释都重复同一句话:"the model-facing bash tool does not expose it as a parameter"。

字段 模型可设? 谁在用 注释给的模型替代方案
command / workdir / timeoutMs / onExpiry ✅ 模型 —
stdin ❌ hooks 桥(写 JSON 载荷) "a model that needs stdin uses shell syntax like a heredoc or a pipe"
env ❌ hooks 桥(设 CLAUDE_PROJECT_DIR 等) —
stdoutMaxBytes ❌ 需要解析完整 stdout 的可信消费者 —

信任边界被编码进了类型和文档,而不是靠"模型应该不会乱传"。 而且对每个被拿走的参数都给了模型一条可用的替代路径(heredoc / pipe),而不是简单地不给。


六、沙箱:平台探测链与 enforcement 的两类来源

书稿 §6.5 提到的三平台后端,实测一字不差(packages/sandbox/sandbox-local/src/index.ts:160-167):

const PLATFORM_CHAINS: Record<string, readonly SelectedRunner['runner'][]> = {  
  linux: ['bwrap', 'landlock'],  
  darwin: ['seatbelt'],  
  win32: ['windows-acl'],  
}  

但注释里有一条书稿没写的重要规则:

selection is BY PLATFORM first, probes second: a platform's chain is probed in preference order only when it has MORE than one candidate (probing arbitrates; it does not re-validate a choice that has no alternative).

探测只在候选 > 1 时用于仲裁。darwin 和 win32 各只有一个候选,不做任何探测直接选中。

6.1 ★ enforcement 的两类来源

实测 STATIC_ENFORCEMENT(:178-184):

bwrap:          'full'  
landlock:       'full'  
seatbelt:       'full'  
'windows-acl':  'partial'  

但这张表的适用范围注释才是关键:

Enforcement completeness a rung claims when selected WITHOUT a probe (a chain of one) ... landlock is listed for the table's totality but is unreachable without a probe (the Linux chain has two rungs, so it is only ever selected through its probe, whose report is what distinguishes full from per-ABI-partial — and the launcher additionally self-reports partial enforcement on stderr at every confined run).

于是 SandboxEnforcement = 'full' | 'partial' 有两个来源:

来源 适用 例子
静态档案事实 链长为 1、无需探测的 runner bwrap / seatbelt / windows-acl
运行时报备 经探测选中的 runner landlock —— 由探测报告 + 启动器 stderr 自报共同决定

而 windows-acl 在静态表里就是 partial —— 书稿 §6.5 那句"对它'人人(Everyone)/硬链接边界'的环境缺口报告部分强制"完全属实。

这就是书稿 §6.5 那句"执行强制力是一个被报告的事实"在代码里的完整形态:不是布尔字段,而是"这个事实从哪来"也要能被回答。


七、★ 勘误:接缝之间会互相消费,Provider/Consumer 不是树

我的体检脚本按包名前缀判定角色,把 sandbox-policy(Per-call sandbox policy resolver and current model context)归到了"横切/其他",把 subprocess 标成"Consumer=0"。这两个判定都不够准确:

  • sandbox-policy 实际上消费 ctx.sandbox(它是策略解析器)
  • terminal-bash 是 ctx.subprocess 的 Consumer("Persistent shell PTY backend over the DeepSeek Harness subprocess seam")
  • bash-sandbox 同时是 ctx.shell 的 Provider 和 ctx.sandbox 的 Consumer

真实的依赖结构不是树,是有向图。 一个包可以同时是 A 接缝的 Provider 和 B 接缝的 Consumer;persistent 变体尤其明显。

书稿 §6.3–6.6 逐个讲接缝时隐含的是"每个接缝三件套各自独立"的读法——作为入门叙述可以,作为架构图会误导。这条要补。



本章实验(附录)

八、动手验证

export PATH="/opt/homebrew/bin:$PATH"  
cd /Users/ygs/ygs/deepseek-harness  
node --import tsx/esm "ygsdoc/学习笔记/labs/M6-seams-lab.ts"  

七组输出:①~⑤ 五个接缝的完整体检表(含每个包的 description);⑥ 角色分离度体检(★ 标记 Provider > 1 的真可替换接缝);⑦ 沙箱探测链与静态 enforcement 表。

角色分离度那一项的实测结果:

★ shell       Provider=4  Consumer=4  可替换  
  subprocess  Provider=1  Consumer=0  单一实现  
  terminal    Provider=1  Consumer=1  单一实现  
★ sandbox     Provider=2  Consumer=0  可替换  
  lsp         Provider=1  Consumer=1  单一实现  

M6 通过标准:能说出 landlock 的 enforcement 为什么不能从静态表读(因为它永远经探测选中,必须由运行时报备决定),以及 'windows-acl': 'partial' 这条静态记录意味着什么。


实验脚本:labs/M6-seams-lab.ts

运行(在仓库根目录):

export PATH="/opt/homebrew/bin:$PATH"  
cd /Users/ygs/ygs/deepseek-harness  
node --import tsx/esm "dsh-源码精读/labs/M6-seams-lab.ts"  

预期输出(本机实测,任何一行对不上就说明基线变了):

══════════════════════════════════════════════════════════════════════════  
══════════════════════════════════════════════════════════════════════════  
      · bash-local         Local-subprocess implementation of the DeepSeek Harness bash  
      · bash-sandbox       Sandbox-consuming implementation of the DeepSeek Harness bas  
      · pwsh-local         Local PowerShell implementation of the DeepSeek Harness bash  
      · pwsh-sandbox       Sandbox-consuming implementation of the DeepSeek Harness Pow  
      · tool-bash          Model-facing bash tool with optional generic background-job  
      · tool-bash-persistent Model-facing owner-scoped persistent Bash tool backed by the  
      · tool-pwsh          Model-facing pwsh tool over the bash executor seam  
      · tool-pwsh-persistent Model-facing owner-scoped persistent PowerShell tool backed  
      · shell-env          Tool-independent managed DSH_* shell environment registry  
══════════════════════════════════════════════════════════════════════════  
══════════════════════════════════════════════════════════════════════════  
      · subprocess-local   Local-subprocess implementation of the DeepSeek Harness subp  
      · win32-process      Shared low-level Win32 process, stdio, and Job Object primit  
══════════════════════════════════════════════════════════════════════════  
══════════════════════════════════════════════════════════════════════════  
      · terminal-bash      Persistent shell PTY backend over the DeepSeek Harness subpr  
      · tool-terminal      Six model-facing persistent PTY tools with owner isolation a  
══════════════════════════════════════════════════════════════════════════  
══════════════════════════════════════════════════════════════════════════  
      · sandbox-local      Local process-sandbox backends for the DeepSeek Harness sand  
      · sandbox-windows-acl Windows ACL write-restriction sandbox backend (restricted-to  
      · sandbox-policy     Per-call sandbox policy resolver and current model context:  
══════════════════════════════════════════════════════════════════════════  
══════════════════════════════════════════════════════════════════════════  
      · lsp-stdio          Generic stdio language-server provider for the DeepSeek Harn  
      · tool-lsp           Model-facing lsp tool over the DeepSeek Harness LSP capabili  
══════════════════════════════════════════════════════════════════════════  
══════════════════════════════════════════════════════════════════════════  
★ shell       Provider=4  Consumer=4  可替换  
★ sandbox     Provider=2  Consumer=0  可替换  
★ = 真正可换实现的接缝。shell 有 4 个 Provider(bash/pwsh × local/sandbox),是全场最完整的一个。  
══════════════════════════════════════════════════════════════════════════  
══════════════════════════════════════════════════════════════════════════  
    bwrap: 'full',  
    landlock: 'full',  
    seatbelt: 'full',  
    'windows-acl': 'partial',  
   …(完整输出见运行脚本)  

本章的勘误条目见 附录 A · 勘误总表。