第 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 结构是有向图不是树
- 沙箱的强制力是被报告的事实,而且要能回答"这个事实从哪来"
本章目标
- 拿到五个执行接缝的实测体检表:ctx 键、Definition、Provider 数、Consumer 数
- 读懂
ShellExecRequest→ShellExecSpec这对拆分——AGENTS.md 说的"显式优于隐式"模板 - 掌握
env/dshEnv的三重保险:受管事实不可能被陈旧值或调用方顶掉 - 理解
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 行接缝权威表)回来交叉验证,本节的分类错了三处:
tool-bash-persistent/tool-pwsh-persistent不是ctx.shell的消费者 —— 实测export const inject = ['tools', 'terminals'],它们消费的是ctx.terminals。pwsh-sandbox是替换者而非并列实现 —— 它自述 "Registers asctx.shellin place of the local pwsh executor",生成表也只列三个实现(bash-local/bash-sandbox/pwsh-local)。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-resolvedShellExecSpec.
默认值不在 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
三道保险:
- 执行器先丢弃环境中的全部
DSH_*—— 防止 harness 进程里的陈旧值渗进来 dshEnv最后合并 —— 调用方的env条目无法顶掉受管条目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) ...
landlockis 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 · 勘误总表。