Repository navigation
Description
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
For an npm package-lock project, scan --mode vendored decides which patches "the wet run would refuse" with preflight_refused_purls (planning) and preview_vendor (the --dry-run preview). For npm package-lock, both only ask npm_lock_vendor_preflight, the project-level check that refuses a lockfileVersion 1 lock. The per-entry refusals the npm backend raises later aren't checked:
vendor_lock_entry_not_rewritable: the only copy is installed from a remote tarball URL or a git spec (also a bundled orhasShrinkwrapcopy).vendor_workspace_member: the package is a workspace member link.
So for those packages:
scan --mode vendored --dry-runandget … --mode vendored --dry-runreportverified("Would download and vendor 1 patch"), exit 0. The wet run then refuses with exit 1 (partialFailure). Compare the v1-lock case, where the same preview correctly prints[would-refuse] … (vendor_lockfile_version_unsupported).- With
--max-new-patches N, the refused package still takes a rollout slot. If it is the most severe new patch, it takes the slot on every run, and a vendorable patch ranked below it is deferred forever. Each re-scan downloads the refused patch again, fails and defers the same vendorable patch.
Hosted mode on the same project is fine: it skips the copy with redirect_npm_non_registry_entry_skipped and pins the other package within the cap.
Impact
- With a
--max-new-patches/maxNewPatchesrollout, every scan exits 1 and vendors nothing. Lower-ranked fixable patches are never delivered, and "commit and run scan again" is a loop. - CI that gates on the dry run sees a clean preview, then the wet run fails. That's the same misprediction already fixed for other lanes: pnpm hosted-to-vendored scan/get dry-run previews success for a refused takeover #853 (pnpm takeover), uv scan/get dry-run does not predict a refused hosted-to-vendored takeover #944 (uv), Vendored
vendor --dry-runpreviews success on a uv project with an inline[tool.uv]/sourcestable, but the real run refusespypi_uv_lock_parse_failed(exit 1) #979, Gemvendor --dry-runeject previewseject_planned(exit 0) for a hosted gem declared in agroupblock, but the realvendorrefusesgemfile_declaration_not_editable(exit 1) #1761 (gem), Vendored dry run gives no symlink warning for uv script locks or pylock files: a symlinkeds.py,s.py.lock,pylock.tomlorpylock.<name>.tomlpreviews clean, and the real run then refuses (exit 1) #891 (PyPI symlinks).
Repro (Linux, Node 22.23.4, npm 10.9.9, main cbbbdb9)
This uses a local mock of the org patch API on 127.0.0.1:8899 with the same contract as crates/socket-patch-cli/tests/e2e_redirect_npm_build.rs (patches/batch, by-package, package (granted tarball + sha512), view/<uuid>, tarball route). It serves one free patch each for pkg:npm/left-pad@1.3.0 (severity high) and pkg:npm/is-number@7.0.0 (severity medium).
API="--api-url http://127.0.0.1:8899 --org org --api-token fake --patch-server-url http://127.0.0.1:8899"
mkdir app && cd app
cat > package.json <<'EOF'
{"name":"app","version":"1.0.0","dependencies":{
"left-pad":"https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
"is-number":"7.0.0"}}
EOF
npm install
socket-patch scan --mode vendored --dry-run --yes $API # "Would download and vendor 2 patches", exit 0
socket-patch scan --mode vendored --max-new-patches 1 --yes $API; echo "exit=$?"
socket-patch scan --mode vendored --max-new-patches 1 --yes $API; echo "exit=$?" # same againActual (both runs):
Downloading 1 patch...
[fetch] pkg:npm/left-pad@1.3.0
Error: Cannot vendor pkg:npm/left-pad@1.3.0: … lock entry `node_modules/left-pad` is not installed from the registry (the project depends on it as "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", …) and CANNOT be rewritten …
Vendored 0 packages; 1 failed.
exit=1
JSON rollout: {"counts": {"new": 1, "deferred": 1}, "deferred": [{"purl": "pkg:npm/is-number@7.0.0", …}]}. .socket/vendor/state.json is never written.
Control: with "left-pad": "1.3.0" (registry), run 1 vendors left-pad and defers is-number, and run 2 vendors is-number (exit 0 both times).
Same result with "left-pad": "github:stevemao/left-pad#v1.3.0" (vendor_lock_entry_not_rewritable), and with a workspace member packages/left-pad ("workspaces": ["packages/*"], the upstream 1.3.0 sources) (vendor_workspace_member).
Expected vs actual
- Expected: CLI_CONTRACT.md (
scan --mode vendoredparagraph and the error-code table row "Bun / pnpm-takeover / PyPI refusal codes") describes the dry run as a per-patch preview that lists a package the wet run would refuse asskippedwith that refusal code. The rollout plan comment inscan/mod.rssays "Vendored eligibility is the wet run's Bun / vlt / npm-lock / gem takeover preflight", andpreflight_refused_purlssays "so a refused NEW patch holds no rollout slot". A package the npm backend is certain to refuse should be previewedwould_refuseand should not take a slot, just like the v1-lock case. - Actual: previewed
verified, takes the slot, refused at vendor time, every run.
Matrix (Linux)
| npm | shape | dry run | --max-new-patches 1 |
main cbbbdb9 |
v5.0.1 |
|---|---|---|---|---|---|
| 8.19.4 (v2) | URL spec | verified |
left-pad refused, is-number deferred | repro | repro |
| 10.9.9 (v3) | URL spec | verified |
same | repro | repro |
12.2.1 (allow-remote=all) |
URL spec | verified |
same | repro | not run |
| 10.9.9 | git spec (github:) |
verified |
same (vendor_lock_entry_not_rewritable) |
repro | repro |
| 10.9.9 / 12.2.1 | workspace-member link | verified |
same (vendor_workspace_member) |
repro | repro |
| 6.14.18 (v1 lock) | registry | would_refuse (correct) |
— | ok | — |
| any | registry (control) | verified |
vendors 1, defers 1, next run vendors it | ok | ok |
It's not a regression: v5.0.1 behaves the same.
Suspect code
crates/socket-patch-cli/src/commands/scan/vendor_flow.rs:385npm_lock_refusal→crates/socket-patch-core/src/vendor/npm_lock.rs:413npm_lock_vendor_preflightcovers only the project-level lock-version refusal, and it's the only npm package-lock input to bothpreflight_refused_purls(vendor_flow.rs:349, used by the rollout plan atscan/mod.rs:3306) andpreview_vendor(vendor_flow.rs:193).- The per-entry refusals are only raised inside the backend:
npm_lock.rs:525(vendor_workspace_member) andnpm_lock.rs:556(vendor_lock_entry_not_rewritable).
Related, but not the same bug: #1908 / PR #1930 (the planning pass drops pypi_refusals; its acceptance criteria keep the npm-lock preview rows unchanged, so this gap stays after it lands). #1965 is a different gap: an override to a URL isn't refused at all. #1850 covers the dry-run headline counting already-vendored rows.
No probe runs: probe branches are blocked in this session (see ledger), so macOS and Windows weren't run. The logic is OS-independent.
Activity
npm: run 64 (#1976)
mikolalysenko commentedon Nov 2, 2026
[agent] Triaged as
priority:p1(npm). Not a duplicate: #1908 / #1930 is about the planning pass dropping the PyPI refusals and #1965 / #1966 is about overrides to a URL; neither makes the npm package-lock preview ask the per-entry refusals (vendor_lock_entry_not_rewritable,vendor_workspace_member). No open or merged PR addresses it.Generated by Claude Code
mikolalysenko commentedon Nov 2, 2026
[agent] Claiming this issue (shared root cause: the vendored lock-text refusal check,
npm_flavor::lock_text_refusals, skips the npm package-lock flavor, and the dry-run preview and rollout planning never ask it). Branch: agent/fix-npm-lock-entry-preview-refusals. Claim-ID: 2026-11-02T08:23:10Z-6878beGenerated by Claude Code
mikolalysenko commentedon Nov 2, 2026
[agent] Draft PR: #1977
Generated by Claude Code
Refuse npm package-lock entries before download
Preview npm lock-entry refusals in vendored runs