fix(gem): production-safe bundler plugin — tolerant bootstrap, honest digest, project-scoped stamp - #178
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c11248c. Configure here.
|
Polish pass for the adversarial review findings (commit e356b1b), all empirically validated against real bundler 4.0.15: Major 1 — stamp-existence bootstrap gate (committed-dir stamp): Reproduced the resurrection first: a stale Major 2 — matrix.json gem row: flipped to Minor (strict message lies): Minor (header claim about deleting the stamp): with the gate rework the original claim is now true — verified with a real Minor (remove leaves stamp orphaned): gem Minor (launcher_guard env scrub): Tests: core 2463 pass; 🤖 Generated with Claude Code |
|
Bugbot finding (Medium, plugins.rb Verification before fixing (host ruby 3.4.10):
Fix: glob a slash-normalized base ( Regression test ( Runs: 🤖 Generated with Claude Code |
d063cb1 to
99f9cd0
Compare
… digest, project-scoped stamp
The generated Bundler setup plugin could deadlock a project on its own
bootstrap, its design comment described trigger behavior bundler does not
have, and its digest stamp was a machine-global file blind to the actual
gem-file state. All trigger claims below were re-derived empirically
against real bundler 4.0.15 (host) and 2.7.2 (docker image).
plugins.rb template (+ published socket-patch-bundler twin):
- [P0] bootstrap deadlock: bundler evaluates plugins.rb at plugin
REGISTRATION, before any project gem is installed; the load-time
SocketPatch.apply! got apply's exit 1 ("No packages found") and raised
Bundler::BundlerError, so the FIRST `bundle install` of every fresh
clone of a setup-wired project died (exit 29 under 4.0.15, exit 1
under 2.7) and every retry failed identically (registration never
completed). Patch failures now warn once per process — naming what
failed and the manual remediation — and let the install continue;
SOCKET_PATCH_STRICT=1 restores the raise. The load-time and per-gem
triggers are additionally stamp-gated so the bootstrap install stays
quiet and defers to the forced after-install-all pass.
- [P1] trigger reality: the header claimed plugins.rb runs during the
Gemfile pass "on EVERY bundle invocation" — false; bundler evaluates a
plugin when a subscribed hook event first fires. Measured surface
(identical on 2.7 and 4.0): every `bundle install` — fresh AND fully
cached — fires before/after-install(-all); `bundle pristine` fires
ONLY the per-gem events; `bundle exec`/`bundle check`/`gem pristine`
fire nothing. The plugin now also subscribes `after-install`
(digest+stamp-gated), which catches `bundle pristine`'s patch
reversion in the same run, and the digest folds in the on-disk CONTENT
of every gem-patch target (resolved from the manifest purls under
Bundler.bundle_path/gems), recomputed after apply — so out-of-band
reversion flips the digest even when every committed input is
byte-identical. Header documents precisely which flows re-apply and
which cannot, including the stale .bundle/plugin/index caveat for
checkouts registered by an older plugin version.
- [P2] stamp location: the digest stamp was a fixed-name file under
Bundler.bundle_path — the interpreter's machine-global gem dir when no
bundle path is configured, shared and clobbered across every
socket-patch project on the host. It now lives at
.socket/gem-plugin-stamp (project-local, excluded from its own digest
inputs); the legacy global stamp is deleted best-effort and never read.
launcher.rb (gem/socket-patch):
- Windows arm now propagates the child's real exit code instead of
collapsing every non-zero exit to 1.
- the binary-cache install is atomic: staged as a temp file in the
destination dir, chmodded, then renamed into place (cross-run race on
Windows rename tolerated when the winner already published).
- first-run failures outside LauncherError exit with a clean one-line
message instead of a raw backtrace; the PowerShell Expand-Archive
fallback quotes paths containing single quotes; `version`'s documented
from-a-checkout fallback never engaged because Gem::MissingSpecError
is a Gem::LoadError (ScriptError family), not a StandardError — found
by the new launcher guard.
- socket-patch-bundler.gemspec: stale `git:` comment corrected to
`path:` (the source has been path: since #150).
setup-matrix driver (gem-scoped, npm-family byte-identical — verified by
diffing the fixtures the old and new driver produce for npm across all
patchsets):
- the gem fixture now serves the REAL git-blob beforeHash probed from
the published .gem (`gem fetch` + `gem unpack`, mirroring
docker_e2e_gem's probe; verified against an independent oracle), so
hash-gated gem apply passes the variant gate without --force. With the
deadlock fix this turns the formerly-gapped gem with-setup docker
cases green: the full 6-case gem matrix passes in BOTH host mode
(bundler 4.0.15, real rubygems.org installs) and docker mode (rebuilt
image, bundler 2.7) — no dependency on any sibling apply change.
Tests (each red without its fix):
- core template invariants: test_plugin_template_failure_policy_and_
stamp_location (new) + test_templates_are_well_formed (extended) pin
the tolerant reporter, strict hatch, stamp constants, legacy cleanup,
target-content digest, and the published twin's parity — 2 failures
against the old template.
- setup_matrix_gem::plugin_runtime drives the plugin generated by the
REAL binary through REAL `bundle install` runs with a fake apply:
first_bundle_install_survives_failing_apply (the P0 repro: red at exit
29 on the old template), strict_mode_fails_bundle_install_on_apply_
failure, successful_apply_stamps_project_scoped (stamp path + exactly
one forced apply per cached install), digest_tracks_gem_file_content_
and_legacy_stamp_is_removed (plain-ruby drive; red on the old
manifest-only digest and old stamp path).
- setup_matrix_gem::launcher_guard drives launcher.rb with host ruby:
windows_branch_propagates_child_exit_code (red: 7 collapsed to 1),
unexpected_download_errors_exit_cleanly (red: raw backtrace),
powershell_quote_doubles_single_quotes and
install_executable_is_atomic_into_place (red: helpers absent).
7 of 8 runtime/launcher guards fail against the base-branch code.
Verified: core 2461/0, cli lib 350/0, setup_matrix_gem 11/11 (incl. the
docker-mode 6-case matrix on a fresh image AND host-mode 4.0.15 run),
docker_e2e_gem 2/2, docker_e2e_vendor_gem 1/1, e2e_gem 11/11 (incl.
live lifecycle), clippy+fmt clean on both crates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ir stamp; gitignore + lifecycle it The require_stamp gate trusted the EXISTENCE of .socket/gem-plugin-stamp — a file living in the directory every other workflow file tells users to COMMIT. Reproduced against bundler 4.0.15: a stale stamp that reaches version control passes the gate at plugin REGISTRATION on a fresh clone, digest-mismatches (targets absent), shells apply, and under SOCKET_PATCH_STRICT=1 resurrects the exact bootstrap deadlock this plugin exists to avoid (exit 29, "Failed to install plugin", no plugin index, every retry identical). Deleting the stamp had the inverse sharp edge: the gated triggers went dead, so `bundle pristine` left the patches reverted until the next `bundle install`. - plugins.rb template + published twin: the bootstrap gate now bails while NONE of the manifest's gem-patch targets exist on disk, reading the live gem tree and never the stamp (which is now a pure digest cache). Registration on a fresh clone stays quiet regardless of stamp state, and pristine heals in the same run even with the stamp deleted — both verified against real bundler 4.0.15. - report_failure: the trailer now states what the ACTIVE mode does — the strict raise says the install is failing because SOCKET_PATCH_STRICT is set, instead of claiming "`bundle install` continues". - setup wires /gem-plugin-stamp into .socket/.gitignore (append-only, sparing user lines) so the stamp never lands in git status or a blanket `git add .socket`; `--check` demands the entry (check/setup agreement); `--remove` best-effort deletes the stamp and strips our line. - matrix.json: the gem row records reality — hook_family bundler-plugin, baseline_supported true — so a future regression of the with-setup flow classifies as blocking regression, not a known gap. - launcher_guard::run_ruby scrubs RUBYOPT/BUNDLE_*/GEM_*/SOCKET_* like plugin_runtime::scrub, so the suite survives `bundle exec`. New pins: plugin_runtime::committed_stale_stamp_does_not_deadlock_strict_ fresh_clone (registration recorded, hook-only failure, retry converges), plugin_runtime::bootstrap_gate_keys_on_target_presence_not_stamp (both gate directions), strict-trailer asserts in the strict-mode test, gitignore and stamp-lifecycle asserts in host_guard + core gem tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dir.glob treats backslash as an escape on EVERY platform, and Bundler.bundle_path carries Windows backslash separators through verbatim (verified: BUNDLE_PATH='vendor\bundle' yields <root>/vendor\bundle/ruby/3.4.0). The platform-install wildcard in patch_target_files (<gems>/<name>-<version>-*/<rel>) therefore escape-ate the separator and matched nothing on Windows: platform installs (nokogiri-1.15.0-x64-mingw-ucrt) dropped out of the digest, so a bundle pristine reversion of them left the stamp matching and the re-apply skipped. Fix: glob a slash-normalized base (forward slashes are valid separators on Windows); the direct non-glob join stays byte-faithful. Applied to both the setup template and the published gem twin, pinned by new needles in the core parity test. Regression test (verified red without the fix): plugin_runtime::backslash_bundle_path_still_digests_platform_gem_files drives the generated plugins.rb with plain ruby under a backslash-bearing BUNDLE_PATH while the real tree lives at the slash spelling (the two-spellings-one-directory situation Windows creates): the platform install must be enumerated as a patch target and its reversion must flip the digest and re-run apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ee9383c to
c954115
Compare
|
Rebased Transplanted exactly this PR's 3 commits via
Conflicts: none — all three commits replayed identically (range-diff shows Verification (all green):
New tip: c954115 🤖 Generated with Claude Code |

STACKED on PR #175 (
fix/gem-bundler-audit) — merge that first; this branch's base is its tip (10c0079).Makes the bundler setup plugin production-safe. Every trigger claim was re-derived empirically against real bundler 4.0.15 (host) and 2.7.2 (the gem docker image); every behavior change is pinned by a test that fails without it (red-run evidence below).
P0 — bootstrap deadlock (fixed)
Bundler evaluates the generated
plugins.rbat plugin registration, before any project gem is installed. The load-timeSocketPatch.apply!got apply's exit 1 ("No packages found") and raisedBundler::BundlerError, so on a fresh clone of a setup-wired project the FIRSTbundle installdied (exit 29 under 4.0.15, exit 1 under 2.7) and every retry failed identically — registration never completed. Reproduced in a scratch project before fixing.Now: patch failures never break
bundle install— they warn once per process, naming what failed and the remediation (socket-patch apply --ecosystems gem), plus a "resolved" note if a later trigger succeeds.SOCKET_PATCH_STRICT=1restores raise-on-failure. The load-time and per-gem triggers are additionally stamp-gated, so a bootstrap install is silent and defers to the forcedafter-install-allpass (the actual patch point).P1 — trigger reality + honest digest (fixed)
The old header claimed plugins.rb runs "on EVERY bundle invocation" during the Gemfile pass — false. Measured surface (identical on 2.7 and 4.0):
bundle install— fresh and fully cached — firesbefore/after-install(-all)and (re-)evaluates plugins.rb;bundle pristinefires only the per-gemafter-installevents;bundle exec/bundle check/ plainruby/gem pristinefire nothing.Changes: the plugin now also subscribes
after-install(digest+stamp-gated), which catchesbundle pristine's patch reversion in the same run (validated on both bundler versions with a mutating fake apply); the digest folds in the on-disk content of every gem-patch target file (resolved from the manifest purls underBundler.bundle_path/gems, absence-marked, recomputed after apply) so out-of-band reversion flips the digest even when every committed input is byte-identical. The header now documents exactly which flows re-apply and which cannot (gem pristine/bundle execheal at the nextbundle install), including the stale.bundle/plugin/indexcaveat: hook subscriptions are recorded at registration, so checkouts registered by an older plugin keep their old subscription set until re-registration (fresh clones/CI always re-register).P2 — stamp location (fixed)
The digest stamp was a fixed-name file under
Bundler.bundle_path— with no bundle path configured, the interpreter's machine-global gem dir, shared and clobbered across every socket-patch project on the host. It now lives at.socket/gem-plugin-stamp(project-local, safe to gitignore/delete, excluded from its own digest inputs). Migration: the legacy global stamp is deleted best-effort and never read.P2 + nits — launcher.rb
File.rename(Windows rename race tolerated when a concurrent run already published).LauncherErrorfirst-run failures (DNS/TLS/...) exit with a clean one-line message instead of a raw backtrace.Expand-Archivefallback quotes paths containing single quotes (powershell_quote,''-doubling).version's documented from-a-checkout fallback never engaged —Gem::MissingSpecErroris aGem::LoadError(ScriptError family), not aStandardError; the rescue now names it.socket-patch-bundler.gemspec: stalegit:comment →path:(true since Structure-review batch: sweep fixes, module taxonomy, npm-family dedup, coverage gaps, CI honesty #150).setup-matrix: gem with-setup cases are GREEN
run-case.shnow serves the real git-blob beforeHash for gem fixtures, probed from the published .gem (gem fetch+gem unpack, mirroring docker_e2e_gem's probe; verified against an independent oracle hash). Combined with the deadlock fix, the formerly-gapped gem docker cases (baseline_with_setup,alt_content_patchset) — and the formerly-redwrong_target_patchset— now pass with no dependency on any sibling apply-side change: the full 6-case gem matrix is green in host mode (bundler 4.0.15, real rubygems.org installs) and docker mode (image rebuilt from this branch, bundler 2.7). npm-family separability: the fixtures the old and new driver produce for npm are byte-identical across all patchsets (diff-verified); non-gem ecosystems keep the zero placeholder. Note for local runs: a gem image built before this branch bakes the old binary (old raising template) and will still red-flag the with-setup cases — rebuild the image or useSOCKET_PATCH_TEST_HOST=1(the stale-image hint is in the harness failure message).Tests (each red without its fix)
setup_matrix_gem::plugin_runtime::first_bundle_install_survives_failing_apply(realbundle install, fake apply exit 1, generated by the real binary)plugin_runtime::strict_mode_fails_bundle_install_on_apply_failureplugin_runtime::successful_apply_stamps_project_scoped(stamp path, 64-hex content, no legacy file, exactly +1 forced apply per cached install)plugin_runtime::digest_tracks_gem_file_content_and_legacy_stamp_is_removed(plain-ruby drive; reversion must re-trigger; legacy stamp deleted)test_plugin_template_failure_policy_and_stamp_location(new) +test_templates_are_well_formed(extended; also pins published-twin parity)launcher_guard::windows_branch_propagates_child_exit_code(win_platform stub, child exits 7)launcher_guard::unexpected_download_errors_exit_cleanly(stubbedNet::HTTP.startraisingSocketError)launcher_guard::powershell_quote_doubles_single_quotes,install_executable_is_atomic_into_placeRed-run evidence: with only the base-branch
plugins.rb.tmpl+launcher.rbrestored, 7 of 8 runtime/launcher guards fail and both core template tests fail; all green with this branch.Verification
setup_matrix_gem11/11 — including the docker-mode 6-case gem matrix on an image rebuilt from this branch and a host-mode (4.0.15) matrix rundocker_e2e_gem2/2,docker_e2e_vendor_gem1/1,e2e_gem11/11 (incl. live lifecycle)Out-of-scope notes (not in my owned files)
CLI_CONTRACT.md§gem hook table and README §705 still describe the old "load-time digest gate + after-install-all" wording and don't mentionSOCKET_PATCH_STRICT; follow-up doc touch-up needed..bundle/plugin/index(after-install-all only) until re-registration;setupdoes not force re-registration (deleting.bundle/plugincould break users' other plugins offline). Documented in the template header instead.wrong_target) repeats on every install by design — the honest signal until the stale record is removed; strict mode makes it fatal.🤖 Generated with Claude Code
Note
Medium Risk
Changes default
bundle installbehavior for all setup-wired Ruby projects (warn vs fail) and re-apply logic; misconfiguration could leave gems unpatched until strict mode or manual apply, but scope is gem/Bundler integration rather than core auth or data paths.Overview
Makes the generated Bundler plugin safe for fresh clones and honest about on-disk gem state, with matching updates to the published
socket-patch-bundlertwin and the RubyGems launcher.Bootstrap / failure policy: Apply failures no longer raise
Bundler::BundlerErrorby default (fixes firstbundle installdeadlock whenplugins.rbruns before gems exist). Failures warn with remediation;SOCKET_PATCH_STRICT=1restores fatal behavior. Load-time and per-gemafter-installtriggers userequire_stampso bootstrap installs defer to forcedafter-install-all.Triggers & digest: Adds digest-gated
after-install(coversbundle pristinein the same run). Digest folds in actual patch-target file bytes under the bundle path, excludes the stamp from its own inputs, and recomputes after apply.APPLY_LOCKmutex avoids concurrent apply children.Stamp: Digest stamp moves to
.socket/gem-plugin-stamp; legacy.socket-patch-gem-stampunderBundler.bundle_pathis deleted best-effort.Tests & matrix: New
plugin_runtimetests (realbundle installwith fake apply) andlauncher_guardtests; setup-matrix gem fixtures use realbeforeHashfrom published gems viaresolve_before_hashinrun-case.sh.Launcher: Windows propagates child exit codes; atomic cache install; clean non-
LauncherErrorfailures; PowerShell path quoting;Gem::LoadErrorfallback forversion.Reviewed by Cursor Bugbot for commit c11248c. Configure here.