Skip to content

Full Gradle support in agent, hosted and vendored modes - #646

Merged
Mikola Lysenko (mikolalysenko) merged 77 commits into
mainfrom
feat/gradle-support
Oct 5, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 77 commits into
mainfrom
feat/gradle-support

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

Brings Gradle up to parity with the other JVM ecosystems across all three patch modes.

What each mode now does for Gradle

  • Agent: discovers Gradle's module cache (~/.gradle/caches/modules-2/files-2.1, honoring GRADLE_USER_HOME) through the shared jvm_cache seam, so scan, get, apply and rollback find and patch Gradle-resolved artifacts in place.
  • Hosted: wires the hosted repository into Gradle builds automatically (no more manual-only setup), including the suffixed .module metadata so Gradle's metadata-first resolution picks up patched artifacts.
  • Vendored: closes several gaps in vendored Gradle output (vendored repo layout, Gradle module metadata, checksum/verification handling and revert), so vendored builds resolve the patched jars offline.

Dependency

Depends on SocketDev/depscan#27220 for serving suffixed .module files (and the mavenModuleSha256 grant identifier). Without it the CLI degrades gracefully and reports redirect_gradle_module_metadata_unavailable instead of failing.

Test coverage

  • Lean PR tier in ci.yml: ubuntu x 4 Gradle majors (6, 7, 8, 9).
  • Full grid in gradle-compatibility.yml: 3 OS x 4 Gradle majors x 3 modes (agent / hosted / vendored).

Notes

The sbt session (feat/sbt-support) builds on the same shared jvm_cache seam introduced here.

Closes #347
Closes #348
Closes #349
Closes #395
Closes #396
Closes #428
Closes #429
Closes #461
Closes #487
Closes #511
Closes #533
Closes #551

🤖 Generated with Claude Code


Note

High Risk
Changes how agent mode writes JVM/Gradle caches and how hosted mode rewrites Gradle settings and locks; mistakes could break builds or leave patches ineffective despite broad new e2e coverage.

Overview
Adds Gradle v5.0 across agent, hosted, and vendored modes, with contract docs and CI to match.

Agent (apply) no longer treats Maven as a single install dir: it resolves a JVM scope, patches every consumed copy (~/.m2 and Gradle files-2.1 hash dirs), supports whole-jar member swaps via the patch service, and enforces Gradle-specific guards (dependency verification, read-only cache, mavenLocal() consumption, unexpected pristine bytes, transform/jar staleness, daemon locks). Blob-gap probing and vendor preverify walk Gradle hash dirs the same way.

Hosted / vendor flows gain Gradle takeover preflight before vendored→hosted revert, a guard against creating settings.gradle over an unreadable existing file, broader eject snapshots (owned Gradle scripts, wiring files, FIFO-safe reads), and hosted confirmation via confirmed_gradle_uuids in the contract.

CI introduces jvm_tool matrix legs, multi-suite e2e execution with per-suite pass checks and Gradle probe artifacts, bumps Gradle test versions (e.g. 7.6.6, JDK 21 on 8/9), and adds gradle-compatibility.yml for the full OS × Gradle × mode grid (nightly + path filters). .gitattributes pins the hosted settings script as binary-safe.

get narrows release variants using all Maven install copies (select_installed_variants_any); remove surfaces nested rollback warnings.

Reviewed by Cursor Bugbot for commit f4b4339. Configure here.


Generated by Claude Code

Introduce crawlers/jvm_cache: one list of JVM project markers, a
layout-tagged cache root (Maven2 / GradleModules2 / Coursier / Ivy) that
MavenCrawler crawls and resolves PURLs through, and a per-build-tool
project_dependency_set provider list. Behavior is unchanged: only the
Maven2 root is populated and no provider is registered yet. Gradle and
sbt support each plug into this seam from their own modules.

Also counts build.gradle.kts and settings.gradle(.kts) as manifest
markers in scan policy, which previously listed only build.gradle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Gradle detect/run/skip helpers, the classpath and lockfile readers,
the project writer and the Windows verbatim-path strip lived inside
e2e_vendor_jvm_build.rs, so every new Gradle suite would have had to copy
them. They move to tests/gradle_build_common/ unchanged in behaviour, and
the module gains what the agent/hosted/vendored suites need: the Gradle
major/minor and JDK banner, Isolated Projects runs, per-DSL project
writers, a configuration-cache-safe printRuntimeClasspath task plus an
assertion on the bytes Gradle actually consumed, a test-only mirror init
script for the fake origins, an autocrlf clone and per-cell probe reports.
The launcher scrub now also drops GRADLE_RO_DEP_CACHE and GRADLE_HOME.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Gradle hosted suites drive the same Socket API and suffixed maven2
repository as the real-Maven hosted capstone. The wiremock Server, the
API mounts and the suffixed-pom rewrite move to tests/hosted_maven_common/
behind a Hosted descriptor of the patched GAV and grant, so they can be
reused for other coordinates. e2e_redirect_maven_build keeps its
constants and thin wrappers and behaves exactly as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Once the crawlers learn Gradle's user home they resolve it from
GRADLE_OPTS / JAVA_OPTS (-Dgradle.user.home), GRADLE_USER_HOME and
~/.gradle, plus the read-only GRADLE_RO_DEP_CACHE, and m2 from ~/.m2.
Inherited as-is, a developer's warm caches would leak into every test
that does not pin them. The common and prebuilt harnesses now scrub
those variables by default and pin HOME / USERPROFILE to an empty
stand-in (carrying version-manager roots over); a test passes a cache
explicitly when it wants one, and prebuilt_common's fixture server
serves explicit GRADLE_USER_HOME / GRADLE_RO_DEP_CACHE trees as maven2
repositories (with a slot for sbt's COURSIER_CACHE).

Install detection learns the files-2.1/<sha1>/ layout (the jar and the
pom live in different hash dirs), and fabricate_files21 lays out a
Gradle cache under the real sha1 names, padded or with leading zeros
dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gradle 9 prints `Launcher JVM:` / `Daemon JVM:` instead of `JVM:`, so the
JDK the harness logged and probed was empty on 9.x. The new
SOCKET_PATCH_GRADLE_E2E_ARGS knob appends arguments to every Gradle run,
which is how the compatibility grid's configuration-cache and Isolated
Projects cells reach every suite without per-test plumbing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Gradle suites need artifacts real Central cannot give them on demand:
a victim at two versions with Gradle module metadata and classifier jars,
a transitive range consumer, a parent pom, a BOM, a platform .module that
requires the victim, a buildscript-classpath library whose class prints a
marker from build logic, artifact-level maven-metadata.xml, checksum
sidecars, PGP signatures, and a jar whose sha1 starts with 0 (Gradle drops
that zero from the files-2.1 hash dir).

tests/jvm_fixture_repo/ generates all of it byte-for-byte reproducibly
(stored zip entries with fixed timestamps and permissions, hand-assembled
Java 8 class files, fixed-order JSON/XML, a tabulated MD5) and serves it
from wiremock as FakeCentral, with overlays and a patched-jar route for
the member-keyed swap. Only the signatures of a committed THROWAWAY key,
the key itself and SHA256SUMS are committed; the stability self-test
regenerates the repository on every OS and compares it with SHA256SUMS,
and SOCKET_PATCH_JVM_FIXTURES_REGENERATE=1 re-signs it reproducibly.

gradle_multi_project_fake_central_mirror_smoke_both_dsls resolves the
victim through the test-only mirror init script in both DSLs under
FAIL_ON_PROJECT_REPOS, through a pom range and from the settings
buildscript classpath, and records the hash-dir naming in a probe report.
Locally Gradle 6.9.4, 7.6.6, 8.14.3 and 9.8.0 all name the dir with the
leading zero dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Gradle campaign lands its suites package by package, so CI needs the
rows before the tests exist, without letting an empty leg pass forever.

ci.yml: JVM legs carry `jvm_tool` (gradle | maven | sbt). One step picks
the JDK from the runner image (JAVA_HOME_<N>_X64 / _arm64), falling back
to setup-java, and decides whether Maven is needed: Maven legs, and Gradle
legs whose filter selects a Maven-seeded test (gradle_vendor_*,
multi-project); agent / hosted Gradle legs run without it. The PR tier
is the lean table: ubuntu x {6.9.4/11, 7.6.6/17, 8.14.3/21, 9.8.0/21} x
{agent + hosted, vendor + multi-project}, plus the existing windows
8.14.3 multi-project leg. A row's `suite` may list several binaries;
`allow_empty` skips suites that have not landed and tolerates zero tests,
and a Gradle leg without it that runs nothing fails. Probe reports are
uploaded.

gradle-compatibility.yml runs the full grid (3 OSes x 4 lines x 3 modes,
fail-fast off, 60 min) plus JDK-ceiling, configuration-cache, Isolated
Projects (recording only) and real-Central rows, path-filtered on PRs,
nightly and on dispatch. It compiles its own binaries once per OS and
documents the JDK ceilings per Gradle line; 9.8.0 is still current.

ci-e2e-bundle.py learns multi-suite rows, `--suites` and a per-suite
prefix guard: every #[ignore] test of a Gradle suite must start with
gradle_agent_ / gradle_hosted_ / gradle_vendor_ / gradle_multi_project,
the prefixes the rows filter on, or the bundle (and `--check`) fails.
test_ci_gradle_prefixes.py covers the guard (including a stray name) and
forces `allow_empty` off once every suite of a row has landed;
test_ci_e2e_tiers.py pins the PR table, the jvm_tool steps and the grid
expansion.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two format!-of-a-constant pom heads and a cloned single-element slice in
the files-2.1 self-test; behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A developer's global commit signing or hooks would break the fixture
commit, and a global autocrlf would change what the clone checks out.
The helper now runs git against an empty global config, and a self-test
pins the result: LF as committed, CRLF in the clone, -text files
untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Gradle support in agent, hosted and vendored mode needs one shared,
filesystem-free model of a Gradle build so every mode reads scripts,
versions and locks the same way and can be tested in memory on every OS.
This starts crate::gradle with the contract the later packages code
against: TextReadFn / ListFn / Env / Os.

- dsl: a comment- and string-aware Groovy/Kotlin tokenizer (copied from
  the vendored planner's lexer and extended: Kotlin raw strings and
  nested comments, `${}` templates with nested strings, decoded escapes,
  BOM handling, strict UTF-8 decode) plus call-site parsing for both
  parenthesised and Groovy command-expression calls.
- eol: CRLF sniffing, re-spelling and line-ending-blind comparison for
  files a core.autocrlf clone checks out with CRLF.
- selector: Gradle's version ordering and selector scheme. Checked
  against real Gradle 6.9.4, 7.6.6, 8.14.3 and 9.8.0, which showed two
  behaviours changed in Gradle 7 (the special-qualifier set, and an
  exclusive upper bound also rejecting qualified versions of the bound,
  so `[1.9,1.10.0)` admits `1.10.0-socket.<hex>` only on 6.x), so the
  comparator and admits take the Gradle major. The golden tables are
  exported for the hosted script's Groovy port, and
  tests/gradle_selector_golden.rs asks real Gradle for every row when
  SOCKET_PATCH_GRADLE_E2E_GRADLE is set.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Discovery and agent mode must find the same Gradle caches Gradle itself
uses, and hosted mode must rewrite locked versions without disturbing
anything else in a lock file.

- home: GradleHome::resolve over an explicit Env (the process-env adapter
  lives with the crawler): -Dgradle.user.home from GRADLE_OPTS then
  JAVA_OPTS (quote-aware per OS, last wins), a non-empty
  GRADLE_USER_HOME, then <home>/.gradle with USERPROFILE first on
  Windows; the files-2.1 cache, the read-only GRADLE_RO_DEP_CACHE copy,
  GRADLE_HOME, and the init-script locations (init.gradle(.kts), both
  init.d directories, sorted as Gradle runs them).
- locks: every gradle.lockfile / buildscript- / settings- lock file and
  legacy gradle/dependency-locks/*.lockfile under a root (pruning build
  output, .gradle, node_modules, .socket and .git, eight levels deep),
  a parser for both formats with empty= and CRLF, and a one-entry
  rewrite that keeps each line's ending and configuration tail and
  merges into an already-locked target version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every mode has to reason about the whole build, not only the root
scripts: #461 (exclusiveContent and Android checks that only read the
root build), #428 (vendoring from a subproject), #511 / #533 (range,
rich, catalog and classifier declarations the root-only scan missed)
and #551 (mavenLocal declared in an init script or convention plugin).

ScriptGraph::collect follows, statically and with caps (8 levels of
apply-from / included-build nesting, 512 files, 1 MiB each): the root
settings, literal include forms with implied parents, projectDir and
buildFileName overrides, each project's build script, buildSrc and
literal includeBuild roots with their subprojects and precompiled
convention plugins, literal apply-from targets (including
rootProject.file, file(), new File(rootDir, ..) and "$rootDir/.."
spellings, with a visited set), each build's libs.versions.toml and
versionCatalogs files(..) catalogs, and the caller's init scripts.
Anything it cannot follow (computed paths, URLs, escapes, missing,
oversized or malformed files, caps) lands in `unresolved`, so callers
that must fail safe can.

Queries: settings_includes / project_dirs, subproject_owner for an
ancestor settings file, declarations_of (string, map, Kotlin named and
positional, rich version blocks, `!!`, classifier in all four forms,
catalog entries with version refs), exclusive_content_filters with
filter_claims_group (non-literal or uncompilable rules claim),
android_or_kmp, settings_classpath_has, maven_local (Declared /
NotDeclared / Undetermined), custom_lock_file and wrapper_version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
print_cp_task's Kotlin branch read a script-level `val`, so the doLast
lambda dragged the script object into the configuration cache and every
--configuration-cache run failed; GRADLE_APP captured the configuration
provider, which the cache serializes as a fixed file collection that
`.get()` then rejects. Both now capture a task-local FileCollection.

A cache reuse also skips the settings script, so the multi-project
capstone only asserts the settings marker when configuration ran
(configuration_reused). A new gradle_multi_project test runs the print
task twice under --configuration-cache in both DSLs on Gradle >= 8.1, so
the "configuration-cache safe" claim is exercised on every vendor leg.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The guard admitted every Gradle-campaign prefix in every suite, but each
suite only runs under the prefixes its own rows select: a gradle_vendor_
test in the discovery suite, or a gradle_hosted_ test in the agent suite,
passed --check and then ran in no row (or only on the ubuntu PR tier).
GRADLE_SUITE_PREFIXES now maps each suite to its rows' prefixes, and a
test pins that every admitted (suite, prefix) pair is selected by a ci.yml
row and a gradle-compatibility.yml mode.

Both workflows summed passed tests across a leg's suites, so one suite's
tests hid another whose filter selected nothing, and the real-Central row
(filter gradle_vendor_511/487) counted the always-landed
e2e_vendor_jvm_build and failed on every run until WP3 lands. Each landed
suite must now run a test on its own; compat rows take a `suites`
override, and the real-Central row names only e2e_vendor_gradle_build.
The SOCKET_PATCH_GRADLE_E2E_REAL_CENTRAL knob gains its reader,
gradle_build_common::real_central().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review found several shapes where the graph gave a confident answer
(mavenLocal NotDeclared, no declarations, "not owned") for builds Gradle
actually configures differently, which would drop ~/.m2 as a root or
miss the vulnerable declaration:

- `mavenLocal { content { … } }` (the Action form, the usual way to
  scope it) now counts as a declaration, not just `mavenLocal(`.
- Scripts a settings script applies are parsed as settings too, spliced
  in at the `apply from` as Gradle runs them, for both the graph and
  subproject_owner. Inside them `file()`, nested `apply from` and catalog
  `files()` resolve against the applied script's own directory while a
  bare `includeBuild 'x'` stays settings-relative (measured on Gradle
  6.9.4, 7.6.6 and 9.8.0).
- include / projectDir statements are applied in source order, and an
  implied child is created under its parent's directory as it stands at
  the include (Gradle puts 🅰️b at modules/a/b after relocating :a).
- Binary plugin sources (.kt/.java/.groovy) of buildSrc and of plugin
  projects in included builds are read as ScriptKind::PluginSource, so a
  `repositories.mavenLocal()` or plugin id in a Plugin<Project> class is
  seen. Product sources of ordinary included builds are not read.
- android_or_kmp also reads catalog `[plugins]` ids, which is the only
  place the id appears for `alias(libs.plugins.android.application)`.
- ScriptGraph::lockfile_paths / locks::lockfile_paths_in list only the
  lock files of the build's own projects, so a hosted rewrite cannot
  touch a nested sample or fixture build the checkout does not include.
  locks::lockfile_paths stays as the whole-tree inventory.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On Unix the JVM's user.home is the passwd entry's home, not $HOME, and
Gradle derives its user home from it; in container CI jobs the two
differ (HOME=/github/home, pw_dir=/root), so the CLI would scan and
patch a cache Gradle never reads. GradleHome::resolve now prefers the
caller's home_dir (which must be the passwd home) over $HOME on Unix.

A wrapper build runs the init.d of the distribution it unpacked under
<user home>/wrapper/dists, not $GRADLE_HOME's, and custom corporate
distributions ship mavenLocal/mirror scripts there. init_scripts_with
now includes every unpacked wrapper distribution's init.d, and
init_scripts_for / wrapper_init_dirs narrow that to the build's
distributionUrl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Disassembling VersionRangeSelector / DefaultVersionSelectorScheme (the
same on 6.9.4, 7.6.6 and 9.8.0) showed a range bound cannot hold
whitespace, the single-value form allows whitespace only after its `[`,
and both `[a]` and `[a,a]` become ExactVersionSelector (string
equality). The port's lazy bound pattern accepted `[ 1.1 ]` and inner
spaces, and `[a,a]` compared instead of matching exactly. New golden
rows pin each case, including `[1.01]` NOT admitting `1.1`, and pass
against real Gradle on all four majors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, home, eol, selectors)

Brings in the pure, I/O-light Gradle layer (DSL scanning, script graph,
lockfiles, Gradle home resolution, EOL handling, version selectors) that
the later work packages build on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ted fixtures, CI Gradle tiers, prefix guard)

WP5a lands the shared real-Gradle test harness, deterministic fake Maven
Central and hosted-API fixtures, the ci.yml jvm_tool scaffold with lean
Gradle rows, and the path-filtered gradle-compatibility.yml grid that the
later mode packages build on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
aca8b1c routed every cache root through a per-layout match whose
GradleModules2 arms were empty, so `scan --global-prefix
~/.gradle/caches/modules-2/files-2.1` found nothing (before, the plain
.pom walk happened to read the poms there).

Add crawlers/gradle_cache with the files-2.1 layout: a walk over exactly
three literal levels (group keeps its dots, then artifact, version) and
the 1-40 hex digit hash dirs below them, skipping bookkeeping and unsafe
coordinates. Each version dir with `<a>-<v>.{jar,pom,module}` in some
hash dir is one package whose path is the version dir; find_by_purls
resolves the same dirs. hash_eq / pristine compare hash dir names as
40-digit numbers, since some Gradle releases drop the sha1's leading
zeros.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Gradle package path is the version dir, but its files live one level
down, each in the hash dir its sha1 names, and the same jar can sit in
two hash dirs after a re-download. Every join site (apply, rollback,
verify, VEX, select_installed_variants) needs the real file locations.

Add gradle_cache::installed_copies, the one layout-agnostic resolver: a
Gradle version dir maps each key's file name to every hash dir holding
it (keys found nowhere stay on the version dir so they verify as not
found); any other path is returned unchanged. installed_copies_detailed
reports the missing keys apart. jvm_cache::locate_artifact lists every
copy of one artifact file per cache layout, and
gradle_cache::stale_derived_copies finds the instrumented and transformed
copies Gradle keeps outside files-2.1, for agent mode to refuse on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Gradle build resolves from its user home's files-2.1 (and the
read-only GRADLE_RO_DEP_CACHE), not ~/.m2, unless something declares
mavenLocal(). Scanning only m2 missed every Gradle-cached package (#349)
and reported m2 contents a Gradle-only build never uses (#551).

Cache roots now come from a JvmEnv (process env by default, an explicit
Env in tests): the Gradle user home resolved like Gradle does it (the
gradle.user.home property, GRADLE_USER_HOME, then the passwd home on
Unix) and the Maven local repository. A Gradle build, or a global scan,
crawls files-2.1 and the read-only cache. m2 stays a scan root for a
pom.xml, a non-Gradle cwd, a global scan, or a Gradle build where the
script graph plus the init scripts that apply (user home, GRADLE_HOME,
and the wrapper's own distribution, wherever distributionBase/Path
unpack it) declare mavenLocal() or cannot rule it out. A custom wrapper
distribution that is not unpacked yet, an unreadable init script or an
unfollowable script reference keeps m2 (undetermined). PURL lookups keep
m2 regardless, since its bytes still serve vendoring and apply.

--global-prefix accepts a Gradle user home, caches/modules-2 or a
read-only modules-2 for the files-2.1 inside them; a Maven repository
named `caches` is left alone. jvm_cache gains all_local_roots for byte
sourcing, gradle_cache the fs/env adapters (fs_text_read, fs_list,
home_from_process_env, init-script reads) the other Gradle packages
share, and locked_gavs for the lock-membership annotation.

The CLI test harness now also pins GRADLE_USER_HOME to the stand-in
home: with Gradle's home taken from the passwd entry, pinning HOME alone
would let a developer's real ~/.gradle into every test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
scan now says when the Gradle side of discovery is not what the user may
expect, on the run-level warnings[] channel:

- gradle_build_ignores_m2: a Gradle-only build declares no mavenLocal(),
  and modules its locks or patch records name exist only in ~/.m2, which
  the build never resolves from and the scan leaves out (#551).
- gradle_maven_local_undetermined: m2 stays a root because a script or
  init script could not be read literally.
- gradle_user_home_differs: Gradle's home follows the passwd entry, not
  $HOME.

Each Gradle-cached package in packages[] carries an additive inLock
flag from the build's graph-scoped lock files. It only annotates: an
unlocked buildscript or plugin dependency is still reported.

e2e_gradle_discovery_build runs the real binary against fabricated
caches everywhere, and gradle_agent_349_scan_finds_gradle_cache lets
real Gradle fill a fresh user home from the fake Central, then checks
the scan reports the module and that the crawled version dir expands to
the hash dir whose jar Gradle consumed. On 6.9.4, 7.6.6, 8.14.3 and
9.8.0 that dir drops the jar sha1's leading zero.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get_maven_repo_paths now lists the Maven local repository ahead of the
Gradle caches. Callers that still take the first copy (agent apply's
Maven arm, hosted VEX copies) then keep resolving where they always
did; the Gradle copies remain reachable for the all-copies fan-out that
follows.

e2e_maven's Gradle-marker scan now declares mavenLocal(): a Gradle-only
build without it does not read ~/.m2, so the scan no longer counts the
m2 artifact for it (#551).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
stale_derived_copies counted any file named after the jar as stale and
stopped silently after 200,000 entries. A copy Gradle rebuilt from the
patched jar has the same name, so the warning could never clear, and a
truncated walk looked exactly like "no stale copies". It now returns
DerivedCopies: copies proven pristine-derived (identical bytes or the
pristine sha1 in a dir or stem) are stale, other same-named files are
unknown, and an entry cap or unreadable entry sets incomplete. The walk
is sorted so it is deterministic.

gradle-wrapper.properties is now read the way java.util.Properties
reads it: ISO-8859-1, whitespace as a separator, escapes and
continuations. A wrapper file that names no distribution, or cannot be
read, marks mavenLocal() undetermined instead of falling back to "no
wrapper", so a custom distribution's init.d cannot drop ~/.m2 silently.

get_maven_repo_paths no longer returns Gradle files-2.1 roots. Its
callers (apply, rollback, vendor, VEX) join file keys onto the package
path. A Gradle version dir holds no files directly, so a Gradle-only
GAV failed with NotFound instead of being skipped as not installed. The
Gradle roots move to the new get_maven_copy_paths for callers that
expand version dirs through installed_copies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The vendor baseline pre-check ran verify_file_patch on the crawled
package path, and for a Gradle version dir that always returns
NotFound. It now goes through installed_copies, so any hash-dir copy
that differs from the baseline flags the patch.

inLock is now written only when the cwd's Gradle locks were actually
read. A global run inside a Gradle build reads them too, and a run
outside one has no inLock at all, where it used to report false for
every package. gradle_user_home_differs is skipped under
--global-prefix, because the user home is not used then.

The Gradle warning codes now carry a level in the JSON warnings[]: info
for gradle_maven_local_undetermined and gradle_user_home_differs, warn
for gradle_build_ignores_m2. Human mode prints the info ones as "Note:"
and leaves them out under --silent.

The real-Gradle capstone now takes the package path from the crawler,
over the roots this build scans, and expands that path rather than one
it built itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…copies resolver, init-script-aware m2 gating, artifact locator (#349, #551 roots)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent mode could only patch files that sit directly in a version
directory, so member-keyed Maven records (#264) were unpatchable and a
Gradle files-2.1 version dir verified nothing. patch/jvm_jar classifies
a record as leaf- or member-keyed, verifies jar members against an
explicit jar name (so hosted copies check their suffixed jar), swaps in
the patch service's build of the whole jar after checking every
unpatched member is upstream's, and keeps the original under
.socket/jvm-originals/ where blob cleanup never looks. Rollback restores
that backup byte for byte, or re-downloads a Gradle copy's jar and
accepts it only when it hashes to the copy's hash directory.

Variant selection, verify_patch_record, judge_installed_record and the
hosted copy check now expand Gradle version dirs through
installed_copies, and VEX takes every installed copy of a Maven purl and
reports the ones that do not verify. A Gradle cache copy no longer
counts against a vendored entry, since the vendored build never reads
it.

Maven ~/.m2 .sha1/.md5 files are rewritten only when they matched the
pre-patch bytes, and Gradle cache writes carry Info advisories about
refreshes, daemons and the shared user home. The registry fetch helpers
in vendor/maven_repo.rs become pub(crate) for the upstream fallback.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Vendored Gradle reported success on builds it did not actually patch:
a pom.xml next to a Gradle build skipped the Gradle side (#395), a
subproject was wired as its own root (#428), an autocrlf checkout failed
--check and left the script behind on revert (#429), exclusiveContent and
Android checks only looked at the root build script (#461), pgp-only
verification entries broke the build (#487), ranges downgraded to an
unpatched release because the tree listed no versions (#511), and a
declared classifier stopped resolving (#533).

- detect() reports every build of the root (Detected{maven, gradle}); a
  mixed root is planned through both planners in one transaction, and a
  refusal of either writes nothing. Revert, --check, VEX liveness and
  repair run both halves.
- not_build_root refuses vendor and repair from a Gradle subproject, from
  includes that cannot be read, and from a project configured by an
  ancestor settings file.
- Owned text (script, index, .gitattributes, derived metadata,
  .mvn/maven.config) is compared line-ending blind; new owned
  .socket/gradle/.gitattributes and .socket/vendor/.gitattributes keep
  them out of EOL conversion. A vendor-created settings file is deleted
  once only whitespace is left.
- The planner builds crate::gradle::graph::ScriptGraph and refuses a
  conflicting exclusiveContent or an Android/KMP plugin anywhere it can
  follow (subprojects, convention plugins, apply from, catalogs), naming
  the file; what it cannot follow is degraded.
- A pgp-only metadata entry gets a sha256 beside its <pgp>; --check and
  the parent-chain warning require a checksum when metadata verification
  is on.
- Each vendored GA gets a derived maven-metadata.xml (Gradle version
  order, no lastUpdated), recomputed on revert and deleted with the GA's
  last row. A range is noted; one admitting no vendored version refuses.
- JvmPatch.extra_artifacts serves declared classifiers (and sources when
  found) from the tree; a declared one that cannot be sourced refuses.
- Upstream files come from the crawler's directory and every local JVM
  cache (jvm_cache::locate_artifact over all_local_roots), Gradle copies
  authenticated by their hash directory.
- The settings helpers WP4 needs are pub(crate) and parameterized by
  WiringTarget; the vendored defaults keep the output byte-identical. The
  lexer is crate::gradle::dsl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agent mode patched only the first copy of a Maven purl, and Gradle
version directories were joined like ~/.m2 ones, so a GAV cached by
Gradle stayed vulnerable while apply reported success (#551). The Maven
lookup now returns every cache holding a copy (get_maven_copy_paths), and
a JvmScope sorts them into the copies a build consumes, the read-only
cache, and an ~/.m2 a Gradle-only build never reads.

apply patches each consumed copy, expanding Gradle version dirs into the
hash dirs holding the record's files, and swaps the whole jar for
member-keyed records (#264). Each Gradle hazard has its own code:
gradle_verification_metadata_present refuses with nothing written,
gradle_build_ignores_m2 fails an ~/.m2-only GAV, gradle_ro_cache_shadows
fails a run with a read-only copy, gradle_copy_unexpected_bytes leaves a
pristine download of other bytes alone, and gradle_transform_copy_stale
fails a copy whose derived transforms still hold the pristine jar.

rollback groups by (base purl, copy), restores each hash dir and checks
the restored bytes hash to their directory (gradle_rollback_hash_mismatch
otherwise), restores whole jars from their backups, and puts ~/.m2
checksum files back. get narrows release variants over every copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 65112a8)
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #769

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

* Re-vendor Pipenv locks to a newer patch

A Pipenv project vendored at one patch never moved to a newer patch
for the same package: the re-vendor refused with
pypi_pipenv_source_already_exists and the run exited 1, although the
dry run previewed would_revendor.

When the vendor ledger records the Pipfile.lock entry the older patch
wrote, and that entry is unchanged, it is now rewired in place to the
new wheel. The record carries the older entry's pre-vendor registry
original forward, so vendor --revert still restores the user's pin.
Without that record, or after an edit, it still refuses as before.

Refs #769

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

* Re-vendor PyPI installs from an older patch

When a venv was installed from the vendored wheel of an older patch
(pipenv sync after vendoring), re-vendoring to a newer patch skipped
the package as package_not_installed and exited 1: the installed
files are the old patch's bytes, so they failed the new patch's
installed-variant check.

When the vendor ledger holds exactly this package at an older patch
uuid, such an install is now treated like a lock-only checkout: the
pristine wheel comes from the lock, registry or patch service, and the
package is re-vendored. The service download plan makes the same call.

Fixes #769

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

* Keep the ledger-less Pipenv wrappers test-only

check_target_guards and wire_pipenv now have no production caller
(the vendor flow passes the ledger through the _superseding
variants), so clippy flagged them as dead code. Compile them for
tests only and point the docs at the variants production uses.

Refs #769

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

* Port #851's vex alias test fix

Main has been red since 4646693 (#605): two
commands::vex_consumed tests built for #738 assume the name-keyed
resolver never returns npm-aliased copies, which #605 changed. This is
the same test-only change as #851 and becomes a no-op once that lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXCFoPbraq7rNKJpXEP2n

* Port #878's Gradle digest routing

Main is red since 1714299 (#865): its
production_digests_go_through_the_helpers guard flags the inline
digests that #646 added in gradle_cache.rs, jvm_jar.rs and
sidecars/maven.rs. This is the same change as #878 and becomes a
no-op once that lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VSXCFoPbraq7rNKJpXEP2n

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #410

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

* Fix pip rollback refusing all-hosted requirements

Hosted rollback, remove and the hosted-to-vendored takeover refused a
requirements.txt in which every requirement was a hosted pin (a lone
`six==1.16.0`, or one beside `-e .`). They couldn't tell whether the
original line used pip's hash-checking mode, so the only way back was
version control.

The restore now counts an editable line as unhashed evidence (pip
refuses editables in hash-checking mode). When no other line settles
the mode, it reads the hosted line itself: the rewriter writes
`--hash` only into an already hashed file and otherwise pins by the
url's `#sha256=` fragment. With nothing else in the file to conflict
with, either restored form installs.

Fixes #410

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

* Update restore golden for sole-pin requirements

The golden test asserted that a requirements.txt holding only the
hosted pin is refused as ambiguous, which is the #410 bug. It now
asserts that both the unhashed and hashed sole-pin files round-trip,
and keeps the mixed hashed/unhashed refusal.

Refs #410

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

* Fix vex alias tests broken by store-copy merge

#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)

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #364

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

* Refuse hosted yarn classic with an offline mirror

A yarn classic project that sets yarn-offline-mirror (in .yarnrc or
.npmrc) had its lock rewired to the hosted tarball. Yarn looks mirror
tarballs up by file name, and the hosted one has the same name as the
upstream tarball already in the mirror, so every install got the
unpatched bytes and failed the integrity check (or, offline, never
found the patched tarball) while the scan reported success and VEX
attested the patch.

The hosted rewrite now leaves yarn.lock untouched in that case, warns
with redirect_yarn_classic_offline_mirror and points to vendored mode,
which works with a mirror. The dependency is not counted as redirected
or attested. Both config files are read only beside a classic lock.

Fixes #364

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

* Keep mirrored yarn classic vendored on takeover

A vendored-to-hosted takeover reverted the vendored yarn classic
wiring before the hosted rewrite refused the offline mirror, leaving
the package patched in neither mode. The takeover now checks the
mirror first and keeps the package vendored.

Adds a real-yarn e2e (yarn 1.22.22, populated mirror) showing the scan
refuses, writes no attestation, and fresh installs still work online
and offline.

Refs #364

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

* Document the yarn classic offline mirror refusal

Refs #364

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

* Re-bless pdm and poetry rewrite goldens

These goldens hash the Debug text of the whole rewrite result, which
now carries the empty refused_yarn_classic_uuids set. With that field
stripped from the text, the old goldens still match every case, so
only the output digests change; case keys and inputs are identical.

Refs #364

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

* Fix mirror e2e on yarn releases before 1.7

yarn 1.0 to 1.6 install nothing from an offline mirror even without
socket-patch, so the fresh-install leg of the new mirror e2e failed on
the yarn-classic 1.0.2 and 1.6.0 matrix legs. Those releases now pin
that known limitation; the hosted refusal is still checked on every
release.

Refs #364

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

* Detect a .yarnrc offline mirror written with a colon

yarn 1's .yarnrc parser ends an unquoted key at ':', so
`yarn-offline-mirror: ./mirror` and `yarn-offline-mirror:./mirror`
configure the mirror just like `yarn-offline-mirror ./mirror`. The
mirror check only split on whitespace, so either spelling slipped
through and hosted mode still rewired the lock, reproducing #364.

Refs #364

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n

* Port vex alias test fix from #851

main has been red since #605 taught the name-keyed resolver to return
npm-aliased copies, which broke two vex_consumed tests added by #738.
Port #851's test update so this PR's CI goes green; it no-ops once
#851 lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n

* Port Gradle digest-helper fix from #878

main has been red since #865 added a check that production code
computes digests through utils::digest, while #646's Gradle code
still hashes inline. Port #878's change so this PR's coverage and
test-release go green; it no-ops once #878 lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uQyhodCtdJGrKaD7AAV1n

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #367

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

* Keep a project's own bun patch when rewiring Bun

Bun applies a patch from `bun patch` (package.json
patchedDependencies, mirrored in bun.lock) only while the lock
resolves the package to its registry name@version. Hosted and
vendored mode moved that entry to a hosted URL or a vendored
tarball, so every later install silently dropped the user's own
patch while socket-patch reported success.

Hosted mode now leaves such a package on its registry entry in
both bun.lock and bun.lockb, warns
redirect_bun_patched_dependency_skipped naming the key, and keeps
the in-run VEX from assuming the Socket patch applied. Vendored
mode refuses it with vendor_lock_entry_unsupported before any
write or download. Other packages in the lock are still rewired.

Fixes #367

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

* Format the new Bun patch tests

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

* Never confirm a Bun package the user patched

Bugbot review of #873 found two gaps in the bun patch guard.

A text bun.lock only reached the root package.json through its
workspaces section, so a lock without one never saw the
patchedDependencies keys and rewired the package anyway. The
manifest is now read beside either Bun lock.

A package left on the registry could still be counted as
switched when a sibling package-lock.json took the hosted URL,
although Bun keeps installing the registry bytes. Such uuids are
now recorded as refused and never confirmed.

Refs #367

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

* Keep rewrite goldens stable with the new field

The refused-Bun uuid set added to RewriteResult changed the
serialized and Debug output that the redirect equivalence goldens
hash. The set is now left out of serialization when empty, like
the other per-ecosystem uuid sets, and the two goldens that hash
the Debug output (poetry, pdm) are re-blessed. Only their output
digests change; every case and input digest is identical.

Refs #367

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

* Read a JSONC package.json for Bun patch keys

Bun accepts comments and trailing commas in package.json. The
patchedDependencies reader parsed it as strict JSON, so such a
manifest yielded no keys. A bun.lockb project has no text mirror
to fall back on, so the project's own bun patch could still be
rewired away. The reader now strips JSONC comments and trailing
commas before parsing, leaving string contents untouched.

Refs #367

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

* Skip a BOM before reading Bun patch keys

A Windows-saved package.json can start with a UTF-8 byte order
mark, which Bun ignores but serde_json rejects. The reader found
no patchedDependencies keys in such a manifest, so a bun.lockb
project could still lose its own bun patch. The mark is now
stripped first, as the crate's other manifest readers do.

Refs #367

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #826

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

* Keep gem declarations sharing a line with ;

A Gemfile line like `gem "a", "1"; gem "b", "2"` had its second
declaration deleted when socket-patch redirected or vendored gem "a",
because the rewrite replaces the whole line and the safety check did
not know that `;` starts a new statement. The next frozen
`bundle install` then failed. Such lines are now refused with a
warning and left untouched.

A declaration ending in a bare `;` (optionally followed by a comment)
was refused as "continues on the next line" since #637. It is complete,
so it is rewritten again, without the `;`.

Fixes #826

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

* Use the reported line shape in the ; e2e test

The `;`-joined fixture had no version argument, so the old check
already refused it as "unexpected tokens" and the test passed without
the fix. Use `gem "x", "v"; gem "y", "v"` from #826, which the old code
rewrote and lost the second gem.

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

* Port #878: route Gradle digests through helpers

main is red: #646 added inline sha1/sha256 calls that #865's
production_digests_go_through_the_helpers guard rejects. This ports
the fix from #878 so this PR's coverage job can go green. It becomes a
no-op once #878 lands.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #760, #762

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

* Test that a Poetry/PDM lock renders once per scan

Hosted scans rewrite poetry.lock and pdm.lock once per patched
package, rendering and re-parsing the whole lock each time. Count the
engine's whole-lock renders and require one per lock for a dozen
patched packages. Both tests fail today with 12 renders.

Refs #760, #762

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

* Rewrite each Poetry/PDM lock in one pass

A hosted scan rewrote poetry.lock and pdm.lock once per patched
package, and every rewrite rendered and re-parsed the whole lock. A
project with a dozen patches paid for a dozen full parses, which made
Poetry and PDM scans 3.5-4.5x slower per package than other managers.

The shared lock-splice engine now plans every package against one
parsed lock, applies all the changes, renders and re-parses once, and
splices each package's changed fragments into the original text. The
result is checked against the rendering byte for byte. When a lock
mixes line endings, a package is rewritten twice, or any check fails,
the rewrite falls back to the old package-by-package path, so output
and recorded edits never change.

Differential tests run both paths over every Poetry and PDM lock
generation, LF, CRLF and mixed, with refusals, missing packages and
re-runs mixed in, and require identical text and per-package results.

Fixes #760, #762

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

* Read PDM lock_version from the parsed original

The rewrite never changes [metadata] lock_version, so read it from the
parse the presence probe already took instead of parsing the output.

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

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

* Drop the unrelated formatting sweep

Running cargo fmt over the whole workspace reformatted 117 files this
PR does not otherwise touch, because main is not rustfmt-clean and CI
does not check formatting. Restore those files to main and keep the
diff to the Poetry/PDM rewrite and the ported digest fix.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start refactor for #845

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

* Bound crawler and tool probes by one deadline

A version-manager shim that never answers (`gem`, `python3`, `npm`,
`composer` behind rbenv/asdf, a Ruby waiting on a network gem home)
used to hang `scan`, `apply`, `vex` and every other crawling command
forever with no output: the crawler probes waited on `output()` with
no deadline.

Every probe now runs through one `utils::process::output_within`
primitive: null stdin, captured stdout, dropped stderr, and the child
killed and reaped at the deadline without waiting on a grandchild
that still holds the pipe. Crawler probes get the same 10 s budget
that the Pipenv and Hatch version probes and the self-update
`--version` check already used, and those three sites drop their
hand-rolled `tokio::time::timeout` + `kill_on_drop` blocks for it.

Refs #845.

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

* Port #878: Gradle digests through utils::digest

`main` fails `utils::digest::tests::production_digests_go_through_the_
helpers` because #646 left inline sha1/sha256 calls in
`gradle_cache.rs`, `jvm_jar.rs` and `sidecars/maven.rs`, which turns
`test`, `test-release` and `coverage` red on every PR. This is #878's
change verbatim; it no-ops once #878 merges.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Cancel superseded npm/pnpm/Pipenv PR runs

npm-compatibility, pnpm-compatibility and pipenv-compatibility had no
concurrency group, so every push to a PR left the previous run's full
matrix (11, 26 and 6+ jobs) running to completion against a commit
nobody will merge. Over the last 100 PR runs of each (about 8 hours),
52 runs were superseded while still running and spent ~445 job-minutes
after the newer push landed, competing for runners with the live runs.

Group PR runs per PR number with cancel-in-progress, as ci.yml and the
other compatibility workflows already do. Every non-PR event gets its
own group (run_id) so no main push or dispatch is ever cancelled, not
even while pending behind another run in the group.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQTUzCY6pkBLc3BNJhz9Nu

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Run pnpm install-proof as one job per Node

The pnpm install-proof matrix spawned 25 single-version jobs (one
per pnpm/Node pair) whose real work is ~20 s each. Most of each job was
runner setup, and any one leg that never got a runner left the run
red: on 2026-10-05 20/28 pnpm runs failed, every failed leg checked
being an ubuntu-latest job cancelled with no runner and no log.

Group the legs by Node runtime (10, 16, 24): each job installs its
pnpm versions, then runs both pinned suites per version in turn with
a per-version TMPDIR so the shared cache sandbox starts empty, as it
did on a fresh runner. Every pnpm/Node pair still runs on every PR
and main push; a failure is reported per version via ::error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uej6tnJfjRU8NCUz2jdDG4

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #915

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

* Patch the gem Bundler loads under path.system

A project that switched from vendor/bundle back to system gems
(`bundle config set path.system true`, or BUNDLE_PATH__SYSTEM=true)
usually still has the old gitignored vendor/bundle. The gem crawler
always counted that leftover store and, because it held gems, stopped
looking in the `gem env` homes. Agent apply then patched only the
unused copy, and vex attested not_affected while Bundler kept loading
the unpatched system gem.

The crawler now works out which Bundler settings tier decides the
install path (local config, then environment, then global config) and,
when that tier sets a truthy path.system, skips the default
vendor/bundle root so the system gem homes are crawled. path.system
values now follow Bundler's own boolean coercion, so "1" or "yes"
count as true too.

Fixes #915

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

* Skip env bundle path shadowed by path.system

When the Bundler tier that wins sets path.system, Bundler also ignores
an env BUNDLE_PATH below it or beside it. If that value named the
leftover vendor/bundle, the crawler still probed it as the default
root and hid the system gem homes again, so apply and vex kept
targeting the unused copy. The env root is now skipped in that case
too.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #908, #521

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

* Restore berry and vlt pins from project registry

Hosted rollback and remove looked up a package's version document on
the default registry (npmjs or SOCKET_NPM_REGISTRY) only. On a project
that installs from a mirror whose tarball URLs are off the usual path,
that broke the restored lock:

- yarn berry wrote a bare npm: locator, so a cold-cache install asked
  the mirror for a path it never serves and failed with a 404 (#908).
- vlt rebuilt slot [3] as <registry>/<name>/-/<leaf>-<ver>.tgz instead
  of the URL the registry advertises, which vlt ci can 404 on (#521).

The restore now reads the document from the registry the project
resolves the package against (.yarnrc.yml npmRegistryServer, the vlt
node's registry) and vlt takes slot [3] from its dist.tarball. If that
registry can't be read (for example it needs credentials), the old
default-registry lookup is used and upstream_registry_fallback warns.

Fixes #908
Fixes #521

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

* Name the per-registry npm cache type

Keeps clippy's type_complexity lint quiet for the restore client's
registry-keyed version-document cache.

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

* Keep npmScopes packages on the default lookup

A scoped package in a .yarnrc.yml with an npmScopes block may resolve
against its scope's registry rather than npmRegistryServer, so berry
restore keeps reading its document from the default registry, as
before, instead of asking a registry that may not host it.

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

* Route Gradle digests through utils::digest

main's test suite is red: the Gradle cache, jar and Maven sidecar code
from #646 hashes inline, which the digest guard test from #865 forbids,
so coverage and the macOS/Windows test jobs fail on every PR. This is
the same change as #878, ported so this PR can go green; it no-ops
once #878 lands.

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

* Serialize berry checksum tests that read SOCKET_NPM_REGISTRY

The npm dist cache is now keyed by registry base, and these two tests
seed it under npm_registry_base(), which reads SOCKET_NPM_REGISTRY.
Serial vlt/bun tests set that variable, so when one ran in parallel the
lookup key no longer matched the seeded entry and the test fetched
left-pad from the other test's mock server (404). That is the
test (windows-latest) failure on 48798c4. Serializing them with the
env-mutating tests closes the race.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Bench: add gradle hosted and rescan scenarios

#646 gave Gradle builds a hosted mode: scan crawls Gradle's
modules-2/files-2.1 cache, pins suffixed versions in gradle.lockfile
and wires the build through an owned settings script and index under
.socket/gradle/. None of that was benchmarked; the maven scenarios
only reach the pom.xml + ~/.m2 path.

The gradle fixture is a single-project Groovy build with dependency
locking (1000 locked artifacts, 25 patched direct deps), its cache
under the fixture's GRADLE_USER_HOME with jar and pom in separate
sha1 dirs. The Maven-coordinate generator, pom writer and maven2
grant builder are shared with the maven fixture, whose bytes are
unchanged. The grant's indexUrl is https because the Gradle planner
refuses anything else; scan never fetches it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

* Bench: add hatch hosted and rescan scenarios

Hatch hosted mode (#680, #743) rewrites pyproject.toml and hatch.toml
in place, with no lockfile, through utils::hatch::plan. No scenario
exercised that rewriter: hatch.toml is a HOSTED pypi input, and a
hatch project with no lock fell through every existing pypi fixture.

The fixture is a lockless hatchling app. Direct deps go in [project],
and a hatch.toml default env (in-project .venv) pins every patched
transitive, since hosted Hatch only redirects deps a Hatch table
declares. A scan rewrites both files and adds
[tool.hatch.metadata] allow-direct-references. It is sized at 1000
packages / 25 patched so a scan takes about 75-85 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
)

* Start fix for #926

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

* Match PyPI names in get by their PEP 503 form

`socket-patch get ruamel.yaml` (or `typing_extensions`, or
`Typing.Extensions`) reported "No packages matching" and exited 0
for an installed, patchable package. The package-name search only
lowercased the query, but the crawler stores PyPI names in PEP 503
form (`ruamel-yaml`), so any spelling with `.`, `_` or a run of
separators never matched. Users who copy a name from
requirements.txt or `pip list` were told nothing could be patched.

PyPI packages are now compared with both the query and the name
canonicalized per PEP 503, for exact, prefix and contains matches.
npm and other ecosystems keep the plain case-insensitive compare,
since `_` and `.` are distinct characters in their names.

Fixes #926

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #504, #947

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

* Test Pipenv crawl never reads the system Python

A Pipenv project with no Pipenv venv yet must not have the OS
Python's site-packages crawled as if they were the project's: agent
mode patched them in place (#504), and vendored mode tried to vendor
system-only packages into Pipfile.lock and exited 1 (#947).

Replace the test that pinned the global fallback for a Pipfile marker
with one asserting the opposite, and add CLI scans for agent, hosted
and vendored modes.

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

* Stop Pipenv projects falling back to system Python

When a Pipenv project had no Pipenv venv (a fresh checkout before
pipenv install, or a project that only has a plain venv/), scan read
the OS Python's site-packages instead. Agent mode then patched the
system Python in place and VEX attested the project as fixed (#504);
vendored mode tried to vendor system-only packages and failed with a
misleading 'run pipenv lock' error (#947).

A Pipenv project's env is only ever the one Pipenv resolves, so an
empty result there is final. Lock-only packages still come from
Pipfile.lock.

Fixes #504
Fixes #947

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Ported from #878 so CI on this PR runs against a green
base; it no-ops once #878 lands on main.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #956

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

* Quote scoped names in pnpm 7/8 vendored locks

Vendoring a scoped package (@scope/pkg) into a pnpm 7 (lock 5.4) or
pnpm 8 (lock 6.0) project wrote `name: @scope/pkg` into the rekeyed
packages entry. A bare `@` cannot start a YAML scalar, so pnpm refused
the whole lock with ERR_PNPM_BROKEN_LOCKFILE: every frozen install
failed after a scan that reported success, and lock-only VEX kept
attesting not_affected from a lock pnpm could not read.

The name is now written through the shared YAML scalar quoting, which
gives `name: '@scope/pkg'`, byte-identical to what pnpm 7.33.7 and
8.15.9 serialize themselves for the same override.

Tests: a byte-exact unit oracle captured from real pnpm 7/8 for
@isaacs/string-locale-compare (vendor, in-sync re-run, revert), and
scoped real-pnpm lifecycle legs (frozen install, moved checkout,
manifest-less VEX, revert) in e2e_vendor_pnpm_build, also run in the
pinned pnpm 7/8 matrix. Fixes #956.

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

* Skip scoped legacy leg on pnpm 8.0.0-8.1.0

pnpm 8.0.0 and 8.1.0 refuse their own lock for a scoped file: tarball
override under --frozen-lockfile (ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY
on the key they just wrote); 8.1.1 fixed it. Measured with real pnpm
on Node 16: the lock pnpm itself writes fails the same way, so no
vendored scoped lock can pass there. The pinned matrix keeps the
unscoped leg on those versions and runs the scoped leg everywhere else.

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

(cherry picked from commit 659ac2c)

Ported from #878 so this PR's CI is green while main's digest guard
test is red; it no-ops once #878 lands.

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

* Re-vendor rewrites a stale unquoted scoped name

edit_packages treated a packages entry as in sync once its file: key
and resolution matched, without looking at name:. A lock vendored by a
release before the #956 fix still carries `name: @scope/pkg`, which
pnpm 7/8 can't load, so a later vendor reported the package already
vendored and left the lock broken.

The in-sync check now also requires the canonical quoted name: line, so
the old spelling is rewritten like any other stale wiring. The new test
revendor_heals_an_unquoted_scoped_name fails without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UepoBazrbnBjy7HkD9YVJN

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #900

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

* Name the real cause when vendor --check fails

`vendor --check` reported every dead vendored entry as "no lockfile or
config references .socket/vendor/... any more; re-run `socket-patch
vendor`". In two common cases that was false and the remedy did
nothing, so the CI gate stayed red for good:

- Another lock (e.g. package-lock.json beside a wired yarn.lock or
  bun.lock) resolves the same version from the registry. The check now
  says the wiring is contested, names both locks, and says to delete
  the lock the project does not install from.
- The dependency left the lock (`npm uninstall` or an upgrade). The
  check now says the dependency was removed and points at
  `socket-patch scan --mode vendored --prune`, the command that reverts
  the entry. This matches scan's own hint.

Discovery now keeps the refs it drops as contested, so callers can
name the contesting lock. `vex`'s vendor_unwired phrase no longer
claims nothing wires the artifact when the cause is a contest or a
removed dependency.

Fixes #900

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

* Document vendor --check cause-specific reasons

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #974

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

* Run the musl binary on musl hosts in npm wrapper

Yarn classic ignores the `libc` field, so on Alpine it installs both
the -gnu and the -musl platform package. The npm wrapper always took
the first package that resolved (-gnu), whose glibc binary cannot
start on musl, and then exited 1 without printing anything. Every
socket-patch command failed silently in yarn classic projects on
Alpine and in node:*-alpine CI images.

The wrapper now detects the host libc (Node's runtime report, then
the musl loader probe scripts/install.sh uses) and tries the -musl
package first on musl. If a binary cannot be spawned it tries the
next installed candidate, and if none can run it prints the spawn
error instead of exiting silently.

Fixes #974

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
…884) (#901)

* Start fix for #884

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

* Refuse hosted runs from npm/yarn/bun members

A hosted scan or get run from a member of an npm, yarn (classic or
berry) or Bun workspace found the member's copy of a patched package,
saw no lockfile in the member directory, pinned nothing and exited 0
with "success". The package manager then installed the unpatched copy
from the workspace root's lockfile, and the only hint was a warning
about a missing package-lock.json.

The workspace-member pre-check only knew about pnpm and cargo. It now
also finds the nearest ancestor package.json whose "workspaces" list
matches the member directory. If that root holds a package-lock.json,
npm-shrinkwrap.json, yarn.lock, bun.lock or bun.lockb, the run is
refused before anything is written with
redirect_workspace_lockfile_elsewhere, and the message names the
workspace root to run from. Vendored mode already refused this layout.

Fixes #884

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

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

* Follow nested workspaces to the outer lock

A workspace root with no lockfile of its own can itself be a member of
an outer workspace (yarn berry's nested worktrees), where the outer
root holds the lockfile both use. The member check stopped at the inner
root and let the hosted run report success with nothing pinned. It now
keeps walking with the inner root as the member and refuses at the
outer root.

Refs #884

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

* Stop the member walk at a nested pnpm root

A pnpm workspace nested inside an outer yarn or npm workspace owns its
members: pnpm installs them from the nested pnpm-lock.yaml. The member
walk treated that nested root as lockless and went on to the outer
root, so the refusal named the wrong directory to run from. The walk
now stops at a root holding any npm-family lock (pnpm, vlt, Rush) and
leaves it to the pnpm check, which names the right root.

Refs #884

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

* Refuse at the nearest workspace lock root

The previous change stopped the member walk at a nested root holding a
pnpm, vlt or shrinkwrap.yaml lock and left it to the pnpm check. That
check only knows pnpm workspaces with pnpm-workspace.yaml or
lockfile-dir, so a stray pnpm-lock.yaml there could still let a hosted
run from the member report success with nothing pinned.

The pnpm check now runs first, so a pnpm workspace still gets its own
precise message. The package.json walk then refuses at the first
matching root that holds any npm-family lock, naming that root. Only a
Rush root, whose locks live under common/config, ends the walk without
a refusal.

Refs #884

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

* Prefer the nearer root over an outer pnpm one

When a yarn or npm workspace sits inside a pnpm workspace, the member's
lockfile is the nearer one. The pnpm check ran first and named the outer
pnpm root, so a follow-up run from there would rewrite pnpm-lock.yaml
and leave the member's real lockfile unpatched.

The member check now weighs both roots and names the nearer one. When
they are the same directory, pnpm's own message wins.

Refs #884

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

* Count only npm, yarn and Bun locks at roots

pnpm reads only pnpm-workspace.yaml and vlt only vlt.json, so a pnpm or
vlt lock sitting at a package.json "workspaces" root does not govern
that root's members. Counting such a stray lock as ownership let it
beat the outer pnpm workspace that really installs the member, and the
refusal pointed at a directory whose run would rewrite the wrong
lockfile.

The workspaces walk now counts only npm, yarn and Bun locks. Roots that
pnpm governs are left to the pnpm check, and when both kinds govern a
member the nearer root still wins.

Refs #884

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

* Drop unrelated rustfmt churn from the #884 fix

f92cb6a ran a workspace-wide cargo fmt, reformatting 118 files that the
fix does not otherwise touch. That buried the real change in ~2,300
lines of formatting diff and invites merge conflicts with every other
open PR. Restore those files to their merge-base versions; each was
checked to be rustfmt-equivalent to its main version, so behavior is
unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Start fix for #701, #932

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

* Stop hosted PyPI pinning platform-only wheels

When the patch service granted a PyPI patch as a platform- or
ABI-tagged wheel (for example cp311 manylinux), hosted scan pinned
that one wheel into the project's cross-platform lock: uv.lock, PEP
723 script locks, pylock.toml, Pipfile.lock, poetry.lock, pdm.lock,
requirements.txt or Hatch's pyproject. It reported success, but
installs then failed on every other Python version, OS and
architecture, and hosted rollback refused to undo it.

Hosted mode now checks the granted wheel's tags once, where every
PyPI lock writer is dispatched. A platform-specific wheel is withheld
from all of them and reported with a redirect_pypi_platform_wheel
warning, the same way hosted gem refuses platform gems. Nothing is
written or attested for that patch; other patches in the run are
unaffected. The tag rule is the one vendored mode already uses for
vendor_platform_locked, now shared between both modes.

Fixes #701, #932.

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

* Keep vendored PyPI patch on a platform grant

A vendored PyPI package that a hosted scan takes over is reverted to
its registry entry first, and only then pinned to the hosted wheel.
With platform-tagged hosted wheels now refused, that order would strip
the live vendored patch and leave the package unpatched.

The takeover now asks the same platform-wheel check before it reverts
anything, so the package stays vendored and patched, and both the wet
run and the dry run name redirect_pypi_platform_wheel as the reason.

Refs #701, #932.

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

* Format the takeover test's hosted route

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

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

* Check the Pipenv refusal exit code without VEX

The new Pipenv platform-wheel test asserted exit 0 on a run that also
asked for --vex. With nothing pinned, VEX correctly fails with
manifest_not_found, so the run exits 1 and the coverage job failed.

Assert the hosted refusal's exit 0 on a plain scan, then run --vex
separately and check only that it attests nothing.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

Hosted Gradle snippet silently builds the unpatched jar in any project using dependency locking (gradle.lockfile), on every Gradle major and OS Vendored Maven on a project with both pom.xml and build.gradle reports success with no Gradle warning, the Gradle build keeps the unpatched jar, and VEX attests not_affected scan reports success with 0 packages on a resolved Gradle project because the Gradle cache (~/.gradle/caches/modules-2) is never crawled Hosted Gradle snippet is always Groovy DSL, so pasting it into a build.gradle.kts fails to compile Hosted Gradle exclusiveContent snippet is not fail-closed: a transitive request for the base version wins conflict resolution and the unpatched jar is used

4 participants