Skip to content

Commit dc0dc75

Browse files
Fix hosted uv missing uv<0.5.6 advisories (#1569) (#1609)
* Start fix for #1569 Assisted-by: Claude Code:claude-opus-5-5 * Warn hosted uv users when old uv drops the patch A hosted uv scan wires a transitive patched package through a marked override-dependencies entry, and repoints [manifest] constraints in uv.lock. uv before 0.5.5 honours neither: a plain uv sync silently reinstalls the unpatched wheel, or uv sync --locked fails. Vendored mode already warns about both; hosted printed nothing and exited 0. Hosted scans now emit redirect_uv_override_requires_uv_0_5_6 and redirect_uv_constraints_require_uv_0_5_6 on the run that wires those shapes (project and script locks). They advise only: the redirect still counts and the exit code is unchanged. Fixes #1569 Assisted-by: Claude Code:claude-opus-5-5 * Test the hosted uv advisories end to end Re-bless the uv rewrite golden: the only change in its 1500 seeded cases is the new advisory (verified by filtering those two codes and matching the previous golden exactly), and require that the sweep reaches both codes. The real-uv hosted transitive and constraints lanes now assert the advisory, and the uv_hosted transitive case expects it. Refs #1569 Assisted-by: Claude Code:claude-opus-5-5 * Cover user-written uv overrides in the advisory Review fixes for the hosted uv advisories: - A user's own override-dependencies entry, which the hosted rewrite reuses for its source, now gets the advisory too: old uv ignores the source there just the same. - Read the [manifest] constraints from the lock session's document instead of parsing the lock again per package. - Hosted and vendored now share one advisory text, which names the real affected range (uv before 0.5.5) and the canonical name. - Vendored-to-hosted takeover no longer names a uv advisory as the reason another package stayed vendored. Refs #1569 Assisted-by: Claude Code:claude-opus-5-5 * Keep uv advisories out of takeover skip names The takeover's skip explanation matched a warning that names the package before it filtered out landed-pin advisories, so a package named like a word in the uv advisory (wheel, uv, sync) could inherit another pin's advisory as its reason for staying vendored. Filter the uv advisories out of both passes. Refs #1569 Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f361e8d commit dc0dc75

12 files changed

Lines changed: 655 additions & 165 deletions

File tree

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,7 @@ command's integration tests check its output.
15471547
| `redirect_poetry_stale_install_risk` | `warnings[]` (warning) | scan `--mode hosted` (poetry): same writer test as above — a warm virtualenv keeps the upstream package after the redirect on Poetry < 1.4 (1.4+ re-installs from the new source); fresh installs pick up the patched wheel. Emitted once per rewritten lock, only on the run that rewrites it. |
15481548
| `redirect_poetry_source_constraint` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the pyproject pins the redirected package to a named source (`source = "pypi"`) and the lock is in format 2.1+ and was not written by Poetry 1.3 – 2.2 (those writers, and any `lock-version` 1.0 / 1.1 lock, which every Poetry 1.3+ re-solves and refuses (#1503), are refused with `redirect_poetry_lock_unsupported`, also when the lock is already redirected and before a vendored → hosted takeover reverts anything). The rewrite stands, and an already-redirected lock is still confirmed; the detail says a Poetry 1.3 – 2.2 installer would refuse the lock and to drop the `source` key if the project is installed with one. Exit code and `status` unchanged. |
15491549
| `redirect_poetry_entry_not_found` / `redirect_poetry_missing_sha256` / `redirect_poetry_lock_unsupported` | `warnings[]` (warning) | scan `--mode hosted` (poetry): the lock has no `[[package]]` at the granted version (uv-parity twin of `redirect_uv_entry_not_found`); the grant carries no SHA-256 (gated once per dep, not per lock); the lock is refused — Poetry 0.12 layout (URL sources ignored), an unsupported `lock-version`, a forked package listed at several versions, a user-authored `[package.source]` on another origin (an earlier Socket URL for the same wheel is superseded in place), a package the pyproject pins to a named source (`source = "pypi"`) over a lock written by Poetry 1.3 – 2.2, whose installer refuses that constraint on a URL-sourced package, or over a `lock-version` 1.0 / 1.1 lock, which every Poetry 1.3+ re-solves and refuses the same way (#1503), a malformed `[metadata.files]`/`[metadata.hashes]`, or a wheel whose filename does not match the locked package. Exit code and `status` unchanged (hosted-refusal posture). A vendored → hosted takeover over a Poetry 0.x lock is retracted with this code (wet and `--dry-run`, see **Staged takeover**): the purl stays vendored and patched and is skipped with this code as `skipped` (hosted event). The detail names the remedy and its reach: run `socket-patch vendor --revert` (it reverts EVERY vendored package in the project, not just this one), upgrade to Poetry >= 1.0 and re-lock, then re-run `scan --mode hosted`. |
1550+
| `redirect_uv_override_requires_uv_0_5_6` / `redirect_uv_constraints_require_uv_0_5_6` | `warnings[]` (warning) | scan/get `--mode hosted` (uv project and script locks, #1569): the hosted twins of vendored's `pypi_uv_override_requires_uv_0_5_6` / `pypi_uv_constraints_require_uv_0_5_6`. The run wired a transitive package through a `[tool.uv] override-dependencies` entry, its own marked one or the user's (uv before 0.5.5 re-resolves it and a plain `uv sync` reinstalls the upstream wheel), or repointed a `[manifest]` `constraints` / `build-constraints` entry (uv before 0.5.5 rejects it under `uv sync --locked`). The CLI cannot tell those uv releases apart from the lock, so these advise and the redirect still counts. Emitted on a run that writes the package's wiring (a first pin or a re-pin to a superseding patch); a settled re-run is silent. Exit 0. |
15501551
| `redirect_pdm_refused` / `redirect_pdm_legacy_sync_required` | `warnings[]` (warning) | scan `--mode hosted` (pdm): the `pdm.lock` rewrite was refused — an unsupported `[metadata] lock_version` (the identity-losing `3.1` / `4.0`–`4.2` formats or an untested future format), an unsupported `strategy`, a package listed at several versions (fork) or absent, a user-authored `url`/`path`/VCS/`editable` source, hash-less or malformed `files`, or a wheel whose filename does not match the locked package (`redirect_pdm_refused`); or the lock was written in format `2` (PDM 0.12–1.4), whose upstream freshness bug lets `pdm install` regenerate the lock — use `pdm sync` (`redirect_pdm_legacy_sync_required`). A refused uuid is withheld from every other PyPI rewriter when `pdm.lock` is the install driver, and its patch is not confirmed. Exit code and `status` unchanged (hosted-refusal posture). |
15511552
| `redirect_bun_lock_unsupported` | `warnings[]` (warning) | scan/get `--mode hosted` (bun): the text lock's `lockfileVersion` is not 0, 1 or 2 (a newer version: update socket-patch, re-locking would reproduce it; no integer: re-lock with Bun ≥ 1.2 — the shared gate's text, identical to vendored's `vendor_lockfile_version_unsupported`), or its `packages` section is not bun's single-line grammar. Nothing rewritten; exit 0 (hosted-refusal posture). |
15521553
| `redirect_bun_workspace_unsupported` | `warnings[]` (warning) | scan/get `--mode hosted` (bun): a lockfileVersion-0 lock (Bun 1.1.39–1.1.45 `--save-text-lockfile`) holds `workspace:` packages; frozen installs of that grammar cannot keep the hosted tuple. Detail: "Bun version-0 workspace locks cannot preserve hosted tarballs on frozen installs; delete bun.lock and re-run `bun install` with Bun >= 1.2 (which writes lockfileVersion 1, accepted by hosted mode) — a plain in-place `bun install` bumps the version only when a workspace depends on another workspace (e.g. root -> member); otherwise it keeps version 0 or fails to resolve" (measured: Bun 1.2.0 keeps 0, 1.2.23–1.4.2 exit 1 "failed to resolve" on a root that does not depend on its members). Version-1/2 workspace locks are rewritten. Exit 0. |

‎crates/socket-patch-cli/src/commands/scan/hosted.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2338,13 +2338,16 @@ fn describe_skip_reason(reason: &str) -> String {
23382338
/// package has no lock entry: the count `format_unredirected` turns into its
23392339
/// "(see the warning below)" hint. `redirect_bun_default_trust_lost` rides in
23402340
/// the same vector but is about a pin that *was* written (Bun's default trust
2341-
/// lost on the hosted URL, #371), so it never explains a missing entry.
2341+
/// lost on the hosted URL, #371), so it never explains a missing entry. The
2342+
/// uv < 0.5.6 advisories (#1569) likewise describe a written pin.
23422343
fn lock_entry_warning_count(
23432344
warnings: &[socket_patch_core::patch::redirect::RewriteWarning],
23442345
) -> usize {
2346+
use socket_patch_core::patch::redirect::UV_OLD_RELEASE_ADVISORIES;
23452347
warnings
23462348
.iter()
23472349
.filter(|w| w.code != "redirect_bun_default_trust_lost")
2350+
.filter(|w| !UV_OLD_RELEASE_ADVISORIES.contains(&w.code.as_str()))
23482351
.count()
23492352
}
23502353

@@ -4398,6 +4401,14 @@ mod tests {
43984401
lock_entry_warning_count(&[w("redirect_bun_default_trust_lost")]),
43994402
0
44004403
);
4404+
// Nor the uv < 0.5.6 advisories, which follow a written pin (#1569).
4405+
assert_eq!(
4406+
lock_entry_warning_count(&[
4407+
w("redirect_uv_override_requires_uv_0_5_6"),
4408+
w("redirect_uv_constraints_require_uv_0_5_6"),
4409+
]),
4410+
0
4411+
);
44014412
assert_eq!(
44024413
lock_entry_warning_count(&[
44034414
w("redirect_bun_default_trust_lost"),

‎crates/socket-patch-cli/src/commands/scan/hosted/takeover.rs‎

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,8 @@ const LANDED_PIN_ADVISORIES: &[&str] = &[
437437
/// wired outside the root file #699, a uv entry pinned down from another
438438
/// locked version #723, a Poetry 0.x lock #945); a rewriter warning naming
439439
/// the package; the rewrite's first warning (a lock-level refusal names no
440-
/// package), skipping [`LANDED_PIN_ADVISORIES`]; else [`NOT_PINNED`].
440+
/// package), skipping [`LANDED_PIN_ADVISORIES`] and the uv < 0.5.6
441+
/// advisories (#1569); else [`NOT_PINNED`].
441442
async fn explain(
442443
root: &std::path::Path,
443444
entry: &VendorEntry,
@@ -447,19 +448,22 @@ async fn explain(
447448
if let Err(w) = socket_patch_core::patch::redirect::preflight_pypi_takeover(root, entry).await {
448449
return (w.code.clone(), vec![w]);
449450
}
450-
if let Some(w) = dep.and_then(|dep| {
451-
warnings
452-
.iter()
453-
.find(|w| names_package(&w.detail, &dep.name))
454-
}) {
451+
// The uv < 0.5.6 advisories ride on a pin that landed, and their
452+
// wording ("wheel", "uv", "sync") can read as another package's name,
453+
// so neither pass may pick one (#1569).
454+
let candidates = || {
455+
warnings.iter().filter(|w| {
456+
!socket_patch_core::patch::redirect::UV_OLD_RELEASE_ADVISORIES
457+
.contains(&w.code.as_str())
458+
})
459+
};
460+
if let Some(w) = dep.and_then(|dep| candidates().find(|w| names_package(&w.detail, &dep.name)))
461+
{
455462
return (w.code.clone(), vec![w.clone()]);
456463
}
457464
// A pin that did land can carry a success advisory; it never explains
458465
// a pin that did not.
459-
match warnings
460-
.iter()
461-
.find(|w| !LANDED_PIN_ADVISORIES.contains(&w.code.as_str()))
462-
{
466+
match candidates().find(|w| !LANDED_PIN_ADVISORIES.contains(&w.code.as_str())) {
463467
Some(w) => (w.code.clone(), vec![w.clone()]),
464468
None => (NOT_PINNED.to_string(), Vec::new()),
465469
}
@@ -674,4 +678,43 @@ mod tests {
674678
assert_eq!(code, "redirect_lock_refused");
675679
assert_eq!(explained, vec![warn("redirect_lock_refused")]);
676680
}
681+
682+
/// #1569: another pin's uv advisory says "wheel" and "uv sync", whole
683+
/// words that are also package names; it never explains a staged
684+
/// package that was not pinned, by name or as the lock-wide fallback.
685+
#[tokio::test]
686+
async fn uv_old_release_advisories_never_explain_a_skip() {
687+
let entry: VendorEntry = serde_json::from_value(serde_json::json!({
688+
"ecosystem": "npm",
689+
"basePurl": "pkg:npm/wheel@1.0.0",
690+
"uuid": "u1",
691+
"artifact": { "path": ".socket/vendor/npm/u1/wheel.tgz" },
692+
"wiring": [],
693+
}))
694+
.expect("minimal vendor entry");
695+
let dep = socket_patch_core::patch::redirect::DepOverride {
696+
ecosystem: "npm".into(),
697+
name: "wheel".into(),
698+
namespace: None,
699+
version: "1.0.0".into(),
700+
token: String::new(),
701+
patch_uuid: "u1".into(),
702+
artifact_url: String::new(),
703+
registry_override: None,
704+
integrity: Default::default(),
705+
};
706+
let warnings: Vec<RewriteWarning> =
707+
socket_patch_core::patch::redirect::UV_OLD_RELEASE_ADVISORIES
708+
.iter()
709+
.map(|code| RewriteWarning {
710+
code: code.to_string(),
711+
detail: "uv.lock: repointed at the hosted wheel; uv sync".into(),
712+
})
713+
.collect();
714+
let root = std::path::Path::new(".");
715+
assert_eq!(
716+
explain(root, &entry, Some(&dep), &warnings).await.0,
717+
NOT_PINNED
718+
);
719+
}
677720
}

‎crates/socket-patch-cli/tests/vex_e2e_common/uv.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,24 @@ pub fn run_lane(suite: &str, uv: &Uv, mode: Mode, lane: Lane) {
13451345
"{}: the stale pristine install is not reported: {env:#}",
13461346
report.what("scan --mode hosted")
13471347
);
1348+
// #1569: the wiring shapes uv before 0.5.5 does not honour carry
1349+
// the same advisory vendored gives, in its hosted spelling.
1350+
let advisory = match lane {
1351+
Lane::Transitive => Some("redirect_uv_override_requires_uv_0_5_6"),
1352+
Lane::Constraints => Some("redirect_uv_constraints_require_uv_0_5_6"),
1353+
_ => None,
1354+
};
1355+
for code in [
1356+
"redirect_uv_override_requires_uv_0_5_6",
1357+
"redirect_uv_constraints_require_uv_0_5_6",
1358+
] {
1359+
assert_eq!(
1360+
env.to_string().contains(code),
1361+
advisory == Some(code),
1362+
"{}: `{code}` advisory expected only on its own lane: {env:#}",
1363+
report.what("scan --mode hosted")
1364+
);
1365+
}
13481366
assert!(
13491367
!embedded_doc.exists(),
13501368
"a failed in-run --vex wrote a document"

0 commit comments

Comments
 (0)