Skip to content

Support hosted and vendored PDM patches - #243

Merged
Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
codex/pdm-patch-compatibility
Sep 18, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
codex/pdm-patch-compatibility

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

LLM Description written by Codex:gpt-6-astra

Hosted PDM scans did not produce a confirmed redirect, while vendored scans rejected historical lock formats and CRLF files. Both modes now share a validated source-and-integrity rewriter with legacy metadata.files support, base/extras entries, byte-preserving edits, idempotency and exact rollback. Conflicting sources, ambiguous package entries, malformed strategies and unsupported schemas are refused before files change. Failed PDM rewrites cannot be reported as successful through a generic manifest rewrite.

The native compatibility matrix covers every stable PDM major family (0.x, 1.x, 2.x), with thirteen releases spanning the lock-format transitions. PDM 0.12.3 supports both modes with pdm sync; the CLI warns about its upstream freshness bug, which can cause ordinary install to regenerate some locks. PDM 1.15.5 and 2.0–2.7 lose URL/path candidate identity in native dependency lookup, so their schemas refuse safely without changing inputs. PDM 2.8.2 through 2.29.2 pass both patch modes. Unknown future formats also refuse.

Validation:

  • Core library: 3,512 tests passed; CLI library: 546 tests passed; all 30 PDM-focused tests passed. Workspace clippy with all features and warnings denied passed.
  • Real Windows/Linux native matrix: 584 cases across 26 jobs, built from this PR's 75476589b04f1d66d0d92a747ccbb7dc79673067 commit.
  • Checks actual installed patch file hashes, repeated scans, lock and manifest preservation, rollback, corrupt-hash rejection, direct/transitive dependencies, extras, optional/development groups, included/excluded Python and platform markers, CRLF, static URLs and space/Unicode paths.
  • Unsupported releases have direct native source-identity probes and unchanged-input assertions; their original registry locks still install.
  • Fifteen real native lock snapshots and hardened pure/filesystem tests are checked in. Full installer evidence and reproduction live in depscan fixture PR #26378, stacked on core logic PR #26371. The shared annotation integration suite passes all 15,253 assertions.

Note

Medium Risk
Changes how Python lockfiles are rewritten and how hosted scans attest PyPI patches when pdm.lock is present; incorrect confirmation or lock edits could affect supply-chain enforcement, though the design is fail-closed with extensive tests.

Overview
Adds PDM (pdm.lock) to hosted registry redirects and aligns vendored PDM wiring with the same lock rewriter.

A shared pdm_lock module validates supported lock versions/strategies, rewrites wheel url/path and file hashes (including legacy [metadata.files] and extras variants), preserves line endings, and produces fragment edits for replay. Hosted scan collects pdm.lock and only confirms PyPI patches when confirmed_pdm_uuids says the PDM rewrite fully landed; refused PDM uuids are excluded like pnpm/cargo.

The redirect pipeline runs PDM first and drops overrides that PDM refused before other ecosystems run. Vendor pypi_pdm drops bespoke text splicing in favor of rewrite_pdm_lock / pdm_lock_edits, broadens supported lock formats (with fail-closed refusal for untested versions), treats legacy [tool.pdm.*-dependencies] as direct deps, and handles CRLF/non-canonical spacing via span-based edits. Replay groups redirect_pdm_lock_package with other PyPI edits. Byte-exact pdm-native lock fixtures and .gitattributes -text rules back the matrix.

Reviewed by Cursor Bugbot for commit 7547658. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Support hosted PDM patches and older locks Support hosted and vendored PDM patches Sep 17, 2026
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review September 17, 2026 20:45

@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 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Leftover PDM lock blocks live PyPI redirects
    • PDM rewriter now only runs when pdm.lock is the active lock file (uv.lock not present), preventing stale pdm.lock from blocking uv.lock/requirements.txt patches.

Create PR

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 7547658. Configure here.

Comment thread crates/socket-patch-core/src/patch/redirect/mod.rs Outdated
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Review + hardening: rebased onto main, 14 correctness fixes, full real-PDM matrix

I ran an adversarial multi-agent review of this PR and a full real-PDM compatibility
matrix, then hardened the code path. The work is on a churn-free rebase of this PR
onto current main (which now carries the merged Poetry #241).

Rebase + cleanup

  • Rebased onto main. Poetry Support hosted and legacy Poetry patches #241 merged after this PR forked; the branch needed
    it anyway. The vendored PDM backend already shipped on main, so this PR's real new
    surface is hosted PDM + a shared pdm.lock rewriter the vendored backend is
    refactored onto.
  • Dropped ~100 files of unrelated cargo fmt churn that commit 607e20f folded in
    without mentioning it (its actual 13-line fix was buried in +1834/-886 of reflow).
    The rebased diff is PDM-only.

Defects found by testing + fixed (verified against real PDM 0.12–2.29)

  1. Hosted lock-only checkouts were never redirected — the lock inventory didn't read
    pdm.lock, so a fresh clone / CI scan --mode hosted discovered nothing. Fixed
    (discovery-only, mirrors poetry).
  2. Hosted rollback broke after an external pdm lock — PDM un-patches and reflows
    CRLF→LF on relock, leaving the ledger's recorded fragment stale (a matrix FAIL on
    2.27/2.29). Fixed via the shared rebase machinery plus a PDM-specific original
    adoption; Poetry's relock (which keeps the patch) is unchanged.
  3. Coexistence gatingpdm.lock now drives (and can veto siblings) only when it's
    the actual PyPI install driver (!uv.lock && !poetry.lock), so a leftover pdm.lock
    neither blocks nor is falsely attested.

Correctness fixes from the review (adversarially verified)

  • Strategy validation: refuse a lock whose strategy lacks inherit_metadata
    (--strategy no_inherit_metadata) — PDM 2.12–2.23 re-resolves it and can't match the
    rewritten source (CandidateNotFound); read the 4.3 boolean strategy spelling.
  • Veto breadth: a pdm.lock refusal withholds the sibling rewriters only for a
    package the lock contains but refuses, never one it simply lacks (a requirements.txt
    redirect that used to land was being dropped).
  • Stale confirmation: an inactive pdm.lock no longer feeds the substring
    confirmation probe (an untouched uv/poetry project could report a bogus redirected: 1).
  • Warm-install advisory: redirect_pdm_stale_install_risk for PDM < 2.11 locks, which
    don't reinstall over an already-installed same-version package.
  • Multi-version marker forks refused with an accurate message (hosted + vendored).
  • Vendored integrity: refuse a symlinked pdm.lock before wire/revert; require a real
    sha256 in the pre-flight guard; refuse a pdm add/partial-relock unit that kept the
    patched hash but lost the vendored path; supersede a prior Socket URL in place.
  • Fragments/keys: trailing fragment boundary (replay convergence); legacy
    [metadata.files] keyed by PDM's safe_name().lower() (dotted names).
  • Boundary correction: PDM 2.8.0 writes an indistinguishable 4.3 lock but shares the
    identity-loss bug, so the supported floor is 2.8.1, not 2.8.0 (docs).

Evidence

  • Compatibility matrix (scripts/backtest-pdm.py, new): real PDM 0.12–2.29 × 17
    shapes × hosted/vendored/agent, installed-byte verification, tamper rejection, relock
    convergence, byte-exact rollback. Final run: 523 pass, 268 refused-as-expected, 46
    unsupported (__pypackages__ agent/vendored, documented), 0 failures.
  • Core lib 3530 tests green, clippy --all-features -D warnings clean, ~9 new PDM
    unit tests plus 3 CLI-level hosted regression tests.
  • New CI: .github/workflows/pdm-compatibility.yml (Linux/Windows/macOS), docs at
    docs/testing/pdm-compatibility.md, and the ecosystem/hosted-e2e/CLI_CONTRACT/CHANGELOG
    docs corrected (they still denied hosted PDM).

Bugbot's HIGH ("leftover pdm.lock blocks live redirects") is resolved: the !uv.lock gate
now also excludes poetry.lock, and the leftover/inactive lock is excluded from
confirmation.

Adds hosted PDM redirects and refactors the vendored PDM backend onto one
validated `pdm.lock` rewriter (`utils/pdm_lock.rs`). Hosted `scan --mode hosted`
rewrites the target `[[package]]` to the hosted wheel URL + patched SHA-256
(`redirect_pdm_lock_package`); vendored wires the same unit to a committed
wheel. Both support the legacy `[metadata.files]` table and `extras` entries,
preserve line endings and non-canonical spacing, are idempotent, and leave
`pyproject.toml` / `content_hash` untouched. Supported lock formats are `2`
(PDM 0.12-1.4) and `4.3`-`4.5.1` (PDM 2.8+); `3.1` / `4.0`-`4.2` (PDM 1.8-2.7)
and unknown formats are refused before any write.

Hardening beyond the original change:
- Hosted lock-only checkouts are discovered and redirected: the lock inventory
  now reads `pdm.lock` (discovery-only, mirroring poetry). Vendored keeps
  refusing a lock-only checkout since PDM installs into `__pypackages__`, which
  the crawler does not probe.
- A re-scan after an external `pdm lock` rebases the ledger's edits onto the
  relocked text. PDM un-patches AND may reflow line endings (CRLF -> LF) on
  relock, so for the pdm edit kind the rebase adopts the fresh `original` (the
  relocked registry fragment); poetry's relock keeps the patch and keeps its
  older original, so its path is unchanged.
- When several Python lockfiles coexist, `uv.lock` and `poetry.lock` drive
  hosted PyPI redirects ahead of `pdm.lock`; a leftover `pdm.lock` neither
  blocks a live redirect nor is falsely attested. The same predicate gates the
  redirect dispatch and the hosted confirmation filter.

Adds byte-exact `pdm-native` fixtures and CLI-level hosted regression tests
(lock-only redirect + attest + rollback, legacy `[metadata.files]`, and
relock-reflow convergence).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents hosted PDM support (ecosystems.md, hosted-production-e2e.md,
CLI_CONTRACT.md new codes, CHANGELOG) and adds the per-release matrix:
docs/testing/pdm-compatibility.md with a generated table, scripts/backtest-pdm.py
(real PDM releases via uv, hosted/vendored/agent, installed-byte verification,
tamper rejection, relock convergence, rollback), and
.github/workflows/pdm-compatibility.yml (Linux/Windows/macOS).

Measured 0.12-2.29 across 17 shapes x 3 modes: 523 pass, 268 refused as
expected (identity-losing 3.1/4.0-4.2, forked two-version locks, non-default
lock filenames), 46 unsupported (`__pypackages__` agent/vendored, a documented
layout limitation), 0 failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 12-finder review (42 confirmed findings) plus a real-PDM verification pass
surfaced correctness gaps beyond the initial three. Fixes, each verified
against real PDM binaries:

Confirmation / attestation (false-success class):
- Refuse a lock whose `strategy` lacks `inherit_metadata` (lock_version
  4.4.1/4.5.x) — PDM 2.12-2.23 re-resolves such a lock and cannot match a
  rewritten `url`/`path` source (CandidateNotFound); read the 4.3 boolean
  strategy spelling (`cross_platform`/`static_urls`) that PDM 2.8-2.9 write.
- A `pdm.lock` refusal now vetoes the sibling pypi rewriters only for a package
  the lock CONTAINS but refuses, never one it simply lacks (which a
  `requirements.txt`/pylock legitimately installs) — the old veto silently
  dropped those redirects.
- An inactive `pdm.lock` (a `uv.lock`/`poetry.lock` drives) no longer feeds the
  substring confirmation probe, so an untouched uv/poetry project cannot report
  a bogus `redirected: 1`.
- Emit `redirect_pdm_stale_install_risk` for PDM < 2.11 locks (2/4.3/4.4), which
  do not reinstall over an already-installed same-version package.
- A marker/multi-target lock holding one package at several versions is refused
  with an accurate, ordering-independent message (hosted and vendored).

Vendored integrity:
- Refuse a symlinked `pdm.lock` before wire/revert (the rename-over would
  replace the link and strand its target) — mirrors the uv/hosted guards.
- Require a real sha256 in the pre-flight hash guard so a non-sha256 lock is
  refused before the wheel is built.
- Refuse a `pdm add`/partial-relock unit that kept the patched hash but lost the
  vendored `path`, so a stale patched hash is never recorded as the registry
  original.
- Supersede a prior Socket-hosted `url` (rotated token / new uuid, same origin
  and wheel leaf) in place instead of refusing it as foreign.
- Fix the version-mismatch refusal to print the plain version, not `Some("…")`.

Fragments / keys:
- Give the recorded package fragment a trailing boundary so a lock_version-2
  pristine fragment is not a prefix of the rewritten one (replay convergence).
- Key legacy `[metadata.files]` by PDM's `safe_name().lower()` (dots preserved),
  so dotted-name packages in format-2 locks are found.

Docs: PDM 2.8.0 writes an indistinguishable `4.3` lock but shares the
identity-loss bug, so the supported floor is 2.8.1; the docker vendored e2e
awk counter now matches the inline `files` shape.

Adds unit tests for the strategy gate, multi-version fork, URL supersede,
dotted-name key, absent-package veto skip, stale advisory, symlink guard and
the non-sha256 pre-flight refusal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit e146e03 into main Sep 18, 2026
54 of 55 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the codex/pdm-patch-compatibility branch September 18, 2026 15:54
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (07a6b88) — conflicts with Hatch #244 and Pipenv #242 resolved

The branch is again the same three PDM commits, now on top of main. Nine files overlapped with the two PRs that landed after this one forked; how each was merged:

Hosted redirect dispatch (core/src/patch/redirect/mod.rs) — order is now pdm (only when pdm.lock drives: no uv.lock / poetry.lock) → pipenv → the rest. This follows the vendored flavor precedence on main (uv > poetry > pdm > pipenv): a pdm refusal for a package the lock contains is withheld from the Pipenv rewriter and every later pypi rewriter; a live-Pipfile conflict still vetoes the siblings as #242 specified. Hatch already yields to any lock, pdm.lock included.

Hosted confirmation filter (cli/src/commands/scan/hosted.rs) — refused-pipenv and refused-pdm both short-circuit to unconfirmed; when pdm.lock drives, a pypi dep is confirmed only by confirmed_pdm_uuids. That pdm check sits ahead of the hatch gate from #244: is_hatch is true for any pyproject.toml with build-backend = "hatchling.build", and in that case the hatch rewriter registers every pypi uuid as hatch-owned and then yields to the lock without confirming any — so with the gate first, a PDM project using hatchling as its build backend would rewrite pdm.lock and attest nothing. Regression test added (hatchling_build_backend_does_not_veto_the_pdm_lock_redirect); verified it fails with the gate first and passes with this ordering. The third commit's pdm_inactive exclusion from the substring probe carried over unchanged.

Lock inventory (core/src/vendor/lock_inventory.rs) — pdm.lock slots between poetry.lock and the Pipfile.lock + requirements.txt pair (which #242 reads together), same precedence as above. Both new inventory readers kept.

replay.rs / utils/mod.rs — additive (pdm kind joins the pypi ReplaceFragment group beside pipenv's PipenvEntry and hatch's HatchDocument).

DocsCLI_CONTRACT.md rewriter paragraph and the ecosystems.md PyPI row now describe uv / poetry / pdm / pipenv all as hosted; hosted-production-e2e.md no longer claims any PyPI lock flavor is vendored-only; both CHANGELOG entries kept.

One test adjusted from #244: hatch_confirmation_uses_successful_lock_writers used a package-less pdm.lock stub; the pdm rewriter (deliberately, per the hardening commit) refuses a lock with no [[package]] array, which withheld the uuid from hatch and broke the stub's assertion. It now uses the real pdm-native/2.29.2.lock fixture, mirroring the test's poetry fixtures, and additionally asserts pdm confirmed through its own set.

Verification on the rebased branch: core lib 3,585 tests green; CLI suite 2,961 tests across 204 binaries green; cargo clippy --workspace --all-features -- -D warnings clean. No non-PDM file changes beyond the merge sites (35 files vs main, same set as before).

Main-side finding, not fixed here (out of scope): the same hatch-gate ordering affects Pipenv on current main — a Pipfile.lock project whose pyproject.toml names hatchling.build fails scan --mode hosted with exit 1 (nothing confirmed). Reproduced by adding such a pyproject.toml to lock_only_pipenv_project_redirects_attests_rescans_and_rolls_back. The fix is the same one-line ordering change for confirmed_pipenv_uuids; happy to open a follow-up.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants