@jieai/dsh-plugin-vet
Plugin trust pipeline for deepseek-harness (DSH): deterministic static scan, runtime guard (T1 sentinel + T2 hooks), audit protocol skill, GUI shield. Alarm-only monitor, never an enforcer.
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:wulun811/dsh-plugin-vet说明文档
阅读完整 README ↗@jieai/dsh-plugin-vet — Trust pipeline for DSH plugins
English | 中文
Before installing any plugin, run it through dsh-plugin-vet: static rules produce a verdict (deterministic, unforgeable), the agent investigates sensitive points and quality issues following the
vet-audit-protocolskill (no one can substitute for that), and a final scorecard is handed to a human/model to decide.Positioning: a monitoring alarm, not an enforcer. vet only does "check → alarm → advise": checks at write time (static scan), watches at run time (runtime guard), and surfaces alarms (scorecard + GUI shield status light). vet never acts on your behalf — it never auto-uninstalls, never kills processes, never rewrites configs; deny mode is an explicit opt-in by the deployer and is not part of the product identity. The final disposition is always decided by the user on their own DSH.
@jieai/dsh-plugin-vet is the trust-layer plugin in the deepseek-harness ecosystem: it occupies the whole
download → scan → audit → score → decide → runtime watch trust pipeline. The runtime watch ships built-in
honeypot lures: anyone quietly rifling through key files gets caught red-handed (opt-in, honeypot.enabled).
It does not provide a plugin marketplace itself (catalog/distribution).
- 📚 Architecture: docs/ARCHITECTURE.md
- 🧾 Audit protocol: AUDIT_PROTOCOL.md
- 🛡️ Security policy: SECURITY.md
- 🤝 Contributing: CONTRIBUTING.md
Installation
dsh plugin --profile
add @jieai/dsh-plugin-vet
Install-and-activate chain: pnpm install → reconcilePlugins reads dsh.bundle → on next start loadProfile
resolves the bundle and mounts the plugin. Default configuration is in the Config section below
(fail-open: reports only, never blocks).
Local tarball install (offline or verify-before-release scenario):
dsh plugin --profile
add ./jieai-dsh-plugin-vet-0.1.4.tgz
# or unpack directly into the profile's node_modules:
# tar -xzf jieai-dsh-plugin-vet-0.1.4.tgz -C ~/.dsh/profiles/
/node_modules/@jieai/
// and add an insert mount entry in the profile's cordis.patch.yml:
// - insert:
// - id: plugin-vet
// name: '@jieai/dsh-plugin-vet'
// config:
// mode: report
// autoScan: true
Paths / relative paths / URLs all work (
dsh plugin addfalls back to pnpm'sfile:protocol; a local tgz is resolved directly).First-install time note: the first
dsh plugin addinto a large profile can take several minutes — during that time pnpm does a full dependency resolution, updates the lockfile for 500+ packages and runs supply-chain policy validation over the whole dependency tree (vet itself carries only 2 runtime dependencies; the bulk of the time is parsing/validating the profile's existing tree, not vet). Subsequent installs/updates take seconds (validation results are reused).
Compatibility: vet targets DSH 0.1.0-rc.6+ (peer range
^0.1.0-rc.6). pnpm may warn about unmet peer dependencies — this is expected: profile templates setautoInstallPeers: false, and at runtime the packages resolve from the DSH install closure ($DSH_HOME/profiles/node_modulesfallback layer); you neither need nor should install another copy of the cordis family in the profile.
Watch scope = the profile vet is installed into. vet's guards are in-process events (
internal/plugin) — whichever profile vet is installed into is the one whose loaded plugins it guards. For multi-profile deployments, install vet into every profile you want guarded (dsh plugin --profile add @jieai/dsh-plugin-vet) and pointrequireAuditat the matching profile's cordis.patch.yml.
Config (cordis.yml)
| Key | Default | Description |
|---|---|---|
mode | report | report reports only, never blocks; deny explicitly enables blocking |
autoScan | true | Automatically static-scan new plugins (internal/plugin) |
scannerTimeoutMs | 15000 | Static-scan subprocess timeout |
requireAudit | false | Audit gate (opt-in): when enabled, loading a new plugin checks ~/.dsh/vet/audits/ for a health record — without one, report mode logs a yellow audit-required alarm, deny mode blocks. Records are written to disk by hand by the agent following the vet-audit-protocol skill |
rules | {} (all on) | Per-rule switches (R1-R12) |
denyOn | critical | Blocking threshold in mode: deny |
allowlist | [] | Package/plugin-id allowlist (skip scanning) |
runtimeGuard | off | Runtime guard (performance/stability cost, opt-in): off = disabled; watch enables the T1 sentinel + T2 hooks, alarm-only |
runtimeIntervalMs | 2000 | T1 sentinel /proc sampling interval |
runtimeMemLimitMb | 2048 | T1 memory alarm threshold (host VmRSS, over limit → red) |
runtimeForkBurstN | 5 | T1 child-process burst alarm threshold (single-round delta → red) |
runtimeFdLimit | 512 | T1 file-descriptor alarm threshold (→ yellow) |
runtimeGrowthMb | 256 | T1 sustained memory-growth alarm threshold (net RSS growth over the full window → yellow, suspected leak; an early-window spike does not count as window-level sustained growth, so no false positive) |
runtimeGrowthWindowMs | 600000 | Growth-detection window (default 10 minutes) |
honeypot.enabled | false | Honeypot lures (needs runtimeGuard: watch): plants fake key lures in honeypot.dir; T2 reports touches (read/write/delete) of lure paths as a separate honeypot alarm class. Directory/file names and contents carry no honeypot keywords (anti-honeypot), default location ~/.dsh/.local, lure values are well-formed but invalid fake credentials |
honeypot.dir | '' | Lure directory; empty = $HOME/.dsh/.local |
osvCheck | true | Query Google OSV for known vulnerabilities when scanning package.json (exact-version queries only: ranges (*/>=/^/~) and version-less main packages are skipped, P3-1/P3-3 — avoids stale full-history false positives; since round-7 ranges are no longer stripped to query as exact lower bounds). Verified targets = the plugin itself + direct dependencies (cap 8, official @deepseek-ai/* packages skipped, P3-10); transitive trees exceed the OSV v1 scope and the scan budget. Default on sends package names to api.osv.dev; network failure degrades silently. Set false if privacy-sensitive |
Official @deepseek-ai/* packages are exempt by default (built-in trust).
Tools
scan_plugin— deterministic static scan:target=dynamic-code(source string) /package(package directory) /file(single file). Returns a scorecard (verdict + staticScore + findings). The verdict is produced only by static rules.vet-audit-protocol(skill) — audit-process protocol (AUDIT_PROTOCOL.md): the agent audits a new plugin in preset steps — scan_plugin static criteria (incl. R12 Cordis/DSH contract) → read manifest/source → verify each finding → proactively dig deeper (network/files/processes/credentials/library semantics) → contract & code-quality audit (step 4.5: entry/Config-schema consistency, error handling/synchronous blocking/resource leaks/async correctness and other "badly written" issues — statically clean ≠ worth installing) → hand-write a health record to~/.dsh/vet/audits/ --.mdusing the system write capability. vet ships no audit tooling and does not investigate for the agent — it only provides the criteria and the on-disk convention.
Automatic behavior
internal/pluginauto-scan (autoScan: true): newly installed third-party npm packages are static-scanned on load;denymode + verdict ≥denyOn→ load rolled back.- Audit gate (
requireAudit: true): loading a third-party plugin without a health record —reportmode logs a yellowaudit-requiredalarm (enters the /vet/status.json alarm list, plugin loads normally);denymode rolls back the load (referencesvet-audit-protocolas a prompt to audit first). Records match by exact version (P-1): after a plugin upgrade the old version's record no longer authorizes the new version — re-audit is required to clear the alarm/block. tools/executeinterception:cordis_define/run_code/workfloware scanned before execution (cordis_run's real schema carries no code payload, so the guard slot stays dormant — P3-11 synced);reportmode prefixes non-clean results withVET:(clean executions don't pollute machine-readable output),denymode blocks outright (isError).- Runtime guard (
runtimeGuard: watch) — alarm-only:- T1 sentinel: a sidecar subprocess reads the host /proc every
runtimeIntervalMs(VmRSS / child-process count / fd count) and streams alarm JSON lines back to the host → shield turns yellow/red. - T2 hooks: in-process wrappers around fs / child_process (incl. fs.promises); dangerous operations
(sensitive-path writes/deletes, key-file reads, subprocesses with shell/download/exfiltration keywords,
honeypot-lure touches,
~/.dshconfig-root reconnaissance) are attributed via the stack to the plugin package name before alarming; official packages get full-class noise reduction via attribution (capability grant — official packages are the platform itself; their high-frequency~/.dshsession/config/storage reads don't spam; third parties can't forge attribution). Never blocks a call. Self-harm exemptions (fixed after real-world false positives):- node_modules package-directory exemption: package names/inner files are public artifacts — package
names containing credential/secret words are normal ecosystem (
@aws-sdk/credential-provider-*,@deepseek-ai/dsh-credentials-local, etc.), and both host module resolution (require.resolve's internal realpathSync/stat of inner package.json) and vet's own scan reads touch them at high frequency, so they no longer false-positive as fs-probe; path segments before node_modules still judged normally (~/.ssh/node_modules/xstill hits .ssh), and write/delete of system roots (/usr etc.) still alarms. - Attribution excludes vet itself: the wrapper frame is always the top of the alarm stack, and the vet root never participates in attribution mapping — host/unowned alarms are no longer pinned on vet (the alarm still fires, attributed to the real caller).
- Toolchain temp artifacts (tsc
. ..tmpdir,*.tmp,*.temp,*.swp, etc.) are auto-exempt — the secrets/credentials in their names are just source filenames being compiled; deleting them is cleanup, not destruction; parent segments still judged normally (~/.ssh/config.bakstill alarms).
- node_modules package-directory exemption: package names/inner files are public artifacts — package
names containing credential/secret words are normal ecosystem (
- T1 sentinel: a sidecar subprocess reads the host /proc every
- GUI shield: a browser half registers into
conversation.session.header.actionsand polls /vet/status.json to show a green/yellow/red light + alarm count. Activation requires adsh webrestart (client-modules only scans thedsh.clientdeclaration at startup).- Interaction: clickable — clicking expands the alarm panel (live metrics: memory/CPU/I-O/
child-process/fd; guard status: when off, one click writes a
runtimeGuard: watchconfig (takes effect on restart); alarm list with severity/attribution/per-item advice; recent-scan echo, refresh, updated time), outside clicks close it; when alarms exist a count badge appears next to the shield (green/yellow/red theme color, light/dark adaptive). - Per-item dismiss: each alarm can be "dismissed" — display-only (no longer counts toward shield level or count), the record is kept and can be "restored"; a dismissed alarm auto-expires once the alarm stops, so a recurrence is visible again (and can be dismissed again). Dismiss state shares the alarm store's lifecycle (resets on restart). Auth boundary (P3-12 recorded): dismiss/restore only do same-origin validation (alarm-only display-layer risk — a same-origin page script could hide alarms, but records aren't deleted and nothing else is affected; acceptable within the system).
- Display caps: the panel shows the latest 8 alarms; the store is a ring buffer capped at 20, deduped per id within 60s, 24h TTL (sustained triggers naturally renew) — 100 alarms are not displayed in full, and needn't be (new alarms push out the oldest). Recent-scan echo (suspicious → yellow) also expires on the 24h TTL (P3-2: one suspicious scan no longer turns the shield permanently yellow; sustained scanning renews naturally).
- Interaction: clickable — clicking expands the alarm panel (live metrics: memory/CPU/I-O/
child-process/fd; guard status: when off, one click writes a
Static rule table (R1-R12)
| ID | Name | Default level | Scope | Determinism |
|---|---|---|---|---|
| R1 | constructor-chain escape | critical | code + files | certain/likely |
| R2 | Dynamic execution (eval/Function/import/require) | high (files) / medium (code; bin entries drop to medium) | both | certain/likely |
| R3 | Direct process access (runtime-graded; read-only members/generic/bin entries/app-type packages → info) | critical (host) / high (sandbox) | both | certain |
| R4 | Host closure capture (agent/TextEncoder…) + host-global prototype pollution | critical (code) / high (files, independent of targetKind) | both | certain/likely |
| R5 | ctx-escape attempt signal (withheld members/undeclared services; ctx.logger and other officially injected services are allowlisted) | medium | code only | likely |
| R6 | String coarse-scan fallback (obfuscation signals need combined evidence with dynamic execution) | info | both | heuristic |
| R7 | Hardcoded secrets | high | both | likely |
| R9 | Resource safety (unbounded allocation / exit-less synchronous loops / spawn-in-loop / ReDoS / non-terminating recursion / growth patterns in loops) | high (allocation/dead-loop/fork) / medium (ReDoS/recursion/Map.set) / info (resident loops/+=/Promise.all) | both | certain/likely/heuristic |
| R10 | Supply chain (package.json install hooks / dependency manifest) | high (install hooks) / info (dependency manifest) | files | likely/heuristic |
| R11 | Destructive file operations (fs deletes / sensitive-path reads-writes) | high (sensitive paths) / medium (deletes) | both | likely |
| R12 | Cordis/DSH contract (entry file / bundle-patch declaration / name / engines.node) | high (missing patch / missing entry) / medium (no entry / missing name) / info (low node version) | files | certain/likely |
Scoring model
staticScore = max(0, 100 - Σ(severity weight × hits × confidence coefficient))
verdict (the single authoritative judgment; heuristics never upgrade): critical ≥ 1 → critical; otherwise
high ≥ 1 → suspicious; otherwise → clean. The verdict is produced only by the static layer: staticScore
and verdict are shown separately and never merged into a single total.
Capability boundary (honest list)
Static scanning is a "speed bump + forensics layer", not a security boundary. The following is split by impact on the verdict, and the forms it explicitly does not detect are listed truthfully (all empirically verified).
Detected — verdict-level (changes the verdict)
| Rule | Problem class | Hit → verdict | Verified |
|---|---|---|---|
| R1 | Constructor-chain escape: x.constructor("return process") / x["constructor"]("return " + "process") / new (globalThis.constructor.constructor)("return process")() (dot/bracket-access + new forms; s |