Repository navigation
Fix gem pair model ignoring custom lockfile and Bundler 1 twins (#749, #751) - #768
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 |
Resolve hosted/engine.rs: keep main's undecodable_reads retain (#724) alongside this PR's twin-manifest-ambiguous gem refusal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The same failure appeared at the same time on unrelated branches ( I found no fix PR. The proposed one-line patch matches the comment - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
+ uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2I'm not re-running the check, because it will fail the same way until that line changes on main. Generated by Claude Code |
Resolves the CLI_CONTRACT.md conflict by keeping main's new redirect.patches[] text and this branch's two gem warning codes. Assisted-by: Claude Code:claude-opus-5-5
Upstream moved setup-php's v2 tag, so the "# v2" comment on the 2.37.2 commit pin now fails the Audit GitHub Actions check (ref-version-mismatch). Same change as #1118. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] Took this PR over because its heartbeat was stale (03:28Z), the head conflicted with
Local checks: Generated by Claude Code |
A Gemfile + gems.rb twin whose other spelling is a symlink, an unreadable file or not UTF-8 was not seen as a twin by hosted mode, so the readable pair was still pinned although Bundler 2+ loads gems.rb. Lock inventory already counted such a twin and warned gem_lock_unsupported, so the scan both warned and rewrote. The twin check now counts every spelling Bundler sees, readable or not. Found by Bugbot on #768. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
A Gemfile + gems.rb twin whose second spelling is a regular file the scan cannot read (permission denied, I/O error) left no trace in the read lists, so hosted mode still pinned the readable pair. The twin check now also asks the project view whether the file exists, the same File.file? test lock inventory uses. Found by Bugbot on #768. Assisted-by: Claude Code:claude-opus-5-5
Hosted file-set scans see a git symlink Gemfile or gems.rb as a symlink marker with no target. Lock inventory took that as absent, so a Gemfile + gems.rb twin was inventoried from the regular spelling's lock with no gem_lock_unsupported warning, although Bundler follows the link and may load the other pair. The twin check now fails closed on a symlinked spelling. Found by the security review on #768. Assisted-by: Claude Code:claude-opus-5-5
|
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 2ec14fc. Configure here.
Resolve two CLI_CONTRACT.md conflicts by keeping main's new text (the atomic vendored-to-hosted takeover wording from #1039 and the gem_lock_unsupported warning from #768) while re-applying this PR's migration away from the removed spellings: `scan --apply` becomes `scan --mode agent`, and `--apply`/`--vendor` in the lockfile supplement become agent mode / vendored mode. Co-Authored-By: Claude <noreply@anthropic.com>
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 are now refused before anything is written.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.Gemfile+gems.rbtwin under default discovery is refused withredirect_gem_twin_manifest_ambiguous(manifest::twin_manifest_refusal), whatever its locks'BUNDLED WITHsay. Nothing is written or attested.BUNDLED WITHrecords which Bundler wrote a lock, not which one installs it, and the twin's pair depends only on the running major. Lock inventory and VEX discovery read no lock for a twin (gem_lock_unsupported). A spelling counts as present whenever Bundler'sFile.file?may see it, which includes one this run couldn't read (symlinked, unreadable or non-UTF-8).BUNDLE_GEMFILEnaming either spelling still selects that pair, and a loneGemfileorgems.rbis unchanged. This matches vendored mode, which already refused twins. An earlier revision picked the pair fromBUNDLED WITH; see the security-review follow-up below.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_gemfile_gems_rb_twin_is_refused(1.x, 2.x and mixed stamps)engine::twin_redirects_nothing_whatever_the_locks_sayengine::twin_with_an_unreadable_spelling_redirects_nothing(memory symlink / non-UTF-8 spelling)engine::twin_with_an_unreadable_disk_spelling_redirects_nothing(mode-000gems.rb, non-root)lock_inventory::tests::gem_inventory_diagnoses_a_lock_it_cannot_read(symlinked twin spellings)engine::default_discovery_wires_a_lone_gems_rb(control)e2e_redirect_gem_build::gem_hosted_twin_redirects_nothing(every Bundler line; checks the untouched twin still installs frozen)manifest.rsunit tests (with_lockfile_*,config_lockfile_*)Local 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.Security-review follow-ups (
a3d31b9,d44c4fb)BUNDLE_LOCKFILEon a memory view (a3d31b9). The in-memory engine compared the lockfile against/, soBUNDLE_LOCKFILE: "/Gemfile.lock"passed as the project's own lock and the pair was rewritten. An absolute value there is nowUnsupportedLockfile. Test:engine::absolute_bundle_lockfile_redirects_nothing(red→green).BUNDLED WITH(d44c4fb). Every default twin is refused, as described under Fix.Notes / follow-ups
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.Takeover follow-up (hourly agent run, 2026-10-08)
2cb153f: mergesmain. Resolved the CLI_CONTRACT.md conflict by keeping main'sredirect.patches[]text plus this PR's two gem codes.42657c6: setup-php pin comment# v2→# 2.37.2(zizmorref-version-mismatch, the same as Label the setup-php pin in ci.yml with its real tag #1118) for the redAudit GitHub Actionscheck.95f1894(Bugbot): the hosted twin refusal also counts a spelling that's symlinked, unreadable or not UTF-8.d00e81f(Bugbot): the same check also asksview.is_file, so an on-disk spelling with a permission-denied read still counts.2ec14fc(security review): lock inventory's twin check (bundler_loaded_lock_diagnosed_in) fails closed on a memory-view symlinked spelling.completed/success), and every review thread is resolved. The only checks still running are the two non-required Windows gradle hosted cells.🤖 Generated with Claude Code
https://claude.ai/code/session_018ULgrJMQMWEBAsiuprY449