Skip to content

Fix gem VEX ignoring out-of-tree bundle path (#709) - #712

Open
Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
agent/fix-gem-config-path-verification
Open

Mikola Lysenko (mikolalysenko) wants to merge 5 commits into
mainfrom
agent/fix-gem-config-path-verification

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #709

Summary

If .bundle/config sets a bundle path outside the project (bundle config set --local path /opt/bundle, ~/.bundle-store, or a Windows D:\... path), hosted gem verification used to treat the gem as not installed. scan --mode hosted gave no stale-install warning, and both the in-run --vex and a later vex marked the advisory not_affected, even though Bundler kept loading the unpatched gem from that path. This PR makes both checks read that path. apply and rollback still never write there.

Root cause

The ruby crawler's containment guard (resolve_config_bundle_path) refuses a config-sourced BUNDLE_PATH that resolves outside the project. That's deliberate: crawled roots are apply write targets, and a committed .bundle/config is untrusted input. But the refusal also hid the root from the two read-only consumers whose job is to catch an unpatched install:

  • the hosted gem stale-install probe (gem_stale_install_warnings), so no redirect_gem_stale_install was raised and the purl stayed in the in-run --vex assume_applied set;
  • vex's installed-copy lookup (find_manifest_package_copies_reusing), so the gem came back package_not_found and the hosted lockfile-basis excuse (vex.rs ~L581) marked it not_affected.

Fix

  • Core (ruby_crawler.rs): BundleStoreDiscovery now also records the resolved root it refused (skipped_config_root). New RubyCrawler::verification_only_gem_paths[_with_env] returns the gem stores under that root, for local mode only. They are deliberately never part of get_gem_paths, which is what apply and rollback write through.
  • Hosted stale probe (scan/hosted.rs): also reads those stores. A stale copy there gets the project-local delete-list remedy, plus the committed vendor/cache archive fold-in, instead of the shared-gem-home caveat. gem_stale_install_warning now takes project_local: bool instead of deriving it from cwd.
  • vex (ecosystem_dispatch.rs): find_manifest_package_copies_reusing is called only by vex, which only reads. It now adds gem copies found under those stores. An unpatched copy fails verification (not_applied/hash_mismatch) and is no longer excused as absent. A patched copy there is accepted as patched.
  • CLI_CONTRACT.md: the config-skip and "Gem stale-install guard" sections now document the read-only behavior.

The npm/pypi/gem wrappers only dispatch to the binary, so they need no changes.

Tests (each new test was seen failing before the fix and passing after)

Issue facet Test Before → after
#709 stale-install probe misses the configured root scan::hosted::tests::gem_stale_probe_reads_refused_out_of_tree_config_path red (0 warnings) → green
#709 standalone vex false not_affected e2e_vex_redirect::gem_hosted_ref_is_verified_under_an_out_of_tree_config_bundle_path red (exit 0, verified) → green (unpatched: exit 1; patched: attests; nothing installed: lockfile basis still attests)
#709 in-run scan --mode hosted --vex false not_affected e2e_redirect_gem_stale_install::gem_hosted_stale_install_under_out_of_tree_config_path_is_not_attested green with fix (the stale warning now fires, so the purl is excluded)
Write paths never see the refused root; ~ spelling; env dedup; global mode; BUNDLE_IGNORE_CONFIG (Bugbot) ruby_crawler::tests::refused_config_root_is_verification_only green

Local runs:

  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test --workspace --all-features --no-fail-fast: all pass except 12 permission-based write-failure tests (vendor/repair/copy_tree/vlt_heal/pypi). Those rely on chmod making files read-only, which root ignores in this sandbox. They are unrelated to this diff and pass in CI.
  • cargo fmt: main itself isn't rustfmt-clean with the pinned 1.93.1, so I only formatted the lines I changed.

Note

Medium Risk
Changes gem VEX and hosted stale-install behavior (attestation-sensitive) but leaves apply/rollback write containment unchanged; scope is limited to read-only Ruby crawler paths.

Overview
Fixes #709: when .bundle/config points BUNDLE_PATH outside the project, the crawler still refuses that root for apply/rollback writes (untrusted committed config), but read-only checks now probe it because Bundler installs and loads gems there anyway.

Core: RubyCrawler records the refused root as skipped_config_root and exposes it via verification_only_gem_paths (not part of get_gem_paths). BUNDLE_IGNORE_CONFIG skips this extra probe.

Hosted scan: The gem stale-install guard unions those stores into discovery; installs under the project’s configured bundle path get the project-local delete-list remedy even when the path sits outside --cwd. gem_stale_install_warning takes an explicit project_local flag.

vex: find_manifest_package_copies_reusing also finds gem copies under verification-only stores so unpatched installs fail verification instead of being treated as absent (and falsely attested via lockfile basis); stale purls stay out of in-run --vex assume_applied.

Docs + tests: CLI_CONTRACT.md documents write vs read behavior; unit and e2e tests cover stale warnings, VEX, and verification-only path rules.

Reviewed by Cursor Bugbot for commit c8d4d3a. Configure here.

Assisted-by: Claude Code:claude-opus-5-5
A .bundle/config "path" outside the project (bundle config set
--local path /opt/bundle) is refused as an install root because
apply writes there. That refusal also hid the root from the
read-only checks, so hosted scan gave no stale-install warning and
both the in-run --vex and a later `vex` attested not_affected while
bundler kept loading the unpatched gem from that path.

The refused root is now exposed as a verification-only store: the
hosted stale-install probe and vex's installed-copy lookup read it,
while apply and rollback still never write there. A stale copy
there gets the project-local remedy.

Fixes #709

Assisted-by: Claude Code:claude-opus-5-5
Covers #709 end to end: a stale gem under a .bundle/config path
outside the project now warns with the project-local remedy, and the
same run's --vex does not attest it.

Assisted-by: Claude Code:claude-opus-5-5
The regression test for #709 was nested inside another test function,
so it compiled but never ran. Move it back to module level.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 3, 2026 18:55
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[agent] native (ubuntu-latest, 2.1.5) (PDM patch compatibility) failed on one cell, space-unicode vendored (refusalCodeReported, 26.8s against 3–6s for its siblings). This PR only changes gem discovery, the hosted gem stale probe and vex's gem copy lookup. It doesn't touch PDM or Python vendoring, and the same workflow is green on other open PRs this afternoon. I've re-run the failed job once (run 37145221768). If it fails again I'll treat it as real and investigate.


Generated by Claude Code

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

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/crawlers/ruby_crawler.rs
With BUNDLE_IGNORE_CONFIG set, bundler reads no config file, so a
.bundle/config path is neither an install root nor a root bundler
loads from. Discovery now skips the app config's BUNDLE_PATH in that
case, the same way the cache-path and Gemfile readers already do, so
leftover gems under that path no longer raise a stale-install
warning or fail VEX.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

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

✅ 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 c8d4d3a. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down agent] Ready for review at head c8d4d3a.

  • CI: 485/485 check runs passed (479 success, 6 matrix/gated jobs skipped by design: e2e-full, e2e-docker, canary, downgrade, and two unexpanded matrix templates). The earlier PDM space-unicode vendored failure passed on re-run.
  • Bugbot: reviewed c8d4d3a, no new issues. Its one earlier finding (BUNDLE_IGNORE_CONFIG not honored) was fixed in c8d4d3a, and the thread is resolved.
  • Merges cleanly into main.
  • For reviewers: the out-of-tree BUNDLE_PATH is only read for verification (stale probe and vex). Check that verification_only_gem_paths never reaches get_gem_paths, the apply/rollback write path.

Slack announcement not sent: this run has no Slack send tool.


Generated by Claude Code

This branch has not been deployed

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

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

2 participants