sbt, Mill and scala-cli support in agent, hosted and vendored modes - #690
Mikola Lysenko (mikolalysenko) wants to merge 18 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 |
|
Ready for review (burn-down agent). This is stacked on #646, so review and merge #646 first.
Generated by Claude Code |
Tanmay Singla (Tanmay182003)
left a comment
There was a problem hiding this comment.
[agent] I reviewed this PR and left inline comments on the bugs I verified in the code. Two more issues are outside the diff hunks:
- Rolling back a jar swap doesn't resync Coursier sidecars. Apply resyncs
.a-1.jar__sha1/.computedviadispatch_fixup_with→coursier::fixup, butrollback_jar_swap(patch/jvm_jar.rs~L781) only runsmaven_sidecars::resync. A rolled-back member-keyed jar in a Coursier cache is left beside the patched jar's checksum/digest pair. That contradicts CLI_CONTRACT.md, which says the CLI resyncs them "after apply and rollback". A second rollback hits the already-Readyearly return, so it doesn't repair it either. - A cut-short evidence walk gives the wrong warning. When the evidence scan fails,
distillreturns a doc withdeps_digest = "".digestis thenSome(""), notNone, so every new pin is refused asredirect_sbt_unsafe_value("not 8 lowercase hex digits") instead of the run-levelno_resolution_evidencewarning that carriesread_error. It fails closed, but the diagnosis is wrong.
Lower-confidence items I didn't verify as fully: takeover pre-checks only the gate, so later sbt planner refusals can leave a pin neither hosted nor vendored (commands/vendor.rs ~L2523-2560); holds_artifact skips pom-only GAV dirs in Coursier caches; --global-prefix <coursier v1> keeps only the first repository's copy (or_insert).
… scans - sbt VEX verify now also requires what check_new requires of a new pin: fresh evidence, evidence for every declared project, statically readable project definitions and no dependencyOverrides reassignment. Otherwise a partial `sbt core/test` could attest a pin that `app` overrides away. - The hosted writer's created-over-existing guard now covers socket-patch.sbt (redirect_sbt_owned_file_unreadable), so an unreadable user file is not overwritten and later deleted by the restore. - The vendored scala-cli gate checks repository directives before the already-vendored early returns, so adding `//> using repository` refuses before the next build refreshes the evidence. - sbt vendored detect leaves a scala-cli-wired root on the scala-cli backend instead of re-planning it as sbt and orphaning its wiring. - xml_unescape bounds its `;` search to entity length and build-source line numbers come from one newline index, removing two quadratic scans over repo-controlled input. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KHkK9FFA1HRxPF21XndGW
`get` fails a whole-run refusal with exit 1 (scan exits 0), so the test runs the CLI directly instead of through get_hosted's exit-0 assertion. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KHkK9FFA1HRxPF21XndGW
|
bugbot run Generated by Claude Code |
|
bugbot run Generated by Claude Code |
Hosted confirmation already treats both as unverified; standalone vex now withholds the lockfile basis for them too, so it cannot attest a pin whose repository was dropped or whose lock would reject the suffix. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018KHkK9FFA1HRxPF21XndGW
|
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 650f561. Configure here.
|
Burn-down agent: labeled Ready for review.
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
Medium Risk
Touches JVM cache patching, hosted/vendored wiring, and eject flows where mistakes could leave builds half-patched or break offline resolution; mitigated by extensive CI matrices and evidence gates, but scope spans security-sensitive dependency integrity paths.
Overview
Adds Maven patch support for Scala builds (sbt 0.13.18+, Mill, scala-cli) across agent, hosted, and vendored modes without editing user
build.sbt/ Mill / scala-cli sources.Agent mode 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 tightens Gradle-onlymavenLocal()warnings so Coursier/Ivy paths are not treated as unconsumed~/.m2.Hosted mode wires sbt through a single generated
socket-patch.sbt(pins, local repo, load-time verification), gated on resolution evidence undertarget/; Mill and scala-cli get manual snippets. Vendored mode addssocket-patch-vendor.sbt/ scala-cli Coursier trees under.socket/vendor/, with shared evidence gates, new refusal/warning codes, and safer hosted→vendored eject (preflight +eject_incompletebackstop).CI & tests:
sbtjoins docker e2e/coverage matrices; newsbt-compatibility.yml(version × JDK × mode matrix, Mill, scala-cli, macOS/Windows);Dockerfile.sbt, warm-seed scripts, byte-exact fixtures in.gitattributes. Hermetice2e_sbt*,docker_e2e_sbt, and VEX sbt modules exercise the flows. Docs (CHANGELOG,README,CLI_CONTRACT) describe behavior and limits.Reviewed by Cursor Bugbot for commit 650f561. Configure here.
Generated by Claude Code