dsh-plugin-content-policy
Preventive content policy for DeepSeek Harness tool results: rewrite matched text with regex rules, or drop/truncate unpredictable payloads by field path before they are normalised, rendered, or persisted. The upstream counterpart to dsh-plugin-redact.
Who this is for
- developers using DSH for coding with Git workflows
Install
dsh plugin --profile web add dsh-plugin-content-policy
Sandbox test results
| Stage | Result |
|---|---|
| T0 static check | — |
| T1 install | ✓ |
| T2 list | ✓ |
| T3 mount | ✓ |
| T4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-plugin-content-policy
预防式内容策略:在 DSH 的工具结果被规范化、落盘、进入下一次 provider 请求之前改写它。
它是 dsh-plugin-redact(事后补救:原地脱敏已经写进 session.vN.jsonl.zstd 的日志)的事前对应物:
redact 擦掉已经留下的东西,本插件让不该留下的东西根本没机会留下。
两条互补的策略,挂在同一条接缝上(tools/execute),顺序固定为 先 strip、后正则:
策略 · 配置 · 认识内容吗? · 适用场景
内容改写 · rules[] · 认识:你给字面量/正则,命中即替换 · 你已经知道要挡什么(手机号、密钥、内部域名…)
结构最小化 · strip[](+ stripDefaults) · 不认识:只按字段路径整段丢弃/截断 · 事先无法枚举的载荷(公开检索回来的任意段落),对策是"让它根本不进来"
1. 为什么必须发生在 append 之前
一次 session.append('tool/result', …) 有两个后果,而且它们是同一份数据:
后果 · 证据
它成为持久日志里的一行 · dsh-agent-loop/lib/index.js:703-712(session.append("tool/result", { turn, step, message, …result.meta }))
它是将来每一次请求里那条工具结果消息 · dsh-session/lib/index.js:216:deriveEventMessage 对 tool/result 直接 return event.data.message;请求由 session.deriveMessages() 折叠同一批事件得到
所以"事后脱敏"永远慢一步:内容在写入的那一刻就已经同时进了日志和(下一次请求的)上下文。
本插件把自己插在 tools/execute 瀑布里 —— 那是结果被规范化之前唯一的改写点。
尤其要注意两条通道:web_search 把同一段 snippet 同时放进渲染文本(content)和
presentationMeta(持久化 meta)。只换 content 会漏掉 meta,见接缝表第 6、7 行。
2. 接缝表(每条都对着 shipped 源码核过)
# · 接缝 · 源码位置 · 本插件怎么用
1 · tools/execute 瀑布 · 声明:dsh-tools/lib/types/index.d.ts:49 · 主接缝:包装器返回值会被注册表重新规范化
2 · 返回值规范化 · dsh-tools/lib/index.js:3213-3214(waterfall('tools/execute', …) → normalizeDispatchResult) · 我们返回的对象就是下游 tools/post-execute 看到的 result
3 · 同一性快路径 · dsh-tools/lib/index.js:3448(canonicalResults.get(result) === exec.token ⇒ 原样返回) · 无改动时按引用返回原结果 ⇒ 零开销、不二次渲染
4 · 成功结果从 value 重新派生 content · dsh-tools/lib/index.js:3458 → :3422(createSuccessResult → tool.output.render(exec.arguments, value)) · 我们改写 value,content 由注册表自己重算
5 · 成功结果从 value 重新派生 meta · dsh-tools/lib/index.js:3428-3436(presentationMeta(exec.arguments, value)) · 同上:meta 也自动跟着 value 变
6 · 只换 content 会留下旧 meta · dsh-tools/lib/index.js:3401-3405({...result, content} 展开保留 meta) · 这是本插件改写 value 而不是 content 的原因
7 · meta 会被持久化 · dsh-agent-loop/lib/index.js:708(...result.meta !== void 0 ? { meta: result.meta } : {}) · 只换 content ⇒ 命中文本仍随 meta 落盘
8 · 同一段文本双通道的真实例子 · dsh-tool-web/lib/index.js:62-79(snippet 进 content)与 :103-124(同一 snippet 进 presentationMeta) · web_search 是最典型的"只擦 content 不够"案例
9 · 失败结果不能替换 value · dsh-tools/lib/index.js:3392(tools/post-execute cannot replace the value of a failed result) · 失败结果只能重建 content;strip 对它不生效
10 · 失败结果的 content/meta 逐字复制 · dsh-tools/lib/index.js:3449-3455 · 返回自己的 { isError: true, error, content } 并省略 meta ⇒ meta 被丢弃
11 · 失败结果持久化时的约束 · dsh-session/lib/index.js:241-248(error 存在时要求 message.content[0].isError === true) · 由 createToolResultMessage 依据 result.isError 生成,天然满足
12 · 参数在工具体运行前就已落盘 · dsh-agent-loop/lib/index.js:586(appendToolCall)早于 :588(prepare/dispatch) · 所以"阻断"不能阻止参数入库,只能阻止执行(见第 6 节)
13 · tools/pre-execute 的 {kind:'deny'} · dsh-tools/lib/index.js:3116-3139 · block 规则:命中参数即拒绝,工具体不运行
14 · ctx.tools.guard(fn) · dsh-tools/lib/types/index.d.ts:620(单调守卫) · 未使用:瀑布的 deny 已经终止派发;理由见第 7 节
15 · tools/ptc-dispatch-log 瀑布 · 声明:dsh-tools/lib/types/index.d.ts:75;消费:dsh-tools/lib/index.js:1235-1251 · 兜底改写 run_code 子调用的日志副本 content
16 · 输出 schema 的受支持子集 · dsh-tools/lib/index.js:33-47(type/oneOf/properties/required/additionalProperties/items/enum/const + 注解)、:195-206 · 只有 enum/const 能固定一个字符串;也没有 minLength/maxLength(这是 maxChars 截断安全的前提)
17 · Cordis 的 Config 契约 · cordis/lib/index.js:955-960(runtime.Config["~standard"].validate(config))、:1630(Config: plugin.Config) · 导出的 Config 必须实现 Standard Schema
18 · patch 覆盖是整体替换 · dsh-app-boot/lib/index.js:70-105(target[key] = value) · 覆盖 config 时要把字段写全(第 4 节)
19 · 真空操作默认的约定 · dsh-spill-policy/lib/index.js:104(if (maxInlineBytes === void 0) return) · 未配置 rules 且未配置 strip ⇒ 一个监听器都不注册
20 · 工具输出 schema 是编译后的 · dsh-tools/lib/index.js:846-847(defineTool 调 valueSchemaSpecToJsonSchema)→ :594-603(属性级 required: true 变成对象级 required: [...] 数组) · 所以"哪个键是必需的"可以从 ctx.tools.get(name).output.schema.required 直接判定
21 · 缺 required 字段 ⇒ 值非法 · dsh-tools/lib/index.js:454-455(missing required property "…")、:466-467(additionalProperties: false 的多余键) · strip 拒绝删除 required 键的依据
22 · 非法 value ⇒ 整条调用变成终态错误 · dsh-tools/lib/index.js:3417-3418(validateJsonSchemaValue → throw new ToolOutputError)、:3226-3231(catch → toolErrorResult)、:2454-2458(INVALID_TOOL_OUTPUT) · 这就是"宁可响亮失败、也不静默产出非法值"的代价说明
23 · web_search 的 required 只有 3 个 · dsh-tool-web/lib/index.js:270-298:required 在编译后是 ['sources','truncated'],sources[].url 必需;content/title/snippet/publishedAt 可选;additionalProperties: false · stripDefaults 的两条规则据此选定(丢 sources.*.snippet 与 content)
24 · web_search 的两条投影都是纯函数 · dsh-tool-web/lib/index.js:62-79(formatSearchOutput,逐处 !== void 0 判断)、:103-124(projectSource + searchMetaFromValue) · 删掉可选字段后它们自然"看不见"该字段 ⇒ content 与 meta 同时干净
25 · ptc 日志副本只有 content 块 · dsh-tools/lib/index.js:1235-1242(shapeDispatchLog({… content: result.content})) · strip 在这条接缝上无从下手(没有 value)
26 · spill 策略只管纯文本结果 · dsh-spill-policy/lib/index.js:75-83(flattenPlainText:任何非 text 块 ⇒ undefined)、:156-172(tools/post-execute,prepend: true) · 它完全不碰 web_search 这类结构化结果;这正是 strip 要补的缺口
27 · spill 策略的落盘预览 · dsh-spill-policy/lib/index.js:88-101(头尾预览)、:114-150(存 spill 文件) · 全文仍以文件形式留在本地(ctx.spillStore),只是不进上下文
28 · 配置校验失败 = 加载期错误,不是运行期 · cordis/lib/index.js:955-961(resolveConfig 调 Config["~standard"].validate 并 throw new ValidationError)、:939-944(文案 = invalid config: + 每条 message (at path)) · 数值约束(maxScannedBytes / maxChars)因此放在 Config 里,报错点紧贴配置本身、且带字段路径
2.1 Config 的两条轨道:谁在生效,以及它们真正的差异
先说结论:用户实际拿到的是"兜底轨道"那一套。 插
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Embed the badge
Two badges, each using its own site's identity: left is the **standard** (published by DSHIO), right is the **testing organization** (deepseekbase, which ran the inspection). Each links to the standard text and the test report.
Markdown(推荐,两个一起)
[](https://dshio.org/10/) [](https://deepseekbase.com/zh/plugins/dsh-plugin-content-policy/)
Note: this is not a certification. DSHIO/T 32 is not in effect.
Versions
Published on npm::2,最后更新 2026-09-12
Links
Frequently asked
dsh plugin --profile web add dsh-plugin-content-policy in DSH, or paste the package name into the DSH Desktop plugin page.dsh --version), then look at the first error in the test-results section. If it still fails, open an issue on the repository.dsh plugin --profile web remove dsh-plugin-content-policy.Same category
Compare with peers
| Plugin | Downloads | Verdict | Size |
|---|---|---|---|
| cc-safety-net | 111.9K | pass | 2.1 MB |
| dsh-trust-check | 18.7K | pass | 2.03 MB |
| dsh-perm-gate | 15.6K | install_failed | — |
| dsh-workspace-api-key | 9.1K | pass | 0.2 MB |
| dsh-isolation-gate | 8.3K | pass | 0.42 MB |
| dsh-auto-review | 6.4K | pass | 16.91 MB |
| dsh-weiwen-law | 6.0K | pass | 2.23 MB |
| @goodandready/dsh-time-machine | 5.5K | pass | 13.73 MB |