[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
In a pnpm workspace with the default isolated linker, scan --mode agent <PATH> can't scope to a workspace member. scan --mode agent packages/a (also packages/a/** and packages/a/node_modules/left-pad) reports No installed packages found under the given path: packages/a., scans 0 packages and exits 0, even though packages/a depends on left-pad@1.3.0 and a patch for it exists. An unscoped scan, or scan --mode agent node_modules/.pnpm, finds and patches it.
rollback packages/a uses the same pattern and does select left-pad. Its results list both copies: ./node_modules/.pnpm/left-pad@1.3.0/node_modules/left-pad and ./packages/a/node_modules/left-pad. So the two commands that CLI_CONTRACT says share glob semantics disagree on the most common monorepo scope.
The cause looks like this. Every pnpm workspace member's node_modules/<dep> is a symlink into the root node_modules/.pnpm. The npm crawler's seen name@version dedup records each package once, at the first path it meets, and the root .pnpm store is walked before the member trees. Scan's path filter then sees only the .pnpm copy. Rollback's find_all_packages_for_rollback returns every copy, including the member link.
Impact
- The CLI_CONTRACT example
scan packages/foo (and apps/**) never selects anything in a pnpm workspace. CI that patches per member with socket-patch scan --mode agent --json packages/<name> gets status: success, scannedPackages: 0, exit 0, and the member's vulnerable dependency stays unpatched with no warning.
- Patch and undo aren't symmetric:
rollback packages/a reverts packages that scan packages/a could never have applied.
- A single-package pnpm project isn't affected: the root
node_modules/left-pad link is recorded first, so scan node_modules/left-pad matches. Neither is node-linker=hoisted (members have no node_modules).
Repro (pnpm 12.8.1, Linux, main 045d7ec)
mkdir -p ws/packages/a ws/packages/b && cd ws
echo '{"name":"root","version":"1.0.0","private":true}' > package.json
printf "packages: ['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":{"is-number":"7.0.0"}}' > packages/b/package.json
pnpm install
ls -l packages/a/node_modules/ # left-pad -> ../../../node_modules/.pnpm/left-pad@1.3.0/node_modules/left-pad
# patch API serving a patch for pkg:npm/left-pad@1.3.0 (local mock used here)
socket-patch scan --mode agent --dry-run --json packages/a # scannedPackages 0, packagesWithPatches 0
socket-patch scan --mode agent --yes packages/a # "No installed packages found under the given path: packages/a." exit 0
socket-patch scan --mode agent --dry-run --json node_modules/.pnpm # scannedPackages 2, packagesWithPatches 2
socket-patch scan --mode agent --dry-run --json # scannedPackages 2, packagesWithPatches 2
# rollback with the same pattern (after an unscoped apply of a local manifest + blobs)
socket-patch apply --offline
socket-patch rollback --offline --dry-run --json packages/a
# "manifest": {"removedEntries": ["pkg:npm/left-pad@1.3.0"]}
# results[].path: ./node_modules/.pnpm/left-pad@1.3.0/node_modules/left-pad, ./packages/a/node_modules/left-pad
Expected vs actual
CLI_CONTRACT.md, "Path-scoped scans": in agent mode, "a package is in scope iff ANY of its crawled installed copies sits under a matching path", "a pattern matching any ancestor directory of the copy path also matches, so a bare scan packages/foo scopes the whole subtree". It also says the glob semantics are "shared with rollback's path targets".
- Expected:
scan --mode agent packages/a selects left-pad@1.3.0 (its installed copy for member a is packages/a/node_modules/left-pad), just as rollback packages/a does.
- Actual: scan sees only the deduplicated
.pnpm path. The scope is empty and the run exits 0 with nothing patched.
OS × version
| OS |
pnpm |
scan packages/a (and /**, and the link path) |
rollback packages/a |
| Linux |
8.15.9 (lock 6.0) |
0 packages, exit 0 |
selects |
| Linux |
9.15.9 |
0 packages, exit 0 |
selects |
| Linux |
10.34.5 |
0 packages, exit 0 |
selects |
| Linux |
12.8.1 |
0 packages, exit 0 (reproduced 2×) |
selects (both copies listed) |
| Linux |
10.34.5 node-linker=hoisted |
n/a: there's no member node_modules |
n/a |
| Linux |
12.8.1 single-package project, scan node_modules/left-pad |
pass (1 package) |
selects |
macOS and Windows weren't tested, but the crawl order and dedup aren't OS-specific.
First bad version
This isn't a regression. Release 4.0.0 has no scan [PATHS]; path-scoped scans arrived with v5 on main.
Suspect code
crates/socket-patch-cli/src/commands/scan/mod.rs:1750: the scope filter tests pkg.path of the crawled records, which hold one path per name@version.
crates/socket-patch-core/src/crawlers/npm_crawler.rs:1811: the seen name@version dedup. The root .pnpm store wins before the workspace member links are visited.
crates/socket-patch-cli/src/commands/rollback.rs:1374: rollback matches against find_all_packages_for_rollback, which returns every copy, including member links. That's why the two commands diverge.
Other symlinked layouts (yarn's pnpm linker, Bun's isolated linker, vlt, npm install-strategy=linked) probably behave the same way. Each sibling routine can confirm its own.
[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
In a pnpm workspace with the default isolated linker,
scan --mode agent <PATH>can't scope to a workspace member.scan --mode agent packages/a(alsopackages/a/**andpackages/a/node_modules/left-pad) reportsNo installed packages found under the given path: packages/a., scans 0 packages and exits 0, even thoughpackages/adepends onleft-pad@1.3.0and a patch for it exists. An unscoped scan, orscan --mode agent node_modules/.pnpm, finds and patches it.rollback packages/auses the same pattern and does selectleft-pad. Its results list both copies:./node_modules/.pnpm/left-pad@1.3.0/node_modules/left-padand./packages/a/node_modules/left-pad. So the two commands that CLI_CONTRACT says share glob semantics disagree on the most common monorepo scope.The cause looks like this. Every pnpm workspace member's
node_modules/<dep>is a symlink into the rootnode_modules/.pnpm. The npm crawler'sseenname@version dedup records each package once, at the first path it meets, and the root.pnpmstore is walked before the member trees. Scan's path filter then sees only the.pnpmcopy. Rollback'sfind_all_packages_for_rollbackreturns every copy, including the member link.Impact
scan packages/foo(andapps/**) never selects anything in a pnpm workspace. CI that patches per member withsocket-patch scan --mode agent --json packages/<name>getsstatus: success,scannedPackages: 0, exit 0, and the member's vulnerable dependency stays unpatched with no warning.rollback packages/areverts packages thatscan packages/acould never have applied.node_modules/left-padlink is recorded first, soscan node_modules/left-padmatches. Neither isnode-linker=hoisted(members have nonode_modules).Repro (pnpm 12.8.1, Linux, main
045d7ec)Expected vs actual
CLI_CONTRACT.md, "Path-scoped scans": in agent mode, "a package is in scope iff ANY of its crawled installed copies sits under a matching path", "a pattern matching any ancestor directory of the copy path also matches, so a bare
scan packages/fooscopes the whole subtree". It also says the glob semantics are "shared withrollback's path targets".scan --mode agent packages/aselectsleft-pad@1.3.0(its installed copy for memberaispackages/a/node_modules/left-pad), just asrollback packages/adoes..pnpmpath. The scope is empty and the run exits 0 with nothing patched.OS × version
scan packages/a(and/**, and the link path)rollback packages/anode-linker=hoistednode_modulesscan node_modules/left-padmacOS and Windows weren't tested, but the crawl order and dedup aren't OS-specific.
First bad version
This isn't a regression. Release 4.0.0 has no
scan [PATHS]; path-scoped scans arrived with v5 on main.Suspect code
crates/socket-patch-cli/src/commands/scan/mod.rs:1750: the scope filter testspkg.pathof the crawled records, which hold one path per name@version.crates/socket-patch-core/src/crawlers/npm_crawler.rs:1811: theseenname@version dedup. The root.pnpmstore wins before the workspace member links are visited.crates/socket-patch-cli/src/commands/rollback.rs:1374: rollback matches againstfind_all_packages_for_rollback, which returns every copy, including member links. That's why the two commands diverge.Other symlinked layouts (yarn's pnpm linker, Bun's isolated linker, vlt, npm
install-strategy=linked) probably behave the same way. Each sibling routine can confirm its own.