Skip to content

Fix hosted pnpm pin of a lock pnpm ignores (#1074) - #1431

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-pnpm-lockfile-disabled
Oct 12, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-pnpm-lockfile-disabled

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1074

Root cause

socket-patch never read pnpm's lockfile setting (lockfile: false in pnpm-workspace.yaml for pnpm 11+, lockfile=false in .npmrc for pnpm 9/10). With it off, pnpm neither reads nor writes pnpm-lock.yaml and installs every dependency from the registry. So:

  • the hosted engine pinned the committed lock, added trustLockfile: true and reported applied for a patch no install applies;
  • lock-only VEX (vex/discover/npm.rs extract_pnpm_lock) gave those pins the lockfile basis and attested not_affected while pnpm install fetched the upstream package.

Fix

  1. Reader (utils/pnpm_workspace.rs lockfile_disabled): reads the setting from the file the project's pnpm major reads. pnpm 11+ reads pnpm-workspace.yaml; pnpm 10 and older read .npmrc (the issue's matrix shows pnpm 10 ignores the YAML key). Within each source, lockfile counts when it is a boolean. Otherwise npm's package-lock / packageLock counts, because every pnpm from 7 to 11 computes useLockfile that way. The major comes from node_modules/.modules.yaml, else the packageManager pin, else the lock grammar, through the existing pnpm_settings_major helpers. With no major known, either file turning it off counts, since withholding is the safe side. Only the project's own files are read, through the view's fingerprinted reader. Hosted mode also reads the npm_config_* / pnpm_config_* environment; vex does not.
  2. Hosted (hosted/engine.rs): only in a pnpm project (a root pnpm lock or a pnpm-workspace.yaml), every pnpm lock (root and member) is left out and the run warns redirect_pnpm_lockfile_disabled. Nothing is written and no trustLockfile key is added. The remedy names --mode vendored only when a pnpm lock exists for it to wire; otherwise it says to remove the setting and run pnpm install. This is the same contract as redirect_pnpm_git_branch_lockfile: exit 0 with the warning, applied: 0. Both refusals share one withhold_root_pnpm_locks helper. Disk and in-memory engines behave the same.
  3. VEX (vex/discover/npm.rs): hosted refs in the root and member locks keep no lock pin, and one patched_ref_unattributable per lock names them, so only an installed tree whose copies verify can attest. Vendored refs are untouched. Rush locks are excluded. This mirrors the contract for a vlt lock that vlt discards.
  4. Docs: CLI_CONTRACT.md (new paragraph plus the pnpm row of the discovery table) and docs/ecosystems.md.

Known limit: a workspace member scanned from its own directory, with no settings file of its own, does not inherit its root's lockfile: false. Walking the ancestors would invalidate the re-scan read cache on every run (see review finding 1 below). Wrappers (npm/, pypi/, gem/) only dispatch the binary, so they need no change.

Tests (red → green)

Case Test Red without fix
hosted, pnpm 11/12 YAML lockfile: false (disk + in-memory) hosted::engine::tests::a_disabled_lockfile_is_never_pinned ✅ failed
hosted, pnpm 9/10 .npmrc lockfile=false same test ✅ failed
hosted, member locks (sharedWorkspaceLockfile: false) a_disabled_lockfile_pins_no_member_lock ✅ failed
hosted, per-major file choice (pnpm 9 ignores the YAML key, 12 ignores .npmrc) a_disabled_lockfile_is_read_where_the_pnpm_major_reads_it n/a (review follow-up)
hosted, npm's package-lock=false / packageLock: false fallback; lockfile=true wins package_lock_false_disables_the_lockfile ✅ failed (review follow-up)
no pnpm lock → no vendored remedy; npm project .npmrc → no pnpm refusal a_disabled_lockfile_without_a_pnpm_lock n/a (review follow-up)
control: on / unset / non-boolean / file the major ignores an_enabled_lockfile_is_pinned passes before and after
lock-only VEX (lockfile both spellings, package-lock=false, member locks) vex::discover::npm::tests::pnpm_disabled_lockfile_withholds_the_lockfile_basis ✅ failed (lock basis still ok)
end to end, real pnpm 11 e2e_redirect_pnpm_build::pnpm11_disabled_lockfile_is_neither_pinned_nor_attested ✅ failed: re-scan reported success, applied: 1, only redirect_pnpm_trust_lockfile

The e2e leg runs in the existing e2e_redirect_pnpm_build matrix on all three OSes. Locally it ran against real corepack pnpm@11 with SOCKET_PATCH_PNPM_E2E_REQUIRED=1. After the fix the re-scan returns applied: 0 with redirect_pnpm_lockfile_disabled, the lock and workspace file are byte-identical, and lock-only vex exits non-zero without the purl. The control, with the setting removed, attests. On 0363b42, e2e_redirect_pnpm_build (20) and in_process_redirect_pnpm (32) both pass.

Local gate

  • cargo fmt --all -- --check ✅
  • cargo clippy --workspace --all-features -- -D warnings ✅
  • cargo test --workspace --all-features --no-fail-fast (on e7e7bb2): 246 test binaries pass. 13 tests fail, and every one also fails on origin/main in this sandbox. They are write-failure and permission tests (chmod 0o555 dirs, unremovable locks), plus one bun test, and the sandbox runs as root, so root's writes succeed. I re-ran each of the 13 on main and got the same failures. None touch pnpm. On 0363b42, socket-patch-core --lib gives 6340 passed, and only the same 4 root-only tests fail.

Review

Reviewer thread (package-lock fallback): confirmed in the pnpm 7.33.5 / 8.15.9 / 9.15.9 / 10.34.7 / 11.28.5 bundles and fixed in 0363b42. The thread is resolved.

/code-review high, fixed in e7e7bb2:

  1. The ancestor walk ended the read-cache recording window on nearly every run. The ancestor walk is removed; only view reads remain.
  2. The setting was read the same way for every pnpm major. It is now read from the file each major actually reads.
  3. The --mode vendored remedy was a dead end with no lock. It is now named only when a pnpm lock exists.
  4. The refusal fired for non-pnpm projects. It is now gated on a pnpm lock or workspace file.
  5. The withholding code was duplicated. It is now one shared helper.
  6. Env vars leaked into vex. vex no longer reads them.
  7. The pnpm_refusal field doc was stale. Updated.

Not fixed:

  • 4 (a stale lock's hosted refs still count as hosted wiring for list/rollback/rollout): intentional. rollback/remove must still find these pins to restore the registry resolution. The vlt "discarded lock" case works the same way (ref kept, basis withheld). The scan surfaces the refusal warning.
  • 5 (lock-only vendored wiring, a registry key hand-pointed at a vendored tarball with no overrides): socket-patch's pnpm vendor backends always write overrides, which pnpm applies without a lock (verified in the issue on 12.10.1). A hand-edited lock with no override is out of scope here.
  • 7 (duplicate settings reads): the reads go through the view's cached reader, and lockfile_disabled returns before the major lookup unless a file sets the key. The remaining cost is two cached reads per run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AY4fDHDqR3oTHhUXpqBGcU


Note

Medium Risk
Changes hosted redirect and lock-only VEX behavior for pnpm; it is fail-closed (no silent pins) but can block attestations that previously succeeded from committed locks alone.

Overview
Fixes false confidence when pnpm ignores the lockfile (#1074). If lockfile is off (YAML on pnpm 11+, .npmrc on older majors, or npm package-lock=false), pnpm installs from the registry and never applies hosted pins in pnpm-lock.yaml.

The change adds lockfile_disabled detection (pnpm major from install record, packageManager, or lock grammar) and, in pnpm projects only, skips all pnpm lock rewrites before branch-lock handling—emitting redirect_pnpm_lockfile_disabled, leaving locks and workspace YAML untouched, and not adding trustLockfile. Lock-only VEX still sees hosted URLs but drops the lockfile basis and reports patched_ref_unattributable per lock (Rush locks unchanged; vendored refs unchanged).

Docs (CLI_CONTRACT, ecosystems table) and unit/e2e tests cover YAML vs .npmrc, member locks, package-lock=false, and real pnpm 11 behavior.

Reviewed by Cursor Bugbot for commit 0363b42. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
With pnpm's `lockfile` setting turned off (`lockfile: false` in
pnpm-workspace.yaml for pnpm 11+, `lockfile=false` in .npmrc for
pnpm 9/10), pnpm never reads pnpm-lock.yaml and installs the upstream
package from the registry. Hosted scans still pinned the committed
lock, added `trustLockfile: true` and reported success for a patch no
install applies.

Hosted mode now leaves every pnpm lock untouched for such a project
and warns `redirect_pnpm_lockfile_disabled`, pointing at
`--mode vendored` (pnpm applies its overrides without a lock). The
setting is read the way the gitBranchLockfile refusal already reads
its own (project files, governing workspace, environment), through a
shared lookup.

Refs #1074

Assisted-by: Claude Code:claude-opus-5-5
With pnpm's `lockfile` setting off, pnpm never reads pnpm-lock.yaml,
yet lock-only `vex` (no node_modules: the usual CI/SBOM run) attested
a hosted pin there as not_affected while pnpm installed the upstream
package.

The hosted references of the root and member pnpm locks now keep no
lockfile basis in that case, so only an installed tree whose copies
verify can attest them, and one patched_ref_unattributable per lock
names them. Vendored references are unchanged: pnpm applies the
overrides that wire them without a lock. Rush locks are not affected.

Adds a real-pnpm 11 e2e leg covering both the hosted refusal and the
lock-only VEX, and documents the setting in the CLI contract.

Fixes #1074

Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups for the #1074 refusal:

- Read `lockfile` from the file the project's pnpm major reads (YAML on
  pnpm 11+, .npmrc on pnpm 10 and older), from the install record,
  the packageManager pin or the lock grammar; with no major known,
  either file turning it off counts. A pnpm 9 project with a stray
  YAML `lockfile: true` and `.npmrc` `lockfile=false` is now refused.
- Read only the project's own files through the view, so the re-scan
  read cache is no longer disabled by an ancestor walk on every run,
  and keep the environment out of `vex`.
- Refuse only in a pnpm project, so an npm project's `.npmrc` keeps
  npm's hints, and name `--mode vendored` only when a pnpm lock exists
  for it to wire.
- Share the root-lock withholding with the gitBranchLockfile refusal.

Refs #1074

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 12, 2026 14:37
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e7e7bb2. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 12, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review (burn-down agent)

  • Head: e7e7bb27
  • CI: 36/36 green, 50 path-skipped; no main-wide failures.
  • Mergeable: yes, no conflicts with main.
  • Bugbot: reviewed e7e7bb2, no findings. No open review threads.
  • For reviewers: the per-pnpm-major read of the lockfile setting in utils/pnpm_workspace.rs, and the hosted-engine refusal path in hosted/engine.rs when pnpm's lockfile is off.

Generated by Claude Code

Comment thread crates/socket-patch-core/src/utils/pnpm_workspace.rs Outdated
Every pnpm (7 through 11) reads `lockfile` only when it is a boolean
and otherwise falls back to npm's `package-lock` to decide whether it
uses its lock. A pnpm 9/10 project with the common npm-era
`package-lock=false` in .npmrc therefore never reads pnpm-lock.yaml,
yet hosted mode still pinned the lock and lock-only vex attested it.

The hosted refusal and the VEX withholding now take `package-lock`
(`packageLock` in pnpm-workspace.yaml) into account when `lockfile`
is unset; an explicit `lockfile=true` still wins.

Refs #1074

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] coverage (and therefore ci-ok) failed on 0363b42 in -p socket-patch-cli --test in_process_edge_cases. That is the intermittent coverage-job flake tracked in #1416. This PR doesn't touch that suite: it has no pnpm or lockfile tests, and the diff only changes pnpm settings reading, the hosted pnpm refusal and pnpm VEX discovery. The suite passes 3/3 locally on this commit (28 passed each run). I've re-run the failed jobs once. If it fails again I'll treat it as real and dig in.


Generated by Claude Code

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

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants