wulun811/dsh-plugin-vet2

@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.

包名
@jieai/dsh-plugin-vet
版本
0.1.6
许可证
MIT
最近更新
2026年8月16日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:wulun811/dsh-plugin-vet

@jieai/dsh-plugin-vet — Trust pipeline for DSH plugins

English | 中文

npm version License: MIT Node dsh.so security

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-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). 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).


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 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 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 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.

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 point requireAudit at the matching profile's cordis.patch.yml.

Config (cordis.yml)

KeyDefaultDescription
modereportreport reports only, never blocks; deny explicitly enables blocking
autoScantrueAutomatically static-scan new plugins (internal/plugin)
scannerTimeoutMs15000Static-scan subprocess timeout
requireAuditfalseAudit 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)
denyOncriticalBlocking threshold in mode: deny
allowlist[]Package/plugin-id allowlist (skip scanning)
runtimeGuardoffRuntime guard (performance/stability cost, opt-in): off = disabled; watch enables the T1 sentinel + T2 hooks, alarm-only
runtimeIntervalMs2000T1 sentinel /proc sampling interval
runtimeMemLimitMb2048T1 memory alarm threshold (host VmRSS, over limit → red)
runtimeForkBurstN5T1 child-process burst alarm threshold (single-round delta → red)
runtimeFdLimit512T1 file-descriptor alarm threshold (→ yellow)
runtimeGrowthMb256T1 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)
runtimeGrowthWindowMs600000Growth-detection window (default 10 minutes)
honeypot.enabledfalseHoneypot 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
osvChecktrueQuery 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/ --.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.

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.
  • 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 — P3-11 synced); 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) — 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, ~/.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 . ..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).
  • 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 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).

Static rule table (R1-R12)

IDNameDefault levelScopeDeterminism
R1constructor-chain escapecriticalcode + filescertain/likely
R2Dynamic execution (eval/Function/import/require)high (files) / medium (code; bin entries drop to medium)bothcertain/likely
R3Direct process access (runtime-graded; read-only members/generic/bin entries/app-type packages → info)critical (host) / high (sandbox)bothcertain
R4Host closure capture (agent/TextEncoder…) + host-global prototype pollutioncritical (code) / high (files, independent of targetKind)bothcertain/likely
R5ctx-escape attempt signal (withheld members/undeclared services; ctx.logger and other officially injected services are allowlisted)mediumcode onlylikely
R6String coarse-scan fallback (obfuscation signals need combined evidence with dynamic execution)infobothheuristic
R7Hardcoded secretshighbothlikely
R9Resource 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)bothcertain/likely/heuristic
R10Supply chain (package.json install hooks / dependency manifest)high (install hooks) / info (dependency manifest)fileslikely/heuristic
R11Destructive file operations (fs deletes / sensitive-path reads-writes)high (sensitive paths) / medium (deletes)bothlikely
R12Cordis/DSH contract (entry file / bundle-patch declaration / name / engines.node)high (missing patch / missing entry) / medium (no entry / missing name) / info (low node version)filescertain/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)

RuleProblem classHit → verdictVerified
R1Constructor-chain escape: x.constructor("return process") / x["constructor"]("return " + "process") / new (globalThis.constructor.constructor)("return process")() (dot/bracket-access + new forms; s