Fix gem takeover un-hosting a grouped gem (#775) - #776
Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Vendored mode cannot edit a gem declared inside a `group` block (or any other declaration its line grammar refuses), but the hosted -> vendored takeover only finds that out after it has restored the hosted pin. gem_vendor_target_preflight runs the backend's Gemfile declaration gate on the Gemfile and Gemfile.lock text the restore would leave, without writing anything, so the takeover can refuse first (#775). Assisted-by: Claude Code:claude-opus-5-5
`scan --mode vendored` and `get --mode vendored` over a hosted gem declared inside a `group ... do` block restored its upstream entry and only then hit `gemfile_declaration_not_editable`, so the gem ended up neither hosted nor vendored and the next frozen install loaded the unpatched gem. The dry run promised `would_vendor`. The takeover now asks the gem declaration preflight before the restore: the wet run fails `gemfile_declaration_not_editable` with the hosted Gemfile and Gemfile.lock untouched, and the dry-run preview reports the gem as `would_refuse` with the same code (#775). Fixes #775 Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko)
marked this pull request as ready for review
October 4, 2026 12:15
Collaborator
Author
|
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 ec8f5f1. Configure here.
Collaborator
Author
|
[agent] Ready for review at
Generated by Claude Code |
Tanmay Singla (Tanmay182003)
approved these changes
Oct 5, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #775
Summary
When a hosted gem is declared inside a
group … doblock,scan --mode vendored/get --mode vendoredno longer un-host it and then refuse it. The takeover now asks the gem backend's Gemfile declaration gate before it restores the hosted pin:failed gemfile_declaration_not_editable(exit 1 /partial_failure). The hostedGemfile/Gemfile.lockare byte-untouched, so the gem stays hosted-patched.would_refusewitherrorCode: gemfile_declaration_not_editable(exit 0, the same posture as the Bun/vltwould_refuserows). It no longer sayswould_vendor.socket-patch vendor(eject) already rolled back correctly and is unchanged.Root cause
The hosted→vendored takeover (
vendor_records_reusingincommands/vendor.rs) runsrestore_upstream, which writes the upstreamGemfile+Gemfile.lock, before the gem vendored backend runs. The only gem gate before the restore wasgem_manifest_refusal(a gems.rb twin orBUNDLE_GEMFILE). The declaration gate (plan_gemfile_edit/refuse_append_of_direct_dependency) only ran inside the backend, after the restore had been written. The dry-run preview is a ledger classification with no engine refusals, so it could not see the problem either.Changes
socket-patch-core/src/vendor/gem.rs: newgem_vendor_target_preflight, the gem twin ofyarn_berry_vendor_target_preflight. It runs a dry-runrestore_upstreamof the pin and evaluates the declaration gate on the restoredGemfile/Gemfile.locktext. That way socket-patch's own hostedsource … doblock is never mistaken for the user's declaration. It never writes.socket-patch-cli/src/commands/vendor.rs:gem_takeover_refusal(manifest gate, then the declaration preflight) is used by the takeover loop before the restore.gem_takeover_preview_refusalsdoes the same for the dry-run preview.scan/vendor_flow.rs,scan/mod.rs,get.rs:preview_vendor_jsontakes the resolved takeover refusals and renders them aswould_refuserows (JSON and the human[would-refuse]lines).CLI_CONTRACT.md: new "Gem preflight before the takeover" paragraph under "Takeover reconciliation".No wrapper changes are needed:
npm/,pypi/andgem/only dispatch to the binary.Test evidence
get/scan --mode vendored)e2e_redirect_gem_build::gem_hosted_group_block_pin_survives_a_refused_vendored_takeover(real Bundler 4.0.17)vendor_takeover_reverted_redirectemitted, Gemfile/lock un-hosted. Green:failed gemfile_declaration_not_editable, files byte-identical.dry_run=truelegs forgetandscan"action": "would_vendor". Green:would_refuse+errorCode.vendor::gem::tests::takeover_preflight_*(3 tests: group block refused and nothing written, top-level hosted gem passes, a refused restore is left to the takeover)scan::vendor_flow::preview_tests::preview_marks_a_refused_takeover_would_refuseRed was shown by stubbing
gem_vendor_target_preflightto returnNone. Both the dry legs and the wet leg failed exactly as reported in #775.The e2e respells the mock upstream as
https://rubygems.org/in the hosted pair and serves it throughSOCKET_RUBYGEMS_URL. The takeover restore only re-derives a CHECKSUMS sha256 for a rubygems.org remote, which is the shape of the report.Commands run locally:
cargo clippy --workspace --all-features -- -D warnings: clean.cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build -- --ignored: 12 + 6 passed.cargo test --workspace --all-features: all pass except 16 write-failure tests across 4 binaries. Those depend onchmodmaking files unwritable, which does not hold when running as root (uid 0) in this sandbox. They do not touch the changed code, and CI runs them non-root.mainis notcargo fmt-clean under the pinned toolchain, and CI has no fmt check. Only the hunks this PR changes are rustfmt-formatted, so the diff stays reviewable.Follow-ups (not in this PR)
groupblock and then refuses to vendor it (gemfile_declaration_not_editable), so the project silently goes back to unpatched #775's side note: a top-level declaration whose service has no prebuilt artifact (vendor_prebuilt_required) is still un-hosted before the failure. That is the same "restore written before the vendor step can fail" class as npm lockfileVersion 1: scan/get --mode vendored un-host a hosted patch and then refuse to vendor it, so the project silently goes back to unpatched (vendor eject rolls back correctly) #659 / npm vendored refuses a registry package with vendor_workspace_member whenever a local file: directory (or workspace member) has the same name@version, and the hosted→vendored takeover then un-hosts it, leaving it unpatched #688. A general fix would snapshot the takeover's restore and roll it back on any backend failure, asvendoreject already does. That is a cross-ecosystem change and is better as its own PR.🤖 Generated with Claude Code
https://claude.ai/code/session_0145M3bGaAVYdPzNXXD75tqe
Note
Medium Risk
Changes hosted→vendored takeover ordering for gems and dry-run preview semantics; wrong preflight could block valid takeovers or still allow bad restores, but scope is gem takeover paths with new tests.
Overview
Fixes #775 by running gem vendored-mode checks before a hosted→vendored takeover restores upstream lockfiles, mirroring the existing Bun preflight.
Wet runs (
vendor,scan/get --mode vendored): for hostedpkg:gem/pins, the CLI now runs manifest refusal plus a newgem_vendor_target_preflight(dry-runrestore_upstream, then the Gemfile declaration gate on the restored text). If vendored mode cannot wire the gem—e.g. it lives inside agroupblock—the takeover fails withgemfile_declaration_not_editableand leaves hostedGemfile/Gemfile.lockunchanged instead of un-hosting first.Dry runs:
gem_takeover_preview_refusalsfeeds those refusals intopreview_vendor_json, which emitswould_refuserows (notwould_vendor) for scan/get vendored previews.Docs (
CLI_CONTRACT.md) and e2e/unit tests cover the group-block scenario.Reviewed by Cursor Bugbot for commit ec8f5f1. Configure here.
Generated by Claude Code