Skip to content

sbt, Mill and scala-cli support in agent, hosted and vendored modes - #690

Open
Mikola Lysenko (mikolalysenko) wants to merge 13 commits into
feat/gradle-supportfrom
feat/sbt-support
Open

Mikola Lysenko (mikolalysenko) wants to merge 13 commits into
feat/gradle-supportfrom
feat/sbt-support

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

This adds sbt, Mill and scala-cli support for pkg:maven patches in agent, hosted and vendored modes. It is stacked on #646 (Gradle), so review and merge #646 first; this PR's diff is against feat/gradle-support.

In every mode the user's own build files (build.sbt and friends) are left untouched. socket-patch only writes files it owns.

How each mode works

Mode sbt (0.13.18, 1.x, 2.x) Mill scala-cli
Agent Patches jars in place in the Coursier cache (sbt 1.3+) and the Ivy cache (sbt 0.13–1.2, or useCoursier := false). Coursier's checksum sidecars are resynced. In place (Coursier) In place (Coursier)
Hosted Writes one owned root file, socket-patch.sbt Guidance snippet Guidance snippet
Vendored Writes socket-patch-vendor.sbt and a committed .socket/vendor/maven2 tree Guidance only (wiring it means editing user files) Owned socket-patch.scala plus 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:

  • Scope. It works through inThisBuild, so it reaches every subproject and transitive dependency, and never adds a dependency.
  • Version. It pins the suffixed <v>-socket.<hex8> version with dependencyOverrides. This is needed because Ivy and Coursier both rank the plain release above the suffix.
  • Integrity at load. It checks a sha256 for every patched jar and pom when sbt loads. A wrong hash fails the load.
  • Integrity after resolution. A post-resolution check fails the build if a resolved jar's bytes aren't pinned, or if a project declares a newer version of a patched package.
  • Repository order. On 0.13 and 1.x it puts its repository first, so offline builds resolve.

Scoping. sbt has no lockfile, and dependencyOverrides would downgrade a project that resolves a newer version. Hosted and vendored modes therefore pin a package only when sbt's own resolution records under target/ show the project resolves exactly the patched base version.

  • With no records, or stale ones, the run warns once and wires nothing.
  • socket-patch never runs sbt.

Background: docs/design/sbt-support.md, docs/design/sbt-template-probe.md and docs/design/sbt-evidence-probe.md. New codes are listed in CLI_CONTRACT.md.

Test coverage

These are the real-tool docker runs (scripts/sbt-compat-matrix.sh):

Group Versions Result
hosted sbt 0.13.18, 1.2.8, 1.3.13 (JDK 8); 1.9.9, 1.13.0, 2.0.9 (JDK 17); 2.0.9 (JDK 21) 14/14 tests per cell
vendored same 8/8 tests per cell
agent sbt 0.13.18, 1.2.8, 1.13.0, 2.0.9; useCoursier := false pass
Mill agent 0.11.13, 0.12.17, 1.1.10 pass
scala-cli 1.17.1, agent and vendored pass

Before 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 harness redirect_sbt_golden and the vendored VEX cells all pass.

CI changes:

  • New sbt-compatibility.yml: a docker matrix, scala-tools legs, and native macOS and Windows legs.
  • New blocking sbt rows in ci.yml.

Known limits

🤖 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 only socket-patch.sbt, gated on target/ resolution evidence (update-cache JSON, Ivy reports, scala-cli Bloop). Vendored sbt adds socket-patch-vendor.sbt and .socket/vendor/maven2; scala-cli gets owned socket-patch.scala plus 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.yml adds blocking Docker sbt agent (1.2.8, 1.13.0) and ubuntu e2e hosted/vendored 1.13.0. New docker_e2e_sbt, hermetic e2e_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

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>
@socket-security

socket-security Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​sbt/​setup-sbt@​6158cb0903b8ceeae04f830055f3155e1b6a5ad710010090100100

View full report

@socket-security-staging

socket-security-staging Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub/​sbt/​setup-sbt@​6158cb0903b8ceeae04f830055f3155e1b6a5ad710010090100100

View full report

- 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>
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 3, 2026 14:18

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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.

Comment thread crates/socket-patch-core/src/crawlers/ivy_cache.rs
Comment thread crates/socket-patch-cli/src/commands/vendor.rs
Comment thread crates/socket-patch-core/src/patch/redirect/sbt.rs
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to stack #702 October 3, 2026 14:52
Comment thread crates/socket-patch-cli/tests/e2e_sbt_hosted.rs Fixed
Comment thread crates/socket-patch-cli/tests/e2e_sbt_vendor_build.rs Fixed
Comment thread crates/socket-patch-cli/tests/e2e_sbt_vendor_build.rs Fixed
…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>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-cli/tests/e2e_sbt_hosted.rs Fixed
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>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-cli/tests/e2e_sbt_hosted.rs Fixed
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>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 3, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review (burn-down agent). This is stacked on #646, so review and merge #646 first.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants