Repository navigation
Fix hosted runs from npm/yarn/bun workspace members pinning nothing (#884) - #901
Mikola Lysenko (mikolalysenko) wants to merge 9 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A hosted scan or get run from a member of an npm, yarn (classic or berry) or Bun workspace found the member's copy of a patched package, saw no lockfile in the member directory, pinned nothing and exited 0 with "success". The package manager then installed the unpatched copy from the workspace root's lockfile, and the only hint was a warning about a missing package-lock.json. The workspace-member pre-check only knew about pnpm and cargo. It now also finds the nearest ancestor package.json whose "workspaces" list matches the member directory. If that root holds a package-lock.json, npm-shrinkwrap.json, yarn.lock, bun.lock or bun.lockb, the run is refused before anything is written with redirect_workspace_lockfile_elsewhere, and the message names the workspace root to run from. Vendored mode already refused this layout. Fixes #884 Assisted-by: Claude Code:claude-opus-5-5
main has failed socket-patch-core's lib tests since Gradle support (#646) and the digest helpers (#865) both landed. The guard test production_digests_go_through_the_helpers flags three files #646 added that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and patch/sidecars/maven.rs. That breaks test, test-release and coverage on every open PR. Each inline sha1/sha256 call now goes through sha1_hex_of or sha256_hex_of, which compute the same lowercase hex. Behaviour is unchanged. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Empty crawl skips workspace refusal
- Modified governing_root::refusal to check npm workspace layouts when candidates is empty and package.json exists, ensuring hoisted/PnP members are refused before returning success.
Or push these changes by commenting:
@cursor push 59281f5e08
Preview (59281f5e08)
diff --git a/crates/socket-patch-core/src/hosted/governing_root.rs b/crates/socket-patch-core/src/hosted/governing_root.rs
--- a/crates/socket-patch-core/src/hosted/governing_root.rs
+++ b/crates/socket-patch-core/src/hosted/governing_root.rs
@@ -68,7 +68,12 @@
return Some(refusal);
}
}
- if candidates.iter().any(|c| c.dep.ecosystem == "npm") {
+ // Check npm-family workspace refusals when there are npm candidates OR
+ // when candidates is empty and a package.json exists (hoisted/PnP
+ // members may have zero discovered packages).
+ let has_npm_candidates = candidates.iter().any(|c| c.dep.ecosystem == "npm");
+ let has_package_json = root.join("package.json").exists();
+ if has_npm_candidates || (candidates.is_empty() && has_package_json) {
if !has_own_npm_family_lock(root) {
if let Some(refusal) = package_json_workspace_refusal(root).await {
return Some(refusal);You can send follow-ups to the cloud agent here.
A workspace root with no lockfile of its own can itself be a member of an outer workspace (yarn berry's nested worktrees), where the outer root holds the lockfile both use. The member check stopped at the inner root and let the hosted run report success with nothing pinned. It now keeps walking with the inner root as the member and refuses at the outer root. Refs #884 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
A pnpm workspace nested inside an outer yarn or npm workspace owns its members: pnpm installs them from the nested pnpm-lock.yaml. The member walk treated that nested root as lockless and went on to the outer root, so the refusal named the wrong directory to run from. The walk now stops at a root holding any npm-family lock (pnpm, vlt, Rush) and leaves it to the pnpm check, which names the right root. Refs #884 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The previous change stopped the member walk at a nested root holding a pnpm, vlt or shrinkwrap.yaml lock and left it to the pnpm check. That check only knows pnpm workspaces with pnpm-workspace.yaml or lockfile-dir, so a stray pnpm-lock.yaml there could still let a hosted run from the member report success with nothing pinned. The pnpm check now runs first, so a pnpm workspace still gets its own precise message. The package.json walk then refuses at the first matching root that holds any npm-family lock, naming that root. Only a Rush root, whose locks live under common/config, ends the walk without a refusal. Refs #884 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
When a yarn or npm workspace sits inside a pnpm workspace, the member's lockfile is the nearer one. The pnpm check ran first and named the outer pnpm root, so a follow-up run from there would rewrite pnpm-lock.yaml and leave the member's real lockfile unpatched. The member check now weighs both roots and names the nearer one. When they are the same directory, pnpm's own message wins. Refs #884 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
pnpm reads only pnpm-workspace.yaml and vlt only vlt.json, so a pnpm or vlt lock sitting at a package.json "workspaces" root does not govern that root's members. Counting such a stray lock as ownership let it beat the outer pnpm workspace that really installs the member, and the refusal pointed at a directory whose run would rewrite the wrong lockfile. The workspaces walk now counts only npm, yarn and Bun locks. Roots that pnpm governs are left to the pnpm check, and when both kinds govern a member the nearer root still wins. Refs #884 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
f92cb6a ran a workspace-wide cargo fmt, reformatting 118 files that the fix does not otherwise touch. That buried the real change in ~2,300 lines of formatting diff and invites merge conflicts with every other open PR. Restore those files to their merge-base versions; each was checked to be rustfmt-equivalent to its main version, so behavior is unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run 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 0be357d. Configure here.
|
[burn-down agent] Ready for review at head
Generated by Claude Code |

LLM Description written by Claude Code:claude-opus-5-5
Fixes #884
Root cause
hosted::governing_root::refusal(#598) refuses a workspace member whose lock lives in an ancestor only for pnpm (pnpm-workspace.yaml/lockfile-dir) and cargo. npm, yarn classic, yarn berry and Bun workspaces declare members through the rootpackage.jsonworkspacesfield and keep one lock (package-lock.json,npm-shrinkwrap.json,yarn.lock,bun.lock/bun.lockb) at that root. A hostedscanorget <uuid> --mode hostedfrom a member reads only the member directory, finds no lock and exits 0 withsuccessandredirected: 0. Its only signal isredirect_npm_no_lockfile, which names the wrong package manager. A fresh install then puts the unpatched copy in the member.Fix
governing_rootgets an npm-familyworkspacescase. When the project dir has no npm-family lock of its own (and isn't a Rush repo), it walks the ancestors for the nearestpackage.jsonwhoseworkspaces(array, or the object form'spackages, which covers yarn classicnohoist) matches the member path. Matching supports*,?and**, and a later!negation excludes a path. If that root holds an npm/yarn/Bun lock, the run is refused before any takeover or write with the new coderedirect_workspace_lockfile_elsewhere, and the message names the lock and the root to run from. A lockless root, an unlisted directory or a member with its own lock is left alone. vlt is out of scope: it declares workspaces invlt.json.has_own_npm_family_lock) is now shared by the pnpm and package.json checks.nested_lockless_workspace_defers_to_the_outer_root). When both a pnpm workspace and apackage.jsonworkspace govern the member, the nearer root wins and a tie goes to pnpm'sredirect_pnpm_lockfile_elsewhere(nearer_package_json_root_beats_an_outer_pnpm_workspace). Only npm/yarn/Bun locks count at aworkspacesroot. pnpm reads onlypnpm-workspace.yamland vlt onlyvlt.json, so a stray innerpnpm-lock.yamlis walked past rather than named (nested_pnpm_root_stops_the_package_json_walk,stray_inner_pnpm_lock_does_not_beat_the_outer_pnpm_workspace).CLI_CONTRACT.mddocuments the code.vendor_lockfile_missing), so the two modes now agree.Route Gradle digests through utils::digest, cherry-picked as d4462e9).main(9c43dfc) is red ontest/coverage/test-releaseinutils::digest::tests::production_digests_go_through_the_helpers. That failure isn't this PR's. Route Gradle digests through utils::digest #878 is its fix, and the cherry-pick becomes a no-op once Route Gradle digests through utils::digest #878 lands.Follow-up (not in this PR)
A hosted
scanfrom a hoisted / PnP member with no local copy finds 0 packages and exits 0, as pnpm members do. Whether a member scan should inventory the root's hoistednode_modules, or fail closed on an empty crawl, is a separate discovery-scope decision (Bugbot thread on d4462e9).get <uuid> --mode hostedin that layout is refused.Per-issue checklist
{packages, nohoist}forms), yarn berry (yarn.lock), Bun (bun.lock,bun.lockb), npm (package-lock.json,npm-shrinkwrap.json). Covered by the CLI testin_process_redirect_pnpm::hosted_scan_from_package_json_workspace_member_refuses, which runsscan --mode hostedandget <uuid> --mode hostedfor each shape and checks exit 1,redirect_workspace_lockfile_elsewhereand an untouched root lock. Core unit tests inhosted::governing_root::tests:package_json_workspace_member_is_refused_for_every_root_lock,package_json_object_workspaces_member_is_refused,package_json_workspace_non_members_are_left_alone,package_json_workspace_root_is_the_nearest_listing_ancestor,workspaces_patterns_match_like_npm_and_yarnandworkspace_patterns_reads_both_field_shapes.Test evidence
governing_root.rsfrom main):cargo test -p socket-patch-cli --all-features --test in_process_redirect_pnpm package_json_workspacefailed withpackage-lock.json (object form: false): a found-but-unpinnable patch is not success … left: Some(0) right: Some(1)and"status":"success".in_process_redirect_pnpm17/17 passed,governing_rootunit tests 14/14 passed.cargo clippy --workspace --all-features -- -D warningspasses. The workspace-widecargo fmtchurn from f92cb6a (118 untouched files) was reverted in 0be357d; each restored file is rustfmt-equivalent tomain, so the diff is now just the 6 files this fix touches.cargo test --workspace --all-features --no-fail-fastgave 226 test binaries OK. The only failures were tests that depend on file permissions (chmod 0o555/ unremovable files), which can't fail as root in this sandbox:covgap_commands_vendor(3),in_process_redirect(3 write-failure cases),repair(2), and in core libcopy_treesymlink relax,vlt_healunremovable lock,pypi_poetry/pypi_requirementswrite failures. None of them touch this change. The core digest guard failure is fixed by the Route Gradle digests through utils::digest #878 port.🤖 Generated with Claude Code
https://claude.ai/code/session_01LYxnnBSxyMahcBX9SZag9L
Note
Medium Risk
Changes hosted scan/get exit behavior for monorepo members (fail-closed instead of false success); workspace-root detection is non-trivial but heavily tested.
Overview
Hosted mode no longer silently “succeeds” when run from an npm, Yarn, or Bun workspace member whose lockfile lives at the repo root.
hosted::governing_rootnow walks ancestors for a rootpackage.jsonwhoseworkspacespatterns match the member (array or{ packages }form, with*,?,**, and!negation). If that root holdspackage-lock.json, shrinkwrap,yarn.lock, orbun.lock/bun.lockb,scan/get --mode hostedrefuse before any write withredirect_workspace_lockfile_elsewhere, exit 1, and tell the user to run from the workspace root. Members with their own lock, unlisted paths, and Rush roots are unchanged. When both pnpm andpackage.jsonworkspaces apply, the nearer governing root wins (pnpm message on a tie).CLI_CONTRACT.md documents the new code alongside the existing pnpm/cargo member refusals. Integration tests cover each lock type for
scanandget; core unit tests cover nested workspaces and pnpm/yarn interaction.Also routes Gradle/JVM SHA-1 and SHA-256 through
utils::digesthelpers ingradle_cache,jvm_jar, and Maven sidecars (cherry-pick of #878), replacing duplicated inline digest calls.Reviewed by Cursor Bugbot for commit 0be357d. Configure here.
Generated by Claude Code