DSH plugin 权限门禁与用户提问:DeepSeek Harness 的 ctx.approval
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 插件都可以使用该扩展点(来源)。 最小形态是先判策略、再放行或拒绝:
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 工具(来源)。三条边界要记牢:
- 这个 waterfall 是可重排的策略层 — 当不变式需要单调的最终拒绝时,用
ctx.tools.guard(),而不是在pre-execute里靠顺序取胜。 - plan-mode 的强制约束留在独立的沙箱/审批轴上 — 计划评审与执行强制分开。
- 原生钩子就是普通 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() 自身抛出三类稳定错误:
ASK_ABORTED— 提供的 signal 已经中止或变为中止。CALLER_NOT_LIVE— 提供的 agent 不是注册表里精确的存活实例。DELEGATED_CALLER— 那个存活 agent 归另一个 agent 所有。
决定边界的是运行时所有权,而非持久会话血缘:一个被拥有的子 agent 没有人类应答者,会永久阻塞;而一个带血缘的会话作为新的运行时根恢复后可以正常提问。这条约束正是上面「agent 必须是存活调用方同一实例」的底层原因。
写完自检三项:需要人类作答时是否走了 ask/ctx.approval 而不是硬 deny;提问的 agent 是否是精确的存活运行时根;intent 是否只用于改呈现、且 approve 确实指向本问题的选项。想找审批 / 提问类插件的社区实现,你可以在 DSH Plugin Hub 检索;扩展点归属判据见 功能→机制映射。
常见问题
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-user-questions 提供的用户交互 seam,用于工具或权限插件需要人类回答后 agent 才能继续的场景,是提供方无关的词汇。它做验证加作用域应答 waterfall:调用方用 ctx.userQuestions.ask(request) 提出问题并等待答案,agent-scoped listener 组合可用 UI 界面,其中包括转发到已连接 client 的 listener。
DSH plugin 的 AskUserQuestionIntent 按 kind 打标签(如 plan-review),approve 字段指名肯定选项的 label 而不是依赖选项顺序,这样任何 UI 都无法从选项排列推断裁决。意图只改变呈现方式而不改协议:遵循它的 UI 回答的仍是通用 UI 会发送的那些选项标签,因此调用方两种情况读到的回答字段相同。
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 时,人类交互只对该精确的存活运行时根有效:决定边界的是运行时所有权而非持久会话血缘。一个被拥有的子 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
来源
- DeepSeek Harness 官方文档 - 用户交互· deepseek-ai
- DeepSeek Harness 官方文档 - 实操手册:扩展插件形态· deepseek-ai