# dsh-auto-approve [English](README.md) | 中文 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件:用**关键词**和**审核模型(类别 + 风险等级)**自动允许或拒绝工具调用。每个类别按 `low` / `medium` / `high` 三档各配一个动作,完全由你决定。拿不准就交给原来的网页审批框。 ## 安装 ```sh dsh plugin --profile web add github:DNAlec/dsh-auto-approve ``` 打 tag 发布后也可从 npm 安装: ```sh dsh plugin --profile web add @dnalec/dsh-auto-approve ``` > 需要可复现的版本时在末尾加 `#vX.Y.Z`(tag 与 `package.json` 的版本一致,发布流程会校验)。 重启 `dsh web`。首次启动会把「自动审批」权限预设写进当前 profile 的 `cordis.patch.yml`;写失败时日志会指出文件和原因。 ## 设置 设置页 → **自动审批**。页面上每处只留一行提示,规则在这里说全: 1. **自动审批模式** — 工作区内不审批(`workspace-write`,推荐),或工作区也走判定(`read-only`)。点选即写入;然后重启 `dsh web`,再重新选择「自动审批」或开新会话。 2. **关键词** — 三个桶:拒绝 / 人工 / 允许。匹配**工具名、命令、路径、工作目录**,以及自定义工具(MCP 等)那些不认识名字的参数值;**看不见写入内容、代码正文与数字/布尔开关**(`{content:"rm -rf /"}`、`{recursive:true}` 只能靠审核模型判)。拒绝/人工词也匹配工具名;允许词不匹配工具名,也不匹配会话目录名——避免把 `bash`/`write` 整类放行。 3. **审核表** — 一行 = 英文 `id` + 说明(写清「什么情况下选这个 id」)+ 三格动作(`low` / `medium` / `high` → 动作,等级 id 在页面上原样显示)。审核模型只看 id 与说明,动作由程序按 **(行, 等级)** 查格执行;`other` 不能删除,但说明与三格都可改。id 会显示在审批历史里。 4. **风险等级** — 三档说明都会送进提示词。**等级认不出时按**哪一档(默认 `high`)决定查「那一行的哪一格」,所以它同时决定后果:兜底 `high` 直接拒绝、`medium` 转人工、`low` 自动放行。 5. **审核模型** — 模型、思考强度、审核超时、**送审内容上限**、**输出预算(token)**,以及**超过送审上限 / 撞收集护栏时**转人工还是直接拒绝(**参数没采集到永远直接拒绝,不可配**,见「怎么判定」)。同一张卡片里还有**审核提示词**:用 `{{criteria}}` 插入当前审核表、`{{levels}}` 插入风险等级说明,删掉占位符时会把对应定义附在末尾;可改,恢复默认写回所选语言的出厂模板并把语言切过去。 **输出预算**(`judge.maxTokens`,默认 8192,区间 256–32768)是审核模型**首轮**一次能写多长(推理 token 与正文共享这个上限)。给少了会出事:模型默认就在思考,本地实测每次推理 3.7k–8.3k 字符(≈2–5k token),早先 1024 的档位必然被推理吃光 → 正文为空 → 插件换成大预算重试一次,等于**每次判定白跑一次模型调用**(延迟翻倍,20s 超时更紧);2026-09-14 那次「每次判定都转人工」的故障就是这条链走到极端(当时重试只给 2048,两次都被吃光)。`maxTokens` 是**上限不是预扣**——不思考的路由写完就停,给足空间不会变慢,只会省掉那次注定失败的首轮。它只对**会推理的路由**生效(没配档位但路由报告有推理能力也算);不推理的路由固定 256。判不出来的现场在设置页「测试判定」与审计里可见。 6. **模型转人工**(默认关闭)— 开启后自动拒绝会带上原因,并允许模型把某次操作转成人工审批;工具名可改(重启生效),提示语言可选中英。注意开启意味着审批框变成模型能主动叫醒你的通道——包括它正被不可信内容驱动的时候。 7. 会话权限选 **自动审批**。 恢复默认按钮各带语言(审核表、等级说明、提示词各有中英两份),选中即写入 config,并同时决定框架、卡片文案与理由的语言。 升级说明:审核表一行从「一个 action」变成「三格 action」,老文件的 `action` 会自动播种到三格;`action` 字段会在下次写盘时消失。**出厂三格自 v22 起改成「等级刻度」:所有行统一 low 允许 / medium 人工 / high 拒绝**——这是相对旧版的**行为变更**(旧版风险行三格全拒绝、`safe` 三格全允许、`other` 三格全人工),升级后请复核默认行为与兜底档。只有你自己拉开过格子的行不会被迁移改动。`other` 不再是特殊行——不可删除,但**说明与三格都可改**,它承接「输出认不出」。审核提示词模板加了 `{{levels}}` 与第三行输出;**自定义模板没要求输出等级时,等级一律按「等级认不出时按」那一档执行**(默认 high)。 **自 v23 起两处出厂文案消歧**(同样只刷新**仍是出厂原文**的行与档位,你改过的一个字不动):`system` 行补上「/tmp、/var/tmp 这类临时目录下的临时产物按实际操作判,不因路径落在 /var 就选本行」;`low` 档说明从「本次工作区**或临时产物**」收紧为「只落在**本次工作区内**(含工作区里的临时产物)」。原因是旧文案与 `medium` 的「工作区外文件、缓存」直接重叠——实测同一个「工作区外写一个临时文件」既被判过 `low`(自动放行)也被判过 `medium`(转人工)。另外审核模型**首轮输出预算**从 1024 提到 8192(可在设置页调),详见「设置」第 5 条。 ## 怎么判定 仅当会话预设是「自动审批」时介入。`workspace-write` 下,工作区内写入不会进审批。`danger-full-access` 走同一条管道。 管道:**拒绝关键词 →「参数没采集到」永远直接拒绝(不给人工框:那是插件侧采集故障,会告诉模型重新发起同一次调用)→ 人工关键词 →「看得见吗」闸门(撞收集护栏 / 超过送审上限:按「审核模型」卡片里选的动作,默认转人工)→ 允许关键词 → 审核模型(类别 + 等级)→ 按 (行, 等级) 查三格动作**。顺序有两层含义:用户显式写的**拒绝/人工词**对「闸门里的调用」同样生效;但**允许词不能放行它们**(放行必须在看清之后才成立)。类别认不出(输出认不出)走**兜底行 `other` 的三格**;**判定压根没跑成**(空输出、超时、调用失败、没有可用路由、插件异常)固定转人工,不看 `other` 怎么配;请求被取消不产生判定。**没有「内容多少」的闸门**:只要没超上限,卡片上有什么就原样交审核模型判——空参数、只有 `description`/`workdir`、纯数字/布尔开关都照常送审;「没有可审的操作内容」这道开关已整套删除(见 CHANGELOG)。**自定义工具(MCP 等)参数名不在已知字段里不算缺参**:参数连键名(含 `params.command` 这类嵌套路径)一起送审核模型,也进关键词干草,参数照实记在审批记录里。**数字/布尔参数也算内容**:`{recursive: true, force: true}` 这类开关会上卡片、交审核模型判,不再被当成「没给参数」。 **送审内容要么完整、要么不问模型**:卡片不做任何字段裁剪、也没有条数上限;量的是整条请求(系统提示词 + 卡片)与设置页的**送审内容上限**(默认 20000 字符,区间 8192–1000000;下限必须覆盖英文出厂框架 ~5.8k 字符,否则每次判定都送不进模型)。超过就按设置页**审核模型**卡片里「超过送审上限 / 撞收集护栏」选的动作处理——选「拒绝」时这类调用不弹框,拒绝原因按闭集说明回传给模型——并写一条日志与审计行(`request=<实际>>预算`,撞到收集护栏记 `oversize=collect>8388608`),不会静默转人工。审核模型永远不会拿到被切了一半的操作;事件里的 `argsOmitted` 只是存档层的省略,不代表当时送审过残缺内容。 1. **关键词**(同一条调用命中多个桶时取 拒绝 > 人工 > 允许;允许桶只在闸门之后生效)。匹配工具名、命令、路径和工作目录、请求方给的 `reason` 文本(只会多拦,不会少拦),**以及自定义工具那些不认识名字的参数值**(否则 `{cmd: 'rm -rf /'}` 这类调用会绕过红线);允许桶只认命令/路径/工作目录,参数名认不出时绝不自动放行。**关键词看不到写入内容、代码正文与数字/布尔开关**(`{content:"rm -rf /"}`、`{recursive:true}` 这类只能靠审核模型判)——它只承接「零上下文就确定灾难」的红线,别当成万能拦截器,也别因此把审核表写得太薄。允许词不匹配工具名,也不匹配会话目录名。**出厂拒绝词只留三类「零上下文就确定灾难、不该让模型有发言权」的红线**:① 清根 `rm -rf /`(写法连 `rm -rf /*`、`sudo rm -rf /` 一起覆盖);② 裸设备覆写与格式化(`of=/dev/`、`mkfs`、`wipefs`、`Format-Volume`、`Clear-Disk`、`diskutil eraseDisk`);③ **门控自身的配置**(`.dsh/auto-approve`、`auto-approve/allowlist`、`auto-approve/config.json`、`.dsh/profiles`、`.dsh/config.yml`、`cordis.patch.yml`)与**私钥/云端凭据**(`id_rsa`/`id_ed25519`/`id_ecdsa`/`id_dsa`、`.pem`/`.p12`/`.pfx`/`.jks`、`authorized_keys`、`.netrc`、`.git-credentials`、`.pypirc`、`~/.aws/credentials`、`~/.kube/config`)。公钥不算(`id_rsa.pub`),dd 写 `/dev/null` 这类伪设备不算。 **其余一律交给审核表按各行说明判**:递归删除(`rm -rf` 家族、`sudo rm`、`Remove-Item -Recurse -Force`、`rd /s /q`)、`chmod -R 777`、`git push --force`、`drop table`/`delete from`、`terraform destroy`、`docker system prune`/`volume rm`、关机重启,以及 `.env`/`.npmrc`/docker `config.json` 这类工具会自己改写的凭据文件——可再生成的目录与常规操作放行,删用户数据/源码、破坏生产或远端历史拒绝,拿不准转人工。被移出出厂表的词都在 `RETIRED_DEFAULT_KEYWORDS` 里留档,想恢复硬拒加回 `DEFAULT_DENY_KEYWORDS` 即可;自己的词也可以放**人工桶**(弹网页框问你,默认留空)。点文件按路径字段放宽匹配:`prod.env` 命中,命令里的 `process.env` 不误伤。 2. **审核模型**。一行只有 **id + 说明**:说明写「什么情况下选这个 id」,模型输出 id、风险等级和一句理由,动作由程序按 **(行, 等级)** 查三格执行。等级按「可回补性 + 影响范围」描述,与类别各自独立判断;模型没给等级、给了认不出的词、或自定义提示词没要求输出等级时,按设置页选的**兜底档**(默认 high)查格。审核模型看到与网页工具卡片相同的字段,**外加自定义工具自己的参数名**(MCP 的 `cmd`、嵌套的 `params.command`,按 `参数 <键>: 值` 追加在卡片末尾),并明确标记为不可信数据;模型理由与描述不算操作本体。类别必须**唯一**:剥掉卡片回显后,认得出的表内行只允许一条,出现两条互不相同的行(含「表内一条 + 认不出的标签行」)就按歧义失败关闭、落兜底行;等级同理,互相矛盾就作废、交给「等级认不出时按」那一档——**回显不能决定放不放行**。输出认不出时归到 `other`,不再直接转人工——它走的是 `other` 的三格。**判定压根没跑成**(超时、空输出、调用失败、没有可用路由、插件异常)是另一回事:这些不是模型的结论,**固定转人工**,不看 `other` 的三格。 默认审核表(id 是英文小写,会显示在审批历史里;说明与三格动作都可改;**出厂每一行都用同一套三格:low 允许 / medium 人工 / high 拒绝**——等级就是默认风险刻度;**other** 不能删除,但说明与三格都可改): | id | 什么情况下选它 | 出厂三格(low/medium/high) | |---|---|---| | deletion | 删除/清空/截断/不可逆覆盖用户数据、数据库、备份、历史、未提交内容(单文件源码编辑不算;能确认是本地/临时开发库的常规改动也不算) | 允许 / 人工 / 拒绝 | | credential | 改动密钥、token、证书私钥、`.env`、`authorized_keys`、`kubeconfig`、`~/.aws`、带 token 的 `.npmrc`、git/pypi 凭据 | 允许 / 人工 / 拒绝 | | remote | 写远程主机/生产/数据库,ssh/kubectl/云 CLI 变更,云资源删除(`terraform destroy`、`s3 rb --force`)、强制推送改写远端历史、破坏性 SQL、发包或生产部署(只读查询、普通 push、**能确认是本地/临时开发库的操作**不算;连接目标不明确时仍按本行) | 允许 / 人工 / 拒绝 | | system | 改 `/etc`、`/usr` 等系统目录、服务与防火墙、关机重启、用户与权限管理(含放宽到 777)、crontab、shell rc、系统级安装 | 允许 / 人工 / 拒绝 | | bulk | 递归/通配/循环删除或覆盖用户数据、源码、配置、未提交内容,格式化、向块设备写 `dd`、`git reset --hard`、`git clean`、`rsync --delete`、销毁数据卷(可再生成的依赖/构建/缓存/临时目录不算) | 允许 / 人工 / 拒绝 | | approval-config | 改插件自己的配置:allowlist、plugin config、profile patch(`cordis.patch.yml`,默认在 `~/.dsh` 下) | 允许 / 人工 / 拒绝 | | safe | 能确认是常规可回补操作:源码/文档/测试改动、构建产物、装依赖、清日志或缓存、清临时目录、**本地/临时开发库的常规改动**、可撤销单文件编辑(发包/提权/外发数据/拿不准不要选) | 允许 / 人工 / 拒绝 | | other | 以上条目全部不符合或无法确认(不能删除,但说明与三格和别的行一样可改) | 允许 / 人工 / 拒绝 | 风险等级默认三档(说明可改,等级 id 固定): | 等级 | 默认说明 | 出厂动作(所有行相同) | |---|---|---| | low | 可原样回补、且改动只落在本次工作区内(含工作区里的临时产物与构建产物) | 允许 | | medium | 可回补但需要额外步骤,或只影响本机配置、缓存、工作区外文件 | 人工 | | high | 不可回补,或影响远端、生产、他人、系统与凭据 | 拒绝 | **等级认不出时按**哪一档(`levels.fallback`,默认 `high`)是可配项:模型少写一行、写了个认不出的词(`critical`、`高危`),或自定义提示词压根没要求输出等级时都用它。 审核提示词语言没有独立开关:在「恢复中文/英文默认审核表」或「恢复中文/英文默认提示词」时选,选中的语言同时决定发给审核模型的框架、卡片文案和理由语言。恢复默认审核表只换表,不动自定义提示词;出厂中英包 id 与动作相同,**只有说明不同**。说明写清「什么情况下选这个 id」,它既是模型标准也是设置页文案。自定义提示词按语言分别保存,设置页编辑的是当前语言那一份。 转人工时弹的审批框里,**详情行由本插件渲染**:命令优先;没有命令就列路径 / 内容 / 参数摘要(参数多于 4 个、或内容过长,都会写明「共多少个 / 共多少字」,绝不静默少印)。DSH 自带的渲染只认顶层 `command`,`write`、`edit`、MCP 这类调用本来是空白的——现在也能看清要批准什么。详情行下还会写一行「**自动判定:…**」(关键词命中带词、审核表带类别与等级),因为原来那句话是请求方给的、插件改不了,只能在自己渲染的这一行补上。真的问过审核模型时,这一行下面再附一行「**审核理由:…**」——就是模型那次判定写的 `理由:`(单行、最多 200 字,截断会写明「共 N 字」;详情行的操作摘要占满 600 字额度时理由只留 80 字)。**判定压根没跑成**(输出为空 / 超时 / 调用失败 / 无可用路由 / 插件异常)时**不写**这一行:那时事件里那句是 `err.*` 闭集证据,不是模型说的理由,判决行那句「判定失败转人工」已经交代了结局,原始证据在「审批」tab 里。关键词人工桶那种压根没问过模型的转人工同样没有理由可写。 ## 审核模型:「模型默认」不等于不思考 「思考强度」只有「模型默认」(空值)与路由自报的档位两类选项。**「模型默认」不是关闭思考**:它的实现是**不往请求里放思考参数**,不表态 ≠ 关:模型按自己的默认跑,默认思考的模型会继续思考。而推理 token 与正文**共享同一个输出上限**,预算被推理吃光时 `finish=max-tokens`、正文为空 → 插件判定失败 → **固定转人工**(安全,但你会看到「怎么全都弹人工框」)。 插件为此做了两件事,装完不需要你懂上面这些: 1. **首轮预算给足、失败再按原因升级**:会推理的路由首轮就给 **8192**(可在设置页按 `judge.maxTokens` 调,区间 256–32768),不再用必被推理吃光的 1024;万一还是空输出,`finish=max-tokens` 的那次重试给到 `max(8192, 首轮×2)`(**必须严格大于首轮**——首轮默认已是 8192,固定值就不再是升级),`finish=stop` 的空输出仍按翻倍处理(那不是预算问题)。 2. **让失败可见、也能自检**:设置页「审核模型」卡片有**测试判定**按钮——拿一张固定的小卡片真跑一次,回显 `类别 / 正文长度 / 耗时`;失败时把现场写出来( `err.judgeEmpty finish=max-tokens ...`;本次运行出现过「空输出转人工」时,卡片顶部显示告警与计数(判定健康度是进程内的,重启 `dsh web` 清零)。 还是判不出来,三条路选一条: | 办法 | 怎么做 | 代价 | |---|---|---| | **换一个不默认思考的模型**(最稳) | 设置页 → 审核模型,选一个不推理的模型 | 判定质量看模型本身 | | 让它真的发「关」 | 该路由是 pi-ai 的通用 OpenAI 格式时,给这个模型的 `reasoningEfforts.off` 一个字符串 wire 值(如 `none`,原来通常是 `null`) | 取决于网关认不认;不认会 400 → 判定失败 → 仍然安全地转人工 | | 显式选低档 | 思考强度选 `minimal` / `low`(档位会真的发出去,比「模型默认」可控) | 还是会推理,只是更短 | 插件**不会**去改你的 `~/.dsh/settings.yaml`:那里是全部路由与密钥,档位该怎么配取决于你的网关。(历史上设置页还有过一个 `off` 选项:它与「模型默认」在适配层是同一个请求,却要走「档位必须在路由档位表里」那条校验——路由没把 `off` 列进档位表时每次判定都会以路由失败告终。现在手写进配置的 `off` 会被读成「模型默认」。) ## 拒绝之后:模型知道为什么,也能叫你来定 自动拒绝时,DSH 原本只会告诉模型一句 `the user rejected tool "…"`——**归因是错的**:关键词红线、审核表判定、插件异常在模型眼里全成了「用户拒绝」。本插件在那次被拒的调用后面补一条说明,写清是**机器判定**、以及原因(命中的关键词、审核表的类别 id 与等级、缺参/截断、判定失败的具体来源),并告诉模型下一步该怎么办。 原因只给**闭集派生**的信息:关键词是你自己词表里的词、类别 id 是你自己写的那一行。**审核模型那段「理由」原文不会回传**——它含命令片段与文件内容,把它灌回上下文等于开一次注入入口。 人工拒绝、转人工但没人应答,这两种也各有一句:它们同样被 DSH 渲染成「用户拒绝」,模型需要知道这次到底是谁否掉的。 ### 模型转人工(默认关闭) 设置页「模型转人工」卡片开启后,每次自动拒绝的说明里会多一条路:**如果模型认为这一步必须执行,它可以调用一个工具把这次操作转成人工审批**,由你决定放不放行。 - 开关打开后,人工审批框就是模型可以主动叫醒你的通道——包括它正被不可信内容驱动的时候。所以**默认关闭**,你自己决定。 - 转人工请求**永远由人决定**:不过关键词、不过审核表、不过三格。否则「转人工请求本身被自动拒绝」会变成死循环。 - 批准**只对同一个工具 + 完全相同的参数生效一次**。批准后模型立刻重试,那一次被放行;参数改一个字符就重新走完整管道判定。参数里若有被收集护栏丢掉的超大字段(单值或累计超过 8MB),凭证**不成立**——那次调用照常走闸门,因为那些字段既没上卡片也没进记录,你没看见过它们。 - **人工拒绝是终局**:同一个操作本会话不会再问你第二次,模型也会被告知别再试。 - 复核框里的话由插件拼成一句:**模型请求人工复核「工具名」。自动判定:{判决}。操作:{摘要}。模型理由:{理由}**。判决(命中关键词 / 审核表类别与等级)来自留档的机器结论,摘要由插件从原调用参数里压出来(超长时留首尾并写明「共 N 字」、参数多于 4 个写明「只列前 4 个」),理由才是模型自己的说辞(被截到 300 字时会写明「已截断」)。**这几段只能用「。」连成一句**:DSH 的审批框标题是普通文本节点,换行会被折成空格。原生越权框那条路里,插件接管了详情行并会再补一行「自动判定:…」;DSH 自带的渲染只认顶层 `command`,所以 `write`/MCP 那类调用本来没有详情行。 - 工具名可改(默认 `request_human_approval`),改完需重启 `dsh web`;提示语言(中文/英文)独立于审核提示词语言。 ## 数据 位于 `$DSH_HOME`(默认 `~/.dsh/`),不进 git: | 路径 | 用途 | |---|---| | `auto-approve/allowlist.json` | 关键词、审核表(三格动作)、风险等级说明与兜底档、超过送审上限 / 撞收集护栏时的动作、**审核超时**(设置页写这里,运行时以此为准) | | `auto-approve/config.json` | 审核模型、提示词语言与自定义提示词、**送审内容上限**(默认 20000)、模型转人工,以及预设沙箱等插件配置(`judge.timeoutMs` 只作缺省) | | `auto-approve/audit.log` | `ALLOW` / `REJECT` / `HUMAN` / `FAILED` / `OUTCOME` | | `auto-approve/events.jsonl` | 审批 tab 事件 | 损坏文件不会被覆盖。卸载插件不会删除这个目录。 ## 配置 ```yaml - id: dsh-auto-approve config: onlyAutoApprovePreset: true presetSandbox: workspace-write # 或 read-only judgePromptLang: zh # 当前提示词语言:由设置页「恢复默认」时选择,一般不用手改 judgePrompts: # 可选:按语言覆盖提示词模板;'' = 用出厂模板 zh: '' en: '' judge: provider: '' model: '' reasoningEffort: '' timeoutMs: 20000 # profilePatch: /home/me/.dsh/profiles/web/cordis.patch.yml # 可选:显式指定 patch 文件 ``` 审核模型字段为空则跟随部署默认。没有可用路由时当次转人工。设置页「审核超时」写入 `allowlist.json`,运行时以此为准。 「自动审批」预设写进当前运行 profile 的 patch 文件(路径从 profile 目录 `ctx.baseUrl` 推导);profile 不放在默认位置时可用 `profilePatch` 显式指定。 patch 是按 id **整块替换** `config` 的,所以这个 profile 用的权限表就是插件写进去的那份副本(出厂预设 + auto-approve)。DSH 升级后如果出厂预设表新增/改名,插件启动时会和 `@deepseek-ai/dsh-base` 的 patch 比对,把缺的名字打进日志,并在设置页显示一张提示卡片(更新插件或手工合并那一行);插件不会自动改写你的文件。 ## 开发 ```sh node --test tests/*.test.mjs npm run check # 语法检查 + 文案同步校验 npm run locales:sync # 按 locales.mjs 重新生成 client.js 内联字典 ``` 见 [AGENTS.md](AGENTS.md) 与 [CHANGELOG.md](CHANGELOG.md)。 ## 推荐搭配 [dsh-message-push](https://github.com/DNAlec/dsh-message-push) 本插件把绝大多数工具调用自己判掉了,只有拿不准的才会回到网页审批框或提问。**dsh-message-push 负责在这种时候把你叫回来**,长任务跑着你可以离开屏幕:会话停下(完成 / 中断 / 阻塞 / 出错 / 触顶 token)推一条,**待审批**和**待回答**则立即推,推到你配置的消息平台。 两者是刻意分开的:本插件负责回答审批(允许 / 拒绝,否则把请求交回原网页框),dsh-message-push 只旁路观察,不做任何决定,也不会把回复注入会话。它自己还对外提供 `messagePush` 服务,别的插件可以复用它的渠道。 ```sh dsh plugin --profile web add github:DNAlec/dsh-message-push ``` 装完重启 `dsh web`,在设置页 → **消息推送**里至少连通一个渠道;支持哪些平台见它自己的 README。 ## 许可证 [MIT](LICENSE)