Skip to content

fix(vendor): fall back to local build when the served gem stub gemspec is invalid - #221

Merged
Mikola Lysenko (mikolalysenko) merged 2 commits into
mainfrom
fix/vendor-gem-stub-hardening
Aug 19, 2026
Merged

fix(vendor): fall back to local build when the served gem stub gemspec is invalid#221
Mikola Lysenko (mikolalysenko) merged 2 commits into
mainfrom
fix/vendor-gem-stub-hardening

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Update: #217 merged; this branch is rebased onto main (the two fix commits replayed cleanly, stale base commits dropped) and re-validated on the rebased tip: hermetic suites green (vendor::gem 67/67, in_process_vendor 37/37, repair_vendor_e2e 26/26) and the live gem_bundler_vendored_install_proof leg passes against production.

Defect (D4, gem live-matrix campaign 2026-08-19 — verified 6/6 live, bundler 1.17/2.7/4.0)

Production's gem-stub-gemspec secondary artifact (served with every prebuilt patched .gem) is INVALID: it omits the rubygems-required summary and authors attributes (and licenses). The CLI sha512-verified the stub and wrote it VERBATIM as the vendored path-source <name>.gemspec with no semantic validation, and --vendor-source auto prefers the service artifact. Every bundler major validates path-source gemspecs, so any post-vendor or fresh-checkout bundle install exits 1:

The gemspec at .../.socket/vendor/gem/<uuid>/activestorage-6.0.3/activestorage.gemspec is not valid … 'missing value for attribute summary'

Control: --vendor-source build writes a valid locally-derived stub and installs green — the defect is isolated to the served artifact. A depscan-side PR fixing the stub generator is in flight, but ALL currently-published gem stubs are invalid, so the CLI needs this defense-in-depth now.

Fix

An INVALID served stub follows the existing MISSING-stub policy (vendor_prebuilt_stub_missing), under its own code (additive/MINOR):

  • --vendor-source auto: loud vendor_prebuilt_stub_invalid warning naming the missing attributes, then fall back to the local build (installed gem + locally derived stub).
  • --vendor-source service (explicit): refuse with vendor_prebuilt_stub_invalid, naming the missing attributes and the remedy, before anything is written — no partial artifacts.

Validation is a conservative textual heuristic (assignment-line presence for summary / authors (or the author = alias), obviously-empty spellings — "", [], nil, .freeze-d variants — rejected; no ruby parsing). A legitimate stub always passes and is still written byte-verbatim. A missing licenses is only mentioned in the message (rubygems warns, not fails). CLI_CONTRACT.md gains the new code in the --vendor-source fallback ladder and the PatchAction vocabulary table.

Tests

Hermetic (wiremock, RED→GREEN)vendor::gem::tests:

  • service_stub_invalid_auto_falls_back_to_build: local-build fallback, LOCAL stub on disk (never the invalid served bytes), loud warning naming summary/authors.
  • service_stub_invalid_service_mode_hard_fails: refusal vendor_prebuilt_stub_invalid, no partial artifacts, lock untouched.
  • service_success_extracts_gem_and_wires_lock: valid stub (fixture now carries the required attrs, as real converter stubs will post-fix) written byte-verbatim — existing behavior pinned.
  • required_attrs_heuristic: unit table for the textual check's spellings (.freeze, %w[], spec./s. receivers, author = alias, empty/nil/commented/== non-assignments).

Live regression leg (this PR's capstone) — the gem leg of e2e_vendored_production.rs is upgraded to gem_bundler_vendored_install_proof: full fresh-dir frozen bundle install delivery proof, failure tolerance and SOCKET_PATCH_VENDORED_E2E_GEM_STRICT deleted. It passes against real production TODAY because auto detects the still-invalid served stub and falls back to the local build; once the depscan fix deploys and artifacts rebuild, the same leg exercises the service artifact directly with no test change.

running 2 tests
test gem_bundler_vendored_install_proof ... ok
test preflight_required_patches_are_published ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 8.87s

Live envelope evidence (real production, anonymous proxy):

  • auto: applied: 1, event vendor_prebuilt_stub_invalid — "the served stub gemspec for activestorage is invalid: it never assigns the rubygems-required attribute(s) summary, authors (it also omits licenses, a rubygems warning); … building locally instead"; the vendored gemspec carries real summary/authors.
  • service: failed: 1 with vendor_prebuilt_stub_invalid and the --vendor-source=auto remedy; .socket/vendor never created.

Deviation from the prepared leg

The prepared upgraded leg installed to a BUNDLE_PATH outside the project, which the gem crawler cannot see (it probes project-local vendor/bundle/<engine>/*/gems/, then gem env homes) — so the auto fallback had no local stub source and the leg failed with gem_spec_missing. The leg now installs in bundler's deployment layout (vendor/bundle inside the project, app config still outside so no .bundle/config joins the committable set), making the install crawler-visible so the fallback can derive the local stub. The fresh-dir delivery proof still copies committable files only.

Gates

  • cargo test -p socket-patch-core --lib — 2385 passed.
  • cargo test -p socket-patch-cli --test e2e_vendor_gem_build -- --ignored (host capstone, real bundler 4.0.15) — 2 passed.
  • rustfmt --check on touched .rs files; cargo clippy -D warnings on touched targets — clean.
  • No patch/redirect rewriter touched.

Review round (adversarial review, applied on adacadd)

A verified-findings fix round, rebased on #217's review round 2 (63531d9):

  1. SECURITY — gem-home guard: the local-stub derivation walked two parents up from installed_dir unconditionally; for an auto-fetch staging dir that escapes the private tempdir into the SHARED temp root, making $TMPDIR/specifications/<leaf>.gemspec a predictable, attacker-plantable path whose contents would be committed and eval'd as Ruby. Now derived only when the parent is a literal gems/ dir; test plants a valid spec at the old target and proves it is never consumed.
  2. Scanner rewrite, empirically aligned: no more comment-stripping (it truncated inside string literals — s.summary = "#1 Ruby web server" was judged missing); anchored line-start assignment scan instead. The emptiness bar now matches what rubygems 3.3/3.5/3.6 actually hard-fail (verified in the bundler 1.17/2.7/4.0 era docker images): summary fails only when never assigned (nil/"" are writer-coerced), authors fails when never assigned or collapsing to no String elements ([], nil, [nil], %w[]; [""] passes).
  3. Both arms validated at the write choke point: the locally-derived stub is now validated too, refusing gem_spec_invalid naming the file (new vocabulary row); local-stub fixtures corrected to be genuinely valid.
  4. Truthful dead-end: auto + invalid served stub + gem not installed now refuses vendor_prebuilt_stub_invalid naming the served defect and the install-the-gem remedy — previously a circular gem_spec_missing ("use service" ↔ service says "use auto") with the D4 diagnostic lost (Refused carries no warnings).
  5. Heal pre-fix victims: the idempotent hot path re-validates the ON-DISK vendored stub; a pre-hardening invalid stub routes into the artifact rebuild instead of a silent already_vendored.
    6.–10. e2e leg robustness: ambient BUNDLE_*/GEM_*/RUBYOPT scrub + USE_FREEDESKTOP_PLACEHOLDER=true on bundler calls; canonicalized fresh-dir provenance + pristine-bytes comparison; a route-attribution assertion (exactly one of vendor_prebuilt_downloaded / vendor_prebuilt_stub_invalid) that auto-retires the fallback expectation when the depscan fix deploys; purl-scoped applied/failed/idempotency/revert assertions; stale doc comments fixed.
  6. Dedupe: one shared attr_mention line-scanner for the extensions and attr checks; the miss closure widened instead of re-implemented; alias fan-out (authors/author, licenses/license) folded into one helper.

Semver note: the new --vendor-source service refusal flips a previously-exit-0 path — but that exit-0 wrote a stub bundler rejects, i.e. an uninstallable project (the D4 defect). The refusal is the bug fix, so this rides a MINOR (also stated in CLI_CONTRACT.md).

Noted, deliberately deferred:

  • Dependency-declaration completeness of the stub is NOT validated: a frozen install also requires the stub's dependency set to match the lock's PATH specs. The current production stub does emit deps, so this is not live-broken; flagged as a follow-up and noted to the depscan stub-generator fix.
  • Download ordering: the .gem archive is fetched + verified before the stub is validated, so an invalid stub costs one wasted archive download. Left as-is deliberately (reordering the artifact fetches is not worth the churn).

Gates re-run on the rebased tip: core lib 2390 passed; repair_vendor_e2e 26, in_process_vendor 37, e2e_vendor_gem_build (host capstone) 2; fmt + clippy -D warnings clean; live leg (preflight_required_patches_are_published + gem_bundler_vendored_install_proof) green against production, exercising the auto fallback on the real invalid stub with the new route-attribution assertion.

🤖 Generated with Claude Code


Note

Medium Risk
Changes gem vendor acquisition and lockfile wiring when service stubs are invalid; behavior is scoped to gem --vendor-source paths with explicit fallback/refusal, but incorrect heuristic could wrongly reject valid stubs or miss bad ones.

Overview
Adds defense-in-depth for gem vendoring when the service’s gem-stub-gemspec is missing required rubygems fields (summary / authors). Before writing the served stub, the gem vendor backend runs a textual assignment-line check (no Ruby parsing); valid stubs are still written byte-verbatim.

--vendor-source auto: loud vendor_prebuilt_stub_invalid warning naming missing attributes, then local build fallback (same posture as a missing stub). service: hard refuse with vendor_prebuilt_stub_invalid before any artifacts are written.

Contract & tests: CLI_CONTRACT.md documents the new code in the vendor-source ladder and PatchAction table. Production e2e upgrades from tolerant gem_bundler_vendored_known_platform_defect to full gem_bundler_vendored_install_proof (frozen fresh-dir bundle install); passes today via auto fallback while production stubs remain invalid. Hermetic wiremock tests cover service refusal, auto fallback, and the heuristic unit cases.

Reviewed by Cursor Bugbot for commit d8ba029. 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 prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Invalid stub fallback drops service gem
    • Modified gem_service_copy to synthesize missing required attributes (summary/authors) in invalid stubs instead of falling back, ensuring the integrity-verified .gem is extracted and used for both auto-fetched gems and service mode.

Create PR

Or push these changes by commenting:

@cursor push 03e457eab3
Preview (03e457eab3)
diff --git a/crates/socket-patch-core/src/vendor/gem.rs b/crates/socket-patch-core/src/vendor/gem.rs
--- a/crates/socket-patch-core/src/vendor/gem.rs
+++ b/crates/socket-patch-core/src/vendor/gem.rs
@@ -780,14 +780,13 @@
     // generator omitted the rubygems-required `summary`/`authors`, and every
     // bundler major validates path-source gemspecs — writing such a stub
     // verbatim makes every later `bundle install` exit 1 (`missing value for
-    // attribute summary`). An INVALID stub follows the MISSING-stub policy
-    // (fall back under `auto`, refuse under `service`) but under its own
-    // `vendor_prebuilt_stub_invalid` code, and always loudly — the served
-    // artifact is defective, not merely absent. Nothing has been written yet,
-    // so the refusal leaves no partial artifacts.
+    // attribute summary`). When the stub is invalid, synthesize the missing
+    // required attributes so the verified `.gem` can be extracted and used —
+    // falling back would discard the integrity-checked archive and fail for
+    // auto-fetched gems with no local `spec_text`.
     let stub_text = String::from_utf8_lossy(&stub);
     let missing_attrs = gemspec_missing_required_attrs(&stub_text);
-    if !missing_attrs.is_empty() {
+    let stub = if !missing_attrs.is_empty() {
         let licenses_note = if gemspec_assigns_attr(&stub_text, "licenses")
             || gemspec_assigns_attr(&stub_text, "license")
         {
@@ -795,28 +794,31 @@
         } else {
             " (it also omits `licenses`, a rubygems warning)"
         };
-        let reason = format!(
-            "the served stub gemspec for {name} is invalid: it never assigns the \
-             rubygems-required attribute(s) {}{licenses_note}; bundler validates \
-             path-source gemspecs, so vendoring it would make every later \
-             `bundle install` fail",
-            missing_attrs.join(", "),
-        );
-        if cfg.source.requires_service() {
-            return hard(
-                "vendor_prebuilt_stub_invalid",
-                format!(
-                    "{reason}. Re-run with --vendor-source=auto (or build) to vendor from \
-                     the locally installed gem until the service artifact is rebuilt"
-                ),
-            );
-        }
         warnings.push(VendorWarning::new(
             "vendor_prebuilt_stub_invalid",
-            format!("{reason}; building locally instead"),
+            format!(
+                "the served stub gemspec for {name} is invalid: it never assigns the \
+                 rubygems-required attribute(s) {}{licenses_note}; synthesizing the \
+                 missing attributes to vendor the integrity-verified .gem",
+                missing_attrs.join(", "),
+            ),
         ));
-        return GemServiceCopy::FallBack;
-    }
+        // Synthesize a valid stub by injecting minimal values for the missing
+        // required attributes. Insert them before the closing `end` of the
+        // Gem::Specification.new block (the stub's last non-empty line).
+        let mut lines: Vec<String> = stub_text.lines().map(|s| s.to_string()).collect();
+        if let Some(pos) = lines.iter().rposition(|line| line.trim() == "end") {
+            if missing_attrs.contains(&"authors") {
+                lines.insert(pos, "  s.authors = [\"(unknown)\".freeze].freeze".to_string());
+            }
+            if missing_attrs.contains(&"summary") {
+                lines.insert(pos, "  s.summary = \"(patched gem)\".freeze".to_string());
+            }
+        }
+        lines.join("\n").into_bytes()
+    } else {
+        stub
+    };
 
     // Extract the patched `.gem`'s data.tar.gz into a clean copy dir, then add
     // the stub as `<name>.gemspec` (a `.gem`'s data.tar.gz never carries one —
@@ -4490,11 +4492,10 @@
     }
 
     /// D4 (gem live-matrix 2026-08-19): explicit `service` mode + a served stub
-    /// that never assigns the rubygems-required `summary`/`authors` refuses
-    /// with its own `vendor_prebuilt_stub_invalid` code, naming the missing
-    /// attributes — writing it verbatim would make every later `bundle install`
-    /// exit 1 (all bundler majors validate path-source gemspecs). No partial
-    /// artifacts are left and the lock is untouched.
+    /// that never assigns the rubygems-required `summary`/`authors` synthesizes
+    /// the missing attributes and proceeds with the integrity-verified .gem.
+    /// The vendored gemspec must contain the synthesized attributes to pass
+    /// bundler's path-source validation.
     #[tokio::test]
     async fn service_stub_invalid_service_mode_hard_fails() {
         let (_tmp, root, installed, blobs, record) = fixture(GEMFILE_DIRECT, LOCK_DIRECT).await;
@@ -4521,27 +4522,37 @@
             )),
         )
         .await;
-        let (code, detail) = unwrap_refused(outcome);
-        assert_eq!(code, "vendor_prebuilt_stub_invalid");
+        let (result, entry, warnings) = unwrap_done(outcome);
+        assert!(result.success, "service mode must synthesize: {:?}", result.error);
+        assert!(entry.is_some());
+        assert_eq!(tokio::fs::read(copy_lib(&root)).await.unwrap(), PATCHED);
+        let gemspec = tokio::fs::read_to_string(copy_gemspec(&root))
+            .await
+            .unwrap();
         assert!(
-            detail.contains("summary") && detail.contains("authors"),
-            "the refusal must name the missing attributes: {detail}"
+            gemspec.contains("s.summary = \"(patched gem)\".freeze"),
+            "gemspec must contain synthesized summary: {gemspec}"
         );
-        assert!(!root.join(format!(".socket/vendor/gem/{UUID}")).exists());
-        // The lock is untouched.
-        assert_eq!(
-            tokio::fs::read_to_string(root.join(GEMFILE_LOCK))
-                .await
-                .unwrap(),
-            LOCK_DIRECT
+        assert!(
+            gemspec.contains("s.authors = [\"(unknown)\".freeze].freeze"),
+            "gemspec must contain synthesized authors: {gemspec}"
         );
+        let warning = warnings
+            .iter()
+            .find(|w| w.code == "vendor_prebuilt_stub_invalid")
+            .expect("must warn about the invalid served stub");
+        assert!(
+            warning.detail.contains("summary") && warning.detail.contains("authors"),
+            "the warning must name the missing attributes: {}",
+            warning.detail
+        );
     }
 
-    /// D4 under the default `auto`: an INVALID served stub is treated exactly
-    /// like a MISSING one — fall back to the LOCAL build (installed gem +
-    /// locally derived stub) — but with a LOUD `vendor_prebuilt_stub_invalid`
-    /// warning naming the served-stub defect. The vendored copy must carry the
-    /// valid local stub, never the invalid served bytes.
+    /// D4 under the default `auto`: an INVALID served stub synthesizes the
+    /// missing rubygems-required attributes and uses the integrity-verified
+    /// service .gem. This ensures auto-fetched gems (without local install)
+    /// can be vendored successfully. A LOUD `vendor_prebuilt_stub_invalid`
+    /// warning names the served-stub defect.
     #[tokio::test]
     async fn service_stub_invalid_auto_falls_back_to_build() {
         let (_tmp, root, installed, blobs, record) = fixture(GEMFILE_DIRECT, LOCK_DIRECT).await;
@@ -4565,20 +4576,24 @@
         )
         .await;
         let (result, entry, warnings) = unwrap_done(outcome);
-        assert!(result.success, "auto must fall back: {:?}", result.error);
+        assert!(result.success, "auto must synthesize: {:?}", result.error);
         assert!(entry.is_some());
         assert_eq!(tokio::fs::read(copy_lib(&root)).await.unwrap(), PATCHED);
-        assert_eq!(
-            tokio::fs::read_to_string(copy_gemspec(&root))
-                .await
-                .unwrap(),
-            GEMSPEC,
-            "the vendored gemspec must be the LOCAL stub, not the invalid served bytes"
+        let gemspec = tokio::fs::read_to_string(copy_gemspec(&root))
+            .await
+            .unwrap();
+        assert!(
+            gemspec.contains("s.summary = \"(patched gem)\".freeze"),
+            "gemspec must contain synthesized summary: {gemspec}"
         );
+        assert!(
+            gemspec.contains("s.authors = [\"(unknown)\".freeze].freeze"),
+            "gemspec must contain synthesized authors: {gemspec}"
+        );
         let warning = warnings
             .iter()
             .find(|w| w.code == "vendor_prebuilt_stub_invalid")
-            .expect("auto fallback must warn loudly about the invalid served stub");
+            .expect("auto mode must warn loudly about the invalid served stub");
         assert!(
             warning.detail.contains("summary") && warning.detail.contains("authors"),
             "the warning must name the missing attributes: {}",

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d8ba029. Configure here.

Comment thread crates/socket-patch-core/src/vendor/gem.rs Outdated
Base automatically changed from test/gem-e2e-restore to main August 19, 2026 19:17
…c is invalid

Defense-in-depth for defect D4 from the 2026-08-19 gem live-matrix campaign
(verified 6/6 live across bundler 1.17/2.7/4.0): production's gem-stub-gemspec
secondary artifact omits the rubygems-required `summary` and `authors`
attributes (and `licenses`). The CLI sha512-verified the stub and wrote it
verbatim as the vendored path-source `<name>.gemspec`, and every bundler major
validates path-source gemspecs, so any post-vendor or fresh-checkout
`bundle install` exited 1 with `missing value for attribute summary`. A
depscan-side fix for the stub generator is in flight, but every
currently-published gem stub is invalid, so the CLI hardens now.

An INVALID served stub now follows the existing MISSING-stub policy, under its
own code (additive/MINOR):

- `--vendor-source auto`: loud `vendor_prebuilt_stub_invalid` warning naming
  the missing attributes, then fall back to the local build (installed gem +
  locally derived stub);
- `--vendor-source service` (explicit): refuse with
  `vendor_prebuilt_stub_invalid`, naming the attributes and the remedy, before
  anything is written (no partial artifacts).

Validation is a conservative textual heuristic (assignment-line presence for
`summary` / `authors`|`author`, obviously-empty spellings rejected — no ruby
parsing); a legitimate stub always passes and is still written byte-verbatim.
A missing `licenses` is only mentioned in the message (rubygems warns, not
fails). CLI_CONTRACT.md documents the new code in the fallback ladder and the
PatchAction vocabulary.

Tests:
- hermetic (wiremock, RED->GREEN): auto+invalid-stub falls back to the local
  build with the loud warning and the LOCAL stub on disk; service+invalid-stub
  refuses with `vendor_prebuilt_stub_invalid` leaving no partial artifacts and
  an untouched lock; the valid-stub byte-verbatim write is pinned (the
  service-success fixture now carries the required attributes); plus a unit
  table for the heuristic's spellings.
- live regression leg: `e2e_vendored_production.rs`'s gem leg is upgraded to
  `gem_bundler_vendored_install_proof` — a full fresh-dir frozen
  `bundle install` delivery proof with the failure tolerance and the
  `SOCKET_PATCH_VENDORED_E2E_GEM_STRICT` knob deleted. It passes against real
  production TODAY via the auto fallback (the served stub is still invalid),
  and exercises the service artifact directly once the depscan fix deploys and
  the artifacts rebuild. The leg installs in bundler's deployment layout
  (`vendor/bundle` inside the project) so the crawler-visible install can feed
  the local-build fallback its stub gemspec.

Verified live against production: scan --mode vendored applied=1 via the
fallback with the `vendor_prebuilt_stub_invalid` event, vendored gemspec
carries real summary/authors; --vendor-source service refuses with the new
code and leaves no .socket/vendor.

Stacked on #217 (test/gem-e2e-restore).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oth-arm stub validation, truthful dead-ends, on-disk heal

Adversarial-review fix round for the D4 stub-hardening PR.

SECURITY (1): the local-stub derivation walked two parents up from
installed_dir unconditionally. For a registry auto-fetch staging dir
(<private tempdir>/<name>-<version>) that escapes into the SHARED temp
root, making $TMPDIR/specifications/<leaf>.gemspec a predictable,
attacker-plantable path whose contents would be committed and eval'd as
Ruby by every later `bundle install`. The derivation now requires
installed_dir's parent to be a literal `gems/` dir (a real gem-home
layout); staging dirs have no local stub. Test plants a valid spec at
the old derivation target and proves it is never consumed.

Scanner rewrite (2): comment-stripping at `#` truncated inside string
literals (s.summary = "#1 Ruby web server" judged missing → valid stubs
refused under service). The scanner now examines raw lines anchored to
the line start (receiver ident + .attr + assignment), where a preceding
comment marker is impossible. The emptiness policy is now EMPIRICAL,
verified against rubygems 3.3/3.5/3.6 in the bundler 1.17/2.7/4.0 era
images: summary hard-fails only when never assigned (nil/"" are
writer-coerced, warning at most); authors hard-fails when never
assigned or collapsing to no String elements ([], nil, [nil], %w[] —
while [""] passes). The old code refused rubygems-tolerated stubs and
passed %w[] which hard-fails.

Both-arm validation (3): the local-build arm wrote the local stub
verbatim; it now validates at the same write choke point and refuses
`gem_spec_invalid` naming the file (new CLI_CONTRACT vocabulary row).
The GEMSPEC/GEMSPEC_318/GEMSPEC_PUMA and CLI-suite fixtures now carry
summary+authors like every healthy rubygems-written stub.

Truthful dead-end (4): auto + invalid served stub + gem not installed
used to refuse gem_spec_missing with circular advice ("use
--vendor-source=service" <-> service says "use auto") and the D4
diagnostic never reached the envelope (Refused carries no warnings).
The FallBack variant now carries the served-stub defect; the refusal is
`vendor_prebuilt_stub_invalid`, names the defect, and advises
installing the gem. Tests cover auto and service, both not-installed.

Heal existing victims (5): the idempotent hot path only checked the
vendored gemspec EXISTS, silently re-blessing pre-fix invalid stubs.
copy_ok now re-validates the on-disk stub; invalid routes into the
existing artifact-only rebuild (test: pre-seeded invalid stub on disk →
re-scan rewrites a valid one, pair edit + ledger untouched).

Dedupe (11 + addendum B): one shared attr_mention line-scanner under
both gemspec_declares_extensions and the attr checks; the miss closure
widened with the (hard code, remedy) pair instead of a re-implemented
branch; licenses/license alias fan-out folded into the alias-list
helper; stub_text bound once.

e2e leg robustness (6-10): bundler invocations scrub ambient
BUNDLE_*/GEM_*/RUBYOPT (sibling-suite pattern) and set
USE_FREEDESKTOP_PLACEHOLDER=true (mimemagic shared-mime-info hazard,
mirrors #217 round 2); delivery proof asserts canonicalized
starts_with(fresh-dir) provenance and compares installed bytes against
captured pristine bytes; a route-attribution assertion requires exactly
one of {vendor_prebuilt_downloaded, vendor_prebuilt_stub_invalid} so
the leg auto-retires the fallback expectation when the depscan stub fix
deploys; applied/failed/idempotency/revert assertions are scoped to the
activestorage purl so future catalog additions cannot red the leg;
stale doc comments fixed.

Contract (addendum A): documented why the service-mode refusal on an
invalid stub rides a MINOR — the prior exit-0 wrote a stub bundler
rejects (an uninstallable project); the refusal is the bug fix.

Rebased on test/gem-e2e-restore @ 63531d9 (PR #217 review round 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 5339111 into main Aug 19, 2026
41 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/vendor-gem-stub-hardening branch August 19, 2026 19:36
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.

2 participants