Spawn CLI test children through one hermetic Command builder (#823) - #850
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Test children inherited ambient SOCKET_* settings through 15 private scrub_socket_env copies and 10 unscrubbed spawners, so a developer's shell (SOCKET_DRY_RUN, SOCKET_OFFLINE, ...) could silently change what a suite exercises. common/hermetic.rs now holds the one builder: hermetic::command seeds and scrubs SOCKET_* and forces SOCKET_NO_CONFIG and SOCKET_NO_UPDATE_CHECK; scrub_extra adds the opt-in yarn, pnpm and venv sweeps. run_bin_with_env is built on it. This moves the 8 copies and 8 unscrubbed spawners that no open fix PR touches onto the builder and deletes those copies. spawn_env_hygiene tests the builder's contract and ratchets the remaining copies and bare binary spawns. Test-only; no production change. Refs #823 Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
Ambient SOCKET_DRY_RUN failed the real-apply leg of apply_dry_run_with_real_patch_verifies_without_mutating; the cli target now gives the same result with or without it. Refs #823 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] The This isn't caused by this PR. Those 2 tests fail identically on Generated by Claude Code |
|
BugBot review 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 efa5cde. Configure here.
Resolves the tests/common/mod.rs conflict with main's hermetic test command builder (#850): run_bin_with_env now starts from hermetic_command and keeps jvm_env::isolate_cli on top. Co-Authored-By: Claude <noreply@anthropic.com>
Main's spawn_env_hygiene ratchet (#850) rejects new bare binary spawns. The six Gradle/Maven suites now build their socket-patch children with the shared hermetic builder, which replaces their private SOCKET_* scrub loops; run_bin_with_env keeps the JVM isolation on top of it. Co-Authored-By: Claude <noreply@anthropic.com>
Main's spawn_env_hygiene ratchet (#850) rejects new bare binary spawns; the two dotenv-view spawns in in_process_redirect_pipenv.rs now start from the shared hermetic builder and keep their own PIPENV_* and venv scrubs on top. Co-Authored-By: Claude <noreply@anthropic.com>
* Add a shared JVM cache-root and project-dependency seam
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>
* Extract the real-Gradle test harness into gradle_build_common
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>
* Extract the hosted Maven API fake into hosted_maven_common
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>
* Isolate CLI test children from ambient Gradle and JVM caches
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>
* Read Gradle 9's launcher JVM banner and take extra Gradle args
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>
* Add a deterministic fake Maven Central for the JVM capstones
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>
* Scaffold the Gradle CI tiers around a jvm_tool matrix key
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>
* Clear the clippy lints the new JVM test helpers introduced
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>
* Isolate the autocrlf clone from global git config and test it
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>
* Add a pure Gradle model: tokenizer, line endings, version selectors
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>
* Add Gradle user-home resolution and dependency-lock parsing
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>
* Add the Gradle script graph and its queries
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>
* Make the Gradle classpath print tasks configuration-cache safe
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>
* Key the Gradle prefix guard and zero-test checks per suite
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>
* Close fail-unsafe gaps in the Gradle script graph
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 :a: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>
* Resolve the Gradle home like the JVM and read wrapper init.d
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>
* Match Gradle's range patterns and single-value exact selectors
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>
* Crawl Gradle's files-2.1 cache again
aca8b1c8 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>
* Expand Gradle version dirs into their hash-dir copies
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>
* Discover Gradle caches and gate ~/.m2 on mavenLocal()
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>
* Report Gradle discovery in scan and add the discovery suite
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>
* Resolve Maven PURLs from ~/.m2 first
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>
* Harden Gradle discovery's safety probes and PURL lookup
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>
* Check every Gradle copy in scan and tighten its Gradle notes
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>
* Add the JVM jar swap and Gradle-aware verification to core
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>
* Fix vendored Gradle wiring gaps and share its wiring API
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>
* Patch every Maven copy a build consumes in agent mode
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>
* Re-hash every consumed JVM copy when generating VEX
VEX judged a Maven purl by its first copy, so a pristine Gradle cache
entry beside a patched ~/.m2 (or the reverse) still got a statement. The
Maven copy set is now every copy a build consumes: ~/.m2 unless the
Gradle build never reads it, each Gradle cache with its hash dirs
expanded, and the read-only cache. Every copy is re-hashed at VEX time,
and a copy Gradle derived from the pristine jar outside files-2.1 also
withholds the statement. Each such copy is named in a
vex_gradle_unpatched_copy warning.
The hosted arm probes the Gradle caches for the suffixed version too,
expands their hash dirs, and checks member-keyed records against the
suffixed jar. Record keys keep their package/ prefix when renamed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Test agent mode over hermetic Gradle caches and m2 sidecars
gradle_agent_cli drives the real CLI over temp-dir Gradle user homes,
~/.m2 and read-only caches. It covers every hash copy being patched and
restored, the #551 m2-only refusal (the designated regression test),
mavenLocal() in the build or in init.d, verification metadata, read-only
shadows, stale transforms, unexpected pristine bytes, offline and
service-backed member-keyed swaps through repair and rollback,
--global-prefix, remove, get narrowing in files-2.1, and a vendored
Gradle entry that must not be flagged out of sync.
maven_sidecar_cli pins the ~/.m2 .sha1/.md5 contract for a pom-only
patch: rewritten when they matched, never created, a mismatched one
left alone, and every rollback byte-exact.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Treat a Gradle copy without the record's files as not installed
A Gradle version dir that holds only the pom of a jar patch (or another
classifier) is not an install of that record. The Maven arm failed the
run with "no matching variant found" for it; the variant now stays
unmatched and is reported as package_not_installed, the same as any
other package that is not on disk. An ~/.m2 copy that matches no
variant is still an error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Add the real-Gradle agent suite and cache canaries
e2e_gradle_agent_build lets real Gradle resolve the fake Central into a
per-test user home, runs socket-patch against it, and asserts on the jar
the printRuntimeClasspath task actually consumes. It covers the #551
tree, mavenLocal() in the build and in init.d, the #264 whole-jar swap
and its rollback, byte-exact rollback and remove, the verification
refusal, the read-only cache, --global-prefix, --refresh-dependencies
against VEX, and a Windows-only daemon lock.
gradle_agent_cache_semantics_canaries records each major's cache
behaviour in a probe report: hash-dir naming, --offline reuse, daemons,
classifier hash dirs, build logic, and a --build-cache jar task.
Measured locally on 6.9.4, 7.6.6, 8.14.3 and 9.8.0: every release drops
the leading zero, reuses the patched jar offline, keeps it on refresh,
and rebuilds build logic and the build-cache jar from it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Keep committed classifier jars and quiet in-sync Gradle re-runs
A re-run that dropped a classifier jar the committed tree already serves
would leave an unindexed file in the version directory, which the settings
script refuses, so committed classifiers stay until revert. Informational
notes (ide_sources_unavailable, range_declared) describe what a vendoring
did and are no longer repeated by an in-sync re-run. The Gradle ancestor
check of not_build_root only applies to Gradle projects, so a Maven-only
project below a Gradle root with computed includes is not refused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Test vendored Gradle fixes through the CLI and real Gradle
vendor_jvm_cli.rs gains one hermetic case per issue (#395 #428 #429 #461
#487 #511 #533), each failing on the base commit, plus repairs of a
mixed root, a classifier jar and the derived maven-metadata.xml.
The new e2e_vendor_gradle_build suite drives real Gradle against the fake
Central and asserts on the bytes Gradle consumes: mixed root (with Maven),
subproject refusal, autocrlf clones, exclusiveContent in a subproject, a
convention plugin and an applied script, pgp-only verification entries,
ranges/rich/catalog selectors, classifier and IDE sources, vendoring with
no Maven repository, a second patch under the configuration cache, and
ports of the bug-hunt scenarios. It passes on 6.9.4, 7.6.6, 8.14.3 and
9.8.0.
The multi-project capstone now expects the new owned files, and since
both suites of the vendor rows have landed their allow_empty scaffold is
dropped (test_ci_e2e_tiers follows the landed rule).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Document the vendored Gradle contract
Gradle caches are a discovery and sourcing source, mixed roots and
not_build_root, owned EOL-blind files, derived metadata, classifiers,
graph-wide checks, pgp-only entries, VEX liveness and repair; the
out-of-scope list and CI coverage claims are corrected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix WP2 review findings in the JVM jar swap and Gradle guards
The WP2 review found that apply, rollback and VEX disagree in a few places, and that several hazards were never tested.
- find_backup rejected every backup of a record that adds a member: an
absent member (None) never equalled its empty beforeHash.
- A member-keyed swap ran once per consumed copy. Its "put back the
copies already swapped" guarantee therefore stopped at one version
dir, and the service jar was downloaded once per copy. It now runs
once per variant across every consumed copy (~/.m2 and Gradle alike).
- gradle_copy_unexpected_bytes only warned, so a consumed copy left
unpatched ended as package_not_installed with exit 0. A hash dir that
holds a variant's files is now a refusal that fails the run, both
when its bytes match no variant (including a qualified key's variant
gate) and when its pristine bytes are not the record's.
- Rollback restored ~/.m2 copies that a Gradle-only build never reads,
which unpatched another build's apply. It now restores only
JvmScope's consumed copies, as apply writes them.
- VEX counted a pom-only Gradle version dir as an unpatched copy, while
apply calls it not installed. Such dirs are now dropped from the copy
set.
- Instrumented build-logic jars are never byte-equal to their input,
so they withheld VEX forever. A derived copy no older than the
patched jar was made from it (Gradle keys these caches by content)
and no longer counts. Clearing old copies and rebuilding now leads to
a statement, as the real-Gradle canary asserts on 6.9 through 9.8.
- The derived-cache walk ran per purl x hash dir x root. Any incomplete
walk (a large Android transforms cache) withheld VEX permanently.
The walk is now indexed once per Gradle home per run and keeps only
the candidate files. An incomplete walk is a warning
(vex_gradle_derived_cache_unchecked), not a withholding.
- On Windows, a rename over a jar held open without FILE_SHARE_DELETE
fails with ERROR_ACCESS_DENIED, not a sharing violation, so
gradle_jar_locked_by_daemon was never raised. Codes 5 (on a writable
existing target) and 303 are now matched too, and rollback writes
map the lock the same way.
New tests: unit tests for a tampered service jar (bad afterHash,
altered or extra member, SRI mismatch) and for rolling back an added
member. Hermetic CLI tests cover these cases:
- a tampered service jar
- a failed swap that restores every copy (macOS uchg)
- a Windows held jar
- unexpected bytes in the only copy, and a qualified variant mismatch
- the rollback scope
- gradle_rollback_hash_mismatch
- instrumented copies
- VEX over a pom-only dir
The canary now asserts gradle_daemon_stale and VEX after the
build-logic rebuild.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Close vendored Gradle review gaps in VEX, repair and legacy roots
Review of WP3 found paths where vendored Gradle state could be
misreported or damaged:
- A single-pom root vendored on the single-pom backend before mixed
roots became one JVM entry would re-route to Shape::Mixed with no
migration, carrying the legacy <repository> record into a JVM entry
(unrevertable) or orphaning it. Such roots now stay on the single-pom
backend, with a legacy_maven_root degraded warning telling the user to
revert and vendor again.
- classifier_unpatched_copy was computed only by the caller, so VEX
liveness (which re-plans from the committed tree) still attested a
package whose declared classifier jar carries the vulnerable member.
The planner now runs the check from JvmPatch.patched_members, the
tree marker lists the patched members beside classifier jars, and
liveness fails closed when classifier jars exist without that list.
- repair swapped a mixed root's Gradle tree without the symlink check
the artifact path gets, and checked_tree_jar reported an escaping
tree file as missing (a repair trigger). Both now refuse with
vendor_path_unsafe.
- An ancestor settings file that is not UTF-8 was treated as absent by
not_build_root; it now refuses like an unparseable one.
- A deleted owned .gitattributes alone never triggered repair; repair
now rewrites the derived metadata and owned .gitattributes for a
healthy JVM entry, without a download.
LocalSources gets unit tests for its trust rules (Gradle hash dirs,
m2 .sha1 sidecars, authenticated lookups, crawler-root derivation),
since the CLI fixtures mirror every local cache through the registry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Clear clippy warnings in the vendored Gradle capstones
The WP3 suite added a duplicate dead_code allow (maven_build_common
already carries an inner one) and compiled the markup-stripping regex
once per report file; both tripped clippy on the integration branch.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Plan hosted Gradle wiring with an owned settings script
A Gradle build used to get only a paste-able exclusiveContent snippet,
which pinned nothing and claimed a fail-closedness it did not have.
Hosted mode now wires the build itself, ledger-free:
- .socket/gradle/socket-patch.hosted.settings.gradle, static bytes
pinned by a snapshot test, applied from every build's settings
(root, buildSrc, literal included builds) with the index digest on the
apply line so a changed index invalidates the configuration cache;
- .socket/gradle/hosted-index.tsv, one row per GA;
- the base entry of every build's lock files moved to the suffixed
version, and an existing verification-metadata.xml given the suffixed
component.
The script routes the suffixed version to its Socket repository with
exclusiveContent, substitutes every request whose selector admits the
base (dependency substitution and eachDependency, through a Groovy port
of gradle::selector), rejects every other candidate at or below the base
and trips on anything that still resolves there or on a jar whose
sha256 is not the pinned one. Versions above the base may resolve.
The planner refuses (writing nothing, printing a per-DSL fallback with
the pin, substitution and reject block under
redirect_gradle_manual_snippet) what it cannot pin safely: same-GAV
grants, Gradle below 6.8, Android/KMP, non-literal includeBuild,
classifier and strictly-excluding declarations, vendored or
exclusive-content conflicts, settings-classpath GAs, third-version or
custom-location locks and a malformed index.
The hosted engine reads the whole script graph and every build's lock
files before the rewrite, and confirms a maven purl in a Gradle build
only from the planner's own report (a pom beside it must pin it too);
Gradle files never confirm by substring. The format registry lists the
Gradle rows in their real roles.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Discover hosted Gradle pins from the owned index
VEX, rollback, vendor eject and the takeover classifier read hosted
state off the project files, so a Gradle build wired by the hosted
planner has to be discoverable. Each hosted-index row becomes a hosted
ref only while the build consumes it: the owned script is ours (line
endings ignored), every build's settings applies it with the CURRENT
index digest, the row's url is Socket-hosted and names the row's uuid,
every lock entry of the GA is the suffixed version and no build script
moves its lock file. Anything else is patched_ref_invalid.
A Gradle ref never takes the not-installed lockfile basis: the script
lets a version above the base resolve, so only the installed suffixed
copies are evidence. Build scripts and locks are read without the
identity sweep, so a pasted snippet is never mistaken for wiring.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Restore hosted Gradle pins to upstream without the network
rollback, remove and the vendor takeover unwind hosted pins through
restore_upstream, which had no Gradle format, so a hosted Gradle pin
could only be refused. Everything the hosted planner wrote is derivable
from its index row, so the restore needs no registry lookup: each lock
entry of the GA moves back from the suffixed version to the base, the
row leaves the index and the suffixed verification component goes when
it is still exactly what the planner wrote (otherwise it stays with
gradle_verification_component_left). The last row takes the index, the
owned script, every apply line (and a settings file left empty) and the
owned .gitattributes (kept while the vendored script lives beside it)
with it; otherwise the apply lines carry the new digest. Line endings
are preserved, and a wire-then-restore round trip is byte-identical.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Take over vendored Gradle entries and snapshot Gradle files on eject
scan --mode hosted reverted a still-vendored purl before redirecting it
only for cargo, npm and golang, so a maven purl vendored into a Gradle
build stayed vendored and the hosted planner refused it
(redirect_gradle_vendored_conflict). A vendored JVM entry wired into a
Gradle build is now taken over like the others: its revert unplans the
vendored Gradle wiring first. A pom-only vendored entry is unchanged.
vendor's eject already finds Gradle pins through discovery; its
rollback snapshot now also covers the hosted and vendored Gradle owned
files and verification-metadata.xml, so a failed eject puts them back.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Leave Gradle resolution failures to Gradle and fix eject rollback
The hosted script's tripwire hashes the suffixed jar through an artifact
view in afterResolve. A strict view rethrew any resolution failure (a
stale lock, an outage) from the socket-patch script, so the user saw our
script blamed for Gradle's own error; the view is now lenient and only
the pinned-bytes check fails from it.
A failed eject restores its snapshot, but the upstream restore can have
removed .socket/gradle/ along with the hosted files in it, so putting
them back failed with "No such file or directory" and the eject
reported eject_rollback_failed. The snapshot restore now recreates a
file's directory first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Test hosted Gradle against real Gradle 6.9 through 9.8
e2e_redirect_gradle_build drives scan --mode hosted over projects that
resolve the deterministic fake Central, with the Socket repository served
at its production path (the mirror init script maps patch.socket.dev
onto it) and a suffixed .module like the patch service's. Every
assertion is on the jar Gradle consumes. The 38 gradle_hosted_ tests
cover direct, transitive (#347) and range (#511) requests, every lock
mode and Gradle 6's legacy locks (#396), Kotlin (#348), catalogs,
buildscript and buildSrc classpaths, included builds with locks,
repositories modes, verification metadata, platforms and BOMs, outage
and tamper failures, a stale lock, the configuration cache, detached
configurations (recorded), rescan, rollback and remove round trips, an
autocrlf clone (#429), the vendored takeover and eject, VEX before and
after the build, a pasted snippet, the fallback snippets in both DSLs
and the Groovy selector port against the golden tables. All pass on
6.9.4/JDK11, 7.6.6/JDK17, 8.14.3/JDK21 and 9.8.0/JDK21.
With the hosted suite landed, every suite of the ubuntu agent+hosted CI
rows exists, so those rows drop allow_empty (test_ci_gradle_prefixes
requires it).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Gate Gradle takeovers and keep user-owned settings files on restore
Hosted scan reverted a vendored Gradle entry before the hosted planner had
a chance to refuse it (a custom lockFile, a settings-classpath GA, a
same-GAV or incomplete grant), destroying a working vendored patch and
leaving the build on the unpatched upstream. The planner's refusals are
now exposed as gradle::takeover_refusal and checked against the build on
disk before any revert, like the bun, berry and vlt gates.
The restore deleted any settings file left empty after its apply line
went, including an empty settings.gradle the user committed to mark a
build root. A settings file the planner creates now carries a `created`
mark after the digest, and only those are deleted.
A GA locked in a settings-gradle.lockfile (pulled in by a settings plugin)
is refused as a settings-classpath dependency: the hosted script runs
after that classpath resolves, so rewriting the lock broke every build.
Discovery never treats such a lock entry as valid wiring.
The eject snapshot also covers every Gradle build's settings and lock
files, so a failed eject undoes whatever the vendor step wrote there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Make the hosted Gradle e2e tests prove what they claim
The fallback-snippet tests accepted a script compile error as a "loud"
failure, so a broken Kotlin snippet passed; a direct request must now
resolve the patched jar, and no failure may come from the pasted script.
The configuration-cache test now adds a second index row (a patched
consumer from its own Socket repository) to a cached build and checks the
entry is invalidated and both GAs resolve patched. The stale-lock test
installs the suffixed jar first, so only discovery's lock check can
withhold the attestation. New tests cover a refused vendored takeover
keeping the vendored patch, and a failed eject rolling back a multi-build
project byte-exactly.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Run the Gradle multi-project capstone without Maven
The capstone seeded ~/.m2 through Maven because the crawler could only
read a maven2 repository. Since WP1 the crawler reads Gradle's files-2.1
cache, and the vendor plan sources parent poms and imported BOM metadata
from it, so the seed only hid whether a Gradle-only machine works.
The test now reads the registry jar from the cache the lock-writing
build fills, points the CLI at that GRADLE_USER_HOME, and keeps an empty
m2 so the user's own repository is never consulted. CI no longer
installs Maven for gradle_multi_project rows; only gradle_vendor_395's
mixed root still needs it, so the windows multi-project row now runs
Maven-free. Every Gradle row has landed all its suites, so the
allow_empty note in ci.yml now says none may set it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Document every Gradle code in the CLI contract and test it
Hosted mode now writes Gradle wiring (the owned settings script, its
index, settings apply lines, lock and verification-metadata edits), yet
the contract still described the pasted snippet, and none of the codes
WP1-WP4 added were listed. Consumers dispatch on these codes, so each
needs a stable meaning.
A new "Gradle builds (v5.0)" section covers discovery (with the level
field and inLock), agent mode (including gradle_copy_unexpected_bytes
now failing the run and vex_gradle_derived_cache_unchecked), hosted
confirmation rules, refusals and edit kinds, vendored reasons, and VEX.
The hosted paragraphs and the discovery table point at it.
contract_gradle_codes.rs scans the non-test source of both crates for
Gradle/JVM code literals, the Gradle sidecar advisory variants and the
vendored Gradle reasons, and fails when one is missing from the
contract, so a new code cannot ship undocumented.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Document Gradle support in the ecosystem guide, testing and changelog
ecosystems.md still said hosted Gradle was a manual snippet that was
fail-closed by repository exclusivity (#347 and #396 showed it was not)
and that Maven sidecars were left stale. It now has a Gradle section on
every mode: discovery and the mavenLocal gate, agent-mode guards
(read-only cache shadowing, transform-copy staleness, daemon locks),
the hosted script and its detached-configuration and module-metadata
limits, vendored mode and VEX. The Maven row and sidecar caveat match
the new behaviour.
docs/testing/README.md gains the Gradle suites, the PR and full-tier
matrix, the probe reports and how to run the suites locally. The
changelog lists the breaking hosted change, the new Gradle support and
the fixed issues. The JVM design doc no longer claims hosted mode is
unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Scan all non-test source in the Gradle contract test
The test cut each file at the first `#[cfg(test)]\nmod `, which also
matches out-of-line `#[cfg(test)] mod x;` declarations near the top of
files. That dropped almost all of patch/redirect/mod.rs, maven_crawler.rs,
vex/mod.rs, vendor/mod.rs and others from the scan, and on a CRLF
checkout the needle never matched, so inline test modules were scanned
instead. Fold CRLF to LF and cut only at an inline `mod name {` body.
The vendored-reason scan also missed reasons emitted from
vendor/maven_repo.rs and vendor/jvm/apply.rs (not_build_root,
legacy_maven_root, ide_sources_unavailable, build_file_outside_root,
...). Scan those files too, allow-list the Maven-reactor-only
maven_config_changed, and add self-checks that fail if the scan stops
reaching redirect/mod.rs or maven_repo.rs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Claim only the Gradle platforms actually tested
The gradle-compatibility.yml grid has not run yet, so the Linux and
Windows cells are unverified. The changelog now lists macOS only, and
the ecosystem guide describes the grid's coverage rather than a result.
Widen the changelog once the grid is green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Fix Gradle CI: 6.9 JDK ceiling 15, LF hosted script, CC-safe printSources, CodeQL
- gradle-compatibility.yml: Gradle 6.x runs on Java <= 15 (16 needs 7.0),
so the 6.9.4 ceiling rows use JDK 15.
- .gitattributes: the hosted settings script is include_str!-embedded and
written byte for byte, so check it out -text like the vendored one
(Windows autocrlf embedded CRLF bytes).
- e2e_vendor_gradle_build: printSources resolves in a provider, not the
task action, so the --configuration-cache cell accepts it.
- jvm_fixture_repo: keep the signing-key path out of the gpg assert message.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Run the selector golden check in every hosted cell; widen the grid's path filter
The core gradle_selector_golden test only ran with a Gradle launcher set,
which no CI job running socket-patch-core tests provides, so the golden
tables were never checked against real Gradle in CI. It moves into
e2e_redirect_gradle_build as gradle_hosted_selector_golden_tables_match_real_gradle,
which every hosted cell runs.
gradle-compatibility.yml's PR path filter now also covers the upstream
restore, sidecar advisories, VEX, hosted engine, the CLI's Gradle join
sites and the shared test harness every grid suite compiles.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Gradle agent: restore unread m2 copies, judge partly-held copies, probe hash dirs
- rollback/remove restore a ~/.m2 copy a Gradle-only build no longer reads
(patched by an earlier apply); skipping it reported success while the
shared jar stayed patched and remove dropped the record.
- A Gradle version dir holding only some of a leaf record's files is an
install: apply patches the held files and fails the missing ones as not
found (as on ~/.m2), and vex keeps the copy so it withholds.
- mismatch_blob_gaps expands Gradle version dirs into their hash dirs, so a
drifted Gradle copy queues the afterHash blob the Warn policy needs.
- New gradle_m2_may_be_unconsumed warning when a Gradle-only build reading
mavenLocal() has only the ~/.m2 copy patched.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Gradle hosted/vendored: review round 1 fixes
Hosted:
- A rescan sets the pom and module entries of an existing suffixed
verification component, so a component written before the service
served the suffixed .module gains it (Gradle fails verification of the
.module it then downloads otherwise).
- A new patch of the same GAV drops the replaced patch's verification
component when it is still as written (else
redirect_gradle_verification_component_left).
- Locks and strictly versions above the base are no longer conflicts
(decision 3): only versions at or below the base refuse.
- The fallback snippet follows the owned script's rules (rewrite only
selectors admitting the base, reject only candidates at or below it,
no strictly / implementation line) instead of forcing every request
down to the patched base.
- New redirect_gradle_dynamic_selector_pinned: a dynamic or range
selector admitting the base is pinned like a lock; docs no longer
promise VEX withholding for unlocked builds.
- Discovery re-runs the planner's build- and GA-level refusals
(settings classpath, unresolved includeBuild, Android/KMP, classifier,
exclusiveContent), so a build changed after the scan stops attesting.
Vendored:
- The verification-file revert is line-ending blind (autocrlf checkouts
left the patched jar hash behind and broke the upstream build).
- Classifier jars the tree serves get their upstream sha256 in an
existing verification file (no .asc is served).
- A non-UTF-8 gradle-index.tsv is unreadable, not absent: the revert of
one patch no longer unwires every other.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Gradle grid: JDK 15 ceiling from Zulu (Temurin never shipped 15)
setup-java has no Temurin 15, so the 6.9.4 ceiling rows take Zulu 15; the
CI-tier tests now pin the 6.9 <= 15 ceiling.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Gradle: review round 2 fixes (m2 rollback, above-base locks, latest.*, snippet, unplan UTF-8)
- rollback/remove: a ~/.m2 copy a Gradle-only build does not read never
fails the run. One holding foreign bytes, missing a file, or a swapped
jar with no backup in this project is left as it is with the new
gradle_m2_copy_not_restored warning, and remove still drops the record.
- Hosted discovery and the planner now agree on a lock above the base:
the row stays a ref (rollback, remove and list find the pin) and is
marked Discovery::unattested, which vex omits as
vex_gradle_lock_above_base. Tested plan -> discovery -> restore.
- latest.release / latest.integration are refused
(redirect_gradle_latest_selector) instead of reported pinned: the pin
cannot rewrite them and the build would fail to resolve.
- The fallback snippet substitutes on strictly/require/prefer like the
owned script, so a prefer-only rich version resolves the patch
(real Gradle 6.9.4 and 9.8.0, both DSLs).
- Vendored unplan refuses a non-UTF-8 settings script or verification
file like the index, instead of dropping it and unwiring the rest.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Gradle e2e: retry a build whose single-use daemon died unanswered
Windows runners intermittently fail a --no-daemon build with
DaemonInitialConnectException ("The first result from the daemon was
empty") before the build reports anything: seen on the agent and hosted
8.14.3 Windows cells of the last two heads, in different tests and steps,
and once more on rerun. The harness now retries that exact launcher
failure (twice at most); every other failure is returned unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Gradle: review round 3 (unreadable m2 copy, unreadable hosted build files)
- rollback/remove: an unconsumed ~/.m2 copy whose file exists but cannot
be read or stat'd is no longer downgraded to
gradle_m2_copy_not_restored; it may still hold the patched bytes, so
the run fails and `remove` keeps the record and its before-blobs. Only
a hash mismatch or a truly absent file (VerifyRollbackResult::is_absent)
skips the copy.
- hosted Gradle: a settings/build/catalog/lock file the script graph
reaches that exists but cannot be read as UTF-8 text (non-UTF-8 bytes,
permissions, not a regular file, content not provided in memory) is
recorded as unreadable, not absent, in both readers (engine
read_gradle_files, read_build_from_disk). The planner and the takeover
check refuse the build with redirect_gradle_build_file_unreadable
instead of "creating" a one-line settings.gradle over the user's.
- The hosted writer refuses to write a settings file it never read over
one on disk (defense in depth).
- Tests: chmod-000 unconsumed m2 copy keeps the record; engine test for
Latin-1 / mode-000 / in-memory unreadable settings; real-Gradle e2e
(6.9.4, 8.14.3) for a Latin-1 settings.gradle left byte-identical.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Read Gradle cache and build files through the FIFO-safe reader
The Gradle apply, rollback and discovery paths read jars, init scripts,
build scripts and wrapper properties from user-writable caches and
workspaces with bare fs::read. A FIFO squatting one of those paths makes
open(2) block waiting for a writer, so apply, rollback or scan could hang
forever. Route these reads through read_regular_to_bytes(_sync), which
opens with O_NONBLOCK and rejects non-regular files on the handle, and add
a unix regression test that a FIFO leaf in a Gradle hash dir is skipped
promptly.
Co-Authored-By: Claude <noreply@anthropic.com>
* Open the FIFO watchdog's writer non-blocking in the Gradle FIFO test
If the timeout fires with no reader parked on the FIFO, a blocking
write-open would wait forever and hang the suite instead of failing it.
Match release_fifo_reader in cargo_config.rs, which already uses O_NONBLOCK.
Co-Authored-By: Claude <noreply@anthropic.com>
* Refuse a non-pristine Gradle before-blob before writing; drop header-sized jar alloc
Gradle rollback checked that the restored file hashes to its hash
directory only after rollback_package_patch had already written the
before-blob, so the "left as it is" refusal was false: the file held
the non-pristine bytes and a second rollback succeeded as "already
original". Check every before-blob against the directory name first and
refuse without writing; the post-restore check stays as a backstop.
verify_member_bytes sized its buffer from the jar entry's own header, so
a crafted jar claiming a huge uncompressed size aborted the process on
allocation instead of reading as NotFound. Grow the buffer as read.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018KHkK9FFA1HRxPF21XndGW
* Predict the Gradle before-blob refusal on dry runs; guard the blob read
The pre-write pristine check is read-only, so it now runs on --dry-run
too and the preview shows the gradle_rollback_hash_mismatch refusal the
wet run gives. It also only reads a before-blob named by a valid 64-hex
hash that is a regular file (not a symlink), the same guards
verify_file_rollback applies; anything else is left to the engine,
which refuses it without reading through it.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018KHkK9FFA1HRxPF21XndGW
* Drop CHANGELOG entry from this PR
Release notes are written when a release is cut, from the merged PR
log and the code, so PRs no longer edit CHANGELOG.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Spawn the Gradle and Maven test CLIs through hermetic::command
Main's spawn_env_hygiene ratchet (#850) rejects new bare binary spawns.
The six Gradle/Maven suites now build their socket-patch children with
the shared hermetic builder, which replaces their private SOCKET_* scrub
loops; run_bin_with_env keeps the JVM isolation on top of it.
Co-Authored-By: Claude <noreply@anthropic.com>
* Read the eject snapshot through the FIFO-safe opener
EjectSnapshot::read used a bare tokio::fs::read, so a FIFO or device at a
snapshotted path (now including Gradle settings, locks and .socket/gradle
files) blocked open(2) and hung vendor eject. read_regular_to_bytes fails
fast on a non-regular file, so the snapshot errors and the eject refuses.
Co-Authored-By: Claude <noreply@anthropic.com>
* Read Gradle derived-cache jars through the FIFO-safe reader
derived_copies_in hashed the hash-dir jar and each unmatched derived copy
under the Gradle user home with a bare std::fs::read. Those are
user-writable cache files, so a FIFO or device there blocked open(2)
and wedged apply/VEX after the cache walk itself had used the safe
reader. read_regular_to_bytes_sync fails fast; an unreadable copy stays
unverified, as before.
Co-Authored-By: Claude <noreply@anthropic.com>
* Qualify std::process::Command in the macOS chflags test
b041571 moved the CLI spawns to hermetic::command and dropped the
std::process::Command import, but the macOS-only failed_swap_restores_every_copy
test still calls Command::new("chflags"), so the gradle_agent_cli test
target no longer compiles on macOS (test and e2e-build jobs red).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Start fix for #645, #546 Assisted-by: Claude Code:claude-opus-5-5 * Find the Pipenv venv that .env and old Pipenv use Agent mode and hosted stale-install checks now find the venv Pipenv really uses in two cases where they used to miss it, patch the system interpreter instead, and let VEX attest not_affected: - WORKON_HOME, PIPENV_CUSTOM_VENV_NAME or PIPENV_VENV_IN_PROJECT set in the project's .env (or PIPENV_DOTENV_LOCATION), which every Pipenv command loads before it picks the venv (#546). - An explicit "not in project" setting next to a ./.venv directory. Only Pipenv 2023.11.14+ skips ./.venv then; 2018.11 to 2023.10.24 still use it, so both venvs are now patched (#645). Assisted-by: Claude Code:claude-opus-5-5 * Test Pipenv .env and .venv discovery end to end Scan-level regressions for both fixes: a .env-named venv is scanned (and PIPENV_DONT_LOAD_ENV turns that off), and an explicit "not in project" setting now scans ./.venv as well as the WORKON_HOME venv. The Pipenv compatibility doc describes the new discovery rules. Assisted-by: Claude Code:claude-opus-5-5 * Fix Pipenv .env test and escapes on Windows The new .env test removed the whole WORKON_HOME on Windows, where site-packages sits one level shallower, so the .env-named venv went with it. .env values now decode exactly python-dotenv's escapes, so a backslash in a quoted Windows path is kept as written. Assisted-by: Claude Code:claude-opus-5-5 * fix(pipenv): honor complete dotenv settings views * fix(pipenv): include cached legacy shell environments * Spawn the pipenv redirect test CLI through hermetic::command Main's spawn_env_hygiene ratchet (#850) rejects new bare binary spawns; the two dotenv-view spawns in in_process_redirect_pipenv.rs now start from the shared hermetic builder and keep their own PIPENV_* and venv scrubs on top. Co-Authored-By: Claude <noreply@anthropic.com> * Resolve a relative WORKON_HOME from the Pipenv project The modern dotenv and process settings views passed WORKON_HOME through unjoined, so a relative value resolved against socket-patch's own cwd instead of the project Pipenv runs from (the legacy cached view already joined it). Under --cwd that missed the project's venv. Join it to the project directory in the shared helper, as the legacy path does. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: mikolalysenko <mik@socket.dev>
LLM Description written by Claude Code:claude-opus-5-5
Refs #823 (slice 1 of the first child of tracking #824). Issue #823 stays open for the files that open fix PRs touch.
Summary
This PR adds one hermetic
Commandbuilder for CLI test children,tests/common/hermetic.rs, and buildscommon::run_bin_with_envon it. Every spawner file that no open fix PR touches now goes through it, and their privatescrub_socket_envcopies are deleted. A newspawn_env_hygienetest target checks the builder's contract and acts as a ratchet, so no new copies or bare binary spawns can appear.Why
Register rows C30 (
scrub_socket_envin 15 files with 14 bodies) and C47 (10 files spawn the binary with noSOCKET_*scrub), in the living document Part 8. Leverage: B 0 · U 1 (first child of #824) · D 8 copies deleted plus 7 unscrubbed spawners made hermetic · R low (test-only). Score ≈ 10, the top of the refactor queue.What changed
common/hermetic.rs(new, standalone likecache_env.rs):command(bin)/binary_command(): seeds the 9 hostileSOCKET_*values and then removes them, removesSOCKET_API_TOKEN, sweeps every other ambientSOCKET_*(keeping the telemetry opt-outs), and forcesSOCKET_NO_CONFIG=1andSOCKET_NO_UPDATE_CHECK=1.scrub_socket_vars: the prefix sweep alone, for package-manager children.scrub_extra(&[Extra::Venv | Extra::Yarn | Extra::Pnpm]): the opt-in per-PM sweeps, with the yarn and pnpm seed-then-scrub guards kept.common::run_bin_with_envis nowhermetic_command(bin)+ args + caller env +output(), andcommon::hermetic_commandis re-exported.e2e_yarn_legacy_cachekey_refusal_build,e2e_vendor_yarn_classic_dev_flow,e2e_redirect_pnpm_build,e2e_vendor_pnpm_build,get/get_batch_paths_e2e,mode_migration_npm,ecosystem_dispatch_e2e(including its hand-rolledGLOBAL_ARG_ENV_VARSscrub inrun_scrubbed),e2e_redirect_rush_sim.repair/repair_vendor_e2e,scan/scan_sync_e2e,cli/api_client_errors_e2e,cli_parse_remove,self_update_e2e,update/covgap_update_swap, pluscli/cli_dry_run_paths_e2e(not in the issue's list, but ambientSOCKET_DRY_RUNfailed it).self_update_failures_e2eandupdate/covgap_update_downloadalready went throughrun_installed.spawn_env_hygiene(new target):SOCKET_NO_CONFIGkept or removed,SOCKET_NO_UPDATE_CHECKforced or not, case ofnpm_config_*, which PM vars are swept, and whether the seeds leak.PENDING_SCRUB_COPIES(7) andPENDING_RAW_SPAWNS(148). Both fail on a new entry and on a stale one, so the lists can only shrink.efa5cdeports #851 (twovex_consumedalias tests thatmain@4646693broke, which turnedcoveragered). It becomes a no-op once Fix vex alias tests broken by store-copy merge #851 lands.Left for later slices (on the allowlists):
scrub_socket_envcopies andscan/scan_invariants/apply/in_process_npm_multicopyare changed by open fix PRs Fix yarn berry hosted pin of catalog deps (#632) #763, Fix store-copy fold dropping copy writes (#756, #772) #774, Fix vendored mode replacing symlinked lockfiles (#627) #802, Fix vlt 1.3 brotli lock nodes being refused (#372) #820, Fix vendored npm-family tarballs dropped by .gitignore (#831) #837, Fix hosted yarn classic with an offline mirror (#364) #839 and Fix vendored gem rewrite breaking positional args (#847) #849.Deleted (
git diff --stat)src/commands/vex_consumed.rs(+15 / −4).common/mod.rs: +94 / −322 (net −228).hermetic.rs+149,spawn_env_hygiene.rs+502 (≈150 of those are the allowlist).Behavior
No production change. In the migrated test children:
SOCKET_NO_CONFIG=1andSOCKET_NO_UPDATE_CHECK=1are forced. Several copies dropped the cargo-configSOCKET_NO_CONFIG, and none forcedSOCKET_NO_UPDATE_CHECK.SOCKET_*seeds are applied everywhere.PM children keep their former sweeps: yarn, pnpm and venv, plus the rush file's explicit
npm_config_store_dir/PNPM_HOMEand dev_flow'sYARN_CACHE_FOLDERremovals.Test evidence
cargo clippy --workspace --all-features -- -D warnings: clean.cargo test -p socket-patch-cli --all-features --tests --no-run: no warnings.cargo test -p socket-patch-cli --all-features --lib: 840/840 with the Fix vex alias tests broken by store-copy merge #851 port (2 fail onmain).Suites, branch:
spawn_env_hygiene9/9,cli_parse_remove19,ecosystem_dispatch_e2e21,scan108,cli86,get75,update55,self_update_e2e27,mode_migration_npm15,e2e_redirect_pnpm_build11,e2e_vendor_pnpm_build20,e2e_vendor_yarn_classic_dev_flow11,e2e_yarn_legacy_cachekey_refusal_build15,e2e_redirect_rush_sim9: all pass.repair: 114 pass, 2 fail. The 2 failures are the known root-only tests (repair_exits_zero_and_stays_quiet_when_lock_file_unremovable,repair_cleanup_failure_is_reported_in_json_and_silent_modes), which chmod a directory and fail in the root sandbox onmaintoo.Red → green (ambient var):
mainSOCKET_DRY_RUN=true cargo test --test repairSOCKET_DRY_RUN=true cargo test --test cliSOCKET_OFFLINE=true cargo test --test cliSOCKET_OFFLINE=true cargo test --test repairscanstill fails under ambient vars only inscan_invariants, which is on the allowlist pending Fix vlt 1.3 brotli lock nodes being refused (#372) #820/Fix vendored gem rewrite breaking positional args (#847) #849.CI on
efa5cde: 414 checks passed and 0 failed as of 13:16Z, includingcoverage,clippy,test (windows-latest)andtest (macos-latest). Some macOS e2e legs were still queued. Bugbot found no issues.Risk
Low. The change is test-only, and the env differences are limited to the forced opt-outs and seeds above, which
common::run*callers already had.🤖 Generated with Claude Code
https://claude.ai/code/session_01Cs49XgUuutoPh15zrPGaTy
Note
Low Risk
Test-only refactor of subprocess environment setup; no production code paths change. Residual risk is limited to test flakiness if hermetic scrubbing diverges from prior per-file behavior.
Overview
Introduces
tests/common/hermetic.rsas the single way CLI integration tests spawnsocket-patch(and related package-manager children): hostileSOCKET_*seed-then-scrub, sweep of ambientSOCKET_*vars, forcedSOCKET_NO_CONFIG/SOCKET_NO_UPDATE_CHECK, and opt-inExtra::Venv | Yarn | Pnpmscrubs.common::run_bin_with_envandcommon::hermetic_commandnow delegate to this builder; migrated suites drop duplicatedscrub_socket_envhelpers and replace bareCommand::new(binary())withhermetic::command.Adds
spawn_env_hygienecontract tests plus allowlist ratchets (PENDING_SCRUB_COPIES,PENDING_RAW_SPAWNS) so new private scrubs or unhermetic spawns fail CI. Several e2e/CLI/get/scan/repair/update tests are wired through the shared helper in this slice.Separately,
vex_consumednpm hosted tests are adjusted for post-#605 behavior where the name-keyed resolver already finds aliases/store peers: tests still exercise alias expansion with an alias-free installed map and assert the resolver’s full copy set matches expectations.Reviewed by Cursor Bugbot for commit efa5cde. Configure here.
Generated by Claude Code