Repository navigation
Warn during pnpm scans when non-registry copies cannot be patched #935
Description
[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
A pnpm workspace has one member that depends on left-pad@1.3.0 from the registry and another that depends on a file: copy of the same package: an unpacked directory (file:../../forks/left-pad) or a tarball (file:../../forks/left-pad-1.3.0.tgz). scan --mode hosted and scan --mode vendored rewire only the registry entry left-pad@1.3.0. They report success and give no warning about the file: copy, which pnpm keeps installing from the user's source.
A lockfile-only vex (no node_modules) then attests pkg:npm/left-pad@1.3.0 as not_affected in both modes. In vendored mode, vex still attests after pnpm install, even though member b's copy is unpatched. Hosted vex after the install is honest: it declines with not_applied.
This is the pnpm side of #326 (fixed for npm locks: drop_non_registry_installs), #497 (Bun) and #921 (yarn classic). CLI_CONTRACT lists the "same lock, non-Socket source contests the reference" rule (#588) for npm and Bun only.
Impact
A VEX document (the CI / compliance artefact) says the product is not affected while the product ships an unpatched copy of exactly the patched name@version. Nothing in the scan output tells the user that copy exists.
Repro (pnpm 12.8.1, main 9c43dfc)
# Mock patch API on :18601 serving pkg:npm/left-pad@1.3.0 (batch, by-package, view, package grant,
# hosted tarball, /registry/left-pad/1.3.0), same contract as tests/e2e_redirect_pnpm_build.rs.
export SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18601 SOCKET_NPM_REGISTRY=http://127.0.0.1:18601/registry
mkdir -p proj/packages/a proj/packages/b proj/forks && cd proj
npm pack left-pad@1.3.0 && tar xzf left-pad-1.3.0.tgz && mv package forks/left-pad # unmodified upstream 1.3.0
echo '{"name":"root","version":"1.0.0","private":true}' > package.json
printf 'packages:\n - packages/*\n' > pnpm-workspace.yaml
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"file:../../forks/left-pad"}}' > packages/b/package.json
pnpm install
socket-patch scan --mode hosted --json --yes --api-url http://127.0.0.1:18601 --org test-org --api-token x
# status success, warnings: [redirect_pnpm_trust_lockfile] only, nothing about packages/b
rm -rf node_modules packages/*/node_modules
socket-patch vex --json --output vex.json --api-url http://127.0.0.1:18601 --org test-org --api-token x
# exit 0: verified pkg:npm/left-pad@1.3.0 not_affected (inline_mitigations_already_exist)
pnpm install --frozen-lockfile --store-dir "$(mktemp -d)"
head -1 packages/a/node_modules/left-pad/index.js # /* SOCKET-PATCHED */
head -1 packages/b/node_modules/left-pad/index.js # upstream header: UNPATCHEDThe lock after the scan:
packages:
left-pad@1.3.0:
resolution: {integrity: sha512-<patched>, tarball: http://127.0.0.1:18601/patch/npm/left-pad/1.3.0/<token>/<uuid>/left-pad-1.3.0.tgz}
left-pad@file:forks/left-pad:
resolution: {directory: forks/left-pad, type: directory}The tarball variant's entry carries the version explicitly, so a lock-only reader can match it:
left-pad@file:forks/left-pad-1.3.0.tgz:
resolution: {integrity: sha512-XI5M…(upstream), tarball: file:forks/left-pad-1.3.0.tgz}
version: 1.3.0In vendored mode the scan writes overrides: {left-pad@1.3.0: file:.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz}. pnpm applies it only to the registry edge, so member b keeps left-pad@file:forks/left-pad.
Expected vs actual
- Expected: CLI_CONTRACT (VEX, "Contested locks"): when another entry of the same lock resolves the wired
name@versionfrom a non-Socket source, the reference is dropped withpatched_ref_unattributable, because "the package manager installs both entries, and that copy stays unpatched". The npm row (lock table) skips "any entry npm installs from a git, URL orfile:spec, together with every ref for the samename@version". pnpm installsfile:directory and tarball deps from the user's spec in exactly the same way. The scan should also warn about the unreached copy, as Bun / vlt / vendored do for bundled copies (*_bundled_instance_skipped). - Actual: no warning from either scan. Lock-only
vexattestsnot_affectedin hosted and vendored mode. Vendoredvexstill attests after the install.
OS × version
Linux, Node 22, real pnpm installs, cold store per install, fresh copy of the working tree for the frozen install:
| pnpm (lock) | Variant | Mode | Scan warns? | Lock-only vex |
b's installed copy | vex after install |
|---|---|---|---|---|---|---|
| 8.15.9 (6.0) | dir | hosted | no | not_affected | unpatched | declines (not_applied) |
| 9.15.9 (9.0) | dir / tgz | hosted | no | not_affected | unpatched | declines |
| 10.34.5 | dir | hosted | no | not_affected | unpatched | declines |
| 11.28.3 | dir | hosted | no | not_affected | unpatched | declines |
| 12.8.1 | dir / tgz | hosted | no | not_affected | unpatched | declines |
| 9.15.9 | dir | vendored | no | not_affected | unpatched | not_affected |
| 12.8.1 | dir | vendored | no | not_affected | unpatched | not_affected |
Each cell was run at least once, and 12.8.1 dir/hosted twice. macOS and Windows weren't probed; the behaviour sits in lock parsing and isn't OS-specific.
First bad version: release 4.0.0 has no manifest-less lockfile VEX (vex there fails manifest_not_found on this checkout), so this isn't a regression of a shipped feature.
Suspect code
crates/socket-patch-core/src/vex/discover/npm.rs:602: a pnpmpackages:entry without atarball(a{directory: …}resolution) only callsresolved_elsewhere(file, pnpm_registry_key_purl(key)), andpnpm_registry_key_purlreturnsNonefor aname@file:…key. Thefile:tarball entry takes the same route at:644. Nothing like npm'sdrop_non_registry_installs(:334) / the same-lockunwiredcheck inpush_uncontested(:128) exists for pnpm.resolved_elsewherealso only contests across different locks (discover/mod.rs:593).- For a directory entry, the version isn't in the lock. The name is (
left-pad@file:…), and the directory'spackage.jsonis in the checkout. Treating any same-name non-registry entry as contesting the ref would be the conservative fix. - The hosted / vendored pnpm rewriters (
patch/redirect,vendor) skip thefile:entry silently. Run 17 of the pnpm ledger already notedredirect_pnpm_entry_not_foundwhen thefile:copy is the only instance; with a registry instance beside it, no warning is given at all.
No probe runs: the issue is in lock parsing, and the Linux evidence above is complete.
Backlog review — 2026-10-08
Priority: P1 → P2. Merged #940 fixed the false VEX half. The open scope is a missing scan warning for non-registry copies; retitle and retain that follow-up.
The title now describes the remaining scope after the partial fixes. The original report is preserved above for historical context.
Activity
mikolalysenko commentedon Oct 6, 2026
[agent] Shares root cause with #938, #939: VEX discovery has no shared same-lock rule for an unpatched copy of the wired name@version.
contest_across_locks(vex/discover/mod.rs:606) skips evidence from the ref's own lock (e.file != r.source_file), so each extractor has to write its own same-lock check. Only npm (push_uncontestedunwired) and yarn classic git /file:dir copies (#924) have one. pnpmfile:entries (here), yarn classic registry blocks (#938) and yarn berry other-name copies (#939) have none. Will be fixed together. The missing scan-side warning here is a separate pnpm rewriter gap and may be split out.Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Claiming this issue (with #938, #939; shared root cause: VEX discovery has no shared same-lock rule that lets an unpatched copy of the wired name@version contest the ref). Branch: agent/fix-same-lock-unpatched-copy-vex. Claim-ID: 2026-10-06T13:22:24Z-2c1fea
Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Draft PR: #940
Generated by Claude Code
Stop VEX attesting beside an unpatched lock copy
mikolalysenko commentedon Oct 6, 2026
[agent] #940 fixes the VEX half: a
file:directory or tarball copy of the wired name@version in the same pnpm-lock.yaml now drops the ref withpatched_ref_unattributablenaming the copy, both lock-only and through the vendored ledger (rule 11). The PR saysRefs #935rather thanFixes. The hosted / vendored pnpm scans still give no warning for that copy. That gap is in the pnpm rewriters, not discovery, so this issue stays open for it.Generated by Claude Code
Fix VEX attesting beside an unpatched same-lock copy (#935, #938, #939)…
mikolalysenko commentedon Oct 7, 2026
[agent] Re-triage on main
05ecc6ewith real pnpm 12.10.1, from the pnpm bug-hunt routine (ledger #303). The fixture is a workspace where memberadepends on registryleft-pad@1.3.0and memberbon afile:directory copy of it.vexafterscan --mode hostedattests nothing and warnspatched_ref_unattributable: "lock entryleft-pad@file:lpinstalls it from the user's file: directory "lp", which no Socket wiring reaches; that copy stays UNPATCHED and nothing is attested". After a frozen install, the registry copy is patched (.pnpm/left-pad@1.3.0),packages/b/node_modules/left-padstays upstream, andvexstill declines with the same warning.scan --mode hosted --jsonreturnssuccesswith onlyredirect_pnpm_trust_lockfile, and says nothing about the unpatchedfile:copy.So the issue stays open for the scan-side warning, as the earlier comment said.
Generated by Claude Code
pnpm bug-hunt run 30: file #1074, re-verify #935/#956/#903/#904
[-]pnpm VEX attests not_affected while a `file:` directory or `file:` tarball copy of the patched package@version in the same pnpm-lock.yaml installs unpatched, and hosted/vendored scans give no warning for that copy[/-][+]Warn during pnpm scans when non-registry copies cannot be patched[/+]mikolalysenko commentedon Oct 8, 2026
[agent] Janitor: releasing the stale claim
Claim-ID: 2026-10-06T13:22:24Z-2c1fea. Its PR #940 merged on 2026-10-07 (c5be5d18) asRefs #935, fixing only the VEX half. No follow-up PR exists and the claimer has been silent for more than 48h. The issue stays open for the pnpm scan-side warning for the unpatchedfile:copy, and any fixer can claim it.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
[agent] pnpm bug-hunt re-check on main
03b9418(after #1058). The hosted half of the remaining scope has changed, and the two modes now disagree.Layout: a workspace whose root depends on
left-pad@1.3.0from the registry and whose memberpackages/adepends onleft-pad: file:../../left-pad-1.3.0.tgz. Real pnpm 9.15.9 and 12.10.1 installs, mock patch API.redirected: 0, exit 0, and the patch lands inredirect.skipped[]asredirect_unattributable("lock entryleft-pad@file:left-pad-1.3.0.tgzinstalls it from the user's file: tarball … that copy stays UNPATCHED"). The lock is untouched, so the scan is no longer silent. Before Decide whether a hosted patch is pinned through lockfile discovery alone #1058 it pinned the registry entry without a warning. The remedy text ("reconcile the project's lockfiles and re-run") gives the user nothing concrete to do in this layout; naming thefile:dependency to drop would help.overrides: {left-pad@1.3.0: file:.socket/vendor/…}and reportssuccesswith no warning about thefile:copy. Standalonevexright afterwards is honest: it omits the patch withvendor_unwiredand warnspatched_ref_unattributable, then failsno_applicable_patches. So vendored writes wiring that its own lockfile discovery contests, which is the case Decide whether a hosted patch is pinned through lockfile discovery alone #1058 now refuses to write for hosted.The missing scan warning in this issue is therefore down to vendored mode only. Hosted now skips loudly instead.
Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Regression from the pnpm bug-hunt routine (ledger #303): the hosted half is silent again on current main (
15c2c83), and I bisected it.The layout is the one from the 2026-10-09 comment: the root depends on registry
left-pad@1.3.0, andpackages/adepends onleft-pad: file:../../left-pad-1.3.0.tgz. Real pnpm 12.11.2, Linux, mock patch API,SOCKET_PATCH_SERVER_URLset.4d06019b(Decide whether a hosted patch is pinned through lockfile discovery alone #1058):redirected: 0,redirect.skipped[]=redirect_unattributable("lock entryleft-pad@file:left-pad-1.3.0.tgzinstalls it from the user's file: tarball …"), lock untouched. That's the loud skip the earlier comment described.15c2c83(main):status: success, warnings[redirect_pnpm_trust_lockfile]only, and the registry entryleft-pad@1.3.0is pinned to the hosted tarball. Nothing is said about thefile:copy. Reproduced 2×. It also reproduces with a member-a registry / member-bfile:layout, for a directory copy and a tarball copy, on pnpm 9.15.9 and 12.11.2 (2× each).f3c6313a"Fix open npm issues (Fix open npm issues #1008)" (git bisect run; parentb17a114ais still good). That squash toucheshosted/engine.rs,vex/discover/mod.rsandvex/discover/npm.rs. The veto that stopped firing is thecontested/vetoedblock incrates/socket-patch-core/src/hosted/engine.rs(~1880–1930 on main). It looks like the in-memory rescan no longer reports the pnpmfile:copy as contesting the would-be pin, even though standalonevexstill does.Standalone lock-only
vexafter the pin is still honest: it warnspatched_ref_unattributablenaming thefile:copy and attests nothing. So the VEX half (#940) holds, and only the scan-time veto regressed. Vendored mode is unchanged (still silent, as before).Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Claiming this issue (shared root cause: the hosted scan's in-memory rescan stopped treating a same-lock pnpm
file:copy as contesting the pin after f3c6313, and vendored pnpm never warns about it). Branch: agent/fix-pnpm-file-copy-scan-veto. Claim-ID: 2026-10-13T06:21:42Z-9bf906Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] Draft PR: #1469
Generated by Claude Code
mikolalysenko commentedon Oct 13, 2026
[agent] #1469 merged and fixes the hosted half.
scan --mode hostedstill pins the registry entry beside a same-lock pnpmfile:directory or tarball copy, but now warnsredirect_copy_unpatchedand names that entry. The run's own--vexno longer attests the package. The PR saidRefs #935, so this issue stays open for the vendored half:scan --mode vendoredon pnpm still writes its override beside thefile:copy without a warning. npm and Bun vendored already emitvendor_non_registry_entry_skippedfor this case, so the pnpm vendor backend (vendor/pnpm_lock.rspreflight) needs the same, reading the copy's version the wayrecord_pnpm_file_copiesdoes. I'm releasing my claim so another run can take this.Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Re-triage from the pnpm bug-hunt routine (ledger #303) on main
887a5f7, which includes #1469 (9c523e4). The scan half is fixed.Fixture: a workspace where member
adepends on registryleft-pad@1.3.0and memberbon afile:copy of the same package, installed with real pnpm. I ranscan --mode hosted --jsonagainst a local mock of the patch API.file:directory copyfile:tarball copysuccess, registry entry pinned, warnsredirect_copy_unpatchedscan --mode hosted --vexon 12.11.2 no longer attests anything. It warnspatched_ref_unattributable,vex_claim_unwiredandvex_omitted, and writes no document.link:copy (12.11.2) pins the registry entry with noredirect_copy_unpatched, which is the expected behaviour for a user-source link.I couldn't test
git+copies because GitHub tarball downloads are blocked in the sandbox. Closing as fixed by #1469.Generated by Claude Code
mikolalysenko commentedon Oct 14, 2026
[agent] Correction: I closed this too early, so I'm reopening it. The issue covers vendored scans as well as hosted ones, and #1469 added
redirect_copy_unpatchedto the hosted arm only. The vendored half still reproduces on main887a5f7.Same fixture (a registry
left-pad@1.3.0in membera, and afile:copy in memberb), on a fresh tree, real pnpm, each cell run twice:file:copyscan --mode vendored --jsonvexsuccess, no warning (warnings: [])patched_ref_unattributable,vendor_unwired), exit 2success, no warningThe scan writes
overrides: {left-pad@1.3.0: file:.socket/vendor/npm/<uuid>/left-pad-1.3.0.tgz}, which pnpm applies only to the registry edge. VEX stays honest; only the scan-time warning is missing. Hosted mode is fixed, as shown in my previous comment.Generated by Claude Code