Skip to content

fix(gem): production-safe bundler plugin — tolerant bootstrap, honest digest, project-scoped stamp - #178

Open
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
fix/gem-bundler-auditfrom
fix/gem-plugin-bootstrap
Open

fix(gem): production-safe bundler plugin — tolerant bootstrap, honest digest, project-scoped stamp#178
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
fix/gem-bundler-auditfrom
fix/gem-plugin-bootstrap

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

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.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 on a fresh clone of a setup-wired project the FIRST bundle install died (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=1 restores raise-on-failure. The load-time and per-gem triggers are additionally stamp-gated, so a bootstrap install is silent and defers to the forced after-install-all pass (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):

  • every bundle install — fresh and fully cached — fires before/after-install(-all) and (re-)evaluates plugins.rb;
  • bundle pristine fires only the per-gem after-install events;
  • bundle exec / bundle check / plain ruby / gem pristine fire nothing.

Changes: the plugin now also subscribes after-install (digest+stamp-gated), which catches bundle 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 under Bundler.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 exec heal at the next bundle install), including the stale .bundle/plugin/index caveat: 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

  • Windows arm propagates the child's real exit code (was: every non-zero collapsed to 1).
  • Binary-cache install is atomic: temp file in the destination dir → chmod → File.rename (Windows rename race tolerated when a concurrent run already published).
  • Non-LauncherError first-run failures (DNS/TLS/...) exit with a clean one-line message instead of a raw backtrace.
  • PowerShell Expand-Archive fallback quotes paths containing single quotes (powershell_quote, ''-doubling).
  • Bonus bug found by the new guard: version's documented from-a-checkout fallback never engaged — Gem::MissingSpecError is a Gem::LoadError (ScriptError family), not a StandardError; the rescue now names it.
  • socket-patch-bundler.gemspec: stale git: 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.sh now 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-red wrong_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 use SOCKET_PATCH_TEST_HOST=1 (the stale-image hint is in the harness failure message).

Tests (each red without its fix)

Pin Test Red on base
P0 deadlock setup_matrix_gem::plugin_runtime::first_bundle_install_survives_failing_apply (real bundle install, fake apply exit 1, generated by the real binary) exit 29, "Failed to install plugin"
strict hatch plugin_runtime::strict_mode_fails_bundle_install_on_apply_failure green on base (base always raised); pins the hatch against the tolerant default
P2 stamp + digest gate plugin_runtime::successful_apply_stamps_project_scoped (stamp path, 64-hex content, no legacy file, exactly +1 forced apply per cached install) stamp under vendor/bundle
P1 content digest + migration plugin_runtime::digest_tracks_gem_file_content_and_legacy_stamp_is_removed (plain-ruby drive; reversion must re-trigger; legacy stamp deleted) manifest-only digest misses reversion; legacy stamp kept
template invariants core test_plugin_template_failure_policy_and_stamp_location (new) + test_templates_are_well_formed (extended; also pins published-twin parity) 2 failures
launcher exit code launcher_guard::windows_branch_propagates_child_exit_code (win_platform stub, child exits 7) got 1
launcher error wrap launcher_guard::unexpected_download_errors_exit_cleanly (stubbed Net::HTTP.start raising SocketError) raw backtrace
ps-quote / atomic install launcher_guard::powershell_quote_doubles_single_quotes, install_executable_is_atomic_into_place helpers absent

Red-run evidence: with only the base-branch plugins.rb.tmpl + launcher.rb restored, 7 of 8 runtime/launcher guards fail and both core template tests fail; all green with this branch.

Verification

  • core 2461/0, cli lib 350/0, clippy + fmt clean on both crates
  • setup_matrix_gem 11/11 — including the docker-mode 6-case gem matrix on an image rebuilt from this branch and a host-mode (4.0.15) matrix run
  • docker_e2e_gem 2/2, docker_e2e_vendor_gem 1/1, e2e_gem 11/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 mention SOCKET_PATCH_STRICT; follow-up doc touch-up needed.
  • Existing wired checkouts keep a stale .bundle/plugin/index (after-install-all only) until re-registration; setup does not force re-registration (deleting .bundle/plugin could break users' other plugins offline). Documented in the template header instead.
  • The plugin's warn-per-process on a manifest that matches nothing (e.g. 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 install behavior 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-bundler twin and the RubyGems launcher.

Bootstrap / failure policy: Apply failures no longer raise Bundler::BundlerError by default (fixes first bundle install deadlock when plugins.rb runs before gems exist). Failures warn with remediation; SOCKET_PATCH_STRICT=1 restores fatal behavior. Load-time and per-gem after-install triggers use require_stamp so bootstrap installs defer to forced after-install-all.

Triggers & digest: Adds digest-gated after-install (covers bundle pristine in 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_LOCK mutex avoids concurrent apply children.

Stamp: Digest stamp moves to .socket/gem-plugin-stamp; legacy .socket-patch-gem-stamp under Bundler.bundle_path is deleted best-effort.

Tests & matrix: New plugin_runtime tests (real bundle install with fake apply) and launcher_guard tests; setup-matrix gem fixtures use real beforeHash from published gems via resolve_before_hash in run-case.sh.

Launcher: Windows propagates child exit codes; atomic cache install; clean non-LauncherError failures; PowerShell path quoting; Gem::LoadError fallback for version.

Reviewed by Cursor Bugbot for commit c11248c. Configure here.

@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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

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.

files.each_key do |key|
rel = key.to_s.sub(%r{\Apackage/}, "")
targets << File.join(gems_dir, "#{name}-#{version}", rel)
targets.concat(Dir.glob(File.join(gems_dir, "#{name}-#{version}-*", rel)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platform gem digest misses on Windows

Medium Severity

patch_target_files builds the platform-gem pattern with File.join and passes it to Dir.glob. On Windows File.join yields backslashes, and Dir.glob treats \ as an escape, so the * never matches. Platform installs (e.g. nokogiri-…-x64-mingw-ucrt) are omitted from the digest, so a bundle pristine reversion can leave the stamp matching and skip re-apply.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c11248c. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

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 .socket/gem-plugin-stamp committed by git add .socket + SOCKET_PATCH_STRICT=1 on a fresh clone gave exit 29, Failed to install plugin, no plugin index, identical retries. Fix is the reviewer's gate rework: apply!'s bootstrap gate (renamed from require_stamp) now bails while none of the manifest's gem-patch targets exist on disk — it reads the live gem tree, never the stamp, which is now a pure digest cache. Registration stays quiet whatever the stamp says; strict enforcement waits for the post-install hooks (where the failure is remediable and registration is already recorded, so retries converge). setup also wires /gem-plugin-stamp into .socket/.gitignore (append-only, user lines spared; --check demands it, keeping check/setup agreement), killing both the git-status churn and the committed-stamp vector. Twin mirrored. Pinned by plugin_runtime::committed_stale_stamp_does_not_deadlock_strict_fresh_clone (registration index present, no Failed to install plugin, hook-only failure, retry with working apply exits 0) — fails on the previous tip.

Major 2 — matrix.json gem row: flipped to hook_family: "bundler-plugin", baseline_supported: true, so a future with-setup regression classifies as blocking regression rather than known_gap. The gem Docker matrix leg ran green with the flip in place.

Minor (strict message lies): report_failure now appends a mode-branched trailer — the strict raise says the install is failing because SOCKET_PATCH_STRICT is set (unset to warn-and-continue); tolerant keeps the old wording. Pinned by new asserts in strict_mode_fails_bundle_install_on_apply_failure.

Minor (header claim about deleting the stamp): with the gate rework the original claim is now true — verified with a real bundle pristine after deleting the stamp: old template never shells apply (patches stay reverted), new one heals in the same run. Header reworded to match; pinned by plugin_runtime::bootstrap_gate_keys_on_target_presence_not_stamp (both gate directions).

Minor (remove leaves stamp orphaned): gem --remove best-effort deletes the stamp and strips our .gitignore line (file deleted when nothing else is left). Pinned in the host_guard remove round-trip + core gem tests.

Minor (launcher_guard env scrub): run_ruby (and the atomic-install test's direct command) now scrub RUBYOPT/BUNDLE_*/GEM_*/SOCKET_* like plugin_runtime::scrub.

Tests: core 2463 pass; setup_matrix_gem 13/13 (incl. the real Docker matrix leg, 76s) ; setup_invariants 29 pass; deno/maven/monorepo/nuget/env_guard matrix suites green. Pre-existing, unrelated to this diff: setup_matrix_npm (stale 3-week image; the documented wrong_target/empty_patchset install_exit=1 bug), setup_matrix_pypi and setup_matrix_composer docker legs fail the same aspirational expectations on this machine — no data-flow from the gem-only diff into those cases.

🤖 Generated with Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Bugbot finding (Medium, plugins.rb patch_target_files platform-gem glob): CONFIRMED and fixed in ee9383c.

Verification before fixing (host ruby 3.4.10):

  • Dir.glob treats \ as an escape on every platform: a pattern base of vendor\bundle escape-eats the separator and matches only a (nonexistent) vendorbundle, so the <name>-<version>-* platform wildcard returns nothing.
  • Bundler.bundle_path carries backslashes through verbatim: BUNDLE_PATH='vendor\bundle'<root>/vendor\bundle/ruby/3.4.0. So on Windows the platform installs (e.g. nokogiri-1.15.0-x64-mingw-ucrt) drop out of the digest and a bundle pristine reversion of them leaves the stamp matching — re-apply skipped, exactly as flagged.

Fix: glob a slash-normalized base (glob_gems_dir = gems_dir.tr("\\", "/")) — forward slashes are valid separators on Windows; the direct non-glob join stays byte-faithful. Applied to BOTH the setup template (crates/socket-patch-core/src/setup/gem/templates/plugins.rb.tmpl) and the published twin (gem/socket-patch-bundler/plugins.rb), with new needles in the core parity test pinning the normalization in each.

Regression test (plugin_runtime::backslash_bundle_path_still_digests_platform_gem_files in setup_matrix_gem.rs): drives the generated plugins.rb with plain ruby under BUNDLE_PATH='vendor\bundle' while the real gem tree lives at the forward-slash spelling — the two-spellings-one-directory situation Windows creates (glob escape semantics are identical everywhere). Asserts the platform install is enumerated as a patch target and that reverting its content flips the digest and re-runs apply. Verified red without the fix (re-apply skipped: 1 call instead of 2, at exactly the flagged failure mode) and green with it.

Runs: cargo test -p socket-patch-core setup::gem (54 passed), SOCKET_PATCH_TEST_HOST=1 cargo test -p socket-patch-cli --features docker-e2e,setup-e2e --test setup_matrix_gem (14 passed), clippy clean on both crates, ruby -c OK on both plugin files.

🤖 Generated with Claude Code

… 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>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Rebased fix/gem-plugin-bootstrap onto the freshly-rebased base branch fix/gem-bundler-audit (new base tip: 99f9cd0, which now carries main's 11 new commits — --vendor-source in vendor_flow, hosted JSON envelope changes, maven/nuget GA, #157/#164/#169).

Transplanted exactly this PR's 3 commits via git rebase --onto origin/fix/gem-bundler-audit d063cb1:

Conflicts: none — all three commits replayed identically (range-diff shows = for each); no behavior-changing judgment calls were needed.

Verification (all green):

New tip: c954115

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant