Repository navigation
Fix npm workspace-member refusal skipped by a stray member lock (#1094) - #1095
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A workspace member that still held its own package-lock.json or npm-shrinkwrap.json skipped the workspace-member refusal. npm never reads a lock inside a member: it installs every member from the workspace root's lock. So hosted scan/get pinned the ignored member lock and vendored mode vendored into it. Both exited 0 while npm kept installing the unpatched package. Hosted scan/get from such a member now refuse with redirect_workspace_lockfile_elsewhere, naming the root lock and the ignored member lock. Vendored refuses with vendor_lockfile_missing, as it does for a member with no lock. A member that also holds a lock its own manager reads (pnpm, yarn, Bun, vlt, Rush), or a workspace root with no npm lock, keeps the own-lock shortcut. Fixes #1094 (hosted and vendored legs) Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] CI notes:
Generated by Claude Code |
…stray-lock # Conflicts: # crates/socket-patch-cli/CLI_CONTRACT.md # crates/socket-patch-core/src/hosted/governing_root.rs
|
[agent] Update:
BugBot review Generated by Claude Code |
|
BugBot review Generated by Claude Code |
A vendored run from an npm workspace member with a stray lock refused only inside the vendor backend, after the hosted-to-vendored takeover had already restored a leftover hosted pin. The takeover preflight now raises the same vendor_lockfile_missing refusal first, so nothing is reverted. The hosted and vendored messages no longer suggest deleting the stray lock: the directory is still a listed workspace member, so it would be refused again. Both name the workspace root to run from. Refs #1094 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
CI note: I cancelled this PR's Bun and vlt compatibility runs from 03:15Z (37721894513, 37721894616). They ran on the pre-#1093 workflow config, which runs macOS legs on PRs, and were holding about 22 macOS jobs while the merge queue was starved. Merging |
|
[agent] Merged Generated by Claude Code |
main replaced the hosted lock-name lists with NpmLockFamily. The stray member-lock check now reads npm's own locks and the other families' locks from that table instead of the removed constants. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] Took this over (stale heartbeat, merge conflict with On 75fdc9a, "Audit GitHub Actions" failed with zizmor Generated by Claude Code |
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 3faf081. Configure here.
|
Ready for review (burn-down agent).
Nothing specific flagged for the reviewer beyond the PR description. Generated by Claude Code |
Takes main's squash of #1095 and keeps only this branch's Bun and vlt changes on top. Also drops the unrelated reformatting an earlier commit picked up from a workspace-wide cargo fmt. Assisted-by: Claude Code:claude-opus-5-5
Main's new contract rows (workspace-lock-elsewhere, eject refusals) and the stray-member-lock test (#1095) now use the top-level error object: the rows say error.code, and the test reads error.message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LLM Description written by Claude Code:claude-opus-5-5
Refs #1094 (fixes the hosted and vendored legs; the
vexleg for a member lock pinned before this fix is a follow-up, see below)Root cause
hosted::governing_root::refusalskips the #884package.json-workspaces member check whenever the member directory holds any npm-family lock (has_own_npm_family_lock). npm reads nopackage-lock.json/npm-shrinkwrap.jsoninside a workspace member: it always installs members from the workspace root's lock. So a stray member lock (common after a package moves into a monorepo with its old lock still committed) suppressed the refusal. Hostedscan/getthen pinned the ignored member lock, and vendored mode vendored into it. Both exited 0 while npm installed the unpatched package.Fix
governing_root::npm_member_stray_lock(new,pub(crate)): a directory whose only own locks are npm locks, listed by an ancestorpackage.jsonworkspaceswhose root holdspackage-lock.jsonornpm-shrinkwrap.json. The workspace-root walk is split out ofpackage_json_workspace_refusal(package_json_workspace_root) so both checks share it. Fix workspace-member refusal for vlt and brace/class globs (#1071, #942) #1073's vlt fallback (merged frommain) lives in that shared walk too.refusalruns that check when the member has its own lock, and refuses with the existingredirect_workspace_lockfile_elsewhere. The message names the root lock and the ignored member lock, plus the workspace root to run from. That's the only remedy that clears the refusal, since the directory stays a listed member whatever lock it holds (Bugbot).npm_flavor::npm_member_stray_lock_refusalrefuses the package-lock flavor in that layout withvendor_lockfile_missing, the same code a member with no lock gets. Bothvendor_npm_anyandnpm_lock_vendor_preflightraise it. The preflight runs before any download or hosted→vendored takeover revert, so a leftover hosted pin is never restored only to have vendoring refused (Bugbot).vendor --revert/ rollback don't go through this path, so anything already vendored there can still be unwound.package.jsonworkspaces, and yarn berry treats a nestedyarn.lockas its own project. Classic yarn and Bun weren't tested in the issue and aren't changed here.redirect_workspace_lockfile_elsewhererow now describes the new case.mainmerge (Decide which lockfile governs installs in one table #1044's governing-lock table), the check reads npm's locks and the other families' locks fromNpmLockFamily/npm_lock_files()instead of the removedNPM_LOCKS/OWN_LOCKSlists.Follow-up (why
Refs, notFixes)With this change, socket-patch no longer writes a pin into an ignored member lock. But a member lock pinned by an earlier version is still read by
vex, which would attest it. Gating that needs a new VEX omission code and note in the contract (the existingunattestedgate's note is Gradle-specific). I left #1094 open for that piece.Tests (red → green)
hosted::governing_root::tests::npm_member_with_stray_npm_lock_is_refusedvendor::npm_flavor::tests::npm_member_with_stray_lock_is_refusedNone)scan+get <uuid>CLI, 3 root/member lock combosin_process_redirect_pnpm::hosted_scan_from_npm_member_with_stray_lock_refusesCommands run locally:
cargo clippy --workspace --all-features -- -D warnings: clean (on d9a569f, and on the 09:24Zmainmerge)cargo test --workspace --all-features --no-fail-fast(on themainmerge 3c8e956): 237 test binaries pass. The only 12 failures are permission-based write-failure tests (*_write_failure_*,*unremovable*,relax_loop_must_not_traverse_symlinked_root), which can't fail as root, the user this sandbox runs as. Re-run as an unprivileged user (setpriv --reuid=65534), all 12 pass.mainmerge):cargo test --workspace --all-features --no-fail-fastfails the same 12 root-only permission tests plusmode_migration_pypi::pipenv_hosted_to_vendored_names_the_unpatched_requirements, which needs pypi.org (unreachable from this sandbox; unrelated to npm). Everything else passes, includingin_process_redirect_pnpm(23) andscan_vendor_e2e(37).cargo fmt: the changed code is formatted.mainitself isn'tcargo fmt --checkclean (about 120 files), and CI doesn't run fmt, so I didn't reformat unrelated files.No wrapper changes (
npm/,pypi/,gem/): the logic lives in the core crate only.🤖 Generated with Claude Code
https://claude.ai/code/session_01479CXaUep24uv6zcJnZ59g
Generated by Claude Code