Skip to content

Stop attesting sbt pins a build reassignment or uncommitted wiring bypasses - #2030

Open
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
fix/sbt-hosted-fallback-and-liveness
Open

Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
fix/sbt-hosted-fallback-and-liveness

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Owner rule: VEX must never attest a copy the build may not use. Review rule: withdraw a pin only when the build provably does not use the pinned bytes, never withdraw a pin that current evidence verifies, and never attest what isn't proven.

1. Hosted sbt: the installed-copy fallback

When the resolution evidence does not verify a hosted pin (verify_with / evidence_blocker in vex/discover/sbt.rs), the pin keeps its ref and loses only its lockfile basis. Before this PR, vex then attested it from the suffixed copy that sbt downloads into .socket/sbt-hosted (vex_consumed::maven_copies). That copy shows the hosted file loaded once. It does not show that the build uses those bytes.

Discovery now marks the ref Unattested with a new kind, UnattestedKind::SbtPinBypassed. The note and failed[].reason is vex_sbt_pin_bypassed. The ref stays discovered, so rollback, remove and list keep working. The mark is set only in two cases:

  • A dependencyOverrides reassignment (:= / ~= / --=) in a build source. It replaces the generated override, so that project resolves its declared version, or the load-time verifier fails its update. Either way the pinned bytes are not used. This is read from the build sources even when there is no evidence.
  • An onLoad reassignment that drops the generated installer (Global / onLoad :=, onLoad in Global :=, any scope, := / ~= / --=) in a build source. The installer holds the update verifier and the resolver order (pin repository first) that the resolver and forced-version decisions below rely on. A := that reads onLoad's own .value (the generated idiom, (Global / onLoad).value andThen …), or a ~= that composes (andThen / compose), keeps the installer and is fine. This is read from the build sources even when there is no evidence.
  • A current resolution record judged exactly as the hosted rewriter re-checks an existing pin (gate::check_existing):
    • The GA's base version is still resolved, so the override is shadowed.
    • The pinned version resolves from outside the pin repository with bytes that are not pinned.
    • The hash predicate treats a file that cannot be read as no proof.

A record is "current" when it is dated after the generated files and after every build source. The evidence walk now records a date for each record file (SbtEvidence::record_dates, generated, newest_source). Current records that verify the pin (verify over the current view) always keep it. The bypass check never runs when the lockfile basis stands.

Decision for each blocker:

Condition Installed-copy fallback Why
dependencyOverrides reassigned withdrawn, unless current records verify the pin The override is replaced where the reassignment applies. A self-referencing := dependencyOverrides.value is kept whenever current evidence verifies the pin.
onLoad reassigned without keeping its previous value withdrawn, unless current records verify the pin It drops the installer: no update verifier, no pin-repository-first order. Nothing stops another copy or a forced version from building silently. It is also an evidence blocker.
onLoad composed (:= (Global / onLoad).value andThen …, ~= (_ andThen …)) kept The installer stays.
resolvers / externalResolvers / fullResolvers reassigned kept, unless onLoad is also dropped The override still forces the Socket-only version, and the verifier checks its bytes, so the build runs the pinned bytes or fails.
sbt.override.build.repos=true kept, unless onLoad is also dropped Same reason as resolvers.
Current record still resolves the base version withdrawn Same rule as redirect_sbt_override_shadowed.
Current record resolves the pinned version from outside the pin repository with unpinned bytes withdrawn Same rule as redirect_sbt_resolved_elsewhere.
Current record resolves some other forced version kept, unless onLoad is dropped The rewriter judges this case the same way, and the verifier fails that update.
Record older than the wiring or a source (a project or cross build not re-resolved) kept It may predate the pin.
Stale or partial evidence, project definitions that cannot be read statically, build.sbt.lock, an evicted or unreadable artifact kept These are not proof. They only withhold the lockfile basis.

A file named socket-patch.sbt or socket-patch-vendor.sbt without socket-patch's header is a root definition that sbt loads. The evidence walk now lists it as a build source, and scan_build_sources reads it (owned_file::is_foreign). Socket-patch's own generated file is still never a source.

2. sbt / scala-cli vendored liveness: committed files only

What I audited:

  • sbt wired_checked (there is no sbt wired): a strict parse of the committed socket-patch-vendor.sbt plus the pin row. It never re-plans, so the Stop vendored Gradle liveness attesting a version the lockfile overrides #2004 flaw (pending writes masking missing committed wiring) cannot occur. check_entry (vendor --check) re-plans and fails on any pending write, which is the safe direction.
  • scala-cli wired_checked: the index row plus byte-exact socket-patch.scala and guard. It never re-plans either.
  • entry_wired_checked (VEX liveness). Neither backend checked whether the committed files still route the build to the vendored copy. These now fail with a reason; the VEX plan omits the entry as vendor_jvm_shape_unsupported ("cannot establish JVM wiring: reason: …"):
    • sbt_pin_conflict: the committed hosted socket-patch.sbt does one of three things:
      • pins the same GA under another patch, so the verifier holds the GA to that hosted row;
      • no longer parses strictly;
      • is a user's own file under that name, which sbt loads like any root .sbt.
    • sbt_onload_reassigned: a build source (every one the evidence walk reads, or the planner's fixed set if the walk hits a cap) reassigns onLoad without keeping the previous value. That drops socket-patch-vendor.sbt's load-time verifier and resolver order.
    • scala_cli_tree_uncommitted: the tree's jar or pom is missing, has no index row of the patch, or does not hash to its row. Without the tree's pom, Coursier reads the module from the next repository (Central), unpatched jar included.
    • scala_cli_repository_shadowed: the gate's conditions (coursier_gate::vendored_bypass). An input declares a repository, or a direct url= for the patched group, or non-stale Bloop evidence from a build after vendoring resolved the GAV outside the tree (resolved_elsewhere).
  • vendor --revert and scan --prune keep the plain wiring test. check_entry uses the attest: false path, and so does entry_wired.
  • Checked and left alone:

Review findings

  1. Confirmed, fixed. The evidence was dated once for the whole build (only the newest record against the generated file). Records are now dated one by one. Test a_record_older_than_the_pin_never_withdraws_it (the 1.9.9 test-compile fixture, with b resolved before the pin and the other projects after) fails on the previous code.
  2. Confirmed, fixed. Current evidence that verifies the pin now always wins, and a pin with the lockfile basis skips the bypass check. Tests: bypass_needs_proof_from_current_records and the e2e hosted_vex_never_withdraws_a_pin_current_evidence_verifies (.sbtopts override.build.repos, and a self-referencing :=). Both fail with the guard removed.
  3. Confirmed, fixed. A foreign socket-patch.sbt made vendored liveness attest. It is now sbt_pin_conflict (liveness_refuses_another_committed_pin_of_the_ga). The evidence walk and scan_build_sources now read a foreign generated-name file (a_foreign_generated_name_is_scanned_as_a_build_source, plus the crawler staleness test).
  4. Confirmed, fixed. scala-cli liveness now applies resolved_elsewhere over non-stale evidence (liveness_needs_the_committed_tree_and_no_shadowing_repository, with on-disk Bloop evidence: current and outside the tree fails; stale passes; inside the tree passes).
  5. Confirmed, fixed. Resolver reassignments and sbt.override.build.repos are no longer a bypass (see the table). dependencyOverrides reassignment is kept: it removes the override itself, which the review allows.
  6. Refuted as a behavior bug: entry_wired maps every Err to true, so the attest path could not change its answer. Fixed anyway as wasted work: it now calls the attest: false path.
  7. Fixed. Build sources are scanned once in build_state and shared with evidence_blocker. The dependencyOverrides reason string lives in one place (overrides_reassigned).
  8. Fixed. bypass now calls gate::check_existing with a hash predicate. I tried widening check_existing to flag every non-pin version. That changed the hosted rewriter's and the vendored gate's verdicts (a test expected no advisory on stale 3.12.0 evidence), so I reverted it. VEX and the rewriter now judge evidence identically: base version shadowed, or bytes from elsewhere.
  9. Fixed. The tree check uses coursier_tree::{tree_dir, committed} and a new coursier_tree::patch_rows, which indexed now uses too.
  10. Fixed. directive_detail / elsewhere_detail return Option<String>. Only the gate wraps them in refuse (shadowed), so there are no dead GateStop arms.

Follow-up: onLoad reassignments. The decisions for findings 5 and 8 rely on the generated onLoad installer. That installer was not detected anywhere before, not even in the planners. Now:

  • scan_build_sources reports such edits as BuildFindings::onload_assignment, which round-trips through the resolution document.
  • VEX hosted treats it exactly like a dependencyOverrides reassignment: an evidence blocker, plus SbtPinBypassed unless current evidence verifies the pin.
  • Vendored liveness reports it as sbt_onload_reassigned.
  • The hosted rewriter's build_refusal refuses new pins and leaves existing pins unconfirmed (redirect_sbt_onload_assignment).
  • The vendored planner refuses a new pin (vendor_sbt_onload_assignment), the same way it handles the other reassignments.

Tests:

  • scan_finds_onload_reassignments_that_drop_the_installer
  • an_onload_reassignment_withdraws_like_an_overrides_one
  • the onLoad case of evidence_blocker_mirrors_the_new_pin_gate
  • liveness_refuses_an_onload_reassignment
  • the onLoad case of refusals_come_before_any_write
  • an_onload_reassignment_refuses_the_pin
  • the e2e cases in hosted_vex_installed_copy_is_withdrawn_by_an_overrides_reassignment (resolvers := with onLoad dropped is withdrawn; a composed onLoad is kept)

With the scan stubbed out, all six core tests fail.

Tests

Each new test fails with its fix stubbed out (I checked this per finding) and passes here. All of these are green:

Run Result
cargo test -p socket-patch-core --lib -- vex::discover:: vendor::jvm:: formats::sbt crawlers::sbt_evidence patch::redirect::sbt hosted::sbt_reads 788 passed
redirect_sbt_golden 2 passed
Golden corpus test 1 passed
e2e_vex_lockfile -- sbt 8 passed
--lib vex_sources 14 passed
e2e_sbt_hosted 32 passed
e2e_sbt_vendor 38 passed
e2e_scala_cli_vendor 32 passed
e2e_sbt_vendor_build 28 passed
e2e_sbt_build 25 passed
contract_gradle_codes passed
clippy -D warnings, fmt --check clean

Not done

  • A walk that hits a cap yields no static findings, so the fallback stays as before.
  • The onLoad scan is a static heuristic. A := that reads the previous .value but then discards it, or a ~= that mentions andThen without keeping its argument, counts as a composition.
  • No CHANGELOG entry (AGENTS.md).

Relation to #2008: this touches vex/discover/sbt.rs (extract, hosted_ref, evidence_blocker's signature) and UnattestedKind. Expect textual conflicts there.

Review findings, round 2 (/code-review high on 67b885d+68dec85, cloud routine)

Fixed in 03408ab.

  1. FALSE ATTESTATION: partial fresh evidence overruled a proven edit. Confirmed and fixed. bypass returned "no bypass" as soon as the current records verified the pin, without first checking for a proven dependencyOverrides or onLoad reassignment. Example: the root aggregates a and b, the user adds dependencyOverrides := Seq() to a, then runs only sbt b/update. b's fresh record verifies the pin, a's stale record is ignored, and the installed copy was attested even though a builds unpatched. Verified evidence now overrules an edit only when it covers the whole build: every record is current and every declared project has one (BuildState::whole). Test: bypass_needs_proof_from_current_records (new "partial" case).

  2. The onLoad keep-check was loose, and <<= was missed: confirmed and fixed.

    • := now needs the previous .value both read and composed. { val _ = (Global / onLoad).value; identity } is flagged.
    • ~= must compose its own argument. (_ => identity andThen f) is flagged; (_ andThen f) and (prev => prev.andThen(f)) are kept.
    • sbt 0.13's <<= is now recognized for onLoad, dependencyOverrides and the resolver settings.

    CLI_CONTRACT.md is updated to match. Test: scan_finds_onload_reassignments_that_drop_the_installer (extended).

Declined:

  • Scope of onLoad (ThisBuild / or project-scoped edits are flagged although sbt reads only Global). This only over-refuses, which is the fail-safe direction. Narrowing it needs sbt-version-specific scope rules. Left for a follow-up.
  • Bloop/scala-cli evidence older than a newly vendored GAV. This withholds attestation until the next build, which is also fail-safe.
  • A foreign generated-name file over the size cap. This is a corner case. The scan still fails closed on the evidence side.
  • A full evidence scan per vendored entry, and the duplicated detail text. Performance and wording only.
  • Another forced version (not the base) isn't proof. That's intentional: the load-time verifier fails that update.

Tests (cloud run): cargo test -p socket-patch-core --lib -- formats::sbt vex::discover:: crawlers::sbt crawlers::scala vendor::jvm patch::redirect::sbt gives 792 passed, golden included. CLI --test e2e_vex_lockfile sbt and --lib vex_sources pass. clippy and fmt are clean.

Landing note: this PR conflicts with #2008 in vex/discover/sbt.rs and UnattestedKind. Once #2008 lands, merge main in here and keep both PRs' behavior.


Note

Medium Risk
Alters VEX attestation rules and JVM vendor liveness; wrong heuristics could under- or over-attest, though coverage is broad and behavior aligns with hosted rewriter gates.

Overview
Hosted sbt VEX. When resolution evidence does not verify a hosted pin, vex no longer always attests from the suffixed copy under .socket/sbt-hosted. Discovery now marks the ref unattested with vex_sbt_pin_bypassed when the build provably bypasses the pin: a dependencyOverrides reassignment, an onLoad reassignment that drops the generated installer (unless it keeps the previous .value or composes with andThen / compose), or current resolution records dated after the wiring and sources that match the hosted rewriter’s shadowed-override or resolved-elsewhere cases. Per-record evidence timestamps (record_dates, generated, newest_source) decide what is “current”; verified current records always keep attestation.

Static analysis plumbing. scan_build_sources now collects onload_assignment, serialized through sbt resolution docs; hosted redirect refuses with redirect_sbt_onload_assignment, vendored planner with vendor_sbt_onload_assignment, and VEX evidence blockers reuse the same findings. Files named socket-patch.sbt / socket-patch-vendor.sbt without the socket-patch header are treated as foreign build definitions.

Vendored sbt / scala-cli liveness. entry_wired_checked (attestation path) adds attestability checks: conflicting or non-strict hosted pins, foreign hosted files, and onLoad bypass → vendor_jvm_shape_unsupported (sbt_pin_conflict, sbt_onload_reassigned); scala-cli checks committed tree rows vs index hashes and repository shadowing via coursier_gate::vendored_bypass. Non-attest callers use a lighter wiring check. coursier_tree::patch_rows supports shared indexing logic.

Docs & CLI. CLI_CONTRACT.md, docs/ecosystems.md, and vex_sources document the new unattested kind and codes; extensive new unit and e2e tests cover bypass scenarios, onLoad detection, and scala-cli shadowing.

Reviewed by Cursor Bugbot for commit 68dec85. Configure here.


Generated by Claude Code

…passes

Hosted sbt: a pin the resolution evidence does not verify keeps its ref
and loses only the lockfile basis, so `vex` still attested it from the
suffixed copy sbt downloaded into `.socket/sbt-hosted`. That copy proves
the hosted file loaded once, not that the build resolves it. Discovery
now also marks the ref unattested (new `UnattestedKind::SbtPinBypassed`,
VEX note `vex_sbt_pin_bypassed`) when the build provably does not apply
the pin:

- a build source reassigns `dependencyOverrides` (the generated
  override is replaced, so the build resolves its own version) or
  `resolvers`, or sets `sbt.override.build.repos=true` (the generated
  resolver is dropped). These are read from the build sources whether
  or not any evidence exists;
- evidence newer than every build source and the generated file records
  another version of the GA, or an artifact of the pinned version whose
  bytes are not pinned.

Stale or partial evidence, project definitions that cannot be read
statically, and a `build.sbt.lock` still only withhold the lockfile
basis: none of them stops the pin from applying.

Vendored sbt / scala-cli liveness: neither re-plans, so no pending write
masked missing wiring, but `entry_wired_checked` (the VEX liveness test)
judged only the wiring rows and owned files. It now also requires that
the committed files route the build to the vendored copy, failing with
a reason (`vendor_jvm_shape_unsupported`, "cannot establish JVM
wiring"); `vendor --revert` and `scan --prune` keep the plain wiring
test, and `vendor --check` is unchanged:

- `sbt_pin_conflict`: the committed hosted `socket-patch.sbt` pins the
  same GA under another patch (the load-time verifier holds the GA to
  the hosted row), or no longer parses strictly;
- `scala_cli_tree_uncommitted`: the index has no row for the patch's
  jar or pom, or the committed file is missing or does not hash to its
  row (without the tree's pom, Coursier reads the module from Central);
- `scala_cli_repository_shadowed`: an input declares a repository or a
  direct `url=` for the patched group, which `vendor` refuses but
  nothing re-checked once vendored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the hosted sbt installed-copy fallback and the vendored
sbt / scala-cli liveness checks.

Hosted sbt (`vex/discover/sbt.rs`):
- Date evidence per record: the crawler now records each library
  evidence file's date (`SbtEvidence::record_dates`, plus `generated`
  and `newest_source`). Only records newer than the generated files and
  every build source count, so a project or cross build not re-resolved
  since the wiring no longer withdraws a valid pin.
- Never withdraw a pin that those records verify (`verify` over the
  current view), and never run the bypass check when the lockfile basis
  stands.
- A `resolvers` reassignment or `sbt.override.build.repos` alone is no
  bypass: the generated override still forces the Socket-only version
  and the load-time verifier checks its bytes. Only a
  `dependencyOverrides` reassignment (the override replaced) or current
  records do.
- Current records are judged by the hosted rewriter's own
  `gate::check_existing` (base version shadowed, or other bytes from
  outside the pin repository), with a hash predicate where an
  unreadable file proves nothing.
- Build sources are scanned once and the reassignment reason is shared
  with `evidence_blocker`.
- A file under a generated name without socket-patch's header is a root
  definition sbt loads: the evidence walk lists it as a build source and
  `scan_build_sources` reads it (`owned_file::is_foreign`).

Vendored liveness:
- sbt: a user's own `socket-patch.sbt` is `sbt_pin_conflict`, not
  absent.
- scala-cli: liveness also applies the gate's post-vendoring
  `resolved_elsewhere` check over non-stale Bloop evidence
  (`coursier_gate::vendored_bypass`). The directive and elsewhere
  checks return their detail, and only the gate wraps them in a
  refusal. The tree check reuses `coursier_tree::{tree_dir, committed}`
  and a shared `patch_rows` index parse.
- `entry_wired` takes the reference path (`attest: false`), since it
  maps every `Err` to live anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The resolver and forced-version decisions rely on the generated file's
`onLoad` installer: its `update` verifier fails a build that does not
resolve the pinned bytes, and its resolver order serves the pin
repository first. A build source that reassigns `onLoad` (`:=`, `~=`,
`--=`, in any scope) without keeping the previous value drops both.

- `scan_build_sources` reports such edits as
  `BuildFindings::onload_assignment`. A `:=` whose right-hand side reads
  `onLoad`'s own `.value` (the generated idiom), or a `~=` that composes
  (`andThen` / `compose`), keeps the installer and is not reported. The
  finding round-trips through the resolution document.
- VEX hosted sbt treats it like a `dependencyOverrides` reassignment
  (`pin_dropped`). It is an evidence blocker and, unless current
  evidence verifies the pin, it withdraws the installed-copy basis
  (`SbtPinBypassed`).
- Vendored sbt liveness: `sbt_onload_reassigned`, over every build
  source the evidence walk reads.
- Planners agree: the hosted rewriter refuses new pins and leaves
  existing ones unconfirmed (`redirect_sbt_onload_assignment`). The
  vendored planner refuses a new pin (`vendor_sbt_onload_assignment`).

Co-Authored-By: Claude Opus 5.5 <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 68dec85. Configure here.

@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review November 6, 2026 12:11
@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Nov 6, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review at 68dec85.

  • CI: 30/32 non-skipped checks green. The only failures are the main-wide e2e … pdm:: --ignored leg (module 'hishel' has no attribute 'SyncSqliteStorage', also red on main; Fix pdm e2e red on every PR: pin hishel<2 #2028 fixes it) and the ci-ok aggregate that follows from it.
  • Bugbot: reviewed 68dec85; no unresolved findings.
  • Promoted from draft by the burn-down agent this run. No Slack announcement was sent this run (the Slack send action wasn't available), so there's no slack-announced marker yet.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief (for the human approver)

What it does

  • Hosted sbt VEX: a pin that evidence does not verify is now marked vex_sbt_pin_bypassed instead of attested, when there is proof the build bypasses it. The proof is a static dependencyOverrides reassignment, an onLoad that drops the installer, or a current record showing the base version.
  • Vendored liveness: vendored sbt and scala-cli liveness get attest-only checks for conflicting or foreign hosted files, onLoad drops, uncommitted tree or index entries, and shadowing repositories.
  • onLoad refusals: the new onLoad finding also refuses new pins in the hosted rewriter and the vendor planner.

Risk: medium-low. Almost every path makes vex attest less, and every bypass branch falls back to main's behaviour. The wider exposure is the onLoad heuristic: it is a regex, and it now also refuses new pins, not just VEX.

Look here

Verified

  • I read the full diff of the core files.
  • Read-only callers (revert, prune, --check) use attest: false, so they are unaffected.
  • An unreadable or evicted file counts as no proof. The WireFindings serde round-trip includes the new field.
  • The unit and e2e tests cover every branch, and their asserts fail with the guard removed.
  • CRLF is harmless. CHANGELOG.md is untouched.
  • CI: 30/32 non-skipped checks green. The only failures are the main-wide pdm/hishel leg and the ci-ok aggregate; Fix pdm e2e red on every PR: pin hishel<2 #2028 fixes both.
  • Bugbot: clean on this head.

Changes I made: none.

Open questions (non-blocking)

  • Partial re-resolve: bypass checks "current records verify" before dropped, and "current" can cover a subset of projects. Add dependencyOverrides := Seq() to a/build.sbt and run only sbt b/update, and the pin still attests. Main attests here too, so it is not a regression, but the description's "withdrawn unless current records verify" doesn't cover it. Should the current view have to cover the project that holds the finding?
  • onLoad false positives: ThisBuild / onLoad :=, project-scoped onLoad :=, and a ~= that keeps prev without andThen/compose are flagged. Each now refuses new pins. That is safe, but the user sees the change.
  • Perf: attestable_checked runs a full sbt_evidence::scan (and scala-cli discover) per vendored entry, which is O(entries × tree) on the vex path.

Auto-merge is not armed yet, because CI is red on the main-wide pdm failure. Once #2028 lands and this goes green, the next final-reviewer run arms it, and approving then sends it straight to the merge queue.


Generated by Claude Code

Second review round on the sbt installed-copy fallback.

- `bypass` returned "no bypass" as soon as the current records verified
  the pin, before looking at a proven build edit (`dependencyOverrides`
  or `onLoad` reassignment). With only some projects re-resolved since
  the edit, the edit may apply to a project whose record is stale, so
  the installed copy was attested while that project builds unpatched.
  Verified evidence now overrules an edit only when every record is
  current and every declared project has one (`BuildState::whole`).
- The `onLoad` scan's keep-check was loose: `:=` reading `.value` and
  dropping it, or `~= (_ => identity andThen f)`, counted as keeping the
  installer. `:=` now needs the `.value` read and a composition; `~=`
  must compose its own argument. sbt 0.13's `<<=` is recognized for
  `onLoad`, `dependencyOverrides` and the resolver settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants