Skip to content

Hosted yarn berry pin of a catalog: dependency keys resolutions by the resolved npm: range, so every yarn install --immutable fails YN0028 (regression from #465) #632

Description

[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).

Summary

Since #465 (203e092), a hosted Berry pin is a root package.json resolutions entry keyed by the locked descriptor (left-pad@npm:^1.3.0), plus the lock entry re-keyed left-pad@<url>. When the dependency is declared through a Yarn catalog ("left-pad": "catalog:", with catalog: {left-pad: ^1.3.0} in .yarnrc.yml), yarn applies resolutions to the manifest descriptor left-pad@catalog: before the catalog is expanded to npm:^1.3.0. So the left-pad@npm:^1.3.0 selector never matches. Yarn resolves the registry release again, and the lock socket-patch wrote no longer matches what yarn computes.

scan --mode hosted exits 0 with status: success, redirected: 1, and no warning. Then:

  • yarn install --immutable (in place or in a fresh checkout) fails with YN0028 ("The lockfile would have been modified"). Yarn wants to turn left-pad@<url> back into left-pad@npm:1.3.0.
  • A mutable yarn install silently installs the unpatched registry bytes and drops the pin from yarn.lock. The stale resolutions entry stays in package.json.
  • Re-running scan --mode hosted reports redirected: 1 again and writes the same broken pin, so the loop never converges.

Release 4.0.0, with its ::__archiveUrl= lock-only pin, handles the same project correctly: the fresh --immutable install gets the patched bytes. This is a regression.

vex does the right thing after the mutable install. The lock no longer carries a pin, so it attests nothing (manifest_not_found). There's no false attestation.

Impact

  • Any Yarn ≥ 4.10 project that uses catalogs, which are increasingly the norm in monorepos, can't use hosted mode. CI (enableImmutableInstalls is on by default under CI) goes red immediately after the hosted commit.
  • Local devs running plain yarn install silently get unpatched code. The command reports success throughout.

Repro (Linux, yarn 4.18.1, node-modules linker)

mkdir c && cd c
echo '{"name":"c","private":true,"dependencies":{"left-pad":"catalog:"}}' > package.json
printf 'nodeLinker: node-modules\nenableGlobalCache: false\ncatalog:\n  left-pad: ^1.3.0\n' > .yarnrc.yml
yarn install
socket-patch scan --mode hosted --json --yes --api-url <mock> --org org --api-token x --patch-server-url <mock>
#  -> status success, redirected 1, warnings []
#  package.json gains: "resolutions": {"left-pad@npm:^1.3.0": "<mock>/patch/npm/left-pad/1.3.0/tok/<uuid>/left-pad-1.3.0.tgz"}
yarn install --immutable
#  ➤ YN0028: -"left-pad@<url>":
#  ➤ YN0028: +"left-pad@npm:^1.3.0":
#  ➤ YN0028: The lockfile would have been modified by this install, which is explicitly forbidden.
yarn install && head -c 12 node_modules/left-pad/index.js   # unpatched registry bytes

The patch API was a local mock serving a granted yarn-berry-zip artifact whose yarnBerry10c0 was bootstrapped with a real yarn resolutions: file: install, the same approach as crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs.

Which selector yarn honours for a catalog dependency (mutable install with each hand-written resolutions key → URL):

resolutions key installed bytes
left-pad@npm:^1.3.0 (what socket-patch writes) unpatched, lock entry left-pad@npm:^1.3.0
left-pad@catalog: patched, lock entry left-pad@<url>
left-pad (bare) patched, lock entry left-pad@<url>

A workspace mixing catalog: (root and member b) with a named catalog:legacy (member a, 1.3.0) fails the same way. socket-patch writes left-pad@npm:1.3.0 and left-pad@npm:^1.3.0, and yarn wants a merged "left-pad@npm:1.3.0, left-pad@npm:^1.3.0" registry entry.

Expected vs actual

  • Expected: docs/ecosystems.md (yarn berry hosted notes) says the redirect "pins the way yarn does for a root resolutions entry … package.json routes the locked descriptor to the hosted tarball", and CLI_CONTRACT.md says a dep counts as redirected only when the pin actually lands. The pin should be keyed by the descriptor yarn matches resolutions against (name@catalog: / name@catalog:<named> for catalog deps). Failing that, a catalog-consumed descriptor should be refused loudly, the way pnpm vendoring refuses catalog-consumed deps (fix(vendor): refuse catalog-consumed deps in pnpm vendoring #184), rather than reported as success.
  • Actual: exit 0, redirected: 1, no warning. The next immutable install fails YN0028, and a mutable install is silently unpatched.

Matrix

OS yarn socket-patch Result
Linux 4.18.1 main 045d7ec (3 runs: minimal root, workspace + named catalog, re-scan loop) fail
Linux 4.12.0 main 045d7ec fail
Linux 4.18.1 release 4.0.0 (npm) pass (fresh --immutable patched; ::__archiveUrl= pin)
Linux < 4.10 — n/a (no catalogs)
macOS / Windows — — untested; the selector choice is OS-independent. No probe branches this run: the sandbox git proxy still drops branch deletes.

First bad commit: 203e092 (#465, "Fix yarn berry hosted pin leaking npm auth (#404)"), which introduced the resolutions pin. Run 8 of this routine passed the same catalog: cell with the old __archiveUrl pin.

Vendored mode isn't affected: it passed catalog: and catalog:legacy in run 8 because it pins by package name.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:4081, in berry_resolutions_pin: selectors are built as format!("{name}@{r}") from the lock entry's npm ranges (key_ranges). For a catalog-consumed dependency the manifest descriptor is catalog: / catalog:<name>, which is what yarn's resolutions matcher sees.
  • No catalog handling exists anywhere in patch/redirect/mod.rs, so the hosted gate has no catalog refusal either.

Activity

  1. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] More evidence from the same run (main 045d7ec, yarn 4.18.1, Linux): the vendored → hosted takeover of a catalog: dependency also falls into this.

    1. scan --mode vendored on the catalog project writes "resolutions": {"left-pad": "file:./.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz"}, and a fresh yarn install --immutable --check-cache gets the patched bytes. Vendored mode works.
    2. scan --mode hosted on that project reports success, redirected: 1, and warns redirect_takeover_reverted_vendored. It reverts the working vendored pin and writes the left-pad@npm:^1.3.0 selector.
    3. The next yarn install --immutable fails with YN0028.

    So the takeover replaces a working patch with a broken one and reports success.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 3, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Yarn Berry). This is a regression from #465. Not a duplicate, and no open PR covers it.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 4, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the hosted yarn berry resolutions pin is keyed by the lock's expanded npm: range, but yarn matches resolutions against the manifest descriptor, which is catalog: / catalog:<name> for catalog dependencies). Branch: agent/fix-yarn-berry-catalog-resolutions. Claim-ID: 2026-10-04T07:21:11Z-1b6a31


    Generated by Claude Code

  4. mikolalysenko commented on Oct 4, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft fix PR: #763


    Generated by Claude Code

  5. added a commit that references this issue on Oct 4, 2026
    7a2287e
  6. mikolalysenko commented on Oct 4, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Verification of draft PR #763 (head 55dc0cb) from the Yarn Berry bug-hunt routine (ledger #305). Yarn 4.18.1, node-modules linker, Linux, with a local patch-API mock (plus /upstream/npm/<uuid>.json and a registry passthrough for rollback).

    catalog shape hosted pin fresh --immutable hardened fresh re-scan rollback
    root catalog: left-pad@npm:^1.3.0 + left-pad@catalog: patched patched no-op (0 files) byte-exact
    root catalog:legacy left-pad@npm:1.3.0 + left-pad@catalog:legacy patched patched no-op byte-exact
    two workspaces, catalog: + catalog:legacy (merged lock entry) 4 selectors patched patched no-op byte-exact
    workspace catalog: + workspace ^1.3.0 2 selectors patched patched no-op byte-exact
    devDependencies + peerDependencies catalog: 2 selectors patched patched no-op byte-exact
    vendored → hosted takeover (catalog: and catalog:legacy) redirect_takeover_reverted_vendored, then pinned patched patched — —

    On main 045d7ec, the same root catalog: project still fails YN0028 on a fresh --immutable. So, from the Berry side, #763 fixes every catalog shape tried here.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions