Development & Runtime

dsh-plugin-vet

wulun811/dsh-plugin-vet · ★ 4

wulun811Curated

Trust pipeline for deepseek-harness plugins: deterministic static scan (11 rules) + LLM-driven audit protocol + two-part scorecard, with optional runtime guard (T1 sentinel / T2 fs & child_process hooks) and honeypot decoys. Monitor-and-alert only — vet never blocks or kills on its own.

License
MIT
Added
2026-08-16
Maintenance
active
Install kind
github
npm
@jieai/dsh-plugin-vet
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-08-19
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add github:wulun811/dsh-plugin-vet

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: en · Read Chinese README

Table of contents
  1. Screenshots
  2. Notable changes since 0.1.x
  3. Installation
  4. Config (cordis.yml)
  5. Safety tiers (0.3)
  6. Environment variables
  7. Tools
  8. Shield panel (0.3 revamp)
  9. Automatic behavior
  10. Static rule table (R1-R20)
  11. Scoring model
  12. Capability boundary (honest list)
  13. Detected — verdict-level (changes the verdict)

English | 中文

npm version License: MIT Node dsh.so risk

🔗 dsh.so plugin submission & security-report pages run vet-led scanning — view

🌐 Landing site: https://wulun811.github.io/dsh-plugin-vet/ — features, architecture, live dashboard numbers, trust boundaries and known limitations (bilingual, dark mode). Source in site/.

Audit before install, guard at runtime. Run every DSH plugin through dsh-plugin-vet before mounting it: static rules produce a verdict (deterministic, unforgeable), the agent investigates sensitive points and quality issues following the vet-audit-protocol skill (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). In the default configuration vet never acts on your behalf — it never auto-uninstalls, never kills processes, never rewrites configs, and blocks nothing. Interception exists only in explicit, documented scopes: the N7 confirmation block wakes together with the runtime guard (confirmBlock — credential-file deletion/overwrite and post-confirmation destructive ops throw once runtimeGuard: watch is on, incl. when the hardened tier or the shield toggle enables it), and deny mode / the paranoid tier roll back plugin loads and block per threshold. Every interception scope is deployer-visible and documented below; none is part of the default 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).

Screenshots

vet shield panel (light theme)

vet shield panel (dark theme)


Notable changes since 0.1.x

If you're upgrading from 0.1.12 or earlier, here's what changed:

  • 0.1.13-0.1.15: Landed the NEXT-GEN-PLAN (N1-N6): hidden capability detection (N1), upgrade behavioral diff (N6), anti-obfuscation decoding, environment snapshot tamper-proofing.
  • 0.1.16: Security hardening batch: bundle-ized entry (C1, closes the require(absolute-path) attack surface), ESM blind-spot explicit coverage (C2), content-baseline integrity (M7).
  • 0.1.17-0.1.19: Bug fixes and noise reduction: npm pack integrity check, rc.8 subpath entryName handling, session-log deletion silence, DSH install-tree exemption widened.
  • 0.1.20: Defense statistics panel (see how many plugins you've protected), startup file existence check, esm-guard-coverage dedup, upgrade-cold linked to audit records, red upgrade-diff now tells you to re-run audit protocol.
  • 0.1.21: Self-scan trust annotation — when vet itself is scanned (scan_plugin target=package, self-dogfooding on dsh.so), the result now carries a selfScan Trusted card instead of raw radar-style criticals: declaration-bound capability downgrade (only declared capability tokens are exempted; any undeclared outbound host / env var / credential path / IPC primitive stays red), per-version artifact pin (vet-self-pins.json, publish-bound — upgrades don't false-flag and swapped bytes fail the pin), and a publish gate rejecting releases with used-but-undeclared capabilities. The raw scan (all findings) stays fully visible. Details: docs/ARCHITECTURE.md §5.12. round-16 additions: the pin now covers the shipped artifact (lib/** + root manifests + docs/**) so production installs (tarball = lib only) reach Trusted instead of being permanently dev-tree, and byte-matching any published pin counts as pinned-match — the upgrade window no longer makes two vet instances distrust each other; official @deepseek-ai/* packages are now statically scanned even on first-seen (only deny escalation is exempted — the hash baseline alone cannot stop name-spoofed tarballs).

If you were only using static scans before, enabling runtimeGuard: watch now gives you the full defense stack: T1 sentinel (memory/fd/child-process monitoring) + T2 hooks (fs/child_process/network interception) + N7 confirmation blocking.


Installation

dsh plugin --profile <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 in the default configuration: reports only, blocks nothing — interception wakes only with explicit config, see confirmBlock / mode / the hardened-and-above tiers below).

Local tarball install (offline or verify-before-release scenario):

dsh plugin --profile <profile> add ./jieai-dsh-plugin-vet-<version>.tgz
# or unpack directly into the profile's node_modules:
# tar -xzf jieai-dsh-plugin-vet-<version>.tgz -C ~/.dsh/profiles/<profile>/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 add falls back to pnpm's file: protocol; a local tgz is resolved directly).

First-install time note: the first dsh plugin add into 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 3 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+ (peers: @deepseek-ai/cordis ^4.0.1, dsh-* ^0.1.1-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.2; verified against npm-public 0.1.1-rc.2 in round-15 and re-verified against 0.1.5-rc.1/0.1.5-rc.2/0.1.7-rc.1 since — the 0.1.7 sync added R12 array-form dsh.bundle.patch support and official-package artifact grading). 0.3.16 (DSH 0.2.0-rc.1 sync): the peer range is now the union of both families — ^0.1.1-rc.1 expands to <0.2.0-0, which by semver's prerelease rule does not satisfy 0.2.0-rc.1, so boot's preflight skipped vet. The new range admits 0.1.1-rc.1/0.1.7-rc.1/0.1.7-rc.2/0.2.0-rc.1/0.2.1 and still refuses 0.1.0-rc.9 and 0.3.0-rc.1 (measured on the shipped gate). DSH's plugin peer preflight accepts vet's ranges (prereleases participate via includePrerelease). pnpm may warn about unmet peer dependencies — this is expected: profile templates set autoInstallPeers: false, and at runtime the packages resolve from the DSH install closure ($DSH_HOME/profiles/node_modules fallback layer); you neither need nor should install another copy of the cordis family in the profile.

0.4.1 (automatic update-adaptation notice): each release declares the DSH range it was verified against in package.json → dshCompat.tested (0.4.1 declares ^0.1.1-rc.1 || ^0.2.0-rc.2 || ^0.2.1-rc.2). The shield's Version & Compatibility card shows two things. Offline (always on, no network): if the running DSH falls outside the declared range, you get a warning that compatibility is unverified. Online (at most once per 24 h): vet asks registry.npmjs.org for the latest VET's declared range and advertises an upgrade only when a newer version exists and it declares coverage of your running DSH — if we have not shipped an adapted version, it stays silent rather than claiming one exists. Set updateCheck: false for a fully offline deployment (or run mode: deny, where egress is suppressed and only the offline layer remains). There is deliberately no one-click upgrade: the panel shows the version, the declared range, a copyable npm i -g command, and the data source. The card's quiet state ("verified for this DSH" / "up to date") auto-hides 20 minutes after you first see it, since it carries no news — states with news (out-of-range, update-available) never hide. After it collapses the same verdict is still available in the About panel. New network destination: none (registry.npmjs.org was already declared); the new behaviour is periodic scheduling.

0.4.1 (DSH 0.2.1-rc.3 re-verification): DSH moved to the 0.2.1-rc.* line while 0.4.1 was waiting for release, so the declared range now ends at ^0.2.1-rc.2 and was tested against both 0.2.1-rc.2 (npm latest) and 0.2.1-rc.3 (npm next). Two host changes touched vet's surface: @deepseek-ai/dsh-invariants is gone from the 0.2.1 family (vet's package-level invariant was already a probe that no-ops when the service is absent — its peer is now genuinely optional), and @deepseek-ai/dsh-client-ui-slots is no longer a standalone client module (the slots service is provided by dsh-client-ui-renderer, so dsh.client.inject names the renderer; inject/register and the conversation.session.header.actions key are unchanged). Verified on the live host: status.json reports dshVersion 0.2.1-rc.3 with compat: ok; before the range update the same host reported out-of-range, which is the offline layer doing its job.

0.3.18 (DSH 0.2.0-rc.2 re-verification): the rc.1→rc.2 delta touching vet's surface is additive-only. @deepseek-ai/cordis is untouched (still 4.0.4); dsh-tools/dsh-session/dsh-invariants/ dsh-client-ui-slots/dsh-client-locale differ from rc.1 only in their package.json version; dsh-client-ui-conversation adds six ask.* locale keys plus an equivalent slots refactor; dsh-llm adds one MessageSourceMap member. The four packages vet patches at runtime (dsh-atomic-write, dsh-storage-json, dsh-config-editor, dsh-credentials-local) are byte-identical, so no denoise anchor moved. The declared peer range already admits 0.2.0-rc.2 (measured), and the devDependencies now track it. Verified live on a 0.2.0-rc.2 host: 76 of 77 scan records are rc.2 and all clean, and vet's client module is served inside the rc.2 plugin bundle. @deepseek-ai/dsh-settings-file is still absent from this family (retired after 0.1.6-alpha.2), matching vet's notes.

npm-public DSH (0.1.1-rc.2+): a profile loads plugins from dsh.profile.bundles in the profile's package.json (boot composes bundle layers + cordis.patch.yml + $DSH_HOME/cordis.patch.yml). After dsh plugin add, add the package name to that bundle list (or insert it with a patch - insert: row) — otherwise the package is installed but not mounted. vet's config block uses the same row-id form (- id: plugin-vet / config: …). Guarded paths/tests are unchanged.

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 <name> add @jieai/dsh-plugin-vet) and point requireAudit at the matching profile's cordis.patch.yml.

Config (cordis.yml)

Key Default Description
profile standard Safety tier (0.3): standard = current defaults (lowest noise); hardened = wakes dormant capabilities (runtime guard, third-party baseline, honeypot; R17/R18/R19 observations surface as yellow); paranoid = hardened + strictest blocking (requireAudit, denyOn: suspicious, N7 family 3/4 block). Presets only override keys still at their default; explicit settings (incl. panel-toggle writes into the patch) always win; verdict semantics never change — see "Safety tiers" below
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, third-party only — official @deepseek-ai/* packages are governed by content-hash baseline + static scan instead, round-17): when enabled, loading a third-party plugin checks ~/.dsh/vet/audits/ for a health record — 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-R20; e.g. {"R17": false} disables the !!js config surface)
scanSurface all on Static scan-surface switches (0.2.6, engine static-v14+, current static-v26): configFiles (cordis.yml/patch !!js detection, R17), instructionFiles (instruction/skill injection observation, R18); disabling only affects the new surfaces, the legacy surface keeps scanning
observeLoopback true Local-API loopback observation (0.2.6 default off; 0.3 default on — loopback + control-plane path + third-party attribution, official attribution exempt, yellow dismissible): when on, plugin requests to 127.0.0.1 enter the N3 ledger, and hits on DSH control-plane paths (/api/, session.*, /plugins/) attributed to third-party plugins raise a yellow loopback-control observation (alarm-only, dismissible). Observation is not a fix — RPC auth is a dsh-side concern
telemetryDiff true Telemetry config sensitization (0.2.6): periodically hashes telemetry exporter url/mode fields in profile config; cold start records only; host change → yellow (requires restart verification, G-3 shape). Hashes only — config content never enters alarms/archive
thirdPartyBaseline false (on under hardened/paranoid) Third-party post-install integrity baseline (0.2.6): records first-install content hash for non-official packages; same-version content change → red (exempt via acknowledgedPackageHashes). Change-detection, not a trust anchor; the static scan still runs regardless
denyOn critical Blocking threshold in mode: deny
updateCheck true Automatic update-adaptation notice (0.4.1). Offline layer (always on, zero network): the running DSH version is compared against this release's package.json → dshCompat.tested; outside that range the shield's Version & Compatibility card warns that compatibility is unverified (an unreadable version yields unknown, never a false warning). Online layer (throttled to once per 24 h, fail-quiet): the latest VET's declared range is read from registry.npmjs.org and an upgrade is advertised only when a newer version exists and it declares coverage of the running DSH — if no adapted version has been published, it stays silent rather than claiming one exists. Any failure (offline, HTTP error, oversized/malformed body) is silent. Set false to disable both layers for a fully offline deployment. Egress destination is unchanged (registry.npmjs.org was already declared by registry-verify); deny mode suppresses the network call while keeping the offline layer. There is deliberately no one-click upgrade — the panel shows facts plus a copyable command
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) plus the N7 confirmation block (confirmBlock defaults to block — see below; wakes whenever watch is on, incl. via the hardened/paranoid tiers)
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
contentBaseline true Official-package content-hash baseline (P-5): computes a SHA-256 over each @deepseek-ai/* package's files and compares it against the recorded baseline — a same-name impostor (file:/tarball with no registry validation) is judged by the strictest plugin rules on hash mismatch. First-seen stores and trusts the baseline; baseline storage is multi-version by name@version (capped: 1000 files / 50MB / 10s)
networkEgress true Runtime network egress observation (P1): wraps http/https/net/http2/tls/dgram/fetch to observe plugin-originated outbound requests (alarm-only; needs runtimeGuard: watch)
transitiveDeps false Transitive dependency vulnerability audit (P1, opt-in, default off): shells out to a locally installed upstream-radar CLI (never npx-auto-installed); missing / timeout / unexpected output shape degrades silently to direct-dependency-only. Hits surface as OSV-T medium findings
contract enabled; dir ~/.dsh/vet/contracts Runtime contract snapshots (0.3, M1): a per-plugin contract file states the operation surface the plugin declares acceptable; vet reconciles observed runtime actions against it — out-of-surface alarms are recorded as info m1-contract-violation (aggregated per plugin + field), a rejected contract is noted once per plugin (yellow), and an N1 hidden-capability finding invalidates ("distrusts") the contract (yellow, once per plugin). Record-tier only: contracts never gate or block loading. Env override: DSH_PLUGIN_VET_CONTRACTS_DIR
confirmBlock block N7 confirmation block (0.1.14, needs runtimeGuard: watch): only irreversible destruction is intercepted. block (default) — families 1/2 intercept on certain confirmation; alarm — all families alarm-only; off — disabled. Every block throws with an actionable message and writes a red n7-block alarm; process-memory state (cleared on restart)
confirmBlockFamily3 alarm N7 family 3 override (persistence/privilege-surface writes: bashrc/cron/systemd/ld.so.preload/sudoers.d/profile.d/autostart/authorized_keys/hosts/ssl). Explicit block is user opt-in — interception risk is the user's choice; default alarms only
confirmBlockFamily4 alarm N7 family 4 override (supply-chain/install-state writes: node_modules package files, cordis.patch.yml / cordis.yml / plugin.json). Explicit block is user opt-in; default alarms only

Official @deepseek-ai/* packages are exempt by default (built-in trust).

Safety tiers (0.3)

profile preset-expands into the existing per-knob config — a deployment-strategy layer, not a second parallel config system. Three principles:

  1. Tiers never change verdict semantics — the verdict is produced only by the deterministic static layer (trust boundaries 1/4); tiers only change observation depth, alarm surface, and block scope.
  2. Explicit beats preset — keys the user set to a non-default value survive; keys written into the vet entry of the profile cordis.patch.yml (e.g. the shield's runtime-guard toggle) count as explicit and are never overridden. Known boundary: a key explicitly set to its default value via the plugin config section is indistinguishable from unset and gets the preset applied — the patch is the true explicit channel for "off".
  3. False-positive cost scales with the tier — higher tiers trade noise for coverage (see cost column).
Tier Position Preset expansion Cost
standard (shield label: Light defense / 轻度防御) (default) General public, lowest noise nothing — current defaults no runtime layer (static + telemetryDiff + official-package baseline only); the shield shows a hint when the runtime guard is off
hardened (shield label: Medium defense / 中级防御) Wake the capabilities already written runtimeGuard: watch, thirdPartyBaseline: true, honeypot.enabled: true; R17/R18/R19 info observations surface as yellow alarms (alarm-only, verdict unchanged) ~10-20% hot-path overhead; more dismissible yellows
paranoid (shield label: High defense / 高级防御) High-sensitivity environments hardened + requireAudit: true, denyOn: suspicious, confirmBlockFamily3/4: block highest noise; interception expanded (blocks persistent/install-time writes after confirmation)

observeLoopback is on for all tiers (0.3): its signal is specific enough (loopback + control-plane path + third-party attribution; official attribution exempt) that the unattended P15/P16/P17/G-2/G-5 family is back on the alarm surface at zero user action.

The shield panel has a one-tap tier selector (writes the patch via /vet/profile, preserving other config keys) and a tier explainer inside the ? help panel — no manual config editing needed; the runtime guard flips immediately and is persisted to the profile patch, the remaining tier-preset expansion keys apply after a DSH restart/hot-reload.

0.3.1 binding (guard ↔ tier): the defense tier and the runtime guard are no longer two independent knobs — Light defense ⇔ guard off; Medium/High defense ⇔ guard on. Pressing "enable guard" raises the tier to Medium (an already-High setting is never downgraded); pressing "disable guard" returns to Light; selecting a tier switches the guard immediately (the remaining preset-expansion keys apply on the DSH config reload — patch writes trigger the watchUserPatches hot-reload).

Environment variables

All DSH_PLUGIN_VET_* paths are snapshotted at module load (vet loads before third-party plugins — a plugin changing process.env afterwards cannot redirect vet's storage). Set them in the host environment (i.e. in the DSH profile/weekly launch script), not from inside a plugin. Since 0.3.15 every store path derives from a single root (DSH_PLUGIN_VET_STORE_DIR, default ~/.dsh/vet), and a test runtime (VITEST / NODE_ENV=test) defaults to a process-private temp directory — running vet's own test suite can no longer write your real store.

Variable Default Purpose
DSH_PLUGIN_VET_STORE_DIR ~/.dsh/vet Base root (0.3.15) for every store file below (capabilities / baseline / stats / scan-summaries / known-boundaries / official-catalog / forensics / contracts / audits / dismissed-alerts / update-check). Per-file variables below still win when set
DSH_PLUGIN_VET_CACHE_DIR <tmpdir>/dsh-plugin-vet-cache Static-scanner report cache (sha-256 keyed, 0600 files)
DSH_PLUGIN_VET_BASELINE_DIR ~/.dsh/vet Content-baseline store (baseline.json) + N6 capability history (capabilities.json) + version snapshots
DSH_PLUGIN_VET_ARCHIVE_DIR ~/.dsh/vet/audits Audit health records — where requireAudit looks for <plugin>-<version>-<ts>.md
DSH_PLUGIN_VET_FORENSICS_DIR ~/.dsh/vet/forensics Forensics journal root (post-confirmation per-plugin recording, 0700 dirs / 0600 files)
DSH_PLUGIN_VET_CONTRACTS_DIR ~/.dsh/vet/contracts Runtime contract snapshots (state contracts + observation reconciliation)
DSH_PLUGIN_VET_STATS_DIR ~/.dsh/vet Defense statistics (stats.json, atomic write, 0600)
DSH_VET_SIDECAR_PID (internal) T1 sentinel PID registry that survives hot reloads — internal, do not set

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. Optional scanBasis: npm (default — registry tarball artifact, R12 entry/ patch checked against the real release) / git (source-only repo, where lib/ etc. usually aren't committed — R12 entry/patch-missing findings drop to info so git-only rescan doesn't false-positive). Since 0.1.21 the scorecard's capability block also reports the R16 ghost/zombie dependency fields (declared vs imported vs installed). When vet scans itself (realpath-verified, not name-matched), the scorecard adds a selfScan trust annotation — declared-capability-token downgrade (only declared tokens are exempt; undeclared outbound/env/credential/IPC stays red) plus the per-version artifact pin (vet-self-pins.json, round-16: the pin covers the shipped lib/** artifacts so production installs reach Trusted; byte-matching any published pin counts as pinned-match) — the raw findings stay fully visible.
  • vet_diff — read-only, purely local: prints the stored version history of a package and the behavior diff between its last two recorded versions (N6). Outputs hosts/fsPaths/spawnCmds/imports added|removed and network/exec capability flips. No scan, no network.
  • vet_label — read-only, purely local: prints the human-readable "capability nutrition label" (M2) for a package — the files it touches, the hosts / subprocesses it references, its third-party imports (capability unknown), and its network/exec capability flags, plus a summary of the last upgrade diff. Sources from the same local N6 capability history; the label represents declared (static-side) capabilities — runtime observed/dormant capabilities are the domain of the running shield. No scan, no network.
  • 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/<plugin>-<version>-<ts>.md using 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.

Shield panel (0.3 revamp)

The GUI was reskinned per the OBSIDIAN MOSS GOLD design mock (dark recipe; a matching light variant ships in the same token set) and rebuilt as a layer stack — secondary panels slide out flush against the main panel's right edge (never the browser's right edge); the whole stack shifts left when space runs out — panels never overlay one another — and Esc pops layers one at a time:

Layer Panel Contents
L1 Main 3 ring+trend composite cards (memory/CPU/fd: value and direction in one card), foldable memory/IO details, runtime guard + safety tier, defense stats, audit bar, upgrade-diff & honeypot floating cards
L2 Alerts timeline / Recent plugins / Audit & Honeypot / About timeline = rail+dot+card (dismiss/restore/copy); recent plugins = scan-record corridor, 20 per page with "Load more" paging (round-21); audit center = pending-audit backlog + honeypot touches
L3 Plugin details (the only third level) 6-axis capability radar, rule-hit wall, OSV/AI-review meta, upgrade diff, declared-side nutrition label

Data additions (read-only, backward compatible): GET /vet/status.json gains metricsHistory (64-point trend), audit (pending/new/plugin index/honeypot) and lastUpgradeDiff; new GET /vet/plugin?name= detail endpoint; new local scan-summary store ~/.dsh/vet/scan-summaries.json written by both the auto-scan and vet-gate paths. Honest scope: radar/nutrition reflect the declared static capability surface (same discipline as vet_label); the "blocked" mark comes from the N7 family-1 list.

Automatic behavior

  • internal/plugin auto-scan (autoScan: true): newly installed third-party npm packages are static-scanned on load; deny mode + verdict ≥ denyOn → load rolled back.
  • Audit gate (requireAudit: true): loading a third-party plugin without a health record — report mode logs a yellow audit-required alarm (enters the /vet/status.json alarm list, plugin loads normally); deny mode rolls back the load (references vet-audit-protocol as 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. Third-party only (round-17): official @deepseek-ai/* packages are governed by content-hash baseline + static scan (decision 1: first-seen/match still fully scanned, only deny escalation exempt), so DSH-bundled official plugins never fire audit-required.
  • tools/execute interception: cordis_define / run_code / workflow are scanned before execution (cordis_run's real schema carries no code payload, so the guard slot stays dormant as a tripwire — if a future schema adds code/source/script payloads it is scanned immediately; zero false positives today); report mode prefixes non-clean results with VET: (clean executions don't pollute machine-readable output), deny mode blocks outright (isError).
  • Runtime guard (runtimeGuard: watch) — T1/T2 observation is alarm-only; interception lives in the dedicated N7 layer below ("N7 confirmation block"):
    • 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, ~/.dsh config-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 ~/.dsh session/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/x still 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 <src>.<pid>.<uuid>.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.bak still alarms).
      • OS temp roots and macOS /private (0.4.2, user report): on macOS /tmp, /var and /etc are symlinks into /private/*, so the canonical path is /private/tmp/… or /private/var/…. Previously /private/tmp/... (atomic-write staging) and /private/var/folders/<xx>/<yy>/T/... (os.tmpdir(), where DSH's atomic staging and workspace-changes' private git index live) were hit twice — by the private keyword and by the /var system-root prefix — producing a burst of 20 unattributed red fs-destroy alarms. Paths are now normalized (dot-segment folding + macOS /private folding) before judgement, and writes/deletes under /tmp, /var/tmp and os.tmpdir() pass; reads are not exempt (/tmp/id_rsa is still visible as a key copy). Boundaries hold: /private/etc, /private/var/db, /var/log, ~/.dsh/.credentials.yaml still alarm, and /tmp/../etc/hosts folds out of the temp root and still alarms. The roots are HookConfig.tempRoots (/tmp, /var/tmp, os.tmpdir(), materialized at load) and candidates are validated — TMPDIR=$HOME/.ssh cannot turn a credential directory into an exemption surface. Case is folded on darwin as well (macOS volumes are case-insensitive by default): /ETC/hosts is /etc/hosts, yet the root-prefix check compared case sensitively, so an all-caps spelling rewrote /etc/hosts with no alarm at all; Linux/Windows keep exact case (ext4 cannot resolve /ETC/hosts to /etc). Honeypot/canary matching (isHoneypotPath, isIntegrityPath) normalizes both sides too, so a lure spelled /private/tmp/... no longer misses. The N7 confirm-block uses the same normalization (/private/etc/hosts no longer bypasses family 3).
  • GUI shield: a browser half registers into conversation.session.header.actions and polls /vet/status.json to show a green/yellow/red light + alarm count. Activation requires a dsh web restart (client-modules only scans the dsh.client declaration 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: watch config (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 is persisted to ~/.dsh/vet/dismissed-alerts.json (0.2.1+), so it survives restarts. 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).
    • Scan verdicts are dismissible too (0.4.1): a non-clean scan result becomes a first-class alarm (yellow for suspicious, red for critical) instead of implicitly raising the shield through the "last scan" echo. The message names the evidence — for an OSV hit on a declared dependency it says "dependency zod@4.4.3 hits GHSA-…" rather than implying the package's own code is bad — and notes when the package is official. When many official packages carry the same advisory (0.2.1: 54 of them pin the same zod@4.4.3) they collapse into one row with a ×N count, so the fix upstream is one notification rather than fifty; third-party packages are never folded into that row. The alarm id carries an evidence fingerprint, so dismissing silences that evidence (persisted), while a new advisory, a new dependency or a changed rule set produces a new id and re-alerts: one dismiss never blinds you to a different problem. The aggregated official row folds on the set of advisories plus the dependency and the verdict, so a second advisory on the same dependency starts a new row even after the first was dismissed. A clean re-scan of a package retracts its share of the verdict; an unrelated package scanning clean does not.
    • Display caps: the panel shows the most recent alarms (at most 20); 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). The recent-scan echo is display-only since 0.4.1 — it no longer raises the shield by itself (a verdict is an alarm now) — and it expires on the same 24h TTL (P3-2: one suspicious scan never turns the shield permanently yellow; sustained scanning renews naturally).

Static rule table (R1-R20)

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 incl. prepare/preuninstall; dependency manifest → info; OSV exact-version vulnerability query (default-on osvCheck, configurable; network fail-open) high (install hooks) / info (dependency manifest; OSV advisory) 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
R13 Hardcoded network exfiltration sinks (Discord/Telegram/Slack webhooks, cloud-metadata endpoints, .onion) in string literals high both likely
R14 Download-and-exec primitives in shipped non-JS scripts (.sh/.bash/.ps1/.cmd/.bat/.psm1/.zsh: curl|sh, encoded PowerShell, IEX, certutil…; python -c / ruby -e / perl -e download-exec included) high (plugin) / info (generic) files likely
R15 Dynamic network targets (fetch / WebSocket / http(s).request get / net.connect whose target argument cannot be statically resolved — "deliberately obscured" target) info (observation; escalates only when other signals stack, e.g. N1 hidden capability fires) both
R16 Dependency consistency audit: ghost deps (imported by code but not declared in package.json — resolves only via transitive hoisting) and zombie deps (declared in package.json but missing from node_modules) info (advisory; never into verdict) files heuristic
R17 !!js config injection (root-level cordis.yml / cordis.patch.yml / plugin.yml !!js expressions: presence observation + dangerous-verb enumeration + base64/hex decode hook-in; "verb + exfil-host/credential-path" double combos → high; test/CI dirs and generic packages stay info. Text extraction only, never executed) high (double combo) / info (single verb / observation) files (surface.configFiles; engine static-v14+) likely (double combo) / heuristic (observation)
R18 Instruction/skill injection observation (AGENTS.md / CLAUDE.md / CODEGOV.md and SKILL.md under skills/ or *.skill dirs: combined-text features — instruction rewrite × credential/exfil/persistence action, ≥2 independent group hits to fire; v1 all-info observation, escalation after real-corpus tuning) info (observation; never into verdict) files (surface.instructionFiles; engine static-v14+) heuristic
R19 Typosquat observation (package name / deps vs a curated core list of official @deepseek-ai names: Levenshtein <=1 or visual homoglyphs — dshh / d5h / dsh_tool_bash; only against the curated core list; everything else is covered by R10 dep manifest + OSV + manual pre-install review) info (observation; never into verdict) files heuristic
R20 Shell download-and-exec in exec/spawn-family arguments (0.3.2): hardcoded curl|sh / wget|sh / PowerShell -enc/IEX/DownloadString / system download primitives (certutil/bitsadmin/mshta/regsvr32/rundll32) / interpreter -c-style (python/ruby/perl) in exec/spawn/execFile/fork argument literals — incl. array form spawn('sh', ['-c', …]) and N2-decoded args; child_process binding required ("exec call + dangerous command" two-signal gate); curl -o download-to-disk alone is medium (download ≠ exec) high (pipe/encoded/primitive → suspicious) / medium (curl -o) / info (generic, test/CI) both likely

Engine pipeline additions (0.1.13): besides the rule set, the scanner now produces a per-package capability manifest (N1) — hosts/fsPaths/spawnCmds/imports/hasNetwork/hasExec extracted from source (plus R16 ghostDeps/zombieDeps dependency-consistency fields from the package.json vs node_modules audit) (declaration-side facts, never verdicts, conservative over-collection) — and runs a literal decode preprocessor (N2) that statically decodes base64 / hex / Buffer.from / String.fromCharCode / constant concatenation / template literals (all-literal arguments only, ≤4KB, ≤2 nesting layers, never executes code) and feeds the decoded text back into R13/R7/R11/R20 matching (findings carry decodedFrom and the original line for audit). Capabilities enable the cross-layer diff (see Runtime monitoring below).

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.

Artifact grading (0.3.13): files that are not authored source — *.d.ts declarations, files under a package-root-relative build-output directory (lib/dist/build/out/esm/cjs/umd), minified/bundled content — are labelled in the finding message (构建产物: / 压缩产物: / 类型声明:). For official catalog members whose bytes are trusted (first-seen/match — the DSH-upgrade case — or a mismatch whose hash is listed in acknowledged-package-hashes, i.e. a declared local patch) those findings' decisive tiers fold to info (marked (官方包降噪)), because official identity is established by the content-hash/registry layer and a family-wide version bump would otherwise turn machine-generated output into shield-wide noise; an undeclared mismatch keeps the strict scan (auto-scan goes further for declared patches: it skips scanning them entirely and records the "declared local patch" note as info — visible in the panel, excluded from alarmCount/the shield, while an explicit scan_plugin audit still scans and folds). Same tier (0.3.14): an official-family scan-fail (scanner timeout/fault) is an info observation too — identity and integrity belong to the hash/registry layer, and a tool-side event must not level the shield (still visible and dismissible in the panel, logger.error unchanged); third-party failures stay yellow and deny mode still fail-closes on them. Third-party packages keep full severity and only gain the label; authored source (src/**, scripts/**, root-level scripts, package.json) is never folded.

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; string args statically evaluable: literals/templates/concatenation/const bindings; new supports const-alias tracking) critical matrix + multi-file ✓
R2 Dynamic execution: eval() / Function() / new Function / new AsyncFunction (incl. parenthesized new (Function)(...); escape-string args → critical) / (async)=>{}.constructor capture (round-7.2: new X.constructor reported only when the base is a function literal — new n.constructor(n.type, n) object-clone no longer false-positives) / vm.runInContext/runInNewContext / dynamic import() / require() high (files) / medium (code, escape-string → critical); bin entries judged as generic code, drop to medium matrix + round-7/7.2 regression ✓
R3 Direct process access: getBuiltinModule/mainModule/module/exit (incl. reallyExit) → critical; side-effect members (kill/abort/chdir/umask/setuid/dlopen/binding, etc.) and unknown members → high; read-only members (round-7.1): env/cwd/platform/pid/argv/execPath/stdin/stdout/stderr/nextTick/on, etc. → info capability surface (reading cwd/env/pid isn't an escape channel; no-bin MCP/tool plugins like bridges no longer get hurt); runtime='sandbox' caps at high; shape degradation: generic packages / bin entry files / app-type packages → info critical / high / info matrix + round-7.1 regression ✓
R4 Host-closure capture: reading .constructor of agent/parallel/pipeline/phase/log/TextEncoder/TextDecoder/btoa/atob or feeding Object.getPrototypeOf (code scenario); host-global prototype pollution: <builtin>.prototype.<member> = ... override assignments and Object.defineProperty(<builtin>.prototype, ...) (Object/Array/String/Function/TextEncoder/URL/Buffer and 40+ builtins, round-7) critical (code) / high (files, since round-7.1 independent of targe

…