Fix gem source-option guard missing git sources (#652) - #731
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Hosted scans moved a gem declared with `gitlab:`, a custom `git_source(:name)` key or a string-keyed `"git" =>` option into the Socket source block. The option still overrode the block, so bundler kept loading the unpatched git checkout while scan reported the gem redirected and VEX attested it not_affected. String-keyed options such as `"require" => false` were also silently dropped. Both hosted and vendored modes now read gem options through one shared reader that understands every key spelling and treats any key outside bundler's non-source options as a source. Hosted mode also refuses a gem the lock resolves from a GIT, PATH or plugin section. Fixes #652 Assisted-by: Claude Code:claude-opus-5-5
Adds a real-bundler capstone where the gem comes from a custom `git_source` key: the hosted scan must refuse it, write nothing and attest nothing, and bundler must still install the project. The option reader now also honors backslash escapes in single-quoted strings, so a quote inside a value cannot hide a later git option. Refs #652 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
The new git/path refusal re-parsed Gemfile.lock for every patched gem, which made hosted bundler scans about 15% slower on the bench fixture (800 gems, 20 patched). The lock is now parsed once per rewrite; our own edits only touch GEM sections and CHECKSUMS, so the GIT/PATH membership read up front stays accurate. Refs #652 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[burn-down agent] Ready for review at head
Generated by Claude Code |
Conflicts: kept main's gem_line_tail_blocks_edit (#340) alongside the PR's shared gemfile::source_option reader (dropped main's duplicated token lists), ran the #340 structural check before the fail-closed source-option reader in redirect, and kept both sides' e2e drivers/tests. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
#605 taught the name-keyed npm resolver to probe bundled store trees, so it now finds aliased copies (node_modules/lp) and a nested host's store peers itself. Two vex_consumed tests from #738 assumed that set never held aliases, so main's CI went red after both merged. The tests now feed the alias-free set explicitly to keep covering alias expansion, and also check the resolver's own set reaches the same copies with no duplicates. No production code changes. Assisted-by: Claude Code:claude-opus-5-5 (cherry picked from commit 40dac07)
|
[agent] CI was red on 99a6c95 ( The cause is outside this PR. Both tests also fail on What I pushed in c7d18c9:
Checked locally on c7d18c9:
Generated by Claude Code |
|
BugBot review Generated by Claude Code |
The PDM matrix runs against production PyPI and the public patch API. Over the last 7 days 25 pdm-compatibility runs failed on one random cell each, on unrelated PRs. The version, OS, shape, mode and check differed every time (rescanIdempotent, appliedExactlyOne, rescanAfterRelockApplies, ...). Each check judges a CLI scan, install or rollback. `Run` retries a command once, and only on a non-zero exit. The CLI usually reports an exhausted patch API fetch in its JSON while exiting zero, so the cell just fails a later check. Port backtest-poetry.py's case-level retry (#596). A case is re-run from a fresh directory, at most three attempts, only when every failed check recorded transport evidence from the operation it judged. Evidence is a failed command's request error, PyPI give-up, patch API 5xx or exhausted 429, or the same in the CLI's JSON error records. Functional failures are never retried, even when a later step raises a transport error. Failed attempts' logs go under attempts/ and are uploaded. A failing case now prints its failed checks' notes, since the job log alone never said why. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV (cherry picked from commit 4329170)
Bugbot: the final hosted/vendored rollback checks, the unverifiable- write rollback, the refused-lock VEX and the reverted-lock VEX runs named no operation, so a transport failure there never made the case retryable. installedBytesPatched fails together with a blipped pdm sync and blocked the retry the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N8YeCUhdg2tKdqyyY7z3sV (cherry picked from commit 6b0302a)
|
[agent] One more red check, also not from this PR:
Generated by Claude Code |
|
BugBot review Generated by Claude Code |
|
[agent] A single re-run should clear it, but this session can't trigger one (the re-run API returns 403). Could a maintainer re-run that job? I'm still watching the PR. Generated by Claude Code |
Resolves the vendored Gemfile rewrite conflict with #849 (#847). The merged rewrite keeps #847's handling of positional version arguments (`*V`, constants, method calls) and reads the tail through the shared option reader. That reader now classifies positional arguments as version constraints, never sources, and flags `**opts` splats and hash literals as dynamic. Hosted mode still refuses dynamic options, while vendored mode keeps them after `path:` as #847 intends. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Clears the merge conflict GitHub reported against main. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run 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 d902d81. Configure here.
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #652
Summary
In hosted mode, a gem pulled from a git source that the old token list didn't recognize got "redirected" into a Socket source block that its own option still overrides. Bundler kept loading the unpatched git checkout, the scan reported success, and VEX attested
not_affected. Hosted and vendored modes now refuse every source-selecting option, in every Ruby key spelling. Hosted mode also refuses a gem that the lock resolves from a non-registry section.Root cause
Both modes decided whether a
gemline already picks its own source by substring-matching a fixed token list (path:,git:,github:,gist:,bitbucket:,source:). The list was duplicated inpatch/redirect/mod.rsandvendor/gem.rs, and it missed:gitlab:source.git_source(:name) { … }key."git" => …,"git": …).gem_line_trailing_optionsalso read these as a version constraint, so string-keyed options such as"require" => falsewere silently dropped.Fix
formats::gem::gemfilemodule: the one reader of agemline's argument tail. It splits the tail at top-level commas, outside strings (including escapes) and brackets, and reads each option key in all four spellings (key:,"key":,:key =>,"key" =>).source_optiontreats any key outside Bundler's non-source keys (group,require,platforms,install_if,branch/ref/tag, …) as source-selecting. Bundler rejects unknown keys that aren't git sources, so this coversgitlab:and every customgit_sourcewithout parsing thegit_sourcedeclarations. Arguments it can't read (**opts, a hash literal, a method call) fail closed.trailing_optionsreplacesgem_line_trailing_optionsand keeps string-keyed options.redirect_gem_source_option) and vendored (rest_blocks_edit) modes both use the shared reader, so they can't drift again.Gemfile.locklists the gem under aGIT/PATH/PLUGIN SOURCEsection (newGemfileLock::non_registry_section_of). That is what Bundler actually resolves from, and it catches declarations the line reader can't see, e.g. a plaingemline inside agit "…" doblock. Socket's own vendoredPATHwiring still gets the existing "un-vendor first" remedy.Test evidence
Red before the fix, green after:
patch::redirect::tests::gemfile_git_source_options_in_every_spelling_fail_closed:gitlab:, a customlocal:git source,:internal =>,"git" =>,'path' =>,"git":. Failed on main, passes now.patch::redirect::tests::gemfile_string_keyed_options_survive_the_redirect:"require" => falseis kept inside the block. Failed on main.patch::redirect::tests::gem_locked_from_git_or_path_section_fails_closed: a gem in agit … do/path … doblock that the lock resolves fromGIT/PATH. Failed on main.vendor::gem::tests::plan_gemfile_edit_refusal_grammar(extended): vendored mode refusesgitlab:, a custom git source and"git" =>. Failed on main.formats::gem::gemfile::tests::*: unit tests for the reader (spellings, comments,#{}interpolation, escaped quotes, fail-closed cases, option preservation).e2e_redirect_gem_build::gem_hosted_custom_git_source_is_refused_and_still_installs: realgem build+bundle install(Ruby 3.3.6, Bundler 4.0.17) of a gem from a local git repo throughgit_source(:local). The scan must nameredirect_gem_source_option, leave Gemfile/lock byte-identical and attest nothing, andbundle installmust still succeed. With the old token list put back, it fails ("status":"success", nothing refused). With the fix, it passes.Commands run locally:
cargo clippy --workspace --all-features -- -D warnings: clean.cargo test -p socket-patch-core --all-features --lib -- gem: 271 passed.cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build -- --ignored(Bundler 4.0.17): 12 + 6 passed.cargo test --workspace --all-features --no-fail-fast: all gem tests pass. The only failures are write-failure / read-only-permission tests (*_state_write_failure_*,*_unremovable_*,wire_*_failure_*,relax_loop_must_not_traverse_symlinked_root, …). They rely onchmodmaking files read-only and can't fail as designed when the suite runs as root, as it does in this sandbox. None of them touch gem code.cargo fmt: the new module is rustfmt-clean, and the hunks in existing files follow their surrounding format.mainitself is notcargo fmt-clean, so I didn't run a repo-wide reformat.Performance
The first CI run flagged
scan performance:bundler/hosted+15.1% andbundler/rescan+15.6%, because the lock check re-parsedGemfile.lockonce per patched gem. Commit 72db141 reads the non-registry sections once per rewrite. Our own edits only touchGEMsections andCHECKSUMS, so membership read up front stays accurate. A localsocket-patch-bench compare --filter bundler(perf profile, base = main 045d7ec) now showsbundler/hosted-2.2% [-6.5, +2.1] andbundler/rescan-2.7% [-7.4, +6.7]: unchanged.Checklist
gitlab:or customgit_sourcegem as patched, so Bundler keeps loading the unpatched git checkout while VEX attestsnot_affected#652 customgit_sourcearm: unit test + e2e capstonegitlab:or customgit_sourcegem as patched, so Bundler keeps loading the unpatched git checkout while VEX attestsnot_affected#652gitlab:arm: unit tests (hosted + vendored)gitlab:or customgit_sourcegem as patched, so Bundler keeps loading the unpatched git checkout while VEX attestsnot_affected#652 string-keyed"git" =>arm (refused) and"require" => false(preserved): unit testsvendor/gem.rs), also named in the report: same shared reader, vendored test extendedNotes
path:, sogem "x", *V,gem "x", ENV.fetch(…)orgem "x", VERSIONbecomes a Gemfile syntax error and everybundlecommand fails #847), commit 7db1412: the shared reader now classifies positional arguments (*V, constants,ENV.fetch(…),Rack::VERSION) as version constraints, which never select a source.**optssplats and hash literals are flagged dynamic: hosted mode refuses them because a hiddengit:would make the redirect a silent, attested no-op, while vendored mode keeps them afterpath:as Vendored gem rewrite puts non-string arguments afterpath:, sogem "x", *V,gem "x", ENV.fetch(…)orgem "x", VERSIONbecomes a Gemfile syntax error and everybundlecommand fails #847 intends. New unit test:positional_constraints_are_not_sources.vendor/bundlea "shared gem home" when--cwdis left at its default (or relative), so it gives the wrong remedy and drops the committed cache archive from the delete list #729 (relative--cwdstale-install wording) touches neighboring hosted gem code but has a separate cause.🤖 Generated with Claude Code
Note
High Risk
Changes hosted gem redirect and VEX refusal logic; incorrect handling could silently attest patches while bundler still loads unpatched git or path sources.
Overview
Fixes #652 by replacing duplicated Gemfile substring checks with a shared
formats::gem::gemfileparser forgem "name", …tails. Hosted redirect and vendored rewrite now fail closed on any source-selecting option—includinggitlab:, customgit_sourcekeys, and"git" =>/"git":spellings—andtrailing_optionskeeps string-keyed options like"require" => falseinstead of dropping them.Hosted mode also skips redirect when
Gemfile.lockresolves the gem from aGIT/PATH/PLUGIN SOURCEsection (vianon_registry_sources), even when the Gemfile line looks registry-safe. New unit tests and an e2e capstone (ScanVexCustomGitSource) pin the refuse-and-attest-nothing contract.Separately, the PDM compatibility backtest retries cases up to three times on transport-only failures (PyPI/API 5xx/429), records evidence under
attempts/, logs failed-check details, and uploads those artifacts in CI.Reviewed by Cursor Bugbot for commit 7db1412. Configure here.
Generated by Claude Code