dsh-sensitive-guard:把密钥挡在模型上下文之外

前言

智能体开发里有一个反复出现的场景:把带密钥的配置文件、.env、连接串丢进对话,或者让工具执行后把输出送回上下文。凭证一旦进入上下文,模型可能把它原样写进日志、代码和回复里。靠提示词提醒「不要泄露密钥」并不可靠,因为模型拿到文本时已经看到了明文。

dsh-sensitive-guard 就是为这件事做的插件:不是提醒,而是在用户输入和工具结果进入上下文之前就把凭证替换成 <SG_xxxxxxxxxxxx> 占位符,明文加密留在本机保管库,需要用的时候再点开或导出。

下面介绍这个插件:它安装在哪、拦截哪些入口、认得出什么、配置怎么改,以及哪些边界要提前知道。

定位

插件名称 dsh-sensitive-guard,维护者 wangxy496,分类 admin-security,许可证 MIT,当前版本 0.5.0(以 npm 包为准)。目标只做一件事:密钥、令牌、密码在进入模型上下文前被换成 <SG_ 前缀的代号,明文以 AES-256-GCM 加密保存在本地保管库。模型拿到的是代号,用户点一下锁能看到真值。

代号里 12 位十六进制是 HMAC-SHA256(vault.key, 明文) 的前缀。密钥只在本机,所以拿到代号的人没法靠穷举短密码反推;同一个密钥每次得到同一个代号。0.3.4 及更早版本用的是 sha256(明文) 前缀,短密码可以离线猜出来,升级后旧代号仍保留可解锁,新捕获一律换成新算法。

核心功能

插件分 host 端和客户端两半:host 端抓数据、管保管库、开接口;客户端加设置页和对话里的锁形胶囊。

注册点 端 作用
agent/pre-step host 钩子 用户输入进上下文前脱敏
tools/post-execute host 钩子 工具结果进上下文前脱敏
llm/stream host 钩子 出网的流式文本兜底清洗
sensitive_guard_status、sensitive_guard_export host 工具 查状态;把明文导出成 0600 临时文件
/guard/* HTTP 路由 设置页取数、揭示明文、暂停/关闭保护、规则清单与开关、手动录入
settings.section 客户端 slot 设置页里的「敏感保护」
conversation.user.text.node 客户端 slot 对话里的锁形胶囊

前提只要 tools 一个。webServer 不存在时脱敏照常工作,只是设置页和揭示用不了。

拦截点有两层:用户输入走 agent/pre-step,工具结果走 tools/post-execute;llm/stream 再洗一遍出网的流式文本做兜底,只处理 text-delta 和 reasoning-delta,tool-call-delta 直接放过,否则工具参数会被替换成占位符。

安装

要求 dsh 0.2.0 以上、Node 18 以上。Windows 11 和 Ubuntu 22.04 均实测,macOS 未测。资料里给了三种装法,这里列前两种。

第一种直接编辑 ~/.dsh/profiles/<profile>/package.json:

{
  "dependencies": {
    "dsh-sensitive-guard": "0.5.0"
  },
  "dsh": {
    "profile": {
      "bundles": ["dsh-sensitive-guard"]
    }
  }
}

bundles 里保留原有条目,只追加这一行。然后在同目录跑 pnpm install,重启 dsh。

第二种让 dsh 自己装,在会话里说「安装这个目录下的插件:/绝对/路径」,它会走 install_bundle。

装完后可以验证。发一句:

我的 api key 是 sk-proj-abcdefghijklmnopqrstuvwx

应该显示成 我的 api key 是 sk- 加一个锁形胶囊。点开能看到明文,20 秒后自动遮回。再打开「设置 → 敏感保护」,最下面一行 inline reveal: 显示 ok 即正常。

日常使用

用户消息里的占位符会渲染成可点的胶囊,颜色按类型分:密码红、token 紫、key 绿、其它蓝,点开后统一变橙。点击走本机接口,明文不经过模型。

注意:只有用户消息的胶囊能点。AI 回复和工具结果走另一套节点渲染,宿主没有留「包一层」的口子,强行做就得整体替换助手消息渲染,宿主升级会白屏,所以作者没做。这两类位置的明文要去设置页看。

设置页分两个标签。「敏感数据管理」是保管库,列表保留名称/值、分类、规则三列,顶部「添加敏感数据」是弹窗,填名称、明文、类型(可以自定义分类);明文直接加密进保管库,不写 rules.json,之后这段值出现就自动遮。小锁临时查看明文,旁边的「复制明文」按钮走本机接口取完整明文写到剪贴板,明文不进模型上下文。

「自动检测规则」列出 242 条规则(221 条上游 + 21 条内置),可按名称/关键词搜索、按全部/已开/已关筛选、每页 20 条翻页,逐条开关。默认「已开启优先」排序,14 条默认关闭的规则集中在最后两页。

模型拿不到明文。要连库或调接口时让它先导出:

sensitive_guard_export(<SG_xxx>)   明文写到 0600 文件,返回路径
命令里用 $(cat /那个/路径)         明文不进上下文

工具只有两个:sensitive_guard_status 看状态和条目元数据,不含明文,连前 8 位预览都不返回;sensitive_guard_export 负责导出,文件 15 分钟后过期。

一个常被当成 bug 的点:模型读 application.yml 时看到的是占位符,拿它去连库必然失败。要么先导出,要么临时点一次暂停。

认得出什么

系统规则库内置 221 条来自 gitleaks 的厂商规则(1Password、Adobe、Adafruit、Age、AWS、GCP、Azure、Stripe、Slack 等,MIT 许可,见 NOTICE)。每条带关键词预筛、正则和可选熵值阈值,命中后才跑正则,不会 221 条全量扫一遍。默认开 207 条高置信规则;10 条「近似转换」和 4 条宽松规则默认关,在规则标签里自己开。上游还有一条 pkcs12-file 按文件名 .p12/.pfx 判定,文本脱敏用不上,规则页标为「不适用」。规则数据由 scripts/build-gitleaks.mjs 从上游 TOML 转换生成,升级时重跑即可。

覆盖的厂商前缀:sk-、sk_、ghp_、glpat-、npm_、hf_、AKIA、ASIA、xox*-、AIza、whsec_、SG.,以及 PEM 私钥块、JWT、Bearer 令牌。

赋值和环境变量也认:password=、secret:、api_key:、SECRET_KEY=、DB_PASSWORD=。

JSON 和结构化数据一块处理。工具读 .env、cat config.json 返回的常是一整段 JSON,{"password": "Abc!23456"} 这种值没有厂商前缀,只看值形状认不出来。现在只要字段名是 password、passwd、pwd、token、credential、api_key、db_pass、dsn、connection_string 这类(含下划线、连字符、驼峰变体),值一律换代号,键名、层级和别的值都不动;已经解析成对象的工具结果同样处理。

示例和占位不遮:your_password_here、${DB_PASS}、<your-token>、changeme,以及 password_policy、token_count 这种只是描述字段的名字。

中文说法也能识别:数据库密码是…、我的 token 是…。中文语境的密码从 6 位起就拦(我的密码是 123456);英文和 key=value 形式从 8 位起,否则 the secret is hidden 里的 hidden 会被当成密码。句尾语气词和标点留在占位符外面。

连接串和命令行也覆盖:mysql://user:pass@host、postgresql://…、JDBC 参数里的 password=、X-Api-Key:,以及 mysql -uroot -pabc12345、redis-cli -a xxx;只认这几个客户端,docker run -p 8080:80 不会被误伤。

被换行打断的 key 也认:sk-proj-… 粘贴时常被插入 \r、\n 或 \r\n,这类按跨行匹配把两段拼回完整值再入库,占位符与不换行时完全相同。

熵值检测默认关闭,rules.json 里加 "entropy": true 才开。它抓没有前缀的随机串,但只对 base64 或混合字符集有用;纯 hex 串上限是 4.0 bits/char,永远够不到 4.5 的默认阈值,8f3a9b2c… 这种开了也抓不到;阈值降到 3.9 又会误伤英文长句。

配置

配置文件是 <数据目录>/rules.json,存盘后两秒内生效,不用重启。Windows 下是 %USERPROFILE%\.dsh\sensitive-guard\,Linux 下是 ~/.dsh/sensitive-guard/。

{
  "extraRules": [
    { "id": "my-internal", "pattern": "X-WX-Token:\\s*([A-Za-z0-9]{20,})", "flags": "i", "group": 1 }
  ],
  "disabled": ["jwt"],
  "enabledRules": [],
  "outboundScrub": true,
  "entropy": false,
  "entropyThreshold": 4.5,
  "allowCidrs": ["192.168.0.0/16"]
}

group 要遮挡的捕获组序号,0 表示整个匹配。自定义正则的性能需要自己负责,别写 (a+)+、(.*)* 这类结构——它跑在同步钩子里,回溯爆炸会卡住宿主。

disabled 写规则 id 关掉单条默认开的规则,enabledRules 写 id 打开一条默认关的规则;设置页开关就是维护这两个列表。outboundScrub: false 会关掉出网补洗。allowCidrs 默认空数组,意思是只认本机。

网络访问

五个 HTTP 接口默认只接受本机回环:桌面版的浏览器和 dsh 在同一台机器,天然满足。只有 dsh 以 --host 0.0.0.0 启动、又从另一台机器的浏览器打开时会拒。两个办法:一是在服务所在机器的浏览器里打开 127.0.0.1:3080,不用改配置;二是在 rules.json 里加 "allowCidrs": ["192.168.0.0/16"],存盘两秒生效,设置页顶栏会出橙色提示。

没配的时候页面不会白屏,会降级成只读:能看到有哪些条目、什么类型、什么时候入库、命中几次,看不到值和 8 位预览。放行之后除了网段匹配还有三道检查,都不用配:Host 头必须是 IP 字面量(防 DNS rebinding);POST 必须是 application/json(跨站 text/plain 表单发不进来);Origin 必须和 Host 同源。

要清楚代价:开放网段意味着该网段里任何设备都能读到完整明文,不只是预览。只在可信内网开,用完把这一行删掉。没做登录是因为加一套认证会让点小锁多两步,不如把网段收窄。

已知限制

  1. 对话里的胶囊只覆盖用户消息,原因见「日常使用」。
  2. 不做出口改写。平台的 tools/pre-execute 只有 allow、deny、cancel、ask 四个动作,没有改参数的挂点,所以用凭据只能走导出文件。
  3. 保管库防的是误读和落盘明文,不防已经拿到你账户权限的人。加密是 AES-256-GCM,Windows 上密钥文件用 icacls 收紧到当前账户。
  4. 历史会话不受保护。pre-step 在宿主把用户原文写进日志之后才执行,session/event 又只是观察者,插件没有机会在落盘前改。旧会话里的明文还在磁盘上,那批密钥该轮换。
  5. exports/ 下是唯一落盘的明文,15 分钟后在下次导出时清理。
  6. 保管库最多留 500 条,超出按最后命中时间淘汰。
  7. 保管库坏掉时会放行,保证对话不中断,但会计数,可在 sensitive_guard_status 里看到。

小结

dsh-sensitive-guard 适合需要在对话里贴凭证、又不想让明文进模型上下文或日志的场景:它把拦截点放在进上下文之前,用本机加密保管库存真值,并提供点开、复制和导出三种取值方式。安装前建议通读源码和 LICENSE/NOTICE,确认它注册的钩子和 HTTP 路由符合你的环境。插件以当前 dsh 进程权限运行,能读会话内文本,也能在放行网段下把明文暴露给该网段。

目录页:https://www.skillhub.cn/plugins/wangxy496/dsh-sensitive-guard

GitHub:https://github.com/wangxy496/dsh-sensitive-guard

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye