Fix gem pair model ignoring custom lockfile and Bundler 1 twins (#749, #751) - #768
Mikola Lysenko (mikolalysenko) wants to merge 10 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Hosted mode wired the wrong gem files in two Bundler layouts, so the scan reported success (and its VEX attested a patch) while Bundler installed the unpatched gem or frozen installs failed: - Bundler 4's custom lockfile (BUNDLE_LOCKFILE, env or .bundle/config) was ignored, so the lock Bundler reads was never pinned (#749). A lockfile naming anything but the pair's own default lock is now refused in hosted (redirect_gem_bundle_lockfile_unsupported) and vendored (gemfile_not_loaded) mode before any write. - A Gemfile + gems.rb twin always followed Bundler >= 2 and wired gems.rb, but Bundler 1.x loads the Gemfile (#751). A twin whose locks say BUNDLED WITH 1.x is now wired through the Gemfile pair, and twin locks that disagree on the major are refused (redirect_gem_twin_bundler_versions_diverge). Assisted-by: Claude Code:claude-opus-5-5
Two host capstones in e2e_redirect_gem_build: a Bundler 4 project with `lockfile custom.lock` (and a leftover Gemfile.lock) redirects and attests nothing and still installs frozen (#749), and a Bundler 1.x Gemfile + gems.rb twin is wired through the Gemfile and a fresh checkout installs the patched gem (#751). Each skips on the Bundler line it does not apply to. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Ready for review at
Generated by Claude Code |
Release notes are written when a release is cut, from the merged PR log and the code, so PRs no longer edit CHANGELOG.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolve the overlap with #577 (Bundler global config, #621): classify gained the global tier, so with_lockfile takes it too. Bundler 4 reads `lockfile` through Bundler.settings, which includes ~/.bundle/config, so `bundle config set --global lockfile custom.lock` is now refused like the env and app-config spellings instead of slipping past #749's guard. BundlerEnv carries the global config path; main's positional bundler_loaded_manifest_with_env call sites move to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ULgrJMQMWEBAsiuprY449
|
I can't see which test failed yet. The job log's download URL ( What I checked locally on
Next: the macOS and Windows Generated by Claude Code |
|
Found it, and this PR doesn't cause it:
Cause: the two tests came in with #738 (#356) and assume the name-keyed resolver
So the behaviour is right and only the tests' premise is stale. Proposed patch (for
I'm not adding this to #768. It's in code unrelated to the gem change, and no fix for it is open yet. Once Generated by Claude Code |
#605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07)
|
Burn-down agent: pushed
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
Burn-down agent: labeled Ready for review.
Generated by Claude Code |
Conflicts resolved: - ruby_crawler.rs: kept this branch's BundlerEnv and main's (#736) bundler_loaded_manifest_in / bundler_loaded_lock_in. The memory branch of bundler_loaded_manifest_in now also layers BUNDLE_LOCKFILE (with_lockfile), and bundler_loaded_lock_in follows the #751 twin rule (Gemfile.lock for a bundler-1.x twin, no lock for a twin whose locks disagree on the major), so the lock readers main routed through it (inventory, ledger recovery, VEX) agree with the pair the rewriter wires. - hosted/engine.rs: keep_bundler_loaded_gem_files uses main's shared resolver plus this branch's twin/lockfile handling. - vex/discover/gem.rs: the "no loaded lock" diagnostic no longer blames only BUNDLE_GEMFILE. - CLI_CONTRACT.md: both texts (main's Gradle confirmation sentence and this branch's new gem refusal codes). - e2e_redirect_gem_build.rs: both sets of Driver variants. - hosted_memory_engine.rs: both sets of tests. Since #736 the memory engine finds gem candidates only through the lock bundler loads, so a custom BUNDLE_LOCKFILE or a twin with diverging bundler majors yields no gem candidate (same as an unsupported BUNDLE_GEMFILE on main); those two tests now assert nothing is redirected/written, and the refusal warnings are covered by new engine unit tests. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run 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: Unsupported gem layouts silent on lock-only scans
- Modified the condition in hosted/engine.rs to also check for gem manifest files presence, ensuring keep_bundler_loaded_gem_files runs even when there are no gem candidates, allowing warnings to be surfaced for unsupported layouts on lock-only scans.
Or push these changes by commenting:
@cursor push 992984be56
Preview (992984be56)
diff --git a/crates/socket-patch-core/src/hosted/engine.rs b/crates/socket-patch-core/src/hosted/engine.rs
--- a/crates/socket-patch-core/src/hosted/engine.rs
+++ b/crates/socket-patch-core/src/hosted/engine.rs
@@ -545,7 +545,11 @@
}
}
}
- if candidates.iter().any(|c| c.dep.ecosystem == "gem") {
+ if candidates.iter().any(|c| c.dep.ecosystem == "gem")
+ || GEM_MANIFEST_FILES
+ .iter()
+ .any(|&f| out.files.contains_key(f))
+ {
keep_bundler_loaded_gem_files(view, &mut out).await;
}
// A Gradle build: every script, catalog and lock file its script graphYou can send follow-ups to the cloud agent here.
Lock inventory reads only the lock bundler loads (#736), so a custom BUNDLE_LOCKFILE, an unsupported BUNDLE_GEMFILE or a Gemfile + gems.rb twin whose locks disagree on the bundler major left a lockfile-only scan with no gem entries and no warning: the per-candidate redirect refusals never ran because there were no gem candidates. Surface the reason as a gem_lock_unsupported diagnosis on the inventory's existing layout-refusal channel, which scan and the in-memory engine already turn into run-level warnings. An empty BUNDLE_LOCKFILE now shadows the tiers below it, as Settings#[] does, instead of letting a global custom lock through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ULgrJMQMWEBAsiuprY449
utils::digest's production_digests_go_through_the_helpers fails on main: three Gradle/JVM files compute digests inline. That makes `coverage`, `test` and `test-release` red on every PR. #878 routes them through utils::digest. This is the same change, ported so this PR's CI is green. It becomes a no-op once #878 lands. Claude-Session: https://claude.ai/code/session_01LS9AJhpVngXZxng8TRA2Kd Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 4d8cad2)
|
Generated by Claude Code |
|
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 cf746ec. Configure here.
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |

Fixes #749
Fixes #751
Summary
Hosted gem mode wired the wrong files in two Bundler layouts. In both, the scan reported success (and
--vexattested the patch) while Bundler installed the unpatched gem or every frozen install failed. Both now wire the pair Bundler actually loads, or refuse before writing anything.Root cause
formats::gem::manifest::LoadedManifestis the single model of "which manifest/lock pair does Bundler load", and hosted mode (keep_bundler_loaded_gem_files) and vendored mode (gem_manifest_refusal) both rely on it. It modelledBUNDLE_GEMFILE, but:lockfilesetting /BUNDLE_LOCKFILE), so it never pins the lock Bundler uses and frozen installs fail with no warning #749: it derived the lock only from the manifest name, so it never saw Bundler 4's custom lockfile (BUNDLE_LOCKFILEenv, orlockfilein.bundle/config). The lock Bundler reads was never pinned. A leftoverGemfile.lockwas rewritten instead, even though Bundler ignores it.gems.rbin a Gemfile/gems.rb twin locked by Bundler 1.17, which loadsGemfile, so the install stays unpatched while the in-run VEX attests it #751: default discovery for aGemfile+gems.rbtwin always followed Bundler ≥ 2 (gems.rbfirst). Bundler 1.x loadsGemfilefirst.Fix
LoadedManifest::with_lockfileresolves the configured lockfile the wayBundler::CLIdoes (env first, then app config, then the global~/.bundle/configthat Gem settings resolution skips Bundler's global config (~/.bundle/config/BUNDLE_USER_CONFIG), so a globalcache_pathorgemfilegets no warning or refusal and VEX attests an unpatched install #577 / Fix Bundler global config being ignored (#577) #621 added;BUNDLE_IGNORE_CONFIGhonoured; relative to the project root). If it names the loaded pair's own lock, nothing changes. Anything else is the newUnsupportedLockfile:redirect_gem_bundle_lockfile_unsupported(nothing written, nothing attested);gemfile_not_loaded.default_twin_manifestreads both twin locks'BUNDLED WITH(new sharedformats::gem::bundled_with_major):Gemfile+Gemfile.lock;gems.rb(unchanged);redirect_gem_twin_bundler_versions_diverge.7c84ba8.Tests (red → green)
hosted_memory_engine::a_bundler4_custom_lockfile_is_refusede2e_redirect_gem_build::gem_hosted_bundler4_custom_lockfile_redirects_nothing(real Bundler 4.0.17)redirected: 1, rewrittenFiles: [Gemfile, Gemfile.lock], vex statements: 1bundle installof the untouched project succeedsruby_crawler::loaded_manifest_reads_the_lockfile_setting(disk, no leftover lock; env vs config priority;BUNDLE_IGNORE_CONFIG)manifest::with_lockfile_accepts_only_the_pairs_own_lock(global tier, shadowed by the app config) andruby_crawler::loaded_manifest_reads_the_global_config_below_local_and_env(bundle config set --global lockfile)vendor::gem::a_bundler4_custom_lockfile_is_refusedhosted_memory_engine::a_lockfile_setting_naming_the_default_lock_is_wired(control)hosted_memory_engine::a_bundler1_twin_wires_the_gemfile_pairhosted_memory_engine::a_twin_with_diverging_bundler_majors_is_refusedhosted_memory_engine::a_bundler2_twin_still_wires_gems_rb(control)e2e_redirect_gem_build::gem_hosted_bundler1_twin_wires_the_gemfile_and_installsmanifest.rsunit tests (with_lockfile_*,default_twin_manifest_*,config_lockfile_*),bundled_with_major_reads_the_version_lineLocal results
cargo clippy --workspace --all-features -- -D warnings: clean.cargo fmt --all -- --checkis not clean onmainitself (≈500 diffs, and CI has no fmt gate), so I formatted only the hunks I touched.cargo test --workspace --all-features --lib --bins: the core lib has 4848 passed and 4 failed. All four (copy_tree::relax_loop_must_not_traverse_symlinked_root,vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry,pypi_poetry::wire_write_failure_*,pypi_requirements::wire_failure_rolls_back_*) are chmod-based write-failure tests that can't fail writes when run as root (uid 0) in this sandbox. They don't touch gem code.hosted_memory_engine33/33,hosted_memory_parity,in_process_vendorande2e_redirect_gem_stale_installall ok.in_process_redirecthas 104 passed and 3 failed, again only the chmod-based write-failure tests (root sandbox).e2e_redirect_gem_build --ignored(real Bundler 4.0.17): new custom-lockfile, dual-boot and gems.rb arms pass.cargo test --workspacelocally because the sandbox's disk allowance runs out building every integration-test binary. CI runs the full suite.Merge with
main(d502a07)maingained #577 (Bundler's global config, #621) while this PR was open, and the two overlapped inLoadedManifest. The conflicts are resolved by keeping both. Because Bundler 4 readslockfilethroughBundler.settings, which includes the global file,with_lockfilenow takes the global tier too. Without that,bundle config set --global lockfile custom.lockwould have slipped past the #749 guard.BundlerEnvcarries the global config path, and #621's call sites use it. After the merge:cargo clippy --workspace --all-features -- -D warningsis clean; the gem/ruby core lib tests (253),crawler_ruby_e2e(26),hosted_memory_engine(34),hosted_memory_parity,in_process_vendor(106),e2e_redirect_gem_stale_install, and real-Bundlere2e_redirect_gem_build --ignored(17) all pass. The full core lib has 5010 passing and the same 4 chmod-based failures from running as root here.Bugbot follow-up (
9e7af6e)BUNDLE_LOCKFILE, an unsupportedBUNDLE_GEMFILE, or a diverging twin) left lock inventory empty with no warning. Inventory now adds agem_lock_unsupporteddiagnosis, which scan and the in-memory engine report as a run-levelwarnings[]entry (documented in CLI_CONTRACT.md). Test:lock_inventory::tests::gem_inventory_diagnoses_a_lock_it_cannot_read(red→green). Thehosted_memory_enginerefusal tests assert the warning again.BUNDLE_LOCKFILEshadows the tiers below it, asSettings#[]does, so an empty env or app-config value now clears a global custom lock.Notes / follow-ups
LoadedManifest::pair. Once both land, a Bundler 1.x twin's post-install VEX should usedefault_twin_manifesttoo. This PR doesn't touch those readers, to avoid conflicting with Fix gem lock readers ignoring gems.locked (#736) #750.lockfile "custom.lock"DSL (issue matrix row 4) is Ruby code the model can't read. Bundler 4.0.17 itself fails frozen installs on that shape before any scan.BUNDLE_LOCKFILEsetting is refused even on Bundler < 4 (which ignores it). That's a conservative, fail-closed choice.🤖 Generated with Claude Code
https://claude.ai/code/session_018ULgrJMQMWEBAsiuprY449
Note
Medium Risk
Changes which lockfiles get rewritten in hosted/vendored gem mode and when scans attest patches; wrong behavior previously caused false success and broken frozen installs, but lockfile edits remain high-impact for Ruby projects.
Overview
Hosted and vendored gem flows now follow the manifest/lock pair Bundler actually loads, instead of rewriting ignored files while reporting success.
LoadedManifestgainswith_lockfile(Bundler 4BUNDLE_LOCKFILEfrom env → app → global config) anddefault_twin_manifest(uses both locks’BUNDLED WITHmajors to chooseGemfilevsgems.rbunder default discovery). Unsupported custom lockfiles refuse withredirect_gem_bundle_lockfile_unsupported/ vendoredgemfile_not_loaded; diverging twin majors refuse withredirect_gem_twin_bundler_versions_diverge.Lockfile-only scan no longer treats unreadable gem layouts as “no gems”: inventory emits
gem_lock_unsupportedwhen gem files exist but no lock socket-patch can read.Docs (
CLI_CONTRACT.md,ecosystems.md), hosted engine candidate filtering, VEX discovery messaging, and e2e/memory tests cover the new behaviors. Minor unrelated refactor: sharedsha1_hex_of/sha256_hex_ofhelpers in a few JVM/Gradle paths.Reviewed by Cursor Bugbot for commit cf746ec. Configure here.
Generated by Claude Code