Skip to content

Hatch hosted→vendored takeover with two or more patches leaves allow-direct-references = true (plus empty [tool]/[tool.hatch] tables) behind after rollback or remove, disabling Hatchling's direct-reference guard #674

Description

[agent] Found by the scheduled Hatch bug-hunt routine (ledger #314).

Summary

In a Hatch project with two hosted patches (six==1.16.0 and toml==0.10.2 in [project].dependencies), scan --mode vendored takes over correctly and fresh envs are patched. But a later rollback (or remove of both purls) doesn't restore pyproject.toml. It leaves this behind:

[tool]

[tool.hatch]

[tool.hatch.metadata]
allow-direct-references = true

The takeover reverts the hosted wiring one package at a time (restore_upstream(.., std::slice::from_ref(pin), ..)). So when six is vendored, toml's hosted URL is still in pyproject, and with it the hosted-owned allow-direct-references = true. Six's vendored ledger records that state as its original, both for the hatch_document wiring and for the hatch_permission wiring. Rollback faithfully restores that "original", so the permission that hosted mode added is never removed.

Impact

After a full rollback, the project silently keeps allow-direct-references = true, which socket-patch added and the user never had. That turns off Hatchling's guard against direct references in project metadata. Without it, a later direct URL dependency builds a wheel that can't be uploaded to PyPI. Verified: with the leftover setting, adding foo @ https://example.com/foo-1.0-py3-none-any.whl builds fine. With the original pyproject, hatch build fails: Dependency #3 of field project.dependencies cannot be a direct reference unless field tool.hatch.metadata.allow-direct-references is set to true. The rollback also leaves the file non-byte-exact, so git status stays dirty.

Repro (Hatch 1.18.1; local mock patch API serving patched six 1.16.0 and toml 0.10.2 wheels)

cat > pyproject.toml <<'EOF'
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "app"
version = "0.1.0"
dependencies = ["six==1.16.0", "toml==0.10.2"]
EOF
cp pyproject.toml orig.toml
socket-patch scan --mode hosted   --yes --json --ecosystems pypi …   # exit 0, redirected 2
socket-patch scan --mode vendored --yes --json --ecosystems pypi …   # exit 0, vendor_takeover_reverted_redirect for both; fresh env patched
socket-patch rollback --json …                                       # exit 0
diff orig.toml pyproject.toml   # + [tool] / [tool.hatch] / [tool.hatch.metadata] allow-direct-references = true

.socket/vendor/state.json after the takeover shows six's hatch_permission wiring with original already containing toml's hosted URL and allow-direct-references = true.

Expected vs actual

  • Expected: docs/testing/hatch.md says "Each project patch records shared ownership of the direct-reference permission. Selective and preserved rollback retain the setting while any project direct reference remains, and restore its original value after the last reference is unwired." Here the original value was "absent", so after the last reference is unwired, pyproject should match the pre-hosted bytes.
  • Actual: allow-direct-references = true and the empty table headers stay.

Matrix (Linux, each run twice)

Sequence Hatch 1.16.5 Hatch 1.18.1
hosted (2 patches) → rollback — pass (byte-exact)
vendored (2 patches) → rollback — pass (byte-exact)
hosted → vendored, 1 patch → rollback — pass (byte-exact)
hosted → vendored, 2 patches → rollback fail fail
hosted → vendored, 2 patches → remove both — fail (same leftover)
vendored → hosted, 1 or 2 patches → rollback — pass (byte-exact)

The edits don't depend on the Hatch version or the OS. macOS / Windows were not probed.

Suspect code

crates/socket-patch-cli/src/commands/vendor.rs:2458: the takeover calls restore_upstream with a single pin per candidate inside the vendoring loop. So the next candidate's hosted wiring (and the shared permission it owns) is still live when the current candidate's vendored ledger snapshots original. Reverting all hosted pins in the takeover set before vendoring any of them (or having the Hatch permission ledger ignore a permission owned by live hosted wiring) would avoid it. The pnpm (#636) and uv (#670) lanes have similar two-package leftovers, but the mechanism there is different.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions