
你在哪运行示例的第 8、10、11 步——真正跟外部世界打交道的那一层。这是最后一个深度篇。读完你会知道接缝的三个角色为什么缺一不可、一次 provider 替换如何把 Bash/PTY/LSP 一起搬到远程、SandboxMode三档策略与部分执行这个诚实的事实、审批为什么是 fail-closed以及子代理接缝如何把 Claude Code 和 Codex 变成可调用的孩子。缩写对照表缩写英文全称中文PTYPseudo Terminal伪终端LSPLanguage Server Protocol语言服务器协议ACLAccess Control List访问控制列表ABIApplication Binary Interface应用二进制接口CIContinuous Integration持续集成SDKSoftware Development Kit软件开发工具包APIApplication Programming Interface应用程序编程接口一、角色回顾拥有真正跟外部世界打交道的能力——读写文件、跑命令、关进沙箱、问用户、派子代理。刻意不做不互相耦合。在示例中出场第 8 步文件、第 10 步审批、第 11 步命令 沙箱。二、什么才算一个接缝术语表的定义非常严格“seam— aswappable capabilitywith three roles: aService Definition拥有ctx.key和词汇类型的 Cordis Service抽象类或具体注册表绝不是 TypeScriptinterface, one or moreService Providers, and one or moreConsumers.”“The seam is the complete capability, never one role; reserve the term for that meaning.”三个角色缺一个就不是接缝。仓库给的标准示例是packages/shell角色包服务定义dsh-shell提供者dsh-bash-local、dsh-bash-sandbox消费者dsh-tool-bash服务定义dsh-shell拥有 ctx.shell与词汇类型提供者dsh-bash-local提供者dsh-bash-sandbox提供者远程执行你写的消费者dsh-tool-bash面向模型的工具角色通常各占一个包因为它们独立演化但当它们是同一件关切时也可以合并——dsh-llm就同时拥有服务定义和消费者第 9 篇。“Adding a capability means designingall three.”这条对想扩展 DSH 的人是最实用的一句话别只写一个实现先想清楚定义和消费者是谁。三、接缝的复利一次替换整片搬家架构文档里最有说服力的一段“Seams are why one provider swap changes the whole product.Filesystem and subprocess providers share one execution world, so pointing them at a remote sandboxmoves Bash, PTY, and LSP with them, with no provider forks.”换两个 provider一行代码不改跟着一起搬的消费者Bash 工具PTY 持久终端LSP 语言服务远程执行世界ctx.fs → 远程文件同一个世界ctx.subprocess→ 远程 spawn本机执行世界ctx.fs → 本机文件同一个世界ctx.subprocess→ 本机 spawn这张图回答的问题为什么把 agent 的执行环境挪到远程在 DSH 里不是一个大工程。之所以能这样是因为 Bash、PTY、LSP 三个消费者都不认识本机这个概念——它们只认识ctx.fs和ctx.subprocess。四、沙箱三档策略和一个诚实的事实ctx.sandbox的职责很窄把一个同世界子进程的 argv 包进一层文件效果策略里而不让消费者耦合到平台运行器。出厂后端dsh-sandbox-local覆盖三个平台Linux 的 bwrap/Landlock、macOS 的 Seatbelt、Windows 的 ACL 受限令牌。三档模式typeSandboxModeread-only|workspace-write|danger-full-access模式含义read-only只允许必需的 sinkPOSIX 运行器额外放开/dev/null因为 shell 需要它workspace-write还允许写工作区根目录和后端承诺的临时区danger-full-access绕过限制——注意这个模式的消费者根本不调用ctx.sandbox它直接 spawn 原始 argv一个重要的边界声明“Network and process visibility are outside this vocabulary.”沙箱只管文件效果不管网络和进程可见性。说清楚自己不管什么比含糊地暗示我很安全要负责得多。部分执行是一个被上报的事实typeSandboxEnforcementfull|partial“partialmeans an active backend or older kernel ABI cannot govern every promised file effect;callers requiring an absolute boundary must not treat it asfull.”老的 Landlock ABI、Windows ACL 运行器的 Everyone/硬链接边界都是当前的 partial 情形。这是很难得的诚实不是我们支持沙箱而是在这台机器上我能保证到什么程度。策略是按调用携带的不是钉在 provider 上interfaceSandboxExecutionPolicy{mode:SandboxMode workspaceRoot:string// workspace-write 可写的绝对根目录sessionId?:SessionId// 后端按它给每个会话分私有临时目录}“carriedPER CALL, not fixed on the provider: two consumers may confine under different policies at the same instant (bash underread-onlywhile a confined child agent needs its state directory writable), and an approved escalated retry is a new call with a wider policy.”同一瞬间bash 可以是只读而一个受限子代理需要它的状态目录可写。并发会话、不同消费者、一次性提权重试可以向同一个 provider 要不同的边界而不需要修改 provider 状态。还有一个容易被忽略的细节workspaceRoot先按文件系统语义规范化解 symlink再做词法规范化——所以一个含symlink/..的 cwd 标识的是进程真正运行的那个目录而不是字符串拼出来的目录。这是一类经典越权的堵法。五、审批fail-closed 的三态ctx.approval回答一个问题这次具体的动作能不能进行typeApprovalOutcomeallowed-once|rejected|cancelled|unavailabletypeApprovalPolicyask|never四条规则只有allowed-once才放行而且它只授权被问到的那个动作——不是这类动作以后都行。unavailable按拒绝处理。缺失的、不拥有的、抛异常的、不符合形状的答复者一律变成unavailable而不是打开闸门。never策略是确定性的拒绝——不派发任何答复者。这是严格无人值守姿态CI、后台运行也是不问就知道结果的那个策略。策略的有效值来自会话日志里最后一条approval/policy事件回退到服务配置。setApprovalPolicy(session, policy)是唯一写入路径——所以重放能重建这个覆盖第 6 篇的不变量。审批请求故意不带工具参数“an answerer attaches the prompt to the already-streamed tool call throughcallIdinstead ofrendering a second copy that could drift.”权限预设把两个旋钮捆成一个选择器沙箱模式和审批策略是两个独立旋钮。ctx.permissionPresets把它们捆成用户看得懂的一个选择预设沙箱审批workspace-writeworkspace-writeaskdanger-full-accessdanger-full-accessnever这一层自己不做任何强制——它只记录意图然后通过每个旋钮各自的规范写入器写下去。执行、提示叙述、重放仍然各读各的旋钮。当前值也是推导出来的折叠出会话的有效沙箱模式和审批策略匹配表里的项都不匹配就返回派生的customcustom只能显示永远不能作为切换目标。配置错误在插件加载时就失败表里有叫custom的项 → 抛错组合在一个不限制的 bash 执行器之上 → 抛错因为预设是要捆绑沙箱模式的。六、子代理把别家产品变成孩子ctx.subagents是接缝多样性最极端的例子“Subagent providersvary just as widely behind one interface, from a fresh child agent to a delegated turn in another product.”出厂的进程内后端有两个subagent-spawn-in-process、subagent-fork-in-process而标准模式 preset 里还有两行-id:tool-subagent-codexname:deepseek-ai/dsh-tool-subagent-id:tool-subagent-claude-codename:deepseek-ai/dsh-tool-subagent同一个工具包不同配置就把 Codex 和 Claude Code 变成了可以派活的子代理。这两个 provider 刻意不在dsh-base的生产依赖闭包里——需要的 profile 单独装对应的 bundle所以默认安装不会拖进 Claude Agent SDK 和 Codex 的平台负载。子代理还有几个值得知道的概念lineage世系父子关系是数据parentSession、持久的delegationDepth、运行时的subagentDepth永远不影响可见性。作用域是两层且扁平的——作用域注册不向下继承给子代理子树行为用世系数据表达。孩子怎么拿到工具通过composeFrom()绑定父的那次 preset 挂载第 5 篇。文档有一句很硬的话“a child that joins nothing reaches the model withno tools at all”——因为所有面向模型的行都在 agent 平面全局层是空的。Ralph loop一个前台的全新 agent 反复尝试同一个不可变目标的工作流。每一轮是一个全新的子会话不带父或前一轮的对话种子跨轮状态靠共享工作区加一份有界的结构化交接报告状态、摘要、证据、下一步、阻塞原因传递。七、失败行为出什么事怎么办没有审批答复者unavailable→ 拒绝审批策略是never确定性rejected不派发答复者沙箱后端只能部分强制上报partial需要绝对边界的调用方必须区别对待消费者要danger-full-access根本不调ctx.sandbox直接 spawn 原始 argv同时挂了fs-sandbox和dsh-fs-local双重注册ctx.fs加载失败Windows 上把 bash 栈恢复回来但没禁用 pwsh 栈两个执行器家族注册同一个bash服务 →加载时大声失败权限预设表里出现custom插件加载时抛错保留名倒数第二条特别典型不完整的配置改法会在加载时炸掉而不是在运行时给你一个诡异的行为。⚓ 回到示例第 8 步读package.jsonread_file工具是消费者它调ctx.fs。ctx.fs在你这个部署里由fs-sandbox提供dsh-base的默认组合它在本机文件之上加了一层写入围栏。读操作直接过。第 10–11 步pnpm lint是完整的一条链ctx.subprocessctx.sandbox(sandbox-local)ctx.sandboxPolicyctx.shell(bash-sandbox)Web UI 答复者ctx.approvalctx.toolsctx.subprocessctx.sandbox(sandbox-local)ctx.sandboxPolicyctx.shell(bash-sandbox)Web UI 答复者ctx.approvalctx.tools会话策略是 askrequest(callId, ...)派发给答复者allowed-onceallowed-once执行 pnpm lintresolve(session)workspace-write 工作区根目录把 argv 包一层受限 argv 强制程度 full/partialspawn退出码 输出结果这张图回答的问题一次跑个命令到底穿过了几个可替换的接缝。几个细节策略解析在消费者边界不在 provider 里。ctx.sandboxPolicy.resolve()拥有优先级和根目录回退逻辑“所以 bash 和 fs 不用各自重复一遍”。工作区根目录来自会话不可变的 cwd正常工具调用无 agent 的调用才回退到部署配置。假如你在弹窗里选了这次拒绝链条在第 4 行就断了——rejected不是allowed-once工具调用以拒绝结束模型会看到一个失败结果并且知道是被拒绝了可以据此改口。把这条链换掉是这一篇的落脚点。想让pnpm lint跑在公司的远程执行环境里换ctx.fs和ctx.subprocess两个 provider —— bash、持久终端、LSP一起搬过去工具代码、审批逻辑、日志格式一行都不用改。这就是一切皆插件这句口号在一次真实的 lint 命令上的兑现方式。上一篇← 10 · 工具注册表与执行管线下一篇→ 12 · 完整重演一句话请求的完整旅程回到→ 系列索引 返回专栏目录