dsh-tiered-approval
自动放行安全的,拦下不可逆的,拿不准的问人。
🛡️ 静态规则安全网 · 🤖 LLM 审查员 · 🙋 人工兜底
为 DeepSeek Harness(DSH)而写的分级自动审查插件
[!WARNING]
本插件是纯 vibe coding 写的
代码、配置 schema、这份 README 都是 AI agent 生成的,几乎没有人工 review:未经过安全审计、与 DeepSeek 官方无关、不提供任何担保。它把关的是安全决策,请把它当起点,而不是信任边界——
风险自负。review、审计、PR 都特别欢迎。
目录
这是什么
DSH 没有内置自动审批器,只有两个极端:每次越界都弹窗(烦),或全放权什么都不问(怕)。
这个插件填上中间地带:在每个工具调用真正执行前加一道三层裁决——静态规则先拦下不可逆的,LLM 审查员再看一遍拿不准的,剩下真正有疑问的才交回给你。
装完即生效,默认行为就是安全值,不需要你写一条规则。
为什么需要它
| 原生状态 | 体验 | 本插件 |
|---|
ask(审批策略) | 每个逃出沙箱的操作都弹确认 | 静态规则 + 审查员替你裁决,只剩真疑问 |
never / 全放权 | 什么都不问,出事故没兜底 | 不可逆操作被安全网直接拒绝,不弹窗也不花 token |
| 权限预设(read-only / workspace-write / full-access) | 只换沙箱边界,审查强度不变 | 审查强度自动跟随预设(见 perMode) |
它如何工作
DSH 留了两个官方接缝(tools/pre-execute 门禁 + approval/request 应答者),本插件各占一个:
工具调用
│
▼
tools/pre-execute 门禁 ── 能看到完整参数(命令文本、目标路径、升权模式、理由)
│ 第一层 静态规则(零成本、确定性)
│ 命中 deny 规则 ──► 直接拒绝,不弹窗(内置安全网)
│ 命中 allow 规则 ──► 打 "allow" 印记,放行
│ 第二层 LLM 审查员(策略跟随当前 Access 模式)
│ 裁决 allow ──► 打 "review-allow" 印记,放行
│ 裁决 deny ──► 直接拒绝(理由返回给模型)
│ 裁决 ask ──► 升权调用:放行到工具本体弹一次人工;其余:门禁直接问人工
│ 审查不可用/超时/解析失败 ──► 静默回退,不新增弹窗
│ 其余 ──► 保持默认放行,打 "none" 印记
▼
工具本体(例如 pwsh 升权时)──► 发起 approval/request 审批请求
│
▼
approval/request 应答者 ── 依据印记 + 文本规则自动回答
│ reason 命中 denyJustifications ──► rejected(优先级最高)
│ 印记 deny ──► rejected
│ 升权请求:
│ 印记 allow 且模式 ∈ answerer.allowModes ──► allowed-once
│ 印记 review-allow 且模式 ∈ 当前模式的 review.allowModes ──► allowed-once
│ reason 命中 allowJustifications 且模式 ∈ answerer.allowModes ──► allowed-once
│ 其余 ──► next() → 人工审批 UI(现状不变)
三个设计点:
- 印记(stamp):
approval/request 请求不带工具参数,危险与否只能在门禁里用完整参数判断——门禁把结论打在 callId 上,应答者凭印记兑现。
prepend 注册:web 部署里 dsh-host-apiproxy 有个挂起等人工的"终端"应答者,本插件必须排它前面,未命中的请求才能流回人工。
- 一次性授权:
allowed-once 只管这一次调用,没有 allow-always / 记住授权。
高亮特性
- 静态安全网优先 —— 试图逃出沙箱的破坏性命令(递归删除、格式化磁盘、强推 git、注册表删除、写系统目录)在任何模型调用之前被拒绝:不弹窗、不花 token;
- LLM 审查员(上下文感知) —— 小模型对规则未裁决的调用做全参数审查(
allow / deny / ask),并结合会话里最近一条用户消息判断命令是否对齐用户意图;复用会话自己的模型路由,不开子 agent、不写会话日志;
- self-kill 守卫 ——
taskkill / Stop-Process / killall / pkill 一类进程终止命令确定性拒绝(防 agent 杀掉自己的宿主;后台任务用 job_kill,kill <pid> 保留为逃生口);
- L0 双保险 —— 静态 deny 除了瀑布最前的监听器,还注册了单调
tools.guard(),其他插件旁路不掉这条硬底线;
- 跟随预设 —— 审查强度自动跟随 Access 选择器(Read only / Workspace write / Full access);Full access 下默认全量审查,因为沙箱已经不隔离了;
- 全自主可切换 ——
review.onUncertain: 'deny' 开启两态模式(审查不确定/失败一律拒绝,无人参与);默认 'ask' 保留人工兜底;
- 快捷开关 —— 输入框直接
/auto-review on|off|rules|tiered|auto|status|reset 切档位、看统计,无需改配置(内存态,重启重置);
- 不加弹窗 —— 未命中的请求行为与原生 DSH 完全一致;
- 可插拔 —— 标准 cordis bundle,
dsh plugin add 安装,卸载自动 dispose 全部监听器。
看它工作
真实日志(进程日志里的 [auto-approval] 行):
[auto-approval] deny pwsh <callId>: auto-review: escalated destructive command is refused without prompting (irreversible)
[auto-approval] review-allow pwsh <callId>: install dependencies
[auto-approval] allow escalation pwsh <callId> -> workspace-write
[auto-approval] reviewer call failed: provider exploded
[auto-approval] review inconclusive for pwsh <callId>; falling back
一条被拦住的命令(Remove-Item -Recurse + 升权,静态层直接拒绝——安装时它拦过我们自己的部署命令 😄):
→ pwsh(
command="Remove-Item C:\Users\x -Recurse -Force",
sandbox_permissions="danger-full-access",
justification="clean up"
)
→ Error: auto-review: escalated destructive command is refused without prompting (irreversible)
快速开始
最小配置就是默认配置——装完重启即生效,行为即"安全模型"一节:
# ~/.dsh/profiles/<profile>/cordis.patch.yml(手动挂载时)
- insert:
- id: tiered-approval
name: 'dsh-tiered-approval'
可复现验证三步:
- 插件清单页出现
tiered-approval;
- 进程日志出现
[auto-approval] 决策行;
- 试一次"升权 +
Remove-Item -Recurse"——被直接拒绝且不弹窗(静态安全网生效)。
之后按需调 config:(见「配置」),改完重启生效。
快捷开关(/auto-review)
不想改配置也能随时切审查档位——在输入框直接输入斜杠命令(和 /permission 一个玩法):
| 命令 | 效果 |
|---|
/auto-review 或 /auto-review status | 显示当前档位、审查模式、决策计数和最近决策 |
/auto-review on / /auto-review off | 打开 / 关闭 LLM 审查(off = 纯静态规则) |
/auto-review rules | 纯规则档(等价 off) |
/auto-review tiered | 三态档(默认):静态 → LLM → 人工兜底 |
/auto-review auto | 两态自主档:审查员不确定 / 失败一律拒绝,无人参与 |
/auto-review reset | 回到配置文件里的设置 |
- 内存态:开关只对当前进程生效,重启后重置回配置;要持久化就改
config:。
- 优先级:开关 > 配置 > perMode 默认。
- 档位映射:
rules = review.mode: off;tiered = on + onUncertain: ask;auto = on + onUncertain: deny。
安装
本包是标准 bundle(package.json 声明 dsh.bundle,携带自己的 cordis.patch.yml 层),按官方发布文档(docs/user/develop/basic/publish.md)安装。
方式一:官方 dsh plugin(推荐)
在包含本包目录的上层目录执行:
dsh plugin --profile web add ./dsh-tiered-approval
dsh plugin add 会把包链接进 profile 的 node_modules,并把本包追加到 dsh.profile.bundles(其 cordis.patch.yml 层自动挂载 tiered-approval 行)。前置条件:profile 目录里需要 pnpm 可用(dsh plugin 转发给 pnpm)。
npm 发布后,一行即可:dsh plugin --profile web add dsh-tiered-approval。
方式二:从 GitHub 安装
dsh plugin --profile web add github:Elaina-real/dsh-tiered-approval
本包是编译好的 JS(lib/ 已提交在仓库里),没有 TS 源码 + 构建步骤,所以不需要 prepare 脚本,也不需要 allowBuilds 放行——比 TS 源码包少一道安全门槛。
方式三:手动拷贝(无 pnpm 时兜底)
把整个目录拷到 ~/.dsh/profiles/<profile>/node_modules/dsh-tiered-approval(profile 用 hoisted pnpm 布局,拷贝即可被 loader 解析),并在 profile 的 cordis.patch.yml 里 - insert: 挂载行。
验证与卸载
- 验证:
dsh --profile web --dump-config 应出现 # == dsh-tiered-approval 层;或装完重启后看插件清单页 / 日志。
- 注意:插件代码在 node_modules 里,HMR 不追踪 node_modules——改代码必须重启;改
cordis.patch.yml 配置可能热生效,但别依赖。
- 卸载:见下一节。
卸载 / 禁用
- 临时禁用:bundle 行加
disabled: true(或在 profile 的 patch 里覆盖 tiered-approval 行),重启。
- 彻底卸载(bundle 方式):
dsh plugin --profile web remove dsh-tiered-approval,重启。所有监听器随 cordis fiber 自动 dispose。
配置
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- insert:
- id: tiered-approval
name: 'dsh-tiered-approval'
config:
builtinDeny: true # 内置破坏性命令安全网总开关(默认开;建议永远别关)
builtinDenyRules: [] # 内置危险命令规则列表(默认 = 代码内置那组;可在这里增删改,无需改代码)
log: true # 记录每一次自动决策
deny: [] # 追加硬拒绝规则(命中即拒、不弹窗)
# 例如:
# - tool: pwsh
# where:
# command: ['git\\s+push.*(--force|-f\\b)']
# reason: '禁止强推'
allow: [] # 自动放行规则(配合应答者生效)
# 例如(门禁判定安全,且升权模式被允许时自动批准):
# - tool: pwsh
# where:
# command: ['pnpm\\s+install|npm\\s+install']
# escalating: true
answerer:
allowModes: ['workspace-write'] # 规则层:默认不含 danger-full-access
allowJustifications: [] # 理由命中 ⇒ 自动批准
denyJustifications: [] # 理由命中 ⇒ 自动拒绝(优先)
# 例如:
# denyJustifications:
# - 'drop\\s+database'
# - 'DROP\\s+TABLE'
# - '删除.*(数据库|生产|整个)'
review: # LLM 审查层(默认开)
mode: 'on' # 'off' = 纯规则版
# provider: 'deepseek-official' # 显式路由(必须与 model 成对)
# model: 'deepseek-v4-flash'
skipTools: [read, read_image, glob, grep, web_search,
job_output, job_list, job_kill, ask_user_question,
todo_write, list_agents, interrupt_agent]
skipNested: true # 跳过 run_code 子分发
allowModes: ['workspace-write', 'danger-full-access'] # 审查员可批准的模式
onUncertain: 'ask' # 'ask'=不确定交人工(默认);'deny'=全自主两态,直接拒绝
timeoutMs: 20000
maxTokens: 512
maxInputChars: 12000
perMode: {} # 按沙箱模式覆盖(见下)
规则字段(deny / allow 共用)
| 字段 | 说明 |
|---|
tool | 工具名,* 匹配所有工具(pwsh、bash、write、edit、read、glob、grep…) |
where | 参数匹配表:键是参数的点路径,值是正则列表;所有列出的键都命中才匹配 |
escalating | true ⇒ 只匹配带了 sandbox_permissions(升权)的调用 |
reason | 拒绝时的说明(作为工具错误返回给模型 / 写进日志) |
正则大小写不敏感;YAML 里反斜杠要写双份(\\s)。所有正则加载期校验:写错插件直接拒绝加载,不会静默不匹配。
跟随 Access 选择器(perMode)
审查策略自动跟随会话当前的沙箱模式——界面 Access 下拉(Read only / Workspace write / Full access)会把 sandbox/mode 写进会话,插件每次调用都按当前模式重新解析策略:
review:
perMode:
'read-only': # 只读下保持审查(命令仍会执行,只是改不了文件)
mode: 'on'
'workspace-write': # 标准模式,默认即可
mode: 'on'
'danger-full-access':
# 内置安全默认:若从未自定义过全局 skipTools,Full access 下全量审查
# (skipTools: [])——沙箱不隔离了,审查是静态规则之外的最后防线。
# 想放开就在这写:
# skipTools: [read, glob, grep]
mode: 'on'
| 当前 Access | 默认审查行为 |
|---|
| Read only | 开;跳过只读/搜索/任务控制类工具 |
| Workspace write | 开;跳过只读/搜索/任务控制类工具(同全局默认) |
| Full access | 开且全量(什么都不跳过);全局 skipTools 自定义过则跟随全局 |
[!WARNING]
Full access 预设同时把审批策略设为 never(无人工弹窗)。此时审查员裁决 ask 不会弹窗,而是变成确定性拒绝(fail-closed)。想要人工兜底,就别把会话切成 Full access。
LLM 审查层说明
- 上下文感知:审查输入帧除了工具名 + 完整参数 + 沙箱模式/工作区根,还带会话里最近一条真人消息(
source.kind === 'user',注入的 skill/通知类上下文会跳过),让审查员判断命令是否对齐用户意图。
- 路由:默认取第一个已注册 provider 的第一个模型;想指定就配
review.provider + review.model(必须成对)。
- 成本:每个规则未裁决的调用 = 一次小模型调用(输出 ≤
maxTokens,输入帧截断到 maxInputChars);skipTools 和 skipNested 防止审查调用被放大。
- 故障行为(默认
onUncertain: 'ask'):审查员不可用 / 超时 / 输出畸形 ⇒ 静默回退(记日志,不新增弹窗);静态安全网照常兜底。只有审查员明确裁决 ask 才弹人工。
- 全自主两态(
onUncertain: 'deny'):审查员 ask 与审查失败都变成确定性拒绝(fail-closed),全程无人参与——适合你已经信任模型判断的场景;代价是审查员拿不准的调用会被拒绝而不是问你。
- 收紧:把
review.allowModes 改成 ['workspace-write'],danger-full-access 升权就回到人工审查。
安全模型与默认值
| 情况 | 默认行为 |
|---|
| 工作区内正常操作(读写/搜索/构建) | 静态规则放行;未裁决的交给 LLM 审查员 |
升权到 workspace-write(如 read-only 会话) | 规则或审查员判定安全 → 自动批准 |
升权到 danger-full-access | 规则层:默认不自动批准;LLM 层:审查员允许则自动批准(review.allowModes 默认含它) |
| 升权 + 破坏性命令(递归删除、格式化、强推、注册表…) | 静态层直接拒绝——不弹窗、不调模型 |
升权写入系统目录(C:\Windows、/etc…) | 静态层直接拒绝——不弹窗 |
进程终止命令(taskkill / Stop-Process / killall / pkill) | self-kill 守卫静态拒绝(后台任务用 job_kill;kill <pid> 是逃生口) |
规则版与 LLM 版自由混用:review.mode: 'off' = 纯规则版;开着 LLM 层时静态安全网仍然先执行(不可逆操作从不消耗审查 token)。
权限与数据
| 内容 | 说明 |
|---|
| 读取 | 每个工具调用的完整参数(命令文本、路径、升权理由等)——在门禁内读,仅用于裁决,不落盘 |
| 发送给模型 | 开启 LLM 审查时,把参数帧(工具名、参数、沙箱模式/工作区根、最近一条用户消息)发给你配置的模型 provider(默认与会话同款路由)——参数和用户消息里可能含敏感文本,请知情 |
| 网络 | 无独立网络访问;只通过 DSH 的 ctx.llm 服务发模型请求 |
| 凭据 | 不读取、不存储任何凭据;~/.dsh/.credentials.yaml 由 DSH 凭据服务管理,本插件不触碰 |
| 文件写入 | 无(仅日志由 dsh 进程统一输出) |
| 会话日志 | 不写 session 事件;仅通过插件 logger 输出 [auto-approval] 行 |
| 卸载 | 全部监听器随 fiber dispose,无残留状态 |
兼容性
| 项 | 说明 |
|---|
| DSH 版本 | 针对 @deepseek-ai/dsh 0.1.0-rc.6 开发与验证 |
| 依赖 | @deepseek-ai/cordis ^4.0.1、@deepseek-ai/schemastery ^3.18.1、@deepseek-ai/dsh-llm ^0.1.0-rc.6、@deepseek-ai/dsh-timeout ^0.1.0-rc.6 |
| 平台 | Windows(pwsh 规则集,实测);POSIX 走 bash 规则集,理论上兼容(未实测) |
| 最后验证 | 2026-08(npm test 全绿;冒烟测试 38 项) |
mainline 迭代很快,兼容性结论可能过期;升级 DSH 后请重跑 npm test 并试一次门禁行为再依赖。
边界与限制
- 本插件是纯 vibe coding 写的——见顶部警告。 未审计、非官方、无担保。
- 应答者的文本规则(
allowJustifications / denyJustifications)只匹配模型写的一句理由——真正的判断在门禁(静态规则 + LLM 审查员),那里才有完整参数。
- LLM 审查是概率性的,不是证明;规则是人写的,可能漏掉新姿势;来自文件/网页/工具输出的 prompt injection 可能把 agent 往越界方向带,命令级审查员不一定看得出来。
run_code 子分发经过门禁(印记覆盖),但 LLM 审查默认跳过嵌套调用(skipNested: true),只审查外层 run_code 本身。
- 委派的子 agent 不受影响:DSH 默认把子 agent 的审批策略钉死为
never。
日志与排查
log: true(默认)时,每次自动决策都会写进 dsh 进程日志:
[auto-approval] deny ... —— 静态或审查拒绝
[auto-approval] review-allow / review-deny / review-ask ... —— 审查员裁决
[auto-approval] allow escalation ... -> <mode> —— 自动批准了一次升权
[auto-approval] reviewer call failed ... / review inconclusive ... —— 审查层故障(已静默回退)
排查时 grep [auto-approval],看是哪一层、哪条规则/理由做的决定。
| 症状 | 排查 |
|---|
插件清单里没有 tiered-approval | 重启服务;确认包在 node_modules、patch/bundle 行正确;看启动日志是否报 "plugin failed to load"(如配置 schema 校验失败) |
| 升权不再自动批准,或弹窗变多 | 检查当前 Access 预设(Full access 下 ask 是确定性拒绝);确认 review.mode 与 allowModes |
| 规则没生效 | 配置正则加载期已校验(非法会拒绝加载);确认 tool 名、where 键路径、YAML 转义(\\s) |
| 审查延迟高 | 加了 skipTools 仍慢的工具;或 review.provider/model 指向慢模型;看 timeoutMs |
| 想彻底回滚 | 按「卸载 / 禁用」移除 bundle 或手动行 + 重启;删除包目录即可 |
测试
包内带 test/smoke.mjs —— 38 项断言驱动 apply()(假 ctx:假 llm 服务返回预置裁决、假 sandboxPolicy),覆盖静态层、审查员 allow/deny/ask/故障、skipTools / skipNested、perMode 跟随预设、应答者的升权规则和配置默认值。
独立运行(devDependencies 已声明,不需要 DSH profile):
npm install
npm test # 期望 "ALL PASS"
推送到 GitHub 后,.github/workflows/test.yml 会在每次 push / PR 上自动跑冒烟测试(Node 20 和 22)。
冒烟测试 ≠ 安全审计——用之前先为你的工作流补用例。
贡献
- Issues:bug、建议、看不懂的报错、文档疑问——开一个 就行;
- PRs:欢迎,尤其是 review / 审计——一个 vibe coding 产物最缺的就是人眼;
- 安全相关问题:开 issue 时标注
security,或先私下联系作者。
License
MIT —— 但请看顶部的 vibe coding 警告:风险自负。
如果它帮你少点了很多次鼠标,⭐ 一下 就是最好的支持。