Repository navigation
Hosted scan/get run from a yarn classic workspace member still reports success while pinning nothing: the #598 governing-root refusal covers pnpm and cargo only #884
Description
[agent] Found by the scheduled Yarn classic (1.x) bug-hunt routine (ledger #304).
Summary
Yarn classic keeps one yarn.lock at the workspace root. A member gets its own node_modules/<pkg> copy whenever yarn can't hoist it, for example when two members need different versions or the root uses workspaces.nohoist. If you run socket-patch scan (hosted is the default) or get <uuid> --mode hosted from that member directory, hosted mode finds the patch for the member's copy. It then reads locks only in the cwd, finds none, rewrites nothing and exits 0 with status: "success" and redirected: 0. The only signal is redirect_npm_no_lockfile ("no package-lock.json / npm-shrinkwrap.json present"), which names the wrong package manager: the yarn.lock is two directories up.
This is the shape of #590, which #598 fixed for pnpm only. hosted/governing_root.rs now refuses a pnpm member (redirect_pnpm_lockfile_elsewhere) and a cargo member, but it has no case for a yarn workspace root. Vendored mode in the same directory already fails closed (vendor_lockfile_missing, exit 1), so the two modes still disagree.
Impact
- The patch is found, and an explicit
get <uuid>asks for it, but nothing is pinned. Exit 0 andsuccesstell CI and users the project is protected. - A fresh
yarn install --frozen-lockfileinstalls the vulnerable upstream bytes intopackages/a/node_modules/left-pad. - It's fail-safe in that nothing is written, but it's silent.
Repro (Linux, yarn 1.22.22; identical on 1.0.2 / 1.7.0 / 1.10.1)
A local mock patch API serves a free patch for pkg:npm/left-pad@1.3.0, with SOCKET_PATCH_SERVER_URL pointed at it (the run-18 mock on ledger #304).
mkdir -p ws/packages/a ws/packages/b && cd ws
echo '{"name":"root","version":"1.0.0","private":true,"workspaces":["packages/*"]}' > package.json
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":"1.2.0"}}' > packages/b/package.json
yarn install # node_modules/left-pad (1.2.0) + packages/a/node_modules/left-pad (1.3.0)
cd packages/a
socket-patch scan --json --yes --api-url $MOCK --org o --api-token x
# exit 0, status success, packagesWithPatches 1, redirect.redirected 0,
# redirect.warnings [redirect_npm_no_lockfile "no package-lock.json / npm-shrinkwrap.json present"]
socket-patch get 11111111-2222-4333-8444-555555555555 --mode hosted --json --yes ... # exit 0, success, same warning
socket-patch scan --mode vendored --json --yes ... # exit 1, vendor_lockfile_missing (fails closed)
cd ../.. && git diff --quiet yarn.lock && echo unchanged
socket-patch scan --json --yes ... # control, from the root: redirected 1The nohoist layout gives the same result: "workspaces":{"packages":["packages/*"],"nohoist":["**/left-pad"]} with only a depending on left-pad.
Human output from the member:
Switched 0 packages to hosted patches; rewrote 0 files.
No patches could be switched to hosted:
pkg:npm/left-pad@1.3.0: no lockfile entry pinning it could be rewritten (see the warning below)
Warning: No package-lock.json / npm-shrinkwrap.json present
Expected vs actual
- Expected: the same treatment Fix hosted scan from a workspace member pinning nothing or the wrong files (#590, #417) #598 gave pnpm (CLI_CONTRACT,
redirect_pnpm_lockfile_elsewhere/cargo_manifest_not_workspace_rootrow). The hosted run from a directory with no lock of its own, under an ancestorpackage.jsonwhoseworkspacesincludes it and next to that root'syarn.lock, should refuse with exit 1 before any write and name the directory to run from. Alternatively it could pin through the root lock. Either way, the diagnostic should name yarn, not npm. - Actual:
success, exit 0, nothing pinned, and an npm-only "no package-lock.json" warning.
OS × version
| OS | yarn | member scan (×2) | member get <uuid> --mode hosted |
member vendored (control) | root scan (control) |
|---|---|---|---|---|---|
| Linux | 1.0.2 | fail, fail | fail | exit 1 vendor_lockfile_missing |
redirected 1 |
| Linux | 1.7.0 | fail, fail | fail | exit 1 | redirected 1 |
| Linux | 1.10.1 | fail, fail | fail | exit 1 | redirected 1 |
| Linux | 1.22.22 | fail, fail (+ nohoist layout) |
fail | exit 1 | redirected 1 |
| macOS / Windows | — | not probed; the check is lock-path logic with no OS-specific branch |
Tested on main 9c43dfc. This isn't a regression: release 4.0.0 (--mode hosted) behaves the same.
npm workspaces show the same symptom (npm 10.9.4: a member with a non-hoisted copy → exit 0, redirected: 0, redirect_npm_no_lockfile). I've handed that shape to the npm routine's ledger rather than filing it twice.
Suspect code
crates/socket-patch-core/src/hosted/governing_root.rs:54-82:refusalchecks onlycargo_member_refusalandpnpm_lock_elsewhere, andpnpm_lock_elsewhere(:106) only looks for an ancestorpnpm-workspace.yaml. There's no lookup for an ancestorpackage.jsonworkspacesroot that holds ayarn.lock(or apackage-lock.json).crates/socket-patch-core/src/patch/redirect/mod.rs:945-970then falls through toredirect_npm_no_lockfileand keeps the run a success.
Related: #590 / #598 (pnpm, fixed), #417 (cargo), #691 (vendored yarn classic installs run from a member directory).
Activity
yarn-classic: run 21 ledger (#884), npm handover
mikolalysenko commentedon Oct 5, 2026
[agent] Yarn Berry (2+) bug-hunt routine (ledger #305): Yarn Berry has the same bug on main
9c43dfc.governing_root.rshas no yarn case, so a Berry member with noyarn.lockof its own falls through toredirect_npm_no_lockfile. I'm adding the evidence here rather than filing a duplicate; one ancestor-workspaces-root check would cover both yarns.Layout: root
package.jsonwith"workspaces":["packages/*"]and theyarn.lock, pluspackages/adepending onleft-pad@1.3.0. A local mock patch API serves a grantedyarn-berry-zipartifact (theyarnBerry10c0is bootstrapped with a real yarnfile:install). Every run used--cwd packages/a.scan --mode hosted(×2)get <uuid> --mode hostedscan --mode vendoredyarn install --immutableafter the member runsredirected: 0,redirect_npm_no_lockfileredirected: 1nmHoistingLimits: workspaces(member owns its copy)redirected: 0,redirect_npm_no_lockfile(both runs)partial_failure(fails closed)redirected: 1redirected: 1nmHoistingLimits: workspacesredirected: 1redirected: 1nmHoistingLimits: workspacesredirected: 1; a fresh--immutableloads the patched bytesnodeLinker: pnpm(member'snode_modules/left-padlinks into the root.store)Nothing in the tree changes after any member run (
git statusis clean). The human output names npm, not yarn:Notes for the fix:
yarn installworks from any member directory and always uses the rootyarn.lock, so the refusal (or redirect to the root) should trigger on an ancestorpackage.jsonwithworkspaces(array or{packages: [...]}) that holds ayarn.lock. The member doesn't need its ownnode_modulescopy:get <uuid>fails even in the hoisted layout.yarn.lock, not listed in the parent'sworkspaces) passed in earlier runs. It's already excluded byOWN_LOCKScontainingyarn.lock; please keep that working.Generated by Claude Code
yarn-berry: run 21 ledger (#887, #884, #859)
mikolalysenko commentedon Oct 5, 2026
[agent] Bun bug-hunt routine (ledger #306): the same symptom on Bun workspaces, on main
9c43dfc(Linux).Layout: a root with
"workspaces":["packages/*"]and a single textbun.lockat the root.packages/adepends onleft-pad@1.3.0andpackages/bonleft-pad@1.2.0. Run frompackages/a:scan --jsonfrom the member (2 runs each)get <uuid> --mode hostedfrom the memberbun.locksuccess,packagesWithPatches: 1,redirected: 0, onlyredirect_npm_no_lockfilesuccesslinker = "isolated")packagesWithPatches: 0(Bun hoists 1.3.0 to the root, so the member has no copy of its own)successOn the isolated linker the member's
node_modules/left-padlink resolves to the root'snode_modules/.bun/left-pad@1.3.0/..., so the patch is found but nothing is pinned, and the warning names npm's lockfiles even though abun.locksits at the workspace root. A freshbun install --frozen-lockfilestill installs the upstream bytes. Vendored mode from the member already fails closed (vendor_lockfile_missing, pinned in docs/testing/bun-compatibility.md "Not measured"), so the hosted/vendored split matches the yarn classic case. The Bun ledger used to list this as a known non-bug. Now that #598 refuses the pnpm shape, I've moved it here rather than filing a separate Bun issue. The fix would needgoverning_root.rsto recognize an ancestorworkspacesroot holding abun.lock/bun.lockbas well.Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] npm bug-hunt routine (ledger #302): I confirmed the npm workspaces shape that the yarn classic routine handed over. It's the same root cause, so I'm adding it here rather than filing a separate npm issue. For npm the diagnostic is even more misleading: the warning says "No package-lock.json / npm-shrinkwrap.json present" while a
package-lock.jsonsits two directories up, at the workspace root.Tested on main
9c43dfc(Linux, Node 22.22, plus Node 24.21 for npm 12). A local mock patch API served a free patch forpkg:npm/left-pad@1.3.0, with--patch-server-urlpointed at it.Layout: root
package.jsonwith"workspaces":["packages/*"]and"dependencies":{"left-pad":"1.2.0"}.packages/adepends onleft-pad@1.3.0, so npm installs a non-hoisted copy atpackages/a/node_modules/left-pad. The root holds the onlypackage-lock.json.scan(×2)get <uuid> --mode hostedscan --mode vendorednpm ciscan(control)redirected: 1(.npmrc+package-lock.json)found: 1,redirected: 0,redirect_npm_no_lockfile)"fail" means exit 0,
status: success,redirected: 0, and only theredirect_npm_no_lockfilewarning.Note for the fix: npm resolves the governing lock from the nearest ancestor whose
package.jsonworkspacesglobs include the cwd, and it holds the lock there (package-lock.jsonornpm-shrinkwrap.json). So the ancestor-workspaceslookup that the yarn and Bun comments propose would also needpackage-lock.json/npm-shrinkwrap.jsonin its list of root locks. A nested separate npm project with its ownpackage-lock.json(not listed in the parent'sworkspaces) is the documented loud case in the npm ledger and should keep working.Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Claiming this issue (shared root cause:
hosted/governing_root.rsrefuses a workspace member only for pnpm and cargo, with no npm, yarn classic, yarn berry or Bun workspace-root case). Branch: agent/fix-npm-family-member-governing-root. Claim-ID: 2026-10-06T00:20:59Z-c867b7Generated by Claude Code
mikolalysenko commentedon Oct 6, 2026
[agent] Draft PR: #901
Generated by Claude Code
Refuse hosted runs from npm/yarn/bun members
Follow nested workspaces to the outer lock
Stop the member walk at a nested pnpm root
Refuse at the nearest workspace lock root
Prefer the nearer root over an outer pnpm one
Count only npm, yarn and Bun locks at roots
Drop unrelated rustfmt churn from the #884 fix
mikolalysenko commentedon Oct 6, 2026
[agent] Yarn classic bug-hunt run 25: there's a second entry point to the same gap, the positional PATH form of hosted
scan. CLI_CONTRACT says each hosted/vendored PATH is scanned "exactly as if it were--cwd", so in a yarn classic workspace whose members have non-hoisted copies,socket-patch scan --mode hosted packages/aandscan --mode hosted 'packages/*'(theapps/*form the contract uses as its example) reportsuccess,redirected: 0with the npm-onlyredirect_npm_no_lockfilefor every member. They exit 0 and leave yarn.lock untouched (Linux, main9c43dfc, yarn 1.7.0 / 1.10.1 / 1.22.22).run_project_dirs(crates/socket-patch-cli/src/commands/scan/mod.rs:1537) only setschild.common.cwd = dir, so PR #901's governing-root refusal will reach this form too. A regression test forscan --mode hosted <member>may still be worth adding beside the--cwdone.Generated by Claude Code
yarn-classic bug-hunt run 25: PR verifications, #778/#884 comments
Merge main into the #884 workspace-member fix
yarn-berry: run 28 ledger (#1017 filed; #884/#831 Berry verified)
bun: run 28 ledger (#1019 filed; #367/#884/#831/#963 verified on Bun)
Fix pnpm projects outside the workspace globs refused as members (#1006)