Skip to content

Give mirrored packages a README, and make os-universal builds downloadable - #158

Merged
mgoldsborough merged 3 commits into
mainfrom
feat/ingest-readme-and-platform-resolution
Jul 27, 2026
Merged

Give mirrored packages a README, and make os-universal builds downloadable#158
mgoldsborough merged 3 commits into
mainfrom
feat/ingest-readme-and-platform-resolution

Conversation

@mgoldsborough

@mgoldsborough mgoldsborough commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Mirrored packages render as empty pages. @blackduck/mcp-server and @knowtis/knowtis on staging both show no description at all, and every one of the 38 ingested versions has readme: null.

Why it was empty

The publish path fetches a README from the GitHub API at the release tag (bundles.ts); the ingest path has no equivalent and never set the column. Not a regression — it was never wired.

README, from two sources

The archive wins. inspectBundle already opens the zip and walks every entry for the manifest and the scanability histogram, so reading a root README.md costs no extra network and describes the exact bytes whose digest was verified. Root only: bundles vendor dozens of dependency READMEs, and rendering node_modules/left-pad/README.md on a package page is worse than rendering nothing.

I measured the corpus by pulling all 38 staging bundles and listing each archive: 14 ship a root README, 19 don't, 5 I couldn't resolve.

The declared repository covers the rest, read from raw.githubusercontent.com at the tag recovered from the release-asset URL, falling back to the default branch.

Deliberately not the GitHub API. It allows 60 requests/hour unauthenticated, github-verifier.ts sends no Authorization header and no GITHUB_TOKEN exists in config, and that single budget is shared with repo-stat fetches and with claim verification — a user-facing flow. A nightly pass over ~305 servers would consume it in the first minute and starve them. The raw host is a CDN and is not charged against it, so this needs no token and no new config.

Best-effort by construction: every failure returns null. A mirror with no description beats a run that abandons a server because someone's repository moved.

Separately: an artifact shape nobody can download

While measuring the corpus I found 16 of 38 packages 404ing. Most were my own error (the route needs a version), but two are real and permanent:

@fmadore/iwac-mcp-server   artifacts: win32/any, darwin/any
@rul1an/assay-mcp-server   artifacts: linux/any

arch: 'any' is a stored value, not a query conventioninferPlatform maps server-linux.mcpb (an OS token, no arch token) to linux/any. No caller can name it:

Request Result
no params resolveArtifact looks for any/any → 404
os=linux&arch=any 422, the query enum is ["x64","arm64"]
os=linux&arch=x64 no exact match → 404

So the package appears in the catalog and 404s on every download. A caller on a supported platform asks for its concrete pair, so the exact-match lookup never reaches an os/any build. (Asking for arch=any directly is not a way in either — the query schema admits only x64/arm64 and rejects the request before resolution runs. That rejection also strands the SDK on platforms outside those enums, which is a separate defect: #159.)

Fixed by widening resolveArtifact to fall back by decreasing specificity — exact, then the os-universal build, then the fully portable one. Widening the query enum instead would not have helped, since no real client asks for arch=any. It stays a 404 when the requested OS has no build at all, so a linux-only package never hands a win32 caller a linux binary.

Verification

Against real mirrored artifacts, not fixtures:

blackduck bundle README: 5626 chars — "# Black Duck MCP", vendored READMEs ignored
gearboy  bundle README: NONE
knowtis  -> tag v0.1.0 -> GitHub README: 12704 chars — "# Knowtis"

The two packages from the report resolve through a different source each.

257/257 registry tests pass, typecheck clean, biome clean. 34 new tests cover root-only matching, case, preference order, the oversize drop, tag-then-HEAD ordering, the CDN-not-API constraint, path-traversal refusal on both the slug and the ref, and each platform-resolution branch.

One fake needed updating: fakeUpstream counted every fetch as an artifact download, and those counters are how the ingest tests prove idempotence. It now routes by host so a README fetch can't be mistaken for a re-download.

Scope note

This fills the column going forward. It does not backfill staging's existing 38 rows — even --full skips them, because versionIsCurrent short-circuits on matching digests before any download, and --full only widens the since window.

That same skip makes a failed README resolution permanent, which is worth stating plainly rather than implying otherwise: a row gets whatever was resolvable at the instant of first write, and no later run revisits it. fetchOne collapses 404, 429, 5xx, timeout, and DNS failure into the same null, so a throttled response is indistinguishable from a genuine absence — and the fallback issues up to 10 sequential unauthenticated requests per README-less server, which is the shape that draws throttling. Staging can be re-ingested from empty; production has ingested nothing yet, so nothing is stuck today. The repair lever is filed as #161 rather than built here: gating versionIsCurrent on readme would re-download half the catalog nightly forever, and a bespoke full-refresh option is net-new surface for a path used twice a year.

…al builds

Every ingested version records `readme: null`, so a mirrored package renders
as an empty page. The publish path fills it from the GitHub API at the release
tag; the ingest path had no equivalent and never set the column.

Two sources, in that order:

The archive itself wins. `inspectBundle` already opens the zip and walks every
entry for the manifest and the scanability histogram, so reading a root README
costs nothing and describes the exact bytes whose digest was verified. Only the
root is considered: a bundle vendors dozens of dependency READMEs, and
rendering one of those on the package page would be worse than rendering none.
Of 33 mirrored bundles on staging, 14 ship one.

For the rest, the repository upstream declares is the only description that
exists, read from `raw.githubusercontent.com` at the tag recovered from the
release-asset URL, falling back to the default branch. Deliberately not the
GitHub API: it allows 60 requests/hour unauthenticated, the registry holds no
token, and that budget is shared with repo-stat fetches and with claim
verification. A nightly pass over the upstream catalog would consume it in the
first minute and starve a user-facing flow. Every failure returns null — a
mirror with no description is far better than a run that abandons a server
because someone's repository moved.

Separately, `resolveArtifact` matched a platform exactly, which left an entire
artifact shape unreachable. `arch: 'any'` is a stored value, not a query
convention: `server-linux.mcpb` declares an OS and no architecture and is
recorded as `linux/any`. No caller can name it — the SDK's `detectPlatform`
reports the concrete runtime and the query schema admits no `any` — so a
package whose every artifact has that shape appears in the catalog and 404s on
every download. Two of the 38 mirrored packages are in exactly that state.
Resolution now falls back by decreasing specificity: exact, then the
os-universal build, then the fully portable one. It stays a 404 when the
requested OS has no build at all, so a linux-only package never hands a win32
caller a linux binary.

Verified against real mirrored artifacts rather than fixtures: the blackduck
bundle yields its own 5,626-char README with the vendored ones ignored, and
knowtis — which ships none — resolves through the repository fallback at the
tag it was published at.
…lback

upsertVersion's update clause omitted `readme`, so Prisma left the column
untouched whenever the row already existed. Both callers reach that path
holding a README they have already paid for — ingest by opening an archive it
downloaded, the publish route by spending one of 60 hourly GitHub calls behind
an `if (!existingVersion?.readme)` guard that only fires on update — so the
omission discarded the result of every one of those spends. Same class of drift
as the empty `update: {}` this repository already fixed once.

`undefined` still means "nothing to say about this field" rather than "clear
it", so a bundle that stops shipping a README cannot blank a description an
earlier run recorded.

resolveArtifact's comment claimed callers never send `arch=any`. The SDK's
detectPlatform falls through to `any` for any platform outside {darwin, win32,
linux} × {x64, arm64}, and getBundleDownload sends both params unconditionally,
so a 32-bit ARM client sends `arch=any` and is rejected by the query enum
before resolution runs. The comment now says what is true — an exact-match
caller cannot reach an os-universal build — and names the enum as a separate
defect rather than asserting it away.

Also: the repository-slug guard checked shape but not `..`, and `../..`
satisfies the shape (both segments are legal characters). Harmless in practice,
since the host is fixed and URL normalisation makes it a 404, but githubSlug
regex-matches an upstream-controlled URL instead of parsing it, so the value
does arrive. Excluded explicitly, matching the ref guard.

`README_PATHS` gains the lowercase spelling: the in-bundle lookup compares
case-insensitively, but these are URL paths on a case-sensitive host, so
`readme.md` and `README.md` are different requests and a repository using the
former yielded nothing. Root-entry matching now tolerates the `./` prefix some
archivers write. The unused `timeoutMs` option is gone — one bound fits a
single small file, unlike the artifact download it was mirroring.
@mgoldsborough

Copy link
Copy Markdown
Contributor Author

Round 2 adjudicated. Both Criticals were real and are fixed in 23933db; three of five Suggestions taken; two deferred with evidence.

Critical 1 — README discarded on the update path. Confirmed, fixed. upsertVersion's update clause listed seven fields and not readme, so Prisma left the column untouched on every existing row. The secondary observation is also right and is the sharper half: bundles.ts:1038 guards if (!existingVersion?.readme) and then spends a GitHub API call — from the 60/hour budget this PR argues at length must be protected — and that guard only fires on the update path, so the call was 100% wasted before this PR existed.

Fixed with the conditional spread, matching the surrounding style. undefined still means "nothing to say", so a bundle that stops shipping a README can't blank a stored one — pinned by a second test.

The point about the test not being able to catch it was correct, so I verified the new one is load-bearing rather than assuming: with the fix reverted, writes the README on the update path fails and the other ten pass; restored, 11/11.

Critical 2 — the comment's claim is false. Confirmed, comment corrected. detectPlatform falls through to 'any' in both default branches and getBundleDownload sets both params unconditionally, so a 32-bit ARM client really does send arch=any. I'd written the opposite. The comment now states what's true (an exact-match caller can't reach an os-universal build) and names the enum as a separate defect instead of asserting it away; the PR body sentence resting on it is gone.

Agreed on charter: the enum widening and the SDK 422 are pre-existing and stay out of this PR. Filed as #159 — including the worse case you didn't name, that a FreeBSD/ppc64 client sends os=any&arch=any and so can't download even a fully universal bundle, which is exactly the artifact built for it.

Delete Instead — timeoutMs. Taken, removed. Speculative; the artifact download's configurable timeout bounds a transfer varying by orders of magnitude, a single small file doesn't.

Suggestions 2, 3, 5 — taken. Case-sensitivity verified live rather than reasoned about: README.md → 200, readme.md → 404, Readme.md → 404 on the same repo. readme.md added. Docstring now says "a shorter prefix of the in-bundle order". ./ prefix stripped before the root check, with a test.

Suggestion 4 — taken. /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test('../..') is indeed true, and my existing test gave false confidence because ../../etc/passwd has three segments and fails on shape. Explicit .. check added, and the test now covers ../.. specifically.

Suggestion 1 — real, deferred with measurement. Filed as #160. You flagged it as non-blocking on a 38-bundle sample; I scanned the full upstream catalog instead — 188 pages, 18,779 servers, 682 MCPB artifacts:

Shape Count
OS token, no arch token (correct os/any) 17
OS token + unrecognised arch token (wrong os/any) 1

The one instance is io.github.vernonstinebaker/webdav-mcp shipping webdav-mcp-linux-armv7. It is not exploitable: that server also ships linux-x86_64 and linux-arm64, so x64/arm64 callers exact-match and never reach the fallback, and the only caller who does land on linux/any is an actual armv7 client — for whom it is the right binary. The harmful chain needs a misinferred build and no recognised build for the requester: zero of 682.

So the root fix belongs in inferPlatform as you say, it stays out of this PR, and #160 carries the numbers so the next person doesn't re-derive them.

255/255 tests pass, typecheck and biome clean.

…h in bytes

`zip.readAsText(entry) || null` kept a whitespace-only README, because that
string is truthy. It then won the source contest in ingest — `downloads.find(
d => d.readme)` matched it — and suppressed the repository fallback, so the
package rendered an empty Markdown block. That is precisely the symptom this
change exists to remove, arriving by the preferred path.

The repository source already refused blank content. The guard was on the path
that loses; both sources now agree that whitespace is absence.

The fetch cap compared `text.length` against a byte constant. That counts
UTF-16 code units, so non-ASCII prose could exceed the stated 512 KB several
times over while the check claimed to enforce it. Now measured with
`Buffer.byteLength`, matching both the Content-Length pre-check and the byte
count the in-bundle side reads off the zip header.
@mgoldsborough

Copy link
Copy Markdown
Contributor Author

Round 3 adjudicated. All three Criticals were real. Two are fixed in 178320c; the third was a body correction, now made.

Critical 1 — a failed README resolution is permanent, and the body claimed otherwise. Confirmed, body corrected, lever filed as #161.

Verified the mechanism rather than the summary: IngestOptions (ingest.ts:88-126) has no full field at all — since is the only window control — so --full cannot bypass versionIsCurrent, and deleteMirror is reachable only from the upstream-removal path. There is no operator lever.

The body sentence was overclaiming. "Production has ingested nothing yet, so every row it ever writes gets a README from the start" is only true if resolution never fails; what a row actually gets is whatever was resolvable at the instant of first write, permanently. The 10-sequential-unauthenticated-requests point is the sharp end and is right — I raised that fanout from 8 to 10 in round 2 by adding readme.md, which makes throttling more likely, not less, and a throttled response is indistinguishable from a genuine absence here.

Agreed on not fixing it inline, and for the reasons given — I reached the same two dead ends. #161 records both rejected remedies so they aren't re-proposed, and suggests the shape that avoids both: a targeted repair over readme IS NULL + githubRepo that runs only the fetch, with no bundle download and no versionIsCurrent change. I added a counters suggestion so a throttled backfill is visible at the time rather than discovered later as blank pages.

Critical 2 — blank in-bundle README suppresses the fallback. Confirmed, fixed.

Reproduced before fixing: inspectBundle on a constructed bundle returned " \n\n ", truthy, which downloads.find(d => d.readme) matches. The asymmetry is the real finding — round 2 added text.trim() to the repository source and pinned it with a test, and left the preferred source without it. The guard was on the path that loses.

Fixed with the one-line form. Mutation-checked: reverting it reds the new test and nothing else. (My first attempt at that check silently failed to apply the mutation and reported a false pass — re-ran it properly rather than trusting the green.)

Critical 3 — the sentence is still in the body. Confirmed; my round-2 comment was wrong.

I corrected the code comment at bundles.ts and then wrote that "the PR body sentence resting on it is gone." I never edited the body. That claim was false when I made it. The sentence is now actually removed and replaced with what's true, pointing at #159.

Suggestion — byte cap. Taken. text.length counts UTF-16 code units, so the effective bound was several times the stated 512 KB for non-ASCII prose. Switched to Buffer.byteLength, which also makes it consistent with the Content-Length pre-check and with the byte count the in-bundle side reads off the zip header. Rather than renaming the constant to match the weaker check, the check now matches the constant. New test asserts the gap directly: 200k multibyte characters, under the cap by .length, over it in bytes, rejected.

257/257 tests, typecheck and biome clean. Follow-ups now #159, #160, #161.

@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Jul 27, 2026
@mgoldsborough
mgoldsborough merged commit 6aeadca into main Jul 27, 2026
2 checks passed
@mgoldsborough
mgoldsborough deleted the feat/ingest-readme-and-platform-resolution branch July 27, 2026 23:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant