Skip to content

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

Latest

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 by the catalog it belongs to and serialised by a tested pure
    function, so daily data still cannot churn the plugin data hash.

Fixed

  • The harvest can prove it enumerated everything. npm search caps from at 5,000 and
    silently returns page 0 beyond it; keywords:deepseek-harness stood at 5,094 names when
    the audit ran, 156 short of the point where every daily build would throw with the
    wrong diagnosis. An over-window keyword is partitioned on keywords:a,b intersections —
    the only filtering qualifier the API honors, and it has no negation, so no covering
    partition exists
    . The harvest is safe by check: it measures what it enumerated
    against the answered total and throws on a shortfall.
  • It stops paging on the answered total, never on a short page; a response with no
    total throws; the coverage probe refuses a partial answer and retries a timeout once.
  • The GitHub half pages on its answered total and splits each topic into stars/date/size
    windows under the 1,000-result cap, throwing when a window still exceeds it.
  • A shortfall too small to be anything but registry noise is tolerated.
  • One transport failure no longer aborts the build. A dead packument among ~5,600 is a
    fetch-failed row; a null packument projects to a rejection instead of a TypeError;
    the per-repo isolation is bounded so the search path cannot end the harvest.
  • no-manifest now means the manifest was read, or a 404 answered for it. The old rule
    wrote it with one detail string for 404, 403, 451 and 503 alike, and because a failed
    repo is neither absent nor moved, diffRepoState never re-fetched it — so a blocked
    raw.githubusercontent.com wrote "No package.json at the repository root" into the
    durable record of every repository it could not reach. 1,929 records of 14,863 were
    invalidated once for re-fetch under the corrected rule.
  • Rejection reasons a plugin author can act on: repo-gone omitted its likeliest cause; a
    probed monorepo root's no-bundle detail named the wrong one; a size-refused subpackage
    went unreported; the workspace-deps rejection wrongly applied to release rescues; the
    patch target, not the scripts field, is what "prebuilt" means; a shipped patch file
    is not the modules it names; the loader-selected entry is resolved rather than the
    first scanned; npm's un-deprecate spelling and the legacy license forms are read;
    workspaces globs are anchored at both ends.
  • first-seen.yml was written every build and never committed, so about half the shelf
    was re-stamped added: today and the "stable" content hash churned daily. It is
    committed now, with the git add line held by a test.
  • Eight ways trust leaked across identities, latent only because verified.yml is still
    empty: a fork inheriting a review through a shared bundle name (83 live names are claimed
    by both a fork and an original); a latest older than reviewedVersion rendering
    verified; a repository reviewed by commit rejected as an impersonator of itself;
    an npm publish of a verified GitHub entry's name displacing it and inheriting its added
    date; a repo denial undone by publishing to npm from it; two verified packages within two
    edits rejecting each other; an unvalidated reviewedVersion; a name both reviewed and
    denied.
  • @dsheval/dsh-top100-plugin — a competing dsh plugin market — is withheld from the
    shelf. The heuristic never caught it: the name splits to
    ['@dsheval/dsh', 'top100', 'plugin'], and the dsh-prefix rule needs the second
    segment to be a store keyword exactly.
  • A publish can no longer ship a stale catalog. At 2026-09-03T16:46Z a run against a
    dist/v1 last built on 09-02 shipped it as 2026.903.6; npm moved latest onto it and
    readers were served a catalog two days old and 818 entries short until the dist-tag
    was moved back by hand. Content hashes could not tell them apart, and
    nextCatalogVersion reads the wall clock. publish-catalog.ts now refuses a build older
    than the published latest.
  • The npm token is kept off the backup mirror, an empty-string disable is honored, and when
    a backup also fails the primary's own failure is reported.

Security & hardening

  • One hostile package could put a 203 MB plugins.json in front of every reader.
    capabilities capped the count at 20 and never the item length; license, repository
    and each of 200 peer names were taken verbatim; the GitHub manifest body had no cap at
    all. Measured, not estimated, through the real toCandidate → gate → assignTier → emit
    path. Every free-text field is bounded now, in the pure layer, so an over-long value
    becomes a rejection with an author-readable reason rather than a silently nulled
    field.
  • The five identity fields and the whole per-entry payload are bounded; the second manifest
    read is capped; the reason string itself is bounded.
  • What Pages serves is policy, not whatever landed in dist/. upload-pages-artifact
    was pointed at dist/, so /v1/harvest.json (4,037,180 bytes — every candidate
    verbatim, rejected ones included, carrying unvalidated dsh.catalog values) and
    /v1/report.md (1,722,904 bytes) both answered 200 on the live site. The build now
    stages dist/pages/ from scratch and copies in exactly what pagesArtifactNames
    returns.
  • Bot-written names are JSON-escaped and a repository manifest name is validated at the
    boundary — a name containing " or \ used to break the YAML the bot writes, which
    every later build then threw on at load.
  • Report table cells neutralise control characters and bidi formatting, and escape each
    markdown cell exactly once.
  • Every GitHub, LLM and stars request has a deadline that covers the body; a gateway
    timeout is retried; advisory steps are bounded in aggregate, not only per request; the
    build job itself is bounded. Three deadlines that lied about their own wiring are now
    tested against production.
  • A malformed REPO_BACKFILL_BUDGET stops the build instead of silently harvesting
    nothing.

CI and release

Changed

  • The build job no longer holds a credential that can write to the repository. It
    processes hostile input; it runs at contents: read, and the pushes authenticate through
    a fine-grained token scoped to this repository, held only by the steps that push. Every
    secret is scoped to the step that consumes it.
  • Every action is pinned to a commit SHA, with Dependabot configured to move them and
    the action pairs it would otherwise split grouped, so a bump cannot land half a pair.
    (actions/checkout → 7.0.1, actions/setup-node → 7.0.0, pnpm/action-setup → 6.0.10,
    plus the pages-deploy and artifact-transfer groups.)
  • The catalog harvest serialises across refs rather than per ref; the catalog's own commits
    no longer re-trigger the catalog.
  • plugin.yml runs the root suite, watches the root READMEs and its own file, and pins the
    e2e harness to 0.1.2-rc.1 — the contract its selectors match.
  • The build job round-trips an artifact, where the publish job cannot.

Fixed

  • Bot pushes fetch and rebase, retry once, and report a rejection. Both scheduled runs
    on 2026-09-02 and 2026-09-03 had both pushes rejected and reported success.
  • A push is refused outright when the rebase's autostash did not reapply — the one failure
    mode that can corrupt a registry file on main.

Tests

The standard in this release is mutation, not coverage: a test earns its place by
failing under the one-line bug it claims to catch. Three of the audit's findings exist
because exactly that check was run and the suite stayed green.

  • 823 registry tests across 29 files (22 files at 0.7.4); 25 package test files (21 at
    0.7.4).
  • Seven new registry suites: identity, release-asset, pages-artifacts, repo-guards,
    workflow, strip-types, cwd-independence. The workflow suite reads
    .github/workflows/daily.yml as data, so the next forgotten git add or missing
    rebase fails the suite instead of the publish.
  • Two assertions that could not fail were replaced; three guards that survived their own
    removal were made to discriminate; a dead throw was dropped; the determinism test gained
    a repeated run and lost three partial cases that established nothing.
  • A skipped exit criterion now fails CI instead of passing quietly; each host test file
    owns one temp root and removes it (index.test.ts alone had 41 creations against 2
    removals).
  • Every registry entry point is importable without exiting its host, and under
    --experimental-strip-types.

中文 — the English announcement is in the first half of this page

dsh-plugin-shop 0.8.0 更新公告

本版是 2026-09-03 那次缺陷审计的收尾版本。八条互不相干的审计线索走完了 49db942
0.7.4 的发布提交),产出 86 条 finding,分布在五个子系统上;v0.8.0 以五份可独立发布的
计划把它们关掉。仓库的两半都动了:harness 加载的那个插件,以及构建它所读目录的那条流水线。

npm dsh-plugin-shop@0.8.0latest
上一版 0.7.4(2026-09-03)
验证渠道 beta 标签,0.8.0-beta.00.8.0-beta.4
Tag v0.8.0 @ 0b92017
目录 dsh-plugin-shop-catalog + Pages /v1/ —— 每日重建,这一半在落地时就已上线
审计 86 条 finding,5 份计划

升级

# dsh 已在 PATH 上
dsh plugin --profile web add dsh-plugin-shop@0.8.0
# 或者直接走 npx,什么都不装
npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.8.0

要显式钉版本 —— pnpm 11 会压住刚发布的版本,光写 add dsh-plugin-shop 有一段时间里拿到的
是旧构建。装完重启一次 dsh:新加的 bundle 不会应用到已经在跑的进程上。

建议升级的情形:你会安装 bundle 名字不唯一的插件(线上目录里有 177 个名字被不止一个条目
占用)、你在 Windows 上、或者你曾经读到过一条安装提示但不敢相信它。

破坏性变更:对使用者没有。 installStart 这个 RPC 与 done 的 reducer 状态各多了一个
字段,两者都是插件内部的形状,随插件一起动。

亮点

  1. 同名插件不再互相覆盖。 以前装第二个会在 profile manifest 里静默替换掉第一个。
  2. 安装提示报告证据,不再靠推断。 确认读取 spawn 前后的 manifest 之差;日志在成功后留存;
    失败会说出原因。
  3. Windows 能装 monorepo 子包,不再静默把仓库根目录装进去 —— 而且目录里的字段不再未经
    校验就走到 cmd.exe
  4. harvest 能证明自己枚举完了,距离 npm 搜索窗口压垮每日构建只剩 156 个名字。
  5. 没有无界的东西能走到已发布的产物里。 一个恶意包就能给每个读者产出 203 MB 的
    plugins.json

商店 —— dsh-plugin-shop@0.8.0

新增

  • 统一的安装身份 (source, name, repo, subdir),定义在 src/shared/identity.ts
    validateInstallinstall()installed()、不兼容表、pin 文件与 client 的每一张
    查表共用。
  • name-taken 拒绝:当一次安装会替换掉另一个占着同名 bundle 的插件时直接拒绝,
    并说出是哪个仓库占着这个名字。更新同一个插件不受影响。(#19)
  • 卡片上的重名徽标,在点击之前就给出,并点名占用者 —— 同时有两个问题的卡片会把两个都
    说出来,而不是让一个盖住另一个。(#19)
  • 装后的 loader 冲突检查:两个不同名的 bundle 可能声明同一个 loader entry id,
    那会让 dsh 在下次启动时拒绝加载整棵树。失败信息里给出 id、占用它的包,以及撤销用的命令。(#19)
  • 加载时的 peer 范围告警,只告一次,覆盖商店声明的五个包(三个 harness 包
    ^0.1.1-rc.2@deepseek-ai/cordis ^4.0.1@deepseek-ai/cordis-plugin-include
    ^1.0.6)。刻意把预发布纳入比较 —— harness 只发 -rc
  • 安装日志在成功之后仍然留着donefailed 一样带着自己的日志,面板会渲染它。

变更

  • 安装后的确认读取 spawn 前后的 profile manifest,只报告两者之差,不再从一个推不出结论
    的前提里推断原因。(#18, #20)
  • installedNoRestartNotice 只在热挂载成功时才出现。
  • 失败详情会追加 pnpm 因果链的最后一环,不再只有 ERR_ 错误码。
  • 目录里的 nameversionrepotarball.url 按边界语法校验;不安全的操作数是被拒绝,
    而不是加个引号了事。
  • Incompatible 徽标紧挨着它所限定的按钮,并带上无障碍名称。
  • 货架每次加载只排序一次,共用同一个 collator。

修复

  • installed() 会为一个插件返回两行,其中一行标成 outdated,而那一行的"更新"按钮拉起的
    是另一个仓库的 commit —— 而且按那个条目的 tier 放行。
  • 重装自己的插件会失败,报出一条已发布且为假的 detail
    version-mismatch: dsh-foo@bbbb… is not the cataloged version (aaaa…)
  • 装第二个同名插件会在 profile manifest 里静默覆盖第一个。
  • 以 release tag 形式写入的 GitHub pin 每次读取都被丢弃,那 162 个靠 release 资产救回来的
    条目因此永远报不出 outdated
  • Windows:monorepo 子包条目装成了仓库根目录。 dsh 通过 cmd.exe 拉起 pnpm,而
    github:owner/repo#sha&path:subdir 里的 & 在 cmd 里是命令分隔符。已在
    Windows 11 / dsh 0.1.2-rc.1 / pnpm 11.25.0 上端到端验证。上游成因已报到
    deepseek-ai/deepseek-harness discussion #5815。
  • Windows:真机 e2e 根本跑不起来。 它的前置判断用了 spawnSync('dsh'),而 npm 把 CLI 装成
    dsh.cmd 且没有 .exe,于是这个唯一走真实商店 UI 的测试,在 CI 唯一看不到的平台上自我跳过。
    现在那里 5/5 通过。(#24)
  • 受限(restricted)导出在 peer 检查里被算成缺模块。
  • 一个读不出来或写了一半的 profile manifest,把 deniednot-in-catalog
    version-mismatch 变成了"请重试";现在 undefined 表示"说不了",绝不表示"什么都没装"。
  • 启动时的目录缓存从不过期;mutation 的结果会在刷新已安装状态时丢失;重写用户层时把它原本的
    !!js 写法改掉了。

安全与加固

  • 拉起的安装有时限,并且会杀掉整棵进程树。 以前它既没有 deadline 也没有 kill,完成判定还要
    等继承来的管道 —— 一个卡住的 pnpm 会堵住这个 profile 的整条队列,直到 dsh 重启。
  • npm tarball 在传输层和解压时都设了上限。 一个从被抢到的镜像拿到的 260 KB gzip 炸弹在内存
    里变成 268 MB;而 32 MB 已被接收的 gzip 可以要求约 33 GB。
  • 目录读取设上限并包装成传输失败;批量读取有预算且可中止;并发加载共用同一个请求。
  • 安装还在进行中时拒绝重启;helper 启动失败会被报出来。
  • 自更新检查有时限,并优先问用户自己的 registry。
  • 所有按名字索引的读取都是 own-property 读取 —— 叫 constructor 的插件不会被报成占着某个名字,
    __proto__ 的 star 条目也污染不了查表。
  • 日志行跨流分片仍然完整;热挂载的行按 YAML 序列化,其 patch 路径被约束。

目录流水线

新增

  • release-asset.ts —— release 资产救援路径从 github-client.ts 搬出来,并补上它一直
    缺的检查:资产根目录被锚定、每一处解压调用都有上限、救回来的条目必须是它自称的那个包,
    而且这项检查追溯作用于已经救回来的 162 个条目。(#21, #22)
  • identity.ts —— 由一个模块拥有 (source, name, repo, subdir) 在流水线里需要的四种形态。
  • http-body.ts —— 唯一执行 body 上限的地方。一个守卫测试会扫过每个读 body 的模块,
    绕过它就失败;同处的姊妹守卫会扫无界的 gunzipSync/inflateSync
  • pages-artifacts.ts —— 把"可发布集合"变成纯核心里的策略。
  • 记录在案的市场判定,落在 registry/markets.yml,一个和 verified.yml 一样的构建输入;
    构建报告会点出每一条 by: llm 的撤下以便抽查。
  • 没匹配上任何 harvest 候选者的 registry 行会被报出来,于是评审文件里的拼写错误是可见的。
  • 回填 5,924 个 GitHub first-seen 日期,取自横跨 2026-08-18 → 2026-09-04 的 21 份已提交快照。

变更

  • 一条评审钉住一件确定的产物。 GitHub 评审绑 (repo, commit),npm 评审只认被评审的那个
    版本,release 救援条目绑 repo + sha256。三者都按相等比较。
  • 条目、lock 行与 rejection 都按完整身份排序,输出因此不可能依赖 harvest 返回候选者的顺序;
    被遮蔽的子包按 owner/slug#subdir 单元命名。
  • added被接受的条目上取,按身份索引,于是一个被拒数周后才上架的包,不再带着它第一次
    被 harvest 到的日期。
  • 识别竞争市场的名字启发式,现在只是不做任何决定的候选选择器。 它在线上抓到 73 个条目,
    其中 20 个是无辜的 —— 存茶指南 与 腌菜保存、一个"market"指股市的 A 股量化插件、
    一个"store"是动词的会话日志插件。
  • by: human 那道市场闸门在发出去的当天就被回滚。 notAShop已放行名单,所以要求
    人签字反而把启发式一直藏着的 16 个像商店的名字广告出去了。一个唯一出口是人的机制等于永久空转。
    判定说了算;by 只记录是谁判的。
    设计 §4
  • 两份构建报告发布在目录旁边。 一条 rejection 的 detail 是写给它点名那个包的作者看的,
    而它此前只能在某次 workflow run 的 zip 产物里拿到。上游 discussion #5867 就是实证:一位作者
    在构建元数据里搜自己的 scope,什么都没搜到,于是断定 harvest 看不见他们 —— 他们六个包里其实
    有一个 harvest 到了,并以 no-repository 被拒。(#23)
  • star 数的 sidecar 按它所属的目录索引,并由一个有测试的纯函数序列化,因此每日变动的数据依然
    churn 不了插件数据的哈希。

修复

  • harvest 能证明自己枚举完了。 npm search 把 from 截在 5,000,超过之后静默返回第 0 页;
    审计运行时 keywords:deepseek-harness 是 5,094 个名字,距离每天的构建以错误诊断抛错只差
    156 个
    。超窗的关键词按 keywords:a,b 交集分区 —— 这是 API 唯一认的过滤限定符,而且没有
    取反,因此不存在覆盖式分区。所以 harvest 的安全性来自校验:把枚举到的数量与回答的
    total 相比,少了就抛错。
  • 翻页以回答的 total 为准,绝不以短页为准;不带 total 的响应会抛错;覆盖率探针拒绝部分
    答案,探针超时重试一次。
  • GitHub 那一半也按回答的总数翻页,并把每个 topic 按 star/日期/体积切窗以绕过 1,000 条上限,
    切到最后仍然超限就抛错。
  • 小到只可能是 registry 噪声的缺口会被容忍。
  • 一次传输失败不再中断整次构建。 ~5,600 个 packument 里死掉的那个成为一行 fetch-failed
    null packument 投影成一条 rejection 而不是 TypeError;单仓隔离有了边界,搜索路径同样不能
    终结整次 harvest。
  • no-manifest 现在表示 manifest 真的读到了,或者 404 明确回答了它。 旧规则对 404、403、
    451、503 一律写它,而且用同一条 detail;又因为失败的仓库既不算消失也不算搬迁,
    diffRepoState 永远不会重新拉它 —— 于是一次被拦住的 raw.githubusercontent.com
    "No package.json at the repository root" 写进了每一个它连不上的仓库的持久记录里。
    14,863 条里的 1,929 条被一次性作废,以便在修正后的规则下重新拉取。
  • 插件作者能据以行动的拒绝理由:repo-gone 漏掉了最可能的原因;被探测的 monorepo 根目录说错了
    no-bundle 的原因;因体积被拒的子包没被报出来;workspace 依赖那条拒绝错误地套到了 release 救援
    上;patch 目标而非 scripts 字段才是"prebuilt"的含义;随包发出的 patch 文件不等于它点名
    的模块;解析的是 loader 会选中的条目而非扫到的第一个;正确读取 npm 的取消废弃写法与旧式
    license 形式;workspaces glob 两端都锚定。
  • first-seen.yml 每次构建都写,却从来没被提交,于是大约一半的货架每天被重新盖成
    added: today,所谓"稳定"的内容哈希每天都在变。现在它被提交了,那行 git add 有测试守着。
  • 信任跨身份泄漏的八条通道(之所以只是潜在,是因为 verified.yml 至今为空):fork 通过共用
    bundle 名字继承评审(线上有 83 个名字同时被 fork 和原作占用);比 reviewedVersion 更旧的
    latest 渲染成 verified;被 commit 评审过的仓库被当成自己的冒名者拒掉;把已评审 GitHub
    条目的名字发到 npm 会顶掉它并继承其 added 日期;对仓库的拒绝被作者转发 npm 化解;两个编辑
    距离两步以内的已评审包互相拒绝;reviewedVersion 未校验;同时出现在评审与拒绝名单里的名字。
  • @dsheval/dsh-top100-plugin —— 一个竞争性的 dsh 插件市场 —— 从货架上撤下。启发式一直没抓到
    它:名字切成 ['@dsheval/dsh', 'top100', 'plugin'],而 dsh 前缀规则要求第二段恰好是商店
    关键词。
  • 一次发布不可能再发出过期的目录。 2026-09-03T16:46Z,一次针对最后构建于 09-02 的
    dist/v1 的运行把它当作 2026.903.6 发了出去;npm 把 latest 移到它上面,读者拿到的是一份
    两天前、少 818 个条目的目录,直到 dist-tag 被手工移回。内容哈希分辨不出来,
    nextCatalogVersion 读的是墙上时钟。publish-catalog.ts 现在拒绝比已发布 latest 更旧的构建。
  • npm token 不再流到备用镜像,尊重空字符串这种禁用写法;当备用源也失败时,报的是主源自己
    的失败。

安全与加固

  • 一个恶意包就能把一份 203 MB 的 plugins.json 摆到每个读者面前。 capabilities 只限了
    条目数 20,从未限过单条长度;licenserepository 以及 200 个 peer 名字都是逐字照收;
    GitHub manifest 的 body 根本没有上限。这是走真实的 toCandidate → gate → assignTier → emit
    路径实测而非估算。现在每个自由文本字段都在纯逻辑层设了界,于是超长的值变成一条带作者可读
    理由
    的 rejection,而不是被悄悄置空的字段。
  • 五个身份字段与每个条目的整体载荷都设了界;第二次 manifest 读取有上限;理由字符串自身也有界。
  • Pages 上发什么是策略,不是"dist/ 里恰好落了什么"。 upload-pages-artifact 以前指向
    dist/,于是 /v1/harvest.json(4,037,180 字节 —— 每个候选者逐字保留、包括被拒的,
    还带着未经校验的 dsh.catalog 值)与 /v1/report.md(1,722,904 字节)在线上都返回 200。
    现在构建从零搭建 dist/pages/,只拷入 pagesArtifactNames 返回的东西。
  • 机器写入的名字做 JSON 转义,仓库 manifest 的名字在边界处校验 —— 一个含 "\name
    以前会破坏机器写出的 YAML,之后每次构建都在加载时抛错。
  • 报告表格单元会中和控制字符与 bidi 格式字符,且每个 markdown 单元恰好转义一次。
  • 每个 GitHub、LLM、stars 请求的 deadline 都覆盖 body;网关超时会重试;advisory 步骤按总量
    设界而不只是逐请求设界;构建作业本身也有界。三个此前对自己接线撒谎的 deadline 现在有针对生产
    接线的测试。
  • 一个格式错误的 REPO_BACKFILL_BUDGET 会让构建停下,而不是静默地什么都不 harvest。

CI 与发布

变更

  • 构建作业不再持有能写仓库的凭证。 它处理的是敌意输入;现在以 contents: read 运行,
    推送通过一个只授予本仓库的细粒度 token 完成,且只有推送的那几个步骤持有它。每一个 secret
    都收敛到消费它的那一步。
  • 每个 action 都钉到 commit SHA,并配置 Dependabot 来推进;Dependabot 本会拆开的 action
    成对项被分组,于是一次 bump 不可能只落地一半。(actions/checkout → 7.0.1、
    actions/setup-node → 7.0.0、pnpm/action-setup → 6.0.10,以及 pages-deploy 与
    artifact-transfer 两组。)
  • 目录 harvest 按 ref 之间串行,而不是每个 ref 各自串行;目录自己的提交不再重新触发目录构建。
  • plugin.yml 会跑根套件、盯住根 README 与它自己这个文件,并把 e2e 的 harness 钉在
    0.1.2-rc.1 —— 也就是它的选择器所匹配的那份契约。
  • 构建作业会 round-trip 一次产物,这是 publish 作业做不到的。

修复

  • 机器推送会先 fetch 再 rebase,重试一次,并报告被拒。 2026-09-02 和 2026-09-03 的两次定时
    运行,两个推送都被拒绝,却都报成功。
  • 当 rebase 的 autostash 没有重新应用时,推送被直接拒绝 —— 这是唯一一种能弄坏 main
    registry 文件的失败模式。

测试

这一版的标准是变异,不是覆盖率:一个测试要靠"注入它声称能抓住的那一行 bug 后会失败"来挣得
自己的位置。审计里有三条 finding,正是因为做了这个检查而套件依然全绿才被发现的。

  • 823 个 registry 测试,29 个文件(0.7.4 时 22 个);25 个 package 测试文件(0.7.4 时 21 个)。
  • 七个新的 registry 套件:identity、release-asset、pages-artifacts、repo-guards、workflow、
    strip-types、cwd-independence。其中 workflow 套件把 .github/workflows/daily.yml
    当数据读,于是下一次忘掉的 git add 或缺失的 rebase 会让套件失败,而不是让发布失败。
  • 两条不可能失败的断言被替换;三个在自己被删掉后依然通过的守卫被改成有分辨力;一处死代码
    throw 被删掉;确定性测试加了重复运行,并去掉了三个什么都证明不了的部分用例。
  • 一个被跳过的退出判据现在会让 CI 失败;每个 host 测试文件各自拥有一个临时根目录并负责删除
    (仅 index.test.ts 就有 41 次创建对 2 次删除)。
  • 每个 registry 入口点都能被导入而不会退出宿主进程,在 --experimental-strip-types 下同样如此。

0.7.4 → 0.8.0full diff · npm · live catalog · the audit