dsh 中的权限是两个正交旋钮,由一个预设捆绑:沙箱模式(read-only / workspace-write / danger-full-access,来自沙箱架构)与审批策略(ask / never)。"什么需要审批"由组合这两者决定,而非一张平坦的工具名允许列表。
| 包 | 角色 |
|---|---|
packages/interaction/permission-presets | ctx.permissionPresets:预设表、/permission 命令、permissions 投影 |
packages/interaction/user-approval | ctx.approval:请求/取消/决定、会话策略、审计事件 |
packages/core/tools | ctx.tools:解决审批 ask 的 pre-execute/execute/post-execute 管线 |
packages/client/ui-permission-presets | Web 表面:新会话 Settings 行 + 当前会话命令选择器 |
packages/guard/repeat-tool-reminder | 提示模型重复调用的咨询性提示 |
什么需要审批
审批围绕 ApprovalPolicy(packages/interaction/user-approval/src/index.ts)构建:
'ask'(默认)——委托给组合好的应答器;没有组合任何应答器时,链以'unavailable'fail-closed。'never'——绝不提示任何人;每次请求都确定性地解析为'rejected'(严格的无头/CI 立场)。
因此不存在固定的"危险工具"列表。相反,当 (a) 工具的门返回 ask,或 (b) 沙箱升级路径(sandbox_permissions + justification)被调用时,请求就需要审批——两者都汇聚到同一个 ctx.approval。在 workspace-write 下,工作区之外的更宽写入或 shell 命令必须升级,从而提示用户;在 danger-full-access + never 下,什么都不提示。预设即部署指向其中某一种组合的方式。
预设名与捆绑
预设表(packages/interaction/permission-presets/src/index.ts)把名字映射为 PresetSpec——一个 sandbox 模式加上一个 approval 策略(以及可选展示用的 name/description)。内置默认:
| 预设名 | sandbox | approval |
|---|---|---|
workspace-write | workspace-write | ask |
danger-full-access | danger-full-access | never |
保留名 custom(CUSTOM_PRESET)绝不是切换目标——它是在有效旋钮值不匹配任何表项时的派生状态。表可通过插件 Config.presets 配置;服务构造函数要求一个会隔离的 ctx.shell(暴露 sandboxMode 的那个)以及 ctx.approval——在未隔离的执行器上组合预设是加载期错误。
切换预设会记录持久化、仅日志的用户意图,然后通过规范性 setter 写每个变更的旋钮:
- 选中名字的
permission/preset事件(session.append); setSandboxMode(session, spec.sandbox)——发出sandbox/mode(@deepseek-ai/dsh-sandbox-policy);setApprovalPolicy/ctx.approval.setPolicy(agent, policy)——发出approval/policy。
新会话会被固定(session/created → pinInitialPermission)到当前用户默认预设(defaultPreset),其中真正全新的会话从注册到 settings seam(permission 命名空间)的 defaultSettings 源获得整个捆绑。
工具管线中的权限模型
决策点位于 ctx.tools 的 tools/pre-execute waterfall(packages/core/tools/src/index.ts);规范化顺序记录在官方文档的工具执行管线一章(源码:docs/tool-execution-pipeline.md)。交叉对照该管线:
tools/pre-execute waterfall (hooks、权限、沙箱)
→ 门:allow | deny | ask
→ ask → ctx.approval 一次性提示(缺失/无法应答 → deny)
单调守卫
tools/execute waterfall (timeout、retry、metrics——围绕派发)
工具 body
tools/post-execute waterfall相关门类型是 { kind: 'ask'; reason?: string }。返回 ask 的门由 tools 服务的私有方法 serviceAsk(packages/core/tools/src/index.ts)解决,后者用 ctx.get('approval') 机会式消费:
- 没有挂载审批服务 →
ask退化为deny(tool "…" requires approval (not yet supported)); - 调用上没有 agent →
deny(… no agent to route it through); - 否则调用
approval.request({ agent, toolName, callId, reason, signal })并把结果映射为:allowed-once → allow;rejected → deny;cancelled → deny(带approvalCancelled);unavailable → deny。
被拒的调用跳过工具 body 并流入 tools/post-execute,因此监听器顺序无法把拒绝再变回权限。
沙箱升级路径
sandbox 工具家族(tool-fs、tool-bash)对升级使用同一审批接口。approveEscalation(packages/sandbox/sandbox/src/escalation.ts)是共享编排:
- 对照调用有效模式的严格更宽检查(
WIDER_MODES:read-only → workspace-write → danger-full-access); - 要求已挂载的审批服务与调用方 agent(否则以逐字错误文本 fail-closed);
- 调用
approval.request({ agent, toolName, callId, reason: 'escalate sandbox to <mode>: <justification>', signal }); - 把封闭结果映射为恰好本次调用的授权模式。
模型面向的词汇是 sandboxDenialMarker(mode)([sandbox: file access denied under <mode> mode])与 escalationHintMarker(subject),另加 validateEscalationArgs 强制 sandbox_permissions 与 justification 同来同去。
审批服务本身
ApprovalService(packages/interaction/user-approval/src/index.ts)在应答器之前应用会话策略,并记录每次询问/结果对:
- 事件:
approval/request(waterfall,带作用域过滤),以及仅日志的审计事件approval/asked(id、toolName、callId、reason)与approval/decided(id、outcome)。 - 结果(
ApprovalOutcome,types.ts):'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'。'allowed-once'是唯一的授权——授权是一次性的,只适用于被请求的动作,绝非持久的会话级放行。 - 会话包裹审计:
request()要求会话日志中存在一个未闭合的轮次(turn/start尚未被turn/end闭合),因为在轮次之间的裸事件会在重载时成为崩溃尾部垃圾。它追加approval/asked,解析decide,再追加approval/decided。 'never'静音在服务自身的请求路径内决定,而非由监听器决定,因此一个前插的监听器永远无法绕过文档化的"never → 确定性地 rejected"。- 包含(containment):抛错的应答器会让问题 fail-closed(解析为
unavailable),而非让调用方工具失败;AbortSignal与应答竞速,产出cancelled。
会话/全局作用域:策略按会话(approval/policy 事件折叠,或配置默认),而每次授权按请求(allowed-once)。审计事件位于请求 agent 的会话日志上,因此恢复会重放同一对 ask/decide。
审批与沙箱模式的会话事件
| 事件 | 载荷 | 含义 |
|---|---|---|
approval/asked | { id, toolName, callId?, reason? } | 向应答链提出一个问题(审计) |
approval/decided | { id, outcome } | 每次询问恰好一个:决定、取消或 fail-closed |
approval/policy | { policy, source? } | 会话审批策略覆盖(持久化、可重放;source: 'delegation' 为子会话播种) |
sandbox/mode | { mode, source? } | 会话沙箱模式覆盖(由 sandbox-policy 折叠) |
permission/preset | { preset } | 所选预设(持久化仅日志意图) |
Web 表面
ui-permission-presets(packages/client/ui-permission-presets)是一个表面插件:其宿主 apply() 是空操作;浏览器半区提供新会话 Settings 行与当前会话命令选择器(服务贡献的 /permission 命令)。它通过 displayPermissionPreset(value, name) 渲染每个预设,为 danger-full-access 预设(FULL_ACCESS_PRESET)保留产品化标签——"Full access"。客户端提交的机器值正是服务所解析的预设名。
延伸阅读
- 沙箱架构:总览——本审批模型所管控的文件影响模式与升级阶梯。
- 文件系统观察与沙箱策略——在下一次调用中,被授予的更宽模式如何被 fs 围栏兑现。
- 守卫:超时与重复提醒——pre-execute 门两侧的
tools/execute/tools/post-execute管线。 packages/interaction/user-approval/src/index.ts——ApprovalService、结果与审计事件对。packages/interaction/permission-presets/src/index.ts——PermissionPresetService、预设表与/permission命令。packages/core/tools/src/index.ts(搜索serviceAsk/PreToolDecision)——ask门在哪里被变成决定。