DeepSeek Harness Hub
← 返回列表

金丝雀防泄漏插件jwilson411/dsh-canary

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

植入金丝雀串,拦截回显它的工具参数与出站 URL

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/8/31 · 已提供中文文档

DeepSeek Harness 插件:植入一个金丝雀,并拒绝回显该金丝雀的工具参数/URL(CANARY_TRIP)。

综合分
28.4
GitHub 分
28.4
用户评分
★ Stars
0
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add jwilson411/dsh-canary
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-tools@deepseek-ai/cordis
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-canary

一个 DeepSeek Harness 函数插件,它在模型可见的上下文中植入一个金丝雀字符串,并拒绝任何参数或出站 URL 中回显该字符串的工具调用。拒绝发生在工具运行之前,携带 code: 'CANARY_TRIP',并追加一行不包含金丝雀的 JSONL 记录。

植入内容看起来是这样的——标签,然后是 32 个十六进制字符:

DSHCANARY_…

会话中任何合法内容都不会携带该值。它不是任何东西的参数,不是标识符,也不是数据。因此,持有它的工具调用不存在歧义:有东西读取了上下文并试图把它发送到某处。

它是什么,不是什么

这检测的是所植入金丝雀的回显外泄。它不检测一般的提示注入,也不是红队产品。

- 不是提示注入检测器。 它只捕获一种行为:所植入的金丝雀被重复进工具参数或 URL。一个外泄你实际数据、编辑文件或说服模型跳过某一步的注入在这里是不可见的——金丝雀从未参与其中。一份干净的事件日志意味着该金丝雀未被回显,仅此而已。
- 不是红队产品。 本仓库中没有攻击载荷、没有越狱语料库、没有注入生成器,也没有任何针对任何 harness 的东西。它提供的是一根绊线和一道防护;制造能触发它们的东西不是它的职责,也不在代码树中。
- 不是分类器。 该检查是针对一个精确值的子串匹配。没有模型、没有评分、没有熵启发式、没有需要调优的阈值。一次近似命中——被截断的金丝雀、不同的大小写、该值被拆分到两个字段中——不会触发它,这是有意为之:一次误触发会拒绝一次合法调用,而这正是会让此类插件被关掉的失败模式。
- 不是隐蔽控制。 金丝雀被植入在模型可读取的工具描述中,同时附有不要回显它的指令,以及回显它将被拒绝的声明。触发它的模型已在同一上下文中被告知会发生什么。
- 不是数据丢失防护。 它只监视一个接缝——交给被包装工具的参数,以及你检查的 URL——除此之外别无其他。它不监视文件、出口流量或邮件流。

一次触发是上下文到达了出站参数的强证据。没有触发并不是没有任何东西到达的证据。

安装

dsh plugin --profile default add github:jwilson411/dsh-canary

dsh plugin 会转发到 $DSH_HOME/profiles/default 内的 pnpm,然后协调该 profile:由于此包的清单声明了 dsh.bundle.patch,它会被追加到 profile 清单有序的 dsh.profile.bundles 列表中,其 cordis.patch.yml 成为一个层。用同样的方式移除它,把 add 换成 remove 即可。

固定的 DSH 候选发布版本

本包是针对固定的候选发布版本编写和测试的
0.1.1-rc.2 — @deepseek-ai/dsh-tools@0.1.1-rc.2 被精确锁定在
devDependencies 中,以便测试针对一个已知的 API 运行,而 peer 范围是
^0.1.1-rc.2,与 harness 自身工具包声明它的方式一致。

这个 RC 没有的两个接缝,以及取而代之的做法

这个被锁定的候选版本没有暴露系统提示注入接缝,也没有暴露工具执行中间件接缝。本包既不发明其中任何一个,也不触及任何私有 API。

| 需要什么 | RC 提供了什么 | 本包怎么做 |
|---|---|---|
| 把金丝雀放到模型能读到的地方 | 没有提示注入接缝 | 注册 canary_context,其描述携带金丝雀。工具描述在构造上就是模型可见的。 |
| 在调用执行之前拒绝它 | 没有执行中间件接缝 | 导出 wrapExecute,由宿主在自己的调用点应用。 |

因此,植入物的持久性完全等同于工具注册表:注册发生在 apply 内部,所以 Cordis fiber 拥有它,而停止、更新或重新加载插件都会让植入物退役,无需任何簿记。

它注册了什么

| | |
|---|---|
| Cordis 插件 id | canary(cordis.patch.yml 中的行 id) |
| 注入 | tools — 一个硬依赖;插件会等待而不是降级 |
| 工具 | canary_context(植入物)、canary_status(计数) |
| 参数 | 两个工具都不接受任何参数 |

canary_context 返回 { planted, prefix, reminder, plugin }。canary_status
返回 { planted, prefix, incidents, lastTool, lastWhere, lastAt, log, plugin }。

两个工具都从不返回金丝雀。 prefix 是前 12 个字符——十个字符的标签加上两个十六进制数字,这足以确认哪一个植入物是活跃的,但距离足以重建它还差 120 位。模型可以调用的状态工具,就是注入指令可以调用的状态工具,所以那里没有任何值得为它调用它的东西。

库 API

工具是可见的那一半。守卫才是产品:

import { plantCanary, wrapExecute, CanaryTripError } from 'dsh-canary'

const state = plantCanary()                       // 或 plantCanary({ canary })

const guarded = wrapExecute(tool.execute, state, { tool: tool.name })

try {
await guarded(argsFromTheModel, exec)
} catch (error) {
if (error instanceof CanaryTripError) {
log.warn({ code: error.code, where: error.where, tool: error.tool })
return
}
throw error
}

命中时,内部的 execute 永远不会被调用,会追加一行事件记录,并且错误会向外传播。未命中时,参数原样传递——这个插件不编辑任何内容,也不重写任何内容。

两个更窄的入口点,适用于拥有不同调用点的宿主:

import { assertNoCanary, assertNoCanaryInUrl } from 'dsh-canary'

assertNoCanary(requestBody, state, { tool: 'http' })   // 命中时抛出
assertNoCanaryInUrl(candidate, state, { tool: 'fetch' })
throw 和 write 都不做任何事;wrapExecute 拥有该事件,因此无论宿主采用哪种方式,一次拒绝就是一行。scan(value, canary) 是同一遍历的独立暴露形式,返回 { where, path } 或 null。

什么算作回显

该遍历覆盖字符串、数组和普通对象,并且既扫描对象键也扫描值——以 canary 为键的载荷泄露它的可能性,与包含它的载荷一样确定。循环结构只遍历一次,不会无限循环。

当命中满足以下条件时,where 报告为 url:

- 位于名为 url、uri 或 href 的键下(不区分大小写),任意深度均可;
- 位于一个本身就是绝对 URL 的字符串内——即以 scheme 开头的字符串——无论该字符串位于参数中的何处;
- 位于此类 URL 的路径、查询、片段、用户信息或主机中;
- 位于上述任一位置经过百分号解码之后,最多两轮——粘贴到查询字符串中的 canary 到达时是编码的,而这正是外泄在起作用。

否则报告为 args——嵌入在较长文本中的 URL 会像其他内容一样被拒绝,并记录为 args,因为 where 命名的是回显所在的字段,而不是从文本中抓取 URL。遍历中任何位置的 url 命中都优先于 args 命中,因为出站表面才是值得命名的那个。

这里的绝对 URL 检测刻意基于形状,而不是可抓取性检查:就此目的而言,data: 和 javascript: 都是 URL。某个 URL 是否可以被打开是另一个问题,属于另一个插件。

错误

error instanceof CanaryTripError
error.code    // 'CANARY_TRIP' — 始终如此
error.where   // 'args' | 'url'
error.tool    // 其调用被拒绝的工具
error.prefix  // canary 的前 12 个字符,绝不超过
error.path    // 指向违规字段的点分路径,或 null
error.message // 人类可读,且不含 canary

消息携带前缀,绝不携带值。错误消息是最有可能被记录、渲染,或直接交还给刚刚试图外泄 canary 的模型的东西;把 canary 放进去就等于交出答案。

事件日志

仅追加的 JSONL,以 0600 创建,每次被拒绝的调用一行:

{"ts":"2026-08-31T12:00:00.000Z","tool":"http_get","where":"url"}

三个字段,并且该记录是由三个标量构造的,而不是从调用方的对象展开而来,因此不存在 canary 到达该文件的路径。where 在写入时和读回时都会针对 args / url 进行校验,因此由其他东西追加的日志无法将某个值偷运进 canary_status。

整个 canary 只在一个地方、一次性地发出:在植入时,即 canary_context 描述中。不在日志中,不在状态工具中,不在错误中。

写入失败被刻意不吞掉——一个审计追踪悄然停止的绊线,比一个大声失败的绊线更糟。

配置

| 键 | 类型 | 默认值 | |
|---|---|---|---|
| canary | string | 生成 | 一个固定的植入值,DSHCANARY_ + 32 个小写十六进制字符 |
| incidentLog | string | .dsh-canary.jsonl | 仅追加的 JSONL 日志路径 |

从配置文件自身的 cordis.patch.yml 中设置它们——注意,以 id 为目标的补丁会替换该行的整个 config,因此要重新声明你希望保留的每个字段:

- id: canary
config:
incidentLog: /var/log/dsh/canary.jsonl

解析顺序是补丁配置,然后是环境变量,最后是生成值:补丁行是部署的明确意图,因此不会被环境变量静默覆盖。环境变量回退项是 DSH_CANARY 和 DSH_CANARY_LOG,仅当补丁行完全省略该键时才使用。

在生产环境中不要设置 canary。 每次 apply 都会生成一个新的 128 位 canary,因此植入值只在一个插件生命周期内有效,没有人能获得第二个会话来处理同一个值。只有在植入值必须可预测时才固定它:

- 一个必须对该值进行断言的测试套件——本仓库自己的套件就是该键的第一个使用者,下面每个测试都植入一个固定的 canary,以便断言是确定性的;
- 一个必须共享同一个植入值的 worker 集群。

配置的值如果不符合 canary 格式,会在 apply 时被拒绝并抛出 InvalidCanaryError,而不是悄悄替换为随机值:静默替换会使所有下游预期都出错,而过短的值会匹配会话中一半的参数。

布局

package.json          manifest + dsh.bundle.patch — what makes this a bundle
cordis.patch.yml      the bundle's patch layer: one insert, one plugin row
src/canary.js         the pure half: mint, scan, deny, append
src/index.js          the plugin: name, inject, apply(ctx, config), the tools
test/                 offline tests: the walk, a stub context, a hygiene scan
package-lock.json     the pinned dependency tree npm ci installs in CI

测试

npm install
npm test

从构造上就是离线的,而不仅仅是意图上:每个用例都是一次字符串遍历,apply 被传入一个记录注册的桩上下文,工具结果会针对固定为 0.1.1-rc.2 的真实 @deepseek-ai/dsh-tools 进行验证,并且每个事件日志都写在操作系统临时目录下并在之后删除。test/hygiene.test.js 断言发布的源代码不打开任何套接字、不进行任何 fetch、不启动任何子进程,代码树不包含机器名、挂载路径或凭据变量名,并且 CI 不需要任何凭据。

CI 在 Node 22.x 和 24.x 上运行相同的两条命令,使用 contents: read 且不涉及任何机密。

版本

初始版本,2026-08-31。MIT。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群