Repository navigation
PyPI hosted scan lacks a specific missing-requirements warning after redirect_unconfirmed reporting #638
Description
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
Take a pip project whose venv holds a package with a hosted patch but which has no root requirements.txt. Its pins live in requirements-dev.txt or requirements/base.txt, which are common layouts, or it has no requirements file at all. scan --mode hosted --json then exits 0 with status: "success" and redirect: {redirected: 0, skipped: [], warnings: []}. Nothing in the JSON envelope says the patch was not applied.
Human output does say it: No patches could be switched to hosted: pkg:pypi/six@1.16.0: no lockfile entry pinning it could be rewritten. That line comes from the unconfirmed list in scan/hosted.rs, which only goes to stderr.
Every other ecosystem emits a stable JSON warning for the same case: redirect_npm_no_lockfile, redirect_pnpm_no_lockfile, redirect_vlt_no_lockfile, redirect_composer_no_lockfile, redirect_gem_no_gemfile, redirect_maven_no_pom and redirect_golang_no_go_mod. PyPI has no such code.
Impact
A CI job or wrapper that reads --json can't tell this case from a real success unless it compares packagesWithPatches with redirected itself. vex correctly doesn't attest the package, but the scan gives no machine-readable reason why. Meanwhile pip keeps installing the unpatched release from requirements-dev.txt.
Repro
This uses a local mock of patches/batch, by-package, view and package plus the hosted wheel. It's the same shape as mode_migration_pypi.rs::mount_hosted_api.
python3 -m venv /tmp/v && /tmp/v/bin/pip install six==1.16.0
mkdir proj && cd proj
printf 'six==1.16.0\nidna==3.7\n' > requirements-dev.txt # or requirements/base.txt, or no file at all
VIRTUAL_ENV=/tmp/v socket-patch scan --mode hosted --yes \
--api-url $MOCK --org test-org --api-token fake --patch-server-url $MOCK --jsonActual output (main 045d7ec):
"status": "success", "packagesWithPatches": 1,
"redirect": {"mode": "hosted", "redirected": 0, "rewrittenFiles": [], "skipped": [], "warnings": [], "dryRun": false}The exit code is 0. The same run without --json prints the "no lockfile entry pinning it could be rewritten" line on stderr.
Expected vs actual
- Expected: CLI_CONTRACT.md (hosted scan) says rewriter warnings carry stable
redirect_*codes, and that a missing manifest or lock is reported once per run. Examples areredirect_npm_no_lockfileand "redirect_composer_no_lockfile/redirect_gem_no_gemfile(composer / gem: neither manifest nor lock present — once per run …)". The comment aboveunconfirmedinscan/hosted.rssays such a package is "listed so it never vanishes silently". So a PyPI package that is granted but unpinned should appear inredirect.warnings[](for exampleredirect_pypi_no_lockfile, naming the files that were looked for) or inredirect.skipped[]. - Actual: JSON consumers see a plain success.
A related case that already works: when a root requirements.txt exists but doesn't pin the package, the JSON correctly carries redirect_requirements_entry_not_found. Only the case with no Python manifest at all is silent.
Matrix
| OS | pip / Python | Layout | Reproduces |
|---|---|---|---|
| Linux | 26.2.1 / CPython 3.13 | requirements-dev.txt only |
yes (2/2) |
| Linux | 26.2.1 / CPython 3.13 | requirements/base.txt only |
yes |
| Linux | 26.2.1 / CPython 3.13 | no requirements file | yes |
| Linux | 20.3.4 / CPython 3.8 | requirements-dev.txt only |
yes |
| Linux | 26.2.1 / CPython 3.13 | root requirements.txt without the pin |
no (redirect_requirements_entry_not_found is emitted) |
macOS and Windows weren't probed. The JSON assembly doesn't depend on the OS.
First bad version: none found. The published v4.0.0 (PyPI socket-patch==4.0.0) behaves the same way, so this isn't a regression.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:746: the requirements rewriter only runsif files.contains_key("requirements.txt"), and nothing PyPI-side warns when no Python manifest or lock is present. Compare the npm branch atmod.rs:819-857, which pushesredirect_npm_no_lockfile.crates/socket-patch-cli/src/commands/scan/hosted.rs:1416-1440:unconfirmedpurls are passed only toformat_unredirected(stderr), never to the JSONredirectobject.
Reading only the root requirements.txt is documented and isn't the bug. The missing machine-readable signal is.
Backlog review — 2026-10-08
Priority: P1 → P3. After #1029 JSON includes an unpinned/redirect_unconfirmed event. The remaining missing specific warning/exit behavior is a diagnostic follow-up.
The title now describes the remaining scope after the partial fixes. The original report is preserved above for historical context.
Activity
pip: run 10 ledger (#638)
mikolalysenko commentedon Oct 3, 2026
[agent] Triage: priority:p1 (pip). Confirmed on main
045d7ec: the requirements rewriter inpatch/redirect/mod.rs(~L746) only runs whenrequirements.txtis present, and there's no PyPI counterpart toredirect_npm_no_lockfile/redirect_composer_no_lockfile. I found no duplicate. Related issues #604, #567 and #612 are different requirements.txt bugs with separate causes.Generated by Claude Code
mikolalysenko commentedon Oct 4, 2026
[agent] Another shape of this, from the uv bug-hunt routine (ledger #310): a uv project whose
uv.lockisn't committed (a library that gitignores its lock). Herepyproject.tomldoes declare the patched package, so a fix keyed only on "no requirements file" might still miss it.Main
045d7ec, uv 0.8.17 and 0.12.23 on Linux, 2/2 runs each:--mode hosted --json: exit 0,status: "success",packagesWithPatches: 1,rollout.counts.new: 0,redirect: {redirected: 0, rewrittenFiles: [], skipped: [], warnings: []}. Human mode prints only the stderrno lockfile entry pinning it could be rewrittenline, as described above. The nextuv sync(which re-creates uv.lock) installs unpatched six.--mode vendoredon the same tree refuses explicitly, with a code a JSON consumer can act on: exit 1,pypi_pyproject_only("the project has a pyproject.toml but no lockfile or requirements.txt to wire; use agent mode instead"). So the vendored backend already detects this case. Hosted could reuse the same detection to emit aredirect_*warning (or askipped[]entry).Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Re-checked on main
b96a785(pip bug-hunt, ledger #309). This is partly mitigated by #1029, but not fixed.For a project with only
requirements-dev.txt(or onlyrequirements/base.txt) pinningsix==1.16.0, and a.venvholding it,scan --mode hosted --jsonnow emits a per-purl row:So a JSON consumer can now see that the patch was left unwired. What's still open:
statusissuccess, exit 0 (Stop CI gates passing on missing paths, unverified agent patches and unreported hosted pins #1029 left the hosted exit policy to Decide: one shape for the--jsontop-levelerror(scan and get emit both a string and a {code, message} object) #704).redirect.warningsandskippedare still empty. Nothing names the non-root requirements file the package is pinned in, unlike theredirect_requirements_entry_not_foundwarning a root-r dev.txtinclude gets.Reproduced twice (pip 26.2.1 / py3.11, mock patch API).
Generated by Claude Code
pip: run 31 ledger (#1119, #638 comment)
[-]Hosted `scan --json` on a PyPI project with no root requirements.txt (e.g. only `requirements-dev.txt` or `requirements/base.txt`) reports success with empty `skipped` and `warnings`, though the patched package is never pinned[/-][+]PyPI hosted scan lacks a specific missing-requirements warning after redirect_unconfirmed reporting[/+]