Skip to content

Fix Hatch takeover leaving direct-ref permission (#674) - #680

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-hatch-takeover-permission-owner
Oct 5, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-hatch-takeover-permission-owner

Conversation

@mikolalysenko

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

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #674

Summary

After a hosted→vendored takeover of two or more Hatch packages, rollback, remove and vendor --revert used to leave allow-direct-references = true (plus empty [tool] / [tool.hatch] tables) behind. That silently turned off Hatchling's direct-reference guard and kept the pyproject dirty. With this change the pyproject comes back byte-exact.

Root cause

The takeover in commands/vendor.rs calls restore_upstream with one hosted pin per candidate inside the vendoring loop. When the first Hatch package is vendored, the other packages' hosted direct references are still live, and so is the permission hosted mode added for them. The vendored hatch_permission record snapshotted that state as its original. Later packages clone the same record. So once the last reference was unwired, the revert "restored" a permission the user never had.

Fix

  • vendor/pypi_hatch.rs::wire: a new permission record now checks the project for a direct reference the vendored ledger doesn't own (anything that isn't a {root:uri}/.socket/vendor/… reference). If there is one, the permission it finds is held for those references, so the record's original is the document with that permission removed, along with any tables that leaves empty. The hosted unwind already applies this rule: it drops the permission once no project direct reference is left. Because the decision is made once when the record is created, every package that clones the record inherits it, whatever the revert order.
  • utils/hatch.rs: drop_direct_reference_permission and permission_keys now live here. The hosted unwind (redirect/upstream/pypi.rs) and the vendored ledger share them, so both lanes produce the same bytes.
  • revert is unchanged. A permission the user set themselves (with no non-vendored direct reference live at wiring time) is still recorded and restored verbatim.

I didn't restructure the generic per-pin takeover loop. It carries many per-candidate refusal gates, and the pnpm (#636) and uv (#670) leftovers have different mechanisms; #672 covers those.

Tests (red → green)

Test Covers Without fix With fix
vendor::pypi_hatch::tests::takeover_through_hosted_unwind_reverts_byte_exact #674 through the real hosted rewrite, discover_patched_refs, per-pin restore_upstream and vendored wire, reverted in both orders FAILED (leftover [tool]…allow-direct-references = true) ok
vendor::pypi_hatch::tests::takeover_snapshot_of_hosted_permission_is_not_restored #674 in both revert orders, with the permission in pyproject and in hatch.toml [metadata] (sibling key kept) FAILED (same leftover) ok
vendor::pypi_hatch::tests::user_permission_survives_vendored_revert Guard: a user-set permission (inline and in hatch.toml) survives a two-package vendored revert ok ok

Per-issue checklist:

Local validation

  • cargo clippy --workspace --all-features -- -D warnings: clean.
  • cargo test -p socket-patch-core --lib -- pypi_hatch hatch upstream: 87 passed.
  • cargo test --workspace --all-features --no-fail-fast: everything passes except 12 tests that simulate write or removal failures with chmod / read-only dirs. The sandbox runs as root, which bypasses those permissions. None of them touch Hatch code. CI runs unprivileged.
  • SOCKET_PATCH_HATCH_E2E_REQUIRED=1 SOCKET_PATCH_HATCH_E2E_VERSION=1.18.1 cargo test -p socket-patch-cli --all-features --test e2e_vex_build -- hatch:: --ignored: 4 passed.
  • rustfmt --check is clean on the touched files. Note that cargo fmt --all -- --check fails on main itself (about 130 files, from 045d7ec), and CI doesn't run rustfmt. I kept those unrelated reformats out of this PR; only one existing line in utils/hatch.rs was reflowed.

Behaviour note

If a user had both their own direct reference and allow-direct-references = true before vendoring, the permission is kept as long as any project direct reference remains. It is dropped only once none is left, which is what hosted mode already does.

🤖 Generated with Claude Code


Note

Medium Risk
Changes vendored Hatch permission ledger semantics and shared TOML cleanup used on revert; well-covered by new tests but affects dependency wiring rollback paths.

Overview
Fixes #674: after a hosted→vendored Hatch takeover with multiple packages, revert/rollback no longer leaves allow-direct-references = true (and empty [tool] / [tool.hatch] tables) when the project should return to its pre-hosted bytes.

Vendored wiring now treats hatch_permission “original” as the file without the direct-reference permission when non-vendored project direct refs are still live at wire time (e.g. another package’s hosted pin during per-pin takeover). User-set permissions are still snapshotted verbatim.

Shared Hatch helpers — permission_keys and drop_direct_reference_permission — moved into utils/hatch.rs so hosted unwind (restore_hatch) and the vendored ledger apply the same cleanup rule.

Adds regression tests for takeover/revert order, full hosted→vendor lane, and user-owned permissions in pyproject.toml / hatch.toml.

Reviewed by Cursor Bugbot for commit d3076b5. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
After a hosted -> vendored takeover of two or more Hatch packages,
rollback, remove and vendor --revert left allow-direct-references =
true (plus empty [tool] / [tool.hatch] tables) in pyproject.toml,
silently disabling Hatchling's direct-reference guard.

The takeover unwinds hosted pins one at a time, so when the first
package is vendored the other packages' hosted references still hold
the permission hosted mode added, and the vendored ledger recorded
that as the permission's original value. The ledger now records the
value the permission has once those live non-vendored references are
gone, which is the rule the hosted unwind already applies. Both lanes
share one helper for dropping the permission and its emptied tables.

Fixes #674

Assisted-by: Claude Code:claude-opus-5-5
Covers #674 through the hosted rewrite, discovery and the takeover's
per-pin restore_upstream, rather than a simulated unwind, and checks
both revert orders come back to the pre-hosted pyproject bytes.

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 3, 2026 11:11
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


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 d3076b5. 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 — head d3076b5d00100d9e268df66c4e6d63f39ee18ef6.

  • CI: 97/97 checks green on the head (3 skipped by path filters). Branch is up to date with main (0 behind).
  • Bugbot: reviewed d3076b5 and found no issues. No review threads are unresolved.
  • Reviewer focus: vendor/pypi_hatch.rs::wire now decides when the permission record is created whether a live non-vendored direct reference owns allow-direct-references. The permission helpers moved into utils/hatch.rs, so the hosted unwind and the vendored revert produce the same bytes. A permission the user set themselves is still restored verbatim (see user_permission_survives_vendored_revert).

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Reviewed d3076b5d00100d9e268df66c4e6d63f39ee18ef6 against main 045d7ec783d788bf3c5a1310724b51e09fb6505d: ready to merge as-is from this review. No actionable issue introduced by this diff.

The first vendored permission record now excludes permission held by remaining hosted references. Shared ownership survives selective removal, and final cleanup restores Hatchling’s direct-reference guard. The extracted hosted cleanup helper preserves its existing behavior.

Validation:

  • 163 repository tests passed: Hatch/upstream/TOML restoration, PyPI mode migration, and Hatch VEX coverage.
  • One additional probe passed 8 scenarios across both removal orders, inline/external metadata, and LF/CRLF. It verifies dry-run preservation and transactional refusal when the permission changes.
  • 8 selected native lifecycle cases passed with Hatch 1.18.1 / Hatchling 1.32.4, including 3 fresh installs verifying patched modules, byte-exact restoration, retained user references/settings, and the expected post-restore guard decisions.
  • 482 successful checks, 7 skipped; all 13 workflows terminal good. Exact-head Bugbot is clean, with no unresolved threads. Touched-file formatting and core production Clippy passed with the existing macOS unused_variables warning allowance.

A separate exploratory case found existing hosted rewriting removes a comment attached to the permission value. This reproduced before any vendoring with both this binary and a control whose Hatch code matches main; it is preserved separately from the eight passing cases and is not introduced here.

@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit cd75b38 into main Oct 5, 2026
489 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-hatch-takeover-permission-owner branch October 5, 2026 11:24
Mikola Lysenko (mikolalysenko) pushed a commit that referenced this pull request Oct 7, 2026
Hatch hosted mode (#680, #743) rewrites pyproject.toml and hatch.toml
in place, with no lockfile, through utils::hatch::plan. No scenario
exercised that rewriter: hatch.toml is a HOSTED pypi input, and a
hatch project with no lock fell through every existing pypi fixture.

The fixture is a lockless hatchling app. Direct deps go in [project],
and a hatch.toml default env (in-project .venv) pins every patched
transitive, since hosted Hatch only redirects deps a Hatch table
declares. A scan rewrites both files and adds
[tool.hatch.metadata] allow-direct-references. It is sized at 1000
packages / 25 patched so a scan takes about 75-85 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 7, 2026
* Bench: add gradle hosted and rescan scenarios

#646 gave Gradle builds a hosted mode: scan crawls Gradle's
modules-2/files-2.1 cache, pins suffixed versions in gradle.lockfile
and wires the build through an owned settings script and index under
.socket/gradle/. None of that was benchmarked; the maven scenarios
only reach the pom.xml + ~/.m2 path.

The gradle fixture is a single-project Groovy build with dependency
locking (1000 locked artifacts, 25 patched direct deps), its cache
under the fixture's GRADLE_USER_HOME with jar and pom in separate
sha1 dirs. The Maven-coordinate generator, pom writer and maven2
grant builder are shared with the maven fixture, whose bytes are
unchanged. The grant's indexUrl is https because the Gradle planner
refuses anything else; scan never fetches it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Route Gradle digests through utils::digest

main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)

* Bench: add hatch hosted and rescan scenarios

Hatch hosted mode (#680, #743) rewrites pyproject.toml and hatch.toml
in place, with no lockfile, through utils::hatch::plan. No scenario
exercised that rewriter: hatch.toml is a HOSTED pypi input, and a
hatch project with no lock fell through every existing pypi fixture.

The fixture is a lockless hatchling app. Direct deps go in [project],
and a hatch.toml default env (in-project .venv) pins every patched
transitive, since hosted Hatch only redirects deps a Hatch table
declares. A scan rewrites both files and adds
[tool.hatch.metadata] allow-direct-references. It is sized at 1000
packages / 25 patched so a scan takes about 75-85 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <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

3 participants