十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HoRain云--DeepSeek Harness 工具执行流水线与权限门禁

HoRain云--DeepSeek Harness 工具执行流水线与权限门禁 但现在有个疑问模型发出一条工具调用之后到工具真正执行、结果回到模型中间到底走了哪些环节本章节我们将讲清楚 dsh 的「工具执行流水线」以及如何用钩子插件实现权限门禁。一句话一次工具调用会按固定顺序经过多个环节每个环节负责一类策略钩子插件可以在其中允许、拒绝或询问。流水线的整体顺序工具执行不是「调用一下函数」那么简单而是一条有固定顺序的流水线。官方文档把顺序概括为tools/pre-execute → 单调守卫 → tools/execute → tools/post-execute → finalizeContent → tools/result。其中前三个 waterfall 可以改写一次调用而由定义自身控制的 finalizeContent 与 tools/result 在其后运行。先看两个容易混淆的概念waterfall瀑布式事件是一种事件分发模式监听器可以调用next()把决定权委托下去也可以直接返回一个决策短路。单调守卫monotonic guard是一道只允许缩减、不允许撤销的最终防线。下面逐个环节拆开讲。tools/pre-execute可重排的策略层tools/pre-execute是流水线中第一个 waterfall负责承载「钩子、权限、沙箱」这一类可重排的策略。它之所以叫「可重排」是因为监听器可以通过next()把决定权传给下一个监听器多个策略插件的先后顺序可以在配置里调整。这个 waterfall 返回一个类型化决策 PreToolDecision有三种取值决策含义后续行为{ kind: allow }放行这次调用继续走单调守卫与之后的环节{ kind: deny; reason: string }拒绝这次调用物化成一个错误结果工具主体被跳过{ kind: ask; reason?: string }询问用户只有审批服务返回allowed-once才继续否则拒绝注意ask分支它会触发ctx.approval的一次性询问这一块留到下一篇展开。参数不可被改写因为历史记录、审计、UI 与执行必须保持一致。什么时候用 pre-execute当策略需要「允许、拒绝或询问」三类动作之一且希望策略之间可以自由排序时用它。沙箱、权限、plan-mode 等插件都用这个扩展点。单调守卫不可撤销的最终拒绝waterfall 的缺点是后注册的监听器可以推翻前面监听器的决策。当某个不变式invariant需要「最终拒绝、且任何人都不能撤销」时就要用ctx.tools.guard()。守卫的类型是 ToolGuard它故意没有 allow 结果返回字符串表示拒绝返回undefined表示维持现状。// ToolGuard感知作用域的最终预分派策略 type ToolGuard (execution: ReadonlyToolExecution) string | undefined因为守卫没有 allow 结果所以监听器的顺序永远无法把一次拒绝变回允许。这就是「单调」的含义只减不增只收权限不给权限。实践原则可重排的策略放tools/pre-execute必须「最终生效、不可撤销」的不变式放ctx.tools.guard()。tools/execute 与 tools/post-executetools/execute负责「环绕分派」也就是把真正调用工具主体这件事包起来。超时、重试、指标收集都在这一层做它包装的是实际分发生命周期。它拿到的视图是 ToolDispatchExecution只有这一个视图可以替换必需的exec.signal用来施加截止时间。替换规则是可以替换但不能移除注册表会在调用工具主体前重新融合调用方的 signal。tools/post-execute负责在工具执行完、结果归一化之前做检查或改写它返回 PostToolDecision决策含义{ kind: accept; content? }接受结果可替换展示内容保留规范值与元数据{ kind: accept; value }接受结果可替换规范值会重新校验并重算内容{ kind: block; feedback }阻止结果把纠正反馈变成错误结果内容替换是展示策略不是保密策略。要隐藏程序化值必须替换该值或阻止结果。finalizeContent 与 tools/resultfinalizeContent是工具定义ToolDefinition自己拥有的回调注册表恰好调用它一次。它是「最后的仅内容不变式」同步执行只允许做内容层面的最后修正。在这之后注册表会物化并冻结已接受的结果然后触发tools/result。tools/result是同步通知用来观测冻结的、不可变的权威结果。观测者无法变换结果观测者的失败也会被隔离不会影响主流程。需要「审计、指标、捕获最终结果」时用它需要「变换结果或附加上下文」时才用tools/post-execute。选型记忆法pre-execute 决定「能不能做」execute 决定「怎么做」post-execute 决定「结果怎么呈现」result 只负责「看一眼最终结果」。动手示例写一个权限门禁插件官方文档以「权限门禁」为例展示钩子插件如何使用tools/pre-execute。钩子插件就是普通的 Cordis 插件并不需要外部协议。实例// 文件路径my-plugins/permission-gate/src/index.ts// 一个基于 tools/pre-execute 的权限门禁插件。// 它返回类型化的决策命中黑名单就 deny否则调用 next() 委托下去。import type { Context } from deepseek-ai/cordisimport type { PreToolDecision, ToolExecution } from deepseek-ai/dsh-tools// 黑名单runoob 项目里禁止直接写文件系统的工具。// 这里用最简单的集合演示真实项目里可以查数据库、问审批服务。const DENY_TOOLS new Set([fs_write, fs_edit])// 策略判定函数返回这次调用是否被允许。// exec 携带不可变的调用身份callId、name、arguments、agent、token、signal。async function isAllowed(exec: ToolExecution): Promiseboolean {if (DENY_TOOLS.has(exec.name)) return false// 额外示例runoob 演示里禁止修改 .env 文件参数在进入策略前已被冻结。const raw exec.arguments as { path?: string }if (typeof raw.path string raw.path.includes(.env)) return falsereturn true}export const name permission-gateexport function apply(ctx: Context) {// tools/pre-execute 是 waterfall监听器可以返回决策或调用 next() 委托。ctx.on(tools/pre-execute, async (exec, next): PromisePreToolDecision {if (!(await isAllowed(exec))) {// 返回 deny 会立即终止这次调用后续监听器不再执行。return { kind: deny, reason: Denied by policy: this tool is not allowed in the runoob workspace. }}// 放行把决定权交给流水线中后续的监听器。return next()})}这个插件加载后模型每次调用工具都会先经过它。命中黑名单的工具会得到一个带 reason 的错误结果工具主体被跳过。因为它返回的是类型化决策其它策略插件仍然可以在它之后继续参与决策。动手示例用 tools/execute 加超时超时属于「环绕分发」关注点应该用tools/execute包裹而不是写进工具主体。实例// 文件路径my-plugins/tool-guard/src/index.ts// 用 tools/execute 包装实际分发生命周期给每次工具调用加 30 秒超时。import type { Context } from deepseek-ai/cordisexport const name tool-guardexport function apply(ctx: Context) {// tools/execute 是 waterfall必须调用 next() 才会执行真正的工具主体。ctx.on(tools/execute, async (exec, next) {// 注意只有 tools/execute 视图可以替换 exec.signal。// 这里把调用方信号与一个 30 秒截止时间融合覆盖掉原来的 signal。const originalSignal exec.signalconst deadline AbortSignal.timeout(30_000)exec.signal AbortSignal.any([originalSignal, deadline])try {// 委托给真正的工具 execute()超时或调用方取消都会触发 abort。return await next()} finally {// 用完后恢复原信号避免污染后续调用。exec.signal originalSignal}})}规则可以替换 exec.signal但不能移除它。注册表会在调用工具函数体前重新融合调用方的 signal因此包装层替换掉 signal 是安全的。动手示例用 ctx.tools.guard() 做单调拒绝当不变式需要「最终拒绝、不可撤销」时用ctx.tools.guard()。注意守卫返回类型是string | undefined没有 allow 分支。实例// 文件路径my-plugins/invariant-guard/src/index.ts// 一个不可撤销的最终拒绝禁止在 runoob 演示环境执行 run_code。import type { Context } from deepseek-ai/cordisexport const name invariant-guardexport function apply(ctx: Context) {// 注册一个单调守卫。返回字符串即拒绝返回 undefined 表示维持现状。const disposer ctx.tools.guard((execution) {if (execution.name run_code) {return run_code is disabled in the runoob demo profile.}return undefined // 放行让 pre-execute 的决策保持不变})// guard() 返回精确的卸载函数插件卸载时框架会自动调用它。ctx.effect(() disposer)}因为守卫没有 allow 结果即使后面有监听器想放行run_code也无法把这次拒绝变回允许。小结自测工具执行流水线把「策略」与「工具主体」解耦让钩子可以跨越不同工具系列而无需让工具耦合某个策略服务。自测题问题参考想让某个工具「永远无法被任何人放行」应该用 pre-execute 还是 guardguard因为守卫没有 allow 结果不可撤销要给所有工具加统一的超时应该监听哪个事件tools/execute它环绕分派可替换 signal只想记录最终结果做审计、不改任何东西应该用哪个环节tools/result它观测冻结的权威结果
返回列表