@jieai/dsh-plugin-vet

August 27, 2026 ยท View on GitHub

English | ไธญๆ–‡

npm version License: MIT Node dsh.so risk

๐Ÿ”— dsh.so plugin submission & security-report pages run vet-led scanning โ€” view

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; verified against npm-public 0.1.1-rc.2 in round-15 and re-verified in the follow-up review). 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.

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)

KeyDefaultDescription
profilestandardSafety 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
modereportreport reports only, never blocks; deny explicitly enables blocking
autoScantrueAutomatically static-scan new plugins (internal/plugin)
scannerTimeoutMs15000Static-scan subprocess timeout
requireAuditfalseAudit 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)
scanSurfaceall onStatic scan-surface switches (0.2.6, engine static-v14+, current static-v20): configFiles (cordis.yml/patch !!js detection, R17), instructionFiles (instruction/skill injection observation, R18); disabling only affects the new surfaces, the legacy surface keeps scanning
observeLoopbacktrueLocal-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
telemetryDifftrueTelemetry 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
thirdPartyBaselinefalse (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
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) plus the N7 confirmation block (confirmBlock defaults to block โ€” see below; wakes whenever watch is on, incl. via the hardened/paranoid tiers)
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
contentBaselinetrueOfficial-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)
networkEgresstrueRuntime network egress observation (P1): wraps http/https/net/http2/tls/dgram/fetch to observe plugin-originated outbound requests (alarm-only; needs runtimeGuard: watch)
transitiveDepsfalseTransitive 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
contractenabled; dir ~/.dsh/vet/contractsRuntime 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
confirmBlockblockN7 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)
confirmBlockFamily3alarmN7 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
confirmBlockFamily4alarmN7 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).
TierPositionPreset expansionCost
standard (shield label: Light defense / ่ฝปๅบฆ้˜ฒๅพก) (default)General public, lowest noisenothing โ€” current defaultsno 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 writtenruntimeGuard: 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 environmentshardened + requireAudit: true, denyOn: suspicious, confirmBlockFamily3/4: blockhighest 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.

VariableDefaultPurpose
DSH_PLUGIN_VET_CACHE_DIR<tmpdir>/dsh-plugin-vet-cacheStatic-scanner report cache (sha-256 keyed, 0600 files)
DSH_PLUGIN_VET_BASELINE_DIR~/.dsh/vetContent-baseline store (baseline.json) + N6 capability history (capabilities.json) + version snapshots
DSH_PLUGIN_VET_ARCHIVE_DIR~/.dsh/vet/auditsAudit health records โ€” where requireAudit looks for <plugin>-<version>-<ts>.md
DSH_PLUGIN_VET_FORENSICS_DIR~/.dsh/vet/forensicsForensics journal root (post-confirmation per-plugin recording, 0700 dirs / 0600 files)
DSH_PLUGIN_VET_CONTRACTS_DIR~/.dsh/vet/contractsRuntime contract snapshots (state contracts + observation reconciliation)
DSH_PLUGIN_VET_STATS_DIR~/.dsh/vetDefense 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:

LayerPanelContents
L1Main3 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
L2Alerts timeline / Recent plugins / Audit & Honeypot / Abouttimeline = 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
L3Plugin 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).
  • 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 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). 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-R20)

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 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)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
R13Hardcoded network exfiltration sinks (Discord/Telegram/Slack webhooks, cloud-metadata endpoints, .onion) in string literalshighbothlikely
R14Download-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)fileslikely
R15Dynamic network targets (fetch / WebSocket / http(s).requestget / 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
R16Dependency 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)filesheuristic
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)
R18Instruction/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
R19Typosquat 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)filesheuristic
R20Shell 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)bothlikely

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.

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; string args statically evaluable: literals/templates/concatenation/const bindings; new supports const-alias tracking)criticalmatrix + multi-file โœ“
R2Dynamic 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 mediummatrix + round-7/7.2 regression โœ“
R3Direct 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 โ†’ infocritical / high / infomatrix + round-7.1 regression โœ“
R4Host-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 targetKind โ€” pollution semantics don't distinguish plugins from generic packages, generic no longer drops to info)matrix + round-7 regression โœ“
R7Hardcoded secrets: sk- / AKIA / AIza / gh[pousr]_ / xox[baprs]- / env-var assignment / URL-embedded keys (placeholders excluded)high โ†’ suspiciousmatrix โœ“
R9Resource safety: new Array(2**31) / Buffer.alloc(1GB) unbounded allocation (โ‰ฅ1e8), while(true)/for(;;) exit-less synchronous loops (freezes the host; round-7.2: a labeled break whose label wraps the loop โ€” outer: for(;;){ ... break outer } โ€” counts as an exit signal), spawn/exec/fork/new Worker in exit-less loops (fork bomb)high โ†’ suspicious; ReDoS nested quantifiers (a+)+-class and overlapping alternation branches `(aaa)+โ†’ medium (first-char-disjoint branches like(?:[^']
R10Supply chain: preinstall/install/postinstall/prepare/uninstall/preuninstall hooks in package.json scripts (arbitrary code execution at install time) โ†’ high; dependency manifest โ†’ info (known-vulnerability check: OSV exact-version query, osvCheck can be disabled)high โ†’ suspicious (install hooks)matrix โœ“
R11Destructive file operations: fs.unlink/rm/rmdir(+Sync) deleting sensitive paths (/etc/root/.ssh etc.) โ†’ high, plain deletes โ†’ medium; fs.writeFile etc. writing sensitive paths โ†’ high; fs.readdir traversing sensitive directories โ†’ mediumhigh โ†’ suspicious (sensitive paths); medium not into verdictmatrix โœ“
R12Cordis/DSH contract: missing declared dsh.bundle.patch file โ†’ high; no entry (no main/exports["."] and no root index.js) โ†’ medium; declared entry file missing โ†’ high; plugin-intent package missing name โ†’ medium; engines.node major < 22 โ†’ infohigh โ†’ suspicious (declared mount point/entry missing means guaranteed failure); medium/info not into verdictmatrix โœ“
R13Network exfil: hardcoded Discord/Telegram/Slack webhooks, cloud-metadata endpoints (169.254.169.254 / metadata.*.internal / 100.100.100.200) and .onion destinations in string literalshigh โ†’ suspiciousmatrix + R13 tests โœ“
R14Non-JS scripts: curl|sh, wget|sh, PowerShell download-pipe / -enc / IEX, certutil/bitsadmin/mshta/regsvr32/rundll32 in .sh/.bash/.ps1/.cmd/.bat/.psm1/.zsh (python -c / ruby -e / perl -e download-exec also covered; generic โ†’ info)high โ†’ suspicious (plugin); info not into verdict (generic)matrix + R14 tests โœ“
R20Hardcoded download-and-exec in exec/spawn-family arguments (0.3.2): curl|sh / wget|sh / PowerShell -enc/IEX/DownloadString / system download primitives / interpreter -c-style โ€” checked in literal arguments of exec/spawn/execFile/fork (array form and N2-decoded args included; child_process binding required)high โ†’ suspicious (pipe/encoded/primitive); medium not into verdict (curl -o โ€” download โ‰  exec); generic/test-CI โ†’ infomatrix + R20 tests โœ“

Detected โ€” advisory level (downgrades score only, never changes the verdict)

RuleProblem classNote
R5ctx-escape attempt signal: accessing sandbox-withheld framework members / undeclared services (ctx.plugin, etc.)code scenario only; medium
R6String coarse scan: concatenated escape features, getBuiltinModule/child_process/dangerous-require module references, obfuscation features (String.fromCharCode/Buffer.from(base64)/atob(/charCodeAt โ€” since round-7 reported only when combined with an in-file dynamic-execution signal (eval/new Function/vm etc.); routine byte handling for terminal protocols/encoding no longer false-positives)info/heuristic
R8Scan timeout / file-too-large skipinfo meta-rule

Runtime monitoring (when runtimeGuard: watch) โ€” observation alarm-only; the N7 confirmation block (row below) is the only interception layer

LayerMechanismCatchesLimits
T1 sentinelSubprocess polling host /procMemory bomb (>memLimit), sustained memory growth (leak; net window growth alarms by multiple), fork bomb (child-process burst), fd surgeGranularity = host-global (plugins share the process; can't attribute to a plugin)
T2 hooksIn-process wrapping of fs/child_process (incl. fs.promises)Sensitive-path writes/deletes (/etc, ~/.ssh, .envโ€ฆ), key-file reads, spawn with shell/download-exfiltration keywordsStack attribution best-effort; per-call wrapper overhead (I/O-heavy <5%, hot paths 10-20% range)
N1 capability diff (0.1.13)Declared capability manifest (scanner, registered at plugin load) vs observed runtime actions (T2)Hidden capability executed (observed sensitive action with zero static footprint incl. imports) โ†’ red n1-hidden; imports non-empty โ‡’ ใ€Œcapability unknownใ€ conservatively covers any action; only sensitive actions participateRequires a prior scan of the plugin (auto-scan registers it); statically-visible-but-unused capabilities are recorded as dormant, shown in the nutrition label (M2, 0.1.16)
N3 exfil/destruction ledger (0.1.14)Per-plugin byte counters (sensitive-read / net-write, lifecycle cumulative) + 10s destruction signature windows + sequence signatures (READ_SECRET โ†’ SPAWN curl/wget/nc, READ_SECRET โ†’ NET_WRITE)Read-secret-then-send-data: yellow n3-exfil (both counters > 0), red n3-exfil-match (magnitudes match โ€” whole-package exfil), red sequence signatures (30s window); destruction family: mass delete / mass rename-to-encrypted-marker / read-then-overwrite-in-place / write amplification โ†’ yellow, two+ signatures together โ†’ red n3-ransom; honeypot/canary-confirmed (N4) plugins get lowest thresholdsNo session/content inspection (bytes + operation-shape only); cross-session/ultra-slow exfil, native-binary internals, fd-level reads, fetch bodies not counted (documented boundary); per-plugin attribution best-effort
N4 canary watermark (0.1.14)High-entropy canaries embedded in honeypot lure values (in-memory set); network URL/body (write/end), dgram messages, fetch URLs/bodies and spawn args scanned for themCanary found outbound โ†’ red canary-leak (100% exfil confirmation; direct / URL-decode / one base64-decode variants; offending plugin marked suspected in the N3 ledger)Only confirms exfiltration of honeypot material; canary sharding/reassembly not countered (documented); needs honeypot lures (idempotent lures keep their canary)
Integrity canaries (0.1.14)Small marker files under ~/.dsh (fixed content + self sha256); write/delete โ†’ red kind integrityEarliest ransomware trigger on the profile/credentials surface (backstop to N3 destruction signatures)Scope limited to ~/.dsh (documented); reads not alarmed
N7 confirmation block (0.1.14)Wrapper-level intercept of destructive fs ops after certain confirmation (families 1/2) plus optional family 3/4 upgrade-to-block; guards: official attribution / unattributed ops / vet self IO never blocked, exact file-level credential matching, fail-open decision pathFamily 1: post-confirmation (N3 ransom-signature combo / integrity-canary write-delete / N4 canary leak) destructive fs ops (write/unlink/rename/cp/truncate/createWriteStream, plus write-flag open/openSync since 0.3.4/round-22 โ€” fd-path truncation) of that plugin throw; family 2: single-shot immediate block of credential-body deletion + overwrite-to-existing (incl. write-flag open on existing credentials) (exact files: ~/.ssh/id_*, ~/.dsh/.credentials.yaml, ~/.aws/credentials, .pgpass, .netrc, .git-credentials, .npmrc); families 3/4: yellow persistence-write / install-write alarms (never blocked by default)Blocked set is process memory (restart clears); config changes need restart; recoverable writes (appendFile, new files) are never blocked; family 3/4 become blocking only via explicit user override (confirmBlockFamily3/4: block)
N6 version behavioral diff (0.1.15)Per-name@version capability manifests (N1 output) recorded locally at ~/.dsh/vet/capabilities.json (0600, LRU 1000 versions) on every auto-scan; on upgrade the new manifest is diffed against the previous recorded version (by recordedAt, no semver parsing)New capabilities vs. the previous version โ†’ yellow upgrade-diff (new hosts/fsPaths/spawnCmds/imports/network-or-exec gain); a new high-sensitivity combination (exec+network / sensitive-path+network / sensitive-path+exec) โ†’ red; cold start (first install): records only, exec+network double-high gets a yellow upgrade-cold notice; removed capabilities are audit-only, never alarmed; vet_diff tool prints local history + last-two-version behavior changelogCompares declared manifests only (runtime-hidden or dependency-carried capability changes are covered by N1 hidden-capability/N2 decode, not the manifest diff); "previous version" = last version actually scanned here; same-version reinstall not diffed (content-baseline hash covers same-version tampering); local-only (no network), alarm-only
Forensics mode (0.1.21, P0-2)After N4 canary confirmation, the confirmed-malicious plugin is armed and every subsequent fs/child_process/network op of that plugin is appended to ~/.dsh/vet/forensics/<plugin>-<ts>.jsonl (0600/0700, fail-open)Full micro-activity timeline for a confirmed-bad plugin โ€” "no disturbance at rest, full net once confirmed"In-memory armed set (cleared on restart); no session-content capture (operation-shape + target only, same data plane as N3); forensics is an enhancement, never a block path
Hook integrity heartbeat (0.1.21, P0-2 #2)Every T2 wrapper is branded with a module-closure-private Symbol; a periodic check (runtimeIntervalMsร—4, min 5s) re-verifies the current module exports still carry the brandWrapper stripped/replaced (plugin rewrote a built-in module export to bypass T2) โ†’ yellow t2:hook-heartbeat listing the lost ops; alarm-onlyBrand lives on the function object โ€” a copied toString() cannot spoof it; only surfaces vet wrapped this process; brand absent โ†’ taken as stripped (conservative); TTL-bounded alarm, recovery via re-apply
ShieldBrowser conversation.session.header.actions + /vet/status.jsonGreen/yellow/red light + alarm countRequires dsh web restart to activate

Explicitly not detected (empirically verified)

FormEmpirical result
Indirect references: alias function const f = Function; f(...), process["getBuiltinModule"], globalThis.process, indirect eval (0, eval)The alias-to-Function form (const f = Function; f(...)) remains undetected โ€” R6 info or zero findings, verdict=clean (no variable-alias tracking; R1 alias tracking covers only .constructor); round-9 (0.1.16) / F4: process["getBuiltinModule"] (bracket access) โ†’ critical, globalThis.process.* โ†’ member-graded (critical/high/info), (0, eval)/globalThis.eval/window.eval/globalThis['eval'] โ†’ R2 high โ€” all now caught
Runtime/externally constructed payloads: base64 strings, hex/charCode assembly, reading code from network/env/args, self-modifying code0.1.13 (N2): statically decodable base64/hex/charCode/constant-concat payloads are decoded and fed back to R13/R7/R11 (exfil/secret/destructive-path shapes now caught); direct Function(atob(...))/eval(atob(...)) calls are flagged by R2 regardless of arguments; the empirically-tested zero-finding floor is now only the alias/dynamic-base constructor form (x.constructor with a runtime-constructed argument) plus payloads sourced from network/env/args/self-modification; 0.1.15 (N5/R15): such network sinks are flagged info ("ๅˆปๆ„้ฎ่”ฝ" โ€” runtime target not auditable from source)
Non-source files: .jsx/.tsx/.vue/binaries/wasm, arbitrary .md/.yml, and .json outside package.jsonNot in the general scan surface; shell/PowerShell/batch scripts (.sh/.bash/.ps1/.cmd/.bat/.psm1/.zsh) are covered by R14 (download-and-exec); hardcoded download-and-exec in JS/TS exec/spawn-family arguments is covered by R20 (0.3.2); package.json itself is always parsed (R10 install hooks/dependency manifest, R12 contract, R16 dep consistency); 0.2.6 (R17/R18): root-level configs cordis.yml/cordis.patch.yml (!!js) and instruction/skill files AGENTS.md/SKILL.md gained narrow surface-gated extras; README/docs still not scanned
Dependency chain/supply chain (partial โ€” the rest is R10's actual scan surface): full import/require graph resolution, licenses, author reputation, transitive-vulnerability trees (opt-in, off by default)Not parsed beyond the checks below; scanned instead: package.json install hooks (R10, incl. prepare/preuninstall โ†’ high), dependency manifest (R10 โ†’ info), known-vulnerability matching via OSV exact-version query โ€” the plugin package itself and its direct dependencies (default-on osvCheck, configurable off; network, fail-open; exact versions only, ranges skipped), optional transitive tree via a locally installed upstream-radar CLI (transitiveDeps, default off โ€” missing CLI degrades silently to direct-only), import/node_modules consistency (R16 ghost/zombie deps โ†’ info)
Runtime behavior: network exfiltration, dynamic prototype-pollution chains, dead loops/resource exhaustion, timing, permission abuseNo dataflow/behavior analysis; static <builtin>.prototype override assignments are caught by R4 (round-7); 0.1.13 (N2): statically decodable base64/hex/charCode/constant-concat payloads are now caught (decoded corpus fed back to R13/R7/R11); runtime-constructed payloads read from network/env/args remain undetected statically โ€” the cross-layer diff (N1) surfaces them as hidden capabilities when the runtime actually executes a sensitive action the static manifest never declared ; 0.1.14 (N3): the runtime ledger correlates per-plugin sensitive reads with outbound writes (bytes + sequence + destruction signatures), partially closing low-traffic exfil and pure-JS file-destruction; cross-session/ultra-slow exfil remains out of scope (read and write not in the same plugin lifetime)
Semantic knowledge: the actual services a plugin injects, process in bundler polyfills, shadowing boundaryR5 only recognizes 4 variable names; shadowing check is a v1 heuristic (undercounts)
Legitimate process use by host tool packages (process.env config reads, process.stdin/stdout protocol, process.execPath spawn)Resolved: targetKind grading โ€” non-DSH plugin packages/official packages (generic) downgrade R3/R2/R10/R9 dead-loop to capability-surface/advice (info/medium), not into verdict; DSH plugin packages stay strict. round-7 adds shape downgrades: app-type packages (package.json declares bin) and bin entry files are likewise downgraded to capability surface; round-7.1 read-only member classification: pure read-only members (cwd/env/platform/pid, etc.) drop to info even in plugin mode (no-bin MCP/tool plugins like bridges no longer hurt); side-effect/escape members (kill/exit, etc.) stay high/critical. 187 official packages tested all clean (0.1.1-rc.2 installed set)

Trust boundaries

  1. The verdict is produced only by the deterministic static layer โ€” rules are regex/AST judgments, not spoofable by prompt injection.
  2. The static layer is physically isolated from plugin code โ€” the scanner is a separate process; the AST is read-only and never eval'd.
  3. Review goes through the agent protocol โ€” the agent follows the vet-audit-protocol skill steps (static criteria first, sensitive points dug into one by one); the verdict is unaffected by the review step.
  4. No single merged score โ€” merging the verdict with subjective assessment is forbidden, to avoid polluting the verdict boundary.
  5. This product is not a security boundary โ€” positioned as a "speed bump + forensics layer" (bypassable forms in Known Limitations 1 below, aligned with DSH's official stance).
  6. Fail-open by default โ€” default mode: report; deny is explicitly enabled by the deployer.
  7. Observation-first, interception explicit โ€” the runtime guard's T1/T2 layers only watch and never kill. Interception is limited to documented scopes: the N7 confirmation-block of irreversible destruction (default confirmBlock: block families 1/2; active whenever the runtime guard is on โ€” incl. when woken by the hardened tier or the shield toggle), and the explicitly enabled opt-in deny mode / paranoid tier (load rollback + per-threshold block). Alarms only carry advice; disposition is always left to the user on their DSH.

0.1.16 hardening batch (security review)

Fixes from the full code review (C1โ€“C4 critical, M5โ€“M9 major/minor, rule patches; ENGINE static-v12):

  • C1 โ€” host side is published as a single bundle (lib/index.bundle.js); guard internals (setRootIndexing/withVetSelfIo/confirmBlock/canaryStore/capabilityDiff) are closure-private and no longer ship as importable modules โ€” an in-process plugin can no longer disable T2/N7/N4/N1 by absolute-path require. Internal state is also no longer exported by the bundle. Companion fix: package-internal resources (AUDIT_PROTOCOL.md / scanner-bin entry / T1 sidecar / self root) are now resolved form-agnostically by src/pkg-root.ts (upward package.json search + candidate existence) โ€” the old fixed two-level .. locators broke under the bundle main and made a plain restart ENOENT on AUDIT_PROTOCOL.md.
  • C2 โ€” ESM named imports (import { readFileSync } from 'node:fs') bypass all T2 hooks by Node's interop snapshot (verified). The N1 manifest now flags this (esmNamedBuiltins) and, under runtimeGuard: watch, a yellow esm-guard-coverage notice is recorded so the coverage gap is explicit, not silent.
  • C3 โ€” DSH_PLUGIN_VET_CACHE_DIR / DSH_PLUGIN_VET_BASELINE_DIR are snapshotted at module load; in-process process.env tampering can no longer redirect stores/cache. The scanner cache verifies a host-injected random nonce on read โ€” forged clean cache entries (verified attack) are now ignored.
  • C4 โ€” Error.prepareStackTrace/stackTraceLimit tampering is detected: attribution becomes untrustworthy โ†’ red attribution-tampered alarm + N7 family-2 credential blocks still apply via a sentinel identity.
  • M5 โ€” T2 now wraps symlink/link/chmod/chown/mkdir/mkdtemp/utimes/lutimes (+Sync, write surface) and lstat/lstatSync (probe surface).
  • M6/M7/M8/M9 โ€” R9 fork-bomb covers sync spawn variants ยท capability/baseline stores self-check for external overwrite (vet-store-tamper yellow) ยท isSensitiveFsPath matches path segments instead of substrings ยท sidecar kill verifies /proc/<pid>/cmdline before SIGTERM (PID-reuse protection).
  • Rule patches โ€” R2 global/indirect eval forms + require-concat folding, R3 globalThis.process.* member policy, R4 Reflect.defineProperty, R9 escaped-paren ReDoS counting, R10 prepare hook, R14 python/ruby/perl download-exec, R15 undici sinks (see Static rule table).
  • Session-log rotation noise โ€” isSessionLogFile now also recognizes sharded session files (session.jsonl.zstd.<shard>); an unattributed session-log deletion under ~/.dsh/sessions/** is downgraded from red fs-destroy to yellow (host self-maintenance can't attack itself), while an attributed deletion stays red (possible evidence destruction).

Platform Support

The one-glance matrix โ€” what actually runs where. T1 = out-of-process sentinel (samples memory / child-process count / fd every tick and alarms); T2 = in-process hooks (interception, honeypot, GUI shield); panel = live host-metrics display (metrics.js).

CapabilityLinuxmacOS 11+Windows / other
Static scan (scan_plugin, R1โ€“R20, OSV)โœ…โœ…โœ…
T2 runtime hooks + honeypot + shieldโœ… fullโœ… fullโœ… (system-root prefix check is POSIX-shaped; segment-name/keyword checks still hit)
T1 sentinel (out-of-process resource sampling)โœ… /proc, every tickโœ… ps + lsof, fd ~every 6s (since round-19)โ›” skipped via explicit platform gate (zero noise)
Live metrics panel (metrics.js)โœ… fullโœ… async ps/lsof sampling (since round-20); disk-I/O shows โ€”โš ๏ธ V8-side numbers only (rss/heap); OS counters show โ€”/0 fallbacks (by design; since round-21 even childCount is honest โ€”, never a fake 0)

macOS floor and CI: GitHub Actions retired the older hosted macOS images (12 fully gone, 13/14 on the deprecation path; macos-latest = macOS 15 Sequoia), so only modern macOS is ever CI-tested โ€” consistent with the Node 22 floor (macOS 11+). Older macOS never crashes the guard: unparsable ps output degrades to "tick skipped", the same contract as a restricted-/proc container. Windows stays sentinel-less by design: no stock ps/lsof equivalent at comparable cost; T2 + static still guard it. Panel sampling on macOS is deliberately async snapshot-cache (TTL 4s, lsof 15s): the panel polls readHostMetrics every 5s from inside the host process, and a sync execFile there would freeze the host event loop โ€” so reads never block, the first poll shows โ€” and self-heals at the next. Disk-I/O (read_bytes/write_bytes) is Linux-only (no stock per-process byte counter on macOS/Windows): elsewhere the panel shows โ€” (โˆ’1), never a fake 0. The panel also refuses to render parseable non-snapshot JSON (SEC-6 cross-origin 403 envelope, host error envelopes): poll only replaces the live snapshot when the payload matches the wire shape (level string + alarms array), so an error envelope can never paint a fake all-green shield (round-21; shape predicate shared single-source between server and client bundle).

Known Limitations

  1. Static scanning is not a security boundary: obfuscated/encoded/dynamically generated code can bypass the AST rules; R6 only provides a "suspicious" signal. 1b. Source enumeration limits: internal/plugin auto-scan only recursively collects โ‰ค6 levels deep, non-hidden (non-dot-prefixed) .js/.ts/.mjs/.cjs files โ€” deep or hidden directories are silently unscanned (no warning); use scan_plugin(target=package) manually for a full directory scan.
  2. Agent review can be prompt-injected: the verdict never comes from the review step, but the agent may miss things โ€” the confidence field lets users know.
  3. internal/plugin guard doesn't cover runtime dynamic-mount escapes: the vm path is intercepted at the call layer by the tools/execute guard.
  4. R5 is code-only: ctx access in files scenarios isn't reported by default (high false-positive rate).
  5. Scan duration: large plugin packages may time out and skip (R8 info); agent review proceeds per the vet-audit-protocol steps.
  6. The verdict is the static layer's deterministic judgment; the agent's subjective assessment is recorded in the health record and doesn't constitute a security guarantee.
  7. /vet/status.json has no auth: the shield's polling needs anonymous GET, and the route itself isn't authenticated โ€” if dsh web binds to a non-loopback address, LAN clients can read scan conclusions/alarm targets. vet is an alarm-only observer and won't overreach into access control; if you care, keep loopback binding or trust your network (the POST guard toggle already has same-origin validation; no-Origin requests are rejected).
  8. @deepseek-ai/* is trusted by default: if the official ecosystem is ever compromised, tighten this (v1 keeps the switch). scan_plugin judges official packages as generic (capability-surface downgrade) โ€” an official-package supply-chain attack would let static downgrade mask process access (recorded, P-5; official packages are the platform itself, same policy as the internal/plugin official exemption). vet's own exemption is likewise narrowed (P-3): it now matches by name AND verifies via realpath that the target is the current vet instance โ€” a same-name impostor package (file: install has no registry validation) is judged by the strictest plugin rules.
  9. R10's known-vulnerability check depends on an OSV network query: on by default, sending "package name + exact version" to api.osv.dev (disclosed in the README config section; set osvCheck: false if you care); network failure/timeout degrades silently to skip (never false-blocks); only exact versions are queried โ€” */>=/^/~ ranges and version-less main packages are skipped (P3-1/P3-3; round-7 fix: ^/~ no longer strip their prefix to query as exact lower bounds โ€” the lower bound being affected while the actually installed version is already fixed would false-positive). Transitive-dependency vulnerability scanning is opt-in (transitiveDeps: true, default off) โ€” it shells out to a locally installed upstream-radar (never auto-installed); when it isn't installed, times out, or its output shape is unexpected it degrades silently to direct-dependency-only. Independent of OSV, R16 (0.1.21, see the Static rule table) audits the local declarationโ†”importโ†”installation consistency (ghost/zombie deps) with no network.
  10. R11 only recognizes fs.* forms: destructured/aliased calls (const { unlinkSync } = require('fs')) and runtime paths are missed (empirically recorded; a static boundary).
  11. T1/T2 are "security cameras", not "vaults": they catch obvious mischief (memory/fork bombs, sensitive-path operations, third-party spawn) but not worker threads/native plugins/low-traffic slow exfiltration; T2 doesn't cover ESM named-import snapshots, process.binding, and other side channels. Since 0.1.16 the ESM named-import blind spot is explicit: the N1 manifest flags esmNamedBuiltins and runtimeGuard: watch records a yellow esm-guard-coverage notice instead of silent loss of coverage, and attribution tampering (Error.prepareStackTrace/stackTraceLimit) emits red attribution-tampered alarms with N7 family-2 credential blocks still enforced.
  12. T2 attribution & noise reduction: stack attribution is best-effort (shared services/timers across plugins can mis-attribute); official-package spawn is not alarmed by default (capability grant).
  13. Shield activation requires a dsh web restart: client-modules scans the dsh.client declaration at startup; the browser won't load the shield before the restart, but the /vet/status.json endpoint and the runtime guard (host side) take effect on restart.
  14. Runtime guard is off by default (runtimeGuard: 'off'): wrapping fs/child_process carries performance and stability costs; opt-in.
  15. process.kill stays high (intentional, round-7.1): kill is a side-effect member and doesn't degrade with the read-only members โ€” but a plugin killing its own spawned child (MCP/bridge-style) is a normal capability surface (dsh-bridges tested: 98/134 cleared; the remaining highs are all process.kill in run.js/util.js). Statically distinguishing process.kill(child.pid) (pid from this package's own spawn return) from arbitrary pids needs dataflow analysis โ€” high cost, low benefit; kept as-is, to be ruled out manually by the agent during vet-audit-protocol review (conclusion recorded in the health record).
  16. Platform support: see the Platform Support matrix above โ€” single source of truth. One-line summary: static scan, T2 hooks and the shield run everywhere (Linux/macOS/Windows); the T1 sentinel runs on Linux (/proc) and macOS 11+ (ps/lsof, fd ~every 6s, since round-19) and is explicitly skipped on other platforms including Windows; the metrics.js host panel covers macOS since round-20 (async snapshot cache โ€” never blocks the host; disk-I/O stays Linux-only and shows โ€”, never a fake 0); on Windows only the V8-side numbers (rss/heap/external) are real.

Development

npm run build       # scanner-bin + src compiled to lib/ + client bundle
npm run typecheck   # full tsc --noEmit (scanner / src / client)
npx vitest run      # full suite: 74 files / 1095 tests
node scripts/count-assertions.mjs   # assertion census: 2864 standalone expect() calls (+18 chain-matcher helpers; lexical scan, comments/strings excluded)
npx vitest run --coverage   # coverage report (v8 over lib/; thresholds: lines/functions/statements >= 85%, branches >= 80%; measured 89.5/93.5/89.5/84.7)
npm run check:mutants       # mutation gate (34 mutants must all be killed, 8 benign controls stay clean; per-rule kill matrix enforced)
node scripts/gen-self-pin.mjs && node scripts/check-self-contract.mjs   # artifact self-pin + release-pin match
node scripts/check-pack-integrity.mjs   # shipped-file whitelist integrity

Release gate: prepublishOnly runs build + pack-integrity + self-contract + mutant gate automatically. The full pre-release chain is: build โ†’ typecheck โ†’ vitest โ†’ check:mutants โ†’ gen-self-pin โ†’ check-self-contract โ†’ check-pack-integrity (all must be green; the pin is regenerated whenever the shipped artifacts change).

Layout: scanner-bin/ static engine (separate process); src/ plugin body (tools/guards/audit/report/guard); src/client/ GUI shield; test/ fixtures + unit tests + adversarial matrix. Architecture in docs/ARCHITECTURE.md.

License

MIT.