dsh-dhe-allow
Verifieddsh-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) |
规则来自三层,判定时合并:
- inline —— 插件行里的
directories/commands(部署基线,命令不可改); - file ——
${DSH_HOME}/dsh-dhe-allow.json,/allow写入的规则文件; - 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-1ID 时会自动迁移为新格式。 - 无法解析、或含非法规则的规则文件会被改名保留为带唯一后缀的 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
一次请求如何被判定
顺序固定,且每一步只会让结果更保守:
- 插件是开着的,且(默认)请求原因是沙箱升级、目标模式在
allowEscalationTo内; - 被升级调用的参数读得到——来自
tools/pre-execute的实时捕获,或按 callId 从会话日志的tool/call事件回捞; - 规则文件、判定日志、
$DSH_HOME、凭据目录、系统目录,以及你配置的denyPaths/denyCommands优先于任何规则; - 命令行里点名的绝对路径同样在这里先过一遍底线——它必须早于第 5 步,因为一条命令规则回答的是整行文本,否则
Set-Content这类宽规则会把写入带进受保护目录; - 命令规则命中命令行(
prefix另需该行是一条简单命令); - 目录规则必须覆盖这次调用点名的每一个目标——只要有一个路径没被覆盖,整次调用交给人;
- 开启
matchCommandPaths时,目录规则也覆盖命令行里点名的绝对路径,但前提是每个路径 token 都能解析;出现变量、引号路径无法解析或通配符就停止解读,交给人; - 未知工具、未声明的写入字段、持久 Shell(除非显式开启)都不自动回答;
- 否则:弹卡片。
审批 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 模块树(指向 profilenode_modules的 junction 即可);解析不到时报告 skip 而不是失败。
测试不会碰真实的 ~/.dsh:每个用例都用一次性临时目录。
许可证
MIT