Skip to content

dsh-dhe-allow

Verified

dsh-dhe-allow · v0.5.1 · MIT · Web UI

DSH plugin: automatic approval for configured directories and commands — stop the sandbox-escalation popups you already trust, keep the rest. · DSH 自动权限审批插件:按允许目录与「以某命令开头」的允许命令自动放行工作区写模式下的越权审批弹窗,并在审批卡片上提供「始终允许」按钮。

Install

dsh plugin add dsh-dhe-allow

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

Readme


description: "dsh-dhe-allow:为你已经信任的目录和命令自动放行——把工作区写模式下反复弹出的越权审批消掉,其余照旧询问。"

dsh-dhe-allow

English | 中文

DSH 的权限分诊插件。它替你把「本来也会点允许」的沙箱越权弹窗自动答掉,规则放在你自己的规则文件里;其余一切照旧交给人。

workspace-write 会话,agent 想写 D:\shared\report.md
      ↓
tools/pre-execute            ← 本插件记下这次调用到底要做什么
      ↓
沙箱拒绝 → 模型带 sandbox_permissions: danger-full-access 重试
      ↓
approval/request             ← 本插件:有规则覆盖它吗?
      ↓ 覆盖                                      ↓ 无规则 / 被拒 / 受保护
  返回 allowed-once,根本不产生弹窗           照常弹出审批卡片
                                              (卡片上多一个「始终允许」按钮:
                                                点一下就把这类调用写成规则,
                                                下次同类调用不再弹)

它从不拒绝。 答不上来的请求就是人来看的请求:同一张卡片、同样的措辞、同样的「仅此一次」语义,与装插件之前完全一致。

目录

它能做什么

DSH 的 workspace-write 模式把文件效果限制在会话工作区内,并允许工具为单次调用申请一个「严格更宽」的模式。这个申请就是弹窗的来源。如果答案显然是「那目录就是我的」或者「npm run build 当然可以」,弹窗就是纯粹的摩擦。

dsh-dhe-allow 是一个 approval/request answerer。请求到达时,它查出被升级的调用究竟要做什么,用它对照你的规则,然后:

  • 返回 allowed-once —— 调用直接执行,卡片根本不会创建;或者
  • 调用 next() —— 请求继续交给下一个 answerer,在 web profile 里就是你熟悉的那张审批卡片。

两类规则:

类型 覆盖对象 例子
directory 目标路径全部落在该规则路径下的调用 D:\work\shared —— 写、改,以及命令行里点名该路径下文件的命令
command 命令行文本,支持 exact / prefix / contains / glob / regex git status(prefix,命中 git status -sb)或 npm run *(glob)

规则来自三层,判定时合并:

  1. inline —— 插件行里的 directories / commands(部署基线,命令不可改);
  2. file —— ${DSH_HOME}/dsh-dhe-allow.json,/allow 写入的规则文件;
  3. session —— /allow add … --session,只存在于内存,会话结束即丢弃。

安装

dsh plugin --profile web add dsh-dhe-allow

然后重启正在运行的 dsh web 进程:本插件注册的是宿主侧监听器,而已经跑起来的进程早已完成插件树组装。

装完不做任何事——directories 与 commands 默认都是空的——所以单独装上它是安全的。

本地源码调试时,也可以让 profile 直接指向目录:

# ~/.dsh/profiles/web/cordis.patch.yml
- insert:
    - id: dsh-dhe-allow
      name: dsh-dhe-allow
      config:
        configFile: 'D:\dsh-rules\dsh-dhe-allow.json'

配置

插件行接受以下字段(全部可选):

字段 默认值 含义
enabled true 总开关。false 时每个请求都交给人。
configFile ${DSH_HOME}/dsh-dhe-allow.json 规则文件。只写文件名时落在 DSH home 下。
audit true 是否记录每次判定。
auditFile ${DSH_HOME}/dsh-dhe-allow-audit.ndjson 判定日志,超过 5 MB 轮转。
onlySandboxEscalation true 只应答「原因文本是沙箱升级」的请求。
allowEscalationTo ["danger-full-access"] 允许应答的升级目标模式。
matchCommandPaths true 目录规则是否也覆盖命令行里点名的绝对路径。
caseSensitive 跟随平台(Windows 为 false) 匹配是否区分大小写。
builtinDeny true 保留内置的破坏性命令与系统目录底线。
protectDshHome true $DSH_HOME 永不自动放行。
commandName allow /allow 的命令名。
shellTools pwsh、bash、pwsh-persistent、bash-persistent command 参数是命令行的工具名;持久 Shell 默认不自动审批。
allowPersistentShell false 是否允许 pwsh-persistent / bash-persistent 进入自动审批;持久 Shell 可能有 alias、function 和变量状态。
toolEffects {} 工具的显式读写字段契约;未列出的工具 fail-closed,不按碰巧出现的 path 字段猜测。
approvalCard true 是否提供卡片按钮所需的宿主接口(false 时浏览器半边什么也不接管)。
apiPath /dhe-allow/api 卡片按钮调用的宿主接口路径;宿主同时保留默认兼容别名,浏览器会从 summary 学习配置路径。接口由 DSH browser-session 认证保护。
directories [] inline 目录规则。
commands [] inline 命令规则。
denyCommands [] 额外的 deny 正则,先于任何规则判定。
denyPaths [] 额外的 deny 根路径,先于任何规则判定。

toolEffects 例子:

# 工具名 -> 真正代表写入目标的参数字段
univer_export:
  paths: [output]
univer_import:
  paths: [file]

只有配置过 effect contract 的工具才会参与自动审批;未知工具、含未声明写入字段的工具应交给人。

规则文件

${DSH_HOME}/dsh-dhe-allow.json,由 /allow 写入,也可以手改:

{
  "version": 1,
  "enabled": true,
  "directories": [
    { "id": "r20260919170605123123", "path": "D:\\work\\shared", "recursive": true, "note": "团队中转目录" },
    { "id": "r20260919170605123124", "path": "C:\\Users\\me\\.config\\app\\settings.json", "recursive": false }
  ],
  "commands": [
    { "id": "r20260919170605123125", "value": "npm run *", "match": "glob", "note": "构建与测试" },
    { "id": "r20260919170605123126", "value": "git status", "match": "prefix" }
  ]
}
  • recursive: true(默认)覆盖整棵子树;false 精确覆盖该路径,单个文件或单个可执行文件用这个。
  • 文件里的 enabled 是 /allow off 写入的持久开关;插件行里的 enabled: false 优先级更高。
  • 新规则 ID 使用 r + 20 位数字时间戳,例如 r20260919170605123123;同一毫秒内也保证唯一。读取旧版 dir-1 / cmd-1 ID 时会自动迁移为新格式。
  • 无法解析、或含非法规则的规则文件会被改名保留为带唯一后缀的 backup,插件以空规则启动,绝不覆盖它。

/allow 命令

交互的那一半,任何带命令面板的界面都能用(web 输入框、CLI)。它作用于当前 agent,不产生模型消息,写入即持久化。

/allow                                   列出当前生效的全部规则
/allow list
/allow status                            规则文件、各层数量、最近错误、日志路径
/allow add dir <路径> [--file] [--note 说明] [--session]
/allow add cmd <模式> [--exact|--prefix|--contains|--glob|--regex] [--note 说明] [--session]
/allow remove <序号|id>
/allow clear [dir|cmd|all]
/allow on | off
/allow reload                            从磁盘重新读取规则文件
/allow test <命令或路径>                 用真实判定逻辑干跑一次
/allow log [n]                           最近的判定记录
  • --session 只把规则留在本次会话的内存里。
  • --file(目录规则)精确覆盖该路径,而不是整棵子树。
  • 相对 <路径> 按会话工作区解析。

/allow test 是回答「刚才为什么弹窗」的最快方式:

/allow test npm run build
as a command ALLOW  command-rule  [cmd-1]
as a target  ask    no-rule

卡片上的「始终允许」按钮

当请求最终落到人手上时,卡片上会多出一个按钮:它把这次被升级的调用变成一条规则,写进规则文件,然后按「允许一次」执行——下次同类调用不再弹窗。按钮上写明它要授予哪一类东西,规则的准确内容放在它的 tooltip 里:

始终允许此类命令        (命令规则,prefix;tooltip 里是「以 git status 开头的命令都不再询问」)
始终允许此命令          (命令规则,exact;tooltip 里是完整命令行)
始终允许此目录          (目录规则,recursive;tooltip 里是那个目录)

按钮永远不会超过三个:命令与目录各最多一个,多出来的那个插在 拒绝 与 允许一次 之间,允许一次 始终在最右。

设计上刻意保守,因为卡片是被本插件接管渲染的:

  • 卡片是按原版复刻的。 同样的 DOM 结构、同样的 class 与 CSS 声明、同样的文案,两个动作按钮也来自同一个设计系统组件(@deepseek-ai/dsh-client-ui-primitives 的 Button),所以被接管的卡片看起来和没被接管时一模一样——只有多出来的那一个是本插件自己的。test/client.mjs 会拿已安装的原版卡片样式逐条对比,样式一漂移就报错(详见 DESIGN.md §3.10)。
  • 只接管宿主能帮上忙的请求。 有 callId、工具名在宿主的可建议列表里(shellTools + write/edit)、并且宿主自报开关为开——三者缺一,本插件就不参与,由 dsh-client-ui-approval 的原版卡片照旧渲染。
  • 浏览器不能自己编规则。 宿主半边只接受「哪个会话的哪次调用」,规则由宿主用捕获到的真实参数现算(lib/suggest.js),再走和自动放行完全相同的底线检查。客户端塞进来的 path 一律无视。
  • 接口只对本机 harness 页面开放。 拒绝 Sec-Fetch-Site: cross-site,Origin 出现时必须同源,写规则必须 content-type: application/json(跨站表单做不到,且预检无人应答)。
  • 优先写「以某命令开头」这条规则。 这是绝大多数弹窗真正的解法:git status --porcelain 应该记住 git status,而不是那一条带参数的完整命令(否则参数一变又要问你)。只有当这次调用的文本推不出干净前缀时,才退化成 exact——一次性命令不会被记成长期授权。
  • 文件目标只写目录规则。 用「包含它的那个目录 + recursive」,这是用户本来就会手敲的那条规则,且按钮标签把范围写在明面上。
  • 底线先于按钮。 触发任何底线(受保护路径、deny 路径、deny 正则,如 git push --force)的调用干脆不给按钮,只留 拒绝 / 允许一次。
  • 失败就退化成原版卡片。 取不到建议、宿主没起接口、请求被拒、渲染出错、连设计系统组件都加载不到——都只损失这一个按钮,拒绝 / 允许一次 始终在。

关掉它的方式有三种:删掉按钮(approvalCard: false,宿主接口不再注册)、从 profile 的 dsh.profile.bundles / dsh.client 里摘掉浏览器半边,或者干脆 /allow off(接口在,但只会回「没有建议」)。写完规则后要刷新页面才会加载新的浏览器半边,宿主半边的改动要重启 dsh web。

风险须知:接管 composer 意味着这些请求的卡片由本插件渲染。它的排版、文案、两个原生动作都对着原版卡片复刻,也有离线测试兜底(test/client.mjs 在 node:vm 里加载真实 bundle,并把已安装原版卡片的样式逐条对比),但它毕竟不是原版——这是作者已知并接受的取舍,理由见 DESIGN.md §4。

「以某命令开头」到底是什么意思

prefix 规则回答的是一条简单命令,不是「任何以这段文字开头的字符串」:

git status          prefix 规则
git status -sb      命中
git status --porcelain --branch   命中
git -C D:\work status             不命中(前缀是字面文本,见下)
git status; del important.txt     不命中:这是两条命令
git status && git pull            不命中
git status | more                 不命中
git status > out.txt              不命中
git status "$(date)"              不命中:命令替换里藏着第二条命令

最后几条是刻意的:一条 git status 前缀规则表达的是「这类命令」,而 git status; del important.txt 只是以这些字开头的另一条命令——它必须还由人来看。contains / glob / regex 是你手写的模式语言,没有这层约束。

前缀是字面文本,所以参数里的路径也会留在里面:

git -C D:\work push origin main   → 规则 git -C D:\work push

卡片按同一套规则推导:程序名 + 第一个「词」(子命令),中间的开关和路径原样保留,遇到分号、换行、管道、重定向、命令替换或推导不出子命令(node test/run.mjs、ls -la、$p='x')就退化成 exact 规则。想更宽或更窄,直接手写:

/allow add cmd "git status"                 # 默认就是 prefix
/allow add cmd "git status --porcelain" --exact
/allow add cmd "git -C D:\work push"        # 前缀里带上路径,只认这一种形态
/allow add cmd "npm (run|test)" --regex

一次请求如何被判定

顺序固定,且每一步只会让结果更保守:

  1. 插件是开着的,且(默认)请求原因是沙箱升级、目标模式在 allowEscalationTo 内;
  2. 被升级调用的参数读得到——来自 tools/pre-execute 的实时捕获,或按 callId 从会话日志的 tool/call 事件回捞;
  3. 规则文件、判定日志、$DSH_HOME、凭据目录、系统目录,以及你配置的 denyPaths / denyCommands 优先于任何规则;
  4. 命令行里点名的绝对路径同样在这里先过一遍底线——它必须早于第 5 步,因为一条命令规则回答的是整行文本,否则 Set-Content 这类宽规则会把写入带进受保护目录;
  5. 命令规则命中命令行(prefix 另需该行是一条简单命令);
  6. 目录规则必须覆盖这次调用点名的每一个目标——只要有一个路径没被覆盖,整次调用交给人;
  7. 开启 matchCommandPaths 时,目录规则也覆盖命令行里点名的绝对路径,但前提是每个路径 token 都能解析;出现变量、引号路径无法解析或通配符就停止解读,交给人;
  8. 未知工具、未声明的写入字段、持久 Shell(除非显式开启)都不自动回答;
  9. 否则:弹卡片。

审批 seam 只有 allowed-once 这一种授权,所以自动放行永远是单次的;除了你的规则,什么都不会被记住。

永不自动放行的范围

  • 规则文件与判定日志(<configFile>、<auditFile>);
  • $DSH_HOME 下的一切(protectDshHome);
  • ~/.ssh、~/.aws、~/.gnupg、~/.kube、~/.docker、~/.npmrc、~/.netrc、~/.git-credentials;
  • Windows 上所有盘符根目录,以及 C:\Windows、C:\Program Files、C:\Program Files (x86)、C:\ProgramData\Microsoft、C:\Recovery、C:\$Recycle.Bin、C:\Boot;POSIX 上 /、/bin、/sbin、/usr、/etc、/boot、/dev、/proc、/sys、/System、/Library、/private/etc、/private/var/db;
  • rm -rf /、Remove-Item -Recurse -Force C:\、mkfs/format/diskpart、dd of=/dev/…、reg delete、Set-ExecutionPolicy、下载即执行管道(curl … | bash、iwr … | iex)、shutdown、fork bomb、对 / 的 chmod/chown 通配,以及强推(git push --force / --force-with-lease / -f)——所以一条 git push 前缀规则不会把强推一起带走。

builtinDeny: false 会去掉内置底线,protectDshHome: false 会去掉 DSH home 围栏——这两条存在是有原因的:否则插件自己的规则文件就会被它所放行的 agent 改写。

排障

弹窗反复出现。 直接读判定日志,每次判定都带上了结论与原因码:

/allow log 10
2026-01-01T10:00:00.000Z  ask    no-rule  pwsh
/allow test npm run build

值得记住的原因码:no-rule(没有规则覆盖)、no-call-arguments(参数读不到)、unresolved-command-path(命令里有变量或通配符)、protected-path / denied-path / denied-command(底线拦下)、not-an-escalation(请求不是来自沙箱升级)。

完全没有自动放行。 /allow status 会显示开关状态、读到的文件、各层规则数量,以及最近一次加载错误。

想立刻关掉? /allow off 写入持久开关;:enabled: false 是部署级版本。

已知限制

  • 默认只处理越权升级。 其他来源的审批(例如 hook 的权限判定)会直接交给人,除非你把 onlySandboxEscalation 设为 false——那时你的规则会用同样的方式判定它。
  • 命令匹配是文本匹配。 prefix 取字面前缀,所以 prefix: npm 也会命中 npm publish;它只额外要求该行是一条简单命令(;、&&、管道、重定向、换行、命令替换一概不算),多出来的那部分不会跟着被放行。更精确请用 glob/regex,并且在信任一条规则前先看 /allow test 的输出。
  • 目录规则是「按调用」而非「按文件」。 审批 seam 授权的是单次调用,所以同时点名允许与不允许路径的调用不会被自动放行。
  • matchCommandPaths 只是词法解读,不是 shell 解析器。 它从不判断命令做了什么;带空格的引号路径会完整读取,但变量、替换、glob 或无法解析的 token 会让调用交给人。命令规则也不会替未知工具或未声明写入字段放行。
  • 插件不扩展工作区边界。 那需要把额外可写根写进 SandboxExecutionPolicy,而宿主目前没有这个入口;本插件的定位是把该弹的窗替你已经信任的部分答掉。
  • 卡片按钮会接管 composer。 conversation.composer 是「按优先级选一个渲染」的链式插槽,没有增量插槽可挂,所以按钮只能在容器优先级上抢在原版卡片之前。本插件的接管条件很窄(有 callId、工具在可建议列表、宿主自报开关为开),发生接管时也会把原版的排版与两个动作一起复刻;但它确实是第三方渲染路径,取舍与兜底见上节与 DESIGN.md §4。
  • 按钮只在浏览器半边加载后可用。 改完 approvalCard / apiPath 需要重启 dsh web 并刷新页面。API 现在由 DSH browser-session 认证;没有认证 Cookie 的本地 curl/agent 请求会得到 401。

开发

npm test                # 125 项行为 + 18 项浏览器半边 + 80 项打包 + 15 项集成检查
npm run test:behavior
npm run test:client
npm run test:packaging
npm run test:integration
  • test/behavior.mjs——逐个模块、完整判定矩阵,以及用假 context 走通的一次真实越权审批。
  • test/client.mjs——在 node:vm 沙箱里加载真实发布用的 lib/client.js:假的 module loader、脚本化的 fetch、迷你 React(state/effect/重渲染),断言选中条件、三个按钮、点击后先写规则再放行、以及各种失败退化。
  • test/packaging.mjs——加载器与打包器对插件包强制执行的规则,含浏览器半边的契约(module loader 握手、只用 react、无 ESM 语法)。
  • test/integration.mjs——同样的路径,但跑在真实的 @deepseek-ai/cordis 调度器与真实的 @deepseek-ai/dsh-commands 服务上。它需要能解析这两个包的 DSH 模块树(指向 profile node_modules 的 junction 即可);解析不到时报告 skip 而不是失败。

测试不会碰真实的 ~/.dsh:每个用例都用一次性临时目录。

许可证

MIT