dsh-perm-gate
v2.6.2
Published
DSH permission-gate for the DeepSeek Harness 0.1.2+ line: a single self-sufficient, deterministic-first, fail-closed gate covering P0 hard-deny -> P1 session grant -> P2 static rule (allow/deny) chain -> P3 optional LLM semantic classifier -> P4 ask, with
Maintainers
Readme
dsh-perm-gate
- English README
- 中文 README
- 日本語 README
- 한국어 README
- Installation guide
- 中文安装指南
- 日本語インストールガイド
- 한국어 설치 안내
- Changelog
- 日本語 changelog
- 한국어 changelog
兼容性说明: v2.0.0 自带
ja/ko字典,但官方 DSH 的LocaleRuntime只暴露zh/en(LOCALE_IDS = ["zh", "en"])。在原版 DSH 上选择ja/ko会报locale "<id>" is not registered。请使用更新了LOCALE_IDS(locale-settings.ts)与LOCALES标签(client/index.ts)的 DSH fork 并重新构建。
▼ DSH 版本适配
两个 DSH 版本线从两个长期分支分别维护,各有一套版本号系列、
engines.dsh和 npm 分发标签(发布布局):| DSH 版本 | 分支 | 版本号 | npm 标签 | | --- | --- | --- | --- | | 0.1.0-rc.7 ~ 0.1.1-rc.x |
legacy|1.x|@legacy| | 0.1.2-alpha.1 ~ 0.1.5-rc.0 |main|2.x|@latest/@dsh-0.1.2(@2.x是范围) | | 0.1.5-rc.1+ |sync/0.1.5-from-main(=compat/0.1.5) |3.x| GitHub ref 安装(暂无 dist-tag) |版本序列号跟的是 DSH 线(
1.x= DSH ≤ 0.1.1,2.x= DSH 0.1.2+),两条大版本互 相隔离:锁在^1.x的安装绝不会解析到2.x,反之亦然。engines.dsh表达同样的 分界,但 DSH 从不读取它——真正把旧 DSH 钉在1.x上的是版本范围与 dist-tag。
@deepseek-ai/dsh-client-runtime在0.1.2-alpha.1中已被移除——不仅仅是更名。legacy线仍通过它访问ctx.slots;main从@deepseek-ai/dsh-client-ui-renderer/client获得相同的声明。两处版本敏感 接缝通过能力探测处理,而非版本号检查:(1)设置注册使用register,两线 都存在(installSection是新增项,不是替代);(2)effectivePolicy在两 线上都是 user-approval 服务的私有方法,因此通过typeof探测读取,缺失 或抛错时降级为「策略未知」。
版本 2.6.1 —— 变更见 Changelog。
一个单一自足、确定性优先、fail-closed 的 DeepSeek Harness 权限门插件。
对每个工具调用按固定优先级链裁决:
| 阶段 | 决策 | 含义 |
| ---- | ---- | ---- |
| P0 | deny | 确定性硬拒:凭据材料 / 受保护路径改写 / 危险 shell |
| P1 | allow | 精确、有界的会话放行 grant |
| P2 | deny/allow/ask | 静态规则链:黑名单优先,其次 allow,再 ask |
| P3 | allow/deny/ask | 可选 LLM 语义分类器(默认关闭) |
| P4 | ask | 官方 approval seam |
严格 fail-closed:P0 永不因 grant / 规则 / 分类器 / 人工而放行。
特性
- 命令白/黑名单 — 基于 argv 分解匹配(非裸字符串),递归下钻
sh -c/bash -c、识别管道、重定向目标、递归/强制(rm -rf)。 - deny 优先 — 命中黑名单即拒绝,胜过任何 allow。
- 会话放行 — 精确的
(工具, 规范化 fingerprint)grant,带TTL+maxUses;换目标绝不复用。子代理继承但不可自授。 - 纯函数规则引擎 — glob/regex 编译 + ReDoS 上限、坏规则 loud fail、按源内容哈希缓存。
- 审计 — 每次决策写为
{ignorable:true}事件并带callId;模型可见理由与记录一致。 - 自动审查档位(机器值
permissive)——一个独立审批模式(区别于只读、完全权限与白名单档),既不是"自动审批",也不授予泛化权限。前端只暴露一个开关(permissive),后台四个审批策略可组合、由插件设置决定——仍对 P0 保持 fail-closed。权限下拉框与设置行都按产品名「自动审查」显示;图标见下文(内置档自带,插件档需补丁)。 - 沙箱提权自动答复(
trustEscalation)— 沙箱提权是从 shell / pwsh / edit 工具体内部(tools/pre-execute之后)发出的,所以门禁从未见过它,一个它自动放行的调用仍会弹出确认。开启后,门禁以callId精确匹配已放行调用并直接答复。
界面预览
设置卡片 —— 自动审查档位:前端只暴露一个开关,后台四个策略可组合;LlmAssist 可跟随会话当前模型,也可钉住 Provider / Model,并自带健康测试与裁决学习:

门禁实际加载的规则——直接读自 dsh-perm-gate-rules 设置命名空间;面板按设计只读,改不了它显示的内容:

权限审批记录——本会话每一次裁决,最新在上,附带模型看到的理由:

裁决条——自动放行会写明依据(只读工具,或 LLM 判定 safe);停摆也绝不静默:

安装
需要先安装 DeepSeek Harness。
dsh plugin --profile web add dsh-perm-gate完整的安装、升级、迁移与排查步骤见中文安装指南(另有 English / 日本語 / 한국어)。
配置
cordis.yml:
- id: dsh-perm-gate
name: dsh-perm-gate
config:
rulesFile: ./permissions.yaml # 可选;默认 $DSH_HOME/perm-gate/rules.yml
dshHome: $DSH_HOME
defaultAction: ask
gatePresets: [permissive, permissive-full] # 门禁生效的档位(默认值)
sessionSweep: true # 每小时清理已归档/已删除会话的门禁数据会话清扫(session sweep)
插件启动时及每小时读取 DSH 的工作区存储($DSH_HOME/storages/workspace.json,只读),
对门禁持有授权链数据的每个会话做归类。已被 DSH 归档(global.archivedSessionIds)
或彻底不存在的会话,其决策事件会从 $DSH_HOME/perm-gate/events.jsonl 中移除,
其决策前文件快照会从 $DSH_HOME/perm-gate/snapshots/ 中删除——宿主已视为消失的数据,
审查页也不再保留其历史。活跃会话不受影响;无法归属的行(空 sessionId)永不删除;
任何失败都 fail-open:本轮跳过,一小时后重试。设 sessionSweep: false 关闭;
workspaceStoreFile 可覆盖存储路径。恢复归档会话不会找回已被清扫的历史。
规则示例:见 examples/permissions.example.yaml。
网络策略(可选开启)
本地 HTTP/CONNECT 代理,用同一份规则文件审查 shell 子进程的出站流量,并对无规则 覆盖的目标提供审批通道。默认关闭 —— 开启后会绑定回环端口并改写子进程的代理环境变量, 因此绝不隐式启用。
- id: dsh-perm-gate
config:
networkEnabled: false # 总开关(默认 false)
networkMode: whitelist # deny-all | whitelist | allow-all
networkUnlisted: ask # ask | deny —— 未列出目标的处理方式
networkUnattributed: allow # allow | deny —— 无 shell 归属的流量
networkInjectEnv: true # 为子进程改写 HTTP(S)_PROXY / ALL_PROXY
networkAskTimeoutMs: 120000 # 审批等待上限,超时按拒绝处理
networkGrantTtlMs: 1800000 # 一次批准的会话有效期分层行为:没有 allow 规则,任何目标都出不去。未列出的目标会升级到交互审批,挂在该
shell 命令的会话上;批准后该目标在本次会话内放行。deny 规则永不升级为审批 —— 审批
只能为「无规则禁止的目标」拓宽可达性,永远不能推翻一条说「不」的规则。
边界 —— 依赖它之前请先读这段:代理是协作式策略层,不是强制边界。它只能看到 愿意读代理环境变量的客户端的流量。
| 客户端 | 能拦吗 |
|--------|--------|
| curl、wget、git、Go net/http、Python requests | ✅ |
| Node.js http / https / fetch | ❌ 直连,代理看不到 |
| Java(未加 -D 代理参数)、.NET HttpClient | ❌ |
| 原始 socket、自写 TCP | ❌ |
| DNS、QUIC/HTTP3、非 HTTP 协议 | ❌ |
| 连接字面 IP | ❌ |
因此 node -e "require('http').get('http://host/')" 这类命令不会被拦截。请把它当作
「防误操作的护栏 + 声明意图的地方」,而不是密闭沙箱。
DSH 自身的网络流量 —— 内建网络工具与 LLM 传输 —— 刻意不管:这些连接不带 shell 归属,
而 networkUnattributed: allow(默认)会直接放行。审查它们会导致宿主把自己拦死,
那比漏拦严重得多。只有在你确定宿主的客户端不读代理环境变量时,才考虑改成 deny。
实时状态查询:GET /api/dsh-perm-gate/network(模式 / 绑定 / 端口 / 代理存活 / 环境注入
状态 / 阻断计数 / 最近阻断)。
自动审查档位(机器值 permissive)
自动审查是权限下拉框里一个独立审批档,与只读 / 工作区内修改 / 完全权限 / 白名单平行。 它不是泛化的"自动审批"、也不授予泛化权限:只会在人类/LLM 接缝之前收窄或放宽决策, P0 硬拒绝在本门禁自身的档位作用域内始终单调且不可协商。
P0 是档位作用域内的,不是全局的。 门禁只在会话权限档位属于
gatePresets(默认permissive/permissive-full)时生效。其他档位 —— 只读、工作区内修改、完全权限 —— 下整个门禁停用,包括 P0 硬拒绝,因为该档位自身的策略接管了这个会话。这是刻意设计 (见配置表的gatePresets),但也就意味着「P0 不可协商」成立于门禁的档位之内,而非所有档位。 停用不是静默的:每次会话档位切换会记录一条stand-down事件,浏览器在输入框上方常驻一条 GATE OFF 提示条。把gatePresets设为['*']可让 P0 重新变成全局。
提供两个变体 —— 因为预设的 sandbox 与 approval 是两根独立旋钮,把它们绑死会逼出
一个糟糕的取舍:
| 下拉框名称 | 机器值 | sandbox | approval |
|-----------|--------|---------|----------|
| 自动审查 | permissive | workspace-write | ask |
| 自动审查(高权限) | permissive-full | danger-full-access | ask |
普通档保留内置文件沙箱。而那个沙箱同时拒绝子进程启动所需的命名管道 —— 所以 git clone、
MSYS2/Cygwin 的 sh.exe、ConPTY 都会以 Win32 error 5 / couldn't create signal pipe 失败。
又因为门禁只在 gatePresets 列出的档位里生效,想用门禁就必须接受这个限制。
「自动审查(高权限)」解开了这个耦合:审批行为完全相同,但不限制文件沙箱 —— 档位自带的描述已把代价
写明:流程更顺畅、审批仍逐次生效,但不再有系统沙箱兜底。两者都在默认
gatePresets 里,任选其一都能获得完整的 P0–P4 链路 —— 门禁只读预设的名字,从不读 sandbox 模式。
下拉框里的名字是宿主提供的产品名,不是逐语言的字典项:DSH 对插件档位在两个权限界面上
(通用设置默认档行、输入栏权限选择器)都原样渲染补丁里的 name:,只给三个内置档提供自己的本地化
标签,因此 cordis.patch.yml 直接写中文名,对所有会话一致。
图标是另一回事。 输入栏的图标表是闭合的,表自己的注释写明了规则:host-configured names
outside the design set get none。两个档位都不是内置值,图标全靠
npx dsh-perm-gate-patch-glyph 往那张表里补两项 ——「自动审查」复制 workspace-write 的
盾+铅笔,「自动审查(高权限)」复制 danger-full-access 的盾+感叹号,各自对应自己实际共用的
文件沙箱。该补丁改的是宿主包,每次 DSH 升级都会丢 —— 见
DSH 升级后:重打输入区图标补丁。
cordis.yml:
- id: dsh-perm-gate
name: dsh-perm-gate
config:
rulesFile: ./permissions.yaml
defaultAction: ask
permissive: true # 前端唯一的开关(启用独立档)
permissiveStrategies: # 后台策略,可组合
trustAutoAllow: true # 作用域内安全操作自动放行;危险/未知转 ask
alwaysConfirm: false # 一律逐次 ask;允许控件附带重复允许/迁白名单按钮
trustEscalation: true # 门禁已放行的调用,其自身的沙箱提权免确认
llmAssist: false # 先由 LLM 分类裁决;ask/无分类器时回退到人工(llmAssist 的真实接收 LLM 在设置页填 classifierEndpoint / classifierModel,OpenAI 兼容的自定义 API
均可。设置页可选择接收来源:自定义 API(任何 OpenAI 兼容端点,内置小米 MiMo https://api.xiaomimimo.com/v1 等预设)或宿主模型组(复用 DSH 会话已配置的 llm 服务与当前模型组,可用 classifierProvider / classifierModel 覆盖);并提供健康测试按钮,一键验证接收 LLM 的连通性与延迟。)
trustAutoAllow 是中间档基线(rule-allow 自动放行)。alwaysConfirm 让每次越界都走审批面板,其
「允许控件」含两个扩展按钮:本会话重复允许该类(会话限次 grant,approveRepeat)与
允许所有类型(把命令词持久写进 permissions.yaml 的 allow 白名单,approveAllowEverywhere)。
llmAssist 调用配置的真实 LLM(任意 OpenAI 兼容 API)自动裁决 ask,结果不确定/出错时回退人工
接缝——始终 fail-closed。trustEscalation(档位开启时默认开)答复门禁已放行的调用在其工具体内
提出的 sandbox_permissions 提权;见下文。permissive 关闭时,门禁行为与之前完全一致。
沙箱提权:为何 safe 裁决仍会弹窗
一个工具调用可能触发两个独立的审批。门禁负责第一个——它的 ask,在 tools/pre-execute
瀑布上。第二个来自工具体内部的 approveEscalation,在 tools/execute 时刻,只要模型传了
sandbox_permissions + justification;此时 tools/pre-execute 已结算,门禁的放行从未到达它。
LLM 评定为 safe 且门禁自动放行的调用因此仍会弹出确认。
trustEscalation 填补这个缺口。门禁记住每个它正面向上放行的调用(以宿主 callId 为键,提权
请求会重复该值),并在本处自行答复 allowed-once。它仅在全部满足时适用:
- 自动审查档位开启且
trustEscalation开启; - 请求携带门禁放行的
callId,且工具名匹配; - 原因为已知的提权,指明
workspace-write或danger-full-access。
其余所有情况——未知原因、不同的调用、门禁要求或拒绝的调用、approval: never 透传——都保持交给人
工,因此未来 DSH 措辞变更时 fail-closed。自动答复记录在事件流中
(verdict: "escalation-auto",mode: <目标模式>)。关闭开关可使沙箱放宽保持人工审批,其余
自动放行不变。
权限下拉里可选档位
cordis.patch.yml 在 DSH 的 permission.config.presets 里新增了 permissive preset
(sandbox: workspace-write、approval: ask、名称 自动审查),位于工作区内修改与
完全权限之间。DSH 的 bundle patch 对这个 map 是整表替换而非逐键合并,所以该文件还必须重述三个内置档
(read-only / workspace-write / danger-full-access,取自
@deepseek-ai/dsh-base/cordis.patch.yml);test/patch-presets.spec.ts 固定了这份键集合。因此会话权限
下拉里会出现「自动审查」这个独立可选审批档,而不是"auto-approval"档。
门禁只在 gatePresets 列出的档位里生效(默认 ['permissive', 'permissive-full'],即本插件新增的
两个档位)。在其余任何档位
(Read Only、Workspace Write、Full access、custom)里,门禁的判定流程完全不运行:不放行、不弹审批、
不拒绝、不执行 P0 硬拒绝、不做黑名单关键词拦截,也不写审计事件——该档位自己的策略说了算。这正是重点所在:
danger-full-access 的定义就是"全权限、不弹审批",用 ask 去覆盖它毫无意义(该档 approval: never 会让审批接缝
在任何 answerer 运行之前直接返回 rejected,被转发的 ask 只能得到 the user rejected tool "...",面板根本不会
弹出),用硬拒绝去覆盖它则等于悄悄推翻用户选定的档位。gatePresets: ['*'] 可让门禁重新全局生效(含硬拒绝层);
在生效档位内,若会话生效的审批策略为 never,ask 仍会降级为放行。
在 UI 里可配置
该档位也可在运行时从 设置 → 插件 → 自动审查 调整(插件浏览器端渲染的
settings.plugins.tab 页面):一个开关切换 permissive,四个开关编辑后台
permissiveStrategies。host 端 live 读取该命名空间,改动对下一条工具调用即时生效,无需重启。
这是一个独立审批类,不是 DSH 的"auto-approval"档。
风险分级 llmAssist、裁决学习与事件流
开启 llmAssist 后,接收 LLM(自定义 OpenAI 兼容端点,或 DSH 宿主模型组——见上文)按结构化协议逐条评估 ask。判定发生在门禁的 tools/pre-execute 瀑布内部、决策返回宿主之前:safe 直接放行,审批面板根本不会出现;只有真正无法确定的判定才会弹到你面前。
safe→ 自动放行(审计来源为classifier),不弹面板。risky+ 硬风险类别(deletion、credential、remote、system、bulk)→ 维持人工确认(ask)。分类器永不拒绝:拒绝只属于确定性层(P0 硬拒绝、黑名单关键词、显式deny:规则),所以被误判的类别永远可协商,而不会变成无法申诉的封禁。硬类别与neutral仅保留一条关键区别:永不进入学习,因此反复确认也不可能把它沉淀成自动放行。 (拿模型的判断当拒绝依据是实测出来的问题:一条无害的git commit -F …被判remote,直接自动拒绝——没有面板、也没有可重试的授权入口。)risky:neutral→ 若开启riskLearning(设置卡片内,默认关闭),人工批准且真实执行的 neutral 风险会按tool|类别计数;计数达到riskThreshold(默认 3)且新调用的操作指纹(命令词 + 目标基名)命中已确认样本时,同一操作自动放行。不同目标永不复用该放行。开启学习沉淀(riskSediment,默认开)后,满阈值 key 的确认样本会成为确定性放行规则:指纹精确命中即直接放行、无需再过 LLM——即使关闭 llmAssist 也继续生效;沉淀规则在设置卡片中可见、可管理(终止学习 / 删除样本)。- 超时(
riskTimeoutMs,默认 20s,重试 1 次)、传输失败与协议外输出均维持原ask——门禁绝不猜测。
学习状态持久化在插件自有 JSON($DSH_HOME/perm-gate/learning.json 或 learningFile),不写入你的 YAML 规则文件。每次决策都会追加到 $DSH_HOME/perm-gate/events.jsonl(或 eventsFile),并经 GET /api/dsh-perm-gate/events?sessionId=&since= 提供;浏览器端轮询该接口,在输入框上方以提示条展示最新决策(ask 常驻至下一条事件),并在对话视图的「审批记录」页签按时间倒序列出本会话的全部判定。
每次决策涉及的文件都会在改动落地前快照(每事件 ≤5 个文件、单文件 ≤256 KB)到 $DSH_HOME/perm-gate/snapshots/;「审批记录」页签中每个文件 chip 可点开行级改动对比(GET /api/dsh-perm-gate/diff),并可撤销该改动——向会话投递恢复指令(POST /api/dsh-perm-gate/revert)。快照管理条支持按会话或全量清理(GET /api/dsh-perm-gate/snapshots-stats / POST /api/dsh-perm-gate/snapshots-clear)。
转人工的 ask 会被跟踪到人工给出答复为止:一个被动 approval/request 观察者记录封闭结果(allowed-once → 人工通过、rejected → 人工拒绝、cancelled → 人工取消、unavailable → 拒绝,因为不存在审批通道);当观察者无法关联该 ask 时(缺 callId、无 approval 服务、上游监听者短路),由 tools/result 兜底结算同一个 ask。人工通过会显示通过后的学习进度(n/阈值),通知条也会为三种终态分别打标。
插件还内置一份预置黑名单关键词(继承自 dsh-approval-gate 的 DEFAULT_DENY_KEYWORDS:
rm -rf、push --force、drop table、mkfs、git reset --hard、docker system prune 等),
调用文本命中任一关键词(大小写不敏感子串)即直接拒绝,且先于白名单 / 授权 / LLM。黑名单在设置
卡片中按列表查看与增删(预置条目带标签,可一键恢复预置);未设置或为空时应用预置列表——黑名单
不会静默关闭。
「自动审查」与「自动审查(高权限)」在选择器里都画图标 —— 图标由安装指南里描述的那次宿主补丁
从各自共用的文件沙箱档位复制而来(workspace-write 与 danger-full-access)。没有该补丁时,
两个档位在所有界面上都是纯文字;它们的标签与门禁不受影响。
CLI(独立 dry-run)
dsh-perm-gate --rules permissions.yaml --tool bash --args '{"command":"pnpm install"}'
dsh-perm-gate --rules permissions.yaml --list开发
npm run typecheck
npm test
npm run build