sbt, Mill and scala-cli support in agent, hosted and vendored modes - #690
Mikola Lysenko (mikolalysenko) wants to merge 13 commits into
Conversation
Rebuilt on the Gradle branch (f1b0079) from feat/sbt-support 4cd40baa (backup: refs/backup/sbt-support-pre-rebase-20261003); the shared JVM seam (a549d5eb = aca8b1c) is already in the base. - Coursier and Ivy cache crawlers plugged into the shared JVM cache seam: JvmCacheLayout::classify, push_classified, the Scala-tool project gate, pom-only Coursier dirs are no copies; the roots are listed after the Gradle caches and ~/.m2. - sbt / scala-cli resolution evidence readers, the sbt owned-file templates (byte-exact renderer, strict parser) and the sbt gate. - Hosted sbt: the socket-patch.sbt rewriter and restorer (Format::Sbt beside Format::Gradle), confirmed/refused sbt uuid sets on RewriteResult (printed only when non-empty, like the Gradle sets), Mill / scala-cli snippet guidance, hosted sbt reads. - Vendored sbt (socket-patch-vendor.sbt over the suffixed tree) and scala-cli (owned socket-patch.scala over a same-GAV Coursier tree) as Shape::Sbt / Shape::ScalaCli, plugged into the Detected builds value. - The Coursier sidecar resync, called from the one Maven sidecar arm next to the ~/.m2 checksum files. - VEX discovery of the sbt files. The earlier "patch every cached copy of a Maven GAV" hunk (62c9552b) is dropped: the Gradle branch's every-copy fan-out (get_maven_copy_paths, JvmScope, installed_copies) replaces it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- vendor: the vendored sbt / scala-cli gate runs before a hosted pin is restored (a pin it would stop stays hosted), and the eject snapshot also captures the scala-cli owned files. - scan --mode hosted: the sbt build files are among the hosted reads. - vex: a hosted sbt pin is looked up in its own download dir first, then in every Maven copy (get_maven_copy_paths). - Test isolation: COURSIER_CACHE and SBT_OPTS join jvm_env::AMBIENT and COURSIER_CACHE jvm_env::EXPLICIT, replacing the separate scrub in common/mod.rs. - The sbt / Mill / scala-cli CLI suites: hosted, vendored, VEX, the real sbt and scala-cli drivers, and the Docker agent cells. - mismatch_blob_gaps_gates_each_maven_copy: a Coursier copy holding the only classifier jar still queues its blobs (through the every-copy gate the Gradle branch added). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- ci.yml: the sbt capstone rows carry jvm_tool: sbt (the JVM toolchain step keys on it), the blocking sbt slice in coverage-docker and the e2e-docker sbt cells; test_ci_e2e_tiers.py accepts sbt rows. - sbt-compatibility.yml, scripts/sbt-compat-matrix.sh, scripts/sbt-warm-seed.sh and tests/docker/Dockerfile.sbt. - .gitattributes: the sbt evidence fixtures are read byte for byte (-text). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The contract (hosted / vendored sbt, vendored scala-cli, the Coursier / Ivy agent caches), the changelog, the ecosystem guide (the Scala build tools section beside the Gradle one), usage, the design and probe docs and the sbt compatibility page, merged with the Gradle entries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- gradle_cache::installed_copies(_detailed) gains a sibling Ivy branch: an Ivy artifact dir (`jars/`, `bundles/`, `orbits/` beside an ivy-<rev>.xml) is expanded over its module's type dirs, so a `?classifier=sources` record finds `srcs/<a>-<v>-sources.jar`. `expands()` names both kinds; apply_maven_base, the rollback targets and vex_copy_sets use it where they expanded Gradle version dirs only. - JvmScope::split marks only `~/.m2` copies m2_ignored: a Coursier or Ivy copy is always a consumed copy, also beside a Gradle-only build. - push_classified drops a root that is a symlinked spelling of one already listed, so the fan-out never patches the same files twice (replaces the dropped distinct_physical_copies). - locate_artifact reads Coursier (per-repository roots) and Ivy caches. - Tests: Ivy expansion and Coursier / Ivy locate_artifact units, the symlinked-root dedupe, the hermetic crawler tests pin GRADLE_USER_HOME; e2e_sbt agent cells for VEX re-hashing every copy (m2 + Coursier + Ivy) and an Ivy sources jar patched / restored under srcs/; the m2 checksum test now expects the WP2 `.sha1` rewrite (never a Coursier sidecar). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- CLI_CONTRACT, CHANGELOG and the ecosystem guide: Coursier / Ivy copies join the Gradle every-copy fan-out and VEX; each Coursier repository root is its own copy; the Ivy srcs/ lookup. - registry test: the sbt probes are checked among the Gradle ones. - scala_cache_tests: a Gradle-only build without mavenLocal() reads no ~/.m2 (the Gradle m2 gate). - .gitattributes: the sbt harness scripts are checked out LF (they run under bash on the Windows sbt legs). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- hosted: a uuid the sbt rewriter refused is never confirmed by the Gradle planner alone beside it (each build pins on its own, both ways). The pom.xml + sbt rule (pom pin confirms) is unchanged. - crawler: an sbt / Mill / scala-cli build beside a Gradle build keeps ~/.m2 (M2Gate::NotGradleOnly): the Gradle scripts' silence on mavenLocal() says nothing about the sbt build's resolvers. - crawler: get_maven_copy_paths dedupes roots by canonical path, so a symlinked spelling of ~/.m2 is never a second copy. - apply: Ivy type dirs no variant matches are skipped like ~/.m2 and Coursier copies, not failed as gradle_copy_unexpected_bytes (only a Gradle hash dir's name proves pristine bytes). - apply: an Ivy copy holding none of a record's files is package_not_installed, as a Gradle version dir is, not "no matching variant found". - apply: gradle_m2_may_be_unconsumed names only ~/.m2 copies and fires only when one is consumed. - vex: a lone, partly unpatched Ivy copy is named (vex_gradle_unpatched_copy), as a lone Gradle version dir is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
- Test evidence synthesis (sbt_common::record_pinned_resolution and the core cache_roots test) built `file://` + Path::display(), which on Windows yields `file://\\?\C:\...`: backslashes are invalid JSON escapes, so every synthesized update record read as malformed. Build real `file:///C:/...` URIs (verbatim prefix dropped, percent-encoded). - location_path: accept `file://localhost/...` and UNC authorities (`file://srv/share/x`, Java's `file:////srv/share/x`); pin the Windows spellings sbt writes (`file:/C:/...`, `file:///C:/...`, %20, 8.3 names) with string-level tests that run on every OS. - scala-cli is_stale: drop the verbatim prefix from the canonical root so bloop's `C:\...` source paths strip against it. - export_classpath selftests: use this OS's absolute spelling and classpath separator (`C:/...`, `;` on Windows). - scala_evidence test: date a directory through a backup-semantics write handle on Windows (File::open on a directory is Access denied there). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Ivy POM fallback skips layout check
- Added validation to ensure installed_dir is an Ivy artifact directory (jars/, bundles/, or orbits/) before walking to parent, preventing layout escape attacks where malicious POMs could be planted in shared parent directories.
Or push these changes by commenting:
@cursor push ecd6b3d40e
Preview (ecd6b3d40e)
diff --git a/crates/socket-patch-core/src/crawlers/ivy_cache.rs b/crates/socket-patch-core/src/crawlers/ivy_cache.rs
--- a/crates/socket-patch-core/src/crawlers/ivy_cache.rs
+++ b/crates/socket-patch-core/src/crawlers/ivy_cache.rs
@@ -176,6 +176,15 @@
if let Ok(bytes) = read_regular_to_bytes_sync(&installed_dir.join(format!("{a}-{v}.pom"))) {
return Some(bytes);
}
+ // SECURITY: verify installed_dir is an Ivy artifact directory before
+ // walking to its parent, to prevent layout escape attacks.
+ let is_artifact_dir = installed_dir
+ .file_name()
+ .and_then(|n| n.to_str())
+ .is_some_and(|n| ARTIFACT_DIRS.contains(&n));
+ if !is_artifact_dir {
+ return None;
+ }
let original = installed_dir
.parent()?
.join(format!("ivy-{v}.xml.original"));You can send follow-ups to the cloud agent here.
…n re-check - ivy_cache::installed_pom: only an Ivy artifact directory (jars/, bundles/, orbits/ beside an ivy-<rev>.xml) may fall back to the parent's ivy-<v>.xml.original. A Maven2/Coursier version dir or other path would otherwise read a sibling of an unrelated parent. - vendor eject snapshot: read captured files through the FIFO-safe read_regular_to_bytes, so a FIFO or device planted at one of the scala-cli / Coursier owned paths fails the eject instead of hanging it. - sbt rewriter: an existing pin that fails its re-check (unverifiable, shadowed, resolved elsewhere) is now recorded in refused_sbt_uuids, so on a mixed sbt + Gradle root the Gradle planner cannot confirm the uuid alone while sbt may still resolve the unpatched base. Goldens updated. - sbt e2e tests: stop printing uuid-derived strings in panic messages (CodeQL rust/cleartext-logging), as the sibling e2e tests do. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
CodeQL still traces the patch uuid from the subcommand arguments into the captured stdout/stderr, so printing either stream re-raises rust/cleartext-logging. Report the subcommand and exit code only. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
CodeQL taints the exit code and serde error too, since both come from a run whose arguments carry the patch uuid; a static message is the only form that clears rust/cleartext-logging. Co-Authored-By: Claude <noreply@anthropic.com>
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6a358e6. Configure here.
|
Ready for review (burn-down agent). This is stacked on #646, so review and merge #646 first.
Generated by Claude Code |

This adds sbt, Mill and scala-cli support for
pkg:mavenpatches in agent, hosted and vendored modes. It is stacked on #646 (Gradle), so review and merge #646 first; this PR's diff is againstfeat/gradle-support.In every mode the user's own build files (
build.sbtand friends) are left untouched. socket-patch only writes files it owns.How each mode works
useCoursier := false). Coursier's checksum sidecars are resynced.socket-patch.sbtsocket-patch-vendor.sbtand a committed.socket/vendor/maven2treesocket-patch.scalaplus a same-GAV Coursier tree (directory builds)Hosted and vendored sbt. Both modes use a generated file with these properties, all verified by probe on every sbt line:
inThisBuild, so it reaches every subproject and transitive dependency, and never adds a dependency.<v>-socket.<hex8>version withdependencyOverrides. This is needed because Ivy and Coursier both rank the plain release above the suffix.Scoping. sbt has no lockfile, and
dependencyOverrideswould downgrade a project that resolves a newer version. Hosted and vendored modes therefore pin a package only when sbt's own resolution records undertarget/show the project resolves exactly the patched base version.Background:
docs/design/sbt-support.md,docs/design/sbt-template-probe.mdanddocs/design/sbt-evidence-probe.md. New codes are listed inCLI_CONTRACT.md.Test coverage
These are the real-tool docker runs (
scripts/sbt-compat-matrix.sh):useCoursier := falseBefore this rebase, the full matrix also passed with JDK 21 legs for 1.13.0 and 2.0.9.
The hermetic suites (
e2e_sbt,e2e_sbt_hosted,e2e_sbt_vendor,e2e_scala_cli_vendor), the golden harnessredirect_sbt_goldenand the vendored VEX cells all pass.CI changes:
sbt-compatibility.yml: a docker matrix, scala-tools legs, and native macOS and Windows legs.ci.yml.Known limits
installed_copies.mode_migration_npm::berry_vendored_then_hosted_takeover_leaves_pure_hostedfails locally on main as well. Fix yarn berry hosted pin leaking npm auth (#404) #465 changed the hosted Berry pin and the test was never updated. It only runs when real yarn is installed.🤖 Generated with Claude Code
Note
High Risk
Large surface area in Maven patch apply/rollback, cache crawling, and hosted/vendored gating that can mis-patch or leave builds in inconsistent states; extensive real-tool CI reduces but does not eliminate regression risk.
Overview
Adds sbt (0.13.18+), Mill, and scala-cli as Maven-shaped JVM projects across agent, hosted, and vendored modes without editing user build files.
Agent discovers and patches artifacts in Coursier and Ivy caches (alongside
~/.m2), fans out patches to every copy like Gradle, resyncs Coursier checksum sidecars after apply/rollback, and treats Ivy module dirs like Gradle cache expansion in apply/rollback/VEX. Hosted sbt writes onlysocket-patch.sbt, gated ontarget/resolution evidence (update-cache JSON, Ivy reports, scala-cli Bloop). Vendored sbt addssocket-patch-vendor.sbtand.socket/vendor/maven2; scala-cli gets ownedsocket-patch.scalaplus a Coursier vendor tree. Mill/scala-cli hosted paths stay manual snippets; vendored Mill remains docs-only.Vendor/eject runs JVM build-evidence preflight so hosted pins are not dropped when vendoring would refuse; commit hints name generated root files. VEX gains sbt hosted/vendored diagnostics (
sbt_resolution_unverified,vendored_tree_missing, etc.).CI:
sbt-compatibility.yml(full version × mode × JDK matrix, Mill, scala-cli, macOS/Windows native);ci.ymladds blocking Docker sbt agent (1.2.8, 1.13.0) and ubuntu e2e hosted/vendored 1.13.0. Newdocker_e2e_sbt, hermetice2e_sbt/ VEX suites, resolution-evidence fixtures, and.gitattributes/ script LF rules for byte-exact tests.Reviewed by Cursor Bugbot for commit 6a358e6. Configure here.
Generated by Claude Code