DSH plugin 权限门禁与用户提问:DeepSeek Harness 的 ctx.approval

插件开发发布于 2026-10-07作者: DeepSeek Plugin 插件市场
DSH pluginDeepSeek Harness权限门禁ctx.approvalctx.userQuestions
DSH plugin 权限门禁与用户提问怎么做?本文按官方用户交互子系统,讲清 tools/pre-execute 的 ask 决策与 ctx.approval 应答、ctx.userQuestions 的 ask waterfall,以及 approve 指名肯定项与错误语义。

DSH plugin 的「权限门禁」与「用户提问」是两条不同路径:门禁在工具执行前用 tools/pre-execute 拦一次调用并可能返回 ask,用户提问则由提供方无关的 ctx.userQuestions seam 承载(来源)。 本文面向 DSH插件 / DeepSeek插件 作者,讲清门禁的 ask 决策与 ctx.approval 应答、ctx.userQuestions 的 ask waterfall 契约与 approve 语义,以及错误与调用方约束;deny 与 next() 放行的门禁细节见 插件钩子,这里只讲需要人类回答的那部分。

DSH plugin 从 tools/pre-execute 返回 ask 并经 ctx.approval 应答

钩子插件从 tools/pre-execute 门禁返回一个类型化的决策,用于允许或拒绝一次调用;沙箱、权限和 plan-mode 插件都可以使用该扩展点(来源)。 最小形态是先判策略、再放行或拒绝:

ts
export function apply(ctx: Context) {
  ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
    if (!(await isAllowed(exec))) {
      return { kind: 'deny', reason: 'Denied by policy.' }
    }
    return next()
  })
}

当需要人类作答时,走的是 ask 而非 deny:官方架构映射里,「权限系统 / AskUserQuestion」这一行的实现就是——从 tools/pre-execute 返回 ask,并通过 ctx.approval 应答;而「普通用户提问」则注册一个独立的、面向模型的 ask 工具(来源)。三条边界要记牢:

  1. 这个 waterfall 是可重排的策略层 — 当不变式需要单调的最终拒绝时,用 ctx.tools.guard(),而不是在 pre-execute 里靠顺序取胜。
  2. plan-mode 的强制约束留在独立的沙箱/审批轴上 — 计划评审与执行强制分开。
  3. 原生钩子就是普通 Cordis 插件 — 在拦截点上运行,不需要外部协议。

DSH plugin 的用户交互 seam:ctx.userQuestions

ctx.userQuestions 是 dsh-user-questions 的用户交互 seam,是工具或权限插件需要人类回答后 agent 才能继续时所使用的、提供方无关的词汇(来源)。 一次提问的数据结构如下:

  • AskUserQuestionOption — label 是面向用户的选项文字,同时也是面向模型的选中值;description 是可选的 UI 帮助文本。
  • AskUserQuestionItem — 调用方提供稳定的 id,它会随答案原样返回,使批量问题仍可路由;可选 detail 携带辅助文本(提供方随问题渲染,但不放进选项标签);此外有 question、header、options、multiSelect 与 intent。
  • AskUserQuestionRequest — questions 是数组,UI 可在一个流程中呈现相关提示,同时保持每个回答有稳定 id。如提供 agent,它必须与存活调用方是同一实例,且仅当当前注册表将该实例识别为运行时根时才会被接纳。
  • 回答 — 提供方为每个问题 id 返回一个 AskUserQuestionAnswerItem:selected 是选中的选项标签,custom 在用户输入自由文本时携带「其他」回答。单选题里 custom 会覆盖选中项且 selected 为空;多选题里 custom 可以补充 selected;selected 为空且无 custom 的项用于在其余问题均已完成的批次里保留被跳过的问题。

ctx.userQuestions.ask(request) 做验证加作用域应答 waterfall:它向作用域应答者提出结构化输入请求并等待人类答案;user-questions/request 是一个 waterfall——返回一个答案即认领该请求,或调用 next() 委派,作用域过滤分发使 agent-scoped 监听器只收到该 agent 的请求。

AskUserQuestionIntent 是把呈现与协议分开的关键:它按 kind 打标签(如 plan-review),因此可以增加新意图;不认识某个标签的 UI 渲染通用选项列表。approve 指名肯定选项的 label,而不依赖选项顺序——这样任何 UI 都无法从选项排列推断裁决。意图只改变呈现方式,绝不改变协议:遵循它的 UI 回答的仍是通用 UI 会发送的那些选项标签,因此调用方两种情况读到的回答字段相同。ask() 会拒绝两种无法由类型系统表达的断言:approve 未指向该问题自身的任何选项,以及为没有 detail 的问题指定意图。

DSH plugin 用户提问的错误与调用方约束

UserQuestionError 继承 HarnessError,因此 ctx.tools.execute() 会保留 { name, code } 用于面向模型的工具失败,如 EMPTY_QUESTIONS、NO_PROVIDER、ASK_ABORTED 或 UI 侧取消(来源)。 ask() 自身抛出三类稳定错误:

  1. ASK_ABORTED — 提供的 signal 已经中止或变为中止。
  2. CALLER_NOT_LIVE — 提供的 agent 不是注册表里精确的存活实例。
  3. DELEGATED_CALLER — 那个存活 agent 归另一个 agent 所有。

决定边界的是运行时所有权,而非持久会话血缘:一个被拥有的子 agent 没有人类应答者,会永久阻塞;而一个带血缘的会话作为新的运行时根恢复后可以正常提问。这条约束正是上面「agent 必须是存活调用方同一实例」的底层原因。

写完自检三项:需要人类作答时是否走了 ask/ctx.approval 而不是硬 deny;提问的 agent 是否是精确的存活运行时根;intent 是否只用于改呈现、且 approve 确实指向本问题的选项。想找审批 / 提问类插件的社区实现,你可以在 DSH Plugin Hub 检索;扩展点归属判据见 功能→机制映射。

常见问题

DSH plugin 怎么在工具执行前拦一次调用并请求用户确认?

DSH plugin 监听 tools/pre-execute 门禁并返回一个类型化决策:允许就 return next(),拒绝就返回 { kind: 'deny', reason }。要请求用户确认则在 pre-execute 返回 ask 决策,并通过 ctx.approval 应答;沙箱、权限与 plan-mode 插件都可共用这个扩展点。该 waterfall 是可重排的策略层,若需要单调的最终拒绝应改用 ctx.tools.guard()。

DSH plugin 的 ctx.userQuestions 是做什么的?

DSH plugin 的 ctx.userQuestions 是 dsh-user-questions 提供的用户交互 seam,用于工具或权限插件需要人类回答后 agent 才能继续的场景,是提供方无关的词汇。它做验证加作用域应答 waterfall:调用方用 ctx.userQuestions.ask(request) 提出问题并等待答案,agent-scoped listener 组合可用 UI 界面,其中包括转发到已连接 client 的 listener。

DSH plugin 提问时为什么用 approve 指名选项而不是靠顺序?

DSH plugin 的 AskUserQuestionIntent 按 kind 打标签(如 plan-review),approve 字段指名肯定选项的 label 而不是依赖选项顺序,这样任何 UI 都无法从选项排列推断裁决。意图只改变呈现方式而不改协议:遵循它的 UI 回答的仍是通用 UI 会发送的那些选项标签,因此调用方两种情况读到的回答字段相同。

DSH plugin 的 ask() 会抛哪些错误?

DSH plugin 的 ctx.userQuestions.ask() 在 signal 已中止或变为中止时抛 UserQuestionError 的 ASK_ABORTED;当提供的 agent 不是注册表里精确的存活实例时抛 CALLER_NOT_LIVE;当那个存活 agent 归另一个 agent 所有时抛 DELEGATED_CALLER。另外,UserQuestionError 继承 HarnessError,ctx.tools.execute() 会保留 { name, code } 供面向模型的工具失败,如 EMPTY_QUESTIONS、NO_PROVIDER、ASK_ABORTED 或 UI 侧取消。

DSH plugin 提问时提供的 agent 有什么约束?

DSH plugin 中,当调用方提供 agent 时,人类交互只对该精确的存活运行时根有效:决定边界的是运行时所有权而非持久会话血缘。一个被拥有的子 agent 没有人类应答者,会永久阻塞;而一个带血缘的会话作为新的运行时根恢复后可以正常提问。仅当当前注册表将该实例识别为运行时根时,交互 seam 才会接纳该 agent。

相关术语

tools/pre-execute(执行前门禁)
tools/pre-execute 是 DeepSeek Harness 中在工具执行前拦截调用的 waterfall 扩展点:监听器返回类型化决策(允许则 next(),拒绝则 deny),权限系统与 AskUserQuestion 就是从这里返回 ask 决策并经 ctx.approval 应答;沙箱、权限与 plan-mode 插件共用该扩展点。— https://deepseek-harness.github.io/deepseek-harness/reference/cookbook/extension-cookbook
AskUserQuestionIntent(呈现意图)
AskUserQuestionIntent 是调用方声明的已知决策类型,按 kind 打标签(如 plan-review);approve 指名肯定选项的 label 而不依赖选项顺序,意图只改变呈现方式、绝不改变协议,ask() 会拒绝 approve 未指向本问题选项或为无 detail 的问题指定意图这两种断言。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/user-questions
ctx.userQuestions(用户交互 seam)
ctx.userQuestions 即 UserQuestionService,提供验证与作用域应答 waterfall:ask(request) 向作用域应答者提出结构化问题并等待人类答案,作用域过滤分发使 agent-scoped 监听器只收到该 agent 的请求。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/user-questions
UserQuestionError
UserQuestionError 继承 HarnessError,是用户交互失败的稳定错误分类,code 如 EMPTY_QUESTIONS、NO_PROVIDER、ASK_ABORTED;ask() 还会抛 CALLER_NOT_LIVE(提供的 agent 非注册表精确存活实例)与 DELEGATED_CALLER(该存活 agent 归另一个 agent 所有)。— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/user-questions

来源