【DSH】如何将 DeepSeek Harness 嵌入生产环境?万字解构 DeepSeek Agent Harness 的运行机制与安全边界

DeepSeek Harness 全景解剖:从第一次启动到"一切皆插件"的运行时组装

dsh web 看上去像一个启动网页的命令;真正值得追问的是:源码中为什么找不到一个不可替换的"Web Agent Core"?答案不在 Web UI,而在启动时构成的 Cordis plugin tree。UI 只是其中一个 Bundle 加入的表层,Agent、会话、模型适配、工具、Sandbox 与 Approval 也都以同样的方式进入树。

先把研究对象放对位置

模型会生成文本和 tool-call block;它不保存项目会话、不能自行打开文件,也不拥有操作系统权限。Agent Harness 是两者之间的承载运行时:保存任务事实、组装每次请求可见的 Context 与工具 schema、把调用送往真实执行环境,并在边界处施加策略。因而,浏览器不是 Harness 的定义;换成 Headless runner、SDK server 或 ACP transport,核心问题仍然相同。

在本基线中,dsh 是"profile launcher"。dsh web--profile web 的别名;dsh --profile headless "..." 则装配一条一次性运行的 Headless surface。这一结论来自 apps/cli/src/args.ts,并以 pnpm dsh --help 的实际输出验证。CLI 只消费自己的 profile、patch 与 dump 参数;它遇到未知 token 后,把余下参数原样交给已经装配出的 app plugin。这个分界让新 surface 自己拥有命令行,而不必修改 launcher。

四个层次,四个所有者

Cordis 是此仓库的组装框架:Plugin 在共享 ctx 上提供 service、订阅 typed event、注册 effect;plugin 卸载时注册随 effect 反向撤销。"一切皆插件"不是"所有文件都可选",而是没有一个通过修改 Loop 才能替换的特权核心。

实体 谁拥有它 回答的问题
Plugin 代码包 我注册/提供什么能力?
Bundle 可发布包的 dsh.bundle 我贡献哪一层 Cordis patch?
Profile $DSH_HOME/profiles/<name> 这次运行选择哪些 Bundle、安装哪些 out-of-tree 依赖?
Patch Profile、home 或命令行 对既有 row 做何种最终替换/插入?

最容易出错的是把 Bundle 当成 Profile。Bundle 是可分发的"贡献层";Profile 是用户启动的命名组合。二者都使用 package.json 中的 dsh 字段,但前者声明 patch 文件,后者声明有序 bundles 列表。Patch 也不是深度 merge:同一 id 的替换应重述该 row 所需的完整 config。这是安全而清晰的配置契约,不是 YAML 技巧。

Boot:从空树到可运行产品

apps/cli/src/profile-boot.ts 中的 composeProfile() 把 Bundle patch、profile patch、home patch 与 --patch overlays 依次组成 row;随后 boot() 在一个空 root config 上装载它。实际优先级为:
#mermaid-svg-P0x96Kl11s6EcN3I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P0x96Kl11s6EcN3I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P0x96Kl11s6EcN3I .error-icon{fill:#552222;}#mermaid-svg-P0x96Kl11s6EcN3I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P0x96Kl11s6EcN3I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P0x96Kl11s6EcN3I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I .marker.cross{stroke:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P0x96Kl11s6EcN3I p{margin:0;}#mermaid-svg-P0x96Kl11s6EcN3I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster-label text{fill:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster-label span{color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster-label span p{background-color:transparent;}#mermaid-svg-P0x96Kl11s6EcN3I .label text,#mermaid-svg-P0x96Kl11s6EcN3I span{fill:#333;color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .node rect,#mermaid-svg-P0x96Kl11s6EcN3I .node circle,#mermaid-svg-P0x96Kl11s6EcN3I .node ellipse,#mermaid-svg-P0x96Kl11s6EcN3I .node polygon,#mermaid-svg-P0x96Kl11s6EcN3I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .rough-node .label text,#mermaid-svg-P0x96Kl11s6EcN3I .node .label text,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape .label,#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape .label{text-anchor:middle;}#mermaid-svg-P0x96Kl11s6EcN3I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .rough-node .label,#mermaid-svg-P0x96Kl11s6EcN3I .node .label,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape .label,#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape .label{text-align:center;}#mermaid-svg-P0x96Kl11s6EcN3I .node.clickable{cursor:pointer;}#mermaid-svg-P0x96Kl11s6EcN3I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I .arrowheadPath{fill:#333333;}#mermaid-svg-P0x96Kl11s6EcN3I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-P0x96Kl11s6EcN3I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-P0x96Kl11s6EcN3I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P0x96Kl11s6EcN3I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P0x96Kl11s6EcN3I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P0x96Kl11s6EcN3I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-P0x96Kl11s6EcN3I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster text{fill:#333;}#mermaid-svg-P0x96Kl11s6EcN3I .cluster span{color:#333;}#mermaid-svg-P0x96Kl11s6EcN3I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-P0x96Kl11s6EcN3I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P0x96Kl11s6EcN3I rect.text{fill:none;stroke-width:0;}#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape p,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-P0x96Kl11s6EcN3I .icon-shape .label rect,#mermaid-svg-P0x96Kl11s6EcN3I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P0x96Kl11s6EcN3I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-P0x96Kl11s6EcN3I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-P0x96Kl11s6EcN3I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Profile bundles (listed order)
Profile cordis.patch.yml
$DSH_HOME/cordis.patch.yml
--patch overlays (argv order)
Effective Cordis plugin tree

后层可以替换早层的同 id row;命令行 patch 因此适合作为可复现实验的显式覆盖,而不是偷偷改安装文件。Profile boot 还将 launcher 的环境快照和 app arguments 作为服务提供给树,并监听两个用户 patch 文件以允许 config-only reload。它不理解某个 Agent 的行为。

我在 Linux x86_64 / Node 24.14.0 上执行:

sh 复制代码
DSH_HOME="$(mktemp -d)" pnpm dsh --profile headless --dump-default-config

命令以 0 退出,输出 333 行。其前部可直接看到 llmsessionagentsession-persistence-jsonlsubprocesssandboxsandbox-policyapprovalpermission 与工具 rows。这不是"Headless 少了运行时",而是 Headless Bundle 换掉了 surface。第二次以 --patch ../article-1-overlay.yml --dump-config 运行亦以 0 退出,且输出将 tool-web 标记为由该 overlay patch;这验证了层的可观察合成。

dsh web 实际启动什么

最准确的回答不是一个固定对象名称,而是"web Profile 的有效 plugin tree"。dsh-base 给出共同的基础层;dsh-web-app 加入 browser application;dsh-headless 则加入一次性 runner、没有 server。请把 "Web UI" 视作一个可替换 consumer,而不是 Agent Harness 的 owner。

这种设计有一个工程后果:要改变工具、模型、执行 provider 或 UI,先问它属于哪一个 row 和哪一个 service,再决定是在 profile、bundle 还是 patch 改动。修改 dsh web 的启动代码通常是最后选项。

可复现起点

源码路径要求 Node 满足根 package.json 的 engines,使用 pnpm。发布包与本次基线没有同一可核验 git head:registry 的当前包为 0.1.0-rc.6,而冻结源码为 rc.5;因此本文没有将前者的行为冒充为基线证据。本文环境中 node-pty 的原生构建因归档权限失败,故仅验证了不加载 PTY 的 CLI/dump 路径。真实 Web boot 还需要完成官方要求的 build artifacts。

下一篇不再讨论树怎样形成,而问一个更关键的问题:树已经装好后,为什么一句"运行测试"可以在一次 Turn 中发出多次模型请求、持久化多组事实,并最终触及真实环境?

DeepSeek Harness Runtime 深潜:一次 Agent 任务如何从模型推理走向真实世界

用户只说"修复测试",为什么不是一次 completion 就结束?因为 Harness 把"完成一个用户任务"和"发出一次模型请求"分成了 Turn 与 Step。这个区分不是命名偏好,而是日志、重试、工具执行与外部控制能否正确推理的边界。

从 Inbox 到 Turn,再到 Step

Agent 有一个 Inbox。followup() 写入 next-turn 并唤醒 driver;steer() 写入 next-step 并唤醒;inject() 也写入 next-step,但不自行唤醒。ReactLoopAgentagent-loop/src/agent.ts 先持久化 turn/start,原子地 claim 输入,再经过 agent/pre-step waterfall。该 hook 可以拒绝或重写进入模型的 message;因此一个已开始 Turn 可以不花任何 Step 便结束,日志仍能记录尝试。

一个 Step 是一次请求、一次 streaming completion 与其工具批次;同一 Turn 在工具结果或新的 next-step 输入出现时继续下一 Step。核心顺序如下:
Tools LLM Session log Agent Loop Tools LLM Session log Agent Loop #mermaid-svg-6BQ7wjn9D7yw7ciH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6BQ7wjn9D7yw7ciH .error-icon{fill:#552222;}#mermaid-svg-6BQ7wjn9D7yw7ciH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6BQ7wjn9D7yw7ciH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6BQ7wjn9D7yw7ciH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .marker.cross{stroke:#333333;}#mermaid-svg-6BQ7wjn9D7yw7ciH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6BQ7wjn9D7yw7ciH p{margin:0;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6BQ7wjn9D7yw7ciH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6BQ7wjn9D7yw7ciH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .sequenceNumber{fill:white;}#mermaid-svg-6BQ7wjn9D7yw7ciH #sequencenumber{fill:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-6BQ7wjn9D7yw7ciH .messageText{fill:#333;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6BQ7wjn9D7yw7ciH .labelText,#mermaid-svg-6BQ7wjn9D7yw7ciH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .loopText,#mermaid-svg-6BQ7wjn9D7yw7ciH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-6BQ7wjn9D7yw7ciH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-6BQ7wjn9D7yw7ciH .noteText,#mermaid-svg-6BQ7wjn9D7yw7ciH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-6BQ7wjn9D7yw7ciH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6BQ7wjn9D7yw7ciH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6BQ7wjn9D7yw7ciH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actorPopupMenu{position:absolute;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-6BQ7wjn9D7yw7ciH .actor-man circle,#mermaid-svg-6BQ7wjn9D7yw7ciH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-6BQ7wjn9D7yw7ciH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} turn/start, step/start, user/messageassembled requestassistant chunks / messageassistant/message, tool/callguarded execution pipelinefinalized resulttool/result, step/endnext Step when work is owedturn/end

仓库的 JSON-RPC Bash snapshot 给出可核验的具体轨迹:turn/start(1)step/start(1,1) 后,模型产生 bash call;tool/call 的 seq 63 在执行前记录,tool/result 的 seq 64 用 sourceEventSeqs:[63] 链接它;随后 step/end,同一 Turn 的 step/start(1,2) 再请求模型,最终 turn/end。这证明"一条任务 = 一个模型调用"是错误的运行时模型。该轨迹是当前测试 fixture 证据,而非本文环境向外部模型的实测。

SessionEvent 为什么是事实来源

Session 是 append-only log;deriveMessages() 只从带 surfaceOp 的 message-producing event 折叠出模型历史。chunk、turn boundary 这类 raw 事件留在日志中以服务重放/UI,却不会被误塞回 prompt。Loop 在请求前调用 this.session.deriveMessages();可选 invariant 又从 log 重建请求边界。由此得到仓库明确的不变量:Model-visible means logged

这带来一个很实际的设计约束:若你的 plugin 想向模型增加持久上下文,不应偷改内存 messages array;应定义并记录相应的 SessionEvent,再从日志投影。这样 resume、fork、transcript、telemetry 与 UI 都可以从同一事实流工作。反过来,实时状态应在 agent/* 事件表达,不能误称为会话事实。

Tool pipeline:日志先于副作用

工具并非模型直接调用 shell。Loop 先把模型生成的调用持久化,再让 ctx.tools 驱动 pipeline:
#mermaid-svg-MsnotyhtH0EGCnXd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MsnotyhtH0EGCnXd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MsnotyhtH0EGCnXd .error-icon{fill:#552222;}#mermaid-svg-MsnotyhtH0EGCnXd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MsnotyhtH0EGCnXd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MsnotyhtH0EGCnXd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd .marker.cross{stroke:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MsnotyhtH0EGCnXd p{margin:0;}#mermaid-svg-MsnotyhtH0EGCnXd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster-label text{fill:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster-label span{color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster-label span p{background-color:transparent;}#mermaid-svg-MsnotyhtH0EGCnXd .label text,#mermaid-svg-MsnotyhtH0EGCnXd span{fill:#333;color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .node rect,#mermaid-svg-MsnotyhtH0EGCnXd .node circle,#mermaid-svg-MsnotyhtH0EGCnXd .node ellipse,#mermaid-svg-MsnotyhtH0EGCnXd .node polygon,#mermaid-svg-MsnotyhtH0EGCnXd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .rough-node .label text,#mermaid-svg-MsnotyhtH0EGCnXd .node .label text,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape .label,#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape .label{text-anchor:middle;}#mermaid-svg-MsnotyhtH0EGCnXd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .rough-node .label,#mermaid-svg-MsnotyhtH0EGCnXd .node .label,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape .label,#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape .label{text-align:center;}#mermaid-svg-MsnotyhtH0EGCnXd .node.clickable{cursor:pointer;}#mermaid-svg-MsnotyhtH0EGCnXd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd .arrowheadPath{fill:#333333;}#mermaid-svg-MsnotyhtH0EGCnXd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MsnotyhtH0EGCnXd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MsnotyhtH0EGCnXd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MsnotyhtH0EGCnXd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MsnotyhtH0EGCnXd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MsnotyhtH0EGCnXd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MsnotyhtH0EGCnXd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster text{fill:#333;}#mermaid-svg-MsnotyhtH0EGCnXd .cluster span{color:#333;}#mermaid-svg-MsnotyhtH0EGCnXd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MsnotyhtH0EGCnXd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MsnotyhtH0EGCnXd rect.text{fill:none;stroke-width:0;}#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape p,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MsnotyhtH0EGCnXd .icon-shape .label rect,#mermaid-svg-MsnotyhtH0EGCnXd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MsnotyhtH0EGCnXd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MsnotyhtH0EGCnXd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MsnotyhtH0EGCnXd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} assistant tool-call block
persist tool/call
tools/pre-execute waterfall
monotonic guards
tools/execute waterfall + body
tools/post-execute waterfall
finalizeContent + tools/result
persist tool/result

tools/pre-executetools/executetools/post-execute 都是 waterfall:listener 必须以 next() 委托后续链,否则它是在有意短路。pre 可 allow/deny/ask;随后 registered guard 只能收紧、不能放宽既有 owner policy;execute 可包裹调度(timeout、metrics 等);post 可接受、阻断、替换输出或添加 context。最终 finalizeContent 强制 content-only 合约,tools/result 只观察冻结的权威结果。335 项 CLI/Agent Loop 测试在本环境通过,其中 tool invariant 明确检查这三个阶段的次序。

Capability Seam 与 Execution World

所谓 Capability Seam 不是"有一个 interface"就成立。完整的 seam 有三角:Definition 声明 ctx service 合约;Provider 实现它;Consumer(常见为模型可见 Tool)只依赖合约。文件系统、subprocess、sandbox、shell/PTY 的关键含义在于:多个 consumer 可共享一个 execution world。替换 fs/subprocess provider 为远端 sandbox,并不自动迁移 session 或模型;它只移动由该 provider 承担的文件/进程操作。

这也是为何"新增能力"要先问:谁定义、谁提供、谁消费、什么会持久化?只写一个 Tool body 而没有稳定 provider 边界,往往是在制造不可替换的耦合。

安全边界不能合并成一个词

控制什么 不控制什么
Tool exposed to model 模型能请求哪些能力 请求是否批准、OS 是否允许
Permission policy 预设把 mode 映射到策略 它不是内核隔离
Approval 某次 ask 的人类决定 不是长期 OS capability
Sandbox provider 对文件/进程的约束 当前 SandboxMode 不表示网络/进程政策
OS / credentials 进程实际拥有的权限与密钥 Harness 不能凭空收回宿主已授予的一切

默认 headless config 在本次 dump 中将 workspace-write 配为 sandbox workspace-write + approval askdanger-full-access 配为 approval never。这描述 composition,而不是"有 Sandbox 就安全"。Python minimal example 明确使用 danger-full-access,只应在一次性 checkout/container 中运行。

本地没有 API credential,故真实模型 + tool 的端到端路径未 runtime-verified;不要把 snapshot 或源码推断写成"实测模型"。但执行顺序、日志事实与 hook 合约由架构文档、实现、fixture 与通过的测试共同覆盖。

下一篇把这些稳定边界变成行动准则:新增一个能力时何时写 Plugin、何时写 Adapter,以及怎样把同一个 runtime 放进脚本、服务或编辑器生态。

从理解到掌控:扩展 DeepSeek Harness,并将 Agent Runtime 嵌入真实工程系统

如果要增加一个模型工具,为什么不直接在 Agent Loop 的 if 分支里接入?因为 Loop 的职责是驱动 Turn/Step;把某个业务能力塞进去,会让每一个 product composition 都承受它的依赖、权限与失败语义。仓库的边界更严格:新行为通过 Plugin、service、event 或 Tool registry 进入;Loop 不为它改形状。

先选择扩展点

需求 首选机制 不应误用为
新模型可见能力 Tool Plugin 注册到 ctx.tools 修改 Agent Loop
新文件/进程实现 Capability Provider 复制每个 Tool
拦截一次 tool call tools/* waterfall / guard 在 Tool body 中硬编码全局政策
新 provider 协议 LLM Adapter 仅填一个 provider 名称
新可安装产品层 Bundle Profile 本身
非交互运行 Headless / SDK server Web UI 自动化

一个最小 plugin 只是导出 apply(ctx);Cordis 在加载时调用它。注册返回的是 effect,plugin unload 后相应注册撤销。这是 out-of-tree code 可以与 in-tree package 同等参与树的原因。生产插件不应只 console.log:它至少要明确 inject 依赖、注册何种能力、失败时是否阻断调用、以及是否需要 durable event。

Tool:把业务副作用放进已存在的通道

官方 adding-a-tool 指南要求通过 defineTool/ctx.tools 注册 definition。definition 提供可供模型 schema 化的参数、execute 的 canonical value,以及 model-facing output.render;UI card 则是独立的纯 presentation projection。不要在 presenter 中读文件、取时间或做 I/O:它还会在 Session log replay 中运行。

ts 复制代码
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'project-metadata-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'project_metadata',
    description: 'Return controlled project metadata.',
    // Parameter/output schemas must follow the baseline tool cookbook.
    async execute(_args, _signal) {
      return { value: { revision: 'replace-with-real-provider' } }
    },
  }))
}

这是结构示意,不能原样当成可运行文件:当前版本的 schema 与 return contract 需从 docs/cookbook/adding-a-tool.md 复制完整定义,并为输入、拒绝、执行错误与输出固定测试。真正的 non-toy case 是"受控项目元数据":Tool 只消费一个注入的 metadata provider,权限/approval 仍在通用 pipeline,持久化仍由 Loop 的 tool/call/tool/result 完成。这样没有新增一个绕开审计的直接 shell 通道。

Bundle 是分发,Profile 是选择

当 plugin 要交给别人安装,写 Bundle:包内 dsh.bundle 指向 cordis.patch.yml,该 patch insert plugin rows。用户再以 dsh plugin --profile demo add <package> 将 Bundle 放入该 Profile 的有序列表。Profile 仍可用自己的 cordis.patch.yml 覆盖 Bundle,home 与 --patch 层仍在其上。Git 安装与 npm 包不同:Git 安装的是 source,若其 prepare script 获允会在用户机器执行代码;应只信任源码并 pin commit,或分发已构建 tarball/npm artifact。

Provider config 不等于 LLM Adapter

"换模型"有两种完全不同的改动。若已注册 Adapter 认识新 provider route/model id,只需要 composition 中的 provider/model/credential config。若需把 Harness 的 GenerateOptions 翻译为一种新上游协议,则实现 LlmAdapter.stream(),将 provider response 还原为严格 StreamChunk 序列,再调用 ctx.llm.registerAdapter()。Adapter 必须遵守 usage 在 finish 前、finish 后无 chunk、tool arguments 保持 raw JSON、错误转为稳定 code、传播 abort signal 等契约。只改 DEEPSEEK_BASE_URL 并不会实现另一种 streaming dialect。

选择嵌入边界

#mermaid-svg-G072Bm0mR3XVRmPD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G072Bm0mR3XVRmPD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G072Bm0mR3XVRmPD .error-icon{fill:#552222;}#mermaid-svg-G072Bm0mR3XVRmPD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G072Bm0mR3XVRmPD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G072Bm0mR3XVRmPD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD .marker.cross{stroke:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G072Bm0mR3XVRmPD p{margin:0;}#mermaid-svg-G072Bm0mR3XVRmPD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster-label text{fill:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster-label span{color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster-label span p{background-color:transparent;}#mermaid-svg-G072Bm0mR3XVRmPD .label text,#mermaid-svg-G072Bm0mR3XVRmPD span{fill:#333;color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .node rect,#mermaid-svg-G072Bm0mR3XVRmPD .node circle,#mermaid-svg-G072Bm0mR3XVRmPD .node ellipse,#mermaid-svg-G072Bm0mR3XVRmPD .node polygon,#mermaid-svg-G072Bm0mR3XVRmPD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .rough-node .label text,#mermaid-svg-G072Bm0mR3XVRmPD .node .label text,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape .label,#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape .label{text-anchor:middle;}#mermaid-svg-G072Bm0mR3XVRmPD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .rough-node .label,#mermaid-svg-G072Bm0mR3XVRmPD .node .label,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape .label,#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape .label{text-align:center;}#mermaid-svg-G072Bm0mR3XVRmPD .node.clickable{cursor:pointer;}#mermaid-svg-G072Bm0mR3XVRmPD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD .arrowheadPath{fill:#333333;}#mermaid-svg-G072Bm0mR3XVRmPD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G072Bm0mR3XVRmPD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G072Bm0mR3XVRmPD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G072Bm0mR3XVRmPD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G072Bm0mR3XVRmPD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G072Bm0mR3XVRmPD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G072Bm0mR3XVRmPD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster text{fill:#333;}#mermaid-svg-G072Bm0mR3XVRmPD .cluster span{color:#333;}#mermaid-svg-G072Bm0mR3XVRmPD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G072Bm0mR3XVRmPD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G072Bm0mR3XVRmPD rect.text{fill:none;stroke-width:0;}#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape p,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G072Bm0mR3XVRmPD .icon-shape .label rect,#mermaid-svg-G072Bm0mR3XVRmPD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G072Bm0mR3XVRmPD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G072Bm0mR3XVRmPD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G072Bm0mR3XVRmPD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 谁驱动 runtime?
CLI one-shot
Own process
External agent client
Web client business call
dsh --profile headless
TypeScript/Python SDK over stdio JSON-RPC
ACP server
Typert API Gateway

  • Headlessdsh --profile headless "job" 创建 fresh persisted session、打印最终文本并退出;适合 shell automation。
  • SDK JSON-RPC :TypeScript DeepSeekHarness 或 Python DeepSeekHarness 管理一个子进程。客户端只驱动 runtime;cordis.yml 仍拥有组成。Python SDK 文档要求 Python 3.10+、受支持平台与可修改的隔离 workspace;其 bundled runtime 不要求系统 Node。
  • ACP:面向 Agent Client Protocol 的 automation server。它创建 fresh agents,走 stdin/stdout JSON-RPC,只发 committed assistant text,并可处理 bridge-owned one-shot permission request;不等于 UI、transcript 或 editor protocol。
  • API Gateway :Web host/client 的 unary Remote service 层。@Remote/@RemoteScope 描述可跨 connection /api 调用的方法;session event stream 不应伪装为 Remote unary call。

这四条路可并存:它们是入口/transport,不是对同一 capability 的重复实现。SDK 进程关闭、ACP client disconnect 与 Cordis unload 都必须归结到明确的 agent/session dispose 生命周期。

上线前的最小检查

  1. 指出 capability 的 Definition、Provider、Consumer 与 owner;不要只贴 Tool 名字。
  2. 为模型可见或可重放事实设计 SessionEvent,而非临时数组。
  3. 明确 Tool exposure、policy、approval、sandbox、OS/credential 的五层责任。
  4. 固定 cordis.yml、plugin 版本、cwd、credential 来源与日志目录;危险 composition 只在 disposable 环境运行。
  5. 用真实或 mock provider 跑 tool case,读取 JSONL 的 call/result/Step;没有 credential 时只报告 source/test verification。

在本研究容器中,本地 out-of-tree plugin 的真实长驻 boot 和 Python SDK 未完成 runtime verification:前者受 source build/PTY 原生依赖限制,后者还需要 credential 与 bundled wheel。本文不以教程文本替代实验。已经验证的部分是 CLI composition dump 以及 335 项核心测试;其余结论按照文档/源码等级陈述。

读到这里,面对陌生 package 的正确起手式不是"它是不是核心",而是:它定义什么 capability?谁提供?谁消费?何时运行?什么持久化?哪些 hook 能替换它?失败落在哪里?这正是把 Harness 从"能运行"变成"能嵌入、能修改"的方法。

相关推荐
用户9210802628644 分钟前
4. 前端地图大量点位实时更新优化:后端推增量,前端做 diff
前端
SoaringHeart1 小时前
Flutter最佳实践:Web项目中文字体首次加载乱码问题解决
前端·flutter
明航咨询_贾老师1 小时前
CISP-AISE知识体系大纲深度解读:6大模块,国内首个AI安全应用官方认证
人工智能·安全
西安小哥2 小时前
破局与重生:大厂前端如何借力 AI 转型“超级全栈“
前端·人工智能
sunly_2 小时前
TypeScript总结:16、面向对象速查
前端·javascript·typescript
40岁资深老架构师尼恩2 小时前
工业级 AI问数 AST语法 + 知识图谱语义+ 质量规则 三层安全校验 架构设计与实现
人工智能·安全·知识图谱
张元清2 小时前
React useInterval Hook:没有过期闭包的 setInterval (2026)
javascript·react.js
MXN_小南学前端2 小时前
React超长文本域中实现“返回顶部”浮动按钮
前端·javascript·react.js
学习嵌入式的小周2 小时前
Notepad++8.8.7下载安装(附安装包)
前端·notepad++