Skip to content

Fix gem modes wiring a manifest Bundler ignores (#341, #390) - #431

Merged
Mikola Lysenko (mikolalysenko) merged 12 commits into
mainfrom
agent/fix-bundler-manifest-resolution
Oct 1, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 12 commits into
mainfrom
agent/fix-bundler-manifest-resolution

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

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

Fixes #341
Fixes #390

Summary

Gem hosted and vendored modes now wire only the manifest Bundler actually loads. Before this change, both modes could edit a Gemfile that Bundler ignores, report success, and let VEX attest a gem that was installed unpatched.

Root cause

Nothing answered "which manifest does Bundler load?" in one place. The hosted rewriter chose gems.rb or Gemfile by filename alone, and the vendored backend hardcoded Gemfile. Neither read BUNDLE_GEMFILE. Both issues come from that one missing decision, so one shared resolver fixes both:

  • formats::gem::manifest::classify holds the logic, with no I/O (the formats architecture test enforces this).
  • crawlers::ruby_crawler::bundler_loaded_manifest does the environment and app-config reads, next to the existing BUNDLE_PATH config reader.

It follows Bundler's order:

  1. BUNDLE_GEMFILE from the environment.
  2. BUNDLE_GEMFILE: in $BUNDLE_APP_CONFIG/config, else in .bundle/config.
  3. Default discovery.

A relative value from either source is read against the project root, because that is where bundle runs. The user-level ~/.bundle/config is not consulted; the module docs note this as a known limit.

A configured manifest other than Gemfile / gems.rb is refused rather than supported. Hosted rollback, VEX discovery and the lock readers only know the two default lock names (BUNDLER_LOCKS). So a redirect written into Gemfile.next.lock could never be found again to roll back or attest.

Test evidence

Issue Case Test Red without fix Green
#341 vendored: gems.rb twin is refused before any write vendor::gem::tests::gems_rb_twin_is_refused_before_any_write ✅ ✅
#341 real Bundler (premise follows the Bundler era): vendor refuses the twin, all four files untouched e2e_vendor_gem_build::gem_vendor_refuses_a_gems_rb_twin ✅ (exit 0, applied 1) ✅
#341 real Bundler, CHECKSUMS-converged hosted gems.rb pin: get --mode vendored refuses before the takeover reverts it e2e_redirect_gem_build::gem_hosted_gems_rb_pin_survives_a_refused_vendored_takeover ✅ (takeover ran first) ✅
#390 vendored: .bundle/config BUNDLE_GEMFILE: Gemfile.next refused vendor::gem::tests::bundle_gemfile_naming_another_manifest_is_refused ✅ ✅
#390 real Bundler dual boot: vendor refuses, all four files untouched e2e_vendor_gem_build::gem_vendor_refuses_a_bundle_gemfile_dual_boot ✅ ✅
#390 hosted engine: Gemfile.next redirects nothing and warns hosted::engine::tests::bundle_gemfile_naming_another_manifest_redirects_nothing ✅ ✅
#390 hosted engine: BUNDLE_GEMFILE: Gemfile beside gems.rb edits the Gemfile pair hosted::engine::tests::bundle_gemfile_naming_the_gemfile_redirects_it_over_gems_rb ✅ ✅
#390 real Bundler dual boot, scan --mode hosted --vex: nothing redirected or attested, files byte-untouched e2e_redirect_gem_build::gem_hosted_bundle_gemfile_dual_boot_redirects_nothing ✅ (redirected: 1, vex.statements: 1) ✅
#390 (Bugbot) relative env value anchored at the project root, not the process cwd under --cwd formats::gem::manifest::tests::env_wins_over_config_and_is_anchored_at_the_project_root new ✅
#390 (Bugbot) refusal remedy matches the knob (env vs .bundle/config) formats::gem::manifest::tests::unsupported_detail_names_the_knob_that_set_it new ✅
both resolver order, env vs config, other directories, app-config read formats::gem::manifest::tests::*, crawlers::ruby_crawler::tests::loaded_manifest_reads_the_app_config_file new ✅
guard no BUNDLE_GEMFILE: hosted still prefers gems.rb hosted::engine::tests::default_discovery_still_prefers_gems_rb guard ✅

Local runs on ffbe645, with the gem suites re-run on 522febb (Ruby 3.3.6, Bundler 4.0.17):

  • e2e_redirect_gem_build -- --ignored: 8 passed. e2e_vendor_gem_build -- --ignored: 5 passed. These are the full gem capstones, including the existing ones.
  • cargo test -p socket-patch-core --lib gem: 254 passed.
  • cargo test --workspace --all-features --no-fail-fast (ffbe645): 9,340 passed, 12 failed. All 12 are write-failure or permission tests that fail only because the sandbox runs as root (*_write_failure_*, *unremovable*, relax_loop_must_not_traverse_symlinked_root), the same set reported on the other agent PRs. None are in gem code, and CI runs them green.
  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo fmt: the touched files are formatted. main itself isn't fmt-clean, and CI runs no fmt gate.
  • node --test npm/socket-patch/bin/socket-patch.test.mjs: passed. The npm, PyPI and gem wrappers only dispatch, so they need no change.

Docs: docs/ecosystems.md (RubyGems row), CLI_CONTRACT.md (new additive warning code), and a Fixed entry in the CHANGELOG.

🤖 Generated with Claude Code


Note

Medium Risk
Changes which Ruby manifest/lock files are mutated and when VEX attests gems; incorrect behavior previously reported patched state while Bundler installed upstream bytes.

Overview
Gem hosted and vendored modes now target only the manifest Bundler actually loads, fixing false success and VEX attestation when an ignored Gemfile was edited (#341, #390).

A shared resolver (formats::gem::manifest::classify + bundler_loaded_manifest) mirrors Bundler’s order: BUNDLE_GEMFILE from the environment, then .bundle/config, then default discovery (gems.rb before Gemfile on Bundler ≥ 2). Hosted scan narrows redirect candidates to that pair; other configured manifests (e.g. Gemfile.next) redirect nothing and emit redirect_gem_bundle_gemfile_unsupported instead of redirect_gem_no_gemfile. Vendored vendor / takeover refuses with gemfile_not_loaded before any write (and before reverting a live hosted pin), including gems.rb twins and dual-boot layouts.

Docs (CLI_CONTRACT, ecosystems.md, CHANGELOG) and e2e/unit tests cover dual-boot hosted --vex, refused vendored takeover on hosted gems.rb, and real-Bundler vendor refusal paths.

Reviewed by Cursor Bugbot for commit a550b6f. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Vendored gem mode always wired Gemfile and Gemfile.lock. When Bundler
loads a different manifest (a gems.rb twin, or BUNDLE_GEMFILE set in
the environment or in .bundle/config), vendor reported success while
Bundler installed the unpatched gem, and VEX then attested it.

Add one resolver for the manifest Bundler loads, following Bundler's
own order, and refuse vendoring before any write unless that manifest
is the Gemfile.

Refs #341, #390

Assisted-by: Claude Code:claude-opus-5-5
Hosted gem mode picked gems.rb or Gemfile by filename and ignored
BUNDLE_GEMFILE. With 'bundle config set --local gemfile Gemfile.next'
it rewrote Gemfile, reported success and attested the patch in the
in-run VEX, while Bundler kept installing the unpatched gem.

The hosted candidate files now hold only the pair Bundler loads. A
BUNDLE_GEMFILE naming the project's Gemfile or gems.rb selects that
pair; one naming any other file redirects no gem and reports
redirect_gem_bundle_gemfile_unsupported.

Refs #390

Assisted-by: Claude Code:claude-opus-5-5
Real-Bundler e2e capstones for both issues. A hosted scan of a
dual-boot project (BUNDLE_GEMFILE: Gemfile.next in .bundle/config)
redirects and attests nothing. Vendor refuses a gems.rb twin and the
dual-boot layout, leaving every manifest and lock untouched. Docs,
CLI contract and CHANGELOG describe the new refusals.

Refs #341, #390

Assisted-by: Claude Code:claude-opus-5-5
A hosted gems.rb project that is then vendored hit the hosted to
vendored takeover first: the takeover restored the upstream entry,
then vendoring refused gems.rb, leaving the gem unpatched in both
modes. The takeover now runs the gem manifest check first, like the
yarn berry preflight, so a refused gem keeps its hosted pin.

Refs #341

Assisted-by: Claude Code:claude-opus-5-5
Bundler 1.x reads a Gemfile before gems.rb, while 2.x and later load
gems.rb, so the vendored refusal of a gems.rb twin no longer claims
that Bundler always loads gems.rb. The twin is still refused on every
version, because vendor cannot tell which Bundler will install. The
e2e premise now follows the host Bundler, which fixes the 1.17.3 leg.

Refs #341

Assisted-by: Claude Code:claude-opus-5-5
The formats modules are pure by contract, and an architecture test
enforces that. Move the disk and environment reads for the manifest
Bundler loads into the ruby crawler, next to the BUNDLE_PATH config
reader. formats::gem::manifest keeps only the classification.

Refs #341, #390

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 1, 2026 07:32
@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.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/ruby_crawler.rs Outdated
Comment thread crates/socket-patch-core/src/formats/gem/manifest.rs
A relative BUNDLE_GEMFILE was resolved against socket-patch's own
working directory, so a --cwd run classified BUNDLE_GEMFILE=Gemfile as
unsupported. Bundler expands the value in the directory bundle runs
in, which is the project. The refusal also always said to unset the
environment variable; for a .bundle/config setting it now gives the
matching 'bundle config unset --local gemfile' remedy.

Refs #390

Assisted-by: Claude Code:claude-opus-5-5
@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.

Stale Bugbot comment from a previous run.

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

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Ready for review at 522febb3ab2e8460e730fc198a2a6db7bef932da.

  • CI: all checks green on the head (414 check runs: success or skipped, none failing). Up to date with main and mergeable.
  • Bugbot: reviewed 522febb with no new issues. Its two earlier findings on ffbe645 are fixed in 522febb and their threads are resolved: the relative BUNDLE_GEMFILE is now anchored at the project root, and the refusal remedy names the right knob.
  • Reviewer focus: the new shared resolver in crates/socket-patch-core/src/formats/gem/manifest.rs (gems.rb vs Gemfile precedence, env vs .bundle/config), and the new additive warning code in CLI_CONTRACT.md.

Generated by Claude Code

Union the CHANGELOG Fixed entries from both sides.

Co-Authored-By: Claude <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 1, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


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.

Stale Bugbot comment from a previous run.

Keeps both CHANGELOG entries (gem manifest fix and the Poetry venv
fix from #330).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6i4cQ51yarBFnFs2b8HRx
Keeps both CHANGELOG entries (gem manifest fix and the npm non-registry
entry fix from #345).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6i4cQ51yarBFnFs2b8HRx
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


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 a550b6f. Configure here.

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

Copy link
Copy Markdown
Collaborator Author

Burn-down agent check: ready for review at a550b6f1.

  • CI: 97/97 non-skipped checks green on a550b6f (3 skipped)
  • Bugbot reviewed a550b6f and found no new issues. No review threads are open.
  • Reviewers: the last push was a merge of main to resolve a conflict; the fix itself didn't change since it was last reviewed.

Generated by Claude Code

Keeps both new hosted-engine tests: the BUNDLE_GEMFILE cases from this
branch and the Pipfile presence test from #425.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6i4cQ51yarBFnFs2b8HRx
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