Skip to content

Releases: LivXue/dsh-plugin-shop

v0.8.0 — audit remediation: one install identity, a self-verifying harvest, bounded artifacts

Choose a tag to compare

@LivXue LivXue released this 07 Sep 14:27

English — 中文公告见本页下半部分

The remediation release for the 2026-09-03 defect audit. Eight independent passes went
over the tree at 49db942 — the 0.7.4 release commit — and produced 86 findings
across five subsystems. v0.8.0 closes them, as five independently shippable plans. Both
halves of the repository moved: the plugin the harness loads, and the pipeline that builds
the catalog it reads.

npm dsh-plugin-shop@0.8.0latest
Previous 0.7.4 (2026-09-03)
Proven on beta tag, 0.8.0-beta.00.8.0-beta.4
Tag v0.8.0 @ 0b92017
Catalog dsh-plugin-shop-catalog + Pages /v1/ — rebuilt daily, so its half has been live since it landed
Audit 86 findings, 5 plans

Upgrade

# dsh on PATH
dsh plugin --profile web add dsh-plugin-shop@0.8.0
# or straight through npx, nothing installed
npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.8.0

Pin the version — pnpm 11 holds back very recent releases, so a bare
add dsh-plugin-shop can hand you an older build for a while. Restart dsh once
afterwards: a newly added bundle is not applied to a running process.

Upgrade if you install plugins whose bundle name is not unique (177 names in the live
catalog are claimed by more than one entry), you are on Windows, or you have ever read an
install notice and not believed it.

Breaking changes: none for users. The installStart RPC and the done reducer state
each gained a field; both are internal to the plugin and move together with it.

Highlights

  1. Two same-named plugins no longer overwrite each other. The second install used to
    replace the first in the profile manifest, silently.
  2. The install notices report evidence, not inference. The confirm diffs the profile
    manifest across the spawn; the log survives success; a failure names the cause.
  3. Windows installs monorepo subpackages instead of silently landing the repository
    root — and catalog fields no longer reach cmd.exe unvalidated.
  4. The harvest can prove it enumerated everything, 156 names before the npm search
    window would have broken every daily build.
  5. Nothing unbounded reaches a published artifact. One hostile package could produce a
    203 MB plugins.json for every reader.

The shop — dsh-plugin-shop@0.8.0

Added

  • One install identity, (source, name, repo, subdir), in src/shared/identity.ts
    used by validateInstall, install(), installed(), the incompatibility map, the pins
    file and every client lookup map.
  • name-taken refusal: an install that would replace a different plugin holding the
    same bundle name is refused, naming the repository that holds it. Updating the same
    plugin is unaffected. (#19)
  • Name-conflict badge on the card, before the click, with the holder named — and a
    card with two problems at once states both instead of hiding one. (#19)
  • Post-install loader-collision check: two differently-named bundles may declare one
    loader entry id, which makes dsh refuse the whole tree at next boot. The failure names
    the id, the package holding it, and the command that undoes it. (#19)
  • A load-time peer-range warning, once, across the five packages the shop declares
    (three harness packages at ^0.1.1-rc.2, @deepseek-ai/cordis ^4.0.1,
    @deepseek-ai/cordis-plugin-include ^1.0.6). Prereleases are included on purpose —
    the harness ships nothing but -rc.
  • The install log outlives success: done carries its log like failed, and the panel
    renders it.

Changed

  • The post-install confirm reads the profile manifest before and after the spawn and
    reports only the difference, instead of inferring a cause from a predicate that does not
    entail it. (#18, #20)
  • installedNoRestartNotice fires only when the hot mount succeeded.
  • A failure detail appends the last link of pnpm's causal chain, not just its ERR_
    code.
  • Catalog name, version, repo and tarball.url are validated against a boundary
    grammar; an unsafe operand is refused rather than quoted around.
  • The Incompatible badge sits beside the button it qualifies and carries an accessible
    name.
  • The shelf sorts once per load, through one shared collator.

Fixed

  • installed() returned two rows for one plugin, one marked outdated, whose Update
    button spawned another repository's commit — gated on that entry's tier.
  • Re-installing your own plugin failed with a false published detail:
    version-mismatch: dsh-foo@bbbb… is not the cataloged version (aaaa…).
  • A second same-named install silently overwrote the first in the profile manifest.
  • A GitHub pin written as a release tag was dropped on every read, so the 162
    release-rescued entries could never report outdated.
  • Windows: monorepo-subpackage entries installed the repository root. dsh spawns pnpm
    through cmd.exe, where the & in github:owner/repo#sha&path:subdir is a command
    separator. Verified end to end on Windows 11 / dsh 0.1.2-rc.1 / pnpm 11.25.0. Upstream
    cause reported as deepseek-ai/deepseek-harness discussion #5815.
  • Windows: the live-harness e2e could not run at all. Its gate used spawnSync('dsh')
    and npm installs the CLI as dsh.cmd with no .exe, so the only test that walks the
    real shop UI skipped itself on the one platform CI cannot see. 5/5 there now. (#24)
  • A restricted export counted as a missing module in peer checks.
  • An unreadable or half-written profile manifest turned denied, not-in-catalog and
    version-mismatch into "please retry"; undefined now means "cannot say", never
    "nothing is installed".
  • The boot catalog stash never expired; mutation outcomes were lost through an installed
    refresh; the user layer's !!js spelling was rewritten away.

Security & hardening

  • A spawned install is bounded and its process tree is killed. It had no deadline, no
    kill, and completion waited on inherited pipes — one stalled pnpm blocked the profile's
    whole queue until dsh restarted.
  • npm tarballs are capped on the wire and at inflate. A 260 KB gzip bomb from a raced
    mirror became 268 MB in memory; 32 MB of accepted gzip can demand ~33 GB.
  • Catalog reads are capped and wrapped as transport failures; bulk reads are budgeted and
    abortable; concurrent loads share one request.
  • A restart is refused while an install is running; a failed helper startup is reported.
  • The self-update check is bounded and asks the user's registry first.
  • Every name-keyed read is an own-property read — a plugin named constructor is not
    reported as holding a name, and a star entry named __proto__ cannot poison a lookup.
  • Log lines survive stream-chunk boundaries; hot-mount rows are serialised as YAML and
    their patch paths confined.

The catalog pipeline

Added

  • release-asset.ts — the release-asset rescue channel moved out of
    github-client.ts and gained the checks it lacked: the asset root is anchored, the
    inflate is bounded at every call site, a rescued entry must be the package it claims,
    and the check applies retroactively to the 162 entries already rescued. (#21, #22)
  • identity.ts — one module owning (source, name, repo, subdir) in the four shapes
    the pipeline needs.
  • http-body.ts — the one place a body cap is enforced. A guard test scans every
    module that reads a body and fails on one that bypasses it; a sibling guard scans for an
    unbounded gunzipSync/inflateSync.
  • pages-artifacts.ts — the publishable set as policy in the pure core.
  • Recorded market verdicts in registry/markets.yml, a build input like
    verified.yml; the build report names every by: llm withholding for spot-checking.
  • A report of registry rows that matched no harvested candidate, so a typo in a review
    file is visible instead of silent.
  • 5,924 backfilled GitHub first-seen dates, recovered from the 21 committed snapshots
    spanning 2026-08-18 → 2026-09-04.

Changed

  • A review pins one exact artifact. A GitHub review binds (repo, commit), an npm
    review the reviewed version and no other, a release-rescued entry repo + sha256. All
    three compare by equality.
  • Entries, lock lines and rejections sort by the whole identity, so output cannot
    depend on the order the harvest returned candidates in; shadowed subpackages are named
    by their owner/slug#subdir unit.
  • added is stamped from accepted entries, keyed by identity, so a package rejected
    for weeks and then listed no longer carries its first-harvest date.
  • The name heuristic for competing markets is now a candidate selector that decides
    nothing.
    It caught 73 live entries and 20 were innocent — 存茶指南 and 腌菜保存, an
    A-share quant plugin whose "market" is the stock market, a session-log plugin whose
    "store" is a verb.
  • The by: human market gate was reverted the same day it shipped. notAShop is the
    CLEARED list, so requiring a signature advertised the 16 shop-like names the heuristic
    had been hiding. A mechanism whose only exit is a human is a permanent no-op. The verdict
    decides; by records who judged it.
    (design §4)
  • Both build reports are published beside the catalog. A rejection's detail is
    written for the author of the package it names and had been reachable only as a zipped CI
    artifact. Upstream discussion #5867 is the demonstration: an author searched the build
    metadata for their own scope, found nothing, and concluded the harvest could not see
    them — one of their six was harvested and rejected as no-repository. (#23)
  • The stars sidecar is keyed b...
Read more