dsh-plugin-guard
Stop malformed plugin mounts from failing every DeepSeek request with REQUEST_EXTENSION — a guard for DeepSeek Harness.
- Stars
- 1
- Language
- JavaScript
- Created
- Oct 6, 2026
- Updated
- Oct 6, 2026
Introduction
dsh-plugin-guard
Plugin-mount identity guard for DeepSeek Harness.
Stops a malformed plugin mount from failing every DeepSeek request with REQUEST_EXTENSION.
The problem
@deepseek-ai/dsh-plugin-package-inventory-deepseek resolves a package identity for every active
Loader row at request time. When a row's name is a path, it walks up to the nearest
package.json and demands that name and version are both non-empty — otherwise it throws:
function identityFromManifest(path, allowAnonymous) {
const manifest = JSON.parse(readFileSync(path, "utf8"));
if (allowAnonymous && manifest.name === void 0) return void 0; // ← only tolerates a missing name
if (typeof manifest.name !== "string" || manifest.name.length === 0
|| typeof manifest.version !== "string" || manifest.version.length === 0)
throw new Error(`... ${path} must declare non-empty name and version`);
return { name: manifest.name, version: manifest.version };
}
dsh-llm-deepseek then wraps that throw:
catch (error) {
throw new LlmError("DeepSeek request extension preparation failed", "REQUEST_EXTENSION", { cause: error });
}
Result: every official DeepSeek request fails. The UI shows only the generic wrapped message, so it looks like a model or account problem.
Why it is easy to hit
The failing manifest is usually DSH's own profile manifest, which ships with a name but no version:
// $DSH_HOME/profiles/<name>/package.json ← generated by DSH
{ "name": "dsh-profile-desktop" }
So any local plugin mounted without its own package.json triggers it:
# $DSH_HOME/profiles/<profile>/cordis.patch.yml
- insert:
- id: myplugin
name: "C:/Users/<user>/.dsh/profiles/<profile>/plugins/myplugin/index.ts"
nearestManifest() walks up from plugins/myplugin/ → plugins/ → profiles/<profile>/ and lands on
that version-less manifest.
It contradicts the caller's own contract
The caller already tolerates an unresolvable identity:
const identity = resolver.resolve(activeEntry);
if (identity === void 0) continue; // contract: undefined = skip this entry
resolve() is even typed as ... | undefined. Throwing here is error-severity misclassification:
best-effort diagnostic metadata should not hold a request-level veto.
What this plugin does
Three defense layers, all implemented by mirroring the upstream resolution rules exactly.
Ships as a DSH bundle (dsh.bundle.patch), so it can be installed rather than copied.
| # | Mechanism | When | Effect |
|---|---|---|---|
| ① | Startup self-heal | apply(), before any model request | Scans every profile's patch layers; writes a minimal compliant package.json next to any offending plugin module |
| ② | Check-on-write | after any profile .yml/.yaml is written | Re-audits and self-heals immediately, appending the result to the tool output |
| ③ | Three tools | on demand | dsh_guard_audit / dsh_guard_repair / dsh_guard_verify |
Startup self-heal is profile-wide: load the plugin once and every session in that profile is covered. Measured cost: ~1 ms.
Installation
This repository is a DSH bundle: an npm package that declares dsh.bundle.patch, so a profile can
list it in dsh.profile.bundles and get the plugin row inserted automatically.
As a bundle (recommended)
# from a clone
dsh plugin --profile <profile> add /path/to/dsh-plugin-guard
# or straight from git
dsh plugin --profile <profile> add github:Linicc/dsh-plugin-guard
dsh plugin forwards to pnpm inside the profile directory, then appends the package to
dsh.profile.bundles because it declares dsh.bundle. The bundle's cordis.patch.yml is what inserts
the plugin row:
- insert:
- id: plugin-guard
name: dsh-plugin-guard
Note that the row references the package by name, not by path — that is what lets Node resolve the installed code. It also means this row takes the bare package resolution branch, which is the one that does not depend on a nearest-manifest walk.
As a local module (no bundling)
If you would rather not install a package, clone anywhere and mount the file by absolute path:
# $DSH_HOME/profiles/<profile>/cordis.patch.yml
- insert:
- id: plugin-guard
name: "<abs path>/dsh-plugin-guard/src/index.ts"
⚠️ The plugin directory must contain a compliant
package.json. Mounting a directory that has none is exactly the condition this plugin exists to prevent — see the problem. This repository ships one, but if you copy the source elsewhere, copy the manifest too.
Configuration
- id: plugin-guard
name: dsh-plugin-guard
config:
autoRepair: true # write the missing package.json (default: true)
verbose: false # print audit details to the console
# profileRoot: "..." # default: $DSH_HOME/profiles
| Field | Type | Default | Meaning |
|---|---|---|---|
autoRepair | boolean | true | Self-heal at startup and on patch writes |
verbose | boolean | false | Print audit details |
profileRoot | string | $DSH_HOME/profiles | Root scanned for profiles |
Repository layout
dsh-plugin-guard/
├─ package.json declares dsh.bundle.patch; main -> lib/index.js
├─ cordis.patch.yml the layer a profile applies when it lists this bundle
├─ lib/index.js compiled entry (committed, so consumers need no build step)
├─ src/index.ts source
├─ scripts/build.mjs regenerates lib/ from src/ using Node's built-in type stripping
├─ README.md / README.zh.md
└─ LICENSE
Rebuild after editing the source:
npm run build # node scripts/build.mjs — no dependencies required
Tools
dsh_guard_audit
Audits every profile's plugin mount rows and lists those that would throw.
✅ plugin-guard audit passed
profiles 2 · rows 8 · all identities resolvable
· desktop/plugin-guard → dsh-plugin-guard@0.1.0
On failure it names the profile, the row id, the module directory, and the exact problem.
dsh_guard_repair
Writes a minimal package.json for non-compliant plugin directories. Idempotent, never overwrites.
Supports dryRun.
dsh_guard_verify
Pre-install check: given a plugin entry file or directory, tells you whether it will pass the official resolution before you mount it.
Implementation notes
The resolution rules are mirrored one-to-one so that the guard can never disagree with upstream:
| Upstream | Rule | Here |
|---|---|---|
barePackageName(specifier) | starts with . / contains : / isAbsolute() → treat as a path | isPathLike() |
nearestManifest(dir) | walk up from the module directory to the first package.json | nearestManifest() |
identityFromManifest(path, allowAnonymous) | name and version must both be non-empty | identityFromManifest() |
The guard is deliberately more conservative than upstream: upstream only inspects active rows, the guard inspects all rows. The cost is occasionally reporting a row that is disabled today but would fail if enabled — which is the desired behaviour.
Safety boundaries
- Only writes when the plugin's own directory has no
package.jsonat all; never modifies another package's manifest. - When the offending manifest belongs to an ancestor (e.g. the profile root), the finding is marked
repairable: falseand left for a human — the guard will not edit your profile manifest. - Every write and every audit is wrapped in
try/catch: the guard must never become the failure it guards against. - Writes are announced on the console; nothing happens silently.
Model Experience
Request context and condition
What the model sees
Three tool schemas are contributed to the system prompt: dsh_guard_audit, dsh_guard_repair, and
dsh_guard_verify, each with a Chinese description explaining when to call it. The schemas are static;
no conversation content is read.
Tool descriptions (verbatim, abbreviated)
dsh_guard_audit — 审计 DSH 所有 profile 的插件装载行,复刻官方「包身份解析」逻辑,找出会导致 REQUEST_EXTENSION (整轮 DeepSeek 请求失败)的行。任何新增/修改插件之后都应跑一次。
dsh_guard_repair — 为不合规的插件目录补一个最小 package.json(name + version),消除 REQUEST_EXTENSION 隐患。幂等、永不覆盖已有文件。补完后用 dsh_guard_audit 复验。
dsh_guard_verify — 安装一个本地插件**之前**的预检:给定 .ts/.js 文件或目录,判断它作为 patch insert 行时能否通过官方包身份解析。
Token effect
Fixed and small: three tool schemas in the tool catalogue. No result is injected unless a tool is called or a profile patch file is written.
KV Cache effect
Prefix-stable, with one exception. The tool schemas are static, so the prompt prefix is unchanged across
requests. When a .tex-unrelated profile patch file is written, tools/post-execute appends a short
audit note to that tool result — this changes only the tail of the conversation, not the cached prefix.
No other request-context entry is added.
Known Limitations and Deferred Work
- Upstream defect remains — this plugin is user-side prevention. It does not fix
identityFromManifest()in@deepseek-ai/dsh-plugin-package-inventory-deepseek, and any other trigger of the same failure path (for example a stale fallback symlink, see upstream discussion #5439) will still take requests down. Reported upstream: discussion #4950. - YAML parsing is a tolerant subset — only
- id:/name:pairs are extracted. Unusual patch shapes (anchors, merge keys, deeply nested inserts) may not be seen. The guard fails open (reports nothing) rather than failing the boot. - JSON-parsed manifest only — a syntactically broken
package.jsonis reported, not repaired. - No watcher — only
write/edittool activity and host startup trigger an audit. A patch file edited by an external editor is picked up at the next DSH start.
Verification
The exact failure condition can be reconstructed in a sandbox and all four paths exercised
(audit / verify / repair / startup self-heal):
Sandbox:
profiles/desktop/package.json → { "name": "dsh-profile-desktop" } ← name, no version
cordis.patch.yml → insert row pointing at a directory with no package.json
plugins/myplugin/index.ts → entry file only
autoRepair: false
dsh_guard_audit → ok=false, problems=1 ✅ detects
dsh_guard_verify → ok=false + remediation advice ✅ pre-checks
dsh_guard_repair → creates package.json ✅ repairs
audit again → ok=true, problems=0 ✅ re-verifies
autoRepair: true (default)
apply() creates the manifest in ~1 ms ✅ defuses before it fires
Real profile
audit → profiles 2 · rows 8 · problems 0
License
MIT — see LICENSE.