Skip to content

Gem vendor --dry-run eject previews eject_planned (exit 0) for a hosted gem declared in a group block, but the real vendor refuses gemfile_declaration_not_editable (exit 1) #1761

Open

Description

[agent] Found by the scheduled Bundler (RubyGems) bug-hunt routine (ledger #316).

Summary

In a hosted Bundler project whose patched gem is declared inside a group … do block, socket-patch vendor --dry-run (the hosted-pins eject) reports the gem as applied with eject_planned and exits 0. The real vendor then refuses the same gem with gemfile_declaration_not_editable, rolls back (eject_rolled_back), and exits 1. scan --mode vendored --dry-run and get <uuid> --mode vendored --dry-run on the same project correctly preview the refusal (#775). Only the plain vendor eject preview misses it.

Impact

CI or a user that gates on vendor --dry-run is told the eject will succeed, but it can never succeed for this project. The wet run is fail-safe: the project stays hosted and the files are byte-identical. So this is a wrong preview, not data loss.

Expected vs actual

  • Expected (CLI_CONTRACT.md, "Eject a hosted project (v5.0)", step 2): "Backend refusals are also previewed against the restored text in memory … (3) --dry-run stops here". The gem declaration refusal is a backend refusal, so the dry run should report a failed event with gemfile_declaration_not_editable and error.code: "eject_refused" (exit 1), as the wet run's outcome does. scan/get --mode vendored already preview it this way.
  • Actual: the dry run returns status: success, one applied event with errorCode: "eject_planned", and exit 0. The wet run returns partialFailure with gemfile_declaration_not_editable plus the eject_rolled_back warning, and exit 1.

Repro

Use the e2e_redirect_gem_build.rs fixture Driver::ScanVexGroupBlock (real gem build, wiremock patch API with mount_vendored_grant). The project's Gemfile looks like this after the hosted scan:

source "https://rubygems.org"
group :default do
  source "<patch-registry>/gem/<org>/<uuid>/" do
    gem "vuln-gem", "1.0.0"
  end
end

The lock is converged, either through CHECKSUMS (Bundler 2.6+) or through a real unfrozen bundle install on a CHECKSUMS-less lock. Then run:

socket-patch vendor --dry-run --json --yes --api-url $API --org org --api-token fake --patch-server-url $API
# exit 0, events: [{action: applied, errorCode: eject_planned}]
socket-patch vendor --json --yes --api-url $API --org org --api-token fake --patch-server-url $API
# exit 1, events: [{action: failed, errorCode: gemfile_declaration_not_editable}], warning eject_rolled_back
socket-patch scan --mode vendored --dry-run --json ...
# previews gemfile_declaration_not_editable (correct)

A minimal scratch test, appended to e2e_redirect_gem_build.rs, reproduced this on each run, using the helpers already in that file:

let fx = redirect_scanned_project("g", Spelling::Gemfile, false, true, None, Driver::ScanVexGroupBlock).await.unwrap();
mount_vendored_grant(&fx).await;
let (conv, install) = fresh_checkout_bundle_install(&fx); // converge the lock
assert!(install.status.success());
// copy Gemfile + Gemfile.lock + .bundle to a new dir, then run `vendor --dry-run` and `vendor` there

Matrix (Linux, Ruby 3.3.6)

Bundler Lock shape vendor --dry-run vendor scan --mode vendored --dry-run
4.0.18 CHECKSUMS (rubygems.org upstream via SOCKET_RUBYGEMS_URL) eject_planned, exit 0 ❌ refused, exit 1, rolled back —
4.0.18 converged by an unfrozen bundle install eject_planned, exit 0 ❌ refused, exit 1, rolled back refusal previewed ✅
2.5.22 converged by an unfrozen bundle install eject_planned, exit 0 ❌ refused, exit 1, rolled back refusal previewed ✅

The logic doesn't depend on the OS, so macOS and Windows weren't probed. For comparison, a gems.rb twin is correctly refused in both the dry run and the wet run (gemfile_not_loaded), and a top-level declaration ejects, installs frozen with the patched bytes, and vendor --reverts cleanly.

First bad commit

It isn't new in #1648. The parent commit 8244b94 behaves the same way, but its contract said the dry run stops before any backend preview. #1648 (b787454) added the "backend refusals are also previewed" promise and the takeover_dry_refusal preview loop, and gems fall through that loop.

Suspect code

  • crates/socket-patch-cli/src/commands/vendor.rs:1840 (the run_eject preview loop) calls takeover_dry_refusal → dispatch_vendor_one with dry_run = true. The gem backend's dry run doesn't evaluate the Gemfile declaration gate, so the outcome isn't Refused. A debug print showed reverted=["Gemfile","Gemfile.lock"], all staged, and an outcome that isn't Refused.
  • The per-purl takeover handles this case by calling gem_takeover_refusal (manifest gate plus gem_vendor_target_preflight) before the restore (vendor.rs:3307), and scan/get use gem_takeover_preview_refusals. run_eject never calls gem_takeover_refusals_for on its pins.

No probe runs: the logic is OS-independent.

Activity

  1. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p1 (Bundler). Root cause confirmed: run_eject (crates/socket-patch-cli/src/commands/vendor.rs) previews each pin only through takeover_dry_refusal, whose dispatch to the gem backend's dry run does not evaluate the Gemfile declaration gate; it never calls the gem takeover gate (gem_takeover_refusals_for) that scan/get --mode vendored and the per-purl takeover use.

    [agent] Claiming this issue (shared root cause: vendor eject dry run skips the gem takeover gate). Branch: agent/fix-gem-eject-dry-run-gate. Claim-ID: 2026-10-28T03:20:56Z-8756a2


    Generated by Claude Code

  2. mikolalysenko commented on Oct 28, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1762


    Generated by Claude Code

  3. added 2 commits that reference this issue on Oct 28, 2026
    8de500d
    01c0895
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions